Fix shared element state across component instances
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -322,3 +322,136 @@ fn disabled_trigger_ignores_clicks() {
|
||||
assert!(changes.borrow().is_empty());
|
||||
assert!(visual.debug_bounds("menu-panel").is_none());
|
||||
}
|
||||
|
||||
struct PairHarness {
|
||||
open_a: bool,
|
||||
open_b: bool,
|
||||
changes_a: Changes,
|
||||
changes_b: Changes,
|
||||
activated_a: Activated,
|
||||
activated_b: Activated,
|
||||
}
|
||||
|
||||
impl Render for PairHarness {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (changes_a, changes_b) = (self.changes_a.clone(), self.changes_b.clone());
|
||||
let (activated_a, activated_b) = (self.activated_a.clone(), self.activated_b.clone());
|
||||
|
||||
let menu_a = DropdownMenu::new("pair-a")
|
||||
.label("Alpha")
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.open(self.open_a)
|
||||
.on_open_change({
|
||||
let view = cx.entity();
|
||||
let changes = changes_a.clone();
|
||||
move |next, _, cx| {
|
||||
changes.borrow_mut().push(next);
|
||||
view.update(cx, |harness, cx| {
|
||||
harness.open_a = next;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.items(move |_, _| {
|
||||
vec![
|
||||
MenuItem::new("png").handler({
|
||||
let activated = activated_a.clone();
|
||||
move |_, _| activated.borrow_mut().push("png")
|
||||
}),
|
||||
MenuItem::new("pdf").handler({
|
||||
let activated = activated_a.clone();
|
||||
move |_, _| activated.borrow_mut().push("pdf")
|
||||
}),
|
||||
]
|
||||
});
|
||||
|
||||
let menu_b = DropdownMenu::new("pair-b")
|
||||
.label("Beta")
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.open(self.open_b)
|
||||
.on_open_change({
|
||||
let view = cx.entity();
|
||||
let changes = changes_b.clone();
|
||||
move |next, _, cx| {
|
||||
changes.borrow_mut().push(next);
|
||||
view.update(cx, |harness, cx| {
|
||||
harness.open_b = next;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.items(move |_, _| {
|
||||
vec![
|
||||
MenuItem::new("zip").handler({
|
||||
let activated = activated_b.clone();
|
||||
move |_, _| activated.borrow_mut().push("zip")
|
||||
}),
|
||||
MenuItem::new("7z").handler({
|
||||
let activated = activated_b.clone();
|
||||
move |_, _| activated.borrow_mut().push("7z")
|
||||
}),
|
||||
]
|
||||
});
|
||||
|
||||
div().size_full().flex().gap_2().child(menu_a).child(menu_b)
|
||||
}
|
||||
}
|
||||
|
||||
fn pair_window() -> (
|
||||
VisualTestContext,
|
||||
AnyWindowHandle,
|
||||
Changes,
|
||||
Changes,
|
||||
Activated,
|
||||
Activated,
|
||||
) {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(Theme::init);
|
||||
let changes_a = Rc::new(RefCell::new(Vec::new()));
|
||||
let changes_b = Rc::new(RefCell::new(Vec::new()));
|
||||
let activated_a = Rc::new(RefCell::new(Vec::new()));
|
||||
let activated_b = Rc::new(RefCell::new(Vec::new()));
|
||||
let (changes_a_for_window, changes_b_for_window) = (changes_a.clone(), changes_b.clone());
|
||||
let (activated_a_for_window, activated_b_for_window) =
|
||||
(activated_a.clone(), activated_b.clone());
|
||||
let window = cx.add_window(move |_, _| PairHarness {
|
||||
open_a: false,
|
||||
open_b: false,
|
||||
changes_a: changes_a_for_window,
|
||||
changes_b: changes_b_for_window,
|
||||
activated_a: activated_a_for_window,
|
||||
activated_b: activated_b_for_window,
|
||||
});
|
||||
let window = window.into();
|
||||
cx.update_window(window, |_, window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
})
|
||||
.unwrap();
|
||||
(
|
||||
VisualTestContext::from_window(window, &cx),
|
||||
window,
|
||||
changes_a,
|
||||
changes_b,
|
||||
activated_a,
|
||||
activated_b,
|
||||
)
|
||||
}
|
||||
|
||||
/// With a closed sibling dropdown on screen, a pointer-opened menu must still
|
||||
/// arm rows with the keyboard: the menu's open/selection state is per
|
||||
/// instance, not shared keyed state.
|
||||
#[test]
|
||||
fn sibling_dropdowns_keep_independent_menu_state() {
|
||||
let (mut visual, window, changes_a, changes_b, activated_a, activated_b) = pair_window();
|
||||
|
||||
// Menu A sits at the window's top-left; menu B stays closed beside it.
|
||||
click(&mut visual, point(px(30.), px(14.)));
|
||||
assert_eq!(*changes_a.borrow(), vec![true]);
|
||||
draw(&mut visual, window);
|
||||
|
||||
visual.simulate_keystrokes("down enter");
|
||||
assert_eq!(*activated_a.borrow(), vec!["png"]);
|
||||
assert_eq!(*changes_a.borrow(), vec![true, false]);
|
||||
assert!(changes_b.borrow().is_empty(), "B must stay closed");
|
||||
assert!(activated_b.borrow().is_empty());
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ impl Render for UniformShell {
|
||||
.h_full()
|
||||
.w_full(),
|
||||
)
|
||||
.child(Scrollbar::vertical(self.handle.clone())),
|
||||
.child(Scrollbar::vertical("rows", self.handle.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ impl Render for ListShell {
|
||||
.h_full()
|
||||
.w_full(),
|
||||
)
|
||||
.child(Scrollbar::vertical(self.state.clone())),
|
||||
.child(Scrollbar::vertical("rows", self.state.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -271,3 +271,99 @@ fn dragging_the_thumb_drives_a_list_state_and_reports_the_drag() {
|
||||
"the drag ends with the mouse"
|
||||
);
|
||||
}
|
||||
|
||||
struct TwoShell {
|
||||
left: UniformListScrollHandle,
|
||||
right: UniformListScrollHandle,
|
||||
}
|
||||
|
||||
impl Render for TwoShell {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.h(px(VIEWPORT_HEIGHT))
|
||||
.w(px(300.))
|
||||
.child(
|
||||
uniform_list("left-rows", ROWS, |range, _, _| {
|
||||
range
|
||||
.map(|ix| div().h(px(ROW_HEIGHT)).child(format!("row {ix}")))
|
||||
.collect()
|
||||
})
|
||||
.track_scroll(&self.left)
|
||||
.h_full()
|
||||
.w_full(),
|
||||
)
|
||||
.child(Scrollbar::vertical("left", self.left.clone())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.h(px(VIEWPORT_HEIGHT))
|
||||
.w(px(300.))
|
||||
.child(
|
||||
uniform_list("right-rows", ROWS, |range, _, _| {
|
||||
range
|
||||
.map(|ix| div().h(px(ROW_HEIGHT)).child(format!("row {ix}")))
|
||||
.collect()
|
||||
})
|
||||
.track_scroll(&self.right)
|
||||
.h_full()
|
||||
.w_full(),
|
||||
)
|
||||
.child(Scrollbar::vertical("right", self.right.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn two_shell() -> (
|
||||
TestAppContext,
|
||||
VisualTestContext,
|
||||
UniformListScrollHandle,
|
||||
UniformListScrollHandle,
|
||||
) {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(init);
|
||||
let (left, right) = (
|
||||
UniformListScrollHandle::new(),
|
||||
UniformListScrollHandle::new(),
|
||||
);
|
||||
let (left_for_view, right_for_view) = (left.clone(), right.clone());
|
||||
let window = cx.add_window(move |_, _| TwoShell {
|
||||
left: left_for_view,
|
||||
right: right_for_view,
|
||||
});
|
||||
let mut visual = VisualTestContext::from_window(window.into(), &cx);
|
||||
visual.update(|window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
});
|
||||
(cx, visual, left, right)
|
||||
}
|
||||
|
||||
/// Dragging one scrollbar's thumb must never move a sibling scrollbar: the
|
||||
/// drag state is keyed per scrollbar id, not shared across instances.
|
||||
#[test]
|
||||
fn sibling_scrollbars_drag_independently() {
|
||||
let (cx, mut visual, left, right) = two_shell();
|
||||
let metrics = Metrics::measure(&cx, &left);
|
||||
assert!(metrics.max_offset > px(0.));
|
||||
|
||||
press(&mut visual, metrics.thumb_center());
|
||||
drag_to(&mut visual, metrics.full_drag_target());
|
||||
release(&mut visual, metrics.full_drag_target());
|
||||
|
||||
let offset = left.offset().y;
|
||||
assert!(
|
||||
(offset.abs() - metrics.max_offset).abs() < px(0.5),
|
||||
"the left drag reaches the end, got {offset}"
|
||||
);
|
||||
assert_eq!(
|
||||
right.offset().y,
|
||||
px(0.),
|
||||
"the left drag leaked into the right scrollbar"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,3 +153,132 @@ fn arrow_keys_step_the_value_while_focused() {
|
||||
|
||||
assert_eq!(*changes.borrow(), vec![0.0, 10.0, 20.0, 10.0]);
|
||||
}
|
||||
|
||||
struct TwoSliders {
|
||||
changes_a: Rc<RefCell<Vec<f32>>>,
|
||||
changes_b: Rc<RefCell<Vec<f32>>>,
|
||||
value_a: f32,
|
||||
value_b: f32,
|
||||
}
|
||||
|
||||
impl Render for TwoSliders {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let (changes_a, changes_b) = (self.changes_a.clone(), self.changes_b.clone());
|
||||
div()
|
||||
.size_full()
|
||||
.child(
|
||||
div().absolute().top_0().left_0().w(px(200.)).child(
|
||||
Slider::new("slider-a", self.value_a)
|
||||
.min(0.0)
|
||||
.max(100.0)
|
||||
.step(10.0)
|
||||
.on_change({
|
||||
let view = cx.entity();
|
||||
let changes_a = changes_a.clone();
|
||||
move |next, _, cx| {
|
||||
changes_a.borrow_mut().push(next);
|
||||
view.update(cx, |harness, cx| {
|
||||
harness.value_a = next;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().absolute().top(px(40.)).left_0().w(px(200.)).child(
|
||||
Slider::new("slider-b", self.value_b)
|
||||
.min(0.0)
|
||||
.max(100.0)
|
||||
.step(10.0)
|
||||
.on_change({
|
||||
let view = cx.entity();
|
||||
move |next, _, cx| {
|
||||
changes_b.borrow_mut().push(next);
|
||||
view.update(cx, |harness, cx| {
|
||||
harness.value_b = next;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn two_slider_window() -> (
|
||||
VisualTestContext,
|
||||
Rc<RefCell<Vec<f32>>>,
|
||||
Rc<RefCell<Vec<f32>>>,
|
||||
) {
|
||||
let mut cx = TestAppContext::single();
|
||||
cx.update(Theme::init);
|
||||
let changes_a = Rc::new(RefCell::new(Vec::new()));
|
||||
let changes_b = Rc::new(RefCell::new(Vec::new()));
|
||||
let (changes_a_for_window, changes_b_for_window) = (changes_a.clone(), changes_b.clone());
|
||||
let window = cx.add_window(move |_, _| TwoSliders {
|
||||
changes_a: changes_a_for_window,
|
||||
changes_b: changes_b_for_window,
|
||||
value_a: 0.0,
|
||||
value_b: 0.0,
|
||||
});
|
||||
let window = window.into();
|
||||
cx.update_window(window, |_, window, cx| {
|
||||
window.draw(cx).clear(cx);
|
||||
})
|
||||
.unwrap();
|
||||
(
|
||||
VisualTestContext::from_window(window, &cx),
|
||||
changes_a,
|
||||
changes_b,
|
||||
)
|
||||
}
|
||||
|
||||
/// Dragging one slider must never drive a sibling slider: the drag flag is
|
||||
/// per instance, not shared keyed state.
|
||||
#[test]
|
||||
fn two_sliders_drag_independently() {
|
||||
let (mut cx, changes_a, changes_b) = two_slider_window();
|
||||
|
||||
// Slider A occupies y 0..16, slider B starts at y 40.
|
||||
cx.simulate_mouse_down(
|
||||
point(px(100.), px(10.)),
|
||||
MouseButton::Left,
|
||||
Modifiers::default(),
|
||||
);
|
||||
cx.simulate_mouse_move(
|
||||
point(px(120.), px(10.)),
|
||||
MouseButton::Left,
|
||||
Modifiers::default(),
|
||||
);
|
||||
cx.simulate_mouse_up(
|
||||
point(px(120.), px(10.)),
|
||||
MouseButton::Left,
|
||||
Modifiers::default(),
|
||||
);
|
||||
assert_eq!(*changes_a.borrow(), vec![50.0, 60.0]);
|
||||
assert!(changes_b.borrow().is_empty(), "A's drag leaked into B");
|
||||
|
||||
cx.simulate_mouse_down(
|
||||
point(px(100.), px(50.)),
|
||||
MouseButton::Left,
|
||||
Modifiers::default(),
|
||||
);
|
||||
cx.simulate_mouse_move(
|
||||
point(px(40.), px(50.)),
|
||||
MouseButton::Left,
|
||||
Modifiers::default(),
|
||||
);
|
||||
cx.simulate_mouse_up(
|
||||
point(px(40.), px(50.)),
|
||||
MouseButton::Left,
|
||||
Modifiers::default(),
|
||||
);
|
||||
assert_eq!(*changes_b.borrow(), vec![50.0, 20.0]);
|
||||
assert_eq!(
|
||||
*changes_a.borrow(),
|
||||
vec![50.0, 60.0],
|
||||
"B's drag leaked into A"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user