nandi/jolt-nativepublic Fork 0
fa4ecdf
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

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>
nandi committed 2026-08-30T19:04:30-07:00 Browse files
fa4ecdf parent: 5adddc7
modified crates/jolt-vidya/src/tree.rs +57 -0
@@ -239,6 +239,16 @@ pub struct Tree {
239239 /// every re-render, and a row would jump to the left edge for a frame on
240240 /// every keystroke typed into it.
241241 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,
242252 pending: VecDeque<Event>,
243253 /// The event most recently dequeued by `poll`, whose fields the accessors
244254 /// read. Held here so the ABI can return a payload without out-parameters.
@@ -389,6 +399,8 @@ impl Default for Tree {
389399 textures: HashMap::new(),
390400 feeds: HashMap::new(),
391401 row_widths: HashMap::new(),
402+ hovered: None,
403+ in_hover_panel: false,
392404 pending: VecDeque::new(),
393405 current: None,
394406 };
@@ -657,6 +669,32 @@ impl Tree {
657669
658670 // ── events ──────────────────────────────────────────────────────────────
659671
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+
660698 fn emit(&mut self, node: u32, name: &'static str, text: String, num: f64) {
661699 self.pending.push_back(Event {
662700 node,
@@ -1246,6 +1284,25 @@ impl Tree {
12461284 if response.clicked() {
12471285 self.emit(id, "click", label, 0.0);
12481286 }
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+ }
12491306 }
12501307
12511308 // 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 on239 /// 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 accessors253 /// 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 rather1308 // 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 @@
223223 ;; one scratch buffer — jolt copies it as it crosses.
224224 "change" (when-let [f (:on-change hs)] (f (ffi/event-text)))
225225 "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))
226231 ;; Ctrl+V on a clipboard with no text on it. What is on it instead is
227232 ;; the caller's to find out — `clipboard-image-png!` is the only
228233 ;; 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 is231 ;; 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 only232 ;; 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.