This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-09-17
Channels
- # announcements (1)
- # aws (7)
- # babashka (5)
- # calva (56)
- # cider (13)
- # clj-commons (1)
- # clj-kondo (12)
- # clj-yaml (35)
- # clojure (84)
- # clojure-europe (93)
- # clojure-sg (2)
- # clojure-uk (1)
- # clojurescript (10)
- # conjure (37)
- # core-typed (1)
- # cursive (31)
- # duct (1)
- # figwheel-main (4)
- # fulcro (2)
- # holy-lambda (2)
- # humbleui (3)
- # membrane (118)
- # off-topic (46)
- # pathom (8)
- # podcasts-discuss (5)
- # releases (2)
- # rewrite-clj (13)
- # sci (27)
- # shadow-cljs (17)
- # tools-deps (12)
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
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)}))))
it does in the first example
it's because the defui
doesn't understand destructuring, but I hope to fix that next week
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
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"}]}]}))
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 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
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?
I definitely have a similar element lying around somewhere
I just have to remember where I put it
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
Oh, I saw the arrow in #humbleui
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?))
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
oh, haha
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"
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
I do really like the idea of having a visual analog to clojure.walk/prewalk and postwalk
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)
> 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"
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
that's what membrane.ui/wrap-on
does
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
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
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.
Stuff like pinning to the right is pretty easy to do with simple functions similar to membrane.ui/center
Handling text is generally a pain
I think there's some paragraph shaping stuff in skia
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.
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"! 🎉