From 23a061fd808707e95c1029b8266449e1c35ed159 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 6 Sep 2026 10:09:54 +0200 Subject: [PATCH] Dock the develop column under the photograph on a tall window D-N2 dismissed portrait with one number: a 12-inch tablet is about 1024 logical pixels across, which clears the expanded breakpoint. That was worked out for a 4:3 panel. The tablet's is 3000 by 1920, and on that aspect a column beside the photograph in portrait leaves it a strip 540 wide and 1456 tall: a 3:2 frame gets 540 by 360 where a column below it would give 900 by 600, and the portrait frame gains too. D-N7 records the decision: a third property beside the layout class, derived from the window's aspect with hysteresis, that lays the same rail, canvas and column out on the other axis. Not a sheet, not a second layout, and the rail does not move. N6 measures the device the numbers were guessed for, N7 does the frame with the stack stretched as a stopgap, N8 moves the develop callbacks onto a global so the panels can be declared twice cheaply, and N9 is the three-column composition the dock's width is actually for. N5's "no strip along the bottom" is struck where D-N7 reverses it and kept where it does not. --- docs/ui-navigation.md | 228 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 226 insertions(+), 2 deletions(-) diff --git a/docs/ui-navigation.md b/docs/ui-navigation.md index 76ec960..8045139 100644 --- a/docs/ui-navigation.md +++ b/docs/ui-navigation.md @@ -153,6 +153,12 @@ histogram reduction, which already runs per frame over the displayed frame. > still right about *size* and still right that `cfg(target_os)` is the wrong > axis. It is wrong in one place, and the wrong bit is the sentence "touch > changes **hit regions, not layout**". See D-N6. +> +> **Reversed for portrait, 2026-09-06, by arithmetic.** "Both orientations of +> both targets are the expanded class" is still true and is no longer the +> point. It was worked out for a 4:3 panel; the tablet's is 25:16, and on that +> aspect a column *beside* the photograph in portrait leaves it a strip. See +> D-N7, which keeps the layout class and adds an axis D-N2 did not consider. The question was whether desktop and Android should diverge. The answer turns out to be that **neither the platform nor the width axis separates DarkRoom's @@ -265,6 +271,95 @@ one list of seven. It is the tidier model and a much larger change; deferred until the two-section rail has been lived with. §1.1's complaint was that scope was invisible, so this is the same argument arriving from the other end. +### D-N7 — The column docks under the photograph on a tall window · **DECIDED** + +D-N2 dismissed portrait with one number: a 12-inch tablet is about 1024 +logical pixels across in portrait, which clears `EXPANDED_MIN_WIDTH`, so +portrait is expanded, so there is nothing to design. The number was for a 4:3 +panel. **The tablet's panel is 3000 × 1920**, which is 25:16 — closer to a +sheet of A4 than to an iPad — and the same arithmetic on that aspect comes out +the other way. + +**What the photograph gets.** Logical size depends on the density Android +reports, which nothing in this repository records (N6 measures it). At a scale +of 2.0 the window is 960 × 1500 in portrait; at 1.75 it is 1097 × 1714. Take +the first, subtract the 60px rail, the 360px column and the 44px status bar, +and the canvas beside the column is **540 × 1456** — a strip two and a half +times taller than it is wide. Against a column *under* the canvas, 480px tall: + +| Photograph | Beside the column | Under the column | Gain | +|---|---|---|---| +| 3:2, landscape | 540 × 360 | 900 × 600 | 2.8× the area | +| 2:3, portrait | 540 × 810 | 651 × 976 | 1.4× the area | + +At 1.75 the figures move and the ratios hold (2.4× and 1.3×). So on this panel +the dock wins for **both** orientations of the photograph, not only the +landscape frame one would guess it was for. That is what makes it a decision +rather than a preference: the column is on the right because the eye's path is +tool, photograph, adjustment, and in portrait the photograph in the middle of +that path is the thing being starved. + +**What it is not.** N5 said "no bottom sheet, no second layout, no tool strip +along the bottom — those solve a phone". Still true of all three. This is not +a sheet: the column does not slide over the photograph, it sits beside it on +the other axis, with the same contents, the same collapse and the same toggle. +It is not a second layout in D-N2's sense: the layout class is still decided +by width, the compact class still means what it meant, and a tall narrow +desktop window gets exactly what a portrait tablet gets, which is FR-UI-1's +rule. And the rail does not move — `toolrail.slint` argued it never should, +and a vertical list of finger-sized entries wants height, which portrait has +more of. + +**The axis is aspect, not width, and it is independent of the class.** A 960 +wide portrait window is expanded by width and wants the dock; a 1500 wide +landscape one is expanded by width and does not. So this is a third property +beside `layout-class` and `panel-max-width`, set from the same place for the +same reason — a size that both derives from and feeds the layout is a binding +loop in Slint, and `apply_layout_class` already measures the window. The +window-resized callback reports width alone today and grows a height. The +threshold is height above 1.2 × width, with hysteresis wide enough that a +window resized across square does not flap. + +**Slint cannot turn a layout on its side**, and does not need to. The develop +view is one `HorizontalLayout` of rail, canvas and column; it becomes a +`Rectangle` whose three children take `x`, `y`, `width` and `height` from the +flag. The column's own `VerticalLayout` and `Flickable` are untouched. The +alternative — the column subtree declared twice under two `if`s — is 400 lines +of bindings copied, in a file whose own notes record conditional children in +layouts as the shape that has produced binding loops before. + +**The contents are the real cost.** Everything in the column was drawn for a +360px vertical scroll, and the dock is 900 to 1040 wide by about 480 tall. +Stretched to that width the stack works — sliders get longer tracks, the +histogram divides the width, text wraps — and it is one long scroll in a short +box, with the histogram scrolling away from the sliders it serves, which is +§1.2 again. The width is two and a half to three of today's columns, so the +composition that fits it is three of them side by side (N9): the instruments, +the sliders, and the panels the mode adds. That needs the panels declared a +second time, which is cheap only once their callbacks stop being forwarded +through the window root by hand (N8). The stretched stack ships first as the +stopgap (N7), because the photograph gets its area back on day one and the +dock's contents can be got right afterwards. + +**The dock's height is mandated, as the column's width is.** `panel-width` +exists because a column that sizes itself to its contents is a photograph +that changes size when a caption does; a dock has the same disease on the +other axis. `dock-height` in `style.yaml`, 480 until N6 says otherwise: room +for the pinned instruments (about 260px per N4) and a group of sliders under +them, and a 3:2 frame at 900 wide still fits above it at either scale. + +**The filmstrip stays where it is.** It takes its strip off the bottom of the +photograph on demand and it keeps doing so; with the dock below it sits +between the two. The photograph loses 108px while the roll is open, which is +what it loses today, and the roll is not made part of the dock because it is a +different kind of thing — navigation, not adjustment — and D-N6 has already +been through why two kinds of entry in one control need a rule between them. + +**Not remembered separately.** `PanelChoices` keeps the user's open-or-closed +override per layout class. The dock does not add a class and does not add a +remembered state: closing the column in landscape closes the dock in +portrait, because it is the same column. + ### D-N3 — Collapsible panels, not tool tabs · **OPEN** For the expanded layout, extend `ui-refinement.md` Workstream C from @@ -486,13 +581,132 @@ overlays rather than sits beside the canvas, and `apply_layout_class` already remembers the user's override per class. With N4's collapse in place a narrow window is a one-panel-at-a-time column by consequence rather than by design. -**Deliverable.** Confirm the narrow case is usable and fix what is not. No +**Deliverable.** Confirm the narrow case is usable and fix what is not. ~~No bottom sheet, no second layout, no tool strip along the bottom — those solve a -phone, and there is no phone. +phone, and there is no phone.~~ Still no sheet and still no strip; but a +*tall* window is not a narrow one, and D-N7 (2026-09-06) puts the column under +the photograph there. N6–N9 carry it. This workstream keeps the narrow case. **Done when.** A desktop window dragged to 700px shows the photograph and a usable column, and nothing is unreachable that was reachable at 1400px. +### N6 — Measure the tablet + +**Depends on** nothing. Hours. + +Every figure in D-N7 is computed at a guessed scale factor. The tablet's +panel is 3000 × 1920 physical; its logical size is that divided by whatever +density Android reports, and the two plausible answers (2.0 and 1.75) put the +dock's width at 900 or 1037 and its available height 200px apart. + +**Deliverable.** Log the window's scale factor and logical size once at +startup, beside the existing `apply_layout_class` call, at a level that +reaches `adb logcat`. Run it on the tablet in both orientations. Record the +four numbers in D-N7 and, if they move the table, correct it. #30 +(NFR-COMPAT-1) wants a reference device named; this is one of the numbers +that names it. + +**Done when.** D-N7 cites measured logical sizes, not a scale it assumed, and +`dock-height` has been checked against the measured portrait height. + +### N7 — The column docks on a tall window + +**Depends on** N6 only for the value of `dock-height`; the shape does not +wait for it. + +**Deliverable, three parts.** + +*The flag.* `window-resized` reports height as well as width. +`apply_layout_class` derives a third property, `column-below`, from the +aspect with hysteresis (enter above 1.25, leave below 1.15, or thereabouts — +the point is that a window resized across square does not flap), and sets it +beside `layout-class` and `panel-max-width`. It is not a layout class and +`PanelChoices` does not learn about it. + +*The frame.* The `HorizontalLayout` holding `ToolRail`, `canvas-area` and +`develop-column` becomes a `Rectangle` and each child takes `x`, `y`, `width` +and `height` from the flag. The rail is full height on the left in both +cases. With the flag off the geometry is what the layout produced, to the +pixel — screenshot before and after and diff them. With it on, the column is +`dock-height` tall and runs from the rail's edge to the window's; the canvas +has what is left above it. `panel-visible` collapses the dock to zero height +exactly as it collapses the column to zero width. + +*The contents, as a stopgap.* The column's stack stretches to the dock's +width: the Flickable's viewport width follows the dock, the `min-width` floor +that keeps a mandated column honest is still there and is simply not binding. +Sliders take the width. Nothing is reflowed; N9 does that. + +`dock-height` goes in `style.yaml` next to `panel-width`, with the reasoning +D-N7 gives, and is read as `Theme.dock-height`. + +**Done when.** On the tablet in portrait, a 3:2 photograph is drawn at the +width of the canvas, not at the width the old column left it; turning the +tablet moves the column back beside it with the same scroll position and the +same panels open; a desktop window dragged taller than it is wide does the +same; and the screenshot diff in landscape is empty. + +### N8 — Develop callbacks onto a global + +**Depends on** nothing; can run beside N7. + +The develop panels — `AdjustPanel`, `MaskPanel`, `SpotPanel`, `ComposePanel`, +`TransferPanel`, `FocusPanel`, `HistogramPanel`, `InfoPanel` — each forward +their callbacks and take their inputs through the window root, and the +instantiation in `app.slint` that wires one up is twenty to forty lines. N9 +needs each of them declared a second time, and copying that wiring is the +kind of duplication that drifts. + +**Deliverable.** A Slint global (or one per panel family, if a single one +reads badly) carrying the develop callbacks and the inputs the panels bind +to. A panel calls the global directly; Rust hooks the global instead of the +window. The existing instantiation in the column shrinks to the properties +that genuinely differ by placement, which should be none. `Readout` in +`adjust.slint` is the precedent for a global in this codebase. + +The tests in `ui/dr-ui/src` that drive these callbacks through the window +move to the global; count them before starting, so the ticket knows its own +size. + +**Done when.** No develop panel's instantiation in `app.slint` forwards a +callback by hand, every existing test passes, and a second instantiation of +any panel is under five lines. + +### N9 — Three columns in the dock + +**Depends on** N7 and N8. + +**Deliverable.** A second composition of the same panels for the dock, in +three columns of equal width side by side, each its own scroll: + +1. **Instruments** — `InfoPanel`, `HistogramPanel`, `FocusPanel`. What the + sliders are judged against, pinned by construction: it does not scroll + with them because it is not in their column. +2. **Sliders** — `GroupStrip` above `AdjustPanel`, exactly as in the column. + With a group selected this is one screen of sliders; with All it scrolls. +3. **The mode's panels** — `ComposePanel` and `TransferPanel` in photo mode, + `SpotPanel` in repair, `MaskPanel` in local. Empty otherwise, which is + a signal of its own about which mode the view is in (§1.1). + +Three columns at 900 wide are 300 each, and at 1037 they are 346: inside the +280–360 band `PANEL_MIN_WIDTH`'s note says the sliders stay accurate over. +The dock takes the three-column composition only when a third of its width +is at least `PANEL_MIN_WIDTH` — 840px of dock, which both scales of the +tablet exceed. Below that it keeps N7's stretched stack. Two compositions, +not three: a dock too narrow for three columns is a desktop window in an odd +shape, and N5 says that case degrades. + +The panels are declared twice, once per composition, which is what N8 made +cheap. Declaring each once and positioning it by hand in both modes was +considered and rejected: a column that scrolls is a `Flickable`, a +`Flickable`'s children are its children, and a panel cannot be in two. + +**Done when.** On the tablet in portrait the histogram is visible while any +slider in any group is dragged, with no scrolling to arrange it — N4's own +criterion, met in the dock by construction; every panel reachable in +landscape is reachable in portrait; and the instantiation of each panel in +the dock is a handful of lines. + --- ## 5. Sequencing @@ -501,6 +715,10 @@ usable column, and nothing is unreachable that was reachable at 1400px. ui-refinement A ──┬── C ──── N4 ──┐ └── D ──── F ├── N5 N1 ── N2 ── N3 ───────────────────┘ + +N6 ── N7 ──┐ + ├── N9 + N8 ──┘ ``` N1–N3 are independent of `ui-refinement.md` and can start now; they touch the @@ -508,6 +726,12 @@ canvas and the develop column's contents, not its layout. N4 needs C's `Section`. N5 needs both and should land last, as F does — it is the one that rearranges everything. +N6–N9 are the portrait dock (D-N7) and run beside the first row rather than +after it. N7 is the one that touches the develop view's frame, so it should +not land in the same wave as F; N8 touches only plumbing and can. N9 waits +for both, and gains from N4 if N4 is in by then — a collapsible panel in a +300px column is worth more than in a 360px one. + ## 6. Invariants, for every workstream - **FR-DEV-3a.** Adding a pipeline operation must still surface in both