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.
This commit is contained in:
2026-09-27 18:37:56 -04:00
parent 89b464db0c
commit 31bcc3a462
14 changed files with 680 additions and 174 deletions
+34 -11
View File
@@ -53,8 +53,8 @@
import { Theme } from "theme.slint";
import { Icon } from "icons.slint";
import { ViewMode } from "session.slint";
import { PresetRow, PresetOrigin } from "presets.slint";
import { PanelHeading, Label, ScrollBar } from "widgets.slint";
import { PresetRow, PresetOrigin, PresetFolderRow } from "presets.slint";
import { Label, ScrollBar } from "widgets.slint";
// One tool. A struct rather than four parallel arrays so a row cannot be
// half-added — the compiler will not let a new entry omit its icon.
@@ -137,6 +137,8 @@ export component ToolRail inherits Rectangle {
in property <[PresetRow]> presets;
callback preset-picked(string);
callback manage-presets();
/// A category folder in the menu was opened or closed, by its key.
callback folder-toggled(string);
/// The groups this rail actually draws.
///
@@ -217,6 +219,15 @@ export component ToolRail inherits Rectangle {
// scrolls.
private property <length> foot-height: Theme.rail-entry-height + 1px;
// Show the presets menu again, so it takes the height its rows now want.
// A function on the rail rather than the two calls in the row that needs
// them: Slint 1.17 generates Rust that does not compile for a popup's
// `close()` reached from an element nested inside that popup.
function remeasure-menu() {
preset-menu.close();
preset-menu.show();
}
// One row of the presets menu. Shorter than a touch target for the reason
// the film list gives: a row's neighbours are other rows.
private property <length> menu-row-height: 32px;
@@ -540,15 +551,24 @@ export component ToolRail inherits Rectangle {
width: parent.width;
height: root.menu-row-height;
if entry.heading != "": PanelHeading {
x: Theme.gap;
width: parent.width - 2 * Theme.gap;
// Opening a folder rebuilds the rows, and so the
// height the menu wants. A PopupWindow takes its
// geometry once, when shown — a folder opened in
// place stayed the old size and pushed what it
// held under "Save or manage…" — so the menu is
// shown again, measured afresh, its foot still on
// the entry and growing upward.
if entry.folder: PresetFolderRow {
entry: entry;
width: parent.width;
height: parent.height;
text: entry.heading;
vertical-alignment: bottom;
clicked => {
root.folder-toggled(entry.key);
root.remeasure-menu();
}
}
if entry.heading == "": row := TouchArea {
if !entry.folder: row := TouchArea {
mouse-cursor: pointer;
clicked => {
root.preset-picked(entry.name);
@@ -567,12 +587,15 @@ export component ToolRail inherits Rectangle {
: (row.has-hover ? Theme.hover : transparent);
HorizontalLayout {
padding-left: Theme.gap;
// Past the folder's arrow, so a preset
// lines up with its folder's name.
padding-left: Theme.gap-sm + 14px + Theme.gap-sm
+ (entry.depth - 1) * Theme.indent;
padding-right: Theme.gap;
Label {
text: entry.origin == PresetOrigin.changed
? entry.name + " · changed"
: entry.name;
? entry.label + " · changed"
: entry.label;
emphasised: row.has-hover;
vertical-alignment: center;
overflow: elide;