mirror of
https://github.com/storytold/soundcraft.git
synced 2026-11-11 12:52:20 +00:00
Edit window: draw large edited sessions without per-clip work every frame
On a 63,424-clip drum edit the UI thread spent ~187 ms per frame, so with a plugin editor open (a frame every 30 ms) it never went idle and the editor windows, which Windows only paints when the thread's queue is empty, stopped redrawing. Now ~11 ms of CPU per frame: - Overlap shadows: each drawn clip checked every clip of its track; on a sorted track the clips starting inside it are found by binary search (sortedness is checked once per track and frame, and an unsorted track is still scanned). - Fade curves: a fade took at least 65 points, even under a pixel wide. A narrow one takes ~4 * its width in physical pixels; the chord error stays under a quarter pixel (tested). - Clip List: rows are listed and filtered once per document revision and filter text, and only the rows in view are laid out (show_rows). - Universe: each track's clips are merged into same-colour runs covering exactly the same pixels, cached per revision and area (5,000 clips become a handful of rects). - Session::content_end, which walks every clip, is worked out once per revision. - Alternate playlist lanes skip clips outside the view, like the main lane, and clip names are not laid out where no glyph would show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
25d52c646a
commit
09c2e71141
2 files changed
+274
-42
No files matched your search
@@ -115,26 +115,81 @@ pub fn show(app: &mut SoundApp, ui: &mut Ui) {
|
||||
crate::docking::show(app, ui, crate::MainWindow::Edit);
|
||||
}
|
||||
|
||||
/// Universe: the whole session in miniature; the box is the visible area (drag or click to move).
|
||||
fn universe(app: &mut SoundApp, ui: &mut Ui) {
|
||||
let r = ui.max_rect().shrink2(vec2(8.0, 4.0));
|
||||
let s = app.engine.session();
|
||||
/// The Universe's clip blocks for one document revision and area: each track's clips merged into
|
||||
/// runs of one colour. A drum edit holds tens of thousands of clips a pixel or less wide here;
|
||||
/// painting one rect per clip every frame would cost more than all the rest of the window.
|
||||
#[derive(Clone)]
|
||||
struct UniverseRuns {
|
||||
revision: u64,
|
||||
area: Rect,
|
||||
runs: std::sync::Arc<Vec<(Rect, Color32)>>,
|
||||
}
|
||||
|
||||
/// Each clip covers `[x0, max(x1, x0 + 1)]`; neighbours of one colour that touch or overlap are
|
||||
/// painted as one rect, which covers exactly the same pixels in the same order.
|
||||
fn universe_runs(s: &Session, r: Rect) -> Vec<(Rect, Color32)> {
|
||||
let total = (s.content_end() + s.sample_rate.samples(10.0)).max(1) as f32;
|
||||
let tracks: Vec<&Track> = s.tracks.iter().filter(|t| !t.hidden).collect();
|
||||
let n = tracks.len().max(1) as f32;
|
||||
let rh = (r.height() / n).max(1.0);
|
||||
let mut runs = Vec::new();
|
||||
for (i, t) in tracks.iter().enumerate() {
|
||||
let y = r.min.y + i as f32 * rh;
|
||||
let block = |x0: f32, x1: f32, color| (Rect::from_min_max(pos2(x0, y + 0.5), pos2(x1, y + rh - 0.5)), color);
|
||||
let mut run: Option<(f32, f32, Color32)> = None;
|
||||
for c in t.clips() {
|
||||
let x0 = r.min.x + c.start as f32 / total * r.width();
|
||||
let x1 = r.min.x + c.end() as f32 / total * r.width();
|
||||
ui.painter().rect_filled(
|
||||
Rect::from_min_max(pos2(x0, y + 0.5), pos2(x1.max(x0 + 1.0), y + rh - 0.5)),
|
||||
0.0,
|
||||
rgb(c.color.unwrap_or(t.color)),
|
||||
);
|
||||
let x1 = (r.min.x + c.end() as f32 / total * r.width()).max(x0 + 1.0);
|
||||
let color = rgb(c.color.unwrap_or(t.color));
|
||||
run = match run {
|
||||
Some((a, b, col)) if col == color && x0 >= a && x0 <= b => Some((a, b.max(x1), col)),
|
||||
Some((a, b, col)) => {
|
||||
runs.push(block(a, b, col));
|
||||
Some((x0, x1, color))
|
||||
}
|
||||
None => Some((x0, x1, color)),
|
||||
};
|
||||
}
|
||||
if let Some((a, b, col)) = run {
|
||||
runs.push(block(a, b, col));
|
||||
}
|
||||
}
|
||||
runs
|
||||
}
|
||||
|
||||
/// [`Session::content_end`], worked out once per document revision: it walks every clip.
|
||||
fn content_end(app: &SoundApp, ctx: &egui::Context) -> Samples {
|
||||
let key = egui::Id::new("content_end");
|
||||
let revision = app.engine.revision;
|
||||
if let Some((at, end)) = ctx.memory(|m| m.data.get_temp::<(u64, Samples)>(key))
|
||||
&& at == revision
|
||||
{
|
||||
return end;
|
||||
}
|
||||
let end = app.engine.session().content_end();
|
||||
ctx.memory_mut(|m| m.data.insert_temp(key, (revision, end)));
|
||||
end
|
||||
}
|
||||
|
||||
/// Universe: the whole session in miniature; the box is the visible area (drag or click to move).
|
||||
fn universe(app: &mut SoundApp, ui: &mut Ui) {
|
||||
let r = ui.max_rect().shrink2(vec2(8.0, 4.0));
|
||||
let key = egui::Id::new("universe_runs");
|
||||
let revision = app.engine.revision;
|
||||
let runs = match ui.ctx().memory(|m| m.data.get_temp::<UniverseRuns>(key)).filter(|u| u.revision == revision && u.area == r) {
|
||||
Some(u) => u.runs,
|
||||
None => {
|
||||
let runs = std::sync::Arc::new(universe_runs(app.engine.session(), r));
|
||||
ui.ctx().memory_mut(|m| m.data.insert_temp(key, UniverseRuns { revision, area: r, runs: runs.clone() }));
|
||||
runs
|
||||
}
|
||||
};
|
||||
for &(rect, color) in runs.iter() {
|
||||
ui.painter().rect_filled(rect, 0.0, color);
|
||||
}
|
||||
let end = content_end(app, ui.ctx());
|
||||
let s = app.engine.session();
|
||||
let total = (end + s.sample_rate.samples(10.0)).max(1) as f32;
|
||||
let vis0 = s.edit.zoom.scroll as f32 / total;
|
||||
let tlw = app.edit_layout.timeline[2] - app.edit_layout.timeline[0];
|
||||
let vis1 = vis0 + (f64::from(tlw.max(100.0)) * s.edit.zoom.samples_per_px) as f32 / total;
|
||||
@@ -373,9 +428,10 @@ fn pan(app: &mut SoundApp, px: f64) {
|
||||
fn hscrollbar(app: &mut SoundApp, ui: &mut Ui, sb: Rect, tl: Rect) {
|
||||
let t = Tokens::current();
|
||||
ui.painter().rect_filled(sb, 0.0, t.panel_bg);
|
||||
let end = content_end(app, ui.ctx());
|
||||
let s = app.engine.session();
|
||||
let vis = f64::from(tl.width()) * s.edit.zoom.samples_per_px;
|
||||
let total = (s.content_end().saturating_add(s.sample_rate.samples(30.0)).max(1) as f64).max(s.edit.zoom.scroll as f64 + vis);
|
||||
let total = (end.saturating_add(s.sample_rate.samples(30.0)).max(1) as f64).max(s.edit.zoom.scroll as f64 + vis);
|
||||
let frac = (vis / total).clamp(0.02, 1.0) as f32;
|
||||
let off = (s.edit.zoom.scroll as f64 / total).clamp(0.0, 1.0) as f32;
|
||||
let thumb = Rect::from_min_size(pos2(sb.min.x + sb.width() * off, sb.min.y + 3.0), vec2((sb.width() * frac).max(24.0), sb.height() - 6.0));
|
||||
@@ -838,11 +894,12 @@ fn track_row(app: &mut SoundApp, ui: &mut Ui, id: TrackId, row: Rect, tl: 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);
|
||||
for clip in track.clips() {
|
||||
if !clip.range().overlaps(&view) {
|
||||
continue;
|
||||
}
|
||||
draw_clip(app, &painter, &s, track, clip, tl, lane, auto_view.is_some());
|
||||
draw_clip(app, &painter, &s, track, sorted, 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);
|
||||
@@ -885,8 +942,10 @@ fn playlist_lanes(app: &mut SoundApp, ui: &mut Ui, track: &Track, row: Rect, mai
|
||||
ui.painter().rect_filled(lane, 0.0, t.playlist_lane);
|
||||
ui.painter().text(pos2(head.min.x + 4.0, head.center().y), Align2::LEFT_CENTER, &pl.name, regular(10.5), t.text_dim);
|
||||
let painter = ui.painter().with_clip_rect(lane.intersect(ui.clip_rect()));
|
||||
for c in &pl.clips {
|
||||
draw_clip(app, &painter, &s, track, c, tl, lane, false);
|
||||
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));
|
||||
for c in pl.clips.iter().filter(|c| c.range().overlaps(&view)) {
|
||||
draw_clip(app, &painter, &s, track, sorted, c, tl, lane, false);
|
||||
}
|
||||
let resp = ui.interact(lane, ui.id().with(("pl_lane", track.id.0, i)), Sense::click());
|
||||
if resp.clicked()
|
||||
@@ -1511,9 +1570,13 @@ fn fade_curve_points(fade: soundcraft_model::Fade, fade_in: bool, a: f32, b: f32
|
||||
let scale = if pixels_per_point.is_finite() { pixels_per_point.clamp(0.5, 4.0) } else { 1.0 };
|
||||
let extent = (b - a).abs().max(body.height().abs()) * scale;
|
||||
let extent = if extent.is_finite() { extent.clamp(64.0, 4096.0) } else { 64.0 };
|
||||
// A narrow fade needs fewer: the curve between two samples stays inside a box `width / segments`
|
||||
// wide, so it is never further than that from its chord. Zoomed out on an edited drum take,
|
||||
// thousands of fades are a pixel or two wide.
|
||||
let narrow = ((b - a).abs() * scale * 4.0).ceil() + 1.0;
|
||||
// Physical-pixel sampling avoids visible corners, even for steep bends.
|
||||
// Keep an exact midpoint and cap allocations for extreme zoom/clip sizes.
|
||||
let segments = (extent.ceil() as usize).div_ceil(2) * 2;
|
||||
let segments = (extent.min(narrow).max(2.0).ceil() as usize).div_ceil(2) * 2;
|
||||
(0..=segments)
|
||||
.map(|i| {
|
||||
let k = i as f32 / segments as f32;
|
||||
@@ -1582,7 +1645,8 @@ fn grid_veil(line: Color32, bg: Color32) -> Color32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_clip(app: &SoundApp, painter: &egui::Painter, s: &Session, track: &Track, clip: &Clip, tl: Rect, lane: Rect, dim: bool) {
|
||||
/// `sorted`: whether `track.clips()` is sorted by start (worked out once per track and frame).
|
||||
fn draw_clip(app: &SoundApp, painter: &egui::Painter, s: &Session, track: &Track, sorted: bool, 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));
|
||||
@@ -1604,13 +1668,16 @@ fn draw_clip(app: &SoundApp, painter: &egui::Painter, s: &Session, track: &Track
|
||||
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);
|
||||
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),
|
||||
);
|
||||
// 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 body_r = Rect::from_min_max(pos2(r.min.x, r.min.y + name_h), r.max);
|
||||
let vis = body_r.intersect(lane);
|
||||
@@ -1772,8 +1839,16 @@ fn draw_clip(app: &SoundApp, painter: &egui::Painter, s: &Session, track: &Track
|
||||
painter.text(pos2(r.max.x - 6.0, r.max.y - 7.0), Align2::RIGHT_CENTER, "FX", bold(9.0), Color32::from_rgb(250, 230, 120));
|
||||
}
|
||||
if s.edit.flag("view.clip.overlap_shadows") {
|
||||
// Shade where a later clip covers this one (crossfades and layered edits).
|
||||
for o in track.clips() {
|
||||
// Shade where a later clip covers this one (crossfades and layered edits). Only clips that
|
||||
// start inside this one can: on a sorted track, find them instead of checking every clip.
|
||||
let clips = track.clips();
|
||||
let later = if sorted {
|
||||
let from = clips.partition_point(|o| o.start <= clip.start);
|
||||
clips.get(from..clips.partition_point(|o| o.start < clip.end()).max(from)).unwrap_or_default()
|
||||
} else {
|
||||
clips
|
||||
};
|
||||
for o in later {
|
||||
if o.id != clip.id && o.start > clip.start && o.start < clip.end() {
|
||||
let x0 = x_of(s, tl, o.start);
|
||||
let x1 = x_of(s, tl, clip.end().min(o.end()));
|
||||
@@ -2776,6 +2851,60 @@ fn overlay(app: &mut SoundApp, ui: &mut Ui, tl: Rect, area: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod universe_tests {
|
||||
use super::*;
|
||||
use soundcraft_model::{ChannelFormat, Clip, ClipId, SourceId, TrackKind};
|
||||
|
||||
/// The pixel columns each block covers, per colour, as the old one-rect-per-clip painting did.
|
||||
fn covered(blocks: &[(Rect, Color32)]) -> Vec<(i32, Color32)> {
|
||||
let mut px: Vec<(i32, Color32)> = Vec::new();
|
||||
for (r, c) in blocks {
|
||||
for x in (r.min.x * 4.0).round() as i32..(r.max.x * 4.0).round() as i32 {
|
||||
px.retain(|p| p.0 != x);
|
||||
px.push((x, *c));
|
||||
}
|
||||
}
|
||||
px.sort_by_key(|p| p.0);
|
||||
px
|
||||
}
|
||||
|
||||
fn per_clip(s: &Session, r: Rect) -> Vec<(Rect, Color32)> {
|
||||
let total = (s.content_end() + s.sample_rate.samples(10.0)).max(1) as f32;
|
||||
let t = &s.tracks[0];
|
||||
t.clips()
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let x0 = r.min.x + c.start as f32 / total * r.width();
|
||||
let x1 = (r.min.x + c.end() as f32 / total * r.width()).max(x0 + 1.0);
|
||||
(Rect::from_min_max(pos2(x0, r.min.y + 0.5), pos2(x1, r.max.y - 0.5)), rgb(c.color.unwrap_or(t.color)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sliced_track_is_a_few_runs_covering_the_same_pixels() {
|
||||
let mut s = Session::default();
|
||||
let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None);
|
||||
let mut clips = Vec::new();
|
||||
let mut at = 0;
|
||||
for i in 0..5000u64 {
|
||||
let mut c = Clip::audio(ClipId(i + 1), "hit", SourceId(1), 0, at, 200 + (i % 7) as i64 * 50);
|
||||
// A recoloured stretch, and a gap, in the middle.
|
||||
if (2000..2100).contains(&i) {
|
||||
c.color = Some([200, 40, 40]);
|
||||
}
|
||||
at = c.end() + if i == 3000 { 2_000_000 } else { 0 };
|
||||
clips.push(c);
|
||||
}
|
||||
s.track_mut(t).unwrap().playlist_mut().unwrap().clips = clips;
|
||||
let r = Rect::from_min_size(pos2(8.0, 4.0), vec2(900.0, 56.0));
|
||||
let runs = universe_runs(&s, r);
|
||||
assert!(runs.len() <= 6, "{} rects for 5000 clips", runs.len());
|
||||
assert_eq!(covered(&runs), covered(&per_clip(&s, r)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -3444,6 +3573,37 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn narrow_fades_take_few_points_and_stay_within_a_quarter_pixel() {
|
||||
for width in [0.2f32, 1.0, 3.0, 12.0] {
|
||||
for height in [40.0f32, 320.0] {
|
||||
let body = Rect::from_min_size(Pos2::ZERO, vec2(width, height));
|
||||
for scale in [1.0, 2.0] {
|
||||
for shape in soundcraft_model::FadeShape::ALL {
|
||||
for curvature in [-1.0, 0.0, 1.0] {
|
||||
let fade = soundcraft_model::Fade { len: 1000, shape, curvature };
|
||||
let points = fade_curve_points(fade, true, 0.0, width, body, scale);
|
||||
assert!(points.len() <= ((width * scale * 4.0).ceil() as usize + 4), "{width} wide: {} points", points.len());
|
||||
assert_eq!((points.len() - 1) % 2, 0, "an exact midpoint");
|
||||
let segments = (points.len() - 1) as f32;
|
||||
for (i, pair) in points.windows(2).enumerate() {
|
||||
let line = pair[1] - pair[0];
|
||||
for fraction in [0.1, 0.25, 0.5, 0.75, 0.9] {
|
||||
let t = (i as f32 + fraction) / segments;
|
||||
let actual = pos2(width * t, height - height * fade.gain(t));
|
||||
let along =
|
||||
if line.length_sq() > 0.0 { ((actual - pair[0]).dot(line) / line.length_sq()).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let distance = actual.distance(pair[0] + line * along) * scale;
|
||||
assert!(distance < 0.25, "{width}x{height}, scale={scale}, {shape:?}, curve={curvature}, error={distance}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn painted_fade_control(h: &Harness<SoundApp>, clip_rect: Rect) -> Pos2 {
|
||||
let controls: Vec<_> = h
|
||||
.output()
|
||||
|
||||
@@ -184,32 +184,75 @@ fn gutter_scroll<R>(ui: &Ui, gutter: Rect, out: &egui::scroll_area::ScrollAreaOu
|
||||
}
|
||||
}
|
||||
|
||||
/// One row of the clip list: a whole file, or a clip on a track (positions in the session).
|
||||
#[derive(Clone, Copy)]
|
||||
enum ClipRow {
|
||||
Source(usize),
|
||||
Clip { track: usize, clip: usize },
|
||||
}
|
||||
|
||||
/// The clip list's rows for one document revision and filter. A drum edit holds tens of thousands
|
||||
/// of clips: the rows are listed (and filtered) when either changes, not every frame.
|
||||
#[derive(Clone)]
|
||||
struct ClipRows {
|
||||
revision: u64,
|
||||
filter: String,
|
||||
rows: std::sync::Arc<Vec<ClipRow>>,
|
||||
}
|
||||
|
||||
fn clip_rows(s: &soundcraft_model::Session, needle: &str) -> Vec<ClipRow> {
|
||||
let shown = |name: &str| needle.is_empty() || name.to_lowercase().contains(needle);
|
||||
// Whole files first (bold), then clips on tracks.
|
||||
let sources =
|
||||
s.sources.iter().enumerate().filter(|(_, src)| shown(&format!("{} ({}ch)", src.name, src.channels))).map(|(i, _)| ClipRow::Source(i));
|
||||
let clips =
|
||||
s.tracks.iter().enumerate().flat_map(|(track, tr)| {
|
||||
tr.clips().iter().enumerate().filter(|(_, c)| shown(&c.name)).map(move |(clip, _)| ClipRow::Clip { track, clip })
|
||||
});
|
||||
sources.chain(clips).collect()
|
||||
}
|
||||
|
||||
pub fn clip_list(app: &mut SoundApp, ui: &mut Ui) {
|
||||
let t = Tokens::current();
|
||||
panel_header(ui, "CLIPS");
|
||||
let items: Vec<(u64, String, bool, [u8; 3], bool)> = {
|
||||
let s = app.engine.session();
|
||||
let mut v = Vec::new();
|
||||
// Whole files first (bold), then clips on tracks.
|
||||
for src in &s.sources {
|
||||
v.push((src.id.0, format!("{} ({}ch)", src.name, src.channels), true, [150, 150, 150], s.edit.selected_sources.contains(&src.id)));
|
||||
}
|
||||
for tr in &s.tracks {
|
||||
for c in tr.clips() {
|
||||
v.push((c.id.0, c.name.clone(), false, c.color.unwrap_or(tr.color), s.edit.selected_clips.contains(&c.id)));
|
||||
}
|
||||
}
|
||||
v
|
||||
};
|
||||
let fkey = egui::Id::new("clip_filter");
|
||||
let mut filter: String = ui.ctx().memory(|m| m.data.get_temp(fkey)).unwrap_or_default();
|
||||
ui.add(egui::TextEdit::singleline(&mut filter).hint_text("🔍 Find clips").desired_width(f32::INFINITY));
|
||||
ui.ctx().memory_mut(|m| m.data.insert_temp(fkey, filter.clone()));
|
||||
let needle = filter.to_lowercase();
|
||||
let items: Vec<_> = items.into_iter().filter(|(_, n, _, _, _)| needle.is_empty() || n.to_lowercase().contains(&needle)).collect();
|
||||
egui::ScrollArea::vertical().id_salt("clips_scroll").auto_shrink([false, false]).show(ui, |ui| {
|
||||
let rkey = egui::Id::new("clip_rows");
|
||||
let revision = app.engine.revision;
|
||||
let cached = ui.ctx().memory(|m| m.data.get_temp::<ClipRows>(rkey)).filter(|c| c.revision == revision && c.filter == needle);
|
||||
let rows = match cached {
|
||||
Some(c) => c.rows,
|
||||
None => {
|
||||
let rows = std::sync::Arc::new(clip_rows(app.engine.session(), &needle));
|
||||
ui.ctx().memory_mut(|m| m.data.insert_temp(rkey, ClipRows { revision, filter: needle, rows: rows.clone() }));
|
||||
rows
|
||||
}
|
||||
};
|
||||
let row_h = 17.0;
|
||||
egui::ScrollArea::vertical().id_salt("clips_scroll").auto_shrink([false, false]).show_rows(ui, row_h, rows.len(), |ui, shown| {
|
||||
// Only the rows in view: what they show is read now, before any of them acts on the session.
|
||||
let items: Vec<(u64, String, bool, [u8; 3], bool)> = {
|
||||
let s = app.engine.session();
|
||||
rows.get(shown)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter_map(|row| match *row {
|
||||
ClipRow::Source(i) => s.sources.get(i).map(|src| {
|
||||
(src.id.0, format!("{} ({}ch)", src.name, src.channels), true, [150, 150, 150], s.edit.selected_sources.contains(&src.id))
|
||||
}),
|
||||
ClipRow::Clip { track, clip } => {
|
||||
let tr = s.tracks.get(track)?;
|
||||
let c = tr.clips().get(clip)?;
|
||||
Some((c.id.0, c.name.clone(), false, c.color.unwrap_or(tr.color), s.edit.selected_clips.contains(&c.id)))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
for (id, name, whole, color, selected) in items {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 17.0), if whole { Sense::click_and_drag() } else { Sense::click() });
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), row_h), if whole { Sense::click_and_drag() } else { Sense::click() });
|
||||
resp.context_menu(|ui| {
|
||||
if whole {
|
||||
if ui.button("Place on New Track").clicked() {
|
||||
@@ -708,3 +751,32 @@ fn preset_menu(
|
||||
});
|
||||
ui.ctx().memory_mut(|m| m.data.insert_temp(key, name));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use soundcraft_model::{ChannelFormat, Clip, Session, TrackKind};
|
||||
|
||||
#[test]
|
||||
fn clip_rows_list_every_clip_in_track_order_and_filter_by_name() {
|
||||
let mut s = Session::default();
|
||||
for names in [["Kick 1", "Snare 1"], ["Kick 2", "Tom"]] {
|
||||
let t = s.add_track(TrackKind::Audio, ChannelFormat::Mono, None);
|
||||
for (i, n) in names.into_iter().enumerate() {
|
||||
let c = Clip::audio(s.new_clip_id(), n, SourceId(1), 0, i as i64 * 100, 100);
|
||||
s.track_mut(t).unwrap().playlist_mut().unwrap().clips.push(c);
|
||||
}
|
||||
}
|
||||
let names = |rows: Vec<ClipRow>| -> Vec<String> {
|
||||
rows.into_iter()
|
||||
.filter_map(|r| match r {
|
||||
ClipRow::Clip { track, clip } => Some(s.tracks[track].clips()[clip].name.clone()),
|
||||
ClipRow::Source(_) => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
assert_eq!(names(clip_rows(&s, "")), ["Kick 1", "Snare 1", "Kick 2", "Tom"]);
|
||||
assert_eq!(names(clip_rows(&s, "kick")), ["Kick 1", "Kick 2"]);
|
||||
assert!(clip_rows(&s, "cowbell").is_empty());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user