Commit Graph
10 Commits
Author SHA1 Message Date
dtourolle 36556729f1 Let Back close the presets menu instead of the application
The presets menu at the foot of the tool rail is a PopupWindow, and
showing a popup takes focus off the develop view until it closes. The
menu held nothing focusable, so Android's Back gesture, pressed to
dismiss it, found no focus item, went unanswered, and the platform
closed the application. Slint closes a popup on Escape by itself but
not on Back.

The menu now holds a key scope, as the film list does, that closes it
on Back or Escape. The next Back leaves develop for the grid.
2026-09-30 22:11:51 -04:00
dtourolle 31bcc3a462 File presets in folders that open and close, as collections do
The presets menu and sheet listed every preset under flat section
headings, seventy rows to scroll past. They now list folders, closed
until opened, with how many presets each holds; opening one shows what
is inside it, folders and presets indented beneath.

A category is spelled in the name: "Portraits/Warm skin" is Warm skin
in a Portraits folder under Yours. The file format does not change, so
an older build lists the whole path as the name; renaming a preset is
how it moves, and saving or renaming into a folder opens the way to it.
A Lightroom import names what it reads after the folders below the one
chosen, and a "/" in a displayed name becomes "∕" so it files nothing.
The shipped film sections become Film › Colour, Cinema and Black and
white.

The tree is built and flattened in Rust (PresetTree), each row carrying
its depth, and which folders are open is remembered for the session.

A PopupWindow keeps the size it was shown at, so a folder opened in the
menu pushed its contents under "Save or manage…"; the menu is shown
again after each toggle to take its new height. That is a function on
the rail because Slint 1.17 generates Rust that does not compile for a
popup's close() reached from inside the popup. The sheet's list takes a
preferred height of up to 400px, since a Flickable reports next to
nothing and an opened folder showed three rows.

The manual describes the folders and naming. Its pictures show the menu
with Film › Colour open, and a black-and-white stock applied from Film
› Black and white; the scenes aim popup rows from the rail's entry, and
pick the menu's "Film" over the develop column's film chooser.
2026-09-27 18:37:56 -04:00
dtourolle f52c4cb8b6 Open presets from a menu at the foot of the tool rail
Presets were a "Presets…" button in develop's top bar that opened a
sheet over the photograph, so applying one was two clicks with the
picture covered. They are now an entry pinned to the bottom of the tool
rail, apart from the tools because a preset arms nothing, and it opens a
menu beside the rail: the same sectioned rows the sheet lists, one click
to apply to the open photograph. The menu's last row, "Save or manage…",
opens the sheet, which keeps saving, renaming, reverting and importing,
since those take a name or a path.

The menu is a PopupWindow for the film list's reasons, with the list in
a clipped box of its own; the Flickable alone let its last row draw over
the manage row. Choosing from it zeroes preset-apply-count before
applying, since the grid may have left its selection count there.

The manual says where presets are now and gains a picture of the open
menu. The scenes reach the sheet through the menu and aim the manage
row from the rail's entry, as the film list's rows are aimed, because
the automation reports a popup's elements in the popup's coordinates.
2026-09-27 16:08:33 -04:00
dtourolle 84fade99ec Put the developer docs under docs/dev and index the folder for users first
docs/ had 26 developer documents flat beside the manual, and the two
audiences are very differently sized: most readers want the manual and
the gesture reference, a few want the register, the designs and the
measurements. The manual and gestures.md stay at the top; everything for
someone changing the code moves to docs/dev/, and the two documents that
name their own successors — the v0.1 milestone and the UI-refinement plan
— go to docs/dev/archive/ rather than being deleted, since both are still
cited. docs/README.md is the index, users first.

Every reference follows: code comments, Cargo manifests, the workflows,
the pre-commit hook, the bench and traceability tools (which locate the
repo root by docs/dev/requirements.md now), packaging, the Docker READMEs,
CLAUDE.md, CONTRIBUTING.md and the README. The matrix links one level
deeper and is regenerated. Links out of the moved documents into the tree
gain a level; a link checker over every Markdown file finds none broken.
2026-09-20 21:16:03 +02:00
dtourolle 0a5eab0487 Wire the develop panels through globals, so a second copy is one line
Every panel in the develop column declared its inputs and its callbacks and
had `app.slint` bind each one to a property or a callback on the window root.
That is fine while a panel is drawn once. N9 draws them a second time, in the
portrait dock, and the wiring is what would have to be copied: `MaskPanel`
alone ran to forty lines of forwarding, and a callback added to one copy and
not the other compiles, renders, and simply does nothing on the layout nobody
was looking at.

So the wiring moved to Slint globals. A panel reads the global and calls the
global; Rust hooks the global instead of the window; and the instantiation in
the column is now the panel's name and a pair of braces — every one of the ten
children of the column, with no property that differs by placement left to
supply.

