Let a reaction pill answer the pointer, as a face does
A chip carries a tally, and the one thing a tally leaves out is who is in it. The two hover edges and a panel of children, exactly as the avatar already had them, so the caller can hang a list of names off the pill the pointer is resting on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c71fc59 parent: 726f7c6 modified
crates/jolt-vidya/src/tree.rs +57 -0 | @@ -1337,6 +1337,19 @@ impl Tree { | ||
| 1337 | 1337 | if response.clicked() { |
| 1338 | 1338 | self.emit(id, "click", emoji, count as f64); |
| 1339 | 1339 | } |
| 1340 | + // A pill answers the pointer the way a face does: the two | |
| 1341 | + // edges of a hover, and children painted beside the pointer | |
| 1342 | + // while it rests. A reaction is a tally, and who is in it is | |
| 1343 | + // the thing the tally leaves out. | |
| 1344 | + self.track_hover(id, &response); | |
| 1345 | + if response.hovered() && !self.in_hover_panel && self.has_children(id) { | |
| 1346 | + self.in_hover_panel = true; | |
| 1347 | + response.show_tooltip_ui(|ui| { | |
| 1348 | + ui.set_max_width(320.0); | |
| 1349 | + self.paint_children(id, ui, theme); | |
| 1350 | + }); | |
| 1351 | + self.in_hover_panel = false; | |
| 1352 | + } | |
| 1340 | 1353 | } |
| 1341 | 1354 | |
| 1342 | 1355 | Tag::Image => { |
| @@ -1519,6 +1532,50 @@ mod tests { | ||
| 1519 | 1532 | .unwrap_or_default() |
| 1520 | 1533 | } |
| 1521 | 1534 | |
| 1535 | + /// The pointer resting on a reaction pill says so, the way it does on a | |
| 1536 | + /// face: a chip is where a tally is, and who is in the tally is what a | |
| 1537 | + /// hover is for. | |
| 1538 | + #[test] | |
| 1539 | + fn hovering_a_reaction_emits_hover() { | |
| 1540 | + let mut tree = Tree::default(); | |
| 1541 | + let pill = tree.new_node("reaction"); | |
| 1542 | + tree.set(pill, "emoji", Value::Str("\u{1f44d}".to_owned())); | |
| 1543 | + tree.set(pill, "count", Value::Num(2.0)); | |
| 1544 | + tree.append(tree.root(), pill); | |
| 1545 | + let theme = Theme::dark(); | |
| 1546 | + let ctx = egui::Context::default(); | |
| 1547 | + | |
| 1548 | + let mut input = egui::RawInput::default(); | |
| 1549 | + input.events.push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0))); | |
| 1550 | + let _ = ctx.run(input.clone(), |ctx| { | |
| 1551 | + egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme)); | |
| 1552 | + }); | |
| 1553 | + let _ = ctx.run(input, |ctx| { | |
| 1554 | + egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme)); | |
| 1555 | + }); | |
| 1556 | + | |
| 1557 | + let mut names = Vec::new(); | |
| 1558 | + while tree.poll() { | |
| 1559 | + names.push(tree.current().unwrap().name); | |
| 1560 | + } | |
| 1561 | + assert!(names.contains(&"hover"), "no hover from a pill: {names:?}"); | |
| 1562 | + | |
| 1563 | + // And a pill with children paints them beside the pointer. A window | |
| 1564 | + // node is the probe: painting one writes its width back, so the prop | |
| 1565 | + // appearing is the card having been drawn. | |
| 1566 | + let card = tree.new_node("window"); | |
| 1567 | + tree.append(pill, card); | |
| 1568 | + let mut input = egui::RawInput::default(); | |
| 1569 | + input.events.push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0))); | |
| 1570 | + let _ = ctx.run(input, |ctx| { | |
| 1571 | + egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme)); | |
| 1572 | + }); | |
| 1573 | + assert!( | |
| 1574 | + tree.get(card, "window-width").is_some(), | |
| 1575 | + "a hovered pill did not paint its card" | |
| 1576 | + ); | |
| 1577 | + } | |
| 1578 | + | |
| 1522 | 1579 | #[test] |
| 1523 | 1580 | fn root_exists_and_is_a_window() { |
| 1524 | 1581 | let tree = Tree::default(); |
| @@ -1337,6 +1337,19 @@ impl Tree { | |||
| 1337 | if response.clicked() { | 1337 | if response.clicked() { |
| 1338 | self.emit(id, "click", emoji, count as f64); | 1338 | self.emit(id, "click", emoji, count as f64); |
| 1339 | } | 1339 | } |
| 1340 | + // A pill answers the pointer the way a face does: the two | ||
| 1341 | + // edges of a hover, and children painted beside the pointer | ||
| 1342 | + // while it rests. A reaction is a tally, and who is in it is | ||
| 1343 | + // the thing the tally leaves out. | ||
| 1344 | + self.track_hover(id, &response); | ||
| 1345 | + if response.hovered() && !self.in_hover_panel && self.has_children(id) { | ||
| 1346 | + self.in_hover_panel = true; | ||
| 1347 | + response.show_tooltip_ui(|ui| { | ||
| 1348 | + ui.set_max_width(320.0); | ||
| 1349 | + self.paint_children(id, ui, theme); | ||
| 1350 | + }); | ||
| 1351 | + self.in_hover_panel = false; | ||
| 1352 | + } | ||
| 1340 | } | 1353 | } |
| 1341 | 1354 | ||
| 1342 | Tag::Image => { | 1355 | Tag::Image => { |
| @@ -1519,6 +1532,50 @@ mod tests { | |||
| 1519 | .unwrap_or_default() | 1532 | .unwrap_or_default() |
| 1520 | } | 1533 | } |
| 1521 | 1534 | ||
| 1535 | + /// The pointer resting on a reaction pill says so, the way it does on a | ||
| 1536 | + /// face: a chip is where a tally is, and who is in the tally is what a | ||
| 1537 | + /// hover is for. | ||
| 1538 | + #[test] | ||
| 1539 | + fn hovering_a_reaction_emits_hover() { | ||
| 1540 | + let mut tree = Tree::default(); | ||
| 1541 | + let pill = tree.new_node("reaction"); | ||
| 1542 | + tree.set(pill, "emoji", Value::Str("\u{1f44d}".to_owned())); | ||
| 1543 | + tree.set(pill, "count", Value::Num(2.0)); | ||
| 1544 | + tree.append(tree.root(), pill); | ||
| 1545 | + let theme = Theme::dark(); | ||
| 1546 | + let ctx = egui::Context::default(); | ||
| 1547 | + | ||
| 1548 | + let mut input = egui::RawInput::default(); | ||
| 1549 | + input.events.push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0))); | ||
| 1550 | + let _ = ctx.run(input.clone(), |ctx| { | ||
| 1551 | + egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme)); | ||
| 1552 | + }); | ||
| 1553 | + let _ = ctx.run(input, |ctx| { | ||
| 1554 | + egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme)); | ||
| 1555 | + }); | ||
| 1556 | + | ||
| 1557 | + let mut names = Vec::new(); | ||
| 1558 | + while tree.poll() { | ||
| 1559 | + names.push(tree.current().unwrap().name); | ||
| 1560 | + } | ||
| 1561 | + assert!(names.contains(&"hover"), "no hover from a pill: {names:?}"); | ||
| 1562 | + | ||
| 1563 | + // And a pill with children paints them beside the pointer. A window | ||
| 1564 | + // node is the probe: painting one writes its width back, so the prop | ||
| 1565 | + // appearing is the card having been drawn. | ||
| 1566 | + let card = tree.new_node("window"); | ||
| 1567 | + tree.append(pill, card); | ||
| 1568 | + let mut input = egui::RawInput::default(); | ||
| 1569 | + input.events.push(egui::Event::PointerMoved(egui::pos2(20.0, 20.0))); | ||
| 1570 | + let _ = ctx.run(input, |ctx| { | ||
| 1571 | + egui::CentralPanel::default().show(ctx, |ui| tree.paint(ui, &theme)); | ||
| 1572 | + }); | ||
| 1573 | + assert!( | ||
| 1574 | + tree.get(card, "window-width").is_some(), | ||
| 1575 | + "a hovered pill did not paint its card" | ||
| 1576 | + ); | ||
| 1577 | + } | ||
| 1578 | + | ||
| 1522 | #[test] | 1579 | #[test] |
| 1523 | fn root_exists_and_is_a_window() { | 1580 | fn root_exists_and_is_a_window() { |
| 1524 | let tree = Tree::default(); | 1581 | let tree = Tree::default(); |