membrane

zimablue 2022-12-06T11:46:11.305599Z

sorry editing didn't mean to send

zimablue 2022-12-06T12:57:15.828759Z

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

phronmophobic 2022-12-06T17:57:33.727919Z

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.

zimablue 2022-12-06T17:59:41.264409Z

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

phronmophobic 2022-12-06T18:00:20.718249Z

there's way more than 3

zimablue 2022-12-06T18:00:45.377749Z

Haha there's always more

zimablue 2022-12-06T18:01:01.756749Z

Those 3 seem most important but I could well be wrong

phronmophobic 2022-12-06T18:01:41.634849Z

depends on the use case. the nice thing is that you can add more orthogonally to the rest.

zimablue 2022-12-06T18:03:46.999059Z

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

zimablue 2022-12-06T18:04:18.052169Z

But if you did, I think you'd want the minimal bounding rect not the rect to 0

phronmophobic 2022-12-06T18:05:09.354079Z

if you look at how the event handling works. it calls within-bounds on every mouse click as it walks the whole tree

zimablue 2022-12-06T18:05:12.396299Z

Or tbf better would be to check each contained components recy

zimablue 2022-12-06T18:05:28.392579Z

Rect*

zimablue 2022-12-06T18:06:31.597149Z

Re orthogonality that's what I've done, written a rough top-left protocol and extended it across all the basic types

👍 1
phronmophobic 2022-12-06T18:07:01.539279Z

I do want to revisit the model at some point

phronmophobic 2022-12-06T18:08:15.453409Z

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

zimablue 2022-12-06T18:09:52.980329Z

Is backwards compatibility important already?

phronmophobic 2022-12-06T18:11:05.190009Z

I don't want to break any existing code unless there's a good reason

phronmophobic 2022-12-06T18:12:04.502919Z

and usually there's not a good reason since there are methods for making improvements without breaking anything

phronmophobic 2022-12-06T18:12:36.129119Z

however, if there was a compelling reason, I would consider it

phronmophobic 2022-12-06T18:13:17.172889Z

membrane is still in beta

phronmophobic 2022-12-06T18:15:16.238289Z

I don't believe in "perpetual" beta. I am working towards a 1.0 where I can make stronger guarantees about backwards compatibility.

zimablue 2022-12-06T12:57:28.356209Z

is there any inbuilt way to handle loss of focus?

zimablue 2022-12-06T12:57:52.342299Z

usecase is finalizing texteditor modifications on enter or click-away

zimablue 2022-12-06T15:59:35.582839Z

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)))))

phronmophobic 2022-12-06T18:08:48.910869Z

there's always a temptation to trigger events during drawing, but it's almost never the right choice.

phronmophobic 2022-12-06T18:14:10.625739Z

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).

zimablue 2022-12-06T18:16:18.780429Z

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

zimablue 2022-12-06T18:16:27.206309Z

Hammering a dB*

zimablue 2022-12-06T18:16:41.470869Z

*debounce

zimablue 2022-12-06T18:17:25.097269Z

I agree doing it in draw is rough

phronmophobic 2022-12-06T18:17:50.752939Z

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

phronmophobic 2022-12-06T18:23:22.659049Z

> 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.

phronmophobic 2022-12-06T18:23:56.262289Z

what kind of effect is happening during text editing finalization?

zimablue 2022-12-06T18:26:52.744909Z

Want to write the text change back to the dB

phronmophobic 2022-12-06T18:28:34.864649Z

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

zimablue 2022-12-06T18:29:02.373489Z

Effect handler as in the global one?

phronmophobic 2022-12-06T18:29:28.195109Z

there is no global effect handler. I'm referring to the main effect handler for your app.

zimablue 2022-12-06T18:29:32.009079Z

Yes ok 1 sounds like the approach is had considered, 2 I hadn't considered but makes sense

zimablue 2022-12-06T18:29:45.608339Z

Sorry global is imprecise, but the one specified once per app as you say

phronmophobic 2022-12-06T18:29:57.306219Z

I would definitely go for add-watch for your use case.

phronmophobic 2022-12-06T18:30:12.334269Z

You probably want to add debouncing anyway

phronmophobic 2022-12-06T18:30:44.268449Z

you can do simple deduping with add-watch since you get both the old and new state

zimablue 2022-12-06T18:31:25.226639Z

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

zimablue 2022-12-06T18:31:28.722149Z

If that makes sense

phronmophobic 2022-12-06T18:31:32.678919Z

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.

zimablue 2022-12-06T18:31:52.285859Z

Like push an id in somewhere and something to connect the focus path back to that

phronmophobic 2022-12-06T18:34:23.391749Z

> 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.

phronmophobic 2022-12-06T18:35:15.377019Z

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).

phronmophobic 2022-12-06T18:38:13.616849Z

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))

zimablue 2022-12-06T18:38:16.739079Z

Good point about the wrapper, does make it simpler to modify the focus

phronmophobic 2022-12-06T18:38:40.406279Z

that would break if there's another component that sets* its focus value to something else

zimablue 2022-12-06T18:39:24.586359Z

I don't understand the default-get snippet, is that a cleaner way than me explicitly writing the path [membrane/contextual : focus]

zimablue 2022-12-06T18:39:38.321859Z

Sorry on phone I'll ask less stupid questions when I get back

phronmophobic 2022-12-06T18:40:21.861109Z

it's a good question. default-get will look up the value of a reference in the state atom.

phronmophobic 2022-12-06T18:41:02.541189Z

there's the builtin intents, :set, :update, and :get that work with references. component/default-get is the builtin handler for :get

phronmophobic 2022-12-06T18:41:50.106499Z

usually, you're doing something like [:set $my-ref new-val] . there's a corresponding intent, [:get $my-ref].

zimablue 2022-12-06T18:46:55.837509Z

I understand, my direct access method is in a way an implementation detail of the default effect handler right

zimablue 2022-12-06T18:47:08.382379Z

I didn't know about those methods thanks

phronmophobic 2022-12-06T18:49:05.795939Z

I think direct access is ok. There are some cases where having some indirection is useful, but it's sometimes easier to be direct.

phronmophobic 2022-12-06T18:50:11.955489Z

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.

phronmophobic 2022-12-06T18:51:26.484239Z

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.

zimablue 2022-12-06T19:03:37.881199Z

thanks so much for your help, I'll roll with this approach see how I go.

👍 1
phronmophobic 2022-12-06T19:12:15.029019Z

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.