collective

Browse / github.com/storytold/soundcraft / patch

Edit window: butted clips share one seam, and the clip name is a small pill

Edit window: butted clips share one seam, and the clip name is a small pill ## Problem and resulting behavior Two things set the Edit window apart from the incumbent, one at every cut and one on every clip: 1. **A cut is drawn twice.** Two clips that meet exactly (the later one starts where the earlier ends) each draw their own outline, so the cut is two dark lines side by side, with a round corner on each side of it. The incumbent draws a single thin line between them, as thick as a clip's own outline, with square corners. 2. **The clip name is a full-width bar.** A coloured band across the top of the clip holds the name, the rating stars and the lock. It takes a s

Status
open
Score
0.00
Adopters
0 other operators
Author
lucasegen (imported from pull request #974, open)
Base
60087e0f46032118bf309ed9ea161a9b4a872713
Size
+520 −87 in 6 files
Review
pending
Shared
2026-10-28
Id
ce64b85c8636a1d755194ac9

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/edit_window.rs crates/ui-egui/src/edit_window/clips.rs crates/ui-egui/src/edit_window/input.rs crates/ui-egui/src/edit_window/tests.rs crates/ui-egui/src/edit_window/tracks.rs crates/ui-egui/src/video_track.rs

diff --git a/crates/ui-egui/src/edit_window.rs b/crates/ui-egui/src/edit_window.rs--- a/crates/ui-egui/src/edit_window.rs+++ b/crates/ui-egui/src/edit_window.rs@@ -27,9 +27,14 @@ mod universe;  #[cfg(test)] use automation::automation_id;+use clips::clip_cuts;+pub(crate) use clips::draw_clip_labels; pub use clips::strip_silence_stroke; #[cfg(test)]-use clips::{clip_name_height, fade_curve_points};+use clips::{+    ClipCut, PILL_BG, PILL_HEIGHT, PILL_INSET, PILL_MIN_WIDTH, PILL_PAD, clip_cut, clip_cut_in, clip_marks, fade_curve_points, marks_pill_left,+    name_pill_width,+}; use drag::drive_drag; #[cfg(test)] use drag::fade_drag_range;diff --git a/crates/ui-egui/src/edit_window/clips.rs b/crates/ui-egui/src/edit_window/clips.rs--- a/crates/ui-egui/src/edit_window/clips.rs+++ b/crates/ui-egui/src/edit_window/clips.rs@@ -48,8 +48,9 @@ pub(super) fn grid_lines(painter: &egui::Painter, s: &Session, tl: Rect, lane: R     } } -pub(super) fn clip_name_height(s: &Session, lane: Rect) -> f32 {-    if lane.height() >= 40.0 && s.edit.flag("view.clip.name") { 13.0 } else { 0.0 }+/// Whether clips show their name (View › Clip › Name) in a lane tall enough to carry it.+pub(super) fn clip_name_shown(s: &Session, lane: Rect) -> bool {+    lane.height() >= 40.0 && s.edit.flag("view.clip.name") }  pub(super) fn fade_curve_points(fade: soundcraft_model::Fade, fade_in: bool, a: f32, b: f32, body: Rect, pixels_per_point: f32) -> Vec<Pos2> {@@ -138,51 +139,46 @@ fn grid_veil(line: Color32, bg: Color32) -> Color32 {     } } -/// `sorted`: whether `track.clips()` is sorted by start (worked out once per track and frame).+/// `sorted`: whether `track.clips()` is sorted by start (worked out once per track and frame); `cut`: how the clip+/// meets its neighbours in its playlist (see [`clip_cuts`]). pub(super) fn draw_clip(     app: &SoundApp,     painter: &egui::Painter,     s: &Session,     track: &Track,     sorted: bool,+    cut: ClipCut,     clip: &Clip,     tl: Rect,     lane: Rect,     dim: bool, ) {-    let x0 = x_of(s, tl, clip.start);-    let x1 = x_of(s, tl, clip.end()).max(x0 + 1.0);-    let r = Rect::from_min_max(pos2(x0, lane.min.y + 1.0), pos2(x1, lane.max.y - 1.0));+    let r = clip_rect(s, tl, lane, clip);+    let (x0, x1) = (r.min.x, r.max.x);     let selected = s.edit.selected_clips.contains(&clip.id)         || (s.edit.selected_tracks.contains(&track.id)             && !s.edit.selection.is_empty()             && s.edit.selection.start <= clip.start             && s.edit.selection.end >= clip.end());     let color = clip.color.unwrap_or(track.color);-    let (mut body, bar, mut wave) = clip_colors(color, selected);+    let (mut body, _, mut wave) = clip_colors(color, selected);     if clip.muted || dim {         body = body.gamma_multiply(0.45);         wave = Color32::from_rgb(70, 70, 70);     }     let body = if s.edit.flag("view.clip.transparency") { body.gamma_multiply(0.55) } else { body };-    painter.rect(r, CornerRadius::same(2), body, Stroke::new(1.0, Color32::from_rgb(10, 10, 10)), StrokeKind::Inside);-    let name_h = clip_name_height(s, lane);-    if name_h > 0.0 {-        let nb = Rect::from_min_max(r.min, pos2(r.max.x, r.min.y + name_h));-        painter.rect_filled(nb, CornerRadius { nw: 2, ne: 2, sw: 0, se: 0 }, if clip.muted { Color32::from_rgb(70, 70, 70) } else { bar });-        let name_clip = nb.intersect(lane);-        // Narrower than this, no glyph would show: the name starts 4 points in.-        if name_clip.width() > 6.0 {-            painter.with_clip_rect(name_clip).text(-                pos2(nb.min.x.max(lane.min.x) + 4.0, nb.center().y),-                Align2::LEFT_CENTER,-                &clip.name,-                bold(10.5),-                Color32::from_rgb(12, 12, 12),-            );-        }+    let corners = cut.corners();+    let outline = Stroke::new(1.0, CLIP_OUTLINE);+    if cut.right {+        // The cut's single line is drawn by the next clip: stroke past our right edge and clip it away.+        painter.rect_filled(r, corners, body);+        let past = Rect::from_min_max(r.min, pos2(r.max.x + 2.0, r.max.y));+        painter.with_clip_rect(painter.clip_rect().intersect(r)).rect_stroke(past, corners, outline, StrokeKind::Inside);+    } else {+        painter.rect(r, corners, body, outline, StrokeKind::Inside);     }-    let body_r = Rect::from_min_max(pos2(r.min.x, r.min.y + name_h), r.max);+    // The body is the whole clip: the name is a small pill drawn over it (see `draw_clip_pills`).+    let body_r = r;     let vis = body_r.intersect(lane);     // The waveform and fade curve share the same temporary envelope every frame.     let (fade_in, fade_out) = clip_preview_fades(clip, app.gesture.as_ref().filter(|_| !dim));@@ -311,9 +307,6 @@ pub(super) fn draw_clip(     }     // Optional clip overlays (View › Clip).     let ov = Color32::from_rgb(16, 16, 16);-    if s.edit.flag("view.clip.rating") && clip.rating > 0 && name_h > 0.0 {-        painter.text(pos2(r.max.x - 18.0, r.min.y + 7.0), Align2::RIGHT_CENTER, "★".repeat(usize::from(clip.rating.min(5))), regular(9.0), ov);-    }     if s.edit.flag("view.clip.sync_point") && clip.sync_point > 0 {         let x = x_of(s, tl, clip.start + clip.sync_point);         painter.add(Shape::convex_polygon(@@ -321,7 +314,7 @@ pub(super) fn draw_clip(             Color32::WHITE,             Stroke::NONE,         ));-        painter.line_segment([pos2(x, r.min.y + name_h), pos2(x, r.max.y)], Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 255, 255, 90)));+        painter.line_segment([pos2(x, r.min.y), pos2(x, r.max.y)], Stroke::new(1.0, Color32::from_rgba_unmultiplied(255, 255, 255, 90)));     }     let time_text = if s.edit.flag("view.clip.time.current") {         Some(s.format_position(clip.start, s.edit.main_counter))@@ -366,12 +359,179 @@ pub(super) fn draw_clip(             }         }     }-    if clip.edit_locked || clip.time_locked {-        painter.text(pos2(r.max.x - 10.0, r.min.y + 7.0), Align2::CENTER_CENTER, "🔒", regular(9.0), Color32::WHITE);+    // Video clips get their pills from `video_track::draw_lane`, after their thumbnails.+    if !clip.is_video() {+        draw_clip_labels(painter, s, tl, lane, clip);+    }+    if cut.left {+        // The cut's one line, on top of the waveform so it runs the full height.+        painter.rect_filled(Rect::from_min_size(r.min, vec2(1.0, r.height())), 0.0, CLIP_OUTLINE);     }     let _ = app; } +/// Where a clip sits in its lane: one point in from the lane's top and bottom, at least one point wide.+pub(super) fn clip_rect(s: &Session, tl: Rect, lane: Rect, clip: &Clip) -> Rect {+    let x0 = x_of(s, tl, clip.start);+    let x1 = x_of(s, tl, clip.end()).max(x0 + 1.0);+    Rect::from_min_max(pos2(x0, lane.min.y + 1.0), pos2(x1, lane.max.y - 1.0))+}++/// The pills of `clip` (name, marks). Drawn right after the clip itself, so clips and overlays drawn later cover them.+pub(crate) fn draw_clip_labels(painter: &egui::Painter, s: &Session, tl: Rect, lane: Rect, clip: &Clip) {+    let r = clip_rect(s, tl, lane, clip);+    if r.max.x < lane.min.x || r.min.x > lane.max.x {+        return;+    }+    let show_name = clip_name_shown(s, lane);+    let marks = clip_marks(clip, s.edit.flag("view.clip.rating"));+    draw_clip_pills(painter, lane, r, show_name.then_some(clip.name.as_str()), &marks);+}++/// The marks a clip carries at its top-right: its star rating and a lock when edit- or time-locked.+pub(super) fn clip_marks(clip: &Clip, show_rating: bool) -> String {+    let mut marks = if show_rating && clip.rating > 0 { "★".repeat(usize::from(clip.rating.min(5))) } else { String::new() };+    if clip.edit_locked || clip.time_locked {+        if !marks.is_empty() {+            marks.push(' ');+        }+        marks.push('🔒');+    }+    marks+}++/// Gap between a pill and the clip outline's inner edge (so the pill sits 1 pt inside the outline).+pub(super) const PILL_INSET: f32 = 2.0;+pub(super) const PILL_HEIGHT: f32 = 13.0;+/// Horizontal padding between a pill's edge and its text.+pub(super) const PILL_PAD: f32 = 3.0;+/// The narrowest name pill worth drawing: its padding plus about one letter.+pub(super) const PILL_MIN_WIDTH: f32 = 12.0;+/// Half-transparent black behind white text, readable on the light and the dark clip bodies alike.+pub(super) const PILL_BG: Color32 = Color32::from_black_alpha(125);++/// Width of the name pill: the text plus its padding, capped to the `room` the clip leaves it (the text is then+/// cut cleanly at the pill's edge, with no ellipsis). `None` when there is no room for anything useful.+pub(super) fn name_pill_width(room: f32, text_w: f32) -> Option<f32> {+    if room.is_nan() || room < PILL_MIN_WIDTH {+        return None;+    }+    Some((text_w.max(0.0) + 2.0 * PILL_PAD).min(room))+}++/// Left edge of the marks pill, which sits at the clip's top-right: `None` when the clip is too narrow to hold it+/// (it would veil the clip), where `left` is the leftmost x a pill may use.+pub(super) fn marks_pill_left(left: f32, clip_right: f32, w: f32) -> Option<f32> {+    let x = clip_right - PILL_INSET - w;+    (x >= left).then_some(x)+}++/// The clip's name as a small rounded pill at its top-left, and its marks (rating, lock) as a matching pill at the+/// top-right. The name keeps clear of the marks and floats to the left edge of the lane when the clip starts+/// off-screen, so it stays readable while scrolling.+fn draw_clip_pills(painter: &egui::Painter, lane: Rect, r: Rect, name: Option<&str>, marks: &str) {+    let p = painter.with_clip_rect(painter.clip_rect().intersect(r));+    let pill = |rect: Rect, galley: Arc<egui::Galley>| {+        p.rect_filled(rect, CornerRadius::same(3), PILL_BG);+        let at = pos2(rect.min.x + PILL_PAD, rect.center().y - galley.size().y * 0.5);+        p.with_clip_rect(p.clip_rect().intersect(rect)).galley(at, galley, Color32::WHITE);+    };+    let top = r.min.y + PILL_INSET;+    let left = r.min.x.max(lane.min.x) + PILL_INSET;+    let mut right = r.max.x.min(lane.max.x) - PILL_INSET;+    if !marks.is_empty() {+        let galley = p.layout_no_wrap(marks.to_owned(), regular(9.0), Color32::WHITE);+        let w = galley.size().x + 2.0 * PILL_PAD;+        if let Some(x) = marks_pill_left(left, r.max.x, w) {+            right = right.min(x - PILL_INSET);+            pill(Rect::from_min_size(pos2(x, top), vec2(w, PILL_HEIGHT)), galley);+        }+    }+    // No layout where no pill fits: zoomed out on a large session, most clips are too narrow.+    if let Some(name) = name.filter(|n| !n.is_empty() && right - left >= PILL_MIN_WIDTH) {+        let galley = p.layout_no_wrap(name.to_owned(), bold(10.0), Color32::WHITE);+        if let Some(w) = name_pill_width(right - left, galley.size().x) {+            pill(Rect::from_min_size(pos2(left, top), vec2(w, PILL_HEIGHT)), galley);+        }+    }+}++/// The 1 pt outline around a clip.+const CLIP_OUTLINE: Color32 = Color32::from_rgb(10, 10, 10);++/// How a clip meets its neighbours in the playlist. Two clips butted at a cut share ONE 1 pt line, as in+/// the incumbent: the later clip draws it (its left edge), the earlier one drops its right edge, and both+/// sides of the cut have square corners. A gap or an overlap leaves the clip drawn on its own.+/// Selection only recolours the body, so it never changes which edges are drawn.+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]+pub(super) struct ClipCut {+    /// Starts exactly where the clip before it ends.+    pub(super) left: bool,+    /// Ends exactly where the clip after it starts.+    pub(super) right: bool,+}++impl ClipCut {+    /// The body's corners: square on a side that meets another clip, rounded otherwise.+    pub(super) fn corners(self) -> CornerRadius {+        let (nw, ne) = (if self.left { 0 } else { 2 }, if self.right { 0 } else { 2 });+        CornerRadius { nw, ne, sw: nw, se: ne }+    }+}++/// The cut of the clip spanning `start..end`, given the `(start, end)` of every other clip of its playlist, in any+/// order (a playlist's order is its stacking order, not its time order). A side is butted when another clip ends+/// (or starts) exactly there and no third clip covers that point.+pub(super) fn clip_cut(start: Samples, end: Samples, others: impl IntoIterator<Item = (Samples, Samples)>) -> ClipCut {+    // An empty clip has no span to share a line with.+    if end <= start {+        return ClipCut::default();+    }+    let (mut left, mut right, mut left_covered, mut right_covered) = (false, false, false, false);+    for (s, e) in others.into_iter().filter(|(s, e)| e > s) {+        left |= e == start;+        right |= s == end;+        left_covered |= s < start && start < e;+        right_covered |= s < end && end < e;+    }+    ClipCut { left: left && !left_covered, right: right && !right_covered }+}++/// [`clip_cut`] for `clip` among its playlist's `clips`, scanning them all.+pub(super) fn clip_cut_in(clips: &[Clip], clip: &Clip) -> ClipCut {+    clip_cut(clip.start, clip.end(), clips.iter().filter(|c| c.id != clip.id).map(|c| (c.start, c.end())))+}++/// [`clip_cut`] for each clip of a playlist. Sorted by start (the usual case), one pass over the playlist and two+/// binary searches per clip, so large sessions do no per-clip scan every frame; unsorted, a scan per clip.+pub(super) fn clip_cuts(clips: &[Clip]) -> impl Fn(&Clip) -> ClipCut + '_ {+    // reach[k]: the furthest end of the first k clips (empty clips reach nothing).+    let reach: Option<Vec<Samples>> = clips.is_sorted_by_key(|c| c.start).then(|| {+        std::iter::once(Samples::MIN)+            .chain(clips.iter().scan(Samples::MIN, |far, c| {+                if c.end() > c.start {+                    *far = (*far).max(c.end());+                }+                Some(*far)+            }))+            .collect()+    });+    move |clip| {+        let Some(reach) = &reach else { return clip_cut_in(clips, clip) };+        let (start, end) = (clip.start, clip.end());+        if end <= start {+            return ClipCut::default();+        }+        let reached = |k: usize| reach.get(k).copied().unwrap_or(Samples::MIN);+        // Butted and uncovered on the left: the clips starting before it reach exactly its start. On the right: the+        // clips starting before its end (itself included) reach exactly its end, and a clip starts there.+        let before = clips.partition_point(|c| c.start < start);+        let upto = clips.partition_point(|c| c.start < end);+        let next = clips.get(upto..).unwrap_or_default().iter().take_while(|c| c.start == end).any(|c| c.end() > c.start);+        ClipCut { left: reached(before) == start, right: next && reached(upto) == end }+    }+}+ /// The earlier clip of a crossfade pair: the one before `clip` in its playlist, overlapping its start. fn crossfade_partner<'a>(track: &'a Track, clip: &Clip) -> Option<&'a Clip> {     let (p, i) = track.playlists.iter().find_map(|p| {diff --git a/crates/ui-egui/src/edit_window/input.rs b/crates/ui-egui/src/edit_window/input.rs--- a/crates/ui-egui/src/edit_window/input.rs+++ b/crates/ui-egui/src/edit_window/input.rs@@ -1,5 +1,5 @@ use super::automation::{automation_id, breakpoint_at, plugin_range, shown_automation};-use super::clips::{clip_name_height, fade_control_position};+use super::clips::fade_control_position; use super::drag::{drag_snap, fade_curve_drag_value, fade_drag_range, spot_or_run}; use super::layout::{sample_at, x_of}; use super::*;@@ -20,7 +20,7 @@ fn fade_handle_at(s: &Session, tl: Rect, lane: Rect, clip: &Clip, p: Pos2) -> Op     if clip.length <= 0 || clip.end() <= clip.start {         return None;     }-    let y = lane.min.y + 1.0 + clip_name_height(s, lane);+    let y = lane.min.y + 1.0;     [(true, clip.fade_in), (false, clip.fade_out)]         .into_iter()         .filter(|(_, fade)| fade.len > 0)@@ -51,7 +51,7 @@ pub(super) fn fade_midpoint(s: &Session, tl: Rect, lane: Rect, clip: &Clip, fade     if b - a < 32.0 {         return None;     }-    let body = Rect::from_min_max(pos2(a, lane.min.y + 1.0 + clip_name_height(s, lane)), pos2(b, lane.max.y - 1.0));+    let body = Rect::from_min_max(pos2(a, lane.min.y + 1.0), pos2(b, lane.max.y - 1.0));     let p = fade_control_position(fade, a, b, body);     lane.contains(p).then_some(p) }@@ -252,7 +252,7 @@ pub(super) fn lane_interaction(app: &mut SoundApp, ui: &mut Ui, track: &Track, l     {         let fade = if fade_in { c.fade_in } else { c.fade_out };         let anchor = (origin.y, fade.gain(0.5));-        let height = (lane.height() - 2.0 - clip_name_height(&s, lane)).max(1.0);+        let height = (lane.height() - 2.0).max(1.0);         app.gesture = Some(Gesture::FadeCurve { clip: c.id, fade_in, curvature: fade_curve_drag_value(fade, anchor, height, p.y), anchor, height });         ui.ctx().request_repaint();     } else if resp.drag_started()diff --git a/crates/ui-egui/src/edit_window/tests.rs b/crates/ui-egui/src/edit_window/tests.rs--- a/crates/ui-egui/src/edit_window/tests.rs+++ b/crates/ui-egui/src/edit_window/tests.rs@@ -967,7 +967,7 @@ fn fade_midpoint_handles_remain_visible_and_can_be_grabbed_again() {                         h.step();                         assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::ResizeVertical);                         start_drag(&mut h, control);-                        let to = control - vec2(0.0, (lane.height() - 2.0 - clip_name_height(&before, lane)) * 0.1);+                        let to = control - vec2(0.0, (lane.height() - 2.0) * 0.1);                         h.hover_at(to);                         h.step();                         assert!(@@ -1482,3 +1482,266 @@ fn alt_click_on_a_breakpoint_deletes_it() {         assert_eq!(h.state().engine.session().track(track).unwrap().clips(), clips.as_slice(), "{tool}");     } }++const ALONE: ClipCut = ClipCut { left: false, right: false };++#[test]+fn butted_pair_shares_one_seam() {+    // 0..100 then 100..200: the first drops its right edge, the second draws the seam on its left.+    assert_eq!(clip_cut(0, 100, [(100, 200)]), ClipCut { left: false, right: true });+    assert_eq!(clip_cut(100, 200, [(0, 100)]), ClipCut { left: true, right: false });+}++#[test]+fn middle_clip_of_a_run_is_butted_on_both_sides() {+    let cut = clip_cut(100, 200, [(0, 100), (200, 300)]);+    assert_eq!(cut, ClipCut { left: true, right: true });+    let c = cut.corners();+    assert_eq!((c.nw, c.sw, c.ne, c.se), (0, 0, 0, 0));+}++#[test]+fn gap_overlap_and_lone_clips_keep_their_own_outline() {+    // First and last clips: nothing on the open side.+    assert_eq!(clip_cut(0, 100, []), ALONE);+    // A gap of one sample is not a cut.+    assert_eq!(clip_cut(101, 200, [(0, 100), (201, 300)]), ALONE);+    // Overlaps (crossfades, layers) keep both outlines.+    assert_eq!(clip_cut(90, 200, [(0, 100), (150, 300)]), ALONE);+    assert_eq!(clip_cut(0, 100, [(60, 160)]), ALONE);+    // A third clip lying across the cut hides it: no shared seam.+    assert_eq!(clip_cut(100, 200, [(0, 100), (50, 150)]), ALONE);+    // Empty clips share nothing, and are not neighbours either.+    assert_eq!(clip_cut(100, 100, [(0, 100), (100, 200)]), ALONE);+    assert_eq!(clip_cut(100, 200, [(100, 100)]), ALONE);+}++#[test]+fn sorted_playlists_find_the_same_cuts_without_a_scan() {+    let layouts: [&[(Samples, Samples)]; 8] = [+        &[(0, 100), (100, 100)],+        &[(0, 100), (100, 100), (200, 100)],+        &[(0, 100), (101, 99), (201, 99)],+        &[(0, 100), (90, 110), (150, 150)],+        &[(0, 100), (50, 100), (100, 100)],+        &[(0, 100), (100, 0), (100, 100)],+        &[(0, 1000), (100, 100), (200, 100)],+        &[(0, 100), (100, 50), (100, 100), (200, 100)],+    ];+    for spans in layouts {+        let clips: Vec<Clip> = spans.iter().zip(1..).map(|(&(start, len), id)| test_clip(id, start, len)).collect();+        let cut_of = clip_cuts(&clips);+        for c in &clips {+            assert_eq!(cut_of(c), clip_cut_in(&clips, c), "{spans:?}, clip at {}", c.start);+        }+    }+    // Not vacuous: the middle clip of a run is butted on both sides.+    let run: Vec<Clip> = (0..3).map(|i| test_clip(i + 1, i as Samples * 100, 100)).collect();+    assert_eq!(clip_cuts(&run)(&run[1]), ClipCut { left: true, right: true });+}++#[test]+fn only_butted_sides_lose_their_round_corners() {+    let round = ALONE.corners();+    assert_eq!((round.nw, round.sw, round.ne, round.se), (2, 2, 2, 2));+    let first = ClipCut { left: false, right: true }.corners();+    assert_eq!((first.nw, first.sw, first.ne, first.se), (2, 2, 0, 0));+    let second = ClipCut { left: true, right: false }.corners();+    assert_eq!((second.nw, second.sw, second.ne, second.se), (0, 0, 2, 2));+}++#[test]+fn name_pill_is_as_wide_as_its_text_and_cut_cleanly_on_short_clips() {+    // Room to spare: the text plus its padding on both sides.+    assert_eq!(name_pill_width(200.0, 50.0), Some(50.0 + 2.0 * PILL_PAD));+    // A short clip caps the pill at the room it leaves; the text is then cut at the pill's edge.+    assert_eq!(name_pill_width(30.0, 80.0), Some(30.0));+    assert_eq!(name_pill_width(PILL_MIN_WIDTH, 80.0), Some(PILL_MIN_WIDTH));+    // Too narrow to show anything useful, or nonsense numbers: no pill.+    assert_eq!(name_pill_width(PILL_MIN_WIDTH - 0.1, 80.0), None);+    assert_eq!(name_pill_width(-5.0, 80.0), None);+    assert_eq!(name_pill_width(f32::NAN, 80.0), None);+    // A bogus text width never makes a negative or NaN pill.+    assert_eq!(name_pill_width(100.0, f32::NAN), Some(2.0 * PILL_PAD));+    assert_eq!(name_pill_width(100.0, -40.0), Some(2.0 * PILL_PAD));+}++#[test]+fn marks_list_the_rating_and_the_lock() {+    let mut clip = test_clip(1, 0, 100);+    assert_eq!(clip_marks(&clip, true), "");+    clip.rating = 3;+    assert_eq!(clip_marks(&clip, true), "★★★");+    assert_eq!(clip_marks(&clip, false), "");+    clip.edit_locked = true;+    assert_eq!(clip_marks(&clip, true), "★★★ 🔒");+    assert_eq!(clip_marks(&clip, false), "🔒");+    clip.rating = 200;+    assert_eq!(clip_marks(&clip, true), "★★★★★ 🔒");+}++#[test]+fn the_clip_name_is_a_pill_over_the_waveform_not_a_bar() {+    for show_name in [true, false] {+        let mut h = smart_demo();+        h.state_mut().engine.execute("view.clip_name", &json!({"value": show_name})).unwrap();+        h.step();+        let tl = timeline(&h);+        let (clip, lane) = clip_on_screen(&h, |xs, xe| xs > tl.min.x + 30.0 && xe < tl.max.x - 100.0 && xe - xs > 100.0);+        let s = h.state().engine.session();+        let (xs, xe) = (x_of(s, tl, clip.start), x_of(s, tl, clip.end()));+        let clip_rect = Rect::from_min_max(pos2(xs, lane.min.y), pos2(xe, lane.max.y));+        let shapes = &h.output().shapes;+        let pills: Vec<Rect> = shapes+            .iter()+            .filter_map(|shape| match &shape.shape {+                Shape::Rect(r) if r.fill == PILL_BG && clip_rect.contains_rect(r.rect) => Some(r.rect),+                _ => None,+            })+            .collect();+        if !show_name {+            assert!(pills.is_empty(), "names are off");+            continue;+        }+        assert_eq!(pills.len(), 1, "one pill per clip");+        let pill = pills[0];+        // At the top-left, 1 pt inside the outline, as wide as its text plus padding: far from a full-width bar.+        assert_eq!(pill.min, pos2(xs + PILL_INSET, lane.min.y + 1.0 + PILL_INSET));+        assert_eq!(pill.height(), PILL_HEIGHT);+        let text_w = shapes+            .iter()+            .find_map(|shape| match &shape.shape {+                Shape::Text(t) if t.galley.text() == clip.name && pill.contains(t.pos) => Some(t.galley.size().x),+                _ => None,+            })+            .expect("the name is drawn on the pill");+        assert_eq!(pill.width(), text_w + 2.0 * PILL_PAD);+        assert!(pill.width() < clip_rect.width() * 0.5);+    }+}++#[test]+fn the_rating_stars_follow_the_rating_switch_whatever_the_name_switch_says() {+    for (show_name, show_rating) in [(false, true), (true, true), (true, false), (false, false)] {+        let mut h = smart_demo();+        for (cmd, on) in [("view.clip_name", show_name), ("view.clip_rating", show_rating)] {+            h.state_mut().engine.execute(cmd, &json!({"value": on})).unwrap();+        }+        h.state_mut().engine.session_mut().tracks.iter_mut().flat_map(|t| &mut t.playlists).flat_map(|p| &mut p.clips).for_each(|c| c.rating = 3);+        h.step();+        let stars = h.output().shapes.iter().filter(|c| matches!(&c.shape, Shape::Text(t) if t.galley.text() == "★★★")).count();+        assert_eq!(stars > 0, show_rating, "names {show_name}, rating {show_rating}: {stars} rated clips drew their stars");+    }+}++#[test]+fn alternate_playlist_lanes_draw_only_the_clips_on_screen() {+    let mut h = harness(1);+    let tl = timeline(&h);+    let s = h.state().engine.session();+    let (view, color) = (Range::new(sample_at(s, tl, tl.min.x), sample_at(s, tl, tl.max.x)), s.tracks[0].color);+    let len = (view.end - view.start) / 20;+    // One clip well inside the view, one running past its right edge, two far beyond it.+    let starts = [view.start + 4 * len, view.end - len / 2, view.end + 20 * len, view.end + 2_000 * len];+    let s = h.state_mut().engine.session_mut();+    let mut alt = soundcraft_model::Playlist::new("alt");+    alt.clips = starts.iter().map(|&at| test_clip(s.new_clip_id().0, at, len)).collect();+    let track = s.tracks.first_mut().unwrap();+    (track.view, track.active_playlist) = ("playlists".into(), 0);+    track.playlists.push(alt);+    h.run_steps(3);+    // The main playlist is empty: every clip body in the track's rows belongs to the alternate lane.+    let bodies: Vec<Rect> = h+        .output()+        .shapes+        .iter()+        .filter_map(|c| match &c.shape {+            Shape::Rect(r) if r.fill == clip_colors(color, false).0 => Some(r.rect),+            _ => None,+        })+        .collect();+    assert_eq!(bodies.len(), 2, "only the two clips that show are drawn: {bodies:?}");+    assert!(bodies.iter().all(|r| r.max.x > tl.min.x && r.min.x < tl.max.x), "{bodies:?} are not all on screen");+}++fn test_clip(id: u64, start: Samples, len: Samples) -> Clip {+    Clip::audio(ClipId(id), "c", soundcraft_model::SourceId(0), 0, start, len)+}++#[test]+fn cut_is_found_among_the_playlist_neighbours() {+    // Selection never enters: it recolours the body, so a selected clip gets the same edges as any other.+    let clips = [test_clip(1, 0, 100), test_clip(2, 100, 100), test_clip(3, 200, 100), test_clip(4, 400, 50)];+    let cuts: Vec<ClipCut> = clips.iter().map(|c| clip_cut_in(&clips, c)).collect();+    assert_eq!(cuts, [ClipCut { left: false, right: true }, ClipCut { left: true, right: true }, ClipCut { left: true, right: false }, ALONE]);+}++#[test]+fn cut_does_not_depend_on_the_playlist_order() {+    // Bring to Front / Send to Back reorder a playlist (its order is the stacking order): the seam stays.+    let (a, b) = (test_clip(1, 0, 100), test_clip(2, 100, 100));+    let reordered = [b.clone(), a.clone()];+    assert_eq!(clip_cut_in(&reordered, &a), ClipCut { left: false, right: true });+    assert_eq!(clip_cut_in(&reordered, &b), ClipCut { left: true, right: false });+    let run = [test_clip(3, 200, 100), test_clip(1, 0, 100), test_clip(2, 100, 100)];+    let cuts: Vec<ClipCut> = run.iter().map(|c| clip_cut_in(&run, c)).collect();+    assert_eq!(cuts, [ClipCut { left: true, right: false }, ClipCut { left: false, right: true }, ClipCut { left: true, right: true }]);+}++#[test]+fn layered_clips_share_a_seam_only_where_they_really_meet() {+    // A short clip inside a long one does not hide the seam between the long clip and the next.+    let clips = [test_clip(1, 0, 100), test_clip(2, 10, 10), test_clip(3, 100, 100)];+    assert_eq!(clip_cut_in(&clips, &clips[2]), ClipCut { left: true, right: false });+    assert_eq!(clip_cut_in(&clips, &clips[0]), ClipCut { left: false, right: true });+    assert_eq!(clip_cut_in(&clips, &clips[1]), ALONE);+    // Two clips on one start, and a lone clip.+    let same = [test_clip(1, 0, 100), test_clip(2, 0, 100), test_clip(3, 300, 100)];+    assert_eq!(clip_cut_in(&same, &same[1]), ALONE);+    assert_eq!(clip_cut_in(&same, &same[2]), ALONE);+    assert_eq!(clip_cut_in(&[], &test_clip(9, 0, 100)), ALONE);+}++#[test]+fn marks_pill_is_hidden_when_the_clip_is_too_narrow_for_it() {+    // A 60 pt clip from x=100 (pills start at 102) holds a 30 pt pill at its right end.+    assert_eq!(marks_pill_left(102.0, 160.0, 30.0), Some(128.0));+    // Exactly full: the pill's left edge meets the leftmost x.+    assert_eq!(marks_pill_left(102.0, 134.0, 30.0), Some(102.0));+    // Any narrower and it would veil the clip: hidden.+    assert_eq!(marks_pill_left(102.0, 133.0, 30.0), None);+    assert_eq!(marks_pill_left(102.0, f32::NAN, 30.0), None);+}++#[test]+fn an_overlapping_clip_covers_the_pill_of_the_clip_below() {+    let mut h = smart_demo();+    h.step();+    let tl = timeline(&h);+    let (below, lane) = clip_on_screen(&h, |xs, xe| xs > tl.min.x + 30.0 && xe < tl.max.x - 100.0 && xe - xs > 100.0);+    // A copy on top (last in the playlist), starting at the same place.+    let s = h.state_mut().engine.session_mut();+    let (track, _) = s.find_clip(below.id).unwrap();+    let id = s.new_clip_id();+    let top = Clip { id, name: "Top".into(), ..below.clone() };+    s.track_mut(track).unwrap().playlist_mut().unwrap().clips.push(top);+    h.step();+    h.step();+    let s = h.state().engine.session();+    let at = pos2(x_of(s, tl, below.start) + PILL_INSET, lane.min.y + 1.0 + PILL_INSET);+    let shapes = &h.output().shapes;+    let pills: Vec<(usize, Rect)> = shapes+        .iter()+        .enumerate()+        .filter_map(|(i, shape)| match &shape.shape {+            Shape::Rect(r) if r.fill == PILL_BG && r.rect.min == at => Some((i, r.rect)),+            _ => None,+        })+        .collect();+    assert_eq!(pills.len(), 2, "both clips draw a pill at the same spot");+    let ((lower, pill), (upper, _)) = (pills[0], pills[1]);+    assert!(+        shapes[lower + 1..upper].iter().any(|shape| matches!(&shape.shape, Shape::Rect(r) if r.fill != PILL_BG && r.rect.contains_rect(pill))),+        "the top clip's body is painted between the two pills, so it hides the lower pill"+    );+}diff --git a/crates/ui-egui/src/edit_window/tracks.rs b/crates/ui-egui/src/edit_window/tracks.rs--- a/crates/ui-egui/src/edit_window/tracks.rs+++ b/crates/ui-egui/src/edit_window/tracks.rs@@ -86,11 +86,12 @@ pub(super) fn track_row(app: &mut SoundApp, ui: &mut Ui, id: TrackId, row: Rect,     let view = Range::new(sample_at(&s, tl, tl.min.x), sample_at(&s, tl, tl.max.x));     let auto_view = AutoParam::parse(&track.view);     let sorted = track.clips().is_sorted_by_key(|c| c.start);+    let cut_of = clip_cuts(track.clips());     for clip in track.clips() {         if !clip.range().overlaps(&view) {             continue;         }-        draw_clip(app, &painter, &s, track, sorted, clip, tl, lane, auto_view.is_some());+        draw_clip(app, &painter, &s, track, sorted, cut_of(clip), clip, tl, lane, auto_view.is_some());     }     draw_live_take(app, &painter, &s, track, tl, lane);     crate::video_track::draw_lane(app, &painter, &s, track, tl, lane);@@ -139,8 +140,9 @@ fn playlist_lanes(app: &mut SoundApp, ui: &mut Ui, track: &Track, row: Rect, mai         let painter = ui.painter().with_clip_rect(lane.intersect(ui.clip_rect()));         let sorted = track.clips().is_sorted_by_key(|c| c.start);         let view = Range::new(sample_at(&s, tl, tl.min.x), sample_at(&s, tl, tl.max.x));+        let cut_of = clip_cuts(&pl.clips);         for c in pl.clips.iter().filter(|c| c.range().overlaps(&view)) {-            draw_clip(app, &painter, &s, track, sorted, c, tl, lane, false);+            draw_clip(app, &painter, &s, track, sorted, cut_of(c), c, tl, lane, false);         }         let resp = ui.interact(lane, ui.scope_id().with(("pl_lane", track.id.0, i)), Sense::click());         if resp.clicked()diff --git a/crates/ui-egui/src/video_track.rs b/crates/ui-egui/src/video_track.rs--- a/crates/ui-egui/src/video_track.rs+++ b/crates/ui-egui/src/video_track.rs@@ -432,62 +432,65 @@ pub fn draw_lane(app: &mut SoundApp, painter: &Painter, s: &Session, track: &Tra     let ctx = painter.ctx().clone();     app.video.poll(&ctx);     let on = online(s);-    let name_h = if lane.height() >= 40.0 && s.edit.flag("view.clip.name") { 13.0 } else { 0.0 };     let ppp = ctx.pixels_per_point();     for clip in track.clips() {         let ClipContent::Video { source, offset } = clip.content else { continue };-        let x0 = x_of(s, tl, clip.start);-        let x1 = x_of(s, tl, clip.end()).max(x0 + 1.0);-        let body = Rect::from_min_max(pos2(x0 + 1.0, lane.min.y + 2.0 + name_h), pos2(x1 - 1.0, lane.max.y - 2.0));-        let vis = body.intersect(lane);-        if vis.width() < 2.0 || vis.height() < 6.0 {-            continue;-        }-        let p = painter.with_clip_rect(vis);-        let Some(v) = s.video(source) else {-            p.text(vis.center(), egui::Align2::CENTER_CENTER, "movie missing from session", regular(10.0), Color32::from_rgb(220, 120, 120));-            continue;-        };-        if !on {-            p.text(vis.center(), egui::Align2::CENTER_CENTER, "Video Track Offline", regular(10.0), Color32::from_gray(170));-            continue;-        }-        if let Some(why) = &v.offline {-            p.text(vis.center(), egui::Align2::CENTER_CENTER, format!("media offline: {why}"), regular(10.0), Color32::from_rgb(220, 120, 120));-            continue;-        }-        let key = MovieKey::new(source, &v.path);-        if let Some(Err(e)) = app.video.info(key) {-            p.text(vis.center(), egui::Align2::CENTER_CENTER, format!("media offline: {e}"), regular(10.0), Color32::from_rgb(220, 120, 120));-            continue;-        }-        let aspect = app.video.info(key).and_then(|i| i.as_ref().ok()).map_or(v.aspect(), MovieInfo::display_aspect) as f32;-        let th = body.height();-        let tw = (th * aspect).clamp(8.0, 600.0);-        // Pixel height bucket (multiples of 16) so zooming the track does not re-decode constantly.-        let hpx = (((th * ppp) / 16.0).ceil() * 16.0).clamp(16.0, 256.0) as u32;-        let first = ((vis.min.x - body.min.x) / tw).floor().max(0.0) as i64;-        let mut k = first;-        loop {-            let tx = body.min.x + k as f32 * tw;-            if tx > vis.max.x || k > first + 400 {-                break;+        'thumbs: {+            let x0 = x_of(s, tl, clip.start);+            let x1 = x_of(s, tl, clip.end()).max(x0 + 1.0);+            let body = Rect::from_min_max(pos2(x0 + 1.0, lane.min.y + 2.0), pos2(x1 - 1.0, lane.max.y - 2.0));+            let vis = body.intersect(lane);+            if vis.width() < 2.0 || vis.height() < 6.0 {+                break 'thumbs;             }-            let at = sample_at(s, tl, tx);-            let secs = movie_secs(s, clip.start, offset, at.max(clip.start));-            let r = Rect::from_min_max(pos2(tx, body.min.y), pos2((tx + tw).min(body.max.x), body.max.y));-            match app.video.thumb(&ctx, key, &v.path, secs.max(0.0), hpx) {-                Some(tex) => {-                    let full = Rect::from_min_size(r.min, egui::vec2(tw, th));-                    let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2((r.width() / tw).clamp(0.0, 1.0), 1.0));-                    p.image(tex.id(), Rect::from_min_max(full.min, pos2(r.max.x, full.max.y)), uv, Color32::WHITE);+            let p = painter.with_clip_rect(vis);+            let Some(v) = s.video(source) else {+                p.text(vis.center(), egui::Align2::CENTER_CENTER, "movie missing from session", regular(10.0), Color32::from_rgb(220, 120, 120));+                break 'thumbs;+            };+            if !on {+                p.text(vis.center(), egui::Align2::CENTER_CENTER, "Video Track Offline", regular(10.0), Color32::from_gray(170));+                break 'thumbs;+            }+            if let Some(why) = &v.offline {+                p.text(vis.center(), egui::Align2::CENTER_CENTER, format!("media offline: {why}"), regular(10.0), Color32::from_rgb(220, 120, 120));+                break 'thumbs;+            }+            let key = MovieKey::new(source, &v.path);+            if let Some(Err(e)) = app.video.info(key) {+                p.text(vis.center(), egui::Align2::CENTER_CENTER, format!("media offline: {e}"), regular(10.0), Color32::from_rgb(220, 120, 120));+                break 'thumbs;+            }+            let aspect = app.video.info(key).and_then(|i| i.as_ref().ok()).map_or(v.aspect(), MovieInfo::display_aspect) as f32;+            let th = body.height();+            let tw = (th * aspect).clamp(8.0, 600.0);+            // Pixel height bucket (multiples of 16) so zooming the track does not re-decode constantly.+            let hpx = (((th * ppp) / 16.0).ceil() * 16.0).clamp(16.0, 256.0) as u32;+            let first = ((vis.min.x - body.min.x) / tw).floor().max(0.0) as i64;+            let mut k = first;+            loop {+                let tx = body.min.x + k as f32 * tw;+                if tx > vis.max.x || k > first + 400 {+                    break;                 }-                None => {-                    p.rect_filled(r.shrink(1.0), 0.0, Color32::from_rgb(24, 24, 26));+                let at = sample_at(s, tl, tx);+                let secs = movie_secs(s, clip.start, offset, at.max(clip.start));+                let r = Rect::from_min_max(pos2(tx, body.min.y), pos2((tx + tw).min(body.max.x), body.max.y));+                match app.video.thumb(&ctx, key, &v.path, secs.max(0.0), hpx) {+                    Some(tex) => {+                        let full = Rect::from_min_size(r.min, egui::vec2(tw, th));+                        let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2((r.width() / tw).clamp(0.0, 1.0), 1.0));+                        p.image(tex.id(), Rect::from_min_max(full.min, pos2(r.max.x, full.max.y)), uv, Color32::WHITE);+                    }+                    None => {+                        p.rect_filled(r.shrink(1.0), 0.0, Color32::from_rgb(24, 24, 26));+                    }                 }+                p.line_segment([pos2(tx, body.min.y), pos2(tx, body.max.y)], egui::Stroke::new(1.0, Color32::from_black_alpha(160)));+                k += 1;             }-            p.line_segment([pos2(tx, body.min.y), pos2(tx, body.max.y)], egui::Stroke::new(1.0, Color32::from_black_alpha(160)));-            k += 1;         }+        // The name pill goes over the thumbnails.+        crate::edit_window::draw_clip_labels(painter, s, tl, lane, clip);     } }