Let a face answer the pointer
An avatar now reports the two edges of a hover, and one with children paints them into the panel beside the pointer — the only layer above the page there is, in a backend that paints in one. What is painted there reports no hover of its own: the panel sits under the pointer, so a face on a card would take the hover away from the face the card is about, and close itself as it opened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fa4ecdf parent: 5adddc7 modified
crates/jolt-vidya/src/tree.rs +57 -0 | @@ -239,6 +239,16 @@ pub struct Tree { | ||
| 239 | 239 | /// every re-render, and a row would jump to the left edge for a frame on |
| 240 | 240 | /// every keystroke typed into it. |
| 241 | 241 | row_widths: HashMap<u32, f32>, |
| 242 | + /// The node the pointer is over, so that starting and ending a hover can | |
| 243 | + /// be told apart from being in the middle of one. Only one node is | |
| 244 | + /// hovered at a time — the innermost one that senses it. | |
| 245 | + hovered: Option<u32>, | |
| 246 | + /// Set while the children of a hovered node are being painted into the | |
| 247 | + /// panel beside the pointer. What is painted there does not report hovers | |
| 248 | + /// of its own: the panel sits under the pointer, so a face on the card | |
| 249 | + /// would take the hover away from the face the card is about, and the | |
| 250 | + /// card would close itself the moment it opened. | |
| 251 | + in_hover_panel: bool, | |
| 242 | 252 | pending: VecDeque<Event>, |
| 243 | 253 | /// The event most recently dequeued by `poll`, whose fields the accessors |
| 244 | 254 | /// read. Held here so the ABI can return a payload without out-parameters. |
| @@ -389,6 +399,8 @@ impl Default for Tree { | ||
| 389 | 399 | textures: HashMap::new(), |
| 390 | 400 | feeds: HashMap::new(), |
| 391 | 401 | row_widths: HashMap::new(), |
| 402 | + hovered: None, | |
| 403 | + in_hover_panel: false, | |
| 392 | 404 | pending: VecDeque::new(), |
| 393 | 405 | current: None, |
| 394 | 406 | }; |
| @@ -657,6 +669,32 @@ impl Tree { | ||
| 657 | 669 | |
| 658 | 670 | // ── events ────────────────────────────────────────────────────────────── |
| 659 | 671 | |
| 672 | + /// Report the edges of a pointer hover on `node`: "hover" when it starts, | |
| 673 | + /// "unhover" when it ends. One node is hovered at a time, so the previous | |
| 674 | + /// one is closed out here rather than needing a pass of its own — a | |
| 675 | + /// pointer that leaves an avatar for another emits both in one frame. | |
| 676 | + fn track_hover(&mut self, node: u32, response: &egui::Response) { | |
| 677 | + if self.in_hover_panel { | |
| 678 | + return; | |
| 679 | + } | |
| 680 | + if response.hovered() { | |
| 681 | + if self.hovered != Some(node) { | |
| 682 | + if let Some(was) = self.hovered { | |
| 683 | + self.emit(was, "unhover", String::new(), 0.0); | |
| 684 | + } | |
| 685 | + self.hovered = Some(node); | |
| 686 | + self.emit(node, "hover", String::new(), 0.0); | |
| 687 | + } | |
| 688 | + } else if self.hovered == Some(node) { | |
| 689 | + self.hovered = None; | |
| 690 | + self.emit(node, "unhover", String::new(), 0.0); | |
| 691 | + } | |
| 692 | + } | |
| 693 | + | |
| 694 | + fn has_children(&self, id: u32) -> bool { | |
| 695 | + self.slot(id).is_some_and(|n| !n.children.is_empty()) | |
| 696 | + } | |
| 697 | + | |
| 660 | 698 | fn emit(&mut self, node: u32, name: &'static str, text: String, num: f64) { |
| 661 | 699 | self.pending.push_back(Event { |
| 662 | 700 | node, |
| @@ -1246,6 +1284,25 @@ impl Tree { | ||
| 1246 | 1284 | if response.clicked() { |
| 1247 | 1285 | self.emit(id, "click", label, 0.0); |
| 1248 | 1286 | } |
| 1287 | + // The face answers the pointer as well as the tap. A hover | |
| 1288 | + // says so once, when it starts, and once when it ends — | |
| 1289 | + // per-frame events would be a flood, and the caller only | |
| 1290 | + // wants the two edges. What it does with them is its own | |
| 1291 | + // business; the usual answer is to render children here, | |
| 1292 | + // which are painted as the hover's own panel below. | |
| 1293 | + self.track_hover(id, &response); | |
| 1294 | + // A hovered avatar with children shows them beside the | |
| 1295 | + // pointer: a profile card is a thing the tree can already | |
| 1296 | + // describe, and this is the only layer above the page that | |
| 1297 | + // exists to put one in. | |
| 1298 | + if response.hovered() && !self.in_hover_panel && self.has_children(id) { | |
| 1299 | + self.in_hover_panel = true; | |
| 1300 | + response.show_tooltip_ui(|ui| { | |
| 1301 | + ui.set_max_width(320.0); | |
| 1302 | + self.paint_children(id, ui, theme); | |
| 1303 | + }); | |
| 1304 | + self.in_hover_panel = false; | |
| 1305 | + } | |
| 1249 | 1306 | } |
| 1250 | 1307 | |
| 1251 | 1308 | // A reaction chip: the emoji drawn from the Twemoji pack rather |
| @@ -239,6 +239,16 @@ pub struct Tree { | |||
| 239 | /// every re-render, and a row would jump to the left edge for a frame on | 239 | /// every re-render, and a row would jump to the left edge for a frame on |
| 240 | /// every keystroke typed into it. | 240 | /// every keystroke typed into it. |
| 241 | row_widths: HashMap<u32, f32>, | 241 | row_widths: HashMap<u32, f32>, |
| 242 | + /// The node the pointer is over, so that starting and ending a hover can | ||
| 243 | + /// be told apart from being in the middle of one. Only one node is | ||
| 244 | + /// hovered at a time — the innermost one that senses it. | ||
| 245 | + hovered: Option<u32>, | ||
| 246 | + /// Set while the children of a hovered node are being painted into the | ||
| 247 | + /// panel beside the pointer. What is painted there does not report hovers | ||
| 248 | + /// of its own: the panel sits under the pointer, so a face on the card | ||
| 249 | + /// would take the hover away from the face the card is about, and the | ||
| 250 | + /// card would close itself the moment it opened. | ||
| 251 | + in_hover_panel: bool, | ||
| 242 | pending: VecDeque<Event>, | 252 | pending: VecDeque<Event>, |
| 243 | /// The event most recently dequeued by `poll`, whose fields the accessors | 253 | /// The event most recently dequeued by `poll`, whose fields the accessors |
| 244 | /// read. Held here so the ABI can return a payload without out-parameters. | 254 | /// read. Held here so the ABI can return a payload without out-parameters. |
| @@ -389,6 +399,8 @@ impl Default for Tree { | |||
| 389 | textures: HashMap::new(), | 399 | textures: HashMap::new(), |
| 390 | feeds: HashMap::new(), | 400 | feeds: HashMap::new(), |
| 391 | row_widths: HashMap::new(), | 401 | row_widths: HashMap::new(), |
| 402 | + hovered: None, | ||
| 403 | + in_hover_panel: false, | ||
| 392 | pending: VecDeque::new(), | 404 | pending: VecDeque::new(), |
| 393 | current: None, | 405 | current: None, |
| 394 | }; | 406 | }; |
| @@ -657,6 +669,32 @@ impl Tree { | |||
| 657 | 669 | ||
| 658 | // ── events ────────────────────────────────────────────────────────────── | 670 | // ── events ────────────────────────────────────────────────────────────── |
| 659 | 671 | ||
| 672 | + /// Report the edges of a pointer hover on `node`: "hover" when it starts, | ||
| 673 | + /// "unhover" when it ends. One node is hovered at a time, so the previous | ||
| 674 | + /// one is closed out here rather than needing a pass of its own — a | ||
| 675 | + /// pointer that leaves an avatar for another emits both in one frame. | ||
| 676 | + fn track_hover(&mut self, node: u32, response: &egui::Response) { | ||
| 677 | + if self.in_hover_panel { | ||
| 678 | + return; | ||
| 679 | + } | ||
| 680 | + if response.hovered() { | ||
| 681 | + if self.hovered != Some(node) { | ||
| 682 | + if let Some(was) = self.hovered { | ||
| 683 | + self.emit(was, "unhover", String::new(), 0.0); | ||
| 684 | + } | ||
| 685 | + self.hovered = Some(node); | ||
| 686 | + self.emit(node, "hover", String::new(), 0.0); | ||
| 687 | + } | ||
| 688 | + } else if self.hovered == Some(node) { | ||
| 689 | + self.hovered = None; | ||
| 690 | + self.emit(node, "unhover", String::new(), 0.0); | ||
| 691 | + } | ||
| 692 | + } | ||
| 693 | + | ||
| 694 | + fn has_children(&self, id: u32) -> bool { | ||
| 695 | + self.slot(id).is_some_and(|n| !n.children.is_empty()) | ||
| 696 | + } | ||
| 697 | + | ||
| 660 | fn emit(&mut self, node: u32, name: &'static str, text: String, num: f64) { | 698 | fn emit(&mut self, node: u32, name: &'static str, text: String, num: f64) { |
| 661 | self.pending.push_back(Event { | 699 | self.pending.push_back(Event { |
| 662 | node, | 700 | node, |
| @@ -1246,6 +1284,25 @@ impl Tree { | |||
| 1246 | if response.clicked() { | 1284 | if response.clicked() { |
| 1247 | self.emit(id, "click", label, 0.0); | 1285 | self.emit(id, "click", label, 0.0); |
| 1248 | } | 1286 | } |
| 1287 | + // The face answers the pointer as well as the tap. A hover | ||
| 1288 | + // says so once, when it starts, and once when it ends — | ||
| 1289 | + // per-frame events would be a flood, and the caller only | ||
| 1290 | + // wants the two edges. What it does with them is its own | ||
| 1291 | + // business; the usual answer is to render children here, | ||
| 1292 | + // which are painted as the hover's own panel below. | ||
| 1293 | + self.track_hover(id, &response); | ||
| 1294 | + // A hovered avatar with children shows them beside the | ||
| 1295 | + // pointer: a profile card is a thing the tree can already | ||
| 1296 | + // describe, and this is the only layer above the page that | ||
| 1297 | + // exists to put one in. | ||
| 1298 | + if response.hovered() && !self.in_hover_panel && self.has_children(id) { | ||
| 1299 | + self.in_hover_panel = true; | ||
| 1300 | + response.show_tooltip_ui(|ui| { | ||
| 1301 | + ui.set_max_width(320.0); | ||
| 1302 | + self.paint_children(id, ui, theme); | ||
| 1303 | + }); | ||
| 1304 | + self.in_hover_panel = false; | ||
| 1305 | + } | ||
| 1249 | } | 1306 | } |
| 1250 | 1307 | ||
| 1251 | // A reaction chip: the emoji drawn from the Twemoji pack rather | 1308 | // A reaction chip: the emoji drawn from the Twemoji pack rather |
modified
jolt/glimmer-vidya/src/glimmer_vidya/core.jolt +5 -0 | @@ -223,6 +223,11 @@ | ||
| 223 | 223 | ;; one scratch buffer — jolt copies it as it crosses. |
| 224 | 224 | "change" (when-let [f (:on-change hs)] (f (ffi/event-text))) |
| 225 | 225 | "activate" (when-let [f (:on-activate hs)] (f)) |
| 226 | + ;; The two edges of a pointer hover, not the middle of one: the | |
| 227 | + ;; backend says so when it starts and again when it ends, so a | |
| 228 | + ;; handler can put something on screen and take it away again. | |
| 229 | + "hover" (when-let [f (:on-hover hs)] (f)) | |
| 230 | + "unhover" (when-let [f (:on-unhover hs)] (f)) | |
| 226 | 231 | ;; Ctrl+V on a clipboard with no text on it. What is on it instead is |
| 227 | 232 | ;; the caller's to find out — `clipboard-image-png!` is the only |
| 228 | 233 | ;; question the backend answers about it. |
| @@ -223,6 +223,11 @@ | |||
| 223 | ;; one scratch buffer — jolt copies it as it crosses. | 223 | ;; one scratch buffer — jolt copies it as it crosses. |
| 224 | "change" (when-let [f (:on-change hs)] (f (ffi/event-text))) | 224 | "change" (when-let [f (:on-change hs)] (f (ffi/event-text))) |
| 225 | "activate" (when-let [f (:on-activate hs)] (f)) | 225 | "activate" (when-let [f (:on-activate hs)] (f)) |
| 226 | + ;; The two edges of a pointer hover, not the middle of one: the | ||
| 227 | + ;; backend says so when it starts and again when it ends, so a | ||
| 228 | + ;; handler can put something on screen and take it away again. | ||
| 229 | + "hover" (when-let [f (:on-hover hs)] (f)) | ||
| 230 | + "unhover" (when-let [f (:on-unhover hs)] (f)) | ||
| 226 | ;; Ctrl+V on a clipboard with no text on it. What is on it instead is | 231 | ;; Ctrl+V on a clipboard with no text on it. What is on it instead is |
| 227 | ;; the caller's to find out — `clipboard-image-png!` is the only | 232 | ;; the caller's to find out — `clipboard-image-png!` is the only |
| 228 | ;; question the backend answers about it. | 233 | ;; question the backend answers about it. |