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.
403 lines
16 KiB
Plaintext
403 lines
16 KiB
Plaintext
// The icon set, drawn rather than typed.
|
||
//
|
||
// **Why this file exists.** Every glyph in this UI used to be a character in a
|
||
// Text: `⟲` for rotate-left, `☰` for the sidebar, `★` for a rating, `✓` for a
|
||
// tick. That works on a desktop, where the font stack ends in something with
|
||
// full symbol coverage, and it fails on Android, where it does not: the system
|
||
// font is Roboto, fallback there is resolved per *script* rather than per
|
||
// character, and none of the fallback families claimed for the Common script
|
||
// carry the Dingbats, Arrows or Miscellaneous Symbols blocks these characters
|
||
// live in. The result is that the toolbar renders as a row of tofu — a box
|
||
// with an X — and the controls become unusable.
|
||
//
|
||
// No amount of picking "safer" characters fixes that class of bug, because the
|
||
// question "is this codepoint in the font the platform happens to pick" has a
|
||
// different answer on every device. So the icons stop being text. Each one is
|
||
// an SVG path drawn into a 24×24 view box and rendered by `Path`, which goes
|
||
// through the same renderer as every other shape on screen and cannot fall
|
||
// back to anything.
|
||
//
|
||
// **The 24×24 view box is the contract.** `Path` with an explicit view box
|
||
// fits *the box* to the element, not the ink inside it, so two icons drawn to
|
||
// the same box occupy the same optical size even when their drawings differ in
|
||
// extent — a star that reaches the edges and a chevron that does not still
|
||
// look like siblings. Icons are therefore always drawn square; a caller that
|
||
// wants a different aspect should lay out around the icon, not stretch it.
|
||
//
|
||
// **Stroke width is in screen pixels, not view-box units.** Slint applies the
|
||
// stroke after fitting, so a fixed `stroke-width` would thicken as the icon
|
||
// shrank. `weight` scales it with `size` instead, which keeps a 10px tick and
|
||
// a 16px sidebar bar at the same apparent weight. It is also why the filled
|
||
// paths carry a stroke-width with a transparent stroke: the fit shrinks the
|
||
// drawing by the stroke width, so a filled star with no stroke width would
|
||
// come out fractionally *larger* than the outlined one beside it, and the two
|
||
// would jump as a rating changed.
|
||
|
||
// One drawing. Everything an icon path shares lives here so the table below is
|
||
// nothing but geometry; `fill`, `stroke` and `stroke-width` come from the call
|
||
// site because they depend on the enclosing `Icon`'s properties.
|
||
component Ink inherits Path {
|
||
viewbox-width: 24;
|
||
viewbox-height: 24;
|
||
width: 100%;
|
||
height: 100%;
|
||
// Round joins throughout: at 12px a mitred corner on a 1.4px stroke is a
|
||
// single dark pixel that reads as dirt on the screen.
|
||
stroke-line-cap: round;
|
||
stroke-line-join: round;
|
||
}
|
||
|
||
// A single icon, named.
|
||
//
|
||
// Names are strings rather than an enum because Slint enums cannot be extended
|
||
// from another file and the set is consumed across six screens; a typo shows
|
||
// up as a blank box, which is loud enough in review. The vocabulary is:
|
||
//
|
||
// check cross menu trash plus
|
||
// star star-outline
|
||
// chevron-right chevron-down chevron-left arrow-up
|
||
// rotate-ccw rotate-cw flip-h flip-v
|
||
// collection collection-smart
|
||
// offline offline-held
|
||
// photo crop mask repair
|
||
// eye eye-closed
|
||
// help
|
||
export component Icon inherits Rectangle {
|
||
in property <string> name;
|
||
/// The single colour the whole drawing takes. Named `ink` rather than
|
||
/// `color` because Rectangle already has a legacy `color` alias for its
|
||
/// background and Slint refuses to shadow it. Icons are monochrome by
|
||
/// design — they sit in chrome that is already monochrome, and a two-tone
|
||
/// icon would be the only thing on screen carrying colour for decoration.
|
||
in property <brush> ink;
|
||
/// Edge length. Icons are square; see the view-box note above.
|
||
in property <length> size: 16px;
|
||
|
||
/// Stroke thickness, derived rather than set. The floor keeps the hairline
|
||
/// from disappearing entirely on a low-DPI desktop at the smallest sizes.
|
||
out property <length> weight: max(1.2px, root.size / 11);
|
||
|
||
width: root.size;
|
||
height: root.size;
|
||
horizontal-stretch: 0;
|
||
vertical-stretch: 0;
|
||
|
||
// --- marks -----------------------------------------------------------
|
||
|
||
if root.name == "check": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 4.2 12.6 L 9.6 18 L 19.8 6.4";
|
||
}
|
||
|
||
if root.name == "cross": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 6.2 6.2 L 17.8 17.8 M 17.8 6.2 L 6.2 17.8";
|
||
}
|
||
|
||
if root.name == "plus": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 4.5 L 12 19.5 M 4.5 12 L 19.5 12";
|
||
}
|
||
|
||
// --- rating ----------------------------------------------------------
|
||
//
|
||
// Solid and outline are the same ten points, so a filled star and an empty
|
||
// one sit at exactly the same size and position and the strip does not
|
||
// shimmer as a rating is dragged across it.
|
||
|
||
if root.name == "star": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 3.1 L 14.32 9.21 L 20.85 9.53 L 15.76 13.63 "
|
||
+ "L 17.46 19.94 L 12 16.35 L 6.54 19.94 L 8.24 13.63 "
|
||
+ "L 3.15 9.53 L 9.68 9.21 Z";
|
||
}
|
||
|
||
if root.name == "star-outline": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 3.1 L 14.32 9.21 L 20.85 9.53 L 15.76 13.63 "
|
||
+ "L 17.46 19.94 L 12 16.35 L 6.54 19.94 L 8.24 13.63 "
|
||
+ "L 3.15 9.53 L 9.68 9.21 Z";
|
||
}
|
||
|
||
// --- chrome ----------------------------------------------------------
|
||
|
||
if root.name == "menu": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 3.5 6.5 L 20.5 6.5 M 3.5 12 L 20.5 12 "
|
||
+ "M 3.5 17.5 L 20.5 17.5";
|
||
}
|
||
|
||
if root.name == "trash": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 9.2 6.2 L 9.2 3.8 L 14.8 3.8 L 14.8 6.2 "
|
||
+ "M 3.4 6.2 L 20.6 6.2 "
|
||
+ "M 5.6 6.2 L 6.7 20.2 L 17.3 20.2 L 18.4 6.2 "
|
||
+ "M 10.2 9.8 L 10.6 16.8 M 13.8 9.8 L 13.4 16.8";
|
||
}
|
||
|
||
// Disclosure arrows are solid triangles rather than open chevrons: they
|
||
// are the smallest thing drawn in this UI, and at 11px a stroked chevron
|
||
// is two anti-aliased diagonals with nothing left of the shape between.
|
||
|
||
if root.name == "chevron-right": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 9 5.5 L 16.5 12 L 9 18.5 Z";
|
||
}
|
||
|
||
if root.name == "chevron-left": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 15 5.5 L 7.5 12 L 15 18.5 Z";
|
||
}
|
||
|
||
if root.name == "chevron-down": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 5.5 9 L 18.5 9 L 12 16.5 Z";
|
||
}
|
||
|
||
if root.name == "arrow-up": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 20 L 12 5 M 5 12 L 12 5 L 19 12";
|
||
}
|
||
|
||
// --- framing ---------------------------------------------------------
|
||
//
|
||
// The rotation arrows are a 280° arc with a solid head at the end of
|
||
// travel. The gap sits at the top, where the eye looks first, so the two
|
||
// are told apart by which side the head is on rather than by following a
|
||
// curve — at 16px an arrowhead small enough to sit flush with a thin arc
|
||
// is a smudge, and both directions look like a plain circle.
|
||
|
||
if root.name == "rotate-ccw": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 10.82 6.5 A 6.8 6.8 0 1 0 18.39 10.87";
|
||
}
|
||
if root.name == "rotate-ccw": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 13.1 6.49 L 21.12 9.88 L 15.66 11.87 Z";
|
||
}
|
||
|
||
if root.name == "rotate-cw": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 13.18 6.5 A 6.8 6.8 0 1 1 5.61 10.87";
|
||
}
|
||
if root.name == "rotate-cw": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 10.9 6.49 L 2.88 9.88 L 8.34 11.87 Z";
|
||
}
|
||
|
||
// A flip is a mirror about an axis, so the icon is literally that: two
|
||
// arrowheads facing away from the line they would be reflected across.
|
||
|
||
if root.name == "flip-h": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 8.2 5.2 L 1.8 12 L 8.2 18.8 Z "
|
||
+ "M 15.8 5.2 L 22.2 12 L 15.8 18.8 Z";
|
||
}
|
||
if root.name == "flip-h": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 2.6 L 12 21.4";
|
||
}
|
||
|
||
if root.name == "flip-v": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 5.2 8.2 L 12 1.8 L 18.8 8.2 Z "
|
||
+ "M 5.2 15.8 L 12 22.2 L 18.8 15.8 Z";
|
||
}
|
||
if root.name == "flip-v": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 2.6 12 L 21.4 12";
|
||
}
|
||
|
||
// --- collections -----------------------------------------------------
|
||
//
|
||
// A manual collection is a list of rows someone put there; a smart one is
|
||
// a rule. The shapes are deliberately unalike rather than one shape with a
|
||
// badge, because the pair appears down a narrow sidebar at 12px where a
|
||
// badge would be three pixels.
|
||
//
|
||
// Both are drawn thinner than they want to be for the same reason. At 12px
|
||
// the interior of a shape is four or five pixels across, so every internal
|
||
// line spends one of them and its two neighbours to anti-aliasing: the
|
||
// three-rule box this started as, and the diamond with a fat centre, both
|
||
// filled in to a solid blob. One rule and a small centre survive.
|
||
|
||
if root.name == "collection": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 3.4 5.4 L 20.6 5.4 L 20.6 18.6 L 3.4 18.6 Z "
|
||
+ "M 3.4 10.2 L 20.6 10.2";
|
||
}
|
||
|
||
if root.name == "collection-smart": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 2.8 L 21.2 12 L 12 21.2 L 2.8 12 Z";
|
||
}
|
||
if root.name == "collection-smart": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 9 L 15 12 L 12 15 L 9 12 Z";
|
||
}
|
||
|
||
// --- availability ------------------------------------------------------
|
||
//
|
||
// TRACES: FR-NC-6a | FR-NC-6c
|
||
// "Kept on this device": an arrow coming down into a tray. The download
|
||
// shape rather than a pin, because what the gesture does is *fetch* — a
|
||
// pin reads as "stick this to the top of the list", which is what it means
|
||
// in every other sidebar the user has seen.
|
||
//
|
||
// Outline and solid are the same tray with the same arrow, so a collection
|
||
// that finishes downloading does not shift under the finger that started
|
||
// it. Solid fills the tray only: an arrow filled as well loses the shaft
|
||
// at 12px and reads as a blob.
|
||
|
||
if root.name == "offline": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 3.6 L 12 13.8 M 7.4 9.4 L 12 14 L 16.6 9.4 "
|
||
+ "M 4.2 15.4 L 4.2 19.8 L 19.8 19.8 L 19.8 15.4";
|
||
}
|
||
|
||
if root.name == "offline-held": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 3.6 L 12 13.8 M 7.4 9.4 L 12 14 L 16.6 9.4";
|
||
}
|
||
if root.name == "offline-held": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 4.2 15.4 L 19.8 15.4 L 19.8 19.8 L 4.2 19.8 Z";
|
||
}
|
||
|
||
// --- develop tools -----------------------------------------------------
|
||
//
|
||
// One per entry in `ToolRail`'s table (toolrail.slint). They are drawn at
|
||
// 20px in a rail that is nothing but icons, which is larger than anything
|
||
// else in this file gets — and they need it. A rail is *scanned*, not
|
||
// read: the label under each one confirms a guess the silhouette has
|
||
// already made, and if the silhouette makes no guess the label is doing
|
||
// all the work and the icon is decoration.
|
||
//
|
||
// So they are told apart by outline rather than by interior detail. Three
|
||
// of the four deliberately break the frame the fourth sits inside.
|
||
|
||
// The whole photograph: a frame with a horizon in it. The default tool, so
|
||
// it is the most literal drawing here — it means "nothing is armed", and
|
||
// the way to say that is to show the subject with nothing laid over it.
|
||
if root.name == "photo": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 3.4 5.2 L 20.6 5.2 L 20.6 18.8 L 3.4 18.8 Z "
|
||
+ "M 3.4 15.4 L 8.8 10 L 13 14.2 L 15.8 11.4 L 20.6 16.2";
|
||
}
|
||
|
||
// Crop marks: two overlapping right angles, the shape every editor uses.
|
||
// Drawn out to the edge of the view box on purpose — a crop is the one
|
||
// tool here that acts on the frame rather than within it.
|
||
if root.name == "crop": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 7.4 2.6 L 7.4 16.6 L 21.4 16.6 "
|
||
+ "M 2.6 7.4 L 16.6 7.4 L 16.6 21.4";
|
||
}
|
||
|
||
// A local adjustment: the half-filled circle that means "this side and not
|
||
// that one" wherever tone is edited. The fill carries it — an outlined
|
||
// circle is a shape, a half-filled one is a *selection* — and the two
|
||
// halves are the same circle, so nothing shifts if the pair is ever drawn
|
||
// in different colours.
|
||
if root.name == "mask": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 3.6 A 8.4 8.4 0 1 0 12 20.4 A 8.4 8.4 0 1 0 12 3.6 Z";
|
||
}
|
||
if root.name == "mask": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 3.6 A 8.4 8.4 0 0 1 12 20.4 Z";
|
||
}
|
||
|
||
// TRACES: FR-DEV-19c
|
||
// An eye, open: the almond and the pupil. The pupil is filled so that at
|
||
// 16px the open eye reads as a dot with a lid rather than as two arcs
|
||
// that could be anything, and it takes the same ink as the rest so the
|
||
// whole glyph can be drawn in the colour of the mask it shows.
|
||
if root.name == "eye": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 2.4 12 C 5.2 6.8 8.4 4.8 12 4.8 C 15.6 4.8 18.8 6.8 21.6 12 "
|
||
+ "C 18.8 17.2 15.6 19.2 12 19.2 C 8.4 19.2 5.2 17.2 2.4 12 Z";
|
||
}
|
||
if root.name == "eye": Ink {
|
||
fill: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 12 8.6 A 3.4 3.4 0 1 0 12 15.4 A 3.4 3.4 0 1 0 12 8.6 Z";
|
||
}
|
||
|
||
// An eye, closed: the lower lid alone, with three lashes. The lid is the
|
||
// same lower curve as the open eye's, so the two glyphs sit at the same
|
||
// height and toggling between them does not make the row jump.
|
||
if root.name == "eye-closed": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 2.4 12 C 5.2 17.2 8.4 19.2 12 19.2 C 15.6 19.2 18.8 17.2 21.6 12 "
|
||
+ "M 12 19.2 L 12 22 M 6.6 18 L 5 20.4 M 17.4 18 L 19 20.4";
|
||
}
|
||
|
||
// TRACES: FR-UI-4
|
||
// Help: a question mark, drawn rather than typed for the reason at the
|
||
// top of this file. The dot is a zero-length stroke, which the round cap
|
||
// turns into a disc the weight of the hook above it.
|
||
if root.name == "help": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 8.2 8.6 C 8.2 6.2 9.9 4.6 12.1 4.6 C 14.3 4.6 15.9 6.1 15.9 8.1 "
|
||
+ "C 15.9 10.6 12.1 11.2 12.1 14.4 M 12.1 18.9 L 12.1 19";
|
||
}
|
||
|
||
// TRACES: FR-DEV-8
|
||
// A repair: two circles, one sampling into the other. That is literally
|
||
// what a spot is here — a source and a destination, which is why its
|
||
// handles on the canvas come in pairs — so the icon is the tool's own data
|
||
// rather than a metaphor borrowed from a plaster or a rubber stamp.
|
||
if root.name == "repair": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 4.6 14.8 A 5 5 0 1 0 14.6 14.8 A 5 5 0 1 0 4.6 14.8 Z "
|
||
+ "M 11.8 9 A 4 4 0 1 0 19.8 9 A 4 4 0 1 0 11.8 9 Z";
|
||
}
|
||
|
||
// TRACES: FR-DEV-6
|
||
// Presets: a stack of prints, the front one whole and two edges behind
|
||
// it. Not a tool — it arms nothing on the canvas — so it is the one
|
||
// drawing in the rail with no frame broken or marked: a saved edit is a
|
||
// finished photograph, several times over.
|
||
if root.name == "presets": Ink {
|
||
stroke: root.ink;
|
||
stroke-width: root.weight;
|
||
commands: "M 3.4 9.4 L 16.6 9.4 L 16.6 20.6 L 3.4 20.6 Z "
|
||
+ "M 6.2 6.4 L 19.4 6.4 L 19.4 17.6 "
|
||
+ "M 9 3.4 L 20.6 3.4 L 20.6 14.6";
|
||
}
|
||
}
|