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"]]}]}))
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"}]}]}))
looking now!
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)}))))I made some more changes to the second example, will post updated code soon
Will restructuring make the state local to each component?
it does in the first example
Huh Why? 🤔
it's because the defui doesn't understand destructuring, but I hope to fix that next week
Ah, so it can't navigate into a path?
yea, so your code is right, but defui should be improved so that it works as expected
defui also doesn't understand :let in for comprehensions
😳
Macros, a love story
But I really want to get to a computer to show you the updated version, I made some cool changes
(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"}]}]}))
Biggest is semi-open
looks great! not sure if you had a question to go along with it or if you're just showing off
I'm a little distracted at the moment, but I'll probably read through the code more carefully when I get a chance
For the first thing I had a question which you answered 🙂
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
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
will need something that looks like an arrow and not a checkbox 🙂
do you want the collapsible-list to be recursive?
so that you can open and close children as well?
as well as expand to either arbitrary of fixed depth?
Fortunately, not for this use case, but as a general component it would be better
I definitely have a similar element lying around somewhere
Probably in treemap
I just have to remember where I put it
Or the inspector
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"! 🎉
how can I fix a textarea's size?
Other problem actually, stretch and pack
what do you mean by stretch and pack?
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
Is your use case a top level thing or do you need a version that also nests a few levels deep?
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
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.
Stretch goals
🙃
Stuff like pinning to the right is pretty easy to do with simple functions similar to membrane.ui/center
Another hard one is text wrapping
And dynamically wrapping text
Handling text is generally a pain
I think there's some paragraph shaping stuff in skia
Another thing I've been meaning to ask, how do I perform effects asynchronously?
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}))))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.
I didn't imagine it would work 😄
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.
1 is intentional, think like slack, always having some view selected 2 was an experiment which I'll probably roll back
I don't think there's a "correct" answer here. Just wondering
playing with it a bit now
I also have a snippet where I replaced the checkbox with an arrow
Oh, I saw the arrow in #humbleui
Lol, wrong place :D
haha
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?))Right rope
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]))))one difference is that you can set the origin
I just copied check box
oh, haha
How would I know the right way to go about these things 🙃
me neither!
in the future, there might be other protocols that someone could implement that would make a difference
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"
Wrt origin, I do feel like the arrow needs centering
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.
I would be ecstatic to find some other reference point, but even haskell UI libraries don't seem to have data at the bottom.
On another note, have you come across any performance issues yet? Anything you'd want to take a look at?
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&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.
ok, here's my first pass at the collapsible list. Obviously, yours looks better. I'll work on adding back in the styling.
> 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
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
you can calculate the max width of an element's children without knowing anything about them
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
although I'm always a little wary of UI elements that are max generic
you don't need to, it needs to be defined only for one level
then you just nest collapsible lists
macroexpand = FP(macroexpand-1)
apply liberally to all your problems 😄
I do really like the idea of having a visual analog to clojure.walk/prewalk and postwalk
So, this actually simplifies the exercise significantly
and you can trivially implement it on top of IChildren
(for walking UI stuff)
I'd actually not want to use postwalk because it's very slow
I just meant that there's sometimes nice visual analogs to different data structures (eg boolean <-> checkbox).
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)
and https://github.com/phronmophobic/membrane/discussions/51
> 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?
Although I think the analogy breaks pretty fast and you have to start tagging stuff and end up with cljfx
Yea, I've tried building a tool that turns a spec into a form, but it's kind of fiddly.
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"
Really? I'd expect it to translate well
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
okay, next question. Text input box. How do I capture return to send and shift+return to add newline?
there's a slightly helpful example here, https://github.com/phronmophobic/membrane/blob/master/src/membrane/example/todo.cljc#L120
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)Which you can use by replacing body with your main component
which will set the :shift-down? key in the app's context which gets passed implicitly to every component
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
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?
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
Why can't I just intercept textarea's key-press handler?
that's what membrane.ui/wrap-on does
ah
I see, I see
if you don't care about the default behavior, you can completely override it by just using (ui/on :keypress (fn [_] (comment :do-nothing)))
though it would be cool to add means to capture modified key events, e.g. S-RET, C-c, etc
Yea, I've implemented that in an adhoc way for multiple projects. That would be great to "wrap" up 😛
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
Then have handlers like on-key and on-key-chord 🤔
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
Same for other composite events like drag&drop
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
#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