From f9b869b3a4e7f66f8862bfaada1a8e3f54b92441 Mon Sep 17 00:00:00 2001 From: AnhNgoo <110978888+AnhNgoo@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:03:09 +0700 Subject: [PATCH 1/5] fix(grid): prevent partial wrapped text lines in fixed-height rows --- crates/ui-egui/src/grid/quadrant.rs | 13 +- .../ui-egui/tests/it/text_overflow_scroll.rs | 181 ++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/grid/quadrant.rs b/crates/ui-egui/src/grid/quadrant.rs index aa9e6fc07..0fbdf14aa 100644 --- a/crates/ui-egui/src/grid/quadrant.rs +++ b/crates/ui-egui/src/grid/quadrant.rs @@ -260,7 +260,18 @@ pub(crate) fn paint_quadrant( _ => text_left + pad + indent, }; let y = wrapped_cell_text_y(st.align.v, *rect, galley.size().y, z); - p.with_clip_rect(rect.intersect(p.clip_rect())).galley(pos2(x, y), galley, color); + let mut text_clip = rect.intersect(p.clip_rect()); + + // A partially visible wrapped line can leave a few pixels + // touching the row border. For top-aligned text, show only + // complete lines within the available cell height. + if st.align.v == VAlign::Top { + let last_full_bottom = galley.rows.iter().map(|row| y + row.rect().bottom()).rfind(|bottom| *bottom <= rect.bottom() + 0.01); + + text_clip.max.y = last_full_bottom.map(|bottom| text_clip.bottom().min(bottom)).unwrap_or(text_clip.top()).max(text_clip.top()); + } + + p.with_clip_rect(text_clip).galley(pos2(x, y), galley, color); continue; } // Wrap Text off: Excel ignores the line breaks and draws the whole string on one line. diff --git a/crates/ui-egui/tests/it/text_overflow_scroll.rs b/crates/ui-egui/tests/it/text_overflow_scroll.rs index 10d5e1848..da0c22183 100644 --- a/crates/ui-egui/tests/it/text_overflow_scroll.rs +++ b/crates/ui-egui/tests/it/text_overflow_scroll.rs @@ -108,3 +108,184 @@ fn scrolled_overflow_does_not_draw_over_frozen_columns() { assert!(clips.iter().any(|r| r.intersects(e1.shrink(1.0))), "C1's overflow reaches E1: {clips:?}"); assert!(clips.iter().all(|r| !r.intersects(a1.shrink(1.0))), "C1's overflow stays out of frozen column A: {clips:?}"); } + +/// Issue #2752: fixed-height wrapped cells must not paint +/// a fraction of a text line against the next row's border. +#[test] +fn wrapped_text_in_fixed_height_row_shows_only_complete_lines() { + const TEXT: &str = "Row 3 target. This is intentionally long text that must wrap \ + across many lines within a narrow column. It must never \ + paint over the next row."; + + let h = harness(|s| { + s.execute("home.columnWidth", json!({"cols": "B:B", "chars": 12})).unwrap(); + + s.execute("cell.set", json!({"cell": "B3", "input": TEXT})).unwrap(); + + s.execute("home.wrapText", json!({"range": "B3", "on": true})).unwrap(); + + s.execute("home.formatCells", json!({"range": "B3", "style": {"align": {"v": "Top"}}})).unwrap(); + + // Apply the custom height after Wrap Text so AutoFit + // cannot expand the row for this scenario. + s.execute("home.rowHeight", json!({"rows": "3:3", "height": 62.67})).unwrap(); + }); + + let cell = cell_rect(&h, "B3"); + + let shapes: Vec<_> = h + .output() + .shapes + .iter() + .filter_map(|shape| match &shape.shape { + Shape::Text(t) if t.galley.job.text == TEXT => Some((t, shape.clip_rect)), + _ => None, + }) + .collect(); + + assert_eq!(shapes.len(), 1, "expected one painted wrapped text shape"); + + let (text, clip) = shapes[0]; + + assert!(text.galley.rows.len() > 4, "test must produce multiple wrapped lines"); + + assert!(text.galley.size().y > cell.height(), "wrapped text must exceed the fixed row height"); + + assert!(clip.top() >= cell.top() - 0.5 && clip.bottom() <= cell.bottom() + 0.5, "text clip must stay within B3: {clip:?} vs {cell:?}"); + + let mut full_rows_visible = 0; + + for (index, row) in text.galley.rows.iter().enumerate() { + let top = text.pos.y + row.rect().top(); + let bottom = text.pos.y + row.rect().bottom(); + let full_height = bottom - top; + + let visible_height = (bottom.min(clip.bottom()) - top.max(clip.top())).max(0.0); + + let partly_visible = visible_height > 0.5 && visible_height < full_height - 0.5; + + assert!( + !partly_visible, + "wrapped line {} is partially visible: \ + line=[{top:.2}, {bottom:.2}], clip={clip:?}", + index + 1 + ); + + if visible_height >= full_height - 0.5 { + full_rows_visible += 1; + } + } + + assert!(full_rows_visible >= 2, "the clipping fix must still show complete text lines"); +} + +/// Issue #2752: a row shorter than a text line must not +/// render a fragment of wrapped text. +#[test] +fn wrapped_text_in_tiny_row_has_no_partial_lines() { + const TEXT: &str = "This wrapped text should not be partly painted."; + + let h = harness(|s| { + s.execute("home.columnWidth", json!({"cols": "B:B", "chars": 12})).unwrap(); + + s.execute("cell.set", json!({"cell": "B3", "input": TEXT})).unwrap(); + + s.execute("home.wrapText", json!({"range": "B3", "on": true})).unwrap(); + + s.execute("home.formatCells", json!({"range": "B3", "style": {"align": {"v": "Top"}}})).unwrap(); + + s.execute("home.rowHeight", json!({"rows": "3:3", "height": 5})).unwrap(); + }); + + let cell = cell_rect(&h, "B3"); + + let shapes: Vec<_> = h + .output() + .shapes + .iter() + .filter_map(|shape| match &shape.shape { + Shape::Text(t) if t.galley.job.text == TEXT => Some((t, shape.clip_rect)), + _ => None, + }) + .collect(); + + assert_eq!(shapes.len(), 1, "expected the wrapped text shape"); + + let (text, clip) = shapes[0]; + + assert!(text.galley.rows.len() > 1, "text must wrap into multiple lines"); + + assert!(cell.height() < text.galley.rows[0].rect().height(), "test setup must produce a row shorter than one line"); + + assert!(clip.top() >= cell.top() - 0.5 && clip.bottom() <= cell.bottom() + 0.5, "text clip must remain inside its cell"); + + assert!(clip.height() <= 0.5, "no partial line should be visible: clip={clip:?}"); +} + +/// Issue #2752: scrolling through a wrapped cell may clip at +/// the viewport edge, but must not change its row-bottom limit. +#[test] +fn wrapped_text_clipping_survives_partial_vertical_scroll() { + const TEXT: &str = "Wrapped viewport regression text with enough words \ + to occupy many lines in a narrow spreadsheet column."; + + let mut h = harness(|s| { + s.execute("home.columnWidth", json!({"cols": "B:B", "chars": 12})).unwrap(); + s.execute("cell.set", json!({"cell": "B3", "input": TEXT})).unwrap(); + s.execute("home.wrapText", json!({"range": "B3", "on": true})).unwrap(); + s.execute("home.formatCells", json!({"range": "B3", "style": {"align": {"v": "Top"}}})).unwrap(); + s.execute("home.rowHeight", json!({"rows": "3:3", "height": 76})).unwrap(); + }); + + let original_cell = cell_rect(&h, "B3"); + let original_clip = text_clips(&h, TEXT); + assert_eq!(original_clip.len(), 1); + + { + let app = h.state_mut(); + let sheet = app.session.active().unwrap().wb.active().unwrap(); + let scroll = sheet.row_top(2) as f32 + 8.0; + app.view_mut().unwrap().scroll.y = scroll; + } + + h.run_steps(4); + + let cell = cell_rect(&h, "B3"); + let app = h.state(); + let sheet = app.session.active().unwrap().wb.active().unwrap(); + let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll); + let viewport = geo.cells; + + let shapes: Vec<_> = h + .output() + .shapes + .iter() + .filter_map(|shape| match &shape.shape { + Shape::Text(t) if t.galley.job.text == TEXT => Some((t, shape.clip_rect)), + _ => None, + }) + .collect(); + + assert_eq!(shapes.len(), 1, "B3 must remain visible after scrolling"); + + let (text, clip) = shapes[0]; + + assert!(clip.top() > cell.top() + 1.0, "test must scroll through B3: clip={clip:?}, cell={cell:?}"); + + assert!(clip.top() >= viewport.top() - 0.5, "text must not paint above viewport: {clip:?} vs {viewport:?}"); + + assert!(clip.bottom() <= cell.bottom() + 0.5, "text must stay within B3: {clip:?} vs {cell:?}"); + + let full_line_bottoms: Vec = + text.galley.rows.iter().map(|row| text.pos.y + row.rect().bottom()).filter(|bottom| *bottom <= cell.bottom() + 0.01).collect(); + + let expected_bottom = full_line_bottoms.last().copied().expect("at least one wrapped line must fit B3"); + + assert!( + (clip.bottom() - expected_bottom).abs() <= 0.5, + "scrolling must preserve complete-line clipping: \ + clip={clip:?}, expected_bottom={expected_bottom:.2}" + ); + + assert!(original_cell.height() > 0.0 && original_clip[0].height() > 0.0, "unscrolled B3 must display text"); +} From 9e23c98f3a99f030c70256f1b971d237ad5b1e20 Mon Sep 17 00:00:00 2001 From: AnhNgoo <110978888+AnhNgoo@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:53:37 +0700 Subject: [PATCH 2/5] test(grid): address CodeRabbit feedback for wrapped text clipping --- crates/ui-egui/src/grid/quadrant.rs | 8 ++ .../ui-egui/tests/it/text_overflow_scroll.rs | 122 +++++++++++++----- 2 files changed, 100 insertions(+), 30 deletions(-) diff --git a/crates/ui-egui/src/grid/quadrant.rs b/crates/ui-egui/src/grid/quadrant.rs index 0fbdf14aa..22cdfba19 100644 --- a/crates/ui-egui/src/grid/quadrant.rs +++ b/crates/ui-egui/src/grid/quadrant.rs @@ -65,6 +65,14 @@ fn mirror_shape(shape: &mut egui::Shape, axis: f32) { } } +/// Paint a worksheet quadrant, including cell backgrounds, borders, and text. +/// +/// Cell content is clipped to its drawing region and the current painter +/// viewport. For top-aligned wrapped text, only complete lines that fit +/// within the cell height are painted, preventing fragments of an additional +/// line from appearing against the next row's border. +/// +/// Other vertical alignment modes retain their existing rendering behavior. #[allow(clippy::too_many_arguments)] pub(crate) fn paint_quadrant( p: &Painter, diff --git a/crates/ui-egui/tests/it/text_overflow_scroll.rs b/crates/ui-egui/tests/it/text_overflow_scroll.rs index da0c22183..f3bc0a38b 100644 --- a/crates/ui-egui/tests/it/text_overflow_scroll.rs +++ b/crates/ui-egui/tests/it/text_overflow_scroll.rs @@ -12,11 +12,15 @@ const LEFT: &str = "This is a very long left-aligned text that overflows into ma const RIGHT: &str = "This is a very long right-aligned text that overflows into many empty neighbouring cells"; fn harness(setup: impl FnOnce(&mut Session)) -> Harness { + harness_with_size(egui::vec2(1200.0, 800.0), setup) +} + +fn harness_with_size(size: egui::Vec2, setup: impl FnOnce(&mut Session)) -> Harness { let mut s = Session::new(); s.new_workbook(); setup(&mut s); s.execute("selection.set", json!({"range": "A20"})).unwrap(); - let mut h = crate::common::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + let mut h = crate::common::builder().with_size(size).build_ui_state( |ui, app: &mut SheetApp| { app.logic(ui.ctx()); app.ui(ui); @@ -222,70 +226,128 @@ fn wrapped_text_in_tiny_row_has_no_partial_lines() { assert!(clip.height() <= 0.5, "no partial line should be visible: clip={clip:?}"); } -/// Issue #2752: scrolling through a wrapped cell may clip at -/// the viewport edge, but must not change its row-bottom limit. +/// Issue #2752: the viewport bottom can cut through a line +/// that otherwise fits fully inside a wrapped cell. #[test] fn wrapped_text_clipping_survives_partial_vertical_scroll() { - const TEXT: &str = "Wrapped viewport regression text with enough words \ - to occupy many lines in a narrow spreadsheet column."; + let text = "Wrapped text must stay within both the worksheet cell \ + and the viewport during vertical scrolling. " + .repeat(40); - let mut h = harness(|s| { + let mut h = harness_with_size(egui::vec2(1200.0, 520.0), |s| { s.execute("home.columnWidth", json!({"cols": "B:B", "chars": 12})).unwrap(); - s.execute("cell.set", json!({"cell": "B3", "input": TEXT})).unwrap(); + s.execute("cell.set", json!({"cell": "B3", "input": text})).unwrap(); s.execute("home.wrapText", json!({"range": "B3", "on": true})).unwrap(); s.execute("home.formatCells", json!({"range": "B3", "style": {"align": {"v": "Top"}}})).unwrap(); - s.execute("home.rowHeight", json!({"rows": "3:3", "height": 76})).unwrap(); + s.execute("home.rowHeight", json!({"rows": "3:3", "height": 350})).unwrap(); }); - let original_cell = cell_rect(&h, "B3"); - let original_clip = text_clips(&h, TEXT); - assert_eq!(original_clip.len(), 1); + let initial_cell = cell_rect(&h, "B3"); + + let (target_center, zoom) = { + let app = h.state(); + let sheet = app.session.active().unwrap().wb.active().unwrap(); + let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll); + + let shapes: Vec<_> = h + .output() + .shapes + .iter() + .filter_map(|shape| match &shape.shape { + Shape::Text(t) if t.galley.job.text == text => Some(t), + _ => None, + }) + .collect(); + + assert_eq!(shapes.len(), 1, "expected wrapped B3 text"); + + let painted = shapes[0]; + + let center = painted + .galley + .rows + .iter() + .map(|row| { + let top = painted.pos.y + row.rect().top(); + let bottom = painted.pos.y + row.rect().bottom(); + (top, bottom) + }) + .find(|(top, bottom)| { + let center = (top + bottom) / 2.0; + center > geo.cells.bottom() + 32.0 && *bottom < initial_cell.bottom() - 1.0 + }) + .map(|(top, bottom)| (top + bottom) / 2.0) + .expect("fixture must provide a cell-fitting line below the viewport"); + + (center, geo.z) + }; + + let viewport_bottom_before_scroll = { + let app = h.state(); + let sheet = app.session.active().unwrap().wb.active().unwrap(); + let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll); + geo.cells.bottom() + }; { let app = h.state_mut(); - let sheet = app.session.active().unwrap().wb.active().unwrap(); - let scroll = sheet.row_top(2) as f32 + 8.0; - app.view_mut().unwrap().scroll.y = scroll; + app.view_mut().unwrap().scroll.y = (target_center - viewport_bottom_before_scroll) / zoom; } h.run_steps(4); let cell = cell_rect(&h, "B3"); - let app = h.state(); - let sheet = app.session.active().unwrap().wb.active().unwrap(); - let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll); - let viewport = geo.cells; + + let (viewport, scroll_zoom) = { + let app = h.state(); + let sheet = app.session.active().unwrap().wb.active().unwrap(); + let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll); + (geo.cells, geo.z) + }; + + assert!(scroll_zoom > 0.0); let shapes: Vec<_> = h .output() .shapes .iter() .filter_map(|shape| match &shape.shape { - Shape::Text(t) if t.galley.job.text == TEXT => Some((t, shape.clip_rect)), + Shape::Text(t) if t.galley.job.text == text => Some((t, shape.clip_rect)), _ => None, }) .collect(); - assert_eq!(shapes.len(), 1, "B3 must remain visible after scrolling"); + assert_eq!(shapes.len(), 1, "B3 must remain painted"); - let (text, clip) = shapes[0]; + let (painted, clip) = shapes[0]; - assert!(clip.top() > cell.top() + 1.0, "test must scroll through B3: clip={clip:?}, cell={cell:?}"); + assert!(clip.top() >= viewport.top() - 0.5, "clip exceeds viewport top: {clip:?} vs {viewport:?}"); - assert!(clip.top() >= viewport.top() - 0.5, "text must not paint above viewport: {clip:?} vs {viewport:?}"); + assert!(clip.bottom() <= viewport.bottom() + 0.5, "clip exceeds viewport bottom: {clip:?} vs {viewport:?}"); - assert!(clip.bottom() <= cell.bottom() + 0.5, "text must stay within B3: {clip:?} vs {cell:?}"); + assert!(clip.bottom() <= cell.bottom() + 0.5, "clip exceeds cell bottom: {clip:?} vs {cell:?}"); let full_line_bottoms: Vec = - text.galley.rows.iter().map(|row| text.pos.y + row.rect().bottom()).filter(|bottom| *bottom <= cell.bottom() + 0.01).collect(); + painted.galley.rows.iter().map(|row| painted.pos.y + row.rect().bottom()).filter(|bottom| *bottom <= cell.bottom() + 0.01).collect(); - let expected_bottom = full_line_bottoms.last().copied().expect("at least one wrapped line must fit B3"); + let cell_line_bottom = full_line_bottoms.last().copied().expect("at least one line must fit within B3"); + + assert!( + painted.galley.rows.iter().any(|row| { + let top = painted.pos.y + row.rect().top(); + let bottom = painted.pos.y + row.rect().bottom(); + + top + 0.5 < viewport.bottom() && viewport.bottom() < bottom - 0.5 && bottom <= cell.bottom() + 0.01 + }), + "viewport bottom must cross a line that fits B3: \ + viewport={viewport:?}, cell={cell:?}" + ); + + let expected_bottom = viewport.bottom().min(cell_line_bottom); assert!( (clip.bottom() - expected_bottom).abs() <= 0.5, - "scrolling must preserve complete-line clipping: \ - clip={clip:?}, expected_bottom={expected_bottom:.2}" + "clip must respect viewport and complete-line limits: \ + clip={clip:?}, expected={expected_bottom:.2}" ); - - assert!(original_cell.height() > 0.0 && original_clip[0].height() > 0.0, "unscrolled B3 must display text"); } From 6af06e9d30bf27b639adf8cffde4a18cea9d4c64 Mon Sep 17 00:00:00 2001 From: AnhNgoo <110978888+AnhNgoo@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:38:42 +0700 Subject: [PATCH 3/5] fix(grid): preserve full-cell clipping for fitting wrapped text --- crates/ui-egui/src/grid/quadrant.rs | 36 ++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/grid/quadrant.rs b/crates/ui-egui/src/grid/quadrant.rs index 3491c672d..5d60a2441 100644 --- a/crates/ui-egui/src/grid/quadrant.rs +++ b/crates/ui-egui/src/grid/quadrant.rs @@ -68,6 +68,30 @@ fn mirror_shape(shape: &mut egui::Shape, axis: f32) { } } +/// Restrict top-aligned wrapped text to complete lines inside the cell. +/// +/// Preserve the cell and viewport clip when every line fits. Otherwise, +/// exclude overflowing lines while retaining the visible fitting lines. +fn wrapped_text_clip(p: &Painter, rect: Rect, galley: &egui::Galley, y: f32, valign: VAlign) -> Rect { + let mut clip = rect.intersect(p.clip_rect()); + if valign != VAlign::Top { + return clip; + } + + let Some(last_row) = galley.rows.last() else { + return clip; + }; + + if y + last_row.rect().bottom() <= rect.bottom() + 0.01 { + return clip; + } + + let last_full_bottom = galley.rows.iter().map(|row| y + row.rect().bottom()).rfind(|bottom| *bottom <= rect.bottom() + 0.01); + + clip.max.y = last_full_bottom.map(|bottom| clip.bottom().min(bottom)).unwrap_or(clip.top()).max(clip.top()); + + clip +} pub(crate) struct QuadrantPaint<'a> { pub(crate) painter: &'a Painter, pub(crate) geo: &'a Geo, @@ -368,17 +392,7 @@ fn paint_cell(q: &QuadrantPaint<'_>, painted: &(CellRef, Rect, Look, Option text_left + pad + indent, }; let y = wrapped_cell_text_y(st.align.v, *rect, galley.size().y, z); - let mut text_clip = rect.intersect(p.clip_rect()); - - // A partially visible wrapped line can leave a few pixels - // touching the row border. For top-aligned text, show only - // complete lines within the available cell height. - if st.align.v == VAlign::Top { - let last_full_bottom = galley.rows.iter().map(|row| y + row.rect().bottom()).rfind(|bottom| *bottom <= rect.bottom() + 0.01); - - text_clip.max.y = last_full_bottom.map(|bottom| text_clip.bottom().min(bottom)).unwrap_or(text_clip.top()).max(text_clip.top()); - } - + let text_clip = wrapped_text_clip(p, *rect, &galley, y, st.align.v); p.with_clip_rect(text_clip).galley(pos2(x, y), galley, color); return; } From d1f063636a848d07d827a9e60efa9c70c8473eae Mon Sep 17 00:00:00 2001 From: AnhNgoo <110978888+AnhNgoo@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:51:22 +0700 Subject: [PATCH 4/5] test(grid): preserve clipping for fully fitting wrapped text --- .../ui-egui/tests/it/text_overflow_scroll.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/ui-egui/tests/it/text_overflow_scroll.rs b/crates/ui-egui/tests/it/text_overflow_scroll.rs index f3bc0a38b..8756666db 100644 --- a/crates/ui-egui/tests/it/text_overflow_scroll.rs +++ b/crates/ui-egui/tests/it/text_overflow_scroll.rs @@ -113,6 +113,45 @@ fn scrolled_overflow_does_not_draw_over_frozen_columns() { assert!(clips.iter().all(|r| !r.intersects(a1.shrink(1.0))), "C1's overflow stays out of frozen column A: {clips:?}"); } +/// Fully fitting wrapped lines retain the whole cell clip, including +/// room for glyphs extending below their logical line boxes. +#[test] +fn wrapped_text_fitting_lines_keep_full_cell_clip() { + const TEXT: &str = "Short wrapped text with two or three lines."; + + let h = harness(|s| { + s.execute("home.columnWidth", json!({"cols": "B:B", "chars": 15})).unwrap(); + s.execute("cell.set", json!({"cell": "B3", "input": TEXT})).unwrap(); + s.execute("home.wrapText", json!({"range": "B3", "on": true})).unwrap(); + s.execute("home.formatCells", json!({"range": "B3", "style": {"align": {"v": "Top"}}})).unwrap(); + s.execute("home.rowHeight", json!({"rows": "3:3", "height": 120})).unwrap(); + }); + + let cell = cell_rect(&h, "B3"); + + let shapes: Vec<_> = h + .output() + .shapes + .iter() + .filter_map(|shape| match &shape.shape { + Shape::Text(t) if t.galley.job.text == TEXT => Some((t, shape.clip_rect)), + _ => None, + }) + .collect(); + + assert_eq!(shapes.len(), 1, "expected one wrapped text shape"); + + let (text, clip) = shapes[0]; + + assert!(text.galley.rows.len() >= 2, "text must wrap"); + assert!(text.galley.size().y < cell.height(), "all lines must fit"); + + let last_bottom = text.pos.y + text.galley.rows.last().unwrap().rect().bottom(); + + assert!(last_bottom < cell.bottom() - 1.0, "room must remain below the final line"); + + assert!((clip.bottom() - cell.bottom()).abs() <= 0.5, "full cell clip must be retained: clip={clip:?}, cell={cell:?}"); +} /// Issue #2752: fixed-height wrapped cells must not paint /// a fraction of a text line against the next row's border. #[test] From bc1e19f1f652e60c4ca44595da3af58d6f6531a8 Mon Sep 17 00:00:00 2001 From: AnhNgoo <110978888+AnhNgoo@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:02:14 +0700 Subject: [PATCH 5/5] docs(grid): document cell painting and clipping behavior --- crates/ui-egui/src/grid/quadrant.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ui-egui/src/grid/quadrant.rs b/crates/ui-egui/src/grid/quadrant.rs index 5d60a2441..554bdcfdd 100644 --- a/crates/ui-egui/src/grid/quadrant.rs +++ b/crates/ui-egui/src/grid/quadrant.rs @@ -317,6 +317,10 @@ fn should_expand_text(filter_space: f32, overflows: bool, across_end: u32, col: filter_space == 0.0 && (overflows || across_end > col) && !in_merge } +/// Paint one worksheet cell using its computed appearance and visible geometry. +/// +/// Handles text layout, wrapping, alignment, overflow into neighbouring cells, +/// and clipping, while preserving cell decoration and merged-cell constraints. fn paint_cell(q: &QuadrantPaint<'_>, painted: &(CellRef, Rect, Look, Option), merges: &[RangeRef]) { let (cell, rect, lk, cfl) = painted; let c = *cell;