perf(app): rebuild the layout and remeasure widths only when their inputs moved (improvise-felo.13)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
committed by
Edward Langley
parent
2f05f6b62d
commit
b42aae2e08
@@ -434,6 +434,14 @@ impl Default for ModelState {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a layout was built from other than the model.
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
struct LayoutBasis {
|
||||
view: String,
|
||||
/// The address of the frozen records, never dereferenced.
|
||||
drill: Option<usize>,
|
||||
}
|
||||
|
||||
/// What the keypress path measures of a built layout.
|
||||
pub(crate) struct GridGeometry {
|
||||
pub col_widths: Vec<u16>,
|
||||
@@ -466,6 +474,14 @@ pub struct App {
|
||||
layout_builds: usize,
|
||||
/// How many times the grid's column widths were measured, for the same reason.
|
||||
width_passes: std::cell::Cell<usize>,
|
||||
/// The widths of the layout above, measured on first use and dropped with it.
|
||||
geometry: std::cell::OnceCell<GridGeometry>,
|
||||
/// What the layout above was built from besides the model: the view it drew
|
||||
/// and the drill snapshot it froze. `layout_current` compares it.
|
||||
layout_basis: LayoutBasis,
|
||||
/// The model changed since the layout was built. Set by the one writer that
|
||||
/// does not rebuild at once.
|
||||
layout_stale: bool,
|
||||
/// **Transient**: set during `apply_effects` to skip the rest of the batch,
|
||||
/// reset at the start of every call. Reach it through `AbortChain`.
|
||||
pub abort_effects: bool,
|
||||
@@ -566,6 +582,9 @@ impl App {
|
||||
layout,
|
||||
layout_builds: 0,
|
||||
width_passes: std::cell::Cell::new(0),
|
||||
geometry: std::cell::OnceCell::new(),
|
||||
layout_basis: LayoutBasis::default(),
|
||||
layout_stale: true,
|
||||
abort_effects: false,
|
||||
batch_aborted: false,
|
||||
batch_refused: false,
|
||||
@@ -623,6 +642,7 @@ impl App {
|
||||
self.view_state.view_back_stack.clear();
|
||||
self.view_state.view_forward_stack.clear();
|
||||
self.model_state.workbook = workbook;
|
||||
self.layout_stale = true;
|
||||
// Every client's mirror is scoped to the outgoing document too, and an
|
||||
// effect cannot reach the session list. The generation is how it hears.
|
||||
self.document_generation += 1;
|
||||
@@ -914,17 +934,39 @@ impl App {
|
||||
self.layout =
|
||||
GridLayout::with_frozen_records(&self.model_state.workbook.model, view, frozen);
|
||||
self.layout_builds += 1;
|
||||
self.layout_stale = false;
|
||||
self.layout_basis = self.current_layout_basis();
|
||||
self.geometry = std::cell::OnceCell::new();
|
||||
}
|
||||
|
||||
/// Rebuild the layout only if something it is a function of has moved: the
|
||||
/// model (`layout_stale`), the view it draws or its drill snapshot. A cursor
|
||||
/// key moves none of them (improvise-felo.13, `context/code/command.md`).
|
||||
fn refresh_layout(&mut self) {
|
||||
if self.layout_stale || self.layout_basis != self.current_layout_basis() {
|
||||
self.rebuild_layout();
|
||||
}
|
||||
}
|
||||
|
||||
fn current_layout_basis(&self) -> LayoutBasis {
|
||||
LayoutBasis {
|
||||
view: self.view_name().to_string(),
|
||||
drill: self.drill_state.as_ref().map(|d| Rc::as_ptr(&d.records) as usize),
|
||||
}
|
||||
}
|
||||
|
||||
/// The measurements a keypress needs of the grid: column widths and the row
|
||||
/// header. Counted, so a test can see whether a keypress paid for them.
|
||||
pub(crate) fn grid_geometry(&self) -> GridGeometry {
|
||||
self.width_passes.set(self.width_passes.get() + 1);
|
||||
let model = &self.model_state.workbook.model;
|
||||
GridGeometry {
|
||||
col_widths: compute_col_widths(model, &self.layout, &FormatPlan::of(model, self.view())),
|
||||
row_header_width: compute_row_header_width(&self.layout),
|
||||
}
|
||||
pub(crate) fn grid_geometry(&self) -> &GridGeometry {
|
||||
self.geometry.get_or_init(|| {
|
||||
self.width_passes.set(self.width_passes.get() + 1);
|
||||
let model = &self.model_state.workbook.model;
|
||||
let plan = FormatPlan::of(model, self.view());
|
||||
GridGeometry {
|
||||
col_widths: compute_col_widths(model, &self.layout, &plan),
|
||||
row_header_width: compute_row_header_width(&self.layout),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Editable text for the cell under the cursor: staged drill edit, else the
|
||||
@@ -1139,7 +1181,8 @@ impl App {
|
||||
// is read only behind the epoch gate — `context/code/command.md`.
|
||||
let reshaped = self.last_reshaped_views.clone();
|
||||
self.last_reframed_views.extend(reshaped);
|
||||
self.rebuild_layout();
|
||||
self.layout_stale |= touches_model;
|
||||
self.refresh_layout();
|
||||
// After the rebuild, or a batch that crosses into or out of records mode
|
||||
// settles against the grid it started on — `context/code/command.md`.
|
||||
self.settle_normal_mode();
|
||||
@@ -1219,7 +1262,7 @@ impl App {
|
||||
/// Resolve one keypress through the keymap and apply what it produces. The
|
||||
/// projection half is factored out; `ui::reduce` is the named door (improvise-gxi).
|
||||
pub fn dispatch_key(&mut self, key: KeyEvent) -> Dirty {
|
||||
self.rebuild_layout();
|
||||
self.refresh_layout();
|
||||
|
||||
// An armed prefix key takes priority. Resolve it back to its
|
||||
// sub-keymap through the keymap set (improvise-jc8).
|
||||
@@ -1943,7 +1986,7 @@ mod tests {
|
||||
let mut app = two_col_model();
|
||||
let down = || KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
|
||||
app.dispatch_key(down());
|
||||
let narrow = app.grid_geometry().col_widths;
|
||||
let narrow = app.grid_geometry().col_widths.clone();
|
||||
let (builds, widths) = (app.layout_builds, app.width_passes.get());
|
||||
let key = CellKey::new(vec![("Row".into(), "A".into()), ("Col".into(), "X".into())]);
|
||||
let long = CellValue::Text("a value far wider than a column".into());
|
||||
@@ -1957,6 +2000,19 @@ mod tests {
|
||||
assert_ne!(fresh, narrow, "the edit widened a column, or this proves nothing");
|
||||
}
|
||||
|
||||
/// improvise-felo.13: a `ViewOnly` effect that changes which view is drawn
|
||||
/// must still rebuild, so skipping the rebuild is not keyed on `EffectKind` alone.
|
||||
#[test]
|
||||
fn switching_view_rebuilds_though_the_effect_is_view_only() {
|
||||
let mut app = two_col_model();
|
||||
app.model_state.workbook.create_view("Second");
|
||||
app.rebuild_layout();
|
||||
let builds = app.layout_builds;
|
||||
app.apply_effects(vec![Box::new(crate::ui::effect::SwitchView("Second".into()))]);
|
||||
assert_eq!(app.view_state.active_view.as_str(), "Second");
|
||||
assert!(app.layout_builds > builds, "the layout still draws Default");
|
||||
}
|
||||
|
||||
/// improvise-htl6. Evidence: `context/test-evidence/command.md`.
|
||||
#[test]
|
||||
fn the_view_history_walks_both_ways_from_the_keyboard() {
|
||||
|
||||
Reference in New Issue
Block a user