wip hover card
65272e3 parent: ce4d910 modified
glimmer-backends/glimmer-jvui/src/glimmer_jvui/core.clj +39 -5 | @@ -153,6 +153,29 @@ | ||
| 153 | 153 | (when *record-rects?* (swap! n assoc :rect (c/rect-of id))) |
| 154 | 154 | nil) |
| 155 | 155 | |
| 156 | +(def ^:private hovering | |
| 157 | + "Which widgets the pointer was on last frame. | |
| 158 | + | |
| 159 | + The toolkit answers `:hover?` as a state — the pointer is over this | |
| 160 | + rectangle — and a component wants the two EVENTS at its edges. The | |
| 161 | + difference is a set, and it is kept here rather than on the node because | |
| 162 | + a node is replaced by the reconciler and the pointer has not moved." | |
| 163 | + (atom #{})) | |
| 164 | + | |
| 165 | +(defn- hover! | |
| 166 | + "Turn `over?` into on-hover and on-unhover, once each per crossing. | |
| 167 | + | |
| 168 | + Only on the pass that paints: hover is derived from where the pointer is | |
| 169 | + rather than delivered as an event, so it is true on the settling passes | |
| 170 | + too, and a handler called from one of those fires two or three times for | |
| 171 | + one crossing." | |
| 172 | + [n id over?] | |
| 173 | + (when (c/draw-pass?) | |
| 174 | + (let [was (contains? @hovering id)] | |
| 175 | + (cond | |
| 176 | + (and over? (not was)) (do (swap! hovering conj id) (fire! n :on-hover)) | |
| 177 | + (and was (not over?)) (do (swap! hovering disj id) (fire! n :on-unhover)))))) | |
| 178 | + | |
| 156 | 179 | (defn- emit-children! [n] |
| 157 | 180 | (fn [_id _rect] (doseq [c (:children @n)] (emit! c)))) |
| 158 | 181 | |
| @@ -251,13 +274,24 @@ | ||
| 251 | 274 | (:size props) (assoc :size (:size props)))) |
| 252 | 275 | |
| 253 | 276 | :reaction (let [id (c/next-id key) |
| 254 | - glyph (or (:emoji props) s)] | |
| 255 | - (record! n id) | |
| 256 | - (when (w/reaction glyph {:count (or (:count props) 0) | |
| 277 | + glyph (or (:emoji props) s) | |
| 278 | + r (w/reaction glyph {:count (or (:count props) 0) | |
| 257 | 279 | :mine? (boolean (:mine props)) |
| 258 | 280 | :size (:size props) |
| 259 | - :key key}) | |
| 260 | - (fire! n :on-click))) | |
| 281 | + :key key})] | |
| 282 | + (record! n id) | |
| 283 | + (when (:clicked? r) (fire! n :on-click)) | |
| 284 | + (hover! n id (:hover? r)) | |
| 285 | + ;; Whatever the client hung under the pill is its hover | |
| 286 | + ;; card, and a card is drawn over the row rather than in | |
| 287 | + ;; it — see c/overlay!. Under the pill and a little to its | |
| 288 | + ;; right, which is where a pointer that is on the pill is | |
| 289 | + ;; not. | |
| 290 | + (when (and (:hover? r) (seq (:children @n))) | |
| 291 | + (let [[x y _ h] (:rect r)] | |
| 292 | + (c/overlay! id [x (+ y h 4.0)] | |
| 293 | + #(w/card* {:expand :none :key id} | |
| 294 | + (emit-children! n)))))) | |
| 261 | 295 | |
| 262 | 296 | :title (w/title s) |
| 263 | 297 | |
| @@ -153,6 +153,29 @@ | |||
| 153 | (when *record-rects?* (swap! n assoc :rect (c/rect-of id))) | 153 | (when *record-rects?* (swap! n assoc :rect (c/rect-of id))) |
| 154 | nil) | 154 | nil) |
| 155 | 155 | ||
| 156 | +(def ^:private hovering | ||
| 157 | + "Which widgets the pointer was on last frame. | ||
| 158 | + | ||
| 159 | + The toolkit answers `:hover?` as a state — the pointer is over this | ||
| 160 | + rectangle — and a component wants the two EVENTS at its edges. The | ||
| 161 | + difference is a set, and it is kept here rather than on the node because | ||
| 162 | + a node is replaced by the reconciler and the pointer has not moved." | ||
| 163 | + (atom #{})) | ||
| 164 | + | ||
| 165 | +(defn- hover! | ||
| 166 | + "Turn `over?` into on-hover and on-unhover, once each per crossing. | ||
| 167 | + | ||
| 168 | + Only on the pass that paints: hover is derived from where the pointer is | ||
| 169 | + rather than delivered as an event, so it is true on the settling passes | ||
| 170 | + too, and a handler called from one of those fires two or three times for | ||
| 171 | + one crossing." | ||
| 172 | + [n id over?] | ||
| 173 | + (when (c/draw-pass?) | ||
| 174 | + (let [was (contains? @hovering id)] | ||
| 175 | + (cond | ||
| 176 | + (and over? (not was)) (do (swap! hovering conj id) (fire! n :on-hover)) | ||
| 177 | + (and was (not over?)) (do (swap! hovering disj id) (fire! n :on-unhover)))))) | ||
| 178 | + | ||
| 156 | (defn- emit-children! [n] | 179 | (defn- emit-children! [n] |
| 157 | (fn [_id _rect] (doseq [c (:children @n)] (emit! c)))) | 180 | (fn [_id _rect] (doseq [c (:children @n)] (emit! c)))) |
| 158 | 181 | ||
| @@ -251,13 +274,24 @@ | |||
| 251 | (:size props) (assoc :size (:size props)))) | 274 | (:size props) (assoc :size (:size props)))) |
| 252 | 275 | ||
| 253 | :reaction (let [id (c/next-id key) | 276 | :reaction (let [id (c/next-id key) |
| 254 | - glyph (or (:emoji props) s)] | 277 | + glyph (or (:emoji props) s) |
| 255 | - (record! n id) | 278 | + r (w/reaction glyph {:count (or (:count props) 0) |
| 256 | - (when (w/reaction glyph {:count (or (:count props) 0) | ||
| 257 | :mine? (boolean (:mine props)) | 279 | :mine? (boolean (:mine props)) |
| 258 | :size (:size props) | 280 | :size (:size props) |
| 259 | - :key key}) | 281 | + :key key})] |
| 260 | - (fire! n :on-click))) | 282 | + (record! n id) |
| 283 | + (when (:clicked? r) (fire! n :on-click)) | ||
| 284 | + (hover! n id (:hover? r)) | ||
| 285 | + ;; Whatever the client hung under the pill is its hover | ||
| 286 | + ;; card, and a card is drawn over the row rather than in | ||
| 287 | + ;; it — see c/overlay!. Under the pill and a little to its | ||
| 288 | + ;; right, which is where a pointer that is on the pill is | ||
| 289 | + ;; not. | ||
| 290 | + (when (and (:hover? r) (seq (:children @n))) | ||
| 291 | + (let [[x y _ h] (:rect r)] | ||
| 292 | + (c/overlay! id [x (+ y h 4.0)] | ||
| 293 | + #(w/card* {:expand :none :key id} | ||
| 294 | + (emit-children! n)))))) | ||
| 261 | 295 | ||
| 262 | :title (w/title s) | 296 | :title (w/title s) |
| 263 | 297 | ||
modified
jvui/src/jvui/core.clj +64 -1 | @@ -461,6 +461,31 @@ | ||
| 461 | 461 | :rect box-rect :content (:rect f)}) |
| 462 | 462 | result)))) |
| 463 | 463 | |
| 464 | +(defn draw-pass? | |
| 465 | + "True on the walk that paints. Unlike `drawing?` this does not also ask for | |
| 466 | + a painter, so a headless walk answers it — which is what lets an event | |
| 467 | + that is derived from state rather than delivered as one, hover being the | |
| 468 | + case, fire exactly once a frame in a test as well as in a window." | |
| 469 | + [] (boolean (:draw? (ui)))) | |
| 470 | + | |
| 471 | +(defn overlay! | |
| 472 | + "Draw `f` after the rest of the walk, anchored at [x y] and outside the | |
| 473 | + layout. | |
| 474 | + | |
| 475 | + A hover card is the case this exists for: it belongs on top of whatever it | |
| 476 | + is over and must cost the thing it is attached to no space at all. Placed | |
| 477 | + inline it would push the row it is in aside the moment the pointer arrived, | |
| 478 | + which moves the pill out from under the pointer, which takes the card away | |
| 479 | + again — a flicker rather than a card. | |
| 480 | + | |
| 481 | + So it is deferred instead: collected during the walk and run afterwards on | |
| 482 | + a root frame of its own, clear of every clip the walk had set. `key` names | |
| 483 | + it so its size is remembered between frames, which is what lets it be kept | |
| 484 | + on screen — a card anchored near the bottom is moved up by its own height | |
| 485 | + rather than drawn off the edge." | |
| 486 | + [key [x y] f] | |
| 487 | + (upd! update :overlays (fnil conj []) {:key key :at [x y] :f f})) | |
| 488 | + | |
| 464 | 489 | (defn leaf |
| 465 | 490 | "Place a widget that already knows its size. Answers its rectangle." |
| 466 | 491 | ([min-size] (leaf min-size :none [0.0 0.0])) |
| @@ -572,13 +597,50 @@ | ||
| 572 | 597 | nxt (if (neg? cur) 0 (mod (+ cur step) n))] |
| 573 | 598 | (focus! (nth fs nxt)))))))) |
| 574 | 599 | |
| 600 | +(defn- drain-overlays! | |
| 601 | + "Run whatever the walk deferred, each on a root frame of its own. | |
| 602 | + | |
| 603 | + After the walk and not during it, so an overlay is painted over what it | |
| 604 | + overlaps rather than under it, and so it is placed against the window | |
| 605 | + rather than against whatever box happened to be open when it was asked | |
| 606 | + for. An overlay may ask for another — a card with a chip in it — so this | |
| 607 | + loops, with a bound: a cycle would otherwise be a hung frame rather than a | |
| 608 | + wrong one." | |
| 609 | + [ctx-atom w h] | |
| 610 | + (set-clip! nil) | |
| 611 | + (loop [depth 0] | |
| 612 | + (let [os (:overlays @ctx-atom)] | |
| 613 | + (when (and (seq os) (< depth 4)) | |
| 614 | + (swap! ctx-atom assoc :overlays []) | |
| 615 | + (doseq [{:keys [key at f]} os] | |
| 616 | + (let [id [:overlay key] | |
| 617 | + [ox oy] at | |
| 618 | + ;; Last frame's size. Zero on the first, which puts the card | |
| 619 | + ;; exactly where it was asked for and lets the next frame | |
| 620 | + ;; move it — one frame of overhang the first time a card is | |
| 621 | + ;; ever shown near an edge, against a measuring pass for | |
| 622 | + ;; every card that is not. | |
| 623 | + [pw ph] (or (:size (get-in @ctx-atom [:data id])) [0.0 0.0]) | |
| 624 | + x (max 0.0 (min (double ox) (- (double w) (double pw)))) | |
| 625 | + y (max 0.0 (min (double oy) (- (double h) (double ph)))) | |
| 626 | + fr {:id id :dir :vertical | |
| 627 | + :rect [x y (- (double w) x) (- (double h) y)] | |
| 628 | + :spacing 0.0 :share 0.0 :c (counters) :offset [0.0 0.0]}] | |
| 629 | + (swap! ctx-atom update :stack conj fr) | |
| 630 | + (f) | |
| 631 | + (let [ff (peek (:stack @ctx-atom))] | |
| 632 | + (swap! ctx-atom update :stack pop) | |
| 633 | + (swap! ctx-atom assoc-in [:data id] | |
| 634 | + {:size [(ctr ff CROSS) (ctr ff ALONG)]})))) | |
| 635 | + (recur (inc depth)))))) | |
| 636 | + | |
| 575 | 637 | (defn- one-pass! |
| 576 | 638 | [ctx-atom render draw?] |
| 577 | 639 | (swap! ctx-atom |
| 578 | 640 | (fn [c] |
| 579 | 641 | (assoc c :stack [] :refresh? false :focusables [] |
| 580 | 642 | :handled #{} :draw? draw? |
| 581 | - :clip nil))) | |
| 643 | + :overlays [] :clip nil))) | |
| 582 | 644 | (let [[w h] (:size @ctx-atom) |
| 583 | 645 | ;; The root is a box like any other, and it remembers under id 0. It has |
| 584 | 646 | ;; to: without last frame's counts it cannot know how much of the window |
| @@ -595,6 +657,7 @@ | ||
| 595 | 657 | (swap! ctx-atom update :stack conj root) |
| 596 | 658 | (binding [*ui* ctx-atom] |
| 597 | 659 | (render) |
| 660 | + (drain-overlays! ctx-atom w h) | |
| 598 | 661 | (when draw? (move-focus!))) |
| 599 | 662 | (let [f (peek (:stack @ctx-atom))] |
| 600 | 663 | (when (not= [(ctr f N) (ctr f EXPANDERS) (ctr f ALONG)] |
| @@ -461,6 +461,31 @@ | |||
| 461 | :rect box-rect :content (:rect f)}) | 461 | :rect box-rect :content (:rect f)}) |
| 462 | result)))) | 462 | result)))) |
| 463 | 463 | ||
| 464 | +(defn draw-pass? | ||
| 465 | + "True on the walk that paints. Unlike `drawing?` this does not also ask for | ||
| 466 | + a painter, so a headless walk answers it — which is what lets an event | ||
| 467 | + that is derived from state rather than delivered as one, hover being the | ||
| 468 | + case, fire exactly once a frame in a test as well as in a window." | ||
| 469 | + [] (boolean (:draw? (ui)))) | ||
| 470 | + | ||
| 471 | +(defn overlay! | ||
| 472 | + "Draw `f` after the rest of the walk, anchored at [x y] and outside the | ||
| 473 | + layout. | ||
| 474 | + | ||
| 475 | + A hover card is the case this exists for: it belongs on top of whatever it | ||
| 476 | + is over and must cost the thing it is attached to no space at all. Placed | ||
| 477 | + inline it would push the row it is in aside the moment the pointer arrived, | ||
| 478 | + which moves the pill out from under the pointer, which takes the card away | ||
| 479 | + again — a flicker rather than a card. | ||
| 480 | + | ||
| 481 | + So it is deferred instead: collected during the walk and run afterwards on | ||
| 482 | + a root frame of its own, clear of every clip the walk had set. `key` names | ||
| 483 | + it so its size is remembered between frames, which is what lets it be kept | ||
| 484 | + on screen — a card anchored near the bottom is moved up by its own height | ||
| 485 | + rather than drawn off the edge." | ||
| 486 | + [key [x y] f] | ||
| 487 | + (upd! update :overlays (fnil conj []) {:key key :at [x y] :f f})) | ||
| 488 | + | ||
| 464 | (defn leaf | 489 | (defn leaf |
| 465 | "Place a widget that already knows its size. Answers its rectangle." | 490 | "Place a widget that already knows its size. Answers its rectangle." |
| 466 | ([min-size] (leaf min-size :none [0.0 0.0])) | 491 | ([min-size] (leaf min-size :none [0.0 0.0])) |
| @@ -572,13 +597,50 @@ | |||
| 572 | nxt (if (neg? cur) 0 (mod (+ cur step) n))] | 597 | nxt (if (neg? cur) 0 (mod (+ cur step) n))] |
| 573 | (focus! (nth fs nxt)))))))) | 598 | (focus! (nth fs nxt)))))))) |
| 574 | 599 | ||
| 600 | +(defn- drain-overlays! | ||
| 601 | + "Run whatever the walk deferred, each on a root frame of its own. | ||
| 602 | + | ||
| 603 | + After the walk and not during it, so an overlay is painted over what it | ||
| 604 | + overlaps rather than under it, and so it is placed against the window | ||
| 605 | + rather than against whatever box happened to be open when it was asked | ||
| 606 | + for. An overlay may ask for another — a card with a chip in it — so this | ||
| 607 | + loops, with a bound: a cycle would otherwise be a hung frame rather than a | ||
| 608 | + wrong one." | ||
| 609 | + [ctx-atom w h] | ||
| 610 | + (set-clip! nil) | ||
| 611 | + (loop [depth 0] | ||
| 612 | + (let [os (:overlays @ctx-atom)] | ||
| 613 | + (when (and (seq os) (< depth 4)) | ||
| 614 | + (swap! ctx-atom assoc :overlays []) | ||
| 615 | + (doseq [{:keys [key at f]} os] | ||
| 616 | + (let [id [:overlay key] | ||
| 617 | + [ox oy] at | ||
| 618 | + ;; Last frame's size. Zero on the first, which puts the card | ||
| 619 | + ;; exactly where it was asked for and lets the next frame | ||
| 620 | + ;; move it — one frame of overhang the first time a card is | ||
| 621 | + ;; ever shown near an edge, against a measuring pass for | ||
| 622 | + ;; every card that is not. | ||
| 623 | + [pw ph] (or (:size (get-in @ctx-atom [:data id])) [0.0 0.0]) | ||
| 624 | + x (max 0.0 (min (double ox) (- (double w) (double pw)))) | ||
| 625 | + y (max 0.0 (min (double oy) (- (double h) (double ph)))) | ||
| 626 | + fr {:id id :dir :vertical | ||
| 627 | + :rect [x y (- (double w) x) (- (double h) y)] | ||
| 628 | + :spacing 0.0 :share 0.0 :c (counters) :offset [0.0 0.0]}] | ||
| 629 | + (swap! ctx-atom update :stack conj fr) | ||
| 630 | + (f) | ||
| 631 | + (let [ff (peek (:stack @ctx-atom))] | ||
| 632 | + (swap! ctx-atom update :stack pop) | ||
| 633 | + (swap! ctx-atom assoc-in [:data id] | ||
| 634 | + {:size [(ctr ff CROSS) (ctr ff ALONG)]})))) | ||
| 635 | + (recur (inc depth)))))) | ||
| 636 | + | ||
| 575 | (defn- one-pass! | 637 | (defn- one-pass! |
| 576 | [ctx-atom render draw?] | 638 | [ctx-atom render draw?] |
| 577 | (swap! ctx-atom | 639 | (swap! ctx-atom |
| 578 | (fn [c] | 640 | (fn [c] |
| 579 | (assoc c :stack [] :refresh? false :focusables [] | 641 | (assoc c :stack [] :refresh? false :focusables [] |
| 580 | :handled #{} :draw? draw? | 642 | :handled #{} :draw? draw? |
| 581 | - :clip nil))) | 643 | + :overlays [] :clip nil))) |
| 582 | (let [[w h] (:size @ctx-atom) | 644 | (let [[w h] (:size @ctx-atom) |
| 583 | ;; The root is a box like any other, and it remembers under id 0. It has | 645 | ;; The root is a box like any other, and it remembers under id 0. It has |
| 584 | ;; to: without last frame's counts it cannot know how much of the window | 646 | ;; to: without last frame's counts it cannot know how much of the window |
| @@ -595,6 +657,7 @@ | |||
| 595 | (swap! ctx-atom update :stack conj root) | 657 | (swap! ctx-atom update :stack conj root) |
| 596 | (binding [*ui* ctx-atom] | 658 | (binding [*ui* ctx-atom] |
| 597 | (render) | 659 | (render) |
| 660 | + (drain-overlays! ctx-atom w h) | ||
| 598 | (when draw? (move-focus!))) | 661 | (when draw? (move-focus!))) |
| 599 | (let [f (peek (:stack @ctx-atom))] | 662 | (let [f (peek (:stack @ctx-atom))] |
| 600 | (when (not= [(ctr f N) (ctr f EXPANDERS) (ctr f ALONG)] | 663 | (when (not= [(ctr f N) (ctr f EXPANDERS) (ctr f ALONG)] |
modified
jvui/src/jvui/widgets.clj +9 -1 | @@ -633,7 +633,12 @@ | ||
| 633 | 633 | ([s {:keys [key]}] |
| 634 | 634 | (let [id (c/next-id key) |
| 635 | 635 | rect (label s {:colour (c/th :accent)})] |
| 636 | - (:clicked? (c/interact! id rect))))) | |
| 636 | + ;; The whole interaction and not just the click: a reaction is the one | |
| 637 | + ;; chip that answers the pointer resting on it as well as pressing it — | |
| 638 | + ;; who put it there is what the pill's number will not say — so the | |
| 639 | + ;; caller needs `:hover?` too. It also needs the rectangle, to know | |
| 640 | + ;; where to hang the card. | |
| 641 | + (assoc (c/interact! id rect) :rect rect)))) | |
| 637 | 642 | |
| 638 | 643 | (defn emoji |
| 639 | 644 | "One emoji, drawn as a character and sized to sit level with the words |
| @@ -685,6 +690,9 @@ | ||
| 685 | 690 | "A tally wearing a pill: an emoji, how many people, and whether you are one |
| 686 | 691 | of them. |
| 687 | 692 | |
| 693 | + Answers the interaction — `:clicked?`, `:hover?` and the `:rect` it was | |
| 694 | + given — rather than a bare click. | |
| 695 | + | |
| 688 | 696 | The same glyph `emoji` draws, and the same two ways of drawing it: a |
| 689 | 697 | colour face gives a square picture, and everything else is text. So the |
| 690 | 698 | glyph and the tally are measured and drawn apart rather than as one |
| @@ -633,7 +633,12 @@ | |||
| 633 | ([s {:keys [key]}] | 633 | ([s {:keys [key]}] |
| 634 | (let [id (c/next-id key) | 634 | (let [id (c/next-id key) |
| 635 | rect (label s {:colour (c/th :accent)})] | 635 | rect (label s {:colour (c/th :accent)})] |
| 636 | - (:clicked? (c/interact! id rect))))) | 636 | + ;; The whole interaction and not just the click: a reaction is the one |
| 637 | + ;; chip that answers the pointer resting on it as well as pressing it — | ||
| 638 | + ;; who put it there is what the pill's number will not say — so the | ||
| 639 | + ;; caller needs `:hover?` too. It also needs the rectangle, to know | ||
| 640 | + ;; where to hang the card. | ||
| 641 | + (assoc (c/interact! id rect) :rect rect)))) | ||
| 637 | 642 | ||
| 638 | (defn emoji | 643 | (defn emoji |
| 639 | "One emoji, drawn as a character and sized to sit level with the words | 644 | "One emoji, drawn as a character and sized to sit level with the words |
| @@ -685,6 +690,9 @@ | |||
| 685 | "A tally wearing a pill: an emoji, how many people, and whether you are one | 690 | "A tally wearing a pill: an emoji, how many people, and whether you are one |
| 686 | of them. | 691 | of them. |
| 687 | 692 | ||
| 693 | + Answers the interaction — `:clicked?`, `:hover?` and the `:rect` it was | ||
| 694 | + given — rather than a bare click. | ||
| 695 | + | ||
| 688 | The same glyph `emoji` draws, and the same two ways of drawing it: a | 696 | The same glyph `emoji` draws, and the same two ways of drawing it: a |
| 689 | colour face gives a square picture, and everything else is text. So the | 697 | colour face gives a square picture, and everything else is text. So the |
| 690 | glyph and the tally are measured and drawn apart rather than as one | 698 | glyph and the tally are measured and drawn apart rather than as one |