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:
ZacharyZhang-NY committed 2026-10-07 02:50:34 -04:00
1 parent 829ef681ba
commit 078a2d19d8
14 files changed
+279 -88

No files matched your search

+18 -16
View File
@@ -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
View File
@@ -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
View File
@@ -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));
}
}
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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");
-6
View File
@@ -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() {
-6
View File
@@ -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() {
+1
View File
@@ -11,6 +11,7 @@ mod devices;
mod edit;
mod images;
mod player;
mod stale;
mod video;
pub(super) fn setup(cx: &mut TestAppContext) {
+107
View File
@@ -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
View File
@@ -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 {
+2 -1
View File
@@ -40,7 +40,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca
- [x] P23 AI Chat — `src/chat`
- [x] P24 Agent — `src/agent`
- [x] P25 Generative — `src/generative`
- [ ] P26 Media — `src/media`
- [x] P26 Media — `src/media`
- [ ] P27 Files — `src/files`
- [ ] P28 Messaging — `src/messaging`
- [ ] P29 Mail — `src/mail`
@@ -91,3 +91,4 @@ One line per item: what changed, what stayed and why, review rounds.
- P23: a branch version past its total logs and shows a dash, its arrows off; a parameter outside its range logs and pegs; a passage's matches outside it log and stay out. Kept: one-value contracts (progress, scores, shapes, peaks, levels, limits, prices, capacity, format, hour), the composer's trigger rows and stream reveal by construction; `places` skips zero, so `decimals` meets no zero step. Review: 2 rounds.
- P24: a checkpoint or browser frame past its list logs and marks none; progress past its total and memory past its limit log and fill their bars, the text keeping the real counts; three `should_panic` tests became render tests. Kept: the artifact's preview-or-source builder check, one-value contracts (cost, cpu, pointer, totals and limits above zero), registry lookups over a constant table. Review: 1 round.
- P25: a verdict, ratio, style, shot, voice, variation or prompt version gone from its list logs and marks none (no picture, no Vary or Upscale, no diff); embedding points in a gone group stay out; an epoch and GPU memory past their whole log and peg. A seed field holds a seed: each field handed in is fitted, and text that is no seed clears; in `TextInput`, a fit starts a fresh undo history, undo keeps and `set_text` fits against the committed text, never an input method's unfinished one (`committed`), and a lifted mask stroke stays on its picture. Kept: one-value contracts, unique group names, one snapshot's loss lists, two to four sides, the legend's lookup. Review: 5 rounds, the cap; round 5's finding (`set_text` over a composition) is fixed and tested, and P26's review checks it.
- P26: a live position past its media logs and sits at the end (`media::played`); loaded time, a trim and chapters past the length peg or stay out; a gone device or track, or a refused trim, leaves none and clears the last; a dropped root path shows whole; subtitle times no clock holds read as none; a rail layout left no width gives no share (`scrubber::along`), so hover, press and drag skip it; waveform buckets count in `u64`, which a 32-bit web `usize` outgrows. Kept: non-zero lengths, one-value contracts, one list's uniqueness and order, lookups by construction. Review: 4 rounds; it also passed P25's last fix.