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.
This commit is contained in:
2026-09-07 20:00:40 +02:00
parent e13d3a54fc
commit de32c04a68
+226 -2
View File
@@ -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