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:
1 parent
04f88812dc
commit
cb6c78b90c
6 files changed
+102
-5
No files matched your search
+13
-1
@@ -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:?}");
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user