collective

Browse / github.com/storytold/soundcraft / patch

Mark the selection in the ruler with a split arrow and keep the edit cursor drawn while playing

Mark the selection in the ruler with a split arrow and keep the edit cursor drawn while playing ## Problem and resulting behavior Two gaps in how the Edit window shows the selection: 1. The ruler marked a range with two small triangles at the top of the ruler area, and marked nothing for an insertion point. 2. The blinking edit cursor was hidden during playback. On the incumbent, with Link Timeline and Edit Selection on, the arrows in the ruler and the edit cursor in the tracks are the same selection shown twice: an insertion point is one arrow and the cursor, a range is an arrow at each end (guide p. 889). The cursor also stays on screen while the playhead moves; the guide defines ea

Status
open
Score
0.00
Adopters
0 other operators
Author
lucasegen (imported from pull request #973, open)
Base
60087e0f46032118bf309ed9ea161a9b4a872713
Size
+322 −10 in 4 files
Review
pending
Shared
2026-10-28
Id
8b9a1a8a87e6bb7cd45aee4c

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/overlay.rs crates/ui-egui/src/edit_window/rulers.rs crates/ui-egui/src/edit_window/tests.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@@ -44,6 +44,8 @@ use layout::lane_rect; pub use layout::{COLUMN_W, EditLayout, HEADER_W, LANE_H, RULER_H, edit_columns, header_width, rect, sample_at, track_height, x_of}; use overlay::overlay; pub use rulers::ruler_height;+#[cfg(test)]+use rulers::{arrow_half, selection_marker_colour, selection_markers}; use rulers::{column_titles, draw_rulers, views_button}; pub use scroll::zoom_toggle; #[cfg(test)]diff --git a/crates/ui-egui/src/edit_window/overlay.rs b/crates/ui-egui/src/edit_window/overlay.rs--- a/crates/ui-egui/src/edit_window/overlay.rs+++ b/crates/ui-egui/src/edit_window/overlay.rs@@ -5,6 +5,7 @@ use super::scroll::{FollowFrame, follow_update}; use super::*;  /// Selection overlay, insertion point and playhead across rulers and tracks.+/// The insertion point stays drawn while playing, beside the moving playhead. /// While playing, auto-scroll keeps the playhead in view for the follow /// modes (`page`/`continuous`/`center`) — but any outside view move (a manual /// pan, scrollbar, universe jump, or programmatic scroll) pauses it, so the@@ -28,16 +29,9 @@ pub(super) fn overlay(app: &mut SoundApp, ui: &mut Ui, tl: Rect, area: Rect) {         }     }     let sel = s.edit.selection;-    // Timeline selection markers on the main ruler.-    if !sel.is_empty() {-        for x in [x_of(s, tl, sel.start), x_of(s, tl, sel.end)] {-            painter.add(Shape::convex_polygon(-                vec![pos2(x - 5.0, area.min.y + 2.0), pos2(x + 5.0, area.min.y + 2.0), pos2(x, area.min.y + 9.0)],-                t.accent,-                Stroke::NONE,-            ));-        }-    } else if !app.is_playing() {+    // The insertion point blinks while playing too, standing still at its place+    // while the playhead (its own colour) moves on, like the incumbent.+    if sel.is_empty() {         let x = x_of(s, tl, sel.start);         let now = ui.input(|i| i.time);         let (id, key) = (ui.scope_id().with("insertion_blink"), (sel.start, s.edit.selected_tracks.clone()));diff --git a/crates/ui-egui/src/edit_window/rulers.rs b/crates/ui-egui/src/edit_window/rulers.rs--- a/crates/ui-egui/src/edit_window/rulers.rs+++ b/crates/ui-egui/src/edit_window/rulers.rs@@ -82,6 +82,52 @@ fn nice_step(spp: f64, sr: f64, min_px: f64) -> i64 {     (3600.0 * sr) as i64 } +/// Selection markers are blue, or red while any track is record-armed.+pub(super) fn selection_marker_colour(t: &Tokens, armed: bool) -> Color32 {+    if armed { t.rec } else { t.accent }+}++/// How far a half of the selection arrow reaches out from its cut edge, in px.+const ARROW_REACH: f32 = 10.0;++/// Corner points of one half of the selection arrow, tip first: the whole arrow points down at+/// `x`, a shaft on top and a head below with its tip at the foot of the ruler row, and is cut in+/// two along its vertical axis. `outward` picks the half: -1 for the left one (a start), +1 for the+/// right one (an end). Each half is a bar 4 px wide next to the cut and a wing 6 px further out from+/// mid-height, whose slope runs back to the tip. Mirroring `outward` is the only difference between+/// the halves, so they always fit together.+pub(super) fn arrow_half(x: f32, row: Rect, outward: f32) -> [egui::Pos2; 5] {+    let (top, bottom) = (row.min.y + 0.5, row.max.y - 0.5);+    let (mid, bar, wing) = ((top + bottom) / 2.0, x + outward * 4.0, x + outward * ARROW_REACH);+    [pos2(x, bottom), pos2(wing, mid), pos2(bar, mid), pos2(bar, top), pos2(x, top)]+}++/// The markers for a selection from `x0` to `x1`: the left half of the arrow at the start, the right+/// half at the end, each filled and outlined. An empty selection (`x0 == x1`) joins them into the+/// whole arrow on the insertion point, filled and outlined as one so no seam shows.+pub(super) fn selection_markers(x0: f32, x1: f32, row: Rect, fill: Color32, outline: Color32) -> Vec<Shape> {+    let (left, right) = (arrow_half(x0, row, -1.0), arrow_half(x1, row, 1.0));+    let stroke = Stroke::new(1.0, outline);+    // Each half is concave, so it is filled as the bar down to mid-height plus the wing's triangle.+    if x0 == x1 {+        vec![+            Shape::convex_polygon(vec![left[3], right[3], right[2], left[2]], fill, Stroke::NONE),+            Shape::convex_polygon(vec![left[1], right[1], left[0]], fill, Stroke::NONE),+            Shape::closed_line(vec![right[0], right[1], right[2], right[3], left[3], left[2], left[1]], stroke),+        ]+    } else {+        let mut shapes = Vec::new();+        for half in [left, right] {+            let [tip, wing, mid, top, cut] = half;+            let seam = pos2(cut.x, mid.y);+            shapes.push(Shape::convex_polygon(vec![cut, top, mid, seam], fill, Stroke::NONE));+            shapes.push(Shape::convex_polygon(vec![wing, seam, tip], fill, Stroke::NONE));+            shapes.push(Shape::closed_line(half.to_vec(), stroke));+        }+        shapes+    }+}+ /// Draw each enabled ruler and handle its clicks and double-clicks against the visible sample range. pub(super) fn draw_rulers(app: &mut SoundApp, ui: &mut Ui, area: Rect, tl: Rect, rulers: &[String]) {     let t = Tokens::current();@@ -89,6 +135,9 @@ pub(super) fn draw_rulers(app: &mut SoundApp, ui: &mut Ui, area: Rect, tl: Rect,     ui.painter().rect_filled(area, 0.0, t.ruler_bg);     let view = Range::new(sample_at(&s, tl, tl.min.x), sample_at(&s, tl, tl.max.x));     let sr = s.sample_rate;+    // The selection is marked in the main timebase ruler, or in the top one when that is hidden.+    let main = rulers.iter().position(|id| TimeFormat::from_id(id) == Some(s.edit.main_counter)).unwrap_or(0);+    let marker = selection_marker_colour(&t, s.tracks.iter().any(|track| track.mixer.record_arm));     let mut y0 = area.min.y + 3.0;     for (i, id) in rulers.iter().enumerate() {         let h = ruler_height(id);@@ -237,6 +286,17 @@ pub(super) fn draw_rulers(app: &mut SoundApp, ui: &mut Ui, area: Rect, tl: Rect,                 }             }         }+        if i == main {+            let sel = s.edit.selection;+            let (x0, x1) = (x_of(&s, tl, sel.start), x_of(&s, tl, sel.end));+            // The start's left half reaches out to the left of the timeline when the start is at its+            // edge (a new session: insertion at 0, scroll 0), so the clip widens by that much, over+            // the empty strip next to the ruler row only (never into the track headers below). A start+            // scrolled out of view keeps the plain clip, so its half does not show in that strip.+            let reach = if x0 >= tl.min.x { ARROW_REACH } else { 0.0 };+            let markers = ui.painter().with_clip_rect(row.with_min_x(row.min.x - reach));+            markers.extend(selection_markers(x0, x1, row, marker, t.text_dark));+        }         if id == "min_secs" || id == "bars_beats" || id == "timecode" || id == "samples" || id == "feet_frames" {             // Main timebase marker (blue tick like a selected ruler).             if Some(s.edit.main_counter) == fmt {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@@ -1482,3 +1482,259 @@ fn alt_click_on_a_breakpoint_deletes_it() {         assert_eq!(h.state().engine.session().track(track).unwrap().clips(), clips.as_slice(), "{tool}");     } }++#[test]+fn selection_markers_are_blue_or_red_when_a_track_is_armed() {+    for t in [Tokens::DARK, Tokens::LIGHT] {+        let (blue, red) = (selection_marker_colour(&t, false), selection_marker_colour(&t, true));+        assert!(blue.b() > blue.r() && blue.b() > blue.g(), "{blue:?} is not blue");+        assert!(red.r() > red.g() && red.r() > red.b(), "{red:?} is not red");+    }+}++type Piece = (Color32, Vec<egui::Pos2>);++/// The filled pieces of `shapes`, as (fill, points).+fn fills(shapes: &[Shape]) -> Vec<Piece> {+    let filled = |s: &Shape| match s {+        Shape::Path(p) if p.fill != Color32::TRANSPARENT => Some((p.fill, p.points.clone())),+        _ => None,+    };+    shapes.iter().filter_map(filled).collect()+}++/// The outlines of `shapes`: closed, unfilled paths, as (stroke colour, width, points).+fn outlines(shapes: &[Shape]) -> Vec<(egui::epaint::ColorMode, f32, Vec<egui::Pos2>)> {+    let outline = |s: &Shape| match s {+        Shape::Path(p) if p.closed && p.fill == Color32::TRANSPARENT => Some((p.stroke.color.clone(), p.stroke.width, p.points.clone())),+        _ => None,+    };+    shapes.iter().filter_map(outline).collect()+}++/// Leftmost and rightmost x, and topmost and bottommost y, of some pieces.+fn extent(pieces: &[Piece]) -> (f32, f32, f32, f32) {+    let pts = || pieces.iter().flat_map(|(_, p)| p.iter());+    let fold = |f: fn(f32, f32) -> f32, init: f32, get: fn(&egui::Pos2) -> f32| pts().map(get).fold(init, f);+    (fold(f32::min, f32::MAX, |p| p.x), fold(f32::max, f32::MIN, |p| p.x), fold(f32::min, f32::MAX, |p| p.y), fold(f32::max, f32::MIN, |p| p.y))+}++#[test]+fn a_range_gets_the_left_half_of_the_arrow_at_its_start_and_the_right_half_at_its_end() {+    let row = Rect::from_min_max(pos2(0.0, 0.0), pos2(400.0, 17.0));+    let range = selection_markers(100.0, 200.0, row, Color32::BLUE, Color32::BLACK);+    let pieces = fills(&range);+    assert_eq!(pieces.len(), 4, "a bar and a wing for each end");+    // Left half: cut edge on the start, ten pixels of bar and wing out to its left, row-high.+    assert_eq!(extent(&pieces[..2]), (90.0, 100.0, 0.5, 16.5));+    // Right half: cut edge on the end, wing out to its right.+    assert_eq!(extent(&pieces[2..]), (200.0, 210.0, 0.5, 16.5));+    // The wing starts at mid-height and its slope ends at the foot of the cut edge: nothing below it.+    assert_eq!(pieces[1].1, vec![pos2(90.0, 8.5), pos2(100.0, 8.5), pos2(100.0, 16.5)]);+    // Each half is outlined on its own, thin and dark, cut edge included.+    let lines = outlines(&range);+    assert_eq!(lines.len(), 2);+    assert!(lines.iter().all(|(c, w, pts)| *c == egui::epaint::ColorMode::Solid(Color32::BLACK) && *w == 1.0 && pts.len() == 5));+}++#[test]+fn an_empty_selection_joins_the_halves_into_the_whole_arrow() {+    let row = Rect::from_min_max(pos2(0.0, 0.0), pos2(400.0, 17.0));+    let point = selection_markers(150.0, 150.0, row, Color32::BLUE, Color32::BLACK);+    let pieces = fills(&point);+    assert_eq!(pieces.len(), 2, "a shaft and a head, with no seam");+    assert_eq!(extent(&pieces), (140.0, 160.0, 0.5, 16.5), "centred on 150");+    assert_eq!(pieces[0].1, vec![pos2(146.0, 0.5), pos2(154.0, 0.5), pos2(154.0, 8.5), pos2(146.0, 8.5)]);+    assert_eq!(pieces[1].1, vec![pos2(140.0, 8.5), pos2(160.0, 8.5), pos2(150.0, 16.5)], "tip on the position");+    // One outline, built only from the corners of the two halves, nothing along the cut axis.+    let lines = outlines(&point);+    assert_eq!(lines.len(), 1);+    let (left, right) = (arrow_half(150.0, row, -1.0), arrow_half(150.0, row, 1.0));+    assert_eq!(lines[0].2.len(), 7);+    assert!(lines[0].2.iter().all(|p| left.contains(p) || right.contains(p)));+    assert!(lines[0].2.windows(2).all(|w| !(w[0].x == 150.0 && w[1].x == 150.0)));+    // The halves are mirror images across the cut axis, so they always fit together.+    for (l, r) in left.iter().zip(&right) {+        assert_eq!((300.0 - l.x, l.y), (r.x, r.y));+    }+}++/// Everything `draw_rulers` paints for a selection, with the clip rectangle of each shape. The+/// timeline starts at x 100 and the view at sample `scroll`. Rulers are Bars|Beats (row y 3..20)+/// over Min:Secs (row y 20..37); `main` picks which carries the markers.+fn ruler_shapes(selection: Range, scroll: Samples, armed: bool, main: TimeFormat) -> Vec<egui::epaint::ClippedShape> {+    let _palette = crate::theme::lock_test_palette();+    let mut engine = soundcraft_engine::Engine::default();+    engine.execute("track.new", &json!({"count": 1})).unwrap();+    let session = engine.session_mut();+    session.edit.selection = selection;+    session.edit.zoom.scroll = scroll;+    session.edit.zoom.samples_per_px = 1_000.0;+    session.edit.main_counter = main;+    session.tracks[0].mixer.record_arm = armed;+    let mut app = SoundApp::new(engine, None, crate::Services::default());+    let tl = Rect::from_min_max(pos2(100.0, 100.0), pos2(900.0, 500.0));+    let rulers_rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(900.0, 100.0));+    let rulers = ["bars_beats".to_string(), "min_secs".to_string()];+    let ctx = egui::Context::default();+    crate::fonts::install(&ctx);+    let mut output = ctx.run_ui(egui::RawInput::default(), |ui| draw_rulers(&mut app, ui, rulers_rect, tl, &rulers));+    output.textures_delta.clear();+    output.shapes+}++/// The marker pieces (accent or record fill) and the number of dark outlines `draw_rulers`+/// paints for a selection, with the view at the start of the session.+fn ruler_markers(selection: Range, armed: bool, main: TimeFormat) -> (Vec<Piece>, usize) {+    let t = Tokens::current();+    let shapes: Vec<Shape> = ruler_shapes(selection, 0, armed, main).into_iter().map(|s| s.shape).collect();+    let dark = outlines(&shapes).iter().filter(|(c, w, _)| *c == egui::epaint::ColorMode::Solid(t.text_dark) && *w == 1.0).count();+    (fills(&shapes).into_iter().filter(|(c, _)| *c == t.accent || *c == t.rec).collect(), dark)+}++#[test]+fn rulers_mark_a_range_and_a_point_in_the_main_ruler_and_red_when_armed() {+    let _palette = crate::theme::lock_test_palette();+    let t = Tokens::current();+    for armed in [false, true] {+        let colour = if armed { t.rec } else { t.accent };+        // 30_000..60_000 samples at 1_000 samples/px: x 130 and 160, in the Min:Secs row (y 20..37).+        let (range, dark) = ruler_markers(Range::new(30_000, 60_000), armed, TimeFormat::MinSecs);+        assert_eq!(range.len(), 4, "a bar and a wing for each end");+        assert!(range.iter().all(|(c, _)| *c == colour), "{range:?}");+        assert_eq!(extent(&range[..2]), (120.0, 130.0, 20.5, 36.5), "the start, cut edge on 130");+        assert_eq!(extent(&range[2..]), (160.0, 170.0, 20.5, 36.5), "the end, cut edge on 160");+        assert_eq!(dark, 2);+        // A point: the whole arrow centred on the insertion point, one outline.+        let (point, dark) = ruler_markers(Range::point(30_000), armed, TimeFormat::MinSecs);+        assert_eq!(extent(&point), (120.0, 140.0, 20.5, 36.5));+        assert!(point.iter().all(|(c, _)| *c == colour) && dark == 1);+    }+    // The markers follow the main counter: with Bars|Beats main they move to the first row (y 3..20).+    let (bars, _) = ruler_markers(Range::point(30_000), false, TimeFormat::BarsBeats);+    assert_eq!(extent(&bars), (120.0, 140.0, 3.5, 19.5));+}++/// Clip rectangles of the marker pieces (accent fill) `draw_rulers` paints, with their extent.+fn marker_clips(selection: Range, scroll: Samples) -> Vec<(Rect, Rect)> {+    let accent = Tokens::current().accent;+    ruler_shapes(selection, scroll, false, TimeFormat::MinSecs)+        .into_iter()+        .filter_map(|c| match c.shape {+            Shape::Path(p) if p.fill == accent => Some((c.clip_rect, Rect::from_points(&p.points))),+            _ => None,+        })+        .collect()+}++#[test]+fn the_arrow_at_position_zero_is_not_cut_at_the_edge_of_the_timeline() {+    let _palette = crate::theme::lock_test_palette();+    // Insertion at 0 with scroll 0, as in a new session: x 100, the timeline's left edge, so the+    // left wing sits in the 10 px next to it (x 90..100). The Min:Secs row is y 20..37.+    for selection in [Range::point(0), Range::new(0, 30_000)] {+        let clips = marker_clips(selection, 0);+        assert!(clips.len() >= 2, "{clips:?}");+        let (clip, drawn) = clips.iter().fold((Rect::NOTHING, Rect::NOTHING), |(c, d), (clip, piece)| (c.union(*clip), d.union(*piece)));+        assert_eq!(drawn.min.x, 90.0, "the left wing starts 10 px before the edge");+        assert!(clip.contains_rect(drawn), "the clip {clip:?} cuts the arrow {drawn:?}");+        // ...and only the ruler row's strip is opened: the same band of rows, nothing wider.+        assert_eq!((clip.min.x, clip.min.y, clip.max.y), (90.0, 20.0, 37.0), "{clip:?}");+    }+}++#[test]+fn a_start_scrolled_out_of_view_does_not_bleed_into_the_strip_beside_the_timeline() {+    let _palette = crate::theme::lock_test_palette();+    // Scrolled 5 px: the insertion at 0 is at x 95, off the timeline (which starts at x 100).+    let clips = marker_clips(Range::point(0), 5_000);+    assert!(!clips.is_empty());+    assert!(clips.iter().all(|(clip, _)| clip.min.x == 100.0), "{clips:?}");+}++/// x of every insertion-point stroke painted in the last frame.+fn insertion_xs(h: &Harness<SoundApp>) -> Vec<f32> {+    lines(h, Tokens::current().insertion).iter().map(|l| l[0].x).collect()+}++/// Every line segment of `colour` painted in the last frame.+fn lines(h: &Harness<SoundApp>, colour: Color32) -> Vec<[Pos2; 2]> {+    let line = |shape: &Shape| match shape {+        Shape::LineSegment { points, stroke } if stroke.color == colour => Some(*points),+        _ => None,+    };+    h.output().shapes.iter().filter_map(|c| line(&c.shape)).collect()+}++/// Centre x of the ruler arrow's head (a 20 px wide filled triangle in the marker colour) in the last frame.+fn arrow_xs(h: &Harness<SoundApp>) -> Vec<f32> {+    let shapes: Vec<Shape> = h.output().shapes.iter().map(|c| c.shape.clone()).collect();+    let heads = fills(&shapes).into_iter().filter(|(c, pts)| *c == Tokens::current().accent && pts.len() == 3);+    heads.map(|piece| extent(&[piece])).filter(|(x0, x1, _, _)| (x1 - x0 - 20.0).abs() < 0.01).map(|(x0, x1, _, _)| (x0 + x1) / 2.0).collect()+}++/// Steps until the blinking insertion point is on (at most one second and a half).+fn step_to_insertion_on(h: &mut Harness<SoundApp>) {+    for _ in 0..90 {+        if !insertion_rows(h).is_empty() {+            return;+        }+        h.step();+    }+}++/// An insertion point on row 2, then playback from it with the view held still.+fn playing_from_an_insertion_point() -> (Harness<'static, SoundApp>, Samples) {+    let mut h = harness(4);+    h.state_mut().run("options.scrolling", json!({"mode": "none"})).unwrap();+    let a = lane_point(&h, 2);+    click(&mut h, a);+    let at = h.state().engine.session().edit.selection.start;+    h.state_mut().run("transport.play", json!({})).unwrap();+    h.run_steps(5);+    assert!(h.state().is_playing());+    (h, at)+}++#[test]+fn the_insertion_point_stays_drawn_while_playing_beside_the_playhead() {+    let (mut h, at) = playing_from_an_insertion_point();+    step_to_insertion_on(&mut h);+    assert!(h.state().is_playing());+    assert!(h.state().position() > at, "the playhead did not move on");+    let (s, tl) = (h.state().engine.session(), timeline(&h));+    assert_eq!(insertion_rows(&h), [2], "only on the selected track");+    let x = x_of(s, tl, at);+    assert!(matches!(insertion_xs(&h)[..], [i] if (i - x).abs() < 0.01), "{:?} is not on {x}", insertion_xs(&h));+    // The playhead has its own colour and is already further along.+    assert_ne!(Tokens::current().insertion, Tokens::current().playhead);+    // (The overview paints a small playhead of its own: keep the line that crosses all four tracks.)+    let span = row(&h, 3).max.y - row(&h, 0).min.y;+    let playhead: Vec<f32> = lines(&h, Tokens::current().playhead).iter().filter(|l| l[1].y - l[0].y >= span).map(|l| l[0].x).collect();+    assert!(matches!(playhead[..], [p] if p > x), "{playhead:?} is not past the insertion point {x}");+}++#[test]+fn a_click_in_a_track_while_playing_moves_the_insertion_point_and_the_arrow_not_the_playback() {+    let (mut h, at) = playing_from_an_insertion_point();+    let (s, tl) = (h.state().engine.session(), timeline(&h));+    assert!(matches!(arrow_xs(&h)[..], [a] if (a - x_of(s, tl, at)).abs() < 0.01), "{:?} is not on the insertion point", arrow_xs(&h));+    let before = h.state().position();+    let b = lane_point(&h, 0) + vec2(300.0, 0.0);+    click(&mut h, b);+    let s = h.state().engine.session();+    let want = sample_at(s, timeline(&h), b.x);+    assert_eq!(s.edit.selection, Range::point(want));+    assert_eq!(s.edit.selected_tracks, row_ids(&h)[..1]);+    let x = x_of(s, timeline(&h), want);+    assert!(matches!(arrow_xs(&h)[..], [a] if (a - x).abs() < 0.01), "{:?} is not on {x}", arrow_xs(&h));+    // Playback went on from where it was: it neither stopped nor jumped to the click. The harness steps+    // 1/60 s per frame (800 samples at 48 kHz) and the click takes five of them (measured: 4 000 samples),+    // so allow seven. The old bound, one second, let a seek up to 60 frames ahead pass.+    let pos = h.state().position();+    assert!(h.state().is_playing());+    assert!(pos >= before && pos < before + 7 * 800 && pos < want, "playback went from {before} to {pos}, the click is at {want}");+    step_to_insertion_on(&mut h);+    assert!(matches!(insertion_xs(&h)[..], [i] if (i - x).abs() < 0.01), "{:?} is not on {x}", insertion_xs(&h));+    assert_eq!(insertion_rows(&h), [0]);+}