From 17f733a81e7b4fd85dc0f99c7d492cc1919d21a1 Mon Sep 17 00:00:00 2001 From: adifiedio Date: Tue, 3 Nov 2026 12:55:11 +0530 Subject: [PATCH 1/2] Frozen panes: frozen columns keep their text, gridlines and borders while scrolling (#2544) The grid took a column's right edge to be where the next column starts, `x(col + 1)`. At the freeze line the next column is in the scrolled pane and moves with the scroll, so everything measured that way inside the frozen columns shrank as the sheet scrolled right: - text overflowing from a frozen cell was clipped at the scrolled column's edge, until only a character or two showed; - the frozen columns' gridlines stopped short, with a stray vertical line drawn inside them; - a merged range's borders under the last frozen column ended early; - the freeze handle measured the last frozen column wrongly when dropped while scrolled. Geo gains `x_end`/`y_end` (a column's or row's own far edge) and `same_col_pane`, and every such measurement uses them. Text overflows only into empty cells of its own pane. In Page Layout view the gridlines now stop at each page's last column instead of running into the margin. Fixes #2544 --- crates/ui-egui/src/grid/cell_paint.rs | 4 +- crates/ui-egui/src/grid/geo.rs | 18 +++-- crates/ui-egui/src/grid/interact.rs | 4 +- crates/ui-egui/src/grid/quadrant.rs | 31 ++++----- crates/ui-egui/tests/it/borders.rs | 37 +++++++++++ crates/ui-egui/tests/it/freeze_handles.rs | 16 +++++ .../ui-egui/tests/it/text_overflow_scroll.rs | 66 +++++++++++++++++++ docs/ui-parity.md | 3 +- 8 files changed, 153 insertions(+), 26 deletions(-) diff --git a/crates/ui-egui/src/grid/cell_paint.rs b/crates/ui-egui/src/grid/cell_paint.rs index 9c6a0b10b..187790e6c 100644 --- a/crates/ui-egui/src/grid/cell_paint.rs +++ b/crates/ui-egui/src/grid/cell_paint.rs @@ -194,7 +194,7 @@ pub(super) fn merged_edges(wb: &Workbook, si: usize, sh: &Sheet, geo: &Geo, area if sh.col_width(col) == 0.0 { continue; } - let (x0, x1) = (geo.x(sh, col), geo.x(sh, col.saturating_add(1))); + let (x0, x1) = (geo.x(sh, col), geo.x_end(sh, col)); let top = look(wb, si, sh, CellRef::new(m.start.row, col), cf).0.style.borders.top; let bottom = look(wb, si, sh, CellRef::new(m.end.row, col), cf).0.style.borders.bottom; out.push((top, [pos2(x0, rect.top()), pos2(x1, rect.top())])); @@ -205,7 +205,7 @@ pub(super) fn merged_edges(wb: &Workbook, si: usize, sh: &Sheet, geo: &Geo, area if sh.row_height(row) == 0.0 { continue; } - let (y0, y1) = (geo.y(sh, row), geo.y(sh, row.saturating_add(1))); + let (y0, y1) = (geo.y(sh, row), geo.y_end(sh, row)); let left = look(wb, si, sh, CellRef::new(row, m.start.col), cf).0.style.borders.left; let right = look(wb, si, sh, CellRef::new(row, m.end.col), cf).0.style.borders.right; out.push((left, [pos2(rect.left(), y0), pos2(rect.left(), y1)])); diff --git a/crates/ui-egui/src/grid/geo.rs b/crates/ui-egui/src/grid/geo.rs index 33b1b5461..a1005b32f 100644 --- a/crates/ui-egui/src/grid/geo.rs +++ b/crates/ui-egui/src/grid/geo.rs @@ -158,11 +158,21 @@ impl Geo { self.cells.top() + self.frozen_h + (self.ri.start(row) as f32 - self.ri.start(self.fr) as f32 - self.scroll.y) * self.z } } + /// Screen x of `col`'s right edge. Not `x(col + 1)`: across the freeze line (and between + /// pages) the next column is laid out elsewhere (#2544). + pub fn x_end(&self, sh: &Sheet, col: u32) -> f32 { + self.x(sh, col) + sh.col_width(col) * self.z + } + /// Screen y of `row`'s bottom edge (see `x_end`). + pub fn y_end(&self, sh: &Sheet, row: u32) -> f32 { + self.y(sh, row) + sh.row_height(row) * self.z + } + /// Columns `a` and `b` are on the same side of the freeze line, so they scroll together. + pub fn same_col_pane(&self, a: u32, b: u32) -> bool { + (a < self.fc) == (b < self.fc) + } pub fn cell_rect(&self, sh: &Sheet, c: CellRef) -> Rect { - Rect::from_min_max( - pos2(self.x(sh, c.col), self.y(sh, c.row)), - pos2(self.x(sh, c.col) + sh.col_width(c.col) * self.z, self.y(sh, c.row) + sh.row_height(c.row) * self.z), - ) + Rect::from_min_max(pos2(self.x(sh, c.col), self.y(sh, c.row)), pos2(self.x_end(sh, c.col), self.y_end(sh, c.row))) } pub fn range_rect(&self, sh: &Sheet, r: RangeRef) -> Rect { let a = self.cell_rect(sh, r.start); diff --git a/crates/ui-egui/src/grid/interact.rs b/crates/ui-egui/src/grid/interact.rs index 5de25fd53..fc60e0626 100644 --- a/crates/ui-egui/src/grid/interact.rs +++ b/crates/ui-egui/src/grid/interact.rs @@ -891,7 +891,7 @@ fn freeze_boundary(geo: &Geo, sh: &Sheet, p: Pos2, cols: bool) -> u32 { return 0; } let c = geo.col_at(sh, p.x); - let (l, r) = (geo.x(sh, c), geo.x(sh, c.saturating_add(1).min(MAX_COLS - 1))); + let (l, r) = (geo.x(sh, c), geo.x_end(sh, c)); let b = if p.x - l > r - p.x { c.saturating_add(1) } else { c }; let room = geo.ci.at((((geo.cells.width() - 40.0) / geo.z).max(0.0)) as f64); b.min(room).min(MAX_COLS - 1) @@ -900,7 +900,7 @@ fn freeze_boundary(geo: &Geo, sh: &Sheet, p: Pos2, cols: bool) -> u32 { return 0; } let rr = geo.row_at(sh, p.y); - let (t, b2) = (geo.y(sh, rr), geo.y(sh, rr.saturating_add(1).min(MAX_ROWS - 1))); + let (t, b2) = (geo.y(sh, rr), geo.y_end(sh, rr)); let b = if p.y - t > b2 - p.y { rr.saturating_add(1) } else { rr }; let room = geo.ri.at((((geo.cells.height() - 40.0) / geo.z).max(0.0)) as f64); b.min(room).min(MAX_ROWS - 1) diff --git a/crates/ui-egui/src/grid/quadrant.rs b/crates/ui-egui/src/grid/quadrant.rs index 554bdcfdd..9c5712da7 100644 --- a/crates/ui-egui/src/grid/quadrant.rs +++ b/crates/ui-egui/src/grid/quadrant.rs @@ -142,21 +142,17 @@ fn paint_gridlines(q: &QuadrantPaint<'_>) { if sh.show_gridlines { let stroke = Stroke::new(1.0, t.gridline); let top = geo.y(sh, r0); - let bottom = geo.y(sh, r1 + 1); + let bottom = geo.y_end(sh, r1); let left = geo.x(sh, c0); - let right = geo.x(sh, c1 + 1); - for c in c0..=c1 + 1 { - if c <= c1 && sh.col_width(c) == 0.0 { - continue; - } - let x = geo.x(sh, c).round() - 0.5; + let right = geo.x_end(sh, c1); + let xs = (c0..=c1).filter(|&c| sh.col_width(c) > 0.0).map(|c| geo.x(sh, c)).chain([right]); + for x in xs { + let x = x.round() - 0.5; p.line_segment([pos2(x, top), pos2(x, bottom)], stroke); } - for r in r0..=r1 + 1 { - if r <= r1 && sh.row_height(r) == 0.0 { - continue; - } - let y = geo.y(sh, r).round() - 0.5; + let ys = (r0..=r1).filter(|&r| sh.row_height(r) > 0.0).map(|r| geo.y(sh, r)).chain([bottom]); + for y in ys { + let y = y.round() - 0.5; p.line_segment([pos2(left, y), pos2(right, y)], stroke); } } @@ -224,7 +220,6 @@ fn outside_text_cells(q: &mut QuadrantPaint<'_>, merges: &[RangeRef]) -> Vec<(Ce // Text cells just outside the quadrant whose text may overflow into it: the nearest non-empty // cell left of `c0` and right of `c1` in each row (within the same frozen / scrolled pane). let mut outside: Vec<(CellRef, Rect, Look, Option)> = Vec::new(); - let same_pane = |col: u32| (col < geo.fc) == (c0 < geo.fc); for r in r0..=r1 { if sh.row_height(r) == 0.0 { continue; @@ -232,7 +227,7 @@ fn outside_text_cells(q: &mut QuadrantPaint<'_>, merges: &[RangeRef]) -> Vec<(Ce let left = (c0.saturating_sub(20)..c0).rev().map(|col| CellRef::new(r, col)).find(|&n| !empty_ok(n)); let right = (c1.saturating_add(1)..=c1.saturating_add(20).min(MAX_COLS - 1)).map(|col| CellRef::new(r, col)).find(|&n| !empty_ok(n)); for n in [left, right].into_iter().flatten() { - if same_pane(n.col) && sh.col_width(n.col) > 0.0 && in_merge(n).is_none() { + if geo.same_col_pane(n.col, c0) && sh.col_width(n.col) > 0.0 && in_merge(n).is_none() { let (lk, cfl) = look(wb, si, sh, n, cf); outside.push((n, geo.cell_rect(sh, n), lk, cfl)); } @@ -333,7 +328,9 @@ fn paint_cell(q: &QuadrantPaint<'_>, painted: &(CellRef, Rect, Look, Option, painted: &(CellRef, Rect, Look, Option c.col { - right = geo.x(sh, across_end + 1); + right = geo.x_end(sh, across_end); } if matches!(halign, HAlign::Left | HAlign::Center | HAlign::Justify) { let mut cc = c.col + 1; while right - rect.left() < w + 2.0 * pad && cc < MAX_COLS && empty_ok(CellRef::new(c.row, cc)) && cc <= c1 + 20 { - right = geo.x(sh, cc + 1); + right = geo.x_end(sh, cc); cc += 1; } } diff --git a/crates/ui-egui/tests/it/borders.rs b/crates/ui-egui/tests/it/borders.rs index a9f203012..c0213ed92 100644 --- a/crates/ui-egui/tests/it/borders.rs +++ b/crates/ui-egui/tests/it/borders.rs @@ -160,3 +160,40 @@ fn merged_range_draws_the_outline_from_its_edge_cells() { assert_eq!(ys.iter().filter(|y| (**y - max_y).abs() < 0.5).count(), 4, "bottom edge"); assert!(max_x - min_x > 100.0 && max_y - min_y > 40.0, "the outline spans the whole merged area"); } + +/// #2544: a merged range in frozen columns keeps its whole outline while the others scroll: the +/// bottom edge under its last column ends at that column's own right edge, the freeze line. +#[test] +fn frozen_merged_range_keeps_its_outline_when_scrolled() { + let mut h = harness(); + h.state_mut().run("home.borders", json!({"range": "A2:B3", "preset": "thickOutside", "color": "#C81E1E"})).unwrap(); + h.state_mut().run("home.mergeCells", json!({"range": "A2:B3"})).unwrap(); + h.state_mut().run("view.freezePanes", json!({"cell": "C1"})).unwrap(); + h.state_mut().run("selection.set", json!({"range": "H10"})).unwrap(); + h.state_mut().view_mut().unwrap().scroll.x = 300.0; + h.run_steps(6); + let freeze_x = { + let app = h.state(); + let sheet = app.session.active().unwrap().wb.active().unwrap(); + let geo = gridcraft_ui_egui::grid::Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll); + geo.cells.left() + geo.frozen_w + }; + let red = Color32::from_rgb(0xC8, 0x1E, 0x1E); + let right_ends: Vec = h + .output() + .shapes + .iter() + .filter_map(|shape| match &shape.shape { + Shape::LineSegment { points, stroke } if stroke.color == red && (points[0].y - points[1].y).abs() < 0.5 => { + Some(points[0].x.max(points[1].x)) + } + _ => None, + }) + .collect(); + assert_eq!(right_ends.len(), 4, "top and bottom edges, one segment per column: {right_ends:?}"); + assert_eq!( + right_ends.iter().filter(|x| (**x - freeze_x).abs() < 1.0).count(), + 2, + "B's segments end at the freeze line: {right_ends:?} vs {freeze_x}" + ); +} diff --git a/crates/ui-egui/tests/it/freeze_handles.rs b/crates/ui-egui/tests/it/freeze_handles.rs index 7bcc81d1a..e3ccf8c65 100644 --- a/crates/ui-egui/tests/it/freeze_handles.rs +++ b/crates/ui-egui/tests/it/freeze_handles.rs @@ -66,3 +66,19 @@ fn dragging_the_handles_freezes_moves_and_unfreezes() { drag(&mut h, rows.center(), pos2(rows.center().x, g.cells.top() - 5.0)); assert_eq!(freeze(&h), None); } + +/// The last frozen column ends at the freeze line however far the other columns scroll, while +/// the next column's left edge moves with the scroll (#2544). +#[test] +fn last_frozen_column_ends_at_the_freeze_line_when_scrolled() { + let mut h = harness(); + h.state_mut().run("view.freezePanes", serde_json::json!({"cell": "C3"})).unwrap(); + h.state_mut().view_mut().unwrap().scroll = egui::vec2(300.0, 200.0); + h.run_steps(2); + let g = geo(&h); + let sh = h.state().session.active().unwrap().wb.active().unwrap().clone(); + let split = g.cells.min + egui::vec2(g.frozen_w, g.frozen_h); + assert!((g.x_end(&sh, 1) - split.x).abs() < 0.01 && (g.y_end(&sh, 1) - split.y).abs() < 0.01); + assert!(g.x(&sh, 2) < split.x && g.y(&sh, 2) < split.y, "C and row 3 are scrolled under the frozen panes"); + assert!(g.same_col_pane(0, 1) && g.same_col_pane(2, 9) && !g.same_col_pane(1, 2)); +} diff --git a/crates/ui-egui/tests/it/text_overflow_scroll.rs b/crates/ui-egui/tests/it/text_overflow_scroll.rs index 8756666db..076478e52 100644 --- a/crates/ui-egui/tests/it/text_overflow_scroll.rs +++ b/crates/ui-egui/tests/it/text_overflow_scroll.rs @@ -390,3 +390,69 @@ fn wrapped_text_clipping_survives_partial_vertical_scroll() { clip={clip:?}, expected={expected_bottom:.2}" ); } + +/// Issue #2544: text in a frozen column keeps its whole cell while the other columns scroll. +#[test] +fn frozen_column_text_keeps_its_cell_when_scrolled() { + let mut h = harness(|s| { + s.execute("cell.set", json!({"cell": "A1", "input": LEFT})).unwrap(); + s.execute("cell.set", json!({"cell": "C1", "input": "v"})).unwrap(); + s.execute("view.freezeFirstColumn", json!({})).unwrap(); + }); + for col in [1, 2, 3, 8] { + scroll_to_col(&mut h, col); + let a1 = cell_rect(&h, "A1"); + let clips = text_clips(&h, LEFT); + assert_eq!(clips.len(), 1, "A1 is drawn once: {clips:?}"); + assert!( + (clips[0].left() - a1.left()).abs() <= 0.5 && (clips[0].right() - a1.right()).abs() <= 0.5, + "A1's text spans A1 at column {col}: {clips:?} vs {a1:?}" + ); + } +} + +/// Issue #2544: with two frozen columns, A1's text keeps overflowing across the whole of empty B1. +#[test] +fn frozen_text_overflows_to_the_freeze_line_when_scrolled() { + let mut h = harness(|s| { + s.execute("cell.set", json!({"cell": "A1", "input": LEFT})).unwrap(); + s.execute("view.freezePanes", json!({"cell": "C1"})).unwrap(); + }); + for col in [2, 3, 4, 10] { + scroll_to_col(&mut h, col); + let (a1, b1) = (cell_rect(&h, "A1"), cell_rect(&h, "B1")); + let clips = text_clips(&h, LEFT); + assert_eq!(clips.len(), 1, "A1 is drawn once: {clips:?}"); + assert!( + (clips[0].left() - a1.left()).abs() <= 0.5 && (clips[0].right() - b1.right()).abs() <= 0.5, + "A1's text spans A1:B1 at column {col}: {clips:?} vs {a1:?} {b1:?}" + ); + } +} + +/// Issue #2544: the frozen columns' gridlines reach the freeze line, and none is drawn inside +/// them at a scrolled column's edge. +#[test] +fn frozen_column_gridlines_stay_put_when_scrolled() { + let mut h = harness(|s| { + s.execute("view.freezePanes", json!({"cell": "C1"})).unwrap(); + }); + scroll_to_col(&mut h, 6); + let (a5, b5) = (cell_rect(&h, "A5"), cell_rect(&h, "B5")); + let frozen = |s: &egui::epaint::ClippedShape| s.clip_rect.right() <= b5.right() + 0.5 && s.clip_rect.left() <= a5.left(); + let segments: Vec<[egui::Pos2; 2]> = h + .output() + .shapes + .iter() + .filter(|s| frozen(s)) + .filter_map(|s| match &s.shape { + Shape::LineSegment { points, .. } => Some(*points), + _ => None, + }) + .collect(); + let row5 = segments.iter().find(|p| (p[0].y - (a5.top() - 0.5).round()).abs() <= 1.0 && p[0].y == p[1].y); + assert!(row5.is_some_and(|p| p[1].x >= b5.right() - 1.0), "row 5's gridline reaches the freeze line: {row5:?} vs {b5:?}"); + let verticals: Vec = segments.iter().filter(|p| p[0].x == p[1].x && p[0].y < b5.bottom()).map(|p| p[0].x).collect(); + let expected = [a5.left(), a5.right(), b5.right()].map(|x| x.round() - 0.5); + assert!(verticals.iter().all(|x| expected.iter().any(|e| (x - e).abs() <= 0.5)), "only A and B's edges: {verticals:?} vs {expected:?}"); +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 125182d27..213ce49a3 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-02 · **Change:** minor (RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (frozen columns keep their text, gridlines and merged borders whole while the sheet scrolls (#2544); earlier: RefEdit reference boxes in dialogs: Goal Seek, Solver and Data Analysis use them, the arrow keys select while a dialog is collapsed and F4 cycles a reference's anchors; earlier: Python cells: Python mode in the formula bar; earlier: Insert › Equation and the equation editor; earlier: every floating object selects, resizes from eight handles and deletes with the keyboard; earlier: Formulas › Error Checking › Trace Error; trace arrows stay through a zoom (#3097); earlier: Page Setup sets the page order, Down, then over or Over, then down, which printing, the print preview, Page Break Preview and Page Layout view follow; earlier: Formulas › Function Library as labelled buttons with category menus; Recently Used and the Insert Function categories; Esc leaves a File-page text field before closing the view; earlier: checkbox cells print as boxes; Ctrl+arrow and Ctrl+Shift+arrow while pointing a formula reference; editing a hyperlink loads its existing fields; AutoSum over a selected row puts its total to the right; ribbon catalog count 276 / 292) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4 Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md). **UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured @@ -237,6 +237,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | Date | Change | Summary | |---|---|---| +| 2026-11-03 | minor | Frozen panes: text in frozen columns overflows only into its own pane and keeps its whole width while the sheet scrolls; frozen gridlines and merged-cell borders reach the freeze line, and the freeze handle measures the last frozen column itself (#2544). Page Layout view gridlines stop at each page's last column | | 2026-11-02 | minor | RefEdit reference boxes in dialogs: pick a range on the grid, collapse the dialog while selecting; Goal Seek, Solver (with the Model Range of Load/Save) and Data Analysis use them; while collapsed the arrow keys move the selection and Shift+arrow extends it, F4 cycles a reference's anchors, a click elsewhere keeps the box picking, a dialog opened over a picking one puts the selection back, OK stays on the sheet its command showed, and scripted cancel and confirm end a pick like the buttons; the dialog's sheet is followed through the recorded undo steps, so a sheet deleted and recreated under its name is not taken for it and an undo of the deletion brings it back; a box the dialog stops showing is no longer picked into | | 2026-11-02 | minor | Python mode edits a cell's code in the formula bar's code editor (Ctrl+Alt+Shift+P, `=PY` then Tab, editing a Python cell), with the PY badge beside fx; its keys apply in input order, also several in one frame, and Ctrl+Enter commits through `python.set` (empty code clears the cell), keeping the code and showing the command's message when the code is refused | | 2026-11-02 | minor | Insert › Symbols: Equation split button (access keys N E), a gallery of examples, and an equation editor with quick-insert buttons, the error underlined at its position and a live preview; double-clicking an equation edits it | From 56f0679fed05aa521d2e3665870a5d3456b9a098 Mon Sep 17 00:00:00 2001 From: adifiedio Date: Tue, 3 Nov 2026 13:13:29 +0530 Subject: [PATCH 2/2] Frozen panes: test the scrolled freeze-handle drag and the frozen text's painted extent (#2544) Review follow-up: a column-handle drop on the last frozen column while scrolled, which fails if freeze_boundary measures with x(c + 1), and a check that frozen A1's text lies inside its clip, not only that the clip is A1. --- crates/ui-egui/tests/it/freeze_handles.rs | 17 +++++++++++++++++ crates/ui-egui/tests/it/text_overflow_scroll.rs | 14 ++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/crates/ui-egui/tests/it/freeze_handles.rs b/crates/ui-egui/tests/it/freeze_handles.rs index e3ccf8c65..141d0b9ef 100644 --- a/crates/ui-egui/tests/it/freeze_handles.rs +++ b/crates/ui-egui/tests/it/freeze_handles.rs @@ -82,3 +82,20 @@ fn last_frozen_column_ends_at_the_freeze_line_when_scrolled() { assert!(g.x(&sh, 2) < split.x && g.y(&sh, 2) < split.y, "C and row 3 are scrolled under the frozen panes"); assert!(g.same_col_pane(0, 1) && g.same_col_pane(2, 9) && !g.same_col_pane(1, 2)); } + +/// Dropping the column handle on the left part of the last frozen column while scrolled freezes +/// up to its left edge: the column's right edge is the freeze line, not the next column's +/// scrolled position (#2544). +#[test] +fn dragging_the_column_handle_when_scrolled_measures_the_last_frozen_column() { + let mut h = harness(); + h.state_mut().run("view.freezePanes", serde_json::json!({"cell": "C1"})).unwrap(); + h.state_mut().view_mut().unwrap().scroll = egui::vec2(300.0, 0.0); + h.run_steps(2); + let g = geo(&h); + let sh = h.state().session.active().unwrap().wb.active().unwrap().clone(); + let (cols, _) = freeze_handles(&g); + assert!(g.x(&sh, 2) < g.x(&sh, 1), "C is scrolled left of B"); + drag(&mut h, cols.center(), pos2(g.x(&sh, 1) + 10.0, g.cells.top() + 40.0)); + assert_eq!(freeze(&h), Some((0, 1))); +} diff --git a/crates/ui-egui/tests/it/text_overflow_scroll.rs b/crates/ui-egui/tests/it/text_overflow_scroll.rs index 076478e52..371c0aeee 100644 --- a/crates/ui-egui/tests/it/text_overflow_scroll.rs +++ b/crates/ui-egui/tests/it/text_overflow_scroll.rs @@ -53,6 +53,18 @@ fn text_clips(h: &Harness, text: &str) -> Vec { .collect() } +/// Painted extents of the text shapes whose text is `text`, cut to their clip rects. +fn text_visible_rects(h: &Harness, text: &str) -> Vec { + h.output() + .shapes + .iter() + .filter_map(|s| match &s.shape { + Shape::Text(t) if t.galley.job.text == text => Some(t.visual_bounding_rect().intersect(s.clip_rect)), + _ => None, + }) + .collect() +} + fn cell_rect(h: &Harness, cell: &str) -> Rect { let app = h.state(); let sheet = app.session.active().unwrap().wb.active().unwrap(); @@ -408,6 +420,8 @@ fn frozen_column_text_keeps_its_cell_when_scrolled() { (clips[0].left() - a1.left()).abs() <= 0.5 && (clips[0].right() - a1.right()).abs() <= 0.5, "A1's text spans A1 at column {col}: {clips:?} vs {a1:?}" ); + let visible = text_visible_rects(&h, LEFT); + assert!(visible.len() == 1 && visible[0].is_positive(), "A1's text shows inside its clip at column {col}: {visible:?}"); } }