Lists: a selectable list's cursor starts on the owner's pick; the waveform's fit is tested

SelectableList keeps the owner's last selection and the one it last
sent. A selection the owner makes, on first paint or later, moves the
cursor to its first row; the echo of the list's own pick leaves the
cursor where the pick put it. The playlist's keys start from the song
under way. The waveform's bar count and inset move into `fit`, tested
beside `resample`.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 18:44:38 -04:00
1 parent 04f88812dc
commit cb6c78b90c
6 files changed
+102 -5

No files matched your search

+13 -1
View File
@@ -38,13 +38,15 @@ pub(crate) fn picked(
.collect()
}
/// The row the keyboard is on and where a Shift range starts, by index and by key so they follow their rows when the owner reorders them, and the scroll that follows them.
/// The row the keyboard is on and where a Shift range starts, by index and by key so they follow their rows when the owner reorders them, and the scroll that follows them; the owner's last selection, and the one the list last sent.
#[derive(Default)]
struct Cursor {
at: usize,
anchor: usize,
keys: Option<(SharedString, SharedString)>,
scroll: ScrollHandle,
seen: Option<Vec<SharedString>>,
sent: Option<Vec<SharedString>>,
}
type OnSelect = Rc<dyn Fn(&[SharedString], &mut Window, &mut App)>;
@@ -125,6 +127,15 @@ impl RenderOnce for SelectableList {
let cursor =
window.use_keyed_state((self.id.clone(), "cursor"), cx, |_, _| Cursor::default());
let keys: Rc<[SharedString]> = self.rows.iter().map(|(key, _)| key.clone()).collect();
if cursor.read(cx).seen.as_ref() != Some(&self.selected) {
cursor.update(cx, |cursor, _| {
let echo = cursor.sent.take().as_ref() == Some(&self.selected);
if let Some(first) = self.selected.first().filter(|_| !echo) {
cursor.keys = Some((first.clone(), first.clone()));
}
cursor.seen = Some(self.selected.clone());
});
}
let (at, anchor, scroll) = {
let cursor = cursor.read(cx);
let place = |key: Option<&SharedString>, ix: usize| {
@@ -183,6 +194,7 @@ impl RenderOnce for SelectableList {
keys[cursor.anchor.min(keys.len() - 1)].clone(),
));
cursor.scroll.scroll_to_item(ix);
cursor.sent = Some(next.clone());
cx.notify();
});
log::info!("selectable list {id:?}: {next:?}");
+35
View File
@@ -117,6 +117,41 @@ fn presses_pick_one_toggle_with_cmd_and_range_with_shift(cx: &mut TestAppContext
assert_eq!(picks(&view, cx), NAMES);
}
#[gpui::test]
fn the_cursor_starts_on_the_first_selected_row(cx: &mut TestAppContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|_, _| Picks(vec!["c".into()], Vec::new(), None, false));
settle(cx);
cx.update(|window, _| window.focus_next());
cx.simulate_keystrokes("down");
settle(cx);
assert_eq!(picks(&view, cx), ["d"]);
}
#[gpui::test]
fn a_selection_from_the_owner_moves_the_cursor_and_an_echo_does_not(cx: &mut TestAppContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|_, _| Picks(vec!["a".into()], Vec::new(), None, false));
settle(cx);
view.update(cx, |picks, _| picks.0 = vec!["b".into()]);
settle(cx);
cx.update(|window, _| window.focus_next());
cx.simulate_keystrokes("shift-down shift-down");
settle(cx);
assert_eq!(
picks(&view, cx),
["b", "c", "d"],
"the range starts where the owner put it"
);
cx.simulate_keystrokes("up");
settle(cx);
assert_eq!(
picks(&view, cx),
["c"],
"its own range left the cursor at its end"
);
}
/// A row with two actions, 144 across; it reports where the row sits and which action ran.
struct Swiped {
left: Rc<Cell<Pixels>>,
+16 -3
View File
@@ -32,6 +32,13 @@ pub(crate) fn resample(peaks: &[f32], count: usize) -> Vec<f32> {
.collect()
}
/// How many bars `bar` wide with `gap` between fit across `width`, at least one, and the inset that centers them.
pub(crate) fn fit(width: f32, bar: f32, gap: f32) -> (usize, f32) {
let count = ((width + gap) / (bar + gap)).floor().max(1.0) as usize;
let used = (bar + gap) * count as f32 - gap;
(count, ((width - used) / 2.0).max(0.0))
}
/// A seeking drag on a waveform, marked with its waveform.
struct Seek(EntityId);
@@ -114,8 +121,7 @@ impl RenderOnce for AudioWaveform {
move |bounds, _, window, _| {
let width = f32::from(bounds.size.width);
let pitch = f32::from(bar + gap);
let count = ((width + f32::from(gap)) / pitch).floor().max(1.0) as usize;
let inset = (width - (pitch * count as f32 - f32::from(gap))) / 2.0;
let (count, inset) = fit(width, f32::from(bar), f32::from(gap));
for (ix, peak) in resample(&peaks, count).into_iter().enumerate() {
let left = inset + pitch * ix as f32;
let tall = (bounds.size.height * peak).max(bar);
@@ -242,7 +248,14 @@ impl RenderOnce for AudioSpectrum {
#[cfg(test)]
mod tests {
use super::resample;
use super::{fit, resample};
#[test]
fn the_bars_that_fit_sit_centered() {
assert_eq!(fit(100.0, 3.0, 2.0), (20, 1.0));
assert_eq!(fit(98.0, 3.0, 2.0), (20, 0.0));
assert_eq!(fit(2.0, 3.0, 2.0), (1, 0.0));
}
#[test]
fn peaks_gather_into_bars_by_their_loudest() {
+36
View File
@@ -304,6 +304,42 @@ fn down_and_enter_play_the_next_song(cx: &mut TestAppContext) {
assert_eq!(view.read_with(cx, |view, _| view.played.clone()), ["b"]);
}
/// Four songs with the third under way, and the songs it was told to play.
struct Midway {
played: Vec<SharedString>,
}
impl Render for Midway {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let owner = cx.entity();
let songs = [
track("a", "One"),
track("b", "Two"),
track("c", "Three"),
track("d", "Four"),
];
div().w(px(320.0)).child(
Playlist::new("list", songs)
.current("c", true)
.on_play(move |key, _, cx| {
owner.update(cx, |view, _| view.played.push(key.clone()))
}),
)
}
}
#[gpui::test]
fn the_keys_start_from_the_song_under_way(cx: &mut TestAppContext) {
setup(cx);
let (view, cx) = cx.add_window_view(|_, _| Midway { played: Vec::new() });
cx.update(|window, _| window.activate_window());
settle(cx);
tab_to(1, cx);
press("down", cx);
press("enter", cx);
assert_eq!(view.read_with(cx, |view, _| view.played.clone()), ["d"]);
}
#[test]
#[should_panic(expected = "no track c")]
fn the_song_under_way_is_one_of_the_list() {