From 51ade2903221687beda54da9bb424dd56047ec7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 27 Sep 2026 02:23:05 -0400 Subject: [PATCH] =?UTF-8?q?Primitives=20=C2=B7=20a=20raise=20keeps=20its?= =?UTF-8?q?=20state=20under=20its=20owner's=20id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A raise inside another was namespaced by its place in the layer's queue, so one that came before it moved its id path and rebuilt its keyed state and focus: an open date popup would lose its month when a tip appeared. raise now takes its owner's id and draws under it, and float and float_height pass the owner's id on from every call site. A test holds a focused box in a raise while another comes before it. --- AGENTS.md | 2 +- TASKS.md | 1 + src/documents/link.rs | 4 +- src/feedback/toast.rs | 3 +- src/forms/cascade.rs | 1 + src/forms/date/period.rs | 1 + src/forms/date/picker.rs | 2 + src/forms/date/time.rs | 2 + src/forms/date/week.rs | 1 + src/forms/options.rs | 14 ++++--- src/layout/floating.rs | 4 +- src/menus/menu.rs | 24 +++++++++-- src/menus/pie.rs | 1 + src/messaging/status.rs | 1 + src/navigation/menu.rs | 9 ++++- src/overlays/dialog.rs | 1 + src/overlays/floats.rs | 1 + src/overlays/hover.rs | 9 ++++- src/overlays/lightbox.rs | 9 +++-- src/overlays/popover.rs | 9 ++++- src/overlays/spotlight.rs | 10 ++++- src/primitives/backdrop.rs | 3 +- src/primitives/layer.rs | 31 +++++++------- src/primitives/tests.rs | 83 +++++++++++++++++++++++++++++++++++++- src/primitives/tooltip.rs | 2 + 25 files changed, 188 insertions(+), 40 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f1ae2bd..ae9fca2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,7 +48,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - gpui's `Window::dispatch_event` returns a private type, so the gallery scripts mouse input by posting `NSEvent`s to its own queue. - `hover()` needs an element id. - gpui applies hover and group-hover styles once an element has a hitbox, in prepaint and paint, but text takes its color at layout, so a hover cannot recolor text. What shows on hover rests `invisible()` and turns `visible()` through `group_hover` (`chat::ConversationItem`, `messaging::ChatMessage`). -- gpui 0.2.2 panics on a deferred draw inside another. Whatever draws over the page goes through `primitives::raise`: at the top it defers; inside something raised it lays out on its own and draws last there, so a list opened in a dialog lies over the dialog. `scripts/check.sh` refuses gpui's `deferred` anywhere else. +- gpui 0.2.2 panics on a deferred draw inside another. Whatever draws over the page goes through `primitives::raise`: at the top it defers; inside something raised it lays out on its own and draws last there, so a list opened in a dialog lies over the dialog. A raise takes its owner's id and draws under it, so its keyed state and focus hold while other raises come and go. `scripts/check.sh` refuses gpui's `deferred` anywhere else. - `occlude()` blocks the pointer for everything painted before it, its own ancestors too. Put it on the outermost box that should stop the pointer, as the toast stack does, or the ancestors' hover never fires. - A drag that pans, turns or slides goes through gpui's drag, whose moves reach the owner in or out of its box; `on_mouse_move` stops at the box's edge. gpui sends a drag's moves to every listener of its type, so each payload carries its owner's `EntityId` and handlers check it. - gpui 0.2.2 paints pictures without a transform, so a picture turns by its pixels: `media::turn` turns decoded frames by quarter turns off the main thread, an asset keyed by the picture and the turn. diff --git a/TASKS.md b/TASKS.md index 7c4bcb2..9932f57 100644 --- a/TASKS.md +++ b/TASKS.md @@ -349,3 +349,4 @@ Per-component lines live in `tasks/`. Tags there: | T29c | 1 | FAIL | a long chip ran past its tag field at 280px | | T29c | 2 | PASS | | | T29d | 1 | FAIL | the time dialog's rule for a time ahead of now had no test; its test found that a picker opened in a dialog panicked on a deferred draw inside another | +| T29d | 2 | FAIL | a raise inside a raise took its place in the queue as its namespace, so one appearing before it rebuilt its state and dropped its focus | diff --git a/src/documents/link.rs b/src/documents/link.rs index 0b1edc8..e203ecb 100644 --- a/src/documents/link.rs +++ b/src/documents/link.rs @@ -198,7 +198,9 @@ impl RenderOnce for LinkEditor { }), ), ); - div().id(self.id).child(float(anchor, 1, panel, window, cx)) + div() + .id(self.id.clone()) + .child(float(self.id, anchor, 1, panel, window, cx)) } } diff --git a/src/feedback/toast.rs b/src/feedback/toast.rs index 30d7abd..03de386 100644 --- a/src/feedback/toast.rs +++ b/src/feedback/toast.rs @@ -225,7 +225,7 @@ impl RenderOnce for ToastViewport { cards.push(card(&self.id, ix, &self.toaster, window, cx)); } let stack = div() - .id(self.id) + .id(self.id.clone()) .occlude() .flex() .flex_col() @@ -235,6 +235,7 @@ impl RenderOnce for ToastViewport { }) .children(cards); raise( + (self.id, "raised"), anchored() .position(point(viewport.width - margin, viewport.height - margin)) .anchor(Corner::BottomRight) diff --git a/src/forms/cascade.rs b/src/forms/cascade.rs index 7bd8e14..9a78061 100644 --- a/src/forms/cascade.rs +++ b/src/forms/cascade.rs @@ -265,6 +265,7 @@ impl RenderOnce for Cascader { }); let close = state.clone(); float( + self.id.clone(), anchor, deepest, surface((self.id.clone(), "columns"), cx) diff --git a/src/forms/date/period.rs b/src/forms/date/period.rs index f108865..106a9fe 100644 --- a/src/forms/date/period.rs +++ b/src/forms/date/period.rs @@ -228,6 +228,7 @@ fn period_picker( }; let grid = period_grid(&key, build(shown), grid, window, cx); float( + key.clone(), anchor, 6, surface((key.clone(), "popup"), cx) diff --git a/src/forms/date/picker.rs b/src/forms/date/picker.rs index 62f38a2..7d10e1f 100644 --- a/src/forms/date/picker.rs +++ b/src/forms/date/picker.rs @@ -246,6 +246,7 @@ impl RenderOnce for DatePicker { } let (escape, done) = (close.clone(), close.clone()); float( + id.clone(), anchor, CALENDAR_ROWS, surface((id.clone(), "popup"), cx) @@ -347,6 +348,7 @@ impl RenderOnce for DateRangePicker { } let (hover, pick, escape) = (state.clone(), state.clone(), close.clone()); float( + id.clone(), anchor, CALENDAR_ROWS, surface((id.clone(), "popup"), cx) diff --git a/src/forms/date/time.rs b/src/forms/date/time.rs index 3d641d9..2ad3597 100644 --- a/src/forms/date/time.rs +++ b/src/forms/date/time.rs @@ -256,6 +256,7 @@ impl RenderOnce for TimePicker { let columns = time_columns(&id, time, step, commit.clone(), window, cx); let out = close.clone(); float( + id.clone(), anchor, CALENDAR_ROWS, surface((id.clone(), "popup"), cx) @@ -368,6 +369,7 @@ impl RenderOnce for DateTimePicker { ); let out = close.clone(); float( + id.clone(), anchor, CALENDAR_ROWS, surface((id.clone(), "popup"), cx) diff --git a/src/forms/date/week.rs b/src/forms/date/week.rs index d069242..0ec03ec 100644 --- a/src/forms/date/week.rs +++ b/src/forms/date/week.rs @@ -77,6 +77,7 @@ impl RenderOnce for WeekPicker { } let (escape, out, on_change) = (close.clone(), close.clone(), on_change.clone()); float( + id.clone(), anchor, CALENDAR_ROWS, surface((id.clone(), "popup"), cx) diff --git a/src/forms/options.rs b/src/forms/options.rs index 2182f93..635f395 100644 --- a/src/forms/options.rs +++ b/src/forms/options.rs @@ -166,8 +166,9 @@ pub(crate) fn surface(id: impl Into, cx: &App) -> Stateful
{ .text_size(theme.text_size(TextSize::Sm)) } -/// Floats `content` of about `rows` rows under `anchor`, or over it when only above has room. +/// Floats `content` of about `rows` rows under `anchor`, or over it when only above has room; `id` is its owner's. pub(crate) fn float( + id: impl Into, anchor: Bounds, rows: usize, content: impl IntoElement, @@ -178,7 +179,7 @@ pub(crate) fn float( let rem = window.rem_size(); let row = theme.control_height(ControlSize::Md).to_pixels(rem); let height = (row * (rows + 1) as f32).min(theme.list_max_height().to_pixels(rem)); - float_height(anchor, height, content, window, cx) + float_height(id, anchor, height, content, window, cx) } /// Whether `height` goes over `anchor`: it does not fit below, and above has more room. @@ -187,8 +188,9 @@ pub(crate) fn opens_up(anchor: Bounds, height: Pixels, viewport: Pixels) height > below && anchor.top() > below } -/// Floats `content` of a known `height` under `anchor`, or over it when only above has room. +/// Floats `content` of a known `height` under `anchor`, or over it when only above has room; `id` is its owner's. pub(crate) fn float_height( + id: impl Into, anchor: Bounds, height: Pixels, content: impl IntoElement, @@ -220,7 +222,7 @@ pub(crate) fn float_height( }), ) }; - raise(placed.snap_to_window()) + raise(id, placed.snap_to_window()) .with_priority(1) .into_any_element() } @@ -298,7 +300,7 @@ impl Popup<'_> { }) }) }); - let list = surface(self.id, cx) + let list = surface(self.id.clone(), cx) .debug_selector(|| "option-list".into()) .min_w(theme.tooltip_max_width()) .when(anchor.size.width > Pixels::ZERO, |list| { @@ -319,7 +321,7 @@ impl Popup<'_> { self.scroll.zip(self.reveal), |list, (handle, (ix, done))| list.child(revealer(handle, ix, done)), ); - float(anchor, self.rows.len(), list, window, cx) + float(self.id, anchor, self.rows.len(), list, window, cx) } } diff --git a/src/layout/floating.rs b/src/layout/floating.rs index e2ac477..1826199 100644 --- a/src/layout/floating.rs +++ b/src/layout/floating.rs @@ -101,7 +101,7 @@ impl RenderOnce for FloatingPanel { }; let panel = self .base - .id(self.id) + .id(self.id.clone()) .flex() .flex_col() .overflow_hidden() @@ -149,7 +149,7 @@ impl RenderOnce for FloatingPanel { .child(self.title), ) .child(div().p_3().children(self.body)); - raise(anchored().position(at).child(panel)) + raise((self.id, "raised"), anchored().position(at).child(panel)) } } diff --git a/src/menus/menu.rs b/src/menus/menu.rs index cf47410..900f802 100644 --- a/src/menus/menu.rs +++ b/src/menus/menu.rs @@ -382,10 +382,25 @@ pub(crate) fn hang( } }); let placed = match spot { - Spot::Under => float(state.read(cx).anchor, menu.entries.len(), root, window, cx), - Spot::At(point) => raise(anchored().position(point).snap_to_window().child( - root.with_animation((id.clone(), "in"), enter.clone(), |root, t| root.opacity(t)), - )) + Spot::Under => float( + (id.clone(), "panel"), + state.read(cx).anchor, + menu.entries.len(), + root, + window, + cx, + ), + Spot::At(point) => raise( + (id.clone(), "panel"), + anchored() + .position(point) + .snap_to_window() + .child( + root.with_animation((id.clone(), "in"), enter.clone(), |root, t| { + root.opacity(t) + }), + ), + ) .with_priority(1) .into_any_element(), }; @@ -393,6 +408,7 @@ pub(crate) fn hang( let row = state.read(cx).levels[level - 1].row; (row != Bounds::default()).then(|| { raise( + (id.clone(), format!("level-{level}")), anchored().position(row.top_right()).snap_to_window().child( draw::panel(id, menu, state, level, &close, None, cx) .mt_neg_1() diff --git a/src/menus/pie.rs b/src/menus/pie.rs index 2be488b..ee336f3 100644 --- a/src/menus/pie.rs +++ b/src/menus/pie.rs @@ -303,6 +303,7 @@ impl RenderOnce for PieMenu { }); region.child( raise( + (self.id.clone(), "raised"), anchored() .position(point(Pixels::ZERO, Pixels::ZERO)) .child( diff --git a/src/messaging/status.rs b/src/messaging/status.rs index 558a9cf..f18cd20 100644 --- a/src/messaging/status.rs +++ b/src/messaging/status.rs @@ -197,6 +197,7 @@ impl RenderOnce for StatusSetter { done(window, cx); }); float_height( + popup_id.clone(), anchor, height, surface((popup_id, "popup"), cx) diff --git a/src/navigation/menu.rs b/src/navigation/menu.rs index 028d495..0432b88 100644 --- a/src/navigation/menu.rs +++ b/src/navigation/menu.rs @@ -236,7 +236,14 @@ impl RenderOnce for NavigationMenu { .on_mouse_down_out(move |_, _, cx| show(&close, None, cx)) .children(links); let rows = entries[entry].1.len().div_ceil(2) * 2; - float(state.read(cx).anchors[entry], rows, content, window, cx) + float( + (self.id.clone(), format!("menu-{entry}")), + state.read(cx).anchors[entry], + rows, + content, + window, + cx, + ) }); let (keys, key_entries, enter) = (state.clone(), entries.clone(), pick); div() diff --git a/src/overlays/dialog.rs b/src/overlays/dialog.rs index 60d08e4..8989b50 100644 --- a/src/overlays/dialog.rs +++ b/src/overlays/dialog.rs @@ -271,6 +271,7 @@ impl RenderOnce for Dialog { let viewport = window.viewport_size(); let top = cx.theme().titlebar_height().to_pixels(window.rem_size()); return raise( + (self.id.clone(), "raised"), anchored().position(point(Pixels::ZERO, top)).child( div() .w(viewport.width) diff --git a/src/overlays/floats.rs b/src/overlays/floats.rs index 4e6a02b..7b6ef74 100644 --- a/src/overlays/floats.rs +++ b/src/overlays/floats.rs @@ -69,6 +69,7 @@ impl RenderOnce for FloatingToolbar { let lift = cx.theme().float_gap().to_pixels(window.rem_size()); host.child( raise( + (self.id.clone(), "raised"), anchored() .position(anchor.origin - point(gpui::Pixels::ZERO, lift)) .anchor(Corner::BottomLeft) diff --git a/src/overlays/hover.rs b/src/overlays/hover.rs index f1c54be..14ce835 100644 --- a/src/overlays/hover.rs +++ b/src/overlays/hover.rs @@ -132,6 +132,13 @@ impl RenderOnce for HoverCard { .size_full(), ); let (anchor, height) = (state.read(cx).host, state.read(cx).height); - host.child(float_height(anchor, height, card, window, cx)) + host.child(float_height( + self.id.clone(), + anchor, + height, + card, + window, + cx, + )) } } diff --git a/src/overlays/lightbox.rs b/src/overlays/lightbox.rs index 32091c2..d192219 100644 --- a/src/overlays/lightbox.rs +++ b/src/overlays/lightbox.rs @@ -291,8 +291,11 @@ impl RenderOnce for Lightbox { ), ), ); - raise(anchored().position(Point::default()).child(stage)) - .with_priority(1) - .into_any_element() + raise( + (self.id.clone(), "raised"), + anchored().position(Point::default()).child(stage), + ) + .with_priority(1) + .into_any_element() } } diff --git a/src/overlays/popover.rs b/src/overlays/popover.rs index c90f4db..8f66e08 100644 --- a/src/overlays/popover.rs +++ b/src/overlays/popover.rs @@ -176,6 +176,13 @@ impl RenderOnce for Popover { .left_0() .size_full(), ); - host.child(float_height(anchor, height, panel, window, cx)) + host.child(float_height( + self.id.clone(), + anchor, + height, + panel, + window, + cx, + )) } } diff --git a/src/overlays/spotlight.rs b/src/overlays/spotlight.rs index e45ffc6..bc9af13 100644 --- a/src/overlays/spotlight.rs +++ b/src/overlays/spotlight.rs @@ -193,13 +193,21 @@ fn light( div() .child( raise( + (id.clone(), "raised"), anchored() .position(Point::default()) .child(div().w(viewport.width).h(viewport.height).child(frame)), ) .with_priority(1), ) - .child(float_height(target, card_height, card, window, cx)) + .child(float_height( + (id.clone(), "card"), + target, + card_height, + card, + window, + cx, + )) .into_any_element() } diff --git a/src/primitives/backdrop.rs b/src/primitives/backdrop.rs index 1cd59d6..000af60 100644 --- a/src/primitives/backdrop.rs +++ b/src/primitives/backdrop.rs @@ -76,12 +76,13 @@ impl RenderOnce for Backdrop { scrim.on_mouse_down(MouseButton::Left, move |_, window, cx| dismiss(window, cx)) }) .with_animation( - self.id, + self.id.clone(), Animation::new(fade).with_easing(motion::ease_out_cubic), |scrim, t| scrim.opacity(t), ); raise( + (self.id, "raised"), anchored().position(Point::default()).child( div() .relative() diff --git a/src/primitives/layer.rs b/src/primitives/layer.rs index 726bb13..ff81b14 100644 --- a/src/primitives/layer.rs +++ b/src/primitives/layer.rs @@ -5,8 +5,8 @@ use gpui::{ InspectorElementId, IntoElement, LayoutId, Pixels, Point, Position, Style, Window, size, }; -/// What was raised while a layer prepaints its content, each with where it sits and its priority. -type Queue = Vec<(AnyElement, Point, usize)>; +/// What was raised while a layer prepaints its content, each by its owner's id, with where it sits and its priority. +type Queue = Vec<(ElementId, AnyElement, Point, usize)>; thread_local! { static LAYERS: RefCell> = const { RefCell::new(Vec::new()) }; @@ -16,12 +16,13 @@ fn nested() -> bool { LAYERS.with_borrow(|layers| !layers.is_empty()) } -/// Draws `child` over the page, as gpui's `deferred` does. gpui cannot defer a draw inside a deferred one, so what is raised inside something raised is laid out and drawn last within it: a list opened in a dialog lies over the dialog. -pub fn raise(child: impl IntoElement) -> Raised { +/// Draws `child` over the page, as gpui's `deferred` does. gpui cannot defer a draw inside a deferred one, so what is raised inside something raised is laid out and drawn last within it, under `id`: a list opened in a dialog lies over the dialog and keeps its state. +pub fn raise(id: impl Into, child: impl IntoElement) -> Raised { let layer = Layer { child: child.into_any_element(), }; Raised { + id: id.into(), child: Some(layer.into_any_element()), priority: 0, nested: false, @@ -30,6 +31,7 @@ pub fn raise(child: impl IntoElement) -> Raised { /// An element drawn over the page; see [`raise`]. pub struct Raised { + id: ElementId, child: Option, priority: usize, nested: bool, @@ -100,7 +102,7 @@ impl Element for Raised { let queue = layers .last_mut() .expect("a nested element prepaints in its layer"); - queue.push((child, bounds.origin, self.priority)); + queue.push((self.id.clone(), child, bounds.origin, self.priority)); }); } @@ -141,7 +143,7 @@ fn collect(f: impl FnOnce()) -> Queue { impl Element for Layer { type RequestLayoutState = (); - type PrepaintState = Vec; + type PrepaintState = Vec<(ElementId, AnyElement)>; fn id(&self) -> Option { None @@ -171,11 +173,11 @@ impl Element for Layer { _: &mut (), window: &mut Window, cx: &mut App, - ) -> Vec { + ) -> Vec<(ElementId, AnyElement)> { let mut raised = collect(|| { self.child.prepaint(window, cx); }); - raised.sort_by_key(|(_, _, priority)| *priority); + raised.sort_by_key(|(_, _, _, priority)| *priority); let viewport = window.viewport_size(); let space = size( AvailableSpace::Definite(viewport.width), @@ -183,12 +185,11 @@ impl Element for Layer { ); raised .into_iter() - .enumerate() - .map(|(ix, (mut element, at, _))| { - window.with_element_namespace(("raised", ix), |window| { + .map(|(id, mut element, at, _)| { + window.with_element_namespace(id.clone(), |window| { element.prepaint_as_root(at, space, window, cx) }); - element + (id, element) }) .collect() } @@ -199,13 +200,13 @@ impl Element for Layer { _: Option<&InspectorElementId>, _: Bounds, _: &mut (), - raised: &mut Vec, + raised: &mut Vec<(ElementId, AnyElement)>, window: &mut Window, cx: &mut App, ) { self.child.paint(window, cx); - for (ix, element) in raised.iter_mut().enumerate() { - window.with_element_namespace(("raised", ix), |window| element.paint(window, cx)); + for (id, element) in raised.iter_mut() { + window.with_element_namespace(id.clone(), |window| element.paint(window, cx)); } } } diff --git a/src/primitives/tests.rs b/src/primitives/tests.rs index 83235fa..fe7c2ef 100644 --- a/src/primitives/tests.rs +++ b/src/primitives/tests.rs @@ -1,6 +1,8 @@ +use std::{cell::RefCell, rc::Rc}; + use gpui::{ - Context, IntoElement, Modifiers, ParentElement, Render, Styled, TestAppContext, Window, - anchored, div, point, prelude::*, px, + App, Context, Entity, FocusHandle, IntoElement, Modifiers, ParentElement, Render, RenderOnce, + Styled, TestAppContext, Window, anchored, div, point, prelude::*, px, }; use super::raise; @@ -27,11 +29,13 @@ impl Render for Stack { .size(px(200.0)) .on_click(move |_, _, cx| cover.update(cx, |stack, _| stack.heard.push("cover"))); div().size_full().child(raise( + "sheet", anchored().position(point(px(0.0), px(0.0))).child( div() .relative() .size(px(200.0)) .child(raise( + "button", anchored().position(point(px(50.0), px(50.0))).child(button), )) .child(sheet), @@ -51,3 +55,78 @@ fn what_is_raised_inside_a_raise_lies_over_it(cx: &mut TestAppContext) { ["inner"] ); } + +/// A focusable box whose focus handle lives in keyed state; each draw tells the test which one it drew. +#[derive(IntoElement)] +struct Held { + seen: Rc>>>, +} + +impl RenderOnce for Held { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let kept = window.use_keyed_state("kept", cx, |_, cx| cx.focus_handle()); + self.seen.borrow_mut().push(kept.clone()); + let handle = kept.read(cx).clone(); + div().id("held").size(px(40.0)).track_focus(&handle) + } +} + +/// A raised sheet holding a raised tip that comes and goes, then a raised box that must stay itself. +struct Nest { + tip: bool, + seen: Rc>>>, +} + +impl Render for Nest { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let tip = self.tip.then(|| { + raise( + "tip", + anchored() + .position(point(px(0.0), px(0.0))) + .child(div().child("tip")), + ) + }); + let held = Held { + seen: self.seen.clone(), + }; + div().size_full().child(raise( + "sheet", + anchored().position(point(px(0.0), px(0.0))).child( + div().size(px(200.0)).children(tip).child(raise( + "held", + anchored().position(point(px(50.0), px(50.0))).child(held), + )), + ), + )) + } +} + +#[gpui::test] +fn a_raise_keeps_its_state_while_another_comes_before_it(cx: &mut TestAppContext) { + cx.update(Theme::init); + let seen = Rc::new(RefCell::new(Vec::new())); + let (view, cx) = cx.add_window_view(|_, _| Nest { + tip: false, + seen: seen.clone(), + }); + cx.run_until_parked(); + let first = seen.borrow().last().cloned().expect("the box drew"); + let handle = first.read_with(cx, |handle, _| handle.clone()); + cx.update(|window, _| window.focus(&handle)); + view.update(cx, |nest, cx| { + nest.tip = true; + cx.notify(); + }); + cx.run_until_parked(); + let last = seen.borrow().last().cloned().expect("the box drew again"); + assert_eq!( + last.entity_id(), + first.entity_id(), + "the box kept its state" + ); + assert!( + cx.update(|window, _| handle.is_focused(window)), + "the box kept focus" + ); +} diff --git a/src/primitives/tooltip.rs b/src/primitives/tooltip.rs index 4c0396c..6e43a81 100644 --- a/src/primitives/tooltip.rs +++ b/src/primitives/tooltip.rs @@ -139,6 +139,7 @@ impl RenderOnce for TooltipTrigger { let (delay, follow) = (self.delay, self.follow); let content = self.content; + let id = self.id.clone(); div() .id(self.id) .child(self.child) @@ -177,6 +178,7 @@ impl RenderOnce for TooltipTrigger { let body = content(window, cx); let below = cx.theme().cursor_offset().to_pixels(window.rem_size()); trigger.child(raise( + (id, "raised"), anchored() .position(mouse + point(Pixels::ZERO, below)) .snap_to_window()