From 122f9b12ae8f9c9d7d0572f8d5252ca2e1b9c0fe Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 9 Nov 2026 14:35:15 +0700 Subject: [PATCH] Reduce render allocations and redundant work in core components --- crates/core/src/alert.rs | 2 +- crates/core/src/combobox.rs | 8 +++--- crates/core/src/command_palette.rs | 8 +++--- crates/core/src/dropdown_menu.rs | 6 ++-- crates/core/src/field.rs | 12 ++++---- crates/core/src/input.rs | 6 +++- crates/core/src/menu.rs | 8 +++--- crates/core/src/segmented.rs | 46 +++++++++++++++++------------- crates/core/src/select.rs | 24 ++++++---------- crates/core/src/switch.rs | 5 +--- crates/core/src/tooltip.rs | 16 ++++++----- crates/core/tests/field_bench.rs | 45 +++++++++++++++++++++++------ crates/data/src/sidebar.rs | 6 ++-- crates/data/src/tabs.rs | 10 ++++--- crates/editor/src/code_view.rs | 11 +++---- crates/editor/src/diff_view.rs | 38 ++++++++++++++++-------- crates/editor/src/markdown.rs | 4 +-- crates/layout/src/dock.rs | 24 ++++++---------- crates/utils/src/lib.rs | 34 +++++++++++----------- 19 files changed, 174 insertions(+), 139 deletions(-) diff --git a/crates/core/src/alert.rs b/crates/core/src/alert.rs index 57eb08c..f74eb28 100644 --- a/crates/core/src/alert.rs +++ b/crates/core/src/alert.rs @@ -304,7 +304,7 @@ impl RenderOnce for AlertsView { let theme = cx.theme(); let enter_duration = theme.motion.overlay; - let entries: Vec = self.alerts.read(cx).entries().to_vec(); + let entries = self.alerts.read(cx).entries(); deferred( div() diff --git a/crates/core/src/combobox.rs b/crates/core/src/combobox.rs index 4bae38b..3367308 100644 --- a/crates/core/src/combobox.rs +++ b/crates/core/src/combobox.rs @@ -604,8 +604,8 @@ where div() .id(("option", row)) .debug_selector({ - let name = format!("{id}-option-{row}"); - move || name + let id = id.clone(); + move || format!("{id}-option-{row}") }) .flex() .items_center() @@ -681,8 +681,8 @@ where let panel = v_flex() .id((id.clone(), "panel")) .debug_selector({ - let name = format!("{id}-panel"); - move || name + let id = id.clone(); + move || format!("{id}-panel") }) .w(popup_width) .p_1() diff --git a/crates/core/src/command_palette.rs b/crates/core/src/command_palette.rs index da6a388..be2a201 100644 --- a/crates/core/src/command_palette.rs +++ b/crates/core/src/command_palette.rs @@ -335,8 +335,8 @@ impl Render for CommandPaletteState { h_flex() .id(("command", row)) .debug_selector({ - let name = format!("{id}-command-{row}"); - move || name + let id = id.clone(); + move || format!("{id}-command-{row}") }) .gap_2() .px_2() @@ -409,8 +409,8 @@ impl Render for CommandPaletteState { let panel = v_flex() .id((id.clone(), "panel")) .debug_selector({ - let name = format!("{id}-panel"); - move || name + let id = id.clone(); + move || format!("{id}-panel") }) .key_context(COMMAND_PALETTE_CONTEXT) .role(Role::Dialog) diff --git a/crates/core/src/dropdown_menu.rs b/crates/core/src/dropdown_menu.rs index d890356..8873e83 100644 --- a/crates/core/src/dropdown_menu.rs +++ b/crates/core/src/dropdown_menu.rs @@ -422,10 +422,10 @@ impl RenderOnce for DropdownMenu { let items = if self.open { self.items - .map(|build| build(window, cx)) - .unwrap_or_default() + .map(|build| Rc::from(build(window, cx))) + .unwrap_or_else(|| Rc::from([])) } else { - Vec::new() + Rc::from([]) }; if let Some(panel) = anchored_menu_panel( diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index e3ccc3a..bcad282 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -95,8 +95,8 @@ impl RenderOnce for Field { let title = div() .debug_selector({ - let name = format!("{id}-label"); - move || name + let id = id.clone(); + move || format!("{id}-label") }) .text_size(metrics.text_size) .child(self.label.clone()); @@ -105,16 +105,16 @@ impl RenderOnce for Field { .items_center() .gap_2() .debug_selector({ - let name = format!("{id}-control"); - move || name + let id = id.clone(); + move || format!("{id}-control") }) .children(self.control); let error = self.error.map(|error| { div() .debug_selector({ - let name = format!("{id}-error"); - move || name + let id = id.clone(); + move || format!("{id}-error") }) .text_size(metrics.caption_size) .text_color(error_color) diff --git a/crates/core/src/input.rs b/crates/core/src/input.rs index d7ebb8d..0577f27 100644 --- a/crates/core/src/input.rs +++ b/crates/core/src/input.rs @@ -2051,7 +2051,11 @@ impl RenderOnce for Input { canvas .with_animation( animation_id, - Animation::new(CARET_BLINK_INTERVAL * 2).repeat(), + // The blink needs four repaints per period, + // not the display's refresh rate. + Animation::new(CARET_BLINK_INTERVAL * 2) + .repeat() + .with_max_fps(4.0), |canvas, _| canvas, ) .into_any_element() diff --git a/crates/core/src/menu.rs b/crates/core/src/menu.rs index 70b5d27..2bffa8c 100644 --- a/crates/core/src/menu.rs +++ b/crates/core/src/menu.rs @@ -336,11 +336,11 @@ impl RenderOnce for Menu { let items = if self.open { match self.items { - Some(build) => build(window, cx), - None => Vec::new(), + Some(build) => Rc::from(build(window, cx)), + None => Rc::from([]), } } else { - Vec::new() + Rc::from([]) }; if let Some(panel) = anchored_menu_panel( @@ -372,7 +372,7 @@ pub(crate) fn anchored_menu_panel( cx: &mut App, id: &ElementId, open: bool, - items: Vec, + items: Rc<[MenuItem]>, initial_selection: Option, preselect: bool, on_change: Option, diff --git a/crates/core/src/segmented.rs b/crates/core/src/segmented.rs index 5142e9d..7e52a82 100644 --- a/crates/core/src/segmented.rs +++ b/crates/core/src/segmented.rs @@ -9,7 +9,7 @@ use gpui::{ }; use gpui_native_icons::{IconRole, icon}; use gpui_native_theme::{ActiveTheme as _, Size}; -use gpui_native_utils::{adjacent_selectable, focus_handle, focus_ring}; +use gpui_native_utils::{focus_handle, focus_ring}; use crate::common::{Sizable, StyledExt, h_flex}; @@ -64,7 +64,8 @@ where T: Clone + PartialEq + 'static, { id: ElementId, - options: Vec>, + /// Shared with the per-segment and key handlers, so captures are bumps. + options: Rc<[SegmentedOption]>, selected: T, a11y_label: Option, size: Size, @@ -82,7 +83,7 @@ where { Self { id: id.into(), - options: Vec::new(), + options: Rc::from([]), selected, a11y_label: None, size: Size::Small, @@ -163,12 +164,12 @@ where .options .iter() .position(|segment| segment.value == self.selected); + let option_count = self.options.len(); - let selectable: Vec = (0..self.options.len()).collect(); let on_change = self.on_change.clone(); let key_on_change = on_change.clone(); let key_options = self.options.clone(); - let key_selectable = selectable.clone(); + let options = self.options.clone(); let label = self .a11y_label @@ -176,14 +177,13 @@ where .unwrap_or_else(|| SharedString::from(self.id.to_string())); window.with_element_namespace(self.id.clone(), |window| { - let focus = focus_handle(&self.id, window, cx); - let geometry: Entity>>> = window.use_keyed_state("segmented-geometry", cx, |_, _| Vec::new()); let track_origin: Entity>> = window.use_keyed_state("segmented-track-origin", cx, |_, _| None); + let focus = focus_handle(&self.id, window, cx); let anim_config = SpringConfig::new(500., 35., 1.); let track_point = *track_origin.read(cx); @@ -197,12 +197,13 @@ where let segments = self .options - .into_iter() + .iter() .enumerate() .map(move |(index, segment)| { let is_selected = Some(index) == selected_index; let on_change = on_change.clone(); - let value = segment.value; + let options = options.clone(); + let content_color = if is_selected { foreground } else { @@ -228,7 +229,7 @@ where .when_some(segment.icon, |segment, role| { segment.child(icon(role).size(icon_size).color(content_color)) }) - .child(segment.label) + .child(segment.label.clone()) .child( canvas( { @@ -251,8 +252,9 @@ where move |_, window, cx| { if Some(index) != selected_index && let Some(on_change) = &on_change + && let Some(option) = options.get(index) { - on_change(value.clone(), window, cx); + on_change(option.value.clone(), window, cx); } } }) @@ -319,22 +321,26 @@ where .on_key_down({ move |event: &KeyDownEvent, window, cx| { let target = match event.keystroke.key.as_str() { - "right" => { - adjacent_selectable(&key_selectable, selected_index, true) - } - "left" => { - adjacent_selectable(&key_selectable, selected_index, false) - } - "home" => key_selectable.first().copied(), - "end" => key_selectable.last().copied(), + "right" => match selected_index { + Some(index) => { + index.checked_add(1).filter(|next| *next < option_count) + } + None => (option_count > 0).then_some(0), + }, + "left" => selected_index.and_then(|index| index.checked_sub(1)), + "home" => (option_count > 0).then_some(0), + "end" => option_count.checked_sub(1), _ => return, }; + if let Some(next) = target && Some(next) != selected_index && let Some(on_change) = &key_on_change + && let Some(option) = key_options.get(next) { - on_change(key_options[next].value.clone(), window, cx); + on_change(option.value.clone(), window, cx); } + cx.stop_propagation(); } }) diff --git a/crates/core/src/select.rs b/crates/core/src/select.rs index 2acea6c..cddaca5 100644 --- a/crates/core/src/select.rs +++ b/crates/core/src/select.rs @@ -206,24 +206,18 @@ where .position(|option| &option.value == value) }); - let trigger_options = self.options.clone(); - let trigger_selected = self.selected.clone(); - let trigger_placeholder = self.placeholder.clone(); + // The trigger's text is fixed at render time; only it moves into the closure. + let trigger_text = selected_index + .and_then(|index| self.options.get(index)) + .map(|option| option.label.clone()) + .unwrap_or_else(|| self.placeholder.clone()); + + let trigger_has_value = self.selected.is_some(); let trigger_id = self.id.clone(); let trigger_label = label.clone(); let trigger_style = self.style; let trigger = move |_window: &mut Window, _cx: &mut App| { - let text = trigger_selected - .as_ref() - .and_then(|value| { - trigger_options - .iter() - .find(|option| &option.value == value) - .map(|option| option.label.clone()) - }) - .unwrap_or_else(|| trigger_placeholder.clone()); - h_flex() .id((trigger_id.clone(), "select-trigger")) .debug_selector(|| "select-trigger".to_string()) @@ -240,7 +234,7 @@ where .border_color(rest_border) .bg(input) .text_size(metrics.text_size) - .text_color(if trigger_selected.is_some() { + .text_color(if trigger_has_value { foreground } else { muted_foreground @@ -249,7 +243,7 @@ where trigger.shadow(focus_ring(ring)) }) .hover(|style| style.bg(muted).border_color(hover_border)) - .child(text) + .child(trigger_text.clone()) .child( icon(IconRole::ChevronDown) .size(icon_size) diff --git a/crates/core/src/switch.rs b/crates/core/src/switch.rs index 23f1d27..f09c216 100644 --- a/crates/core/src/switch.rs +++ b/crates/core/src/switch.rs @@ -133,10 +133,7 @@ impl RenderOnce for Switch { div() .flex_none() .relative() - .debug_selector({ - let name = format!("{id}-track"); - move || name - }) + .debug_selector(move || format!("{id}-track")) .w(track_width) .h(track_height) .rounded(theme.radius.full) diff --git a/crates/core/src/tooltip.rs b/crates/core/src/tooltip.rs index 31c1e2b..26e7d4f 100644 --- a/crates/core/src/tooltip.rs +++ b/crates/core/src/tooltip.rs @@ -34,8 +34,8 @@ impl Tooltip { { let text = text.into(); let anchor_id = anchor_id.into(); - move |_window, cx| { - let anchor = anchor_bounds(&anchor_id, cx); + move |window, cx| { + let anchor = anchor_bounds(window.window_handle().window_id(), &anchor_id, cx); cx.new(|_| Self { text: text.clone(), anchor, @@ -92,11 +92,13 @@ impl Render for Tooltip { let origin = window.mouse_position() + point(px(1.), px(1.)); let viewport_height = window.viewport_size().height; - let height = anchor_bounds(&ElementId::from(PANEL_BOUNDS_ID), cx) - .map(|bounds| bounds.size.height) - .unwrap_or_else(|| { - text_size * 1.5 + window.rem_size() * 0.5 + BORDER_WIDTH * 2.0 - }); + let height = anchor_bounds( + window.window_handle().window_id(), + &ElementId::from(PANEL_BOUNDS_ID), + cx, + ) + .map(|bounds| bounds.size.height) + .unwrap_or_else(|| text_size * 1.5 + window.rem_size() * 0.5 + BORDER_WIDTH * 2.0); let below = viewport_height - spacing_unit - parent.bottom(); let above = parent.top() - spacing_unit; diff --git a/crates/core/tests/field_bench.rs b/crates/core/tests/field_bench.rs index dd299a5..5b07fe0 100644 --- a/crates/core/tests/field_bench.rs +++ b/crates/core/tests/field_bench.rs @@ -1,3 +1,5 @@ +use std::{cell::Cell, rc::Rc, time::Duration}; + use gpui::{ AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString, Styled as _, TestAppContext, VisualTestContext, Window, div, @@ -9,10 +11,12 @@ use gpui_native_core::input::{Input, InputState}; struct FieldHost { url: Entity, + renders: Rc>, } impl Render for FieldHost { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + self.renders.set(self.renders.get() + 1); let url_error = self .url .read(cx) @@ -34,15 +38,27 @@ impl Render for FieldHost { /// A field page in a real window; the url input is focused unless /// `focus` is false, which leaves the page truly idle. -fn field_host(focus: bool) -> (TestAppContext, Entity, VisualTestContext) { +fn field_host( + focus: bool, +) -> ( + TestAppContext, + Entity, + VisualTestContext, + Rc>, +) { let cx = TestAppContext::single(); cx.update(init); + let renders = Rc::new(Cell::new(0)); + let renders_for_host = renders.clone(); let (url, window) = cx.update(|cx| { let url = InputState::new(cx); let url_for_host = url.clone(); let window = cx .open_window(gpui::WindowOptions::default(), |_, cx| { - cx.new(|_| FieldHost { url: url_for_host }) + cx.new(|_| FieldHost { + url: url_for_host, + renders: renders_for_host.clone(), + }) }) .unwrap(); (url, window) @@ -55,12 +71,12 @@ fn field_host(focus: bool) -> (TestAppContext, Entity, VisualTestCon } window.draw(cx).clear(cx); }); - (cx, url, visual) + (cx, url, visual, renders) } #[test] fn an_idle_field_page_schedules_no_frames() { - let (_cx, _url, mut visual) = field_host(false); + let (_cx, _url, mut visual, _renders) = field_host(false); let frames = visual.update(|window, cx| window.simulate_next_frame(cx)); assert_eq!( frames, 0, @@ -68,12 +84,25 @@ fn an_idle_field_page_schedules_no_frames() { ); } +/// The caret blink is fps-capped: it must never demand a next frame, and its +/// redraws arrive on the executor timer at the blink cadence. #[test] -fn a_focused_field_page_schedules_frames_for_its_blink() { - let (_cx, _url, mut visual) = field_host(true); +fn a_focused_field_blink_is_fps_capped_and_still_redraws() { + let (cx, _url, mut visual, renders) = field_host(true); + let renders_after_focus = renders.get(); + let frames = visual.update(|window, cx| window.simulate_next_frame(cx)); + assert_eq!( + frames, 0, + "the blink must be fps-capped, not per-frame scheduled" + ); + + // The blink redraws through the executor timer (4 fps cap). + cx.executor().advance_clock(Duration::from_millis(251)); + cx.run_until_parked(); + visual.update(|window, cx| window.draw(cx).clear(cx)); assert!( - frames > 0, - "the focused caret's blink needs frames to redraw at the blink cadence" + renders.get() > renders_after_focus, + "the capped blink must still redraw its page" ); } diff --git a/crates/data/src/sidebar.rs b/crates/data/src/sidebar.rs index 2b8abfa..8d80bef 100644 --- a/crates/data/src/sidebar.rs +++ b/crates/data/src/sidebar.rs @@ -195,7 +195,6 @@ impl RenderOnce for Sidebar { .clone() .unwrap_or_else(|| SharedString::from(self.id.to_string())); - let id_text = self.id.to_string(); let scroll_area_id = (self.id.clone(), "entries"); // Entry paint reads the theme through this borrow. @@ -216,9 +215,10 @@ impl RenderOnce for Sidebar { }) .collect(); + let id = self.id; div() - .id(self.id) - .debug_selector(move || format!("sidebar-{id_text}")) + .id(id.clone()) + .debug_selector(move || format!("sidebar-{id}")) .size_full() .flex() .flex_col() diff --git a/crates/data/src/tabs.rs b/crates/data/src/tabs.rs index 9ae9d69..fe13093 100644 --- a/crates/data/src/tabs.rs +++ b/crates/data/src/tabs.rs @@ -307,8 +307,9 @@ fn render_tab( let mut strip_tab = div() .id((tab.key.clone(), "tab")) .debug_selector({ - let id = format!("{tabs_id}-tab-{}", tab.label); - move || id + let tabs_id = tabs_id.clone(); + let label = tab.label.clone(); + move || format!("{tabs_id}-tab-{}", label) }) .flex() .items_center() @@ -411,8 +412,9 @@ fn render_tab_close( .small() .tab_stop(false) .debug_selector({ - let id = format!("{tabs_id}-close-{}", tab.label); - move || id.clone() + let tabs_id = tabs_id.clone(); + let label = tab.label.clone(); + move || format!("{tabs_id}-close-{}", label) }) .tooltip(Tooltip::text("Close")) .accessibility_label(SharedString::from(format!("Close {}", tab.label))) diff --git a/crates/editor/src/code_view.rs b/crates/editor/src/code_view.rs index b0335a1..41d03aa 100644 --- a/crates/editor/src/code_view.rs +++ b/crates/editor/src/code_view.rs @@ -462,10 +462,10 @@ fn render_row( line_height: Pixels, cx: &App, ) -> gpui::AnyElement { - let line = code.code().lines.get(line_ix).cloned().unwrap_or_default(); + let line = code.code().lines.get(line_ix); let mut text = String::new(); - for span in &line.spans { + for span in line.map_or(&[][..], |line| line.spans.as_slice()) { text.push_str(&span.text); } @@ -473,7 +473,7 @@ fn render_row( let mut roles = vec![Role::Text; char_count]; let mut offset = 0usize; - for span in &line.spans { + for span in line.map_or(&[][..], |line| line.spans.as_slice()) { let chars = span.text.chars().count(); for slot in &mut roles[offset..offset + chars] { *slot = span.role; @@ -543,10 +543,7 @@ fn render_row( }; div() - .debug_selector({ - let name = format!("{id}-row-{line_ix}"); - move || name - }) + .debug_selector(move || format!("{id}-row-{line_ix}")) .id(("code-row", line_ix)) .w_full() .h(line_height) diff --git a/crates/editor/src/diff_view.rs b/crates/editor/src/diff_view.rs index 13e67cb..1395e75 100644 --- a/crates/editor/src/diff_view.rs +++ b/crates/editor/src/diff_view.rs @@ -237,6 +237,8 @@ pub fn diff_rows(old: &str, new: &str, language: Language) -> Vec { pub struct DiffViewState { id: ElementId, rows: Vec, + /// The row whose width drives horizontal scroll extent, `rows`-derived. + widest_row: Option, options: DiffViewOptions, /// Drives both scroll axes of the virtualized row list. handle: UniformListScrollHandle, @@ -251,13 +253,32 @@ impl DiffViewState { O: AsRef, N: AsRef, { - Self { + let mut state = Self { id: id.into(), - rows: diff_rows(old.as_ref(), new.as_ref(), language), + rows: Vec::new(), + widest_row: None, options: DiffViewOptions::default(), handle: UniformListScrollHandle::new(), a11y_label: None, - } + }; + state.set_rows(old, new, language); + state + } + + /// Replaces the rows, recomputing `widest_row` with them. + fn set_rows(&mut self, old: O, new: N, language: Language) + where + O: AsRef, + N: AsRef, + { + self.rows = diff_rows(old.as_ref(), new.as_ref(), language); + self.widest_row = self + .rows + .iter() + .enumerate() + .filter(|(_, row)| row.kind != DiffRowKind::Hunk) + .max_by_key(|(_, row)| row.char_count()) + .map(|(ix, _)| ix); } /// Sets how changed rows are marked. @@ -284,7 +305,7 @@ impl DiffViewState { O: AsRef, N: AsRef, { - self.rows = diff_rows(old.as_ref(), new.as_ref(), language); + self.set_rows(old, new, language); self.handle.scroll_to_item(0, ScrollStrategy::Top); // The replaced versions rarely share the old width; start at the left. let offset = ScrollbarHandle::offset(&self.handle); @@ -328,14 +349,7 @@ impl Render for DiffViewState { let state = cx.entity(); let row_count = self.rows.len(); let options = self.options; - - let widest_row = self - .rows - .iter() - .enumerate() - .filter(|(_, row)| row.kind != DiffRowKind::Hunk) - .max_by_key(|(_, row)| row.char_count()) - .map(|(ix, _)| ix); + let widest_row = self.widest_row; let mut rows = uniform_list((id.clone(), "rows"), row_count, { move |range, _window, cx| { diff --git a/crates/editor/src/markdown.rs b/crates/editor/src/markdown.rs index 6c5b0e5..0e54087 100644 --- a/crates/editor/src/markdown.rs +++ b/crates/editor/src/markdown.rs @@ -784,8 +784,8 @@ fn block_frame( let theme = cx.theme(); let content = div() .debug_selector({ - let name = SharedString::from(format!("{id}")); - move || name.to_string() + let id = id.clone(); + move || id.to_string() }) .w_full() .flex() diff --git a/crates/layout/src/dock.rs b/crates/layout/src/dock.rs index 46d5847..ab4fd5e 100644 --- a/crates/layout/src/dock.rs +++ b/crates/layout/src/dock.rs @@ -969,15 +969,10 @@ fn render_pane( .map(|tab| tab.view.clone()); // The cache keeps an un-notified panel from re-rendering on dock frames. + let content_id = SharedString::from(format!("{dock_id}-content-{}", pane.key)); let content = div() - .id(SharedString::from(format!( - "{dock_id}-content-{}", - pane.key - ))) - .debug_selector({ - let id = SharedString::from(format!("{dock_id}-content-{}", pane.key)); - move || id.to_string() - }) + .id(content_id.clone()) + .debug_selector(move || content_id.to_string()) .flex_1() .min_h_0() .overflow_hidden() @@ -999,6 +994,7 @@ fn render_pane( let pane = pane.clone(); let titlebar = *titlebar; let padding_x = metrics.dense_padding_x; + move |window, cx| { let selected = pane .active @@ -1065,15 +1061,11 @@ fn render_pane( } }); + let bar_id = SharedString::from(format!("{dock_id}-pane-{}-bar", pane.key)); + h_flex() - .id(SharedString::from(format!( - "{dock_id}-pane-{}-bar", - pane.key - ))) - .debug_selector({ - let id = SharedString::from(format!("{dock_id}-pane-{}-bar", pane.key)); - move || id.to_string() - }) + .id(bar_id.clone()) + .debug_selector(move || bar_id.to_string()) .flex_none() .w_full() .h(titlebar.height) diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index fd350ea..05f5992 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,7 +1,8 @@ use gpui::prelude::FluentBuilder as _; use gpui::{ App, Bounds, BoxShadow, Div, ElementId, FocusHandle, Global, Hsla, IntoElement, Pixels, - Refineable as _, StatefulInteractiveElement, StyleRefinement, Styled, Window, div, px, + Refineable as _, StatefulInteractiveElement, StyleRefinement, Styled, Window, WindowId, div, + px, }; use gpui_native_theme::ActiveTheme as _; use std::collections::HashMap; @@ -36,39 +37,36 @@ pub fn focus_handle(id: &ElementId, window: &mut Window, cx: &mut App) -> FocusH .clone() } -/// The painted bounds of elements that tooltips anchor to, keyed by element id. -#[derive(Clone, Default)] -struct AnchorBoundsRegistry(HashMap>); +/// The painted bounds of elements that tooltips anchor to, per window and element id. +#[derive(Default)] +struct AnchorBoundsRegistry(HashMap<(WindowId, ElementId), Bounds>); impl Global for AnchorBoundsRegistry {} impl AnchorBoundsRegistry { - fn record(&mut self, id: ElementId, bounds: Bounds) { - self.0.insert(id, bounds); + fn record(&mut self, window: WindowId, id: ElementId, bounds: Bounds) { + self.0.insert((window, id), bounds); } - fn get(&self, id: &ElementId) -> Option> { - self.0.get(id).copied() + fn get(&self, window: WindowId, id: &ElementId) -> Option> { + self.0.get(&(window, id.clone())).copied() } } -/// The last bounds an element painted at. -pub fn anchor_bounds(id: &ElementId, cx: &mut App) -> Option> { +/// The last bounds an element painted at, in the given window. +pub fn anchor_bounds(window: WindowId, id: &ElementId, cx: &App) -> Option> { cx.try_global::() - .and_then(|registry| registry.get(id)) + .and_then(|registry| registry.get(window, id)) } /// A canvas prepaint closure recording the element's painted bounds. pub fn record_anchor_bounds( id: ElementId, ) -> impl Fn(Bounds, &mut Window, &mut App) + 'static { - move |bounds, _window, cx| { - let mut registry = cx - .try_global::() - .cloned() - .unwrap_or_default(); - registry.record(id.clone(), bounds); - cx.set_global(registry); + move |bounds, window, cx| { + let window_id = window.window_handle().window_id(); + cx.default_global::() + .record(window_id, id.clone(), bounds); } }