This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-12-06
Channels
- # adventofcode (71)
- # aleph (1)
- # announcements (6)
- # aws (1)
- # babashka (27)
- # beginners (60)
- # biff (7)
- # calva (3)
- # clj-kondo (3)
- # clj-yaml (1)
- # clojure (60)
- # clojure-europe (43)
- # clojure-nl (3)
- # clojure-norway (75)
- # clojurescript (16)
- # code-reviews (7)
- # css (4)
- # cursive (47)
- # datascript (4)
- # events (5)
- # fulcro (37)
- # gratitude (5)
- # hyperfiddle (4)
- # introduce-yourself (4)
- # joyride (23)
- # juxt (4)
- # malli (4)
- # membrane (64)
- # nbb (8)
- # off-topic (12)
- # other-languages (6)
- # pathom (6)
- # polylith (9)
- # random (3)
- # rdf (66)
- # reitit (3)
- # releases (2)
- # shadow-cljs (18)
- # tree-sitter (10)
the short version is: I think that the origin for a vector should be like (reduce vec-max (map origin elements)) not [0 0] as it currently is, because you need this "real" top-left more often I think, and it means that (origin x) = (origin [x]) which feels right. I know we've had this convo before
there's a couple problems:
• origin
is not the same as top-left
. I know they're mostly conflated in the current model, but they're different concepts.
• the biggest issue is that using the top-left
of all the enclosed elements as the origin
is problematic for generic tree walking with origin
and children
.
I agree that they're not the same thing, my longer message wrote that and the earlier rant, there's 3 concepts imo: where do I start drawing my children, what's my collision top-left, what's the centre of my coordinator system relative to my parent
there's way more than 3
depends on the use case. the nice thing is that you can add more orthogonally to the rest.
Within-bounds uses origin as top-left I think, although it's not a common problem as it would be unusual to add a handler to a vector
if you look at how the event handling works. it calls within-bounds
on every mouse click as it walks the whole tree
Re orthogonality that's what I've done, written a rough top-left protocol and extended it across all the basic types
I do want to revisit the model at some point
I'm hoping that I can add a ui2.clj and backport all the protocols to ui.clj. Users can upgrade their code as they see fit, no breaking changes, and everything happily coexists
I don't want to break any existing code unless there's a good reason
and usually there's not a good reason since there are methods for making improvements without breaking anything
however, if there was a compelling reason, I would consider it
membrane is still in beta
I don't believe in "perpetual" beta. I am working towards a 1.0 where I can make stronger guarantees about backwards compatibility.
one way to do it (check/set something on every render, in this case :focus) I realized was to go through the state atom in the backend and find the path [... :membrane.contextual :focus], add a watcher for when that moves off a textarea, I feel like the expectation is to do this with effects but it seems unnatural as it needs to be checked on every render, when an event ISNT happening to this component.
I considered another workaround which would be to do something like create an AlwaysCallEventHandler like EventHandler, which changes this fragment:
(swap! default-draw-impls
assoc EventHandler
(fn [draw]
(fn [this]
(draw (:drawable this)))))
to something like
(swap! default-draw-impls
assoc EventHandler
(fn [draw]
(fn [this]
(-bubble this [[:set :thing :value]]
(draw (:drawable this)))))
there's always a temptation to trigger events during drawing, but it's almost never the right choice.
It seems like you want to finalize text editing regardless of whether a frame gets drawn or even if there's no display at all (ie. testing, debugging, tooling).
The condition is on loss of focus, otherwise you're handing a dB for no reason, the only other thing I can think would be to denounce it but I don't like that
yea, I definitely think taking action on loss of focus is a valid use case. just working towards the explanation of how to handle it
> when an event ISNT happening to this component.
The general way to add a [:check-finalize]
effect on every change to a component is wrapping a component with on-bubble
, but that doesn't work for loss of focus since focus is a global, contextual property.
Conceptually, the other problem is that events don't "happen" to components. There's the app state and effects that modify the app-state.
what kind of effect is happening during text editing finalization?
the reason I ask is that there are multiple ways to go about it:
• use add-watch
. this is great for non-urgent cleanup. you can put all the app-states in a queue and use something like requestIdleCallback` to clean up when there's some spare cycles.
• you can augment your effect handler's
:set
and :update
intents to check if focus has changed and makes changes in a consistent way
there is no global effect handler. I'm referring to the main effect handler for your app.
Yes ok 1 sounds like the approach is had considered, 2 I hadn't considered but makes sense
I would definitely go for add-watch
for your use case.
You probably want to add debouncing anyway
you can do simple deduping with add-watch
since you get both the old
and new
state
The problem with add-watch is that focus gets set to a gnarly route but I can add something to help me connect those routes back to aj easier description of which component is which
if you're using core.async
, then you can have a queue that gets the state, checks for changes, adds debouncing, maybe add a frequency limiter, etc.
> The problem with add-watch is that focus gets set to a gnarly route but I can add something to help me connect those routes back to aj easier description of which component is which
yea, that's a fair point
if you're using textarea
, there's a lower level component that can help:
(defui textarea
"Textarea component."
[{:keys [text
border?
font
^:membrane.component/contextual focus
textarea-state]
:or {border? true}}]
(on
::request-focus
(fn []
[[:set $focus $text]])
(textarea-view {:text text
:cursor (get textarea-state :cursor 0)
:focus? (= focus $text)
:font font
:down-pos (:down-pos textarea-state)
:mpos (:mpos textarea-state)
:border? (or border?
(nil? border?))
:select-cursor (:select-cursor textarea-state)}))
)
textarea
is just a small wrapper around textarea-view
. Instead of using the default focus value, you can have your own wrapper that uses whatever you want.however, you probably want the focus
value to be related to the identity of the text
property. you can either have your wrapper accept an explicit id (if you have one).
since textarea
uses $text
as its focus value, you can also just grab the value. something like
(component/default-get state-atm (-> @state-atm :membrane.contextual :focus))
that would break if there's another component that sets* its focus value to something else
I don't understand the default-get snippet, is that a cleaner way than me explicitly writing the path [membrane/contextual : focus]
it's a good question. default-get
will look up the value of a reference in the state atom.
there's the builtin intents, :set
, :update
, and :get
that work with references. component/default-get
is the builtin handler for :get
usually, you're doing something like [:set $my-ref new-val]
. there's a corresponding intent, [:get $my-ref]
.
I understand, my direct access method is in a way an implementation detail of the default effect handler right
I think direct access is ok. There are some cases where having some indirection is useful, but it's sometimes easier to be direct.
my main point was that the focus value has the reference to the text passed to the textarea and the :get
intent is how you can lookup the value of a $reference.
I haven't tested this code, but I would probably go with something like:
(defn is-textarea-focus? [x]
(and (vector? x)
(= :textarea (first x))))
(defui textarea
"Textarea component."
[{:keys [text
border?
font
^:membrane.component/contextual focus
textarea-state]
:or {border? true}}]
(on
::request-focus
(fn []
[[:set $focus [:textarea $text]]])
(basic/textarea-view {:text text
:cursor (get textarea-state :cursor 0)
:focus? (= focus $text)
:font font
:down-pos (:down-pos textarea-state)
:mpos (:mpos textarea-state)
:border? (or border?
(nil? border?))
:select-cursor (:select-cursor textarea-state)})))
(add-watch state-atm
::focus-finalize
(fn [k atm old new]
(let [old-focus (-> old ::component/contextual :focus)
new-focus (-> new ::component/contextual :focus)]
(when (not= old-focus new-focus)
(when (is-textarea-focus? old-focus)
(let [[_ $old-text] old-focus
old-text (component/default-get atm $old-text)]
(do-something-with-text old-text)))))))
It changes the focus value so that you can tell if the focused element is a textarea and so you can grab the reference to the text property.I would love to hear how it goes. I've tried a few of these techniques before, but I still want to collect more user stories so that I can add helpers and improve documentation.