diff --git a/crates/op-editor-ui/src/widgets/layer_panel.rs b/crates/op-editor-ui/src/widgets/layer_panel.rs index 64331975c..3c954145e 100644 --- a/crates/op-editor-ui/src/widgets/layer_panel.rs +++ b/crates/op-editor-ui/src/widgets/layer_panel.rs @@ -265,6 +265,7 @@ impl LayerPanel { r.layers_rows_top, r.layers.offset, r.layers_view_h, + LAYER_ROW_HEIGHT, point.y, ) { let item = &self.items[index]; @@ -341,6 +342,7 @@ impl LayerPanel { r.pages_rows_top, r.pages.offset, r.pages_view_h, + PAGE_ROW_HEIGHT, point.y, ) { let page = &self.pages[index]; @@ -371,6 +373,7 @@ impl LayerPanel { r.layers_rows_top, r.layers.offset, r.layers_view_h, + LAYER_ROW_HEIGHT, point.y, ) { let item = &self.items[index]; @@ -507,7 +510,12 @@ impl Widget for LayerPanel { origin: Point2D::new(rect.origin.x, r.pages_rows_top), size: Point2D::new(rect.size.x, r.pages_view_h), }); - for index in visible_row_range(self.pages.len(), r.pages.offset, r.pages_view_h) { + for index in visible_row_range( + self.pages.len(), + r.pages.offset, + r.pages_view_h, + PAGE_ROW_HEIGHT, + ) { let page = &self.pages[index]; y = r.pages_rows_top - r.pages.offset + index as f32 * PAGE_ROW_HEIGHT; let row = Rect { @@ -595,7 +603,12 @@ impl Widget for LayerPanel { origin: Point2D::new(rect.origin.x, r.layers_rows_top), size: Point2D::new(rect.size.x, r.layers_view_h), }); - for index in visible_row_range(self.items.len(), r.layers.offset, r.layers_view_h) { + for index in visible_row_range( + self.items.len(), + r.layers.offset, + r.layers_view_h, + LAYER_ROW_HEIGHT, + ) { let item = &self.items[index]; y = r.layers_rows_top - r.layers.offset + index as f32 * LAYER_ROW_HEIGHT; let row = Rect { diff --git a/crates/op-editor-ui/src/widgets/layer_panel_tests.rs b/crates/op-editor-ui/src/widgets/layer_panel_tests.rs index 78aeabd46..221d1b53d 100644 --- a/crates/op-editor-ui/src/widgets/layer_panel_tests.rs +++ b/crates/op-editor-ui/src/widgets/layer_panel_tests.rs @@ -585,7 +585,12 @@ fn layer_panel_caches_content_widths_on_build() { #[test] fn visible_row_range_jumps_directly_to_scrolled_rows() { - let range = visible_row_range(1_000, LAYER_ROW_HEIGHT * 500.0, LAYER_ROW_HEIGHT * 10.0); + let range = visible_row_range( + 1_000, + LAYER_ROW_HEIGHT * 500.0, + LAYER_ROW_HEIGHT * 10.0, + LAYER_ROW_HEIGHT, + ); assert_eq!(range.start, 500); assert!( @@ -600,9 +605,55 @@ fn row_index_at_maps_scrolled_point_to_item_without_linear_scan() { let scroll = LAYER_ROW_HEIGHT * 500.0; let point_y = rows_top + LAYER_ROW_HEIGHT * 2.5; - let (index, row_top) = row_index_at(1_000, rows_top, scroll, LAYER_ROW_HEIGHT * 10.0, point_y) - .expect("point lands on a visible row"); + let (index, row_top) = row_index_at( + 1_000, + rows_top, + scroll, + LAYER_ROW_HEIGHT * 10.0, + LAYER_ROW_HEIGHT, + point_y, + ) + .expect("point lands on a visible row"); assert_eq!(index, 502); assert!((row_top - (rows_top + LAYER_ROW_HEIGHT * 2.0)).abs() < 0.01); } + +/// Pages rows are 32 px tall; the visible-row window and hit-test must +/// use PAGE_ROW_HEIGHT, not the 28 px layer height. With 45 pages +/// scrolled near the bottom, the window must still start at the row +/// whose top the offset lands on — using the wrong height skips rows +/// and leaves a blank gap above the last page. +#[test] +fn pages_visible_range_uses_page_row_height() { + // Scroll so the top of row 39 is at the viewport top. + let scroll = PAGE_ROW_HEIGHT * 39.0; + let range = visible_row_range(45, scroll, PAGE_ROW_HEIGHT * 6.0, PAGE_ROW_HEIGHT); + assert_eq!( + range.start, 39, + "pages window must start at the 32px-row index, got {range:?}" + ); + assert!( + range.end >= 45, + "window must reach the last page, got {range:?}" + ); +} + +#[test] +fn pages_hit_test_uses_page_row_height() { + let rows_top = 60.0; + let scroll = PAGE_ROW_HEIGHT * 39.0; + // Cursor 2.5 rows down from the viewport top → row 41. + let point_y = rows_top + PAGE_ROW_HEIGHT * 2.5; + let (index, row_top) = row_index_at( + 45, + rows_top, + scroll, + PAGE_ROW_HEIGHT * 6.0, + PAGE_ROW_HEIGHT, + point_y, + ) + .expect("point lands on a page row"); + assert_eq!(index, 41, "hit-test must map to the 32px-row index"); + assert!((row_top - (rows_top + PAGE_ROW_HEIGHT * 2.0)).abs() < 0.01); +} diff --git a/crates/op-editor-ui/src/widgets/layer_panel_walkers.rs b/crates/op-editor-ui/src/widgets/layer_panel_walkers.rs index f5ec6787d..9b7a8756b 100644 --- a/crates/op-editor-ui/src/widgets/layer_panel_walkers.rs +++ b/crates/op-editor-ui/src/widgets/layer_panel_walkers.rs @@ -409,12 +409,13 @@ pub(super) fn visible_row_range( row_count: usize, scroll: f32, viewport_h: f32, + row_height: f32, ) -> std::ops::Range { - if row_count == 0 || viewport_h <= 0.0 { + if row_count == 0 || viewport_h <= 0.0 || row_height <= 0.0 { return 0..0; } - let start = (scroll.max(0.0) / LAYER_ROW_HEIGHT).floor() as usize; - let visible = (viewport_h / LAYER_ROW_HEIGHT).ceil().max(0.0) as usize; + let start = (scroll.max(0.0) / row_height).floor() as usize; + let visible = (viewport_h / row_height).ceil().max(0.0) as usize; let end = start .saturating_add(visible) .saturating_add(2) @@ -429,9 +430,10 @@ pub(super) fn row_index_at( rows_top: f32, scroll: f32, viewport_h: f32, + row_height: f32, point_y: f32, ) -> Option<(usize, f32)> { - if row_count == 0 || viewport_h <= 0.0 { + if row_count == 0 || viewport_h <= 0.0 || row_height <= 0.0 { return None; } if point_y < rows_top || point_y > rows_top + viewport_h { @@ -441,10 +443,10 @@ pub(super) fn row_index_at( if local_y < 0.0 { return None; } - let index = (local_y / LAYER_ROW_HEIGHT).floor() as usize; + let index = (local_y / row_height).floor() as usize; if index >= row_count { return None; } - let row_top = rows_top - scroll.max(0.0) + index as f32 * LAYER_ROW_HEIGHT; + let row_top = rows_top - scroll.max(0.0) + index as f32 * row_height; Some((index, row_top)) }