From 1ee1f26c0d3f6f12969d3b4edf985152271b4154 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Tue, 22 Sep 2026 20:50:48 -0700 Subject: [PATCH] WIP df2w: an open empty panel is Some(vec![]), closed is None Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze --- crates/improvise-wasm-client/src/render.rs | 110 ++++++++++++--------- web/app.js | 8 +- 2 files changed, 65 insertions(+), 53 deletions(-) diff --git a/crates/improvise-wasm-client/src/render.rs b/crates/improvise-wasm-client/src/render.rs index b73785d0..9d571733 100644 --- a/crates/improvise-wasm-client/src/render.rs +++ b/crates/improvise-wasm-client/src/render.rs @@ -42,11 +42,11 @@ pub struct RenderModel { pub categories: Vec, /// Whether the panel is open, so the page does not decide. pub category_panel_open: bool, - /// The view panel's rows, **empty unless it is open** — the same rule the - /// help overlay follows, so the page asks no question about the mode. - pub views: Vec, - /// The formula panel's rows, empty unless it is open. - pub formulas: Vec, + /// The view panel's rows, **`None` when it is closed** and possibly empty + /// when open: an absence is not an empty list (improvise-df2w). + pub views: Option>, + /// The formula panel's rows, on the same terms as `views`. + pub formulas: Option>, /// The tile bar, drawn on every frame as the TUI draws it. Which tile is /// `selected` is the mode's answer, not the page's. pub tiles: Vec, @@ -344,41 +344,39 @@ fn page_axes(state: &ClientState) -> Vec { .collect() } -/// The view panel's rows while it is open. The axis line is +/// The view panel's rows, or `None` while it is closed. The axis line is /// `viewmodel::view_axis_summary`, which the TUI's own panel calls too. -fn view_rows(state: &ClientState) -> Vec { - if !state.view.view_panel_open { - return Vec::new(); - } - state - .views - .views - .iter() - .enumerate() - .map(|(i, summary)| RenderViewRow { - name: summary.name.clone(), - axes: improvise_protocol::viewmodel::view_axis_summary(summary), - active: summary.name == state.view.active_view, - selected: i == state.view.view_panel_cursor, - }) - .collect() +fn view_rows(state: &ClientState) -> Option> { + state.view.view_panel_open.then(|| { + state + .views + .views + .iter() + .enumerate() + .map(|(i, summary)| RenderViewRow { + name: summary.name.clone(), + axes: improvise_protocol::viewmodel::view_axis_summary(summary), + active: summary.name == state.view.active_view, + selected: i == state.view.view_panel_cursor, + }) + .collect() + }) } -/// The formula panel's rows while it is open. -fn formula_rows(state: &ClientState) -> Vec { - if !state.view.formula_panel_open { - return Vec::new(); - } - state - .formulas - .formulas - .iter() - .enumerate() - .map(|(i, text)| RenderFormulaRow { - text: text.clone(), - selected: i == state.view.formula_cursor, - }) - .collect() +/// The formula panel's rows, or `None` while it is closed. +fn formula_rows(state: &ClientState) -> Option> { + state.view.formula_panel_open.then(|| { + state + .formulas + .formulas + .iter() + .enumerate() + .map(|(i, text)| RenderFormulaRow { + text: text.clone(), + selected: i == state.view.formula_cursor, + }) + .collect() + }) } /// The tile bar, always. Only the *selection* is conditional, exactly as @@ -800,34 +798,48 @@ mod tests { client } - /// An empty list is what says a panel is closed — the same rule the help - /// overlay follows, so the page asks no question about the mode. + /// `None` is what says a panel is closed, so the page asks no question + /// about the mode. #[test] fn a_closed_panel_projects_no_rows_and_an_open_one_does() { let mut client = client_with_panels(); let closed = client.model(); - assert!( - closed.views.is_empty(), - "the cache is full but the panel is shut" - ); - assert!(closed.formulas.is_empty()); + assert!(closed.views.is_none(), "the cache is full but the panel is shut"); + assert!(closed.formulas.is_none()); client.state_mut().view.view_panel_open = true; client.state_mut().view.formula_panel_open = true; client.state_mut().view.view_panel_cursor = 1; client.state_mut().view.active_view = "Default".into(); let open = client.model(); - assert_eq!(open.views.len(), 2); + let (views, formulas) = (open.views.unwrap(), open.formulas.unwrap()); + assert_eq!(views.len(), 2); assert_eq!( - open.views[0].axes, "R:Region", + views[0].axes, "R:Region", "the axis line is the shared derivation, and _Dim is not part of it" ); assert!( - open.views[0].active, + views[0].active, "active is ViewState's, not the cache's" ); - assert!(open.views[1].selected, "and selected is the panel cursor's"); - assert_eq!(open.formulas[0].text, "Profit = Revenue - Cost"); + assert!(views[1].selected, "and selected is the panel cursor's"); + assert_eq!(formulas[0].text, "Profit = Revenue - Cost"); + } + + /// Open with nothing in it is not closed: the page must be able to tell + /// them apart from the model alone (improvise-df2w). + #[test] + fn an_open_empty_panel_is_not_a_closed_one() { + let mut client = init(); + let closed = client.model(); + client.state_mut().view.formula_panel_open = true; + let formulas_open = client.model(); + assert_ne!(formulas_open, closed, "an open, empty formula panel"); + + client.state_mut().view.formula_panel_open = false; + client.state_mut().view.view_panel_open = true; + let views_open = client.model(); + assert_ne!(views_open, closed, "an open, empty view panel"); } /// The tile bar is drawn always; only the *selection* waits for the mode, diff --git a/web/app.js b/web/app.js index 2aeed8d0..b3c0149c 100644 --- a/web/app.js +++ b/web/app.js @@ -594,12 +594,12 @@ function edit(td, row, col, seed) { }); } -/** A side panel. An empty row list is what says it is closed — the page asks - * no question about the mode, exactly as `paintHelp` does not. */ +/** A side panel. `null` is what says it is closed, and `[]` is open and empty: + * two facts, never inferred from a count (improvise-df2w). */ function paintPanel(host, rows, build) { - host.hidden = rows.length === 0; + host.hidden = rows === null; const list = document.createElement("ul"); - rows.forEach((row, index) => { + (rows ?? []).forEach((row, index) => { const li = build(row); li.dataset.index = String(index); list.appendChild(li);