The first build of seeing a mask showed the selected layer's, in one global style, from a strip at the top of the panel. It answered the wrong question and answered it somewhere nobody looked. What a photographer asks of two masks is how they meet — where the sky's edge sits against the building's — and that needs both on screen at once, in colours that can be told apart. So each row of the stack has an eye, drawn in the colour its mask is shown in, and each mask has six swatches to choose that colour from. Several can be open at once; a new one comes up open, in the first colour nothing else is using. The style — tint, alpha, outline — is the one setting that stays global, above the stack, because three styles at once are three pictures that cannot be read against each other. Alpha now draws every shown mask, each in its colour, on black. In the pipeline a `Reveal` is a list of `(layer, colour)` rather than one layer, and every reveal block carries its own colour. The brush moves too. Select, Paint and Erase and the three sliders under them sat at the top of the panel, appeared only once a row was selected, and said nothing about which mask they acted on — so "how do I paint" and "how do I correct the model's outline" both had the same answer and nobody found it. They sit under the selected mask's parts now, beside the swatches, and on a subject or a category the hint says what a stroke there does: it becomes a part of this mask, joined to the model's, and can be taken out again. Eyes and colours are viewing state, on the session and not on the layer, so a photograph reopened has every eye closed — the stored-mask round-trip test asserts it.
378 lines
15 KiB
Plaintext
378 lines
15 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
|
||
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-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";
|
||
}
|
||
}
|