Panics · P26 Media: live positions and trims peg to the media, gone choices clear, rails with no width and huge waveforms hold, subtitle times refuse overflow
This commit is contained in:
1 parent
829ef681ba
commit
078a2d19d8
14 files changed
+279
-88
No files matched your search
+18
-16
@@ -2,12 +2,12 @@ use std::{rc::Rc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Corners, DragMoveEvent, ElementId, EmptyView, EntityId, HoverListenerMode,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Point, RenderOnce,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, Window, canvas, div, fill, point, prelude::*, relative,
|
||||
size, transparent_black,
|
||||
};
|
||||
|
||||
use super::scrubber::{OnTime, clock, time_at, time_tip};
|
||||
use super::scrubber::{OnTime, along, clock, time_at, time_tip};
|
||||
use crate::{
|
||||
forms::keyed,
|
||||
layout::fit,
|
||||
@@ -25,10 +25,12 @@ pub(crate) fn resample(peaks: &[f32], count: usize) -> Vec<f32> {
|
||||
"{} peaks into {count}",
|
||||
peaks.len()
|
||||
);
|
||||
// In u64: bar by length outgrows a 32-bit usize on the web.
|
||||
let edge = |bar: usize| (bar as u64 * peaks.len() as u64 / count as u64) as usize;
|
||||
(0..count)
|
||||
.map(|bar| {
|
||||
let from = bar * peaks.len() / count;
|
||||
let to = ((bar + 1) * peaks.len() / count).max(from + 1);
|
||||
let from = edge(bar);
|
||||
let to = edge(bar + 1).max(from + 1);
|
||||
peaks[from..to].iter().copied().fold(0.0, f32::max)
|
||||
})
|
||||
.collect()
|
||||
@@ -66,7 +68,8 @@ impl AudioWaveform {
|
||||
!peaks.is_empty() && peaks.iter().all(|peak| (0.0..=1.0).contains(peak)),
|
||||
"peaks lie within 0 to 1"
|
||||
);
|
||||
assert!(!length.is_zero() && at <= length, "{at:?} of {length:?}");
|
||||
assert!(!length.is_zero(), "{at:?} of no length");
|
||||
let at = super::played(at, length, "audio waveform");
|
||||
Self {
|
||||
id: id.into(),
|
||||
peaks: Rc::new(peaks),
|
||||
@@ -138,9 +141,6 @@ impl RenderOnce for AudioWaveform {
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full();
|
||||
let spot = move |bounds: Bounds<Pixels>, pointer: Point<Pixels>| {
|
||||
(f32::from(pointer.x - bounds.left()) / f32::from(bounds.size.width)).clamp(0.0, 1.0)
|
||||
};
|
||||
let (moved, left) = (wave.clone(), wave.clone());
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
@@ -154,7 +154,7 @@ impl RenderOnce for AudioWaveform {
|
||||
.children(tip)
|
||||
.on_mouse_move(move |event, _, cx| {
|
||||
moved.update(cx, |wave, cx| {
|
||||
wave.hover = Some(spot(wave.bounds, event.position));
|
||||
wave.hover = along(wave.bounds, event.position);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
@@ -175,8 +175,11 @@ impl RenderOnce for AudioWaveform {
|
||||
.focus_ring(cx)
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
|
||||
let time =
|
||||
time_at(spot(pressed_wave.read(cx).bounds, event.position), length);
|
||||
let Some(share) = along(pressed_wave.read(cx).bounds, event.position)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let time = time_at(share, length);
|
||||
log::info!("audio waveform: seek to {}", clock(time));
|
||||
pressed(time, window, cx);
|
||||
})
|
||||
@@ -186,11 +189,10 @@ impl RenderOnce for AudioWaveform {
|
||||
return;
|
||||
}
|
||||
let bounds = dragged_wave.read(cx).bounds;
|
||||
dragged(
|
||||
time_at(spot(bounds, event.event.position), length),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let Some(share) = along(bounds, event.event.position) else {
|
||||
return;
|
||||
};
|
||||
dragged(time_at(share, length), window, cx);
|
||||
})
|
||||
.on_key_down(move |event, window, cx| {
|
||||
let (now, end) = (at.as_secs_f64(), length.as_secs_f64());
|
||||
|
||||
+20
-4
@@ -180,10 +180,11 @@ impl DeviceSelector {
|
||||
/// The device in use, by key.
|
||||
pub fn chosen(mut self, key: impl Into<SharedString>) -> Self {
|
||||
let key = key.into();
|
||||
assert!(
|
||||
self.devices.iter().any(|device| device.key == key),
|
||||
"no device {key}"
|
||||
);
|
||||
if !self.devices.iter().any(|device| device.key == key) {
|
||||
log::error!("device selector: no device {key}; none chosen");
|
||||
self.chosen = None;
|
||||
return self;
|
||||
}
|
||||
self.chosen = Some(key);
|
||||
self
|
||||
}
|
||||
@@ -225,3 +226,18 @@ impl RenderOnce for DeviceSelector {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Device, DeviceKind, DeviceSelector};
|
||||
|
||||
#[test]
|
||||
fn a_device_gone_from_the_list_leaves_none_chosen() {
|
||||
let device = Device {
|
||||
key: "a".into(),
|
||||
name: "Device a".into(),
|
||||
};
|
||||
let selector = DeviceSelector::new("mics", DeviceKind::Microphone, [device]);
|
||||
assert_eq!(selector.chosen("a").chosen("gone").chosen, None);
|
||||
}
|
||||
}
|
||||
+29
-5
@@ -91,10 +91,12 @@ impl Playlist {
|
||||
/// The song under way, by key, and whether it plays.
|
||||
pub fn current(mut self, key: impl Into<SharedString>, playing: bool) -> Self {
|
||||
let key = key.into();
|
||||
assert!(
|
||||
self.tracks.iter().any(|track| track.key == key),
|
||||
"no track {key}"
|
||||
);
|
||||
if !self.tracks.iter().any(|track| track.key == key) {
|
||||
log::error!("playlist: no track {key}; none under way");
|
||||
self.current = None;
|
||||
self.playing = false;
|
||||
return self;
|
||||
}
|
||||
self.current = Some(key);
|
||||
self.playing = playing;
|
||||
self
|
||||
@@ -176,7 +178,8 @@ impl AudioPlayer {
|
||||
length: Duration,
|
||||
at: Duration,
|
||||
) -> Self {
|
||||
assert!(!length.is_zero() && at <= length, "{at:?} of {length:?}");
|
||||
assert!(!length.is_zero(), "{at:?} of no length");
|
||||
let at = super::played(at, length, "now playing");
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: title.into(),
|
||||
@@ -289,3 +292,24 @@ impl RenderOnce for AudioPlayer {
|
||||
.children(self.volume)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{Playlist, Track};
|
||||
|
||||
#[test]
|
||||
fn a_track_gone_from_the_list_leaves_none_under_way() {
|
||||
let track = Track {
|
||||
key: "a".into(),
|
||||
title: "Tall Windows".into(),
|
||||
artist: "Aster Quartet".into(),
|
||||
length: Duration::from_secs(200),
|
||||
};
|
||||
let list = Playlist::new("list", [track])
|
||||
.current("a", true)
|
||||
.current("gone", true);
|
||||
assert_eq!((list.current, list.playing), (None, false));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ mod viewer;
|
||||
#[cfg(all(test, feature = "test-support"))]
|
||||
mod tests;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub use annotate::ImageAnnotator;
|
||||
pub use audio::{AudioSpectrum, AudioWaveform};
|
||||
pub(crate) use camera::name_chip;
|
||||
@@ -39,3 +41,11 @@ pub use subtitles::{Cue, SubtitleEditor};
|
||||
pub use thumbnail::ImageThumbnail;
|
||||
pub use upload::ImageUpload;
|
||||
pub use viewer::ImageViewer;
|
||||
|
||||
/// `at` within `length`: a live position past the end logs and sits at the end.
|
||||
fn played(at: Duration, length: Duration, owner: &str) -> Duration {
|
||||
if at > length {
|
||||
log::error!("{owner}: {at:?} past {length:?}; at the end");
|
||||
}
|
||||
at.min(length)
|
||||
}
|
||||
+2
-1
@@ -60,7 +60,8 @@ impl VideoPlayer {
|
||||
length: Duration,
|
||||
at: Duration,
|
||||
) -> Self {
|
||||
assert!(!length.is_zero() && at <= length, "{at:?} of {length:?}");
|
||||
assert!(!length.is_zero(), "{at:?} of no length");
|
||||
let at = super::played(at, length, "video player");
|
||||
Self {
|
||||
id: id.into(),
|
||||
frame,
|
||||
|
||||
+30
-22
@@ -23,6 +23,12 @@ pub(crate) type OnTime = Rc<dyn Fn(Duration, &mut Window, &mut App)>;
|
||||
/// Seconds an arrow key moves the time; Page keys move ten times as far.
|
||||
const STEP: f64 = 5.0;
|
||||
|
||||
/// Where `pointer` falls along `bounds`, a share of its width held to it; none on a rail layout left no width.
|
||||
pub(crate) fn along(bounds: Bounds<Pixels>, pointer: Point<Pixels>) -> Option<f32> {
|
||||
let width = f32::from(bounds.size.width);
|
||||
(width > 0.0).then(|| (f32::from(pointer.x - bounds.left()) / width).clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
/// The time at `share` of `length`, the share held to the track.
|
||||
pub(crate) fn time_at(share: f32, length: Duration) -> Duration {
|
||||
length.mul_f32(share.clamp(0.0, 1.0))
|
||||
@@ -96,7 +102,8 @@ pub struct Scrubber {
|
||||
impl Scrubber {
|
||||
/// `at` is how far it has played, within `length`.
|
||||
pub fn new(id: impl Into<ElementId>, length: Duration, at: Duration) -> Self {
|
||||
assert!(!length.is_zero() && at <= length, "{at:?} of {length:?}");
|
||||
assert!(!length.is_zero(), "{at:?} of no length");
|
||||
let at = super::played(at, length, "scrubber");
|
||||
Self {
|
||||
id: id.into(),
|
||||
length,
|
||||
@@ -116,22 +123,24 @@ impl Scrubber {
|
||||
|
||||
/// How far it has loaded.
|
||||
pub fn loaded(mut self, loaded: Duration) -> Self {
|
||||
assert!(
|
||||
loaded <= self.length,
|
||||
"loaded {loaded:?} of {:?}",
|
||||
self.length
|
||||
);
|
||||
self.loaded = loaded;
|
||||
if loaded > self.length {
|
||||
log::error!("scrubber: loaded {loaded:?} of {:?}; it fills", self.length);
|
||||
}
|
||||
self.loaded = loaded.min(self.length);
|
||||
self
|
||||
}
|
||||
|
||||
/// A chapter from `start`, after the last one given.
|
||||
pub fn chapter(mut self, start: Duration, title: impl Into<SharedString>) -> Self {
|
||||
let after = self.chapters.last().is_none_or(|(last, _)| *last < start);
|
||||
assert!(
|
||||
after && start < self.length,
|
||||
"a chapter at {start:?} out of order"
|
||||
);
|
||||
assert!(after, "a chapter at {start:?} out of order");
|
||||
if start >= self.length {
|
||||
log::error!(
|
||||
"scrubber: a chapter at {start:?} past {:?}; left out",
|
||||
self.length
|
||||
);
|
||||
return self;
|
||||
}
|
||||
self.chapters.push((start, title.into()));
|
||||
self
|
||||
}
|
||||
@@ -257,9 +266,6 @@ impl RenderOnce for Scrubber {
|
||||
)
|
||||
.children(knob)
|
||||
.children(tip);
|
||||
let spot = move |bounds: Bounds<Pixels>, pointer: Point<Pixels>| {
|
||||
f32::from(pointer.x - bounds.left()) / f32::from(bounds.size.width)
|
||||
};
|
||||
let (moved, left) = (track.clone(), track.clone());
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
@@ -271,7 +277,7 @@ impl RenderOnce for Scrubber {
|
||||
.child(rail)
|
||||
.on_mouse_move(move |event, _, cx| {
|
||||
moved.update(cx, |track, cx| {
|
||||
track.hover = Some(spot(track.bounds, event.position).clamp(0.0, 1.0));
|
||||
track.hover = along(track.bounds, event.position);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
@@ -292,8 +298,11 @@ impl RenderOnce for Scrubber {
|
||||
.focus_ring(cx)
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
|
||||
let time =
|
||||
time_at(spot(pressed_track.read(cx).bounds, event.position), length);
|
||||
let Some(share) = along(pressed_track.read(cx).bounds, event.position)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let time = time_at(share, length);
|
||||
log::info!("scrubber: seek to {}", clock(time));
|
||||
pressed(time, window, cx);
|
||||
})
|
||||
@@ -303,11 +312,10 @@ impl RenderOnce for Scrubber {
|
||||
return;
|
||||
}
|
||||
let bounds = dragged_track.read(cx).bounds;
|
||||
dragged(
|
||||
time_at(spot(bounds, event.event.position), length),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let Some(share) = along(bounds, event.event.position) else {
|
||||
return;
|
||||
};
|
||||
dragged(time_at(share, length), window, cx);
|
||||
})
|
||||
.on_key_down(move |event, window, cx| {
|
||||
let (now, end) = (at.as_secs_f64(), length.as_secs_f64());
|
||||
|
||||
+40
-19
@@ -2,12 +2,12 @@ use std::{rc::Rc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, CursorStyle, DragMoveEvent, ElementId, EmptyView, EntityId, ImageSource,
|
||||
InteractiveElement, IntoElement, MouseButton, ObjectFit, ParentElement, Pixels, Point,
|
||||
RenderOnce, StatefulInteractiveElement, Styled, Window, canvas, div, img, prelude::*, relative,
|
||||
InteractiveElement, IntoElement, MouseButton, ObjectFit, ParentElement, Pixels, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, Window, canvas, div, img, prelude::*, relative,
|
||||
transparent_black,
|
||||
};
|
||||
|
||||
use super::scrubber::{OnTime, clock, time_at};
|
||||
use super::scrubber::{OnTime, along, clock, time_at};
|
||||
use crate::{
|
||||
primitives::{FocusRing, tab_stop},
|
||||
theme::ActiveTheme,
|
||||
@@ -88,10 +88,11 @@ impl VideoThumbnailStrip {
|
||||
) -> Self {
|
||||
let frames: Vec<ImageSource> = frames.into_iter().map(Into::into).collect();
|
||||
assert!(
|
||||
!frames.is_empty() && !length.is_zero() && at <= length,
|
||||
!frames.is_empty() && !length.is_zero(),
|
||||
"{} frames, {at:?} of {length:?}",
|
||||
frames.len()
|
||||
);
|
||||
let at = super::played(at, length, "thumbnail strip");
|
||||
Self {
|
||||
id: id.into(),
|
||||
frames,
|
||||
@@ -105,10 +106,19 @@ impl VideoThumbnailStrip {
|
||||
|
||||
/// The part kept, from its start to its end.
|
||||
pub fn trim(mut self, start: Duration, end: Duration) -> Self {
|
||||
assert!(
|
||||
start + LEAST <= end && end <= self.length,
|
||||
"a trim of {start:?} to {end:?}"
|
||||
);
|
||||
assert!(start + LEAST <= end, "a trim of {start:?} to {end:?}");
|
||||
if end > self.length {
|
||||
log::error!(
|
||||
"thumbnail strip: a trim to {end:?} past {:?}; it ends there",
|
||||
self.length
|
||||
);
|
||||
}
|
||||
let end = end.min(self.length);
|
||||
if start + LEAST > end {
|
||||
log::error!("thumbnail strip: a trim from {start:?} leaves too little; none kept");
|
||||
self.trim = None;
|
||||
return self;
|
||||
}
|
||||
self.trim = Some((start, end));
|
||||
self
|
||||
}
|
||||
@@ -233,9 +243,6 @@ impl RenderOnce for VideoThumbnailStrip {
|
||||
)
|
||||
.children(kept)
|
||||
.child(playhead);
|
||||
let spot = move |bounds: Bounds<Pixels>, pointer: Point<Pixels>| {
|
||||
f32::from(pointer.x - bounds.left()) / f32::from(bounds.size.width)
|
||||
};
|
||||
let (on_seek, on_trim) = (self.on_seek, self.on_trim);
|
||||
let act = move |held: Option<End>, time: Duration, window: &mut Window, cx: &mut App| match (
|
||||
held, trim, &on_trim, &on_seek,
|
||||
@@ -262,7 +269,9 @@ impl RenderOnce for VideoThumbnailStrip {
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
|
||||
let bounds = pressed_strip.read(cx).bounds;
|
||||
let at = spot(bounds, event.position);
|
||||
let Some(at) = along(bounds, event.position) else {
|
||||
return;
|
||||
};
|
||||
let reach = f32::from(reach) / f32::from(bounds.size.width);
|
||||
let held = trim.and_then(|trim| end_at(trim, at, reach, length));
|
||||
pressed_strip.update(cx, |strip, _| strip.held = held);
|
||||
@@ -281,12 +290,10 @@ impl RenderOnce for VideoThumbnailStrip {
|
||||
let strip = moved_strip.read(cx);
|
||||
(strip.bounds, strip.held)
|
||||
};
|
||||
moved(
|
||||
held,
|
||||
time_at(spot(bounds, event.event.position), length),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let Some(share) = along(bounds, event.event.position) else {
|
||||
return;
|
||||
};
|
||||
moved(held, time_at(share, length), window, cx);
|
||||
})
|
||||
.on_key_down(move |event, window, cx| {
|
||||
let command = &event.keystroke.modifiers;
|
||||
@@ -312,10 +319,24 @@ impl RenderOnce for VideoThumbnailStrip {
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{End, end_at, trimmed};
|
||||
use super::{End, VideoThumbnailStrip, end_at, trimmed};
|
||||
|
||||
const TEN: Duration = Duration::from_secs(10);
|
||||
|
||||
#[test]
|
||||
fn a_trim_past_the_video_ends_at_it_or_keeps_none() {
|
||||
let strip = || VideoThumbnailStrip::new("strip", ["a.jpg"], TEN, Duration::ZERO);
|
||||
let second = Duration::from_secs(1);
|
||||
assert_eq!(
|
||||
strip().trim(second * 2, second * 12).trim,
|
||||
Some((second * 2, TEN))
|
||||
);
|
||||
let gone = strip()
|
||||
.trim(second * 2, second * 6)
|
||||
.trim(second * 11, second * 13);
|
||||
assert_eq!(gone.trim, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_trim_end_moves_and_stops_short_of_the_other_and_the_length() {
|
||||
let (trim, length) = ((TEN, TEN * 5), TEN * 10);
|
||||
|
||||
+19
-7
@@ -27,7 +27,7 @@ pub struct Cue {
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
/// A time read from `mm:ss.mmm` or `h:mm:ss.mmm`, its fraction optional; none when it does not read.
|
||||
/// A time read from `mm:ss.mmm` or `h:mm:ss.mmm`, its fraction optional; none when it does not read or no clock holds it.
|
||||
pub(crate) fn parse_time(text: &str) -> Option<Duration> {
|
||||
let text = text.trim();
|
||||
let (clock, fraction) = text.split_once('.').unwrap_or((text, ""));
|
||||
@@ -36,10 +36,10 @@ pub(crate) fn parse_time(text: &str) -> Option<Duration> {
|
||||
.map(|part| part.parse().ok())
|
||||
.collect::<Option<_>>()?;
|
||||
let seconds = match parts.as_slice() {
|
||||
[minutes, seconds] if *seconds < 60 => minutes * 60 + seconds,
|
||||
[hours, minutes, seconds] if *minutes < 60 && *seconds < 60 => {
|
||||
hours * 3600 + minutes * 60 + seconds
|
||||
}
|
||||
[minutes, seconds] if *seconds < 60 => minutes.checked_mul(60)?.checked_add(*seconds)?,
|
||||
[hours, minutes, seconds] if *minutes < 60 && *seconds < 60 => hours
|
||||
.checked_mul(3600)?
|
||||
.checked_add(minutes * 60 + seconds)?,
|
||||
_ => return None,
|
||||
};
|
||||
let millis = match fraction.len() {
|
||||
@@ -49,7 +49,9 @@ pub(crate) fn parse_time(text: &str) -> Option<Duration> {
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(Duration::from_millis(seconds * 1000 + millis))
|
||||
Some(Duration::from_millis(
|
||||
seconds.checked_mul(1000)?.checked_add(millis)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// A time as `mm:ss.mmm`, the hours ahead once it runs past one.
|
||||
@@ -331,7 +333,17 @@ mod tests {
|
||||
Some(Duration::from_millis(3_723_004))
|
||||
);
|
||||
assert_eq!(parse_time("75:00"), Some(Duration::from_secs(4500)));
|
||||
for bad in ["1:60", "a:10", "1:2:3:4", "01:02.5000", "01:02.x", ""] {
|
||||
for bad in [
|
||||
"1:60",
|
||||
"a:10",
|
||||
"1:2:3:4",
|
||||
"01:02.5000",
|
||||
"01:02.x",
|
||||
"",
|
||||
"18446744073709551615:00",
|
||||
"5124095576030431:00:00",
|
||||
"307445734561825860:00",
|
||||
] {
|
||||
assert_eq!(parse_time(bad), None, "{bad:?}");
|
||||
}
|
||||
assert_eq!(stamp(Duration::from_millis(62_500)), "01:02.500");
|
||||
|
||||
@@ -329,12 +329,6 @@ fn the_keys_start_from_the_song_under_way(cx: &mut TestAppContext) {
|
||||
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() {
|
||||
let _ = Playlist::new("list", [track("a", "Tall Windows")]).current("c", false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "track a twice")]
|
||||
fn a_song_is_listed_once() {
|
||||
|
||||
@@ -192,12 +192,6 @@ fn with_none_attached_the_selector_takes_no_focus(cx: &mut TestAppContext) {
|
||||
assert!(cx.update(|window, cx| window.focused(cx).is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "no device c")]
|
||||
fn the_device_in_use_is_one_of_the_list() {
|
||||
let _ = DeviceSelector::new("mics", DeviceKind::Microphone, [device("a")]).chosen("c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "device a twice")]
|
||||
fn a_device_is_listed_once() {
|
||||
|
||||
@@ -11,6 +11,7 @@ mod devices;
|
||||
mod edit;
|
||||
mod images;
|
||||
mod player;
|
||||
mod stale;
|
||||
mod video;
|
||||
|
||||
pub(super) fn setup(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{
|
||||
Bounds, Context, IntoElement, Modifiers, MouseButton, ParentElement, Render, Styled,
|
||||
TestAppContext, Window, div, point, px, size,
|
||||
};
|
||||
|
||||
use super::{settle, setup};
|
||||
use crate::media::{
|
||||
AudioPlayer, AudioWaveform, Device, DeviceKind, DeviceSelector, Playlist, Scrubber, Track,
|
||||
VideoPlayer, VideoThumbnailStrip, played, scrubber::along,
|
||||
};
|
||||
|
||||
const SECOND: Duration = Duration::from_secs(1);
|
||||
|
||||
#[test]
|
||||
fn a_position_past_the_end_sits_at_it() {
|
||||
assert_eq!(played(SECOND * 3, SECOND * 10, "test"), SECOND * 3);
|
||||
assert_eq!(played(SECOND * 12, SECOND * 10, "test"), SECOND * 10);
|
||||
}
|
||||
|
||||
/// Live positions past their media, and choices gone from their lists.
|
||||
struct Lagging;
|
||||
|
||||
impl Render for Lagging {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let (length, past) = (SECOND * 10, SECOND * 12);
|
||||
let track = Track {
|
||||
key: "a".into(),
|
||||
title: "Tall Windows".into(),
|
||||
artist: "Aster Quartet".into(),
|
||||
length,
|
||||
};
|
||||
let device = Device {
|
||||
key: "a".into(),
|
||||
name: "Device a".into(),
|
||||
};
|
||||
div()
|
||||
.w(px(480.0))
|
||||
.child(AudioWaveform::new("wave", [0.5, 0.8], length, past))
|
||||
.child(AudioPlayer::new(
|
||||
"player",
|
||||
"Tall Windows",
|
||||
"Aster Quartet",
|
||||
length,
|
||||
past,
|
||||
))
|
||||
.child(VideoPlayer::new("video", None, 1.5, length, past))
|
||||
.child(
|
||||
Scrubber::new("scrub", length, past)
|
||||
.loaded(past)
|
||||
.chapter(SECOND * 2, "Intro")
|
||||
.chapter(past, "Gone"),
|
||||
)
|
||||
.child(
|
||||
VideoThumbnailStrip::new("strip", ["a.jpg"], length, past).trim(SECOND * 2, past),
|
||||
)
|
||||
.child(Playlist::new("list", [track]).current("c", false))
|
||||
.child(DeviceSelector::new("mics", DeviceKind::Microphone, [device]).chosen("c"))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn live_positions_and_gone_choices_draw(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (_, cx) = cx.add_window_view(|_, _| Lagging);
|
||||
settle(cx);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rail_with_no_width_has_no_place_along_it() {
|
||||
let rail = |width: f32| Bounds::new(point(px(10.0), px(0.0)), size(px(width), px(8.0)));
|
||||
assert_eq!(along(rail(0.0), point(px(10.0), px(4.0))), None);
|
||||
assert_eq!(along(rail(100.0), point(px(60.0), px(4.0))), Some(0.5));
|
||||
assert_eq!(along(rail(100.0), point(px(500.0), px(4.0))), Some(1.0));
|
||||
}
|
||||
|
||||
/// Rails that seek, laid out with no width.
|
||||
struct Squeezed;
|
||||
|
||||
impl Render for Squeezed {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let length = SECOND * 10;
|
||||
let seek = |_, _: &mut Window, _: &mut gpui::App| {};
|
||||
div()
|
||||
.w(px(0.0))
|
||||
.child(Scrubber::new("scrub", length, SECOND).on_seek(seek))
|
||||
.child(AudioWaveform::new("wave", [0.5, 0.8], length, SECOND).on_seek(seek))
|
||||
.child(VideoThumbnailStrip::new("strip", ["a.jpg"], length, SECOND).on_seek(seek))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_pointer_over_rails_with_no_width_seeks_nothing(cx: &mut TestAppContext) {
|
||||
setup(cx);
|
||||
let (_, cx) = cx.add_window_view(|_, _| Squeezed);
|
||||
settle(cx);
|
||||
for rail in ["scrubber", "audio-waveform", "video-strip"] {
|
||||
let bounds = cx.debug_bounds(rail).expect("a rail");
|
||||
let at = point(bounds.left(), bounds.center().y);
|
||||
cx.simulate_mouse_move(at, None, Modifiers::none());
|
||||
settle(cx);
|
||||
cx.simulate_mouse_down(at, MouseButton::Left, Modifiers::none());
|
||||
cx.simulate_mouse_up(at, MouseButton::Left, Modifiers::none());
|
||||
}
|
||||
settle(cx);
|
||||
}
|
||||
+1
-1
@@ -94,7 +94,7 @@ impl RenderOnce for ImageUpload {
|
||||
);
|
||||
let name = path
|
||||
.file_name()
|
||||
.expect("a picked file has a name")
|
||||
.unwrap_or(path.as_os_str())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let opened = match &decoded {
|
||||
|
||||
Reference in new issue
Block a user