There is a global per panel family rather than one for all of them, and the
reason is an import cycle. Each panel's model struct — `ParamRow`, `MaskRow`,
`HistogramView` — is declared in the panel's own file, so a single global
holding `[MaskRow]` and `[ParamRow]` would have to live in a file importing
`masks.slint` and `adjust.slint` while both imported the global back, which
Slint rejects. Breaking that needs six model declarations relocated, which is a
change to the data model and not to the plumbing this is about. A global beside
the panel it serves also lets each name drop the prefix it was carrying only
because the window root is one flat namespace: `root.spot-radius` is
`Repair.radius`, and `root.peaking-on` is `Peaking.showing`.

`session.slint` is new and holds the two facts every family needs and none of
them owns: whether there is an open photograph to edit, and which mode the view
is in, with the three readings of the mode derived once instead of at each of
the dozen places that tested one. `ViewMode` moves there from `adjust.slint`,
where it was only ever a lodger.

Nothing on screen changes. What is not here: the tool rail and the status strip
still take their properties at the instantiation, because they are drawn once
and N9 does not copy them; the preset sheet's own state stays on the window,
because the library grid opens the same sheet and a global cannot bind the
window's state — which is why `Transfer.open-presets` is handled in
`presets.rs`, beside the summary it already had to compute.
2026-09-07 20:01:01 +02:00
dtourolle 2cd49d1cb7 Measure the rail by counting it, so it can scroll instead of clipping
With the adjustment groups in the rail, a short window silently lost them.
At a 1500x680 window the rail drew Photo, Compose, Local, Repair, the seam and
"All", and then stopped: Optics, Light, Colour, Effects and Detail were not
scrolled off, they were gone, with nothing on screen to say so. The develop
view's primary navigation, unreachable by any means.

The Flickable was put here to prevent exactly that, and it could not, because
its viewport asked the layout how tall it wanted to be while the layout was
already taking its height from the viewport. Slint settles that cycle by
handing back the height it was given, so `max(self.height, preferred-height)`
could never exceed `self.height` and there was never anything to scroll.

Counting breaks the cycle. Every entry here is a fixed height by construction —
a tool is `rail-entry-height`, a group is a touch target — so the content is
six plus four fifty-fours plus a gap plus a touch target for each group and
"All", which is exact rather than an estimate and depends on nothing that
depends on it. Four tools and five groups come to 498, against the 340 a
680-pixel window leaves at 2x, and the difference is now scrollable rather
than absent.

Found by shrinking the window with the groups forced into the rail. The
interaction itself is unverified: synthetic input does not reach a Slint
window on this desktop, and the tablet was disconnected, so what is confirmed
is the arithmetic and the clipping it explains, not the scrolling it should
restore.
2026-09-07 00:17:08 +02:00
dtourolleandClaude Opus 5 59917c5183 Call the tool Compose, since that is what its panel says
Benchmarks / CPU and I/O (per commit) (push) Successful in 14m35s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Failing after 1h20m20s
Build and test / Layer separation (push) Successful in 51s
Traceability / Requirement traces (push) Successful in 2m8s
🐳 Android image / Build and push (push) Successful in 5s
Build and test / android-image (push) Successful in 5s
Build and test / Android (aarch64) (push) Successful in 1h5m51s
The rail entry read "Crop" while the panel it opens is headed COMPOSE and the
button leaving it said "Done Cropping". One mode, three names, and the odd one
out was named after a single control rather than after the decision — which is
what made cropping look like a category of its own in the first place.

Straightening, the quarter turns and the flips are already in that panel, and
perspective will be. `ViewMode.crop` keeps its name: it identifies a canvas
interaction, which is exactly what it still is.

Found by looking at the running application rather than by reading, which is
also how the two halves of this were noticed to disagree at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 18:29:08 +02:00
dtourolleandClaude Opus 5 6a97fdf6f9 Put the adjustment groups in the rail where a finger is driving
Reported from the tablet: the tool rail is very useful there, and the same
interface under a mouse and keyboard is not. That is `ui-navigation.md` D-N2's
central assumption failing in use, and the interesting part is which half of
it failed.

D-N2 was right that platform is the wrong axis and width is the wrong axis: a
tablet in landscape wants what a desktop wants, and a desktop window dragged
narrow wants what a small screen wants. `apply_layout_class` still decides the
layout class from the window and nothing here changes that. What D-N2 got
wrong is the sentence "touch changes hit regions, not layout" — it identified
input as the real difference between the targets and then assumed that
difference could never reach the layout.

Two controls answer one question — which group of adjustments am I looking at
— and neither is better in general. A horizontal strip above the column is one
gesture to a target the eye has already found, and it pans when the operation
set is rich, so a group can sit off the end with nothing saying so: a pointer
user tolerates that, a finger user never discovers it. The same list down the
rail is every entry visible at once, each finger-sized, on the edge of the
screen the hand is already holding, and it costs no width because the rail is
already there.

