Fork me on GitHub
#membrane
<
2022-09-17
>
Jakub Arnold02:09:16

I'm trying the membrane-skija-example but uhm, looks like it crashed the JVM, here's the full crash in case it's useful https://gist.githubusercontent.com/darthdeus/6aa51f9b42822bd31efec2ad9f769add/raw/8b7562a64180215ccb8c969ca4c2ee886fc88238/skija-crash.txt ... no really sure if it's a problem on my system or a bug

$ clojure -M -m com.phronemophobic.membrane-skija-example
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007f3aa02ab1ad, pid=172607, tid=172617
#

phronmophobic15:09:52

The example still works for me. Are running it on Windows? The deps file doesn't have native dependencies for Windows included, so it shouldn't work without those, but I'm a little surprised it crashed the JVM rather than throwing an exception. Did you add the Windows dependencies? If so, which ones did you use? Skija is in sort of a weird spot because the glfw integration that it works with seems to have issues ( see https://github.com/phronmophobic/membrane/issues/20). I've been putting off working on it and kinda hoping that either someone will do the work of writing a better GLFW wrapper or fix the issue

Ben Sless10:09:09

Trying to make a collapsible list of parent, children and checkbox. Then when I try to render a few of them, their states are connected 😄 How do I isolate the components' state?

(defui collapsible-list
  [{:keys [parent children open?]}]
  (vertical-layout
   (horizontal-layout
    (basic/checkbox {:checked? open?})
    (label parent))
   (when open?
     (ui/translate 10 0 (basic/dropdown-list {:options children})))))

(backend/run
  (component/make-app
   #'collapsible-list
   {:open? false
    :parent "Options"
    :children [[:this "This"]
              [:that "That "]
              [:the-other "The Other"]]}))

(defui channels [{:keys [channels]}]
  (apply
   vertical-layout
   (for [{:keys [parent children]} channels]
     (collapsible-list {:parent parent :children children}))))

(backend/run
  (component/make-app
   #'channels
   {:channels
    [{:open? false
      :parent "Number 1"
      :children [[:this "abc"]
                 [:that "xyz"]
                 [:the-other "uvw"]]}
     {:open? false
      :parent "Number 2"
      :children [[:this "123"]
                 [:that "456"]
                 [:the-other "789"]]}]}))

Ben Sless13:09:26

I ended up hacking dropdown list, but I would have wanted :open? to not be dynamically calculated but a key inside each element. What do you think of the implementation?

