mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-25 11:16:49 -06:00
Excel's cell cursor doesn't jump: press an arrow key or click a cell and the
border slides over; extend the selection and it grows. The outline of the
current area now does the same, in 0.12 s with a decelerating curve, from
wherever it is drawn (a held arrow key reads as one motion), with its fill
and fill handle. The timing is an estimate, not yet measured against Excel.
The same cells moving on screen (scrolling, zoom, a resized column), another
sheet or workbook, an outline that wasn't drawn the frame before, and moves
that start or end off screen don't glide. Only painting waits: the
selection, hit testing and the cell editor use the real cells at once.
View > Animations (under Display theme) turns it off; view.animations {on?}
and ui.set {animations?} do the same for agents. Saved with the UI
preferences, on by default; translated in the five catalogs.
Scripted pictures never catch an outline in flight: it stays on its cells
while a ui.screenshot is on its way and when egui's animations are off (the
headless harness, so the offscreen snapshot). The marching ants around a
copied range now advance by the clock instead of once per painted frame,
since a glide repaints every frame and made them lurch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
4.1 KiB
Rust
100 lines
4.1 KiB
Rust
//! The selection's fill leaves the active cell clear, so it is plain where typing goes.
|
|
|
|
use egui::Rect;
|
|
use gridcraft_engine::Session;
|
|
use gridcraft_engine::core::RangeRef;
|
|
use gridcraft_ui_egui::grid::{Geo, selection_fill};
|
|
use serde_json::json;
|
|
|
|
fn session() -> Session {
|
|
let mut session = Session::new();
|
|
session.new_workbook();
|
|
session
|
|
}
|
|
|
|
fn geo(session: &Session) -> Geo {
|
|
let sheet = session.active().unwrap().wb.active().unwrap();
|
|
Geo::new(sheet, Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1200.0, 800.0)), egui::Vec2::ZERO)
|
|
}
|
|
|
|
/// Where a cell or range is on screen.
|
|
fn cells(session: &Session, a1: &str) -> Rect {
|
|
let sheet = session.active().unwrap().wb.active().unwrap();
|
|
geo(session).range_rect(sheet, RangeRef::parse(a1).unwrap())
|
|
}
|
|
|
|
/// The fill at rest: the current area's outline is on its cells.
|
|
fn fill(session: &Session) -> Vec<Rect> {
|
|
let doc = session.active().unwrap();
|
|
let sheet = doc.wb.active().unwrap();
|
|
let geo = geo(session);
|
|
selection_fill(&geo, sheet, &doc.selection, geo.range_rect(sheet, doc.selection.current()))
|
|
}
|
|
|
|
fn shaded(fill: &[Rect], cell: Rect) -> bool {
|
|
fill.iter().any(|part| part.intersects(cell.shrink(1.0)))
|
|
}
|
|
|
|
fn area(fill: &[Rect]) -> f32 {
|
|
fill.iter().map(Rect::area).sum()
|
|
}
|
|
|
|
#[test]
|
|
fn the_active_cell_of_a_range_is_not_shaded() {
|
|
let mut s = session();
|
|
// At the corner where a drag began, then wherever Tab and Enter move it inside the range.
|
|
for active in ["B2", "C4", "D5"] {
|
|
s.execute("selection.set", json!({"range": "B2:D5", "active": active})).unwrap();
|
|
let fill = fill(&s);
|
|
assert!(!shaded(&fill, cells(&s, active)), "{active} stays clear: {fill:?}");
|
|
for other in ["B2", "B5", "C3", "C4", "D2", "D5"].into_iter().filter(|c| *c != active) {
|
|
assert!(shaded(&fill, cells(&s, other)), "{other} is shaded with {active} active");
|
|
}
|
|
assert!((area(&fill) - (cells(&s, "B2:D5").area() - cells(&s, active).area())).abs() < 0.5, "the rest of the range, once");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_single_selected_cell_has_no_fill() {
|
|
let mut s = session();
|
|
s.execute("selection.set", json!({"cell": "C3"})).unwrap();
|
|
assert!(fill(&s).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn other_areas_are_shaded_whole_and_a_lone_active_cell_not_at_all() {
|
|
let mut s = session();
|
|
s.execute("selection.set", json!({"range": "A1:A3,C2:D3,F6", "active": "F6"})).unwrap();
|
|
assert_eq!(s.active().unwrap().selection.ranges.len(), 3);
|
|
let fill = fill(&s);
|
|
assert!(!shaded(&fill, cells(&s, "F6")));
|
|
assert!(shaded(&fill, cells(&s, "A2")) && shaded(&fill, cells(&s, "D3")));
|
|
assert!((area(&fill) - (cells(&s, "A1:A3").area() + cells(&s, "C2:D3").area())).abs() < 0.5, "{fill:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_merged_active_cell_is_left_clear_as_a_whole() {
|
|
let mut s = session();
|
|
s.execute("selection.set", json!({"range": "B2:C3"})).unwrap();
|
|
s.execute("home.mergeCells", json!({})).unwrap();
|
|
s.execute("selection.set", json!({"range": "B2:E6", "active": "B2"})).unwrap();
|
|
let fill = fill(&s);
|
|
assert!(!shaded(&fill, cells(&s, "B2:C3")), "the merged cell, not just its first cell: {fill:?}");
|
|
assert!(shaded(&fill, cells(&s, "D2")) && shaded(&fill, cells(&s, "B4")) && shaded(&fill, cells(&s, "E6")));
|
|
assert!((area(&fill) - (cells(&s, "B2:E6").area() - cells(&s, "B2:C3").area())).abs() < 0.5);
|
|
}
|
|
|
|
#[test]
|
|
fn the_current_area_is_shaded_where_its_gliding_outline_is() {
|
|
let mut s = session();
|
|
s.execute("selection.set", json!({"range": "A1:A3,C2:D3", "active": "C2"})).unwrap();
|
|
let doc = s.active().unwrap();
|
|
let sheet = doc.wb.active().unwrap();
|
|
// Mid-glide the outline of the current area (C2:D3) is somewhere else on screen.
|
|
let outline = Rect::from_min_max(egui::pos2(600.0, 400.0), egui::pos2(700.0, 500.0));
|
|
let fill = selection_fill(&geo(&s), sheet, &doc.selection, outline);
|
|
assert!(fill.contains(&outline), "the current area follows its outline: {fill:?}");
|
|
assert!(shaded(&fill, cells(&s, "A2")), "the other areas stay on their cells");
|
|
assert!(!shaded(&fill, cells(&s, "D3")), "nothing is left behind on the current area's cells");
|
|
}
|