Fork me on GitHub
#membrane
<
2022-10-10
>
Ben Sless14:10:05

I'm trying the rss example with the latest branch and it looks like I don't pack one of the items in a map it breaks scroll view (code in thread)

Ben Sless14:10:12

(defui feed-view [{items :item name :title}]
  (ui/vertical-layout
   (ui/label name)
   (basic/scrollview
    {:scroll-bounds [800 800]
     :body
     (apply
      ui/vertical-layout
      (for [{:keys [title description]} items]
        (ui/vertical-layout
         (ui/label title title-font)
         (ui/label description))))})))

(defui op-view [{:keys [feed-uri feeds current-feed]}]
  (ui/on
   ::display-feed (fn [f]
                    (println f)
                    [[:set $current-feed f]])
   (ui/horizontal-layout
    (ui/vertical-layout
     (ui/vertical-layout
      (ui/wrap-on
       :key-press
       (fn [default-handler s]
         (let [effects (default-handler s)]
           (if (and (seq effects)
                    (= s :enter))
             [[::add-feed $feeds feed-uri]
              [:set $feed-uri ""]]
             effects)))
       (basic/textarea {:text feed-uri}))
      (basic/button {:text "Add Feed"
                     :on-click (fn []
                                 [[::add-feed $feeds feed-uri]
                                  [:set $feed-uri ""]])}))
     (apply ui/vertical-layout
            (for [feed feeds]
              (feed-item feed))))
    (when-let [feed (->> feeds
                         (filter (fn [{:keys [feed]}] (= current-feed (:uri feed))))
                         first
                         :feed)]

      (feed-view feed)))))

Ben Sless14:10:27

The only difference is that feed-view isn't passed {:feed feed}

phronmophobic15:10:15

ok, will take a look

👍 1
phronmophobic17:10:36

Thanks for braving the new version! I found a couple issues. I guess that's why it's nice to have use cases in mind. • I doesn't matter that much in this example, but when-let isn't supported for tracking bindings. It would matter if feed-view wanted to manipulate feed. Adding support for when-let and if-let seems pretty easy. • When I was first thinking about supporting non-literal maps, the example I had in mind was something like a textarea. If you pass in a textarea with :text, you probably don't care too much if a bunch of extra keys like :cursor and :selection get added. However, I'm starting to think that your use case is actually much more common. You have a feed-view that displays a feed, and you probably would rather not the feed map get a bunch of extra keys dealing with hover states and scroll positions. Does that sound right? I'll think about it a bit more, but I'll try to have at least a snapshot version soon.

phronmophobic17:10:54

Also, defui feed-view [{items :item name :title}]. While destructuring is supported in the body of defui, it's still not supported generically in the args for defui. It works without issue if you don't need to update items or name, but I thought I'd just point that out. More fodder for https://phronmophobic.github.io/membrane/membrane-topics.html#Defui-Limitations.

Ben Sless17:10:20

you're right regarding the use cases. another example is collapsible list, where the expanded items are unrelated to the actual data in the list

👍 1
Ben Sless17:10:21

naive question - why add support for particular forms instead of macroexpanding first?

phronmophobic17:10:45

At some point, I think that's the right way to go. For now, the main reason is supporting either for or map is really important and

> (clojure.pprint/pprint
                          (macroexpand-1 '(for [x (range 3)]
                                            x)))
(clojure.core/let
 [iter__6373__auto__
  (clojure.core/fn
   iter__24408
   [s__24409]
   (clojure.core/lazy-seq
    (clojure.core/loop
     [s__24409 s__24409]
     (clojure.core/when-let
      [s__24409 (clojure.core/seq s__24409)]
      (if
       (clojure.core/chunked-seq? s__24409)
       (clojure.core/let
        [c__6371__auto__
         (clojure.core/chunk-first s__24409)
         size__6372__auto__
         (clojure.core/int (clojure.core/count c__6371__auto__))
         b__24411
         (clojure.core/chunk-buffer size__6372__auto__)]
        (if
         (clojure.core/loop
          [i__24410 (clojure.core/int 0)]
          (if
           (clojure.core/< i__24410 size__6372__auto__)
           (clojure.core/let
            [x (.nth c__6371__auto__ i__24410)]
            (do
             (clojure.core/chunk-append b__24411 x)
             (recur (clojure.core/unchecked-inc i__24410))))
           true))
         (clojure.core/chunk-cons
          (clojure.core/chunk b__24411)
          (iter__24408 (clojure.core/chunk-rest s__24409)))
         (clojure.core/chunk-cons (clojure.core/chunk b__24411) nil)))
       (clojure.core/let
        [x (clojure.core/first s__24409)]
        (clojure.core/cons
         x
         (iter__24408 (clojure.core/rest s__24409)))))))))]
 (iter__6373__auto__ (range 3)))