So `ToolRail` grows a second section, and `GroupStrip` stands down when it
does. The two are never both on screen, which is why they can share
`adjust-tab-picked`: Rust is not told which was pressed and has no reason to
want to. Mode and group stay independent axes as N1 requires — one entry lit
in each section, and choosing a group while a tool is held still filters
without putting the tool down.

They stay drawn differently, which N1 also required. The tools fill with
`active-dim` and invert their ink; the groups take a bar down the leading
edge — the strip's underline turned ninety degrees — so a lit entry says which
kind of state it is without the reader having to remember which section it was
in. The rule between the sections is the second signal.

The rail scrolls now. Its own note argued against a Flickable because "this
list is four entries written in this file"; with the groups in it the list
comes from the operation set, which is exactly the "something the user's data
decides" that note excluded this control from.

The axis is input, and it is a preference because the automatic answer is a
guess that cannot be made reliable. Neither platform can be asked what the
user is holding: an Android tablet in a keyboard case is being driven like a
desktop, and a touchscreen laptop is whichever its owner says.
`dr_plat::is_touch_first` reports the usual case per platform, and
`GroupNavigation` lets it be overridden. Settings names what Automatic
resolves to on this device rather than leaving it to be found by pressing.

D-N6 records the reversal beside the decision it reverses, including the half
that still stands and the question it opens: whether Local is a mode at all,
or a scope that would collapse the two sections into one list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 18:14:03 +02:00
dtourolleandClaude Opus 5 46706fe622 Let the tool rail be pressed, not only read
The rail is the develop view's primary navigation and reached the platform as
four pieces of static text. The labels got through — a `Text` announces itself
— so a screen reader could read "Photo, Crop, Local, Repair" and had no way to
learn that any of them could be pressed, or which one was currently held. The
one control that decides what a click on the photograph does was a caption.

Each entry now declares itself a checkable button carrying the tool's own
word, with the held state reported rather than left to the fill. Checkable is
unconditional here, unlike `Button`'s: a rail entry is always a held-or-not
state, so an unheld one should say "not pressed" rather than pass for an
ordinary button.

The action repeats the click handler's expression rather than calling it. A
`TouchArea`'s `clicked` is raised by the pointer and cannot be raised from a
binding, so the alternative is a function wrapping two lines — and the
duplicated ternary sits four lines below the original where the two cannot
drift out of sight of each other.

Worth recording for the next control like this: `accessible-*` on an element
inside a `for` does work, despite the accessibility pass skipping repeated
elements. `process_repeater_components` runs first and moves the bindings into
a real component whose root is not repeated; what the later pass skips is the
empty placeholder left behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:27:10 +02:00
dtourolleandClaude Opus 5 ef07e6ca3e Give the canvas tools a rail of their own, and the column one width
Build and test / Desktop (Linux) (push) Failing after 1h14m38s
Build and test / Layer separation (push) Successful in 48s
🐳 Android image / Build and push (push) Successful in 16m30s
Build and test / android-image (push) Successful in 16m31s
Traceability / Requirement traces (push) Successful in 1m47s
Build and test / Android (aarch64) (push) Successful in 1h0m21s
Crop, Local and Repair were chips at the head of the develop column, sharing a
row with the adjustment groups and told apart from them by the shape of their
highlight. Three things followed from that, and only the last is cosmetic: the
column closes, so the way out of a mode went away with the way in — hence the
duplicate "Done Cropping" over the canvas; the chips are generated from the
operation set, so the widest thing in the sidebar was a row nobody had chosen
the contents of; and a mode and a filter are different kinds of state wearing
one control.

They are a fixed 60px rail down the left now, generated from a single table in
toolrail.slint. A tool is one row of it plus a drawing plus a ViewMode variant;
nothing in app.slint is touched to add one. What is left of the strip is the
group filters, so it is GroupStrip.

The column stops measuring itself. Every panel published a content-width and
declared it as min-width, and the column took the largest — which spent the
photograph's pixels on whatever happened to be widest, and moved the image
sideways when switching tools swapped one set of panels for another. It is
panel-width now, one number in style.yaml.

That number is 360 and it is measured, not picked: the contents report a
minimum of 344 in every mode, and they do not compress below it because a Text
that does not elide reports the same minimum as preferred. 320 was tried and
sliced Paste down the middle. The Flickable's viewport is floored at the
layout's minimum rather than its preferred width for the same reason — content
that is never told how much room it has cannot adapt to having less.

Removing the eight content-width declarations repairs three comments an
earlier edit had spliced sentences into. The raw histogram's note on keeping
its hint short is rewritten rather than dropped: an over-long hint no longer
widens the column, it pushes the column's minimum past the width it has and
clips the panel, which makes that constraint sharper rather than obsolete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 09:59:50 +02:00