Files
soundcraft/crates/ui-egui/tests/grid_lines.rs
grovebit bebca3aa85 Draw grid lines over clips in the Edit window
The Edit window painted the bar and grid lines on the empty lane and then
the opaque clip bodies on top, so the grid vanished wherever an audio or
MIDI clip sat. Pro Tools draws grid lines over the clips.

- While grid lines are shown, the lines are drawn after the clips (audio,
  MIDI, video and their overlays) and before automation and the selection.
- The lines are a translucent white (Dark) or black (Light), worked out
  from the palette's line and lane colours, so a line on an empty lane looks
  as before and waveforms and notes stay readable under it. Bar lines now
  also stand out where a grid line lands on them; the opaque grid line used
  to cover them.
- With grid lines hidden, the bar lines stay behind the clips as before.

Test: tests/grid_lines.rs renders the demo Edit window in both themes and
checks that the lines over the Kick clip are painted after it only while
grid lines are shown, and that each one looks like the theme's line colour
on an empty lane.
2026-10-12 20:43:09 +01:00

71 lines
3.1 KiB
Rust

use egui::{Color32, FullOutput, Pos2, RawInput, Rect, Shape, pos2, vec2};
use serde_json::json;
use soundcraft_ui_egui::theme::Tokens;
use soundcraft_ui_egui::{Services, SoundApp, edit_window};
fn frame(ctx: &egui::Context, app: &mut SoundApp) -> FullOutput {
let raw = RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| {
app.logic(ui.ctx());
app.ui(ui);
});
out.textures_delta.clear();
out
}
fn over(top: Color32, bg: Color32) -> [i32; 3] {
let keep = 255 - i32::from(top.a());
[(top.r(), bg.r()), (top.g(), bg.g()), (top.b(), bg.b())].map(|(t, b)| i32::from(t) + i32::from(b) * keep / 255)
}
fn near(a: [i32; 3], b: Color32) -> bool {
a.iter().zip([b.r(), b.g(), b.b()]).all(|(x, y)| (x - i32::from(y)).abs() <= 6)
}
#[test]
fn grid_lines_are_drawn_over_clips_while_shown() {
let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default());
let ctx = egui::Context::default();
for theme in ["dark", "light"] {
for shown in [true, false] {
app.run("ui.theme", json!({"mode": theme})).unwrap();
app.run("view.grid_lines", json!({"value": shown})).unwrap();
frame(&ctx, &mut app);
let out = frame(&ctx, &mut app);
let t = Tokens::current();
let s = app.engine.session().clone();
let kick = s.track_by_name("Kick").unwrap();
let clip = kick.clips()[0].clone();
let [x0, y0, x1, y1] = app.edit_layout.timeline;
let tl = Rect::from_min_max(pos2(x0, y0), pos2(x1, y1));
let row = app.edit_layout.rows.iter().find(|(id, _)| *id == kick.id.0).unwrap().1;
let lane = Rect::from_min_max(pos2(tl.min.x, row[1]), pos2(tl.max.x, row[1] + kick.height.points()));
let (cx0, cx1) = (edit_window::x_of(&s, tl, clip.start), edit_window::x_of(&s, tl, clip.end()));
let body = out
.shapes
.iter()
.position(|c| matches!(&c.shape, Shape::Rect(r) if r.rect.min.x == cx0 && r.rect.max.x == cx1 && r.rect.min.y == lane.min.y + 1.0))
.unwrap();
let lines: Vec<(usize, Color32)> = out
.shapes
.iter()
.enumerate()
.filter_map(|(i, c)| match &c.shape {
Shape::LineSegment { points: [a, b], stroke }
if a.x == b.x && a.x > cx0 + 1.0 && a.x < cx1 - 1.0 && a.y == lane.min.y && b.y == lane.max.y =>
{
Some((i, stroke.color))
}
_ => None,
})
.collect();
assert!(lines.len() > 4, "{theme} {shown}: {} lines", lines.len());
assert!(lines.iter().all(|(i, _)| (*i > body) == shown), "{theme} {shown}");
for (_, col) in &lines {
let look = over(*col, t.playlist_bg);
assert!(near(look, t.grid_line) || near(look, t.bar_line), "{theme} {shown}: {look:?}");
}
}
}
}