787e354b9b
The `build` job fails on main and on every branch merged from it: doc-site: context/code/pr-review.md is neither published nor a named omission NOT a merge loss -- scripts/build-doc-site.mjs in this branch is byte-identical to main's, and neither lists the file. PR #96 added context/code/pr-review.md without a manifest line, which is exactly the gate improvise-res9 added: a doc added without a line publishes nothing and says nothing, so the generator refuses. Omitted rather than published, and the line is cloud-agents.md's rather than tooling.md's. tooling.md explains checkers a reader of the code will meet; pr-review.md is about THIS REPOSITORY'S PROCESS -- where a Codex review appears, how to tell it from our own agents' comments, why absence is not approval. Nobody visiting a website is about to read a PR thread. Both homes updated, because an omission is restated beside the manifest AND in doc-site.md section 2, and the prose there counted them ("Neither is referenced...") so it went stale at three. This strays into scripts/, which is Wave N's ground. Flagged in the hand-back; it is a one-line data entry either way if the orchestrator would rather publish it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
94 lines
24 KiB
HTML
94 lines
24 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>The TUI — improvise docs</title>
|
||
<link rel="stylesheet" href="../style.css" />
|
||
</head>
|
||
<body>
|
||
<div class="shell">
|
||
<nav class="side"><a class="brand" href="../index.html">improvise docs</a><input type="checkbox" id="navtoggle" class="navtoggle" /><label class="navsummary" for="navtoggle">Pages</label><div class="navlinks"><ul><li><a href="../../index.html">← Project home</a></li><li><a href="../../app/">Browser demo</a></li><li><a href="https://github.com/fiddlerwoaroof/improvise">Source on GitHub</a></li></ul><h3>Start here</h3><ul><li><a href="../design-notes.html">Design notes</a></li><li><a href="../design-principles.html">Design principles</a></li><li><a href="../repo-map.html">Repository map</a></li></ul><h3>Decision records</h3><ul><li><a href="../adr/001-concurrent-edits.html">ADR 001 — Concurrent edits</a></li><li><a href="../adr/002-naming-a-cell.html">ADR 002 — Naming a cell</a></li><li><a href="../adr/003-live-backed-models.html">ADR 003 — Live-backed models</a></li></ul><h3>The arguments, by area</h3><ul><li><a href="../code/core.html">improvise-core</a></li><li><a href="../code/formula-and-io.html">improvise-formula and improvise-io</a></li><li><a href="../code/protocol.html">improvise-protocol</a></li><li><a href="../code/command.html">improvise-command</a></li><li><a href="../code/clients.html">The clients</a></li><li><a href="../code/tui.html" class="here">The TUI</a></li><li><a href="../code/gp.html">gp-tree and improvise-gp</a></li><li><a href="../code/kb-tool.html">The kb tool</a></li><li><a href="../code/tooling.html">The repository's own checkers</a></li></ul><h3>Test evidence</h3><ul><li><a href="../test-evidence/core.html">improvise-core</a></li><li><a href="../test-evidence/formula-and-io.html">improvise-formula and improvise-io</a></li><li><a href="../test-evidence/protocol.html">improvise-protocol</a></li><li><a href="../test-evidence/command.html">improvise-command</a></li><li><a href="../test-evidence/clients.html">The clients</a></li><li><a href="../test-evidence/tui.html">The TUI</a></li><li><a href="../test-evidence/gp.html">gp-tree and improvise-gp</a></li><li><a href="../test-evidence/tooling.html">The repository's own gates</a></li></ul><h3>About this site</h3><ul><li><a href="../doc-site.html">About this site</a></li></ul><h3>Reference</h3><ul><li><a href="../tickets.html">Ticket index</a></li></ul></div></nav>
|
||
<main>
|
||
<p class="src">Source: <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/context/code/tui.md">context/code/tui.md</a></p><details class="toc" open><summary>On this page</summary><ul><li class="l1"><a href="#the-terminal-front-end-src">The terminal front end (src/)</a></li><li class="l2"><a href="#src-ui-grid-rs-gridwidget">src/ui/grid.rs — GridWidget</a></li><li class="l3"><a href="#inside-render-grid">Inside render_grid</a></li><li class="l2"><a href="#src-draw-rs">src/draw.rs</a></li><li class="l3"><a href="#the-key-event-boundary">The key-event boundary</a></li><li class="l3"><a href="#semantic-to-terminal-colour-and-the-one-that-is-wrong">Semantic-to-terminal colour, and the one that is wrong</a></li><li class="l3"><a href="#the-minibuffer-shares-the-bottom-row-with-the-status-line">The minibuffer shares the bottom row with the status line</a></li><li class="l3"><a href="#the-caches-are-built-here-per-frame">The caches are built here, per frame</a></li><li class="l2"><a href="#src-ui-import-wizard-ui-rs-importwizardwidget">src/ui/import_wizard_ui.rs — ImportWizardWidget</a></li><li class="l3"><a href="#set-bottom-line-one-writer-for-the-bottom-row">set_bottom_line — one writer for the bottom row</a></li><li class="l3"><a href="#truncate-prefix-bytes-deliberately">truncate / prefix — bytes, deliberately</a></li><li class="l2"><a href="#src-ui-help-rs">src/ui/help.rs</a></li><li class="l2"><a href="#the-panel-widgets">The panel widgets</a></li></ul></details><h1 id="the-terminal-front-end-src">The terminal front end (<a href="https://github.com/fiddlerwoaroof/improvise/tree/main/src"><code>src/</code></a>)</h1>
|
||
<p>The root crate is the ratatui binary: <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/src/draw.rs"><code>src/draw.rs</code></a> (event loop and frame composition), <code>src/ui/*.rs</code> (the widgets), <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/src/main.rs"><code>src/main.rs</code></a> (clap). It is one of two front ends, and design-principles §5 is the rule it is judged by — it paints and wires, and every decision with a silently wrong answer is made in <code>improvise-protocol</code> so the browser cannot answer it differently.</p>
|
||
<p>Test narratives live in <a href="../test-evidence/tui.html"><code>context/test-evidence/tui.md</code></a>. Facts about <code>GridCache</code>, <code>AxisFrame</code>, <code>Projection</code> and the <code>Command</code>/<code>Effect</code> layer belong to <a href="../code/protocol.html"><code>context/code/protocol.md</code></a> and <a href="../code/command.html"><code>context/code/command.md</code></a>; this file cites them and does not restate them.</p>
|
||
<hr />
|
||
<h2 id="src-ui-grid-rs-gridwidget"><a class="anchor" href="#src-ui-grid-rs-gridwidget">#</a><a href="https://github.com/fiddlerwoaroof/improvise/blob/main/src/ui/grid.rs"><code>src/ui/grid.rs</code></a> — <code>GridWidget</code></h2>
|
||
<p>The grid is drawn from a <code>GridCache</code> (<a class="tid" href="../tickets.html#improvise-n10" title="Migrate GridWidget to consume RenderCache + ViewState (not &App)">improvise-n10</a>).</p>
|
||
<p><strong>No <code>&Model</code>.</strong> Every value the grid paints — each cell's text, whether it is an aggregate, the column widths, the page bar and the totals strip — arrives already decided, on the cache. A widget that cannot reach the model cannot quietly answer a question differently from the way the browser client answers it, and that divergence is the whole thing projecting exists to prevent.</p>
|
||
<p><strong>The frame comes off the cache too</strong> (<a class="tid" href="../tickets.html#improvise-anr9" title="Project the five grid-frame facts: unblocks H, z, gz and fixes Food/Alice at once">improvise-anr9</a>). <code>GridCache</code> carries an <code>AxisFrame</code> per axis — the header levels, the label <em>tuples</em> and the groups with their collapse state — so the multi-level headers, the repeat suppression, the group spans and the pieces below are all read rather than derived, and a browser painting the same frame paints the same thing. It used to be derived here from <code>GridLayout</code> + <code>View::is_group_collapsed</code> while the protocol carried <code>GridLayout::row_label</code>'s slash-joined form — the rendering <code>two_row_categories_produce_cross_product_labels</code> asserts must not appear — so the browser drew <code>Food/Alice</code>.</p>
|
||
<p>Two things are still not on the cache and are passed separately, each a different kind of absence:</p>
|
||
<ul>
|
||
<li><strong><code>layout</code> resolves a records-mode column</strong> (<code>record_col</code>), and only that. It keys <code>drill_state</code>'s pending edits, so it is session-local by design and correct to keep off the wire (gotcha 1).</li>
|
||
<li><strong><code>drill_state</code> is session-local residue</strong> (gotcha 30). Pending drill edits belong to whoever staged them and are deliberately not projected, so they shadow the cache rather than being on it.</li>
|
||
</ul>
|
||
<p>The cache is expected to cover the <strong>whole</strong> grid — offsets zero, every row and every column — so a position in it is an absolute position and the widget's scrolling arithmetic is unchanged.</p>
|
||
<h3 id="inside-render-grid"><a class="anchor" href="#inside-render-grid">#</a>Inside <code>render_grid</code></h3>
|
||
<p><code>GridWidget::cursor</code> is this client's cursor in the active view (<a class="tid" href="../tickets.html#improvise-8uz" title="Cursor and scroll are per-client state stored in the shared View">improvise-8uz</a>), passed in rather than read off <code>View</code>, which no longer carries per-client state.</p>
|
||
<p>Everything about the frame is the cache's answer (<a class="tid" href="../tickets.html#improvise-anr9" title="Project the five grid-frame facts: unblocks H, z, gz and fixes Food/Alice at once">improvise-anr9</a>): how many header levels, what each level's label is, which group a position sits in and whether that group is collapsed. Nothing in <code>render_grid</code> reads <code>GridLayout</code> or <code>View</code>, so a browser folding the same <code>AxisFrame</code> draws the same header.</p>
|
||
<p><strong>The bulk forms are built once per frame, and that is not a micro-optimisation.</strong> <code>header_labels(offset)</code>, <code>groups()</code> and <code>shares_ancestry(sel_col)</code> each make one pass. The per-position accessors (<code>item</code>, <code>label</code>, <code>shows_level</code>, <code>group_of</code>, <code>same_prefix</code>) scan the frame from the start, so asking them inside the draw loops is quadratic in the grid and, at a deep scroll offset, grows with the offset. <code>col_lines</code> transposes <code>header_labels</code> to level-major because the column header is drawn one line per level while <code>header_labels</code> answers per column.</p>
|
||
<p><strong>The underline on the column header</strong> marks columns sharing <code>sel_col</code>'s ancestor group through each level: at the bottom level that is only <code>sel_col</code>, at higher levels it spans all the sub-columns.</p>
|
||
<p><strong>Pending drill edits shadow the projected value.</strong> They are this session's uncommitted typing and by construction are not on the cache (gotcha 30).</p>
|
||
<p><strong>Aggregate cells are drawn in italic</strong> to signal "drill to edit", and the fact is <em>not</em> re-derived here: the projection already named what each cell is, and <code>CellId::Aggregate</code> is exactly that fact (ADR 002), decided once by the side that holds the model. The widget used to walk <code>none_cats</code> × <code>kind.is_regular()</code> — a second hand-rolled copy of <code>project::cell_id</code>.</p>
|
||
<p><strong>Whether there is a total row at all is the cache's answer.</strong> An empty totals strip means "no totals" (records mode, or an empty grid), which is why the widget does not read <code>is_records_mode</code> a second time.</p>
|
||
<p><strong>The collapse glyph</strong> (<code>indicator</code>) is the one thing that is legitimately local: the <em>fact</em> — whether the group is collapsed — is on the frame; only its spelling is the terminal's (design-principles §5).</p>
|
||
<hr />
|
||
<h2 id="src-draw-rs"><a class="anchor" href="#src-draw-rs">#</a><a href="https://github.com/fiddlerwoaroof/improvise/blob/main/src/draw.rs"><code>src/draw.rs</code></a></h2>
|
||
<h3 id="the-key-event-boundary"><a class="anchor" href="#the-key-event-boundary">#</a>The key-event boundary</h3>
|
||
<p><code>convert_key_event</code> / <code>convert_key_code</code> / <code>convert_modifiers</code> are the <strong>only</strong> place crossterm's key types and the crate's own <code>command::key</code> types may both appear. Three rules:</p>
|
||
<ul>
|
||
<li>The event loop converts every incoming <code>Event::Key</code> regardless of <code>KeyEventKind</code> (Press/Repeat/Release), matching the pre-conversion behaviour, which never inspected the kind.</li>
|
||
<li>Codes the command layer does not model (media keys, <code>Insert</code>, …) become <code>KeyCode::Null</code>, which no keymap binds.</li>
|
||
<li><code>SHIFT</code>/<code>CONTROL</code>/<code>ALT</code> survive; every other modifier bit (<code>SUPER</code>, <code>HYPER</code>, <code>META</code>) is dropped.</li>
|
||
</ul>
|
||
<h3 id="semantic-to-terminal-colour-and-the-one-that-is-wrong"><a class="anchor" href="#semantic-to-terminal-colour-and-the-one-that-is-wrong">#</a>Semantic-to-terminal colour, and the one that is wrong</h3>
|
||
<p>This file holds the mapping design-principles §5 names as the shape everything else should imitate: <strong>the decision lives in the shared layer as a semantic enum, and only the last translation is local.</strong></p>
|
||
<ul>
|
||
<li><code>axis_fg(AxisColor) -> Color</code> — <code>Axis::color</code> is the single place each axis's accent is decided; this is the single place axis colouring is <em>rendered</em> (<a class="tid" href="../tickets.html#improvise-rml" title="Consolidate Axis display into a method on Axis">improvise-rml</a>, gotcha 26). Before it, the tile bar and the category panel each carried their own copy of the colour table with nothing keeping them in agreement.</li>
|
||
<li><code>minibuf_fg(MinibufColor) -> Color</code> — the same shape for the minibuffer.</li>
|
||
<li><code>src/ui/import_wizard_ui.rs::field_kind_fg(WizardFieldKind) -> Color</code> — the same shape again. <code>improvise-protocol</code> must not name a ratatui <code>Color</code>.</li>
|
||
</ul>
|
||
<p><code>mode_style(&AppMode) -> Style</code> stays here for the same reason (<code>Style</code> and <code>Color</code> are ratatui types and <code>improvise-command</code>, where <code>AppMode</code> lives, carries no rendering-framework dependency), and since <code>improvise-zx8t</code> it is <strong>exhaustive</strong>: the fourteen variants that take the default chrome are spelled out rather than swept up by a <code>_ =></code> wildcard, so adding an <code>AppMode</code> fails the build here until someone chooses its colour.</p>
|
||
<p>It is still not quite the same shape as <code>axis_fg</code> and its two siblings, and the difference is deliberate rather than pending. Those three map a <em>semantic enum</em> the shared layer owns; <code>mode_style</code> matches <code>AppMode</code> itself, because there is no <code>ModeColor</code>. <code>improvise-zx8t</code> offered making one, and it was declined here for two reasons: the colour is the only thing at stake, and the ticket says to decide it together with <code>improvise-k099</code>, which asks the same question of <code>hint_text</code>/<code>label</code> and is not in this unit. Exhaustiveness is what the repo map's "the compiler names every one you must fill in" promise needed; a semantic enum is a separate, larger answer and is still open.</p>
|
||
<p><strong>The guard is the compiler, not a test.</strong> Nothing asserts the colours, and a test enumerating the variants would not fail when a fifteenth is added — only the missing match arm does. Verified by deleting the <code>AppMode::Help</code> arm: <code>error[E0004]: non-exhaustive patterns: </code>&AppMode::Help<code> not covered</code>.</p>
|
||
<h3 id="the-minibuffer-shares-the-bottom-row-with-the-status-line"><a class="anchor" href="#the-minibuffer-shares-the-bottom-row-with-the-status-line">#</a>The minibuffer shares the bottom row with the status line</h3>
|
||
<p><code>draw_bottom_bar</code> used to render the minibuffer <em>instead of</em> <code>draw_status</code>, so a status message written while any text-entry mode was open was never drawn (<code>improvise-mai9</code>, and the stranding gotcha 21 describes). <code>minibuffer_line</code> composes both: the prompt and buffer on the left, any pending <code>status_msg</code> right-aligned. The alternatives and why they lost are in <a href="../test-evidence/tui.html"><code>context/test-evidence/tui.md</code></a>.</p>
|
||
<h3 id="the-caches-are-built-here-per-frame"><a class="anchor" href="#the-caches-are-built-here-per-frame">#</a>The caches are built here, per frame</h3>
|
||
<p><code>project::formulas_of</code>, <code>category_tree_of</code>, <code>view_list_of</code>, <code>tile_bar_of</code>, <code>grid_of</code> and <code>wizard_of</code> are all called from <code>draw</code>/<code>draw_content</code> against the authoritative <code>App</code>. This is the <strong>temporary half of <a class="tid" href="../tickets.html#improvise-35e" title="Temporary adapter: RenderCache::from_app bridge for incremental widget migration">improvise-35e</a></strong>: when the TUI becomes a client it will <em>hold</em> each cache and fold the matching <code>Projection</code> into it, and none of the widgets change either way.</p>
|
||
<p>Two consequences today:</p>
|
||
<ul>
|
||
<li>The grid's viewport covers the <strong>whole</strong> grid, so a cache position is an absolute position (gotcha 44). Only the viewport narrows when the TUI becomes a client.</li>
|
||
<li>The wizard is <strong>built here rather than sent</strong>: it is session-local and has no <code>Projection</code> (<a class="tid" href="../tickets.html#improvise-764" title="Migrate import wizard widget and state to consume RenderCache">improvise-764</a>). It still goes through <code>project</code>, so the widget sees the same shape a browser wizard would.</li>
|
||
</ul>
|
||
<p><code>draw</code> renders overlays last so they appear on top. The which-key popup's armed prefix is stored as the <em>key that opened it</em> and resolved back to a sub-keymap by the keymap set (<a class="tid" href="../tickets.html#improvise-jc8" title="Evict host-only state from ViewState: flip the transient-keymap Arc">improvise-jc8</a>, gotcha 13).</p>
|
||
<hr />
|
||
<h2 id="src-ui-import-wizard-ui-rs-importwizardwidget"><a class="anchor" href="#src-ui-import-wizard-ui-rs-importwizardwidget">#</a><a href="https://github.com/fiddlerwoaroof/improvise/blob/main/src/ui/import_wizard_ui.rs"><code>src/ui/import_wizard_ui.rs</code></a> — <code>ImportWizardWidget</code></h2>
|
||
<p>Drawn from a <code>WizardCache</code> (<a class="tid" href="../tickets.html#improvise-764" title="Migrate import wizard widget and state to consume RenderCache">improvise-764</a>).</p>
|
||
<p><strong>No <code>ImportWizard</code>, no <code>ImportPipeline</code>, no <code>Workbook</code>.</strong> The widget used to hold the live wizard and read through it into <code>improvise-io</code> types and into the model — it called <code>regular_category_names()</code> and <code>measure_item_names()</code> on the built <code>Workbook</code> to write its one-line confirmation summary. Everything it draws now arrives already decided, from <code>project::wizard_of</code>.</p>
|
||
<p>The cache is <strong>not</strong> a <code>Projection</code>, and that is the ticket's answer to where wizard state lives: a half-open import dialogue belongs to the session that opened it, not to every connected client. <code>WizardCache</code>'s own documentation carries the argument.</p>
|
||
<p>Smaller facts the drawing depends on: the root array path is already spelled <code>(root)</code> by the projection, so both ends say it the same way; the date components arrive in the order the wizard offers them, already paired with whether they are on, so the cursor index counts exactly those rows; and on <code>Done</code> the workbook is built but not yet installed, so the step shows what the user is about to accept, warnings included (<a class="tid" href="../tickets.html#improvise-907" title="Import wizard step machine: validate early, allow going back, make the terminal step real">improvise-907</a>).</p>
|
||
<h3 id="set-bottom-line-one-writer-for-the-bottom-row"><a class="anchor" href="#set-bottom-line-one-writer-for-the-bottom-row">#</a><code>set_bottom_line</code> — one writer for the bottom row</h3>
|
||
<p>That row has two writers — each step's key hint, and the wizard message that overrides it while present — and they write in that order to the same <code>y</code>. <code>Buffer::set_string</code> writes exactly as many cells as the string has and leaves the rest alone, so a message shorter than the hint it replaced left the hint's tail sitting beside it: <code>something went wrongcel</code>, the <code>cel</code> from <code>Esc: cancel</code> (<a class="tid" href="../tickets.html#improvise-q0vb" title="Pad the import wizard's hint line to the panel width">improvise-q0vb</a>).</p>
|
||
<p><strong>Padding here rather than at each site is the point.</strong> The bug is not that one of the five writers forgot; it is that the row had five writers and no one of them owned clearing it.</p>
|
||
<p><strong>The padding is measured in terminal cells, not characters.</strong> <code>Buffer</code> lays a string out by display width, so a char count over-pads a message carrying a wide character — enough to run past the panel and overwrite the border — and under-pads a combining mark, which is how a sliver of the hint would be left behind. <a href="https://github.com/fiddlerwoaroof/improvise/blob/main/src/ui/grid.rs"><code>src/ui/grid.rs</code></a> measures the same way.</p>
|
||
<h3 id="truncate-prefix-bytes-deliberately"><a class="anchor" href="#truncate-prefix-bytes-deliberately">#</a><code>truncate</code> / <code>prefix</code> — bytes, deliberately</h3>
|
||
<p><code>truncate</code> cuts by <strong>byte</strong> length. <code>prefix</code> returns the longest prefix at most <code>max</code> bytes that ends on a char boundary, because every string <code>truncate</code> cuts can carry a field name, a formula or a message the user typed — and a multi-byte character straddling the cut used to panic the whole TUI on a slice at a non-boundary index. Walking down to the nearest boundary is the smallest change that cannot: for ASCII, which is what every fixture in this file holds, <code>max</code> is already a boundary and the result is byte-identical, so the characterization tests keep saying what they said.</p>
|
||
<p>It is bytes and not columns <em>deliberately</em>. Byte length is the measure <code>truncate</code> has always used, and changing it to display width would move where every line wraps — a different question from not crashing, and one that belongs with the hint-line padding it travels with. Bytes are never fewer than cells, so <code>truncate</code> cutting by bytes and <code>set_bottom_line</code> padding by cells cannot together overrun.</p>
|
||
<p><strong>The blind spot, stated because the fixtures cannot state it</strong>: the eight exact-frame characterization fixtures in this file are entirely ASCII. They are therefore blind to every question about display width — a change to either measure is invisible to them, and the evidence for those has to be built directly (see <code>set_bottom_line_clears_every_cell_it_covers_and_no_more</code>).</p>
|
||
<hr />
|
||
<h2 id="src-ui-help-rs"><a class="anchor" href="#src-ui-help-rs">#</a><a href="https://github.com/fiddlerwoaroof/improvise/blob/main/src/ui/help.rs"><code>src/ui/help.rs</code></a></h2>
|
||
<p>~600 lines of mostly-static content: five pages of styled <code>(key, description)</code> lines, a tab bar and a footer. <code>improvise-ktv1</code> proposes moving that content into the shared layer so a browser help overlay is not a second copy of it. Until then it is TUI-only and <code>help_page</code> is already in <code>ViewState</code>, which is why gotcha 32 says the overlay needs no <code>Projection</code>.</p>
|
||
<h2 id="the-panel-widgets"><a class="anchor" href="#the-panel-widgets">#</a>The panel widgets</h2>
|
||
<p><a href="https://github.com/fiddlerwoaroof/improvise/blob/main/src/ui/panel.rs"><code>src/ui/panel.rs</code></a> holds the <code>PanelContent</code> trait and the generic <code>Panel</code> widget. The one non-obvious member is <code>panel()</code>: each impl declares its own identity, and the widget compares it against <code>AppMode::panel()</code> to decide whether to draw itself as active, instead of restating the mode match per panel.</p>
|
||
<p><code>category_panel.rs</code>, <code>formula_panel.rs</code>, <code>view_panel.rs</code> and <code>tile_bar.rs</code> each draw one cache and nothing else. Two facts worth keeping:</p>
|
||
<ul>
|
||
<li>The category panel draws one flattened tree — the axis travels on the entry (<a class="tid" href="../tickets.html#improvise-edp" title="Establish ViewModel layer: derived data between RenderCache and widgets">improvise-edp</a>), so there is no <code>View</code> in it.</li>
|
||
<li>The view panel's "which view is this client on" is read off <code>ViewState</code>, not the workbook: the active view is per-client and is deliberately absent from <code>ViewListCache</code> (<a class="tid" href="../tickets.html#improvise-8uz" title="Cursor and scroll are per-client state stored in the shared View">improvise-8uz</a>, gotcha 29).</li>
|
||
</ul>
|
||
<p><code>tile_bar.rs</code> scrolls by whole tiles: it finds the smallest start index at which the selected tile still fits in the available width, and draws <code>‹</code>/<code>›</code> overflow indicators. That is the file's only piece of real arithmetic and it is local geometry, not a shared derivation.</p>
|
||
</main>
|
||
</div>
|
||
</body>
|
||
</html>
|