Fork me on GitHub
#membrane
<
2022-12-06
>
zimablue11:12:11

sorry editing didn't mean to send

zimablue12:12:15

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

phronmophobic17:12:33

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.

zimablue17:12:41

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

phronmophobic18:12:20

there's way more than 3

zimablue18:12:45

Haha there's always more

zimablue18:12:01

Those 3 seem most important but I could well be wrong

phronmophobic18:12:41

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

zimablue18:12:46

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

zimablue18:12:18

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

phronmophobic18:12:09

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

zimablue18:12:12

Or tbf better would be to check each contained components recy

zimablue18:12:31

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

👍 1
phronmophobic18:12:01

I do want to revisit the model at some point

phronmophobic18:12:15

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

zimablue18:12:52

Is backwards compatibility important already?

phronmophobic18:12:05

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

phronmophobic18:12:04

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

phronmophobic18:12:36

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

phronmophobic18:12:17

membrane is still in beta

phronmophobic18:12:16

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

zimablue12:12:28

is there any inbuilt way to handle loss of focus?

zimablue12:12:52

usecase is finalizing texteditor modifications on enter or click-away

zimablue15:12:35

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

phronmophobic18:12:48

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

phronmophobic18:12:10

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

zimablue18:12:18

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

zimablue18:12:27

Hammering a dB*

zimablue18:12:25

I agree doing it in draw is rough

phronmophobic18:12:50

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

phronmophobic18:12:22

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

phronmophobic18:12:56

what kind of effect is happening during text editing finalization?

zimablue18:12:52

Want to write the text change back to the dB

phronmophobic18:12:34

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

zimablue18:12:02

Effect handler as in the global one?

phronmophobic18:12:28

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

zimablue18:12:32

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

zimablue18:12:45

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

phronmophobic18:12:57

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

phronmophobic18:12:12

You probably want to add debouncing anyway

phronmophobic18:12:44

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

zimablue18:12:25

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

zimablue18:12:28

If that makes sense

phronmophobic18:12:32

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.

zimablue18:12:52

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

phronmophobic18:12:23

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

phronmophobic18:12:15

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

phronmophobic18:12:13

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

zimablue18:12:16

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

phronmophobic18:12:40

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

zimablue18:12:24

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

zimablue18:12:38

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

phronmophobic18:12:21

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

phronmophobic18:12:02

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

phronmophobic18:12:50

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

zimablue18:12:55

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

zimablue18:12:08

I didn't know about those methods thanks

phronmophobic18:12:05

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

phronmophobic18:12:11

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.

phronmophobic18:12:26

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.

zimablue19:12:37

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

👍 1
phronmophobic19:12:15

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.