Ben Sless17:10:06

the really hard part about this is propagating bindings, right?

phronmophobic17:10:28

you have to figure out how x derives from (range 3)

phronmophobic18:10:23

if you have

(for [todo todos]
  (todo-item {:todo todo}))
and the todo-item view edits todo, then you need to figure out how to apply that edit within todos

phronmophobic18:10:12

maybe the smart thing to do is write the deep walking macro and just provide m/for to use instead of for

phronmophobic18:10:06

because for is already rewritten to do something slightly different within defui.

Ben Sless18:10:14

ahhhh, I get it. that's what map-indexed is for

👍 1
Ben Sless18:10:47

can you dynamically bind for inside defui to your own implementation? :thinking_face:

phronmophobic18:10:41

yea, there are probably several ways to support (dynamically bind, postwalk replace, then macroexpand)

Ben Sless18:10:53

Besides being bad at writing UIs, I have been messing around with code rewriting and analysis 🙃

phronmophobic18:10:54

I originally wrote defui as a weekend hack. Intuitively, the design feels not quite right, but using it feels pretty good. I still feel like there's some deeper idea that's more general and still covers all the same use cases.

Ben Sless18:10:27

Turn the code into a DAG, perform flow analysis?

🙂 1
Ben Sless18:10:12

Hyperfiddle guys did something similar. Code -> analysis -> EAV -> datascript, then analysis is just queries w/ rules

Ben Sless18:10:38

I also have pretty much working implementation of beta reduce (and alpha reduce falls out of it), I would imagine you could do any sort of interesting things with it.

Ben Sless18:10:32

There's also core.async's compiler which which you can borrow ideas from

phronmophobic18:10:53

There's a bunch "compiler"-esque that would be cool with membrane. Right now, when you do [[:update $bool not]], there's a bunch of extra work that happens. However, it seems possible to determine, at compile time, how to convert that into a simple bounds check on the mouse position and pre-compile the code that makes that change. You could also precompile drawing routines that more efficiently draw and repaint based on model changes (which you may be able to determine at compile time)

Ben Sless18:10:14

In essence, when calling a UI macro, you need to build up a stack of the path from the input argument to the function call?

👍 1
phronmophobic18:10:26

Basically, there's a bunch of work you should be able to do at compile time to build more efficient draw, event, and view functions.

Ben Sless18:10:36

I mean, there's a limited set of what can happen - indexed access, key access, iteration. Anything else to keep track of?

phronmophobic18:10:37

these are the operations that are currently supported:

(defui my-component [{:keys [my-prop]}]
  (let [;; these work
        foo (:foo my-prop)
        foo (:foo my-prop :my-default)
        foo (nth my-prop 0)
        foo (nth my-prop 0 :my-default)
        foo (get-in my-prop [:foo :bar])
        foo (get-in my-prop [:foo :bar] :my-default)
        foo (take 42 my-prop)
        foo (drop 42 my-prop)
        foo (or my-prop 42)
        
        ;; destructuring also works
        {foo :foo
         bar :bar
         [a b & other-foos] :foos} my-prop]
    (my-other-component {:foo foo
                         :bar bar
                         :a a
                         :b b
                         :other-foos other-foos})))

phronmophobic18:10:57

the or operation is important for defaults

phronmophobic18:10:36

drop is required for supported destructuring [a b & other-foos]. if you modify other-foos, then that uses drop.

phronmophobic18:10:46

