From 2dbbda21f4335c236d384d15f9b42d7846e071d4 Mon Sep 17 00:00:00 2001 From: Floyd Wang Date: Wed, 24 Jun 2026 11:52:54 +0800 Subject: [PATCH 1/4] plot: Fix tooltip stretched vertically by stray `top_0()` on `base` --- crates/ui/src/plot/tooltip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui/src/plot/tooltip.rs b/crates/ui/src/plot/tooltip.rs index 1c05fa24b..c307d9f9a 100644 --- a/crates/ui/src/plot/tooltip.rs +++ b/crates/ui/src/plot/tooltip.rs @@ -257,7 +257,7 @@ impl Tooltip { /// Create a tooltip whose box follows the cursor at `cursor` within a `within`-sized plot. pub fn new(cursor: Point, within: Size) -> Self { Self { - base: v_flex().top_0(), + base: v_flex(), gap: px(0.), cross_line: None, dots: None, From 2828d733d7eb267969e5237751895276dbc244f0 Mon Sep 17 00:00:00 2001 From: Floyd Wang Date: Wed, 24 Jun 2026 13:53:00 +0800 Subject: [PATCH 2/4] plot: Defer tooltip overlay so it paints above later siblings The `IntoPlot` derive painted the tooltip overlay inline during the plot's own `paint`, so any sibling drawn after the plot (e.g. a chart card's footer text) covered the part of the tooltip box that overflows the plot bounds. Wrap the overlay in `gpui::deferred(...)` before `prepaint_as_root` so it paints in the deferred pass, on top of the rest of the tree. `prepaint_as_root` keeps the plot-origin offset, so positioning is unchanged. Co-Authored-By: Claude Opus 4.8 --- crates/macros/src/derive_into_plot.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/macros/src/derive_into_plot.rs b/crates/macros/src/derive_into_plot.rs index 22560fda3..6f65aebff 100644 --- a/crates/macros/src/derive_into_plot.rs +++ b/crates/macros/src/derive_into_plot.rs @@ -88,12 +88,18 @@ pub fn derive_into_plot(input: TokenStream) -> TokenStream { // Pass the live cursor so the tooltip box can follow it; the crosshair and // dots in `state` stay snapped to the data point by `tooltip_state`. - let Some(mut overlay) = + let Some(overlay) = ::tooltip(self, &state, position, bounds, window, cx) else { return None; }; + // Defer the overlay so it paints above sibling content drawn after the plot + // (e.g. a chart card's footer text). The tooltip box can extend past the plot + // bounds; without deferral those later siblings would cover the overflow. + let mut overlay = gpui::IntoElement::into_any_element( + gpui::deferred(overlay), + ); overlay.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx); Some(overlay) } From 44ee822c501457d7b621c31e6b45c9dd85df432b Mon Sep 17 00:00:00 2001 From: Floyd Wang Date: Wed, 24 Jun 2026 14:36:17 +0800 Subject: [PATCH 3/4] plot: Use `?` instead of `let...else` in `IntoPlot` codegen The derive generated `let Some(_) = ... else { return None }`, which trips `clippy::question_mark` in downstream crates that derive `IntoPlot` (the generated code is attributed to the call site via `quote!`). Emit `?` instead so the generated code is lint-clean regardless of clippy version. Co-Authored-By: Claude Opus 4.8 --- crates/macros/src/derive_into_plot.rs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/crates/macros/src/derive_into_plot.rs b/crates/macros/src/derive_into_plot.rs index 6f65aebff..a5f4740da 100644 --- a/crates/macros/src/derive_into_plot.rs +++ b/crates/macros/src/derive_into_plot.rs @@ -73,26 +73,15 @@ pub fn derive_into_plot(input: TokenStream) -> TokenStream { cx: &mut gpui::App, ) -> Self::PrepaintState { // No id => tooltips disabled => behave exactly like a non-interactive plot. - let Some(global_id) = global_id else { - return None; - }; + let global_id = global_id?; // Read the cursor position recorded by the previous frame's mouse handler. - let Some(position) = Self::__plot_tooltip_cursor(global_id, window).get() else { - return None; - }; - let Some(state) = ::tooltip_state(self, position, bounds, cx) - else { - return None; - }; + let position = Self::__plot_tooltip_cursor(global_id, window).get()?; + let state = ::tooltip_state(self, position, bounds, cx)?; // Pass the live cursor so the tooltip box can follow it; the crosshair and // dots in `state` stay snapped to the data point by `tooltip_state`. - let Some(overlay) = - ::tooltip(self, &state, position, bounds, window, cx) - else { - return None; - }; + let overlay = ::tooltip(self, &state, position, bounds, window, cx)?; // Defer the overlay so it paints above sibling content drawn after the plot // (e.g. a chart card's footer text). The tooltip box can extend past the plot From 2147d5dfa84995efdf45bf046e6c8aaf760ddfaa Mon Sep 17 00:00:00 2001 From: Floyd Wang Date: Wed, 24 Jun 2026 17:07:43 +0800 Subject: [PATCH 4/4] plot: Confine `CrossLine::both()` axes independently `CrossLine` shared a single `start`/`length` across both axes, so a `both()` crosshair confined with `height()`/`span()` wrongly clipped the horizontal line to the vertical extent. Give each axis its own span and add `width()`/`h_span()` so the vertical and horizontal lines confine independently. Co-Authored-By: Claude Opus 4.8 --- crates/ui/src/plot/tooltip.rs | 53 ++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/crates/ui/src/plot/tooltip.rs b/crates/ui/src/plot/tooltip.rs index c307d9f9a..6a3bffc49 100644 --- a/crates/ui/src/plot/tooltip.rs +++ b/crates/ui/src/plot/tooltip.rs @@ -30,10 +30,12 @@ impl CrossLineAxis { #[derive(IntoElement)] pub struct CrossLine { point: Point, - /// Start offset along the cross axis (vertical line: y; horizontal line: x). - start: f32, - /// Length along the cross axis; `None` spans the full extent. - length: Option, + /// Span `(start, length)` of the vertical line along the y axis; `length` of `None` + /// spans the full height. + vertical: (f32, Option), + /// Span `(start, length)` of the horizontal line along the x axis; `length` of `None` + /// spans the full width. + horizontal: (f32, Option), /// Band thickness perpendicular to the line (solid band mode only). thickness: Pixels, /// `true` (default) draws a dashed hairline; `false` a solid band of `thickness`. @@ -45,8 +47,8 @@ impl CrossLine { pub fn new(point: Point) -> Self { Self { point, - start: 0., - length: None, + vertical: (0., None), + horizontal: (0., None), thickness: px(1.), dashed: true, direction: Default::default(), @@ -74,17 +76,29 @@ impl CrossLine { self } - /// Set the length of the cross line along its axis (from the start edge). + /// Set the vertical line's length along the y axis (from the top edge). pub fn height(mut self, height: f32) -> Self { - self.length = Some(height); + self.vertical.1 = Some(height); self } - /// Confine the cross line to `[start, start + length]` along its axis (vertical - /// line: y; horizontal line: x), so it stays within the plot area. + /// Set the horizontal line's length along the x axis (from the left edge). + pub fn width(mut self, width: f32) -> Self { + self.horizontal.1 = Some(width); + self + } + + /// Confine the vertical line to `[start, start + length]` along the y axis, so it + /// stays within the plot area. pub fn span(mut self, start: f32, length: f32) -> Self { - self.start = start; - self.length = Some(length); + self.vertical = (start, Some(length)); + self + } + + /// Confine the horizontal line to `[start, start + length]` along the x axis, so it + /// stays within the plot area. + pub fn h_span(mut self, start: f32, length: f32) -> Self { + self.horizontal = (start, Some(length)); self } } @@ -107,21 +121,28 @@ impl CrossLine { }; // The dashed hairline is a zero-width strip drawn entirely by its 1px border. let thickness = if self.dashed { px(0.) } else { self.thickness }; + // Each axis carries its own span so a `both` crosshair can confine the vertical + // and horizontal lines independently. + let (start, length) = if vertical { + self.vertical + } else { + self.horizontal + }; let el = div().absolute(); let el = if vertical { el.left(self.point.x - thickness * 0.5) .w(thickness) - .top(px(self.start)) - .map(|el| match self.length { + .top(px(start)) + .map(|el| match length { Some(length) => el.h(px(length)), None => el.h_full(), }) } else { el.top(self.point.y - thickness * 0.5) .h(thickness) - .left(px(self.start)) - .map(|el| match self.length { + .left(px(start)) + .map(|el| match length { Some(length) => el.w(px(length)), None => el.w_full(), })