Fix shared element state across component instances

This commit is contained in:
2026-11-09 14:04:25 +07:00
parent f022f08947
commit 73f7dc31d4
20 changed files with 489 additions and 136 deletions
+6 -2
View File
@@ -510,8 +510,12 @@ where
if state.highlight.is_some() {
state.highlight = None;
} else {
let query = state.query.read(cx).value().to_string();
state.refilter(&query);
if state.selected.is_some() {
state.refilter("");
} else {
let query = state.query.read(cx).value().to_string();
state.refilter(&query);
}
state.highlight = (!state.visible.is_empty()).then_some(0usize);
}
cx.notify();
+25 -39
View File
@@ -1,14 +1,13 @@
//! The command palette: a modal query field over the application's actions.
use std::cell::RefCell;
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, Bounds, Context, ElementId, Entity, InteractiveElement as _, IntoElement,
KeyBinding, MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role,
ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Window,
actions, canvas, div, px, relative,
AnyElement, App, Context, ElementId, Entity, InteractiveElement as _, IntoElement, KeyBinding,
MouseButton, ParentElement as _, Render, Role, ScrollHandle, SharedString,
StatefulInteractiveElement as _, Styled as _, Subscription, Window, actions, canvas, deferred,
div, px, relative,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, Size};
@@ -322,6 +321,7 @@ impl Render for CommandPaletteState {
let id = self.id.clone();
let state = cx.entity();
let query_focus = self.query.read(cx).focus_handle();
let rows: Vec<AnyElement> = self
.matches
@@ -406,9 +406,6 @@ impl Render for CommandPaletteState {
.into_any_element()
};
let panel_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
let query_focus = self.query.read(cx).focus_handle();
let panel = v_flex()
.id((id.clone(), "panel"))
.debug_selector({
@@ -460,20 +457,14 @@ impl Render for CommandPaletteState {
)
.child(rows)
.child(
canvas(
{
let cell = panel_bounds.clone();
move |bounds, _window, _cx| *cell.borrow_mut() = Some(bounds)
},
{
let focus = query_focus.clone();
move |_, _, window, cx| {
if !focus.contains_focused(window, cx) {
window.focus(&focus, cx);
}
canvas(|_, _, _| {}, {
let focus = query_focus.clone();
move |_, _, window, cx| {
if !focus.contains_focused(window, cx) {
window.focus(&focus, cx);
}
},
)
}
})
.absolute()
.inset_0(),
);
@@ -485,28 +476,23 @@ impl Render for CommandPaletteState {
.occlude()
.on_mouse_down(MouseButton::Left, {
let state = state.clone();
let panel_bounds = panel_bounds.clone();
move |event: &MouseDownEvent, _window, cx| {
let on_panel = panel_bounds
.borrow()
.is_some_and(|bounds| bounds.contains(&event.position));
if on_panel {
return;
}
move |_, _window, cx| {
state.update(cx, |state, cx| state.close(cx));
cx.stop_propagation();
}
});
div()
.absolute()
.inset_0()
.flex()
.justify_center()
.items_start()
.pt_10()
.child(scrim)
.child(panel)
.into_any_element()
deferred(
div()
.absolute()
.inset_0()
.flex()
.justify_center()
.items_start()
.pt_10()
.child(scrim)
.child(panel),
)
.into_any_element()
}
}
+2 -1
View File
@@ -207,7 +207,8 @@ impl RenderOnce for DropdownMenu {
let a11y_label = self.a11y_label.or_else(|| self.label.clone());
let focus = focus_handle(&self.id, window, cx).tab_stop(self.tab_stop);
// How the menu was last opened, read by the frame that mounts the panel.
let keyboard_open = window.use_keyed_state("dropdown-open-origin", cx, |_, _| false);
let keyboard_open =
window.use_keyed_state((self.id.clone(), "dropdown-open-origin"), cx, |_, _| false);
let on_open: ChangeHandler = {
let focus = focus.clone();
+6 -2
View File
@@ -1799,7 +1799,9 @@ impl EntityInputHandler for InputState {
) -> Option<usize> {
let layout = self.layout.borrow();
let layout = layout.as_ref()?;
Some(real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len()))
let index =
real_index(&layout.mask_map, layout.index_at(window_point)).min(self.text.len());
Some(utf8_range_to_utf16(&self.text, index..index).start)
}
fn text_length_utf16(
@@ -2516,7 +2518,9 @@ fn utf16_to_utf8_offset(text: &str, utf16_offset: usize) -> usize {
/// Converts a UTF-8 byte range to a UTF-16 range.
fn utf8_range_to_utf16(text: &str, range: Range<usize>) -> Range<usize> {
utf16_to_utf8_offset(text, range.start)..utf16_to_utf8_offset(text, range.end)
let start = text[..range.start].encode_utf16().count();
let end = start + text[range.start..range.end].encode_utf16().count();
start..end
}
/// Converts a UTF-16 range to a UTF-8 byte range.
+4 -3
View File
@@ -382,7 +382,7 @@ pub(crate) fn anchored_menu_panel(
chrome: &MenuChrome,
enter_duration: std::time::Duration,
) -> Option<AnyElement> {
let was_open = window.use_keyed_state("menu-was-open", cx, |_, _| false);
let was_open = window.use_keyed_state((id.clone(), "menu-was-open"), cx, |_, _| false);
if !open {
was_open.update(cx, |was_open, _| *was_open = false);
@@ -392,8 +392,9 @@ pub(crate) fn anchored_menu_panel(
let on_change = on_change.clone();
// Keyboard focus rides the panel across frames
let focus = gpui_native_utils::focus_handle(&(id.clone(), "panel").into(), window, cx);
let scroll = window.use_keyed_state("menu-scroll", cx, |_, _| ScrollHandle::new());
let selection = window.use_keyed_state("menu-selection", cx, |_, _| None);
let scroll =
window.use_keyed_state((id.clone(), "menu-scroll"), cx, |_, _| ScrollHandle::new());
let selection = window.use_keyed_state((id.clone(), "menu-selection"), cx, |_, _| None);
let selected = if *was_open.read(cx) {
*selection.read(cx)
+14 -22
View File
@@ -128,33 +128,35 @@ pub struct Scrollbar {
axis: Axis,
handle: Rc<dyn ScrollbarHandle>,
visibility: ScrollbarVisibility,
key: ElementId,
id: ElementId,
}
impl Scrollbar {
/// A vertical scrollbar driven by `handle`.
pub fn vertical<H>(handle: H) -> Self
/// A vertical scrollbar named by `id`, driven by `handle`.
pub fn vertical<I, H>(id: I, handle: H) -> Self
where
I: Into<ElementId>,
H: ScrollbarHandle,
{
Self {
axis: Axis::Vertical,
handle: Rc::new(handle),
visibility: ScrollbarVisibility::default(),
key: ElementId::Name("scrollbar".into()),
id: id.into(),
}
}
/// A horizontal scrollbar driven by `handle`.
pub fn horizontal<H>(handle: H) -> Self
/// A horizontal scrollbar named by `id`, driven by `handle`.
pub fn horizontal<I, H>(id: I, handle: H) -> Self
where
I: Into<ElementId>,
H: ScrollbarHandle,
{
Self {
axis: Axis::Horizontal,
handle: Rc::new(handle),
visibility: ScrollbarVisibility::default(),
key: ElementId::Name("scrollbar".into()),
id: id.into(),
}
}
@@ -163,12 +165,6 @@ impl Scrollbar {
self.visibility = visibility;
self
}
/// Names this scrollbar's state.
pub fn key(mut self, key: impl Into<ElementId>) -> Self {
self.key = key.into();
self
}
}
impl RenderOnce for Scrollbar {
@@ -177,8 +173,8 @@ impl RenderOnce for Scrollbar {
let handle = self.handle;
let axis = self.axis;
let visibility = self.visibility;
let key = self.key;
move |_, _, window, cx| paint_scrollbar(&handle, axis, visibility, &key, window, cx)
let id = self.id;
move |_, _, window, cx| paint_scrollbar(&handle, axis, visibility, &id, window, cx)
})
.absolute()
.inset_0()
@@ -192,7 +188,7 @@ fn paint_scrollbar(
handle: &Rc<dyn ScrollbarHandle>,
axis: Axis,
visibility: ScrollbarVisibility,
key: &ElementId,
id: &ElementId,
window: &mut Window,
cx: &mut App,
) {
@@ -217,12 +213,10 @@ fn paint_scrollbar(
ScrollbarVisibility::Always => false,
ScrollbarVisibility::Autohide => true,
};
let autohide_state =
autohide.then(|| Autohide::new(scrollbar_state_key("autohide", key), window, cx));
autohide.then(|| Autohide::new(scrollbar_state_key("autohide", id), window, cx));
let track_length = viewport_along - inset * 2.0;
let (thumb_length, thumb_travel) = thumb_geometry(
viewport_along,
viewport_along + max_offset,
@@ -267,9 +261,7 @@ fn paint_scrollbar(
}
};
let drag = window.use_keyed_state(scrollbar_state_key("thumb-drag", key), cx, |_, _| {
Drag::None
});
let drag = window.use_keyed_state(scrollbar_state_key("thumb-drag", id), cx, |_, _| Drag::None);
let dragging = drag.read(cx).is_some();
let alpha = match &autohide_state {
+3 -7
View File
@@ -135,16 +135,12 @@ impl RenderOnce for Slider {
0.0
};
// The track bounds, recorded by the overlay canvas each frame,
// turn pointer positions into values inside the listeners.
let bounds_cell = Rc::new(Cell::new(None::<Bounds<Pixels>>));
// Dragging crosses frames, so it lives in keyed element state,
// a per-render flag would reset when the owner's update repaints.
let dragging = window.use_keyed_state("slider-dragging", cx, |_, _| false);
// A held handle gives the arrow-key listener something to check focus against,
// and the click-to-focus behavior a target.
let focus_handle = gpui_native_utils::focus_handle(&self.id, window, cx);
let dragging =
window.use_keyed_state((self.id.clone(), "slider-dragging"), cx, |_, _| false);
let set_value: ValueSetter = {
let bounds_cell = bounds_cell.clone();
let on_change = self.on_change.clone();