any function that where operating on the transformed value can also be applied to the original value could be supported.

phronmophobic18:10:28

for example, (filter f xs) currently works, but I'm not sure if it should be officially supported

Ben Sless18:10:15

I would expect first/rest/next to work :thinking_face:

phronmophobic18:10:00

yea, those would be easy to implement

phronmophobic18:10:32

I'm not sure how often they come up

phronmophobic18:10:33

I guess the right answer is make it an open set and let people add whatever they want

phronmophobic18:10:15

In addition to just trying to provide implementations for relevant clojure.core functions

phronmophobic04:10:35

ok, I have a version that takes the extra state from the parent component's extra state. https://clojars.org/com.phronemophobic/membrane/versions/0.10.2-beta-SNAPSHOT

phronmophobic04:10:02

for the feed-view, I think you still want to explicitly pass the extra properties so that each feed get it's own scroll state.

(when-let [feed (->> feeds
                         (filter (fn [feed] (= current-feed (:uri feed))))
                         first)]
      (let [feed-extra (get extra [:feed-view (:uri feed)])
            feed (assoc feed
                        :extra feed-extra
                        :$extra $feed-extra)]
        (feed-view feed)))

Ben Sless04:10:39

Would you like me to PR the RSS reader as an example to the repository? I think it's proving to be a good, interesting and self contained case

👍 1
phronmophobic04:10:18

sounds good, I've started to put examples in https://github.com/phronmophobic/membrane/tree/master/examples rather than in the main source tree

Ben Sless04:10:31

Another thing I've been thinking about, related to collapsible list, is menus, specifically how to place them in relation to the rest of the window and elements. It needs to be dynamic, no? Is there a way to get the bounds of the parent components when placing a component?

phronmophobic04:10:06

oh, I thought of some ideas for that when I was writing some of the docs

phronmophobic04:10:46

One way to do it is have a contextual property, :container-size that is the [width height] of the current container. You can then have halign, valign, etc that position child components within the container and set the :container-size property in the contexts for all those child components

phronmophobic04:10:32

I did a little work on the prototype, but before some of the other improvements were made. I think the new improvements should help.

phronmophobic04:10:16

I was trying to implement some of the ideas from https://tonsky.me/blog/humble-layout/

phronmophobic04:10:10

It seemed to work, but one gripe I have with the prototype is that it feels a little verbose:

(defui center-align [{:keys [body]}]
  (halign {:parent-pct 0.5
           :child-pct 0.5
           :body 
           (valign
            {:parent-pct 0.5
             :child-pct 0.5
             :body body})}))

phronmophobic04:10:38

I would prefer syntax more like: (halign 0.5 0.5 child)

phronmophobic04:10:06

to support that syntax, either: • halign, valign, etc would have to be macros 😕halign, valign, etc would have to be specially supported by defui 😕 • I would have to come up with a plan to support either multiple arguments or positional arguments for defui • something else?

phronmophobic04:10:04

another example from the current prototype

(defui test-align [{:keys [state]}]
  [(ui/with-style ::ui/style-stroke
     (ui/rectangle 150 350)
     (ui/center (ui/path [0 0] [0 350])
                [150 350]))
   
   (row {:cols
         [(center-align {:body (ui/label "a")})
          (halign {:parent-pct 0.5
                   :child-pct 0.5
                   :body (ui/label "a")}
                  )
          (halign {:parent-pct 0.5
                   :child-pct 0.5
                   :body (ui/label "a")}
                  )
          (halign {:parent-pct 0.5
                   :child-pct 0.5
                   :body (ui/label "a")}
                  )]})]
  )

phronmophobic04:10:13

the other change I would need to make is the function passed to backend/run doesn't receive any arguments. I need to decide how to make a version that accepts a map. The map could contain some common keys like :container-size and some backend specific keys like the JFrame container

Ben Sless04:10:13

Why not make it positional?

phronmophobic04:10:20

It relies on defui threading the context so it would have to be a defui component.

phronmophobic04:10:18

obviously, you could pass the container size manually

Ben Sless05:10:46

Let me know if you need a trained space monkey to take a wrench to the control panels once it's ready for a test flight 😄

👏 1