Forms: lists reveal their cursor once laid out

- Select, Combobox and MultiSelect opened a long list at its top, the
  choice out of view: gpui skips a scroll asked before the list's first
  frame. Picker and Suggestions share forms::options::reveal, which Popup
  runs from a canvas inside the list
- test: a_long_list_opens_with_its_choice_in_view
- AGENTS: the scroll rule names the reveal
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 07:25:13 -04:00
1 parent 7f4bd8415c
commit 247f836073
7 files changed
+129 -28

No files matched your search

+73 -4
View File
@@ -1,11 +1,15 @@
use gpui::{
AppContext as _, Context, Entity, IntoElement, KeyUpEvent, Keystroke, Render, SharedString,
TestAppContext, VisualTestContext, Window,
AppContext as _, Context, Entity, IntoElement, KeyUpEvent, Keystroke, Modifiers, ParentElement,
Render, SharedString, Styled, TestAppContext, VisualTestContext, Window, div, point, px,
};
use super::setup;
use crate::forms::{
Checkbox, Choice, Combobox, MultiSelect, RadioGroup, Rating, Select, Slider, TextInput,
use crate::{
forms::{
Checkbox, Choice, Combobox, MultiSelect, RadioGroup, Rating, Select, Slider, TextInput,
},
motion,
theme::{ActiveTheme, ControlSize, Theme},
};
fn abc(middle_off: bool) -> [Choice; 3] {
@@ -83,6 +87,71 @@ fn select_opens_moves_and_picks_from_the_keyboard(cx: &mut TestAppContext) {
assert_eq!(view.read_with(cx, |view, _| view.chosen.clone()), "b");
}
/// A select of thirty rows, low in the window, and the rows it picked.
struct Long {
chosen: SharedString,
picks: Vec<SharedString>,
}
impl Render for Long {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
let rows = (0..30).map(|ix| Choice::new(format!("r{ix:02}"), format!("Row {ix:02}")));
div().pt(px(200.0)).w(px(240.0)).child(
Select::new("long", rows)
.selected(self.chosen.clone())
.on_change(move |value, _, cx| {
view.update(cx, |view, cx| {
view.chosen = value.clone();
view.picks.push(value.clone());
cx.notify();
})
}),
)
}
}
#[gpui::test]
fn a_long_list_opens_with_its_choice_in_view(cx: &mut TestAppContext) {
setup(cx);
cx.update(|cx| Theme::update(cx, |theme| theme.reduced_motion = true));
let (view, cx) = cx.add_window_view(|_, _| Long {
chosen: "r25".into(),
picks: Vec::new(),
});
let (trigger, list) = cx.update(|window, cx| {
let theme = cx.theme();
(
theme
.control_height(ControlSize::Md)
.to_pixels(window.rem_size()),
theme.list_max_height().to_pixels(window.rem_size()),
)
});
let field = point(px(40.0), px(200.0) + trigger / 2.0);
cx.simulate_mouse_move(field, None, Modifiers::none());
cx.simulate_click(field, Modifiers::none());
frames(cx);
let foot = point(
px(40.0),
px(200.0) + trigger + motion::NUDGE + list - px(8.0),
);
cx.simulate_mouse_move(foot, None, Modifiers::none());
cx.simulate_click(foot, Modifiers::none());
frames(cx);
let picks = view.read_with(cx, |view, _| view.picks.clone());
assert_eq!(picks, ["r25"], "the list opens with its choice at the foot");
}
/// Frames 2ms apart, past reduced motion's 1ms entrances.
fn frames(cx: &mut VisualTestContext) {
for _ in 0..3 {
std::thread::sleep(std::time::Duration::from_millis(2));
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
}
struct Level {
value: f64,
}