diff --git a/crates/os-app/src/app.rs b/crates/os-app/src/app.rs index dcd97d3a..8b28ed70 100644 --- a/crates/os-app/src/app.rs +++ b/crates/os-app/src/app.rs @@ -19690,6 +19690,22 @@ mod tests { let _ = std::fs::remove_file(&wav); } + /// The playlist draws with a 3/8 project bar and a time-signature marker (bars of another length): no panic, the + /// ruler and grid repaint after a scroll the same as a fresh paint. + #[test] + fn playlist_paints_other_bars() { + use crate::playlist as pl; + let Ok(mut c) = Canvas::new(1920, 1080, 1.0) else { return }; + let (mut app, _, _, _) = ruler_app(&mut c); + app.state.project.bar = Some((3, 2)); + pl::add_marker(&mut app.state, os_model::TimeMarker { kind: os_model::TimeMarker::SIGNATURE, numerator: 5, denominator: 4, ..os_model::TimeMarker::new(1000, "5/4") }); + frame(&mut app, &mut c, None); + app.state.playlist_scroll.0 = 700.0; + frame(&mut app, &mut c, None); + let g = app.pl_grid.expect("playlist shown"); + assert!(g.w > 0.0); + } + /// Tools menu (spec: ui/playlist.md §10): quick quantize moves clip starts onto the snap grid (one undo step); /// export renders every playing track to its own file, all as long as the song. #[test] diff --git a/crates/os-app/src/hold.rs b/crates/os-app/src/hold.rs index c150fea2..d5ead159 100644 --- a/crates/os-app/src/hold.rs +++ b/crates/os-app/src/hold.rs @@ -174,7 +174,7 @@ impl App { let g = pl::PGeom::new(&self.state, grid); let unit = match pl::grid_snap(&self.state) { pl::Snap::None => g.ppq / 4, - sn => sn.ticks(g.ppq, g.bar_w), + sn => pl::snap_ticks(&self.state, sn, g.bar_w), }; // `fresh`: the copies are a change even where they end up. let d = PlDrag::Move { x0: x0 - unit as f32 * g.ppt, y0, orig: copies, fresh: true }; @@ -490,7 +490,7 @@ mod tests { let (mut app, (x, y)) = playlist_app(&mut c); let orig = pl::clips(&app.state)[0]; let g = pl::PGeom::new(&app.state, app.pl_grid().unwrap()); - let unit = pl::grid_snap(&app.state).ticks(g.ppq, g.bar_w); + let unit = pl::snap_unit(&app.state, g.bar_w); let steps = app.state.history.len(); press(&mut app, x, y); let now = at(&app, 700); diff --git a/crates/os-app/src/playlist.rs b/crates/os-app/src/playlist.rs index 13d35bb7..87e60071 100644 --- a/crates/os-app/src/playlist.rs +++ b/crates/os-app/src/playlist.rs @@ -166,6 +166,19 @@ impl Snap { } } +/// `sn`'s unit in ticks in the playlist: as [`Snap::ticks`], but a bar is the project's (its time signature). +pub fn snap_ticks(s: &State, sn: Snap, bar_w: f32) -> u32 { + match sn { + Snap::Bar => s.project.bar_ticks(), + sn => sn.ticks(ppq(s), bar_w), + } +} + +/// The playlist's current snap unit in ticks at the zoom `bar_w` ([`snap_ticks`]). +pub fn snap_unit(s: &State, bar_w: f32) -> u32 { + snap_ticks(s, grid_snap(s), bar_w) +} + /// Ticks between the drawn grid lines: steps when a beat is wide, beats normally, bars when zoomed far out. pub fn grid_ticks(ppq: u32, bar_w: f32) -> u32 { let beat_px = bar_w / 4.0; @@ -1229,10 +1242,101 @@ fn ppq(s: &State) -> u32 { u32::from(s.project.ppq.max(4)) } +/// The project's bar in ticks (its time signature: `Project::bar_ticks`). fn bar(s: &State) -> u32 { + s.project.bar_ticks() +} + +/// Four quarter notes: the span the playlist's zoom is counted in (`State::playlist_bar_w` is the width of this many +/// ticks, whatever the time signature), so the width of a tick does not change with it. +fn ref_bar(s: &State) -> u32 { ppq(s) * 4 } +/// The song's bars. The project's bar from the start; a time-signature marker of the arrangement starts bars of its +/// own length at its position (the bar it lands in is cut short there), as a pattern's marker sets the pattern's bars +/// (spec: ui/pattern-time-signature.md). Bar numbers count from 0. +#[derive(Debug, Clone)] +pub struct Bars { + /// (first tick, bar length in ticks), ascending; the first starts at 0. + segs: Vec<(u32, u32)>, +} + +impl Bars { + pub fn new(s: &State) -> Bars { + let ppq = u32::from(s.project.ppq.max(1)); + let mut marks: Vec<&TimeMarker> = markers(s).iter().filter(|m| m.kind == TimeMarker::SIGNATURE && m.numerator > 0 && m.denominator > 0).collect(); + marks.sort_by_key(|m| m.position); + let mut segs = vec![(0, s.project.bar_ticks())]; + for m in marks { + let len = (ppq * 4 * u32::from(m.numerator) / u32::from(m.denominator)).max(1); + match segs.last_mut() { + Some(l) if l.0 == m.position => l.1 = len, + _ => segs.push((m.position, len)), + } + } + Bars { segs } + } + + fn seg(&self, t: u32) -> usize { + self.segs.partition_point(|&(st, _)| st <= t).saturating_sub(1) + } + + /// How many bars segment `i` holds (the last one cut short counts), `None` for the open-ended last segment. + fn count(&self, i: usize) -> Option { + let (st, len) = self.segs[i]; + self.segs.get(i + 1).map(|n| (n.0 - st).div_ceil(len)) + } + + /// The start of the bar `t` is in. + pub fn floor(&self, t: u32) -> u32 { + let (st, len) = self.segs[self.seg(t)]; + st + (t - st) / len * len + } + + /// The nearest bar line to `t` (halfway goes up). + pub fn nearest(&self, t: f32) -> u32 { + let t = t.max(0.0); + let f = self.floor(t as u32); + let i = self.seg(f); + let (_, len) = self.segs[i]; + let next = self.segs.get(i + 1).map_or(f + len, |n| (f + len).min(n.0)); + if t - f as f32 >= (next - f) as f32 / 2.0 { next } else { f } + } + + /// The number of the bar `t` is in. + pub fn index(&self, t: u32) -> i32 { + let i = self.seg(t); + let before: u32 = (0..i).filter_map(|k| self.count(k)).sum(); + (before + (t - self.segs[i].0) / self.segs[i].1) as i32 + } + + /// The bars that overlap `t0..t1`: (number, first tick, length). + pub fn lines(&self, t0: u32, t1: u32) -> Vec<(i32, u32, u32)> { + let mut out = Vec::new(); + let first = self.seg(t0); + let mut before: u32 = (0..first).filter_map(|k| self.count(k)).sum(); + for i in first..self.segs.len() { + let (st, len) = self.segs[i]; + let end = self.segs.get(i + 1).map(|n| n.0); + if st >= t1 { + break; + } + let mut k = if i == first { (t0 - st) / len } else { 0 }; + loop { + let at = st + k * len; + if at >= t1 || end.is_some_and(|e| at >= e) { + break; + } + out.push(((before + k) as i32, at, end.map_or(len, |e| len.min(e - at)))); + k += 1; + } + before += self.count(i).unwrap_or(0); + } + out + } +} + fn channel_of(s: &State, ch: u16) -> Option<&Channel> { s.project.channel(ch) } @@ -1444,7 +1548,7 @@ pub fn source_len(s: &State, src: ClipSource) -> u32 { ClipSource::Channel(c) => { if let Some(a) = s.project.automation(c) { let beats = a.points.last().map_or(0.0, |p| p.beat); - return ((beats * f64::from(ppq(s))).round() as u32).max(bar / 4); + return ((beats * f64::from(ppq(s))).round() as u32).max(ppq(s)); } // The whole sample at the channel's speed (`fit_audio`). let t = channel_of(s, c).map_or(0.0, |ch| audio_ticks(s, ch)); @@ -1559,8 +1663,15 @@ pub fn snap_place(s: &State, g: &PGeom, tick: f32, alt: bool) -> u32 { if alt || sn == Snap::None { return t as u32; } - let u = sn.ticks(g.ppq, g.bar_w); - if sn == Snap::Cell { t as u32 / u * u } else { (t / u as f32).round() as u32 * u } + let bars = Bars::new(s); + if sn == Snap::Bar { + return bars.nearest(t); + } + // Units count from the start of the bar the tick is in (the same as from 0 while every bar is a multiple of it). + let u = snap_ticks(s, sn, g.bar_w); + let o = bars.floor(t as u32); + let r = t - o as f32; + o + if sn == Snap::Cell { r as u32 / u * u } else { (r / u as f32).round() as u32 * u } } /// The song position set on the ruler: down to the unit (Alt: the tick under the pointer). @@ -1572,8 +1683,9 @@ fn snap_down(s: &State, g: &PGeom, tick: f32, alt: bool) -> u32 { if let Some(c) = catch(s, g, tick.max(0.0), &[]) { return c; } - let u = grid_snap(s).ticks(g.ppq, g.bar_w); - t - t % u + let o = Bars::new(s).floor(t); + let u = snap_unit(s, g.bar_w); + t - (t - o) % u } /// A drag's delta for the edge at `from` (a moved clip's start, a resized edge): under Events and Markers the edge goes @@ -1590,7 +1702,7 @@ pub fn snap_delta(s: &State, g: &PGeom, dt: f32, alt: bool) -> i64 { if alt { return dt.round() as i64; } - let u = grid_snap(s).ticks(g.ppq, g.bar_w) as f32; + let u = snap_unit(s, g.bar_w) as f32; ((dt / u).round() * u) as i64 } @@ -1793,9 +1905,9 @@ pub fn load_view(s: &mut State) { s.playlist.row_h = (v.row_px() as f32).clamp(ROW_MIN, ROW_MAX); let tpp = v.ticks_per_px(); if tpp.is_finite() && tpp > 0.0 { - s.playlist_bar_w = (f64::from(bar(s)) / tpp).clamp(4.0, 600.0) as f32; + s.playlist_bar_w = (f64::from(ref_bar(s)) / tpp).clamp(4.0, 600.0) as f32; let left = v.left(); - let tpp = f64::from(bar(s)) / f64::from(s.playlist_bar_w); + let tpp = f64::from(ref_bar(s)) / f64::from(s.playlist_bar_w); s.playlist_scroll.0 = if left.is_finite() { (left / tpp).max(0.0) as f32 } else { 0.0 }; } s.playlist_scroll.1 = v.scroll_y() as f32; @@ -1804,7 +1916,7 @@ pub fn load_view(s: &mut State) { /// Write the playlist's track height, time zoom and scroll into the project for saving (event 242). pub fn store_view(s: &mut State) { let mut v = s.project.playlist_view.unwrap_or_else(|| os_model::PlaylistView::new(s.project.ppq)); - let tpp = f64::from(bar(s)) / f64::from(s.playlist_bar_w.max(1.0)); + let tpp = f64::from(ref_bar(s)) / f64::from(s.playlist_bar_w.max(1.0)); v.set_ticks_per_px(tpp); v.set_left(f64::from(s.playlist_scroll.0) * tpp); v.set_scroll_y(s.playlist_scroll.1.max(0.0).round() as u32); @@ -2913,7 +3025,7 @@ pub fn reveal_source(s: &mut State, ch: u16) -> bool { s.playlist.sel = clips(s).iter().enumerate().filter(|(_, c)| c.source == src).map(|(i, _)| i).collect(); s.raise(crate::app::Win::Playlist); let Some(first) = clips(s).iter().filter(|c| c.source == src).min_by_key(|c| (c.position, c.track)).copied() else { return false }; - s.playlist_scroll.0 = first.position as f32 / bar(s) as f32 * s.playlist_bar_w; + s.playlist_scroll.0 = first.position as f32 / ref_bar(s) as f32 * s.playlist_bar_w; s.playlist_scroll.1 = Rows::new(s).top(first.track); true } @@ -3585,7 +3697,7 @@ fn snap_round(s: &State, g: &PGeom, tick: f32, alt: bool) -> u32 { if alt { return tick.max(0.0).round() as u32; } - let u = grid_snap(s).ticks(g.ppq, g.bar_w) as f32; + let u = snap_unit(s, g.bar_w) as f32; ((tick / u).round() * u).max(0.0) as u32 } @@ -3689,7 +3801,7 @@ pub fn drag_move(app: &mut App, d: PlDrag, grid: Rect) { PlDrag::ResizeR { x0, orig } => { let skip: Vec = orig.iter().map(|&(i, _)| i).collect(); let dt = orig.first().map_or(0, |(_, c)| snap_edge(s, &g, c.end(), (x - x0) / g.ppt, alt, &skip)); - let min = if alt { 1 } else { grid_snap(s).ticks(g.ppq, g.bar_w) } as i64; + let min = if alt { 1 } else { snap_unit(s, g.bar_w) } as i64; for &(i, o) in &orig { let len = (i64::from(o.length) + dt).max(min) as u32; if !stretch_clip(s, i, &o, o.position, len) { @@ -3785,7 +3897,7 @@ pub fn drag_move(app: &mut App, d: PlDrag, grid: Rect) { let s = &mut app.state; let mut beat = beat0 + f64::from((x - x0) / g.ppt) / f64::from(g.ppq); if !alt { - let u = f64::from(grid_snap(s).ticks(g.ppq, g.bar_w)) / f64::from(g.ppq); + let u = f64::from(snap_unit(s, g.bar_w)) / f64::from(g.ppq); let (w0, _) = window(s, &c); beat = w0 + ((beat - w0) / u).round() * u; } @@ -3890,7 +4002,7 @@ pub fn drag_move(app: &mut App, d: PlDrag, grid: Rect) { fn clips_jumped(s: &State, g: &PGeom, grid: Rect, alt: bool, from: Vec<(usize, Rect)>) -> Vec { /// A magnet's step this small (px) is no step. const NO_STEP: f32 = 3.0; - let stepped = !alt && grid_snap(s).ticks(g.ppq, g.bar_w) as f32 * g.ppt >= NO_STEP; + let stepped = !alt && snap_unit(s, g.bar_w) as f32 * g.ppt >= NO_STEP; let count = clips(s).len(); let mut dirty = Vec::new(); for (i, r0) in from { @@ -4026,7 +4138,7 @@ fn zoom_toggle(app: &mut App) { pub fn zoom_on(s: &mut State, grid: (f32, f32), idx: &[usize]) { let cs: Vec = idx.iter().filter_map(|&i| clips(s).get(i).copied()).collect(); let (Some(a), Some(b)) = (cs.iter().map(|c| c.position).min(), cs.iter().map(|c| c.end()).max()) else { return }; - let bar = bar(s) as f32; + let bar = ref_bar(s) as f32; let span = ((b - a) as f32 / bar).max(1.0); s.playlist_bar_w = (grid.0 / span).clamp(4.0, 600.0); s.playlist_scroll.0 = a as f32 / bar * s.playlist_bar_w; @@ -4671,7 +4783,7 @@ pub fn run(app: &mut App, cmd: Cmd) -> Option { } Cmd::ShiftTime(d) => { snapshot(s); - let u = grid.map_or(ppq(s), |g| grid_snap(s).ticks(ppq(s), PGeom::new(s, g).bar_w)) as i64; + let u = grid.map_or(ppq(s), |g| snap_unit(s, PGeom::new(s, g).bar_w)) as i64; let idx = targets(s); let arr = arr_mut(s); let min = idx.iter().filter_map(|&i| arr.clips.get(i)).map(|c| i64::from(c.position)).min().unwrap_or(0); @@ -5211,7 +5323,7 @@ pub(crate) fn hsl_rgb(h: f32, s: f32, l: f32) -> Rgb { /// nearest snap step (on the nearest beat when snapping is off); lengths stay. One undo step. pub fn quantize_starts(s: &mut State) -> String { let p = ppq(s); - let unit = match grid_snap(s).ticks(p, s.playlist_bar_w) { + let unit = match snap_unit(s, s.playlist_bar_w) { 0 | 1 => p, u => u, }; @@ -5695,6 +5807,53 @@ mod tests { app } + fn sig(pos: u32, num: u8, den: u8) -> TimeMarker { + TimeMarker { kind: TimeMarker::SIGNATURE, numerator: num, denominator: den, ..TimeMarker::new(pos, &format!("{num}/{den}")) } + } + + fn lens(s: &State, to: u32) -> Vec<(i32, u32, u32)> { + Bars::new(s).lines(0, to) + } + + /// The playlist's bars are the project's (3/8: 144 ticks at PPQ 96), not always four beats; Snap > Bar and the + /// ruler's bar lines follow, and a time-signature marker of the arrangement starts bars of its own length. + #[test] + fn bars_follow_the_time_signature() { + let mut app = app(); + let s = &mut app.state; + assert_eq!(lens(s, 800), [(0, 0, 384), (1, 384, 384), (2, 768, 384)], "4/4"); + assert_eq!(snap_ticks(s, Snap::Bar, 0.0), 384); + // The project's bar: 3 beats of 2 steps = 3/8. + s.project.bar = Some((3, 2)); + assert_eq!(snap_ticks(s, Snap::Bar, 0.0), 144); + assert_eq!(lens(s, 300), [(0, 0, 144), (1, 144, 144), (2, 288, 144)], "3/8"); + let b = Bars::new(s); + assert_eq!((b.floor(300), b.index(300), b.nearest(200.0), b.nearest(230.0)), (288, 2, 144, 288)); + // The snap itself: the nearest bar line, a quarter-bar unit counted from the bar's start. + s.playlist.snap = crate::snap::EditorSnap::Grid(Snap::Bar); + let g = PGeom::new(s, Rect::new(0.0, 0.0, 1000.0, 500.0)); + assert_eq!(snap_place(s, &g, 200.0, false), 144); + assert_eq!(snap_place(s, &g, 230.0, false), 288); + assert_eq!(snap_down(s, &g, 300.0, false), 288); + s.playlist.snap = crate::snap::EditorSnap::Grid(Snap::Step); + assert_eq!(snap_place(s, &g, 300.0, false), 288 + 24 * 1, "a step counted from the bar's start (12 ticks off is half a step)"); + // 4/4 again, a 3/4 marker at bar 3 (tick 768): its bars are 288 ticks and count on from the 3rd. + s.project.bar = None; + add_marker(s, sig(768, 3, 4)); + assert_eq!(lens(s, 1400), [(0, 0, 384), (1, 384, 384), (2, 768, 288), (3, 1056, 288), (4, 1344, 288)]); + let b = Bars::new(s); + assert_eq!((b.floor(800), b.index(1100), b.index(100)), (768, 3, 0)); + s.playlist.snap = crate::snap::EditorSnap::Grid(Snap::Bar); + assert_eq!(snap_place(s, &g, 1000.0, false), 1056); + // A marker inside a bar cuts it short and starts new bars there; one at 0 sets the first bar. + s.project.arrangement_mut().unwrap().markers.clear(); + add_marker(s, sig(1000, 3, 4)); + assert_eq!(lens(s, 1290), [(0, 0, 384), (1, 384, 384), (2, 768, 232), (3, 1000, 288), (4, 1288, 288)]); + s.project.arrangement_mut().unwrap().markers.clear(); + add_marker(s, sig(0, 5, 8)); + assert_eq!(lens(s, 500)[0], (0, 0, 240)); + } + /// Rename... on an audio or automation clip renames its channel in the rack's name box; a pattern clip still edits /// its pattern's name in place. #[test] diff --git a/crates/os-app/src/views/playlist.rs b/crates/os-app/src/views/playlist.rs index aa534b55..7aa67a7b 100644 --- a/crates/os-app/src/views/playlist.rs +++ b/crates/os-app/src/views/playlist.rs @@ -159,12 +159,15 @@ pub fn paint(ui: &mut Ui, s: &State, r: Rect, f: &FrameInfo) { let g = PGeom::new(s, grid); let (sx, sy) = s.playlist_scroll; let bar_w = s.playlist_bar_w; + // The zoom counts in four quarter notes (`bar_t`), whatever the time signature; the song's extent is counted in + // the project's bars. let bar_t = (g.ppq * 4) as f32; + let proj_bar = s.project.bar_ticks() as f32; // Overview (horizontal scroll bar with a miniature of the clips) + ruler. The song extends 16 bars past // its last clip, at least 100 bars; 500 tracks. - let last = pl::clips(s).iter().map(|c| c.end() as f32 / bar_t).fold(0.0, f32::max); - let bars = (last.ceil() + 16.0).max(100.0); + let last = pl::clips(s).iter().map(|c| c.end() as f32 / proj_bar).fold(0.0, f32::max); + let bars = (last.ceil() + 16.0).max(100.0) * proj_bar / bar_t; let (hbar, vbar) = bar_sizes(); let over = Rect::new(grid_x, client.y + 1.0, grid_w, hbar); debug_assert_eq!(over, overview_rect(s, grid)); @@ -217,7 +220,8 @@ pub fn paint(ui: &mut Ui, s: &State, r: Rect, f: &FrameInfo) { let x = g.x_of(p as f32).clamp(grid_x, grid.right()); ui.c.fill_rect(Rect::span(grid_x, x, grid.y, grid.bottom()), t.s(0x000000).with_alpha(70)); } - grid_lines(ui, &g, bar_w, start_bar(&g, start)); + let bars = pl::Bars::new(s); + grid_lines(ui, s, &g, &bars, start_bar(&bars, start)); for &row in &visible { let (y, h) = (g.y_of(row), g.rows.height(row)); // The gap line at the theme's line strength (spec: ui/flstheme.md §3, PLGridContrast). @@ -646,8 +650,8 @@ fn overview(ui: &mut Ui, s: &State, over: Rect, total_ticks: f32) { } /// The bar the Start marker is in (0 without one): bars count from it (§7a). -fn start_bar(g: &PGeom, start: Option) -> i32 { - start.map_or(0, |p| (p / (g.ppq * 4).max(1)) as i32) +fn start_bar(bars: &pl::Bars, start: Option) -> i32 { + start.map_or(0, |p| bars.index(p)) } /// Bar numbers, time selection, song position (§7). @@ -662,23 +666,24 @@ fn ruler_paint(ui: &mut Ui, s: &State, g: &PGeom, ruler: Rect, f: &FrameInfo) { // Bars count from the Start marker (§7a): "1" on its bar; before it the ruler is darker and the bars have no // numbers. let start = pl::start_marker(s); - let sb = start_bar(g, start); + let bars = pl::Bars::new(s); + let sb = start_bar(&bars, start); if let Some(p) = start { let x = g.x_of(p as f32).clamp(ruler.x, ruler.right()); ui.c.fill_rect(Rect::span(ruler.x, x, ruler.y, ruler.bottom()), offset(t.ruler, -8)); } - let bar_w = g.bar_w; - let every = label_step(bar_w); + // Labels by the project's bar width; the song's bars follow the time-signature markers (`pl::Bars`). + let every = label_step(s.project.bar_ticks() as f32 * g.ppt); // From the bars whose numbers still reach into view (a number is up to ~40 px wide): which labels are drawn must // not depend on the scroll position, or a scrolled ruler would differ from a repainted one. - let first_bar = ((g.scroll_x - 48.0) / bar_w).floor().max(0.0) as i32; - let last_bar = ((g.scroll_x + ruler.w) / bar_w).ceil() as i32 + 1; - for b in first_bar.max(sb)..last_bar { + let t0 = ((g.scroll_x - 48.0) / g.ppt).max(0.0) as u32; + let t1 = ((g.scroll_x + ruler.w) / g.ppt).max(0.0) as u32 + 1; + for (b, tick, _) in bars.lines(t0, t1) { let n = b - sb; - if n % every != 0 { + if n < 0 || n % every != 0 { continue; } - let x = g.xs(f64::from(b) * f64::from(bar_w)); + let x = g.xs(f64::from(tick) * f64::from(g.bar_w) / f64::from(g.ppq * 4)); let size = if n % (every * 4) == 0 { 15.0 } else { 13.0 }; // Along the ruler's bottom 17 px (all of it unless thin scroll bars made it taller). ui.c.text(super::fnt(size), x + 2.0, ruler.bottom() - 17.0 + (16.0 - size), t.ruler_text, &format!("{}", n + 1)); @@ -800,36 +805,44 @@ pub(crate) fn marker_label(m: &TimeMarker) -> String { /// Vertical lines: grid units (dim), beats, bars (bright); every fourth bar group tinted, counted from bar `sb` (the /// Start marker's, §7a). -fn grid_lines(ui: &mut Ui, g: &PGeom, bar_w: f32, sb: i32) { +fn grid_lines(ui: &mut Ui, s: &State, g: &PGeom, bars: &pl::Bars, sb: i32) { let t = ui.t; let grid = g.grid; + let bar_w = g.bar_w; + // A bar's width is the project's (the time signature); bars of another length follow its markers (`pl::Bars`). + let pb = s.project.bar_ticks() as f32 * g.ppt; let mut bar_step = 1; - while bar_w * (bar_step as f32) < 8.0 { + while pb * (bar_step as f32) < 8.0 { bar_step *= 2; } - let first_bar = ((g.scroll_x / bar_w) as i32 / bar_step) * bar_step; - let n = (grid.w / bar_w).ceil() as i32 + bar_step + 1; // Lines at the theme's line strength (spec: ui/flstheme.md §3, PLGridContrast). let beat = t.pl_line(t.grid_gap.mix(t.grid, 0.4)); let bar_line = t.pl_line(t.grid_bar); - let unit = pl::grid_ticks(g.ppq, bar_w) as f32 * g.ppt; - let per_bar = ((bar_w / unit).round() as i32).max(1); - // Bar b's x on the drawing grid (`PGeom::xs`), plus `d` content pixels. - let bar_x = |b: i32, d: f32| g.xs(f64::from(b) * f64::from(bar_w) + f64::from(d)); - let mut b = first_bar; - while b < first_bar + n { - let x = bar_x(b, 0.0); - if (b - sb).div_euclid(4).rem_euclid(2) == 1 { - ui.c.fill_rect(Rect::new(x, grid.y, bar_x(b + bar_step, 0.0) - x, grid.h), t.grid_alt.with_alpha(160)); + let unit_t = pl::grid_ticks(g.ppq, bar_w); + let unit = unit_t as f32 * g.ppt; + let ref_t = g.ppq * 4; + // Content x of `tick` (`PGeom::xs`): the same exact products as bars counted from 0 in 4/4. + let tick_x = |tick: u32, d: f32| g.xs(f64::from(tick) * f64::from(bar_w) / f64::from(ref_t) + f64::from(d)); + let t0 = (g.scroll_x / g.ppt).max(0.0) as u32; + let t1 = ((g.scroll_x + grid.w) / g.ppt).max(0.0) as u32 + 1; + for (b, at, len) in bars.lines(t0, t1) { + if b.rem_euclid(bar_step) != 0 { + continue; } - if bar_step == 1 && per_bar > 1 { - for q in 1..per_bar { - let c = if unit * q as f32 % (bar_w / 4.0) < 0.5 { beat } else { beat.mix(t.grid, 0.5) }; - ui.c.fill_rect(Rect::new(bar_x(b, q as f32 * unit), grid.y, 1.0, grid.h), c); + let x = tick_x(at, 0.0); + if (b - sb).div_euclid(4).rem_euclid(2) == 1 { + let next = if bar_step == 1 { at + len } else { bars.floor(at + len * bar_step as u32).max(at + len) }; + ui.c.fill_rect(Rect::new(x, grid.y, tick_x(next, 0.0) - x, grid.h), t.grid_alt.with_alpha(160)); + } + if bar_step == 1 && unit_t < ref_t { + let mut q = 1; + while q * unit_t < len { + let c = if (q * unit_t) % g.ppq == 0 { beat } else { beat.mix(t.grid, 0.5) }; + ui.c.fill_rect(Rect::new(tick_x(at, q as f32 * unit), grid.y, 1.0, grid.h), c); + q += 1; } } ui.c.fill_rect(Rect::new(x, grid.y, if bar_step == 1 { 2.0 } else { 1.0 }, grid.h), bar_line); - b += bar_step; } }