mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Keep overflow text visible when its source cell scrolls out of view
Fixes #1613 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
f1894c6da6
commit
84ce99f9c7
@@ -1035,7 +1035,9 @@ pub(crate) fn paint_quadrant(
|
||||
let (r0, r1) = rows;
|
||||
let (c0, c1) = cols;
|
||||
let range = RangeRef::new(CellRef::new(r0, c0), CellRef::new(r1, c1));
|
||||
let merges: Vec<RangeRef> = sh.merges.iter().filter(|m| m.intersects(&range)).copied().collect();
|
||||
// Merges are looked up 20 columns beyond the quadrant too: text overflowing in from off-screen cells stops at them.
|
||||
let near = RangeRef::new(CellRef::new(r0, c0.saturating_sub(20)), CellRef::new(r1, c1.saturating_add(20).min(MAX_COLS - 1)));
|
||||
let merges: Vec<RangeRef> = sh.merges.iter().filter(|m| m.intersects(&near)).copied().collect();
|
||||
let in_merge = |c: CellRef| merges.iter().find(|m| m.contains(c)).copied();
|
||||
// Gridlines.
|
||||
if sh.show_gridlines {
|
||||
@@ -1089,8 +1091,25 @@ pub(crate) fn paint_quadrant(
|
||||
}
|
||||
looks.push((anchor, rect, lk, cfl));
|
||||
}
|
||||
// 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<CfLook>)> = 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;
|
||||
}
|
||||
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() {
|
||||
let (lk, cfl) = look(wb, si, sh, n, cf);
|
||||
outside.push((n, geo.cell_rect(sh, n), lk, cfl));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Text and per-cell decorations.
|
||||
for (c, rect, lk, cfl) in &looks {
|
||||
for (c, rect, lk, cfl) in looks.iter().chain(&outside) {
|
||||
let c = *c;
|
||||
if let Some(picture) = sh.cell_pictures.get(&c) {
|
||||
crate::cell_pictures::paint(p, *rect, picture);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
//! Overflowing text stays visible in its empty neighbours when its own cell is scrolled out (#1613).
|
||||
|
||||
use egui::{Rect, Shape};
|
||||
use gridcraft_engine::Session;
|
||||
use gridcraft_engine::core::CellRef;
|
||||
use gridcraft_ui_egui::{SheetApp, grid::Geo};
|
||||
use serde_json::json;
|
||||
|
||||
type Harness = egui_kittest::Harness<'static, SheetApp>;
|
||||
|
||||
const LEFT: &str = "This is a very long left-aligned text that overflows into many empty neighbouring cells";
|
||||
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 {
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
setup(&mut s);
|
||||
s.execute("selection.set", json!({"range": "A20"})).unwrap();
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SheetApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
SheetApp::new(s, Default::default()),
|
||||
);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
/// Scroll so that `col` is the first column of the scrolled area.
|
||||
fn scroll_to_col(h: &mut Harness, col: u32) {
|
||||
let app = h.state_mut();
|
||||
let sheet = app.session.active().unwrap().wb.active().unwrap();
|
||||
let fc = sheet.freeze.map(|f| f.1).unwrap_or(0);
|
||||
let x = (sheet.col_left(col) - sheet.col_left(fc)) as f32;
|
||||
app.view_mut().unwrap().scroll.x = x;
|
||||
h.run_steps(4);
|
||||
}
|
||||
|
||||
/// Clip rects of the painted text shapes whose text is `text`.
|
||||
fn text_clips(h: &Harness, text: &str) -> Vec<Rect> {
|
||||
h.output()
|
||||
.shapes
|
||||
.iter()
|
||||
.filter_map(|s| match &s.shape {
|
||||
Shape::Text(t) if t.galley.job.text == text => Some(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();
|
||||
let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll);
|
||||
geo.cell_rect(sheet, CellRef::parse(cell).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_text_stays_visible_when_source_cell_scrolls_out_left() {
|
||||
let mut h = harness(|s| {
|
||||
s.execute("cell.set", json!({"cell": "B1", "input": LEFT})).unwrap();
|
||||
});
|
||||
assert!(!text_clips(&h, LEFT).is_empty(), "B1 is drawn before scrolling");
|
||||
scroll_to_col(&mut h, 2);
|
||||
let c1 = cell_rect(&h, "C1");
|
||||
let clips = text_clips(&h, LEFT);
|
||||
assert!(clips.iter().any(|r| r.intersects(c1.shrink(1.0))), "B1's overflow is still drawn into C1 after scrolling: {clips:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_aligned_overflow_stays_visible_when_source_cell_is_right_of_view() {
|
||||
let mut h = harness(|s| {
|
||||
s.execute("cell.set", json!({"cell": "Z1", "input": RIGHT})).unwrap();
|
||||
s.execute("home.formatCells", json!({"range": "Z1", "style": {"align": {"h": "Right"}}})).unwrap();
|
||||
});
|
||||
// Scroll so that Z1 starts just past the right edge of the cell area.
|
||||
{
|
||||
let app = h.state_mut();
|
||||
let sheet = app.session.active().unwrap().wb.active().unwrap();
|
||||
let geo = Geo::new(sheet, app.grid.rect.unwrap(), app.view().scroll);
|
||||
let x = sheet.col_left(25) as f32 - (geo.cells.width() - geo.frozen_w) / geo.z - 2.0;
|
||||
app.view_mut().unwrap().scroll.x = x;
|
||||
}
|
||||
h.run_steps(4);
|
||||
let (y1, z1, cells) = {
|
||||
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.cell_rect(sheet, CellRef::parse("Y1").unwrap()), geo.cell_rect(sheet, CellRef::parse("Z1").unwrap()), geo.cells)
|
||||
};
|
||||
assert!(z1.left() >= cells.right(), "Z1 is out of view on the right ({z1:?} vs {cells:?})");
|
||||
let visible = y1.intersect(cells).shrink(1.0);
|
||||
let clips = text_clips(&h, RIGHT);
|
||||
assert!(clips.iter().any(|r| r.intersects(visible)), "Z1's overflow is drawn into Y1: {clips:?} vs {visible:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolled_overflow_does_not_draw_over_frozen_columns() {
|
||||
let mut h = harness(|s| {
|
||||
s.execute("cell.set", json!({"cell": "C1", "input": LEFT})).unwrap();
|
||||
s.execute("view.freezePanes", json!({"cell": "B2"})).unwrap();
|
||||
});
|
||||
scroll_to_col(&mut h, 4);
|
||||
let a1 = cell_rect(&h, "A1");
|
||||
let e1 = cell_rect(&h, "E1");
|
||||
let clips = text_clips(&h, LEFT);
|
||||
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:?}");
|
||||
}
|
||||
Reference in New Issue
Block a user