(defui collapsible-list*
  [{:keys [items selected open]}]
  (let [labels (for [m items
                     v (cons (:label m) (map :label (:children m)))]
                 (ui/label v))
        max-width (+ 5 (reduce max 0 (map ui/width labels)))
        padding-y 8
        padding-x 12

        rows
        (apply
         vertical-layout
         (for [{:keys [children value] :as item} items
               :let [open? (boolean (get open value))]
               {:keys [label value child?] :as item} (cons item (when open? (map #(assoc % :child? true) children)))]
           (let [hover? (get extra [:hover? value])
                 selected? (= selected value)
                 label (if selected?
                         (ui/with-color [1 1 1]
                           (ui/label label))
                         (ui/label label))

                 [_ h] (bounds label)
                 row-height (+ h 4)
                 row-width (+ max-width (* 2 padding-x))
                 elem (basic/on-hover
                       {:hover? hover?
                        :body
                        (on
                         :mouse-down
                         (fn [_]
                           [[::basic/select $selected value]])

                         [(spacer row-width row-height)
                          (cond

                            selected?
                            (ui/filled-rectangle [0 0.48 1]
                                                 row-width row-height)

                            hover?
                            (ui/filled-rectangle [0.976 0.976 0.976]
                                                 row-width row-height))
                          (translate padding-x 2
                                     label)])})]
             (if child?
               elem
               (horizontal-layout
                (on :mouse-down (fn [_] [[::toggm $open value]]) (ui/checkbox open?))
                elem)))))
        [rows-width rows-height] (bounds rows)]
    [(ui/with-style
       ::ui/style-stroke
       (ui/with-color [0.831
                       0.831
                       0.831]
         (ui/rounded-rectangle rows-width
                               (+ rows-height (* 2 padding-y))
                               4)))
     (ui/with-style
       ::ui/style-fill
       (ui/with-color [1 1 1]
         (ui/rounded-rectangle rows-width
                               (+ rows-height (* 2 padding-y))
                               4)))
     (translate 0 (- padding-y 2)
                rows)]))

(defeffect ::toggm [$open value]
  (dispatch! :update $open #(update % value not)))

(backend/run
  (component/make-app
   #'collapsible-list*
   {:items [{:label "label1"
             :value :value1
             :children [{:value :value1.1
                         :label "label1.1"}
                        {:value :value1.2
                         :label "label1.2"}]}
            {:label "label2"
             :value :value2
             :children [{:value :value2.1
                         :label "label2.1"}
                        {:value :value2.2
                         :label "label2.2"}
                        {:value :value2.3
                         :label "label2.3"}]}]}))

phronmophobic15:09:46

For the first example, the problem is that the defui macro doesn't understand destructuring 😞, but I hope to improve defui so that it does understand destructuring next week. The current workaround is to unpack the data without destructuring:

(defui channels [{:keys [channels]}]
  (apply
   vertical-layout
   (for [channel channels]
     (collapsible-list {:parent (:parent channel)
                        :children (:children channel)}))))

Ben Sless15:09:18

I made some more changes to the second example, will post updated code soon

Ben Sless15:09:54

Will restructuring make the state local to each component?

phronmophobic15:09:25

it does in the first example

Ben Sless15:09:56

Huh Why? :thinking_face:

phronmophobic15:09:20

it's because the defui doesn't understand destructuring, but I hope to fix that next week

Ben Sless15:09:46

Ah, so it can't navigate into a path?

👍 1
phronmophobic15:09:29

yea, so your code is right, but defui should be improved so that it works as expected

phronmophobic15:09:56

defui also doesn't understand :let in for comprehensions

Ben Sless15:09:52

Macros, a love story

😆 1
Ben Sless15:09:14

But I really want to get to a computer to show you the updated version, I made some cool changes

metal 1
Ben Sless15:09:16

(defui collapsible-list*
  [{:keys [items selected open]}]
  (let [labels (for [m items
                     v (cons (:label m) (map :label (:children m)))]
                 (ui/label v))
        max-width (+ 15 (reduce max 0 (map ui/width labels)))
        padding-y 8
        padding-x 12

        rows
        (apply
         vertical-layout
         (for [{:keys [children value] :as item} items
               :let [semi-open? (boolean
                                 (or (get open value)
                                     (= selected value)))
                     open? (boolean
                            (or semi-open?
                                (some #(= selected (:value %)) children)))]
               {:keys [label value child?]} (cons item (when open? (map #(assoc % :child? true) children)))]
           (let [hover? (get extra [:hover? value])
                 selected? (= selected value)
                 label (if selected?
                         (ui/with-color [1 1 1]
                           (ui/label label))
                         (ui/label label))

                 [_ h] (bounds label)
                 row-height (+ h 4)
                 row-width (+ max-width (* 2 padding-x))
                 elem (basic/on-hover
                       {:hover? hover?
                        :body
                        (on
                         :mouse-down
                         (fn [_]
                           [[::basic/select $selected value]])

                         [(spacer row-width row-height)
                          (cond

                            selected?
                            (ui/filled-rectangle [0 0.48 1]
                                                 row-width row-height)

                            hover?
                            (ui/filled-rectangle [0.976 0.976 0.976]
                                                 row-width row-height))
                          (translate padding-x 2
                                     label)])})]
             (if child?
               (ui/translate 15 0 elem)
               (horizontal-layout
                (on :mouse-down (fn [_] [[::toggm $open value]]) (ui/checkbox semi-open?))
                elem)))))
        [rows-width rows-height] (bounds rows)]
    [(ui/with-style
       ::ui/style-stroke
       (ui/with-color [0.831
                       0.831
                       0.831]
         (ui/rounded-rectangle rows-width
                               (+ rows-height (* 2 padding-y))
                               4)))
     (ui/with-style
       ::ui/style-fill
       (ui/with-color [1 1 1]
         (ui/rounded-rectangle rows-width
                               (+ rows-height (* 2 padding-y))
                               4)))
     (translate 0 (- padding-y 2)
                rows)]))

(defeffect ::toggm [$open value]
  (dispatch! :update $open #(update % value not)))

(comment
  (rui
   #'collapsible-list*
   {:items [{:label "label1"
             :value :value1
             :children [{:value :value1.1
                         :label "label1.1"}
                        {:value :value1.2
                         :label "label1.2"}]}
            {:label "label2"
             :value :value2
             :children [{:value :value2.1
                         :label "label2.1"}
                        {:value :value2.2
                         :label "label2.2"}
                        {:value :value2.3
                         :label "label2.3"}]}]}))

👀 1
Ben Sless15:09:29

Biggest is semi-open

phronmophobic15:09:36

looks great! not sure if you had a question to go along with it or if you're just showing off

phronmophobic15:09:49

I'm a little distracted at the moment, but I'll probably read through the code more carefully when I get a chance

Ben Sless15:09:27

For the first thing I had a question which you answered 🙂

Ben Sless15:09:23

for the second, I would like to not drag around open which is where I maintain the map of all uncollapsed lists and have it as a key inside each root element

Ben Sless15:09:52

as a followup to the second, both as a review, asking if it makes sense, and contributing it if you want to add it as a basic component to membrane

Ben Sless15:09:24

will need something that looks like an arrow and not a checkbox 🙂

phronmophobic16:09:44

do you want the collapsible-list to be recursive?

phronmophobic16:09:14

so that you can open and close children as well?

phronmophobic16:09:34

as well as expand to either arbitrary of fixed depth?

Ben Sless16:09:47

Fortunately, not for this use case, but as a general component it would be better

phronmophobic16:09:34

I definitely have a similar element lying around somewhere

Ben Sless16:09:53

Probably in treemap

phronmophobic16:09:00

I just have to remember where I put it

Ben Sless16:09:00

Or the inspector

phronmophobic15:09:38

I'm finally home from vacation. Two more questions about collapsible-list: 1. I noticed you can't unselect 2. To me, it's unexpected that a subtree would close without explicitly collapsing the subtree Both of these could be intended behavior. I guess it just depends on the use case.

Ben Sless15:09:43

1 is intentional, think like slack, always having some view selected 2 was an experiment which I'll probably roll back

phronmophobic15:09:20

I don't think there's a "correct" answer here. Just wondering

phronmophobic15:09:27

playing with it a bit now

Ben Sless15:09:57

I also have a snippet where I replaced the checkbox with an arrow

phronmophobic15:09:14

Oh, I saw the arrow in #humbleui

Ben Sless15:09:07

Lol, wrong place :D

Ben Sless15:09:26

Dropdown arrow:

(defn draw-arrow [checked?]
  (if checked?
    (ui/with-style ::ui/style-stroke
      (ui/path [8 12] [4 5] [12 5] [8 12]))
    (ui/with-style ::ui/style-stroke
      (ui/path [12 8] [5 12] [5 4] [12 8]))))

(defrecord Arrow [checked?]
    ui/IOrigin
    (-origin [_]
        [0 0])

    ui/IBounds
    (-bounds [this]
        (bounds (draw-arrow checked?)))

    ui/IChildren
    (-children [this]
        [(draw-arrow checked?)]))

(swap! ui/default-draw-impls
       assoc Arrow
       (fn [draw]
         (fn [this]
           (draw (draw-arrow (:checked? this))))))

(defn arrow
  "Graphical elem that will draw a checkbox."
  [checked?]
  (Arrow. checked?))

phronmophobic15:09:56

I'm not sure if there's a benefit to creating a defrecord over just having a function:

(defn arrow [checked?]
  (if checked?
    (ui/with-style ::ui/style-stroke
      (ui/path [8 12] [4 5] [12 5] [8 12]))
    (ui/with-style ::ui/style-stroke
      (ui/path [12 8] [5 12] [5 4] [12 8]))))

phronmophobic15:09:01

one difference is that you can set the origin

Ben Sless15:09:13

I just copied check box

Ben Sless15:09:41

How would I know the right way to go about these things 🙃

phronmophobic15:09:16

in the future, there might be other protocols that someone could implement that would make a difference

phronmophobic15:09:04

the design series I wrote was partially to try and explain which ideas were intentional and which ideas might just be "seemed good at the time"

Ben Sless16:09:15

Wrt origin, I do feel like the arrow needs centering

phronmophobic16:09:02

as far as I can tell, building user interfaces like this is very uncommon. the closest analog I can find is elmlang, but they've mostly abandoned building elements out of shapes, text, and images. they now build UIs out of divs and spans which I do not consider data/values.

phronmophobic16:09:05

I would be ecstatic to find some other reference point, but even haskell UI libraries don't seem to have data at the bottom.

Ben Sless16:09:54

On another note, have you come across any performance issues yet? Anything you'd want to take a look at?

phronmophobic16:09:46

User interfaces is an area where speed and efficiency is always a feature so there's always room for improvement. I was looking back at the last time I thought about this: https://clojurians.slack.com/archives/CVB8K7V50/p1657819065021729?thread_ts=1657740002.071019&amp;cid=CVB8K7V50 • the cache suggestion you made previously would probably be a big help • the default event handling uses a bunch of lazy sequences. I tried replacing it with transducers at some point, but my initial implementation was slower than the current one. I'm sure there's a better, faster way to implement event handling that also generates less garbage. • the drawing code is fairly unoptimized. Usually, some well placed ui/->Cached calls fix the issue but 1) ui/->Cached isn't implemented for all the backends and 2) it's always nice if users don't have to think about that. • There's a bunch of interesting ideas in the https://www.chromium.org/developers/ that could be applied • I often don't look at performance as much as I should because I don't have good benchmarks. In theory, it should be straightforward to benchmark the different phases (eg. event handling, layout, drawing). It would also be nice to have some tools to make it easy to bench mark UIs. There's a couple issues that seem to crop up with larger UIs: • usually, the point where the UI starts to slow down is when the app starts spending a bunch of time on GC • as suggested, improving caches and extraneous garbage would help, fixing memory leaks, etc. • I tend to hate scrollviews (I prefer paging), but it's basically the first thing people try (web UIs using infinite scrolling for everything). Although I haven't tested it, scissor views should prevent a lot of unnecessary work when drawing, but a lot of redundant layout work could probably be avoided. It doesn't seem to matter for smaller UIs, but obviously, it would make a big difference if you try and stuff thousands of elements into a scrollview. There are plenty of known techniques for optimizing this (eg. what Apple's https://developer.apple.com/documentation/uikit/uitableview?language=objc do) • There's been a few instances where you end up with hundreds or thousands of elements in a view tree which can cause events to be slow. The simple workaround is to wrap that part of the tree with ui/no-events or use an https://github.com/phronmophobic/treemap-clj/blob/dc5f0bb0ece54945273741a929ae105b8ef44505/src/treemap_clj/view.cljc#L661. I'm sure there's other alternatives from the implementation and design perspective.

phronmophobic17:09:12

ok, here's my first pass at the collapsible list. Obviously, yours looks better. I'll work on adding back in the styling.

Ben Sless18:09:53

> Mine looks better > Never expected to hear this about any piece of UI code I wrote 😄 Anyway, switch out the check box for the arrow, it looks much better that way and lets you propagate styling

😄 1
Ben Sless18:09:25

I think to be most generic, the collapsible list children should be a sequence of UI elements, each level should know nothing about its children besides their max width

phronmophobic18:09:17

you can calculate the max width of an element's children without knowing anything about them

phronmophobic18:09:33

there's probably a way to do it where you have collapsible list just receive the same arguments as tree-seq: branch? children and then maybe a viewf

phronmophobic18:09:03

although I'm always a little wary of UI elements that are max generic

Ben Sless18:09:03

you don't need to, it needs to be defined only for one level

👍 1
Ben Sless18:09:11

then you just nest collapsible lists

🧠 1
Ben Sless18:09:49

macroexpand = FP(macroexpand-1)

Ben Sless18:09:00

apply liberally to all your problems 😄

😄 1
phronmophobic18:09:40

I do really like the idea of having a visual analog to clojure.walk/prewalk and postwalk

Ben Sless18:09:52

So, this actually simplifies the exercise significantly

Ben Sless18:09:18

and you can trivially implement it on top of IChildren

Ben Sless18:09:32

(for walking UI stuff)

Ben Sless18:09:48

I'd actually not want to use postwalk because it's very slow

phronmophobic18:09:27

I just meant that there's sometimes nice visual analogs to different data structures (eg boolean <-> checkbox).

phronmophobic18:09:10

although, I'm not sure it would be good to build on top of IChildren. IChildren is already problematic (eg. https://github.com/phronmophobic/membrane/discussions/43)

Ben Sless18:09:27

> I just meant that there's sometimes nice visual analogs to different data structures (eg boolean <-> checkbox). Sort of like Plumatic's Schema's syntax?

Ben Sless18:09:04

Although I think the analogy breaks pretty fast and you have to start tagging stuff and end up with cljfx

phronmophobic18:09:53

Yea, I've tried building a tool that turns a spec into a form, but it's kind of fiddly.

phronmophobic18:09:59

I still think that type of tool is a good idea, but I haven't figured out the right balance of "just do the right thing" vs "get me 80% of the way there and let me tweak"

Ben Sless18:09:24

Really? I'd expect it to translate well

phronmophobic19:09:52

hmmmm, maybe it's not too bad. I think part of the problem is that I was trying to get it to work in the browser

Ben Sless19:09:04

okay, next question. Text input box. How do I capture return to send and shift+return to add newline?

phronmophobic19:09:19

Here's a snippet I've used for shift-down? :

(ui/wrap-on
 :key-event
 (fn [handler key scancode actions mods]
   (let [intents (handler key scancode actions mods)
         shift-down? (get context :shift-down?)
         shift? (= 1 (bit-and mods 1))]
     (if (not= shift? shift-down?)
       (conj intents [:set $shift-down? shift?])
       intents)))
 body)

phronmophobic19:09:42

Which you can use by replacing body with your main component

phronmophobic19:09:21

which will set the :shift-down? key in the app's context which gets passed implicitly to every component

phronmophobic19:09:55

shift? (= 1 (bit-and mods 1)) according to my https://github.com/phronmophobic/membrane/blob/master/docs/tutorial.md#mouse-event, that should work, but I'm not actually sure that it will work outside of the skia backend. Let me file an issue to follow up on that

Ben Sless19:09:57

then I'll need to wrap my own keystroke handler, check if shift is down, if so check if it's RET and do one thing, else call the default handler?

👍 1
phronmophobic19:09:27

that's what membrane.ui/wrap-on is meant to help with. maybe there's an opportunity to provide some builtin helpers that cover some basic usages

Ben Sless19:09:49

Why can't I just intercept textarea's key-press handler?

phronmophobic19:09:26

that's what membrane.ui/wrap-on does

Ben Sless19:09:47

I see, I see

phronmophobic19:09:43

if you don't care about the default behavior, you can completely override it by just using (ui/on :keypress (fn [_] (comment :do-nothing)))

Ben Sless19:09:05

though it would be cool to add means to capture modified key events, e.g. S-RET, C-c, etc

phronmophobic19:09:50

Yea, I've implemented that in an adhoc way for multiple projects. That would be great to "wrap" up 😛

Ben Sless19:09:20

I'm just enjoying the thought of how cool it would be to have key press handlers with Emacs's syntax. Then just on :C-M-b stuff

Ben Sless19:09:45

Then have handlers like on-key and on-key-chord :thinking_face:

🆒 1
phronmophobic19:09:54

Yea, it shouldn't be too much work to offer it as sort of a middleware. There's a similar existing issue, https://github.com/phronmophobic/membrane/issues/38

phronmophobic19:09:26

Same for other composite events like drag&drop

Ben Sless09:09:26

how can I fix a textarea's size?

Ben Sless09:09:27

Other problem actually, stretch and pack

phronmophobic15:09:02

what do you mean by stretch and pack?

Ben Sless16:09:23

Let's say I want a certain box area to contain some elements. I want to stretch that box to fit the window, pack one element to the top right, other top left. On resize I want the bounding box to resize, with a lower bound of the width of contained elements

phronmophobic16:09:07

Is your use case a top level thing or do you need a version that also nests a few levels deep?

Ben Sless16:09:54

No, just thinking of the scroll area, text area, stuff like that. Just think about slack's UI (or discord's). There's always a minimum height text box of variable width, send button pinned to the right

phronmophobic16:09:29

I guess the short answer is that there's not a great story for how to do that, but it's something I'd like to improve.

Ben Sless16:09:28

Stretch goals

phronmophobic16:09:56

Stuff like pinning to the right is pretty easy to do with simple functions similar to membrane.ui/center

Ben Sless16:09:50

Another hard one is text wrapping

Ben Sless17:09:08

And dynamically wrapping text

phronmophobic17:09:31

Handling text is generally a pain

phronmophobic17:09:57

I think there's some paragraph shaping stuff in skia

Ben Sless17:09:45

Another thing I've been meaning to ask, how do I perform effects asynchronously?

phronmophobic17:09:08

I usually just wrap the body in a future or similar. For example:

(defeffect ::add-todo [$todos next-todo-text]
  (future
    (dispatch! :update $todos #(conj % {:description next-todo-text
                                        :complete? false}))))

phronmophobic18:09:24

The effect handling is intentionally minimal. I don't think membrane should have that much to say about how you handle side effects. The application should be able to decide to handle side effects in whatever makes sense for that use case.

Ben Sless18:09:48

I didn't imagine it would work 😄

😄 1
phronmophobic21:09:53

Just implemented https://github.com/phronmophobic/membrane/issues/54. Still need to do some more testing, but I'm trying it out with the original versions of your collapsible list that use non literal maps and destructuring. It seems like everything "just works"! 🎉

🎉 1