chore: reduce component overhead (#3)
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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<InputState>,
|
||||
renders: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Render for FieldHost {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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<InputState>, VisualTestContext) {
|
||||
fn field_host(
|
||||
focus: bool,
|
||||
) -> (
|
||||
TestAppContext,
|
||||
Entity<InputState>,
|
||||
VisualTestContext,
|
||||
Rc<Cell<usize>>,
|
||||
) {
|
||||
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<InputState>, 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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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