Say what the application is doing, in one bar and one list
Every background job reported into a window property of its own — library-thumbs-done, library-pin-total, library-syncing — which only the grid ever read. A pin download that outlived the view it was started from drew nothing at all once the user opened an image, and there was no answer anywhere to "what is this busy with", because the answer was spread across eight properties nothing collected. They report to one register now (ui/dr-ui/src/activity.rs). It publishes an aggregate, which draws a three-pixel bar across the top of the shell in every view, and a row per job, which the settings page lists: scans, thumbnail batches, pin and open downloads, sidecar uploads, the sync and the trash. Failures stay on the list until they are cleared; routine successes do not, or a scroll would bury them. The handle removes a still-running job when it drops, so a worker that dies mid-transfer takes its row with it rather than leaving the bar sweeping for the rest of the session. Also carries in-flight work from a parallel session — the drawn icon set and the dr-pipeline ops split. dr-pipeline's build script does not compile at this commit; ui/dr-ui does, with clippy clean and its tests passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -585,19 +585,19 @@ export component GeometryPanel inherits Rectangle {
|
||||
clicked => { root.crop-toggled(!root.crop-mode); }
|
||||
}
|
||||
|
||||
// Rotation and flips. Glyphs rather than labels: four controls
|
||||
// Rotation and flips. Icons rather than labels: four controls
|
||||
// named in words would wrap the 280px column, and each of
|
||||
// these shows its own result.
|
||||
HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
IconButton {
|
||||
glyph: "⟲";
|
||||
icon: "rotate-ccw";
|
||||
enabled: root.enabled;
|
||||
clicked => { root.rotate(-1); }
|
||||
}
|
||||
IconButton {
|
||||
glyph: "⟳";
|
||||
icon: "rotate-cw";
|
||||
enabled: root.enabled;
|
||||
clicked => { root.rotate(1); }
|
||||
}
|
||||
@@ -605,13 +605,13 @@ export component GeometryPanel inherits Rectangle {
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
|
||||
IconButton {
|
||||
glyph: "⇔";
|
||||
icon: "flip-h";
|
||||
active: root.flip-h;
|
||||
enabled: root.enabled;
|
||||
clicked => { root.flip-h-toggled(); }
|
||||
}
|
||||
IconButton {
|
||||
glyph: "⇕";
|
||||
icon: "flip-v";
|
||||
active: root.flip-v;
|
||||
enabled: root.enabled;
|
||||
clicked => { root.flip-v-toggled(); }
|
||||
|
||||
+46
-6
@@ -2,11 +2,11 @@ import { Theme } from "theme.slint";
|
||||
import { AdjustPanel, GeometryPanel, ParamRow } from "adjust.slint";
|
||||
import { LaunchScreen } from "launch.slint";
|
||||
import { LibraryGrid, LibraryCell, TimelineBar } from "library.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState } from "widgets.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint";
|
||||
import { CollectionsPanel, CollectionRow } from "collections.slint";
|
||||
import { SettingsPage } from "settings.slint";
|
||||
|
||||
export { LibraryCell, TimelineBar, CollectionRow }
|
||||
export { LibraryCell, TimelineBar, CollectionRow, ActivityRow }
|
||||
|
||||
// Status strip — surfaces the GPU backend and adapter, which matters during
|
||||
// v0.1 because assumption A1 is exactly "does this compositing path work on
|
||||
@@ -274,11 +274,30 @@ export component AppWindow inherits Window {
|
||||
in property <bool> show-library: false;
|
||||
in-out property <[LibraryCell]> library-cells;
|
||||
in property <int> library-total: 0;
|
||||
// --- background activity (FR-CAT-1, FR-NC-6c) ---
|
||||
//
|
||||
// Every background job in one register, filled by `activity.rs`. The
|
||||
// aggregate drives the bar across the top of the shell; the rows are the
|
||||
// list on the settings page, where a stalled transfer can be identified
|
||||
// rather than merely felt.
|
||||
in property <[ActivityRow]> activity-rows;
|
||||
/// Anything at all running. Drives the bar's visibility, so it is a
|
||||
/// property rather than `activity-rows.length > 0` — finished rows stay in
|
||||
/// the list for a while and must not keep the bar alight.
|
||||
in property <bool> activity-busy: false;
|
||||
/// Whether the running jobs, taken together, have a denominator. A scan
|
||||
/// alone does not; a scan beside a download does.
|
||||
in property <bool> activity-determinate: false;
|
||||
in property <float> activity-fraction: 0;
|
||||
in property <int> activity-running: 0;
|
||||
/// Jobs that stopped and are being kept: finished ones as a short history,
|
||||
/// failed ones until they are cleared.
|
||||
in property <int> activity-kept: 0;
|
||||
callback activity-clear-finished();
|
||||
|
||||
in property <bool> library-scanning: false;
|
||||
in property <string> library-status: "";
|
||||
in property <string> library-error: "";
|
||||
in property <int> library-thumbs-done: 0;
|
||||
in property <int> library-thumbs-total: 0;
|
||||
|
||||
// --- offline mode (FR-CAT-9) ---
|
||||
//
|
||||
@@ -674,6 +693,11 @@ export component AppWindow inherits Window {
|
||||
strip-location-toggled(on) => { root.settings-strip-location-toggled(on); }
|
||||
destination-changed(t) => { root.settings-destination-changed(t); }
|
||||
|
||||
activity-rows: root.activity-rows;
|
||||
activity-running: root.activity-running;
|
||||
activity-kept: root.activity-kept;
|
||||
clear-finished() => { root.activity-clear-finished(); }
|
||||
|
||||
close() => { root.settings-close(); }
|
||||
reset-defaults() => { root.settings-reset(); }
|
||||
}
|
||||
@@ -773,8 +797,6 @@ export component AppWindow inherits Window {
|
||||
scanning: root.library-scanning;
|
||||
scan-status: root.library-status;
|
||||
scan-error: root.library-error;
|
||||
thumbs-done: root.library-thumbs-done;
|
||||
thumbs-total: root.library-thumbs-total;
|
||||
|
||||
offline: root.library-offline;
|
||||
offline-reason: root.library-offline-reason;
|
||||
@@ -1383,5 +1405,23 @@ export component AppWindow inherits Window {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- the load bar -------------------------------------------------
|
||||
//
|
||||
// Last in the file, so it is last in z-order and no view can cover it.
|
||||
//
|
||||
// At the top of the *shell* rather than inside a view, because that is
|
||||
// the only place that is true of every view: a pin download outlives
|
||||
// the grid it was started from, and until this existed it drew nothing
|
||||
// at all once the user opened an image. Three pixels, no text and no
|
||||
// hit area — it says only "something is running", and the settings
|
||||
// page says what (FR-CAT-1, FR-NC-6c).
|
||||
if root.activity-busy: ProgressBar {
|
||||
x: 0;
|
||||
y: 0;
|
||||
width: 100%;
|
||||
indeterminate: !root.activity-determinate;
|
||||
fraction: root.activity-fraction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
// release rather than the drop being silently discarded after it.
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Button } from "widgets.slint";
|
||||
import { Button, Icon } from "widgets.slint";
|
||||
|
||||
// One row of the collection tree.
|
||||
export struct CollectionRow {
|
||||
@@ -130,12 +130,12 @@ component TreeRow inherits Rectangle {
|
||||
Rectangle {
|
||||
width: 14px;
|
||||
|
||||
if root.entry.has-children: Text {
|
||||
text: root.entry.expanded ? "▾" : "▸";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
if root.entry.has-children: Icon {
|
||||
name: root.entry.expanded ? "chevron-down" : "chevron-right";
|
||||
ink: Theme.ink-faint;
|
||||
size: 10px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
// Its own hit area: toggling open must not also select, or every
|
||||
@@ -148,11 +148,11 @@ component TreeRow inherits Rectangle {
|
||||
// Smart collections read differently from manual ones — the icon is
|
||||
// the only cue that its contents are computed, and that dropping
|
||||
// images on it will be refused.
|
||||
Text {
|
||||
text: root.entry.smart ? "◈" : "▤";
|
||||
color: root.entry.smart ? Theme.active-dim : Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
vertical-alignment: center;
|
||||
Icon {
|
||||
name: root.entry.smart ? "collection-smart" : "collection";
|
||||
ink: root.entry.smart ? Theme.active-dim : Theme.ink-faint;
|
||||
size: 12px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
// The name, or the field that is replacing it while this row is being
|
||||
@@ -337,7 +337,7 @@ export component CollectionsPanel inherits Rectangle {
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
|
||||
// New collection. A glyph rather than a word: the header is 232px
|
||||
// New collection. A mark rather than a word: the header is 232px
|
||||
// wide and the label would crowd out the title.
|
||||
Rectangle {
|
||||
width: 22px;
|
||||
@@ -347,12 +347,12 @@ export component CollectionsPanel inherits Rectangle {
|
||||
: (add-touch.has-hover ? Theme.hover : transparent);
|
||||
border-radius: Theme.radius-sm;
|
||||
|
||||
Text {
|
||||
text: "+";
|
||||
color: Theme.ink-dim;
|
||||
font-size: Theme.text-lg;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
Icon {
|
||||
name: "plus";
|
||||
ink: Theme.ink-dim;
|
||||
size: 12px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
add-touch := TouchArea {
|
||||
clicked => { root.new-collection(); }
|
||||
@@ -494,11 +494,11 @@ export component CollectionsPanel inherits Rectangle {
|
||||
padding-right: Theme.gap-sm;
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
Text {
|
||||
text: "🗑";
|
||||
color: root.trash-count > 0 ? Theme.warn-ink : Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
vertical-alignment: center;
|
||||
Icon {
|
||||
name: "trash";
|
||||
ink: root.trash-count > 0 ? Theme.warn-ink : Theme.ink-faint;
|
||||
size: 12px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
Text {
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// 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
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Theme } from "theme.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, Field, Disclosure } from "widgets.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, Field, Disclosure, Icon } from "widgets.slint";
|
||||
|
||||
// Launch screen: connect an account, or resume a saved one.
|
||||
//
|
||||
@@ -41,18 +41,16 @@ component FormatCheck inherits Rectangle {
|
||||
border-color: root.checked ? Theme.active : Theme.rule;
|
||||
background: root.checked ? Theme.active : transparent;
|
||||
|
||||
Text {
|
||||
text: "✓";
|
||||
Icon {
|
||||
name: "check";
|
||||
// Dark on the fill: `active` is near-white, and the white
|
||||
// tick this carried against a saturated accent is invisible
|
||||
// against it.
|
||||
color: Theme.ground;
|
||||
font-size: 12px;
|
||||
ink: Theme.ground;
|
||||
size: 11px;
|
||||
visible: root.checked;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +87,7 @@ component FolderRow inherits Rectangle {
|
||||
padding-right: Theme.gap;
|
||||
spacing: Theme.gap;
|
||||
|
||||
Disclosure { text: root.is-parent ? "↑" : "▸"; }
|
||||
Disclosure { icon: root.is-parent ? "arrow-up" : "chevron-right"; }
|
||||
Value { text: root.label; overflow: elide; }
|
||||
}
|
||||
}
|
||||
@@ -261,7 +259,7 @@ export component LaunchScreen inherits Rectangle {
|
||||
}
|
||||
|
||||
Caption {
|
||||
text: "Create one in Nextcloud under Settings → Security → Devices & sessions. It is device-scoped and can be revoked on its own.";
|
||||
text: "Create one in Nextcloud under Settings › Security › Devices & sessions. It is device-scoped and can be revoked on its own.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
+57
-105
@@ -9,7 +9,7 @@
|
||||
// must not look identical (FR-NC-6c).
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Button, IconButton, Label, Value, Caption, EmptyState, FilterChip } from "widgets.slint";
|
||||
import { Button, IconButton, Label, Value, Caption, EmptyState, FilterChip, ProgressBar, Icon } from "widgets.slint";
|
||||
|
||||
// One bar of the capture-time histogram.
|
||||
export struct TimelineBar {
|
||||
@@ -259,52 +259,11 @@ export struct LibraryCell {
|
||||
flag: int,
|
||||
}
|
||||
|
||||
// A horizontal progress bar with two modes.
|
||||
//
|
||||
// Determinate where a real denominator exists (thumbnails: we know how many
|
||||
// cells we asked for). Indeterminate where one does not — a directory walk
|
||||
// discovers its own extent, so any percentage would be invented, and inventing
|
||||
// one is worse than admitting the work is unbounded.
|
||||
component ProgressBar inherits Rectangle {
|
||||
in property <float> fraction: 0;
|
||||
in property <bool> indeterminate: false;
|
||||
|
||||
height: 3px;
|
||||
background: Theme.rule;
|
||||
|
||||
// Determinate: a bar proportional to real progress.
|
||||
Rectangle {
|
||||
x: 0;
|
||||
width: parent.width * clamp(root.fraction, 0, 1);
|
||||
height: parent.height;
|
||||
background: Theme.active-dim;
|
||||
visible: !root.indeterminate;
|
||||
}
|
||||
|
||||
// Indeterminate: a sweep that says "working" without claiming a position.
|
||||
Rectangle {
|
||||
width: parent.width * 25%;
|
||||
height: parent.height;
|
||||
background: Theme.active-dim;
|
||||
visible: root.indeterminate;
|
||||
|
||||
x: root.indeterminate ? -self.width : 0;
|
||||
animate x {
|
||||
duration: 1200ms;
|
||||
iteration-count: -1;
|
||||
easing: ease-in-out;
|
||||
}
|
||||
states [
|
||||
running when root.indeterminate: { x: parent.width; }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// A row of five stars, readable at a glance and clickable to set a rating.
|
||||
//
|
||||
// **Filled versus empty carries the meaning, not colour.** NFR-A11Y-3 forbids
|
||||
// status by hue alone, and the palette is achromatic anyway — so a set star is
|
||||
// a solid glyph at `active` and an unset one is an outline at `ink-faint`. The
|
||||
// a solid star at `active` and an unset one is an outline at `ink-faint`. The
|
||||
// two differ in both shape and luminance, which survives greyscale and low
|
||||
// vision alike.
|
||||
//
|
||||
@@ -343,7 +302,7 @@ export component StarStrip inherits Rectangle {
|
||||
// Separation between the trash target and ★1.
|
||||
//
|
||||
// **This gap is load-bearing.** The star targets are 44px over a 22px
|
||||
// glyph, so they deliberately overlap and a near-miss lands one star out —
|
||||
// cell, so they deliberately overlap and a near-miss lands one star out —
|
||||
// harmless, same control, corrected by clicking again. That reasoning does
|
||||
// not survive a neighbour that *moves a file*, so trash is held off the
|
||||
// scale by a gap wider than the overhang it would otherwise share with
|
||||
@@ -373,18 +332,16 @@ export component StarStrip inherits Rectangle {
|
||||
height: root.star;
|
||||
visible: root.can-trash;
|
||||
|
||||
Text {
|
||||
text: "🗑";
|
||||
Icon {
|
||||
name: "trash";
|
||||
// Reject and trash are the two destructive ends of this UI and
|
||||
// share the palette's one hue, so the gesture reads the same
|
||||
// in both places (NFR-A11Y-3: the glyph carries it, not the
|
||||
// in both places (NFR-A11Y-3: the shape carries it, not the
|
||||
// colour).
|
||||
color: Theme.warn-ink;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
ink: Theme.warn-ink;
|
||||
size: 13px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
TouchArea {
|
||||
@@ -411,18 +368,16 @@ export component StarStrip inherits Rectangle {
|
||||
width: root.star;
|
||||
height: root.star;
|
||||
|
||||
Text {
|
||||
Icon {
|
||||
// Solid versus outline: the shape says it, not the colour.
|
||||
text: root.rating >= n ? "★" : "☆";
|
||||
color: root.rating >= n ? Theme.active : Theme.ink-dim;
|
||||
// Large enough to hit the difference between ★ and ☆ at arm's
|
||||
// length on a tablet; the glyphs differ in fill, which needs
|
||||
// more pixels to read than a difference in shape would.
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
name: root.rating >= n ? "star" : "star-outline";
|
||||
ink: root.rating >= n ? Theme.active : Theme.ink-dim;
|
||||
// Large enough to hit the difference between filled and empty
|
||||
// at arm's length on a tablet; the two differ in fill, which
|
||||
// needs more pixels to read than a difference in shape would.
|
||||
size: 14px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
// Grown past the drawn star to meet FR-UI-3's 44pt minimum, and
|
||||
@@ -453,10 +408,11 @@ export component StarStrip inherits Rectangle {
|
||||
|
||||
// The pick/reject mark.
|
||||
//
|
||||
// A glyph rather than a colour, for the same NFR-A11Y-3 reason as the stars:
|
||||
// ✓ and ✗ are distinguishable without hue, and a reject reads as a reject in
|
||||
// greyscale. A reject also dims its whole cell, which is the cue that carries
|
||||
// at grid scale — the glyph is confirmation, not the primary signal.
|
||||
// A shape rather than a colour, for the same NFR-A11Y-3 reason as the stars:
|
||||
// a tick and a cross are distinguishable without hue, and a reject reads as a
|
||||
// reject in greyscale. A reject also dims its whole cell, which is the cue
|
||||
// that carries at grid scale — the mark is confirmation, not the primary
|
||||
// signal.
|
||||
export component FlagMark inherits Rectangle {
|
||||
in property <int> flag: 0;
|
||||
|
||||
@@ -467,17 +423,14 @@ export component FlagMark inherits Rectangle {
|
||||
background: Theme.surface;
|
||||
opacity: 0.92;
|
||||
|
||||
Text {
|
||||
text: root.flag == 2 ? "✗" : "✓";
|
||||
Icon {
|
||||
name: root.flag == 2 ? "cross" : "check";
|
||||
// Reject earns the one hue in the palette: it is the destructive end
|
||||
// of the axis and the thing a user must not mistake for a pick.
|
||||
color: root.flag == 2 ? Theme.warn-ink : Theme.active;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
ink: root.flag == 2 ? Theme.warn-ink : Theme.active;
|
||||
size: 9px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,10 +488,9 @@ export component LibraryGrid inherits Rectangle {
|
||||
/// column-count change. Rust resizes the loaded window to match.
|
||||
callback capacity-changed(int);
|
||||
|
||||
// Thumbnail progress. Unlike the scan, this has a real denominator — the
|
||||
// number of cells we asked for — so the bar can be honest about position.
|
||||
in property <int> thumbs-done: 0;
|
||||
in property <int> thumbs-total: 0;
|
||||
// Thumbnail progress is no longer reported here: the batch belongs to a
|
||||
// window of the grid rather than to anything the user asked for, and the
|
||||
// shell's bar now carries it along with every other running job.
|
||||
|
||||
// Whole-library indexing, which runs for far longer than one window's
|
||||
// thumbnails and is reported separately so the two do not fight over the
|
||||
@@ -549,9 +501,6 @@ export component LibraryGrid inherits Rectangle {
|
||||
in property <bool> syncing: false;
|
||||
property <bool> sweeping: root.sweep-total > 0 && root.sweep-done < root.sweep-total;
|
||||
|
||||
property <bool> thumbs-running: root.thumbs-total > 0
|
||||
&& root.thumbs-done < root.thumbs-total;
|
||||
|
||||
callback cell-clicked(int);
|
||||
/// A star was clicked on a cell: row, and the rating 0..5.
|
||||
callback cell-rated(int, int);
|
||||
@@ -743,7 +692,7 @@ export component LibraryGrid inherits Rectangle {
|
||||
// interface with a collapsible sidebar puts one, and the only
|
||||
// place that stays put whichever way the panel is.
|
||||
IconButton {
|
||||
glyph: "☰";
|
||||
icon: "menu";
|
||||
active: root.collections-visible;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.toggle-collections(); }
|
||||
@@ -825,7 +774,10 @@ export component LibraryGrid inherits Rectangle {
|
||||
// much more expensive request, and a button that meant either
|
||||
// depending on invisible state would be a trap.
|
||||
if root.scope-label != "": Button {
|
||||
text: root.scope-pinned ? "Pinned ✓" : "Pin offline";
|
||||
// No tick on the pinned label: `active` already inverts
|
||||
// the button, which says the same thing without a symbol
|
||||
// inside a string (see icons.slint).
|
||||
text: root.scope-pinned ? "Pinned" : "Pin offline";
|
||||
active: root.scope-pinned;
|
||||
enabled: !root.scanning;
|
||||
y: (parent.height - self.height) / 2;
|
||||
@@ -908,7 +860,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
}
|
||||
|
||||
for n[i] in [1, 2, 3, 4, 5]: FilterChip {
|
||||
label: "★" + n + "+";
|
||||
icon: "star";
|
||||
label: n + "+";
|
||||
count: root.rating-counts.length > n ? root.rating-counts[n] : -1;
|
||||
active: root.filter-min-rating == n;
|
||||
y: (parent.height - self.height) / 2;
|
||||
@@ -923,7 +876,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
Rectangle { width: Theme.gap; }
|
||||
|
||||
FilterChip {
|
||||
label: "✓ Picks";
|
||||
icon: "check";
|
||||
label: "Picks";
|
||||
active: root.filter-flag == 1;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
@@ -932,7 +886,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
}
|
||||
|
||||
FilterChip {
|
||||
label: "✗ Rejects";
|
||||
icon: "cross";
|
||||
label: "Rejects";
|
||||
active: root.filter-flag == 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
@@ -978,23 +933,20 @@ export component LibraryGrid inherits Rectangle {
|
||||
|
||||
// --- progress -----------------------------------------------------
|
||||
//
|
||||
// Indeterminate during the scan: a directory walk cannot know its own
|
||||
// extent, so a percentage would be fiction. Determinate for
|
||||
// thumbnails, where the denominator is the cells we requested.
|
||||
if root.scanning || root.thumbs-running || root.sweeping: ProgressBar {
|
||||
// Only the sweep has a denominator worth showing. A scan cannot
|
||||
// know its extent, and the window's own fetches are sized by the
|
||||
// viewport rather than by anything meaningful to the user.
|
||||
indeterminate: root.scanning || (!root.sweeping && root.thumbs-running);
|
||||
fraction: root.sweep-total > 0
|
||||
? root.sweep-done / root.sweep-total
|
||||
: 0;
|
||||
}
|
||||
// The bar that used to sit here is now the shell's, drawn across the
|
||||
// top of every view from the activity register (see `activity.rs`).
|
||||
// Two reasons it moved. It only ever knew about the three things the
|
||||
// grid happens to report — a download running while the user was in
|
||||
// develop drew nothing anywhere — and a second bar here would now say
|
||||
// the same thing twice, one line apart.
|
||||
//
|
||||
// The grid keeps its *words*: "indexing 4000 / 17000" in the header
|
||||
// above says which work is running, which a bar cannot.
|
||||
|
||||
// --- pinning ------------------------------------------------------
|
||||
//
|
||||
// Its own line rather than the shared progress bar above: that one is
|
||||
// driven by the scan and the sweep, and a pin runs alongside both.
|
||||
// Its own line, with the count spelled out: the shell's bar says that
|
||||
// something is transferring, and this says how much of what.
|
||||
if root.pin-total > 0: Rectangle {
|
||||
height: 34px;
|
||||
background: Theme.surface;
|
||||
@@ -1464,8 +1416,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
// hidden: the cull is reversible, and a photo
|
||||
// that vanished on one keypress would make the
|
||||
// gesture frightening to use. Dimming is the
|
||||
// cue that reads at grid scale — the ✗ glyph
|
||||
// confirms it up close.
|
||||
// cue that reads at grid scale — the cross on
|
||||
// the mark confirms it up close.
|
||||
opacity: cell.lifted ? 0.25
|
||||
: (cell.flag == 2 ? 0.4 : 1.0);
|
||||
animate opacity { duration: 120ms; }
|
||||
|
||||
+122
-9
@@ -1,5 +1,5 @@
|
||||
import { Theme } from "theme.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, Field } from "widgets.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, Field, ProgressBar, ActivityRow, Icon } from "widgets.slint";
|
||||
|
||||
// Settings: how much disk the app may spend, and what an export defaults to.
|
||||
//
|
||||
@@ -146,15 +146,13 @@ component Switch inherits Rectangle {
|
||||
border-color: root.checked ? Theme.active : Theme.rule;
|
||||
background: root.checked ? Theme.active : transparent;
|
||||
|
||||
Text {
|
||||
text: "✓";
|
||||
color: Theme.ground;
|
||||
font-size: 12px;
|
||||
Icon {
|
||||
name: "check";
|
||||
ink: Theme.ground;
|
||||
size: 11px;
|
||||
visible: root.checked;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +236,60 @@ component EntryRow inherits VerticalLayout {
|
||||
}
|
||||
}
|
||||
|
||||
// One background job: what it is, how far along, and what it last said.
|
||||
//
|
||||
// A row rather than a `Value`/`Caption` pair written out at the call site,
|
||||
// because the list is built by a `for` and every row has to answer the same
|
||||
// three questions in the same order — otherwise a failure reads as a different
|
||||
// kind of thing from the job it happened to.
|
||||
//
|
||||
// The bar is drawn only while the job runs. A stopped job's fraction is either
|
||||
// 1 (it finished, and a full bar is noise) or frozen part-way (it failed, and
|
||||
// a bar sitting at 60% invites the reading "still going").
|
||||
component ActivityItem inherits VerticalLayout {
|
||||
in property <ActivityRow> row;
|
||||
|
||||
spacing: 4px;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: Theme.gap;
|
||||
|
||||
Label {
|
||||
text: root.row.title;
|
||||
body: true;
|
||||
// Faded once it is over: the list is ordered running-first, and
|
||||
// this is what makes that ordering visible at a glance rather than
|
||||
// something the reader has to work out from the text.
|
||||
opacity: root.row.running ? 1.0 : 0.6;
|
||||
}
|
||||
|
||||
Caption {
|
||||
text: root.row.detail;
|
||||
// A failure is the one thing here worth a colour (NFR-A11Y-3 is
|
||||
// satisfied by the words, which say what failed; the hue only
|
||||
// finds them faster).
|
||||
warn: root.row.failed;
|
||||
horizontal-alignment: right;
|
||||
horizontal-stretch: 1;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
|
||||
if root.row.running: ProgressBar {
|
||||
indeterminate: !root.row.determinate;
|
||||
fraction: root.row.fraction;
|
||||
}
|
||||
}
|
||||
|
||||
export component SettingsPage inherits Rectangle {
|
||||
// --- background activity --------------------------------------------
|
||||
in property <[ActivityRow]> activity-rows;
|
||||
in property <int> activity-running: 0;
|
||||
/// Stopped jobs still listed: recent successes, and failures held until
|
||||
/// they are read.
|
||||
in property <int> activity-kept: 0;
|
||||
callback clear-finished();
|
||||
|
||||
// --- cache ---------------------------------------------------------
|
||||
in-out property <string> original-budget;
|
||||
in property <bool> original-unlimited: false;
|
||||
@@ -388,6 +439,68 @@ export component SettingsPage inherits Rectangle {
|
||||
// `min` so a narrow window still uses what it has (FR-UI-1).
|
||||
property <length> column: min(root.width - 2 * Theme.gap-lg, 680px);
|
||||
|
||||
// --- background activity ---------------------------------
|
||||
//
|
||||
// First on the page, and the only panel here that is not a
|
||||
// preference. It earns the position by being the one thing
|
||||
// that changes while the page is open: a transfer the user
|
||||
// came here to check on is answered before they have scrolled.
|
||||
Rectangle {
|
||||
width: content.column;
|
||||
height: activity.preferred-height;
|
||||
|
||||
activity := Panel {
|
||||
width: 100%;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: Theme.gap;
|
||||
|
||||
PanelHeading { text: "BACKGROUND ACTIVITY"; }
|
||||
|
||||
// The count beside the heading, because the list
|
||||
// below carries stopped jobs too and "how many are
|
||||
// running" should not have to be counted by eye.
|
||||
Caption {
|
||||
text: root.activity-running > 0
|
||||
? root.activity-running + " running" : "";
|
||||
horizontal-alignment: right;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
}
|
||||
|
||||
Caption {
|
||||
text: "Scans, transfers and syncs running behind the "
|
||||
+ "interface. Everything here survives leaving "
|
||||
+ "this page; nothing here needs it open.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
Rectangle { height: Theme.gap-sm; }
|
||||
|
||||
// Says nothing is running, rather than showing an empty
|
||||
// box: a list that is empty because the work finished
|
||||
// and one that is empty because the register is broken
|
||||
// look identical otherwise.
|
||||
if root.activity-rows.length == 0: Caption {
|
||||
text: "Nothing running.";
|
||||
}
|
||||
|
||||
for row in root.activity-rows: ActivityItem {
|
||||
row: row;
|
||||
}
|
||||
|
||||
if root.activity-kept > 0: Rectangle {
|
||||
height: Theme.control-height;
|
||||
|
||||
Button {
|
||||
x: 0;
|
||||
text: "Clear finished";
|
||||
clicked => { root.clear-finished(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- storage ---------------------------------------------
|
||||
Rectangle {
|
||||
width: content.column;
|
||||
|
||||
+127
-23
@@ -14,6 +14,9 @@
|
||||
// colour reached forty call sites with no single place to change it.
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Icon } from "icons.slint";
|
||||
|
||||
export { Icon }
|
||||
|
||||
// A text button.
|
||||
//
|
||||
@@ -36,7 +39,7 @@ export component Button inherits Rectangle {
|
||||
/// emphasis stops meaning anything (see the theme preamble).
|
||||
in property <bool> primary: false;
|
||||
/// Sustained state — a toggle that is currently on, not a press. The same
|
||||
/// meaning [`IconButton`] gives it, so a labelled toggle and a glyph
|
||||
/// meaning [`IconButton`] gives it, so a labelled toggle and an icon
|
||||
/// toggle read alike.
|
||||
in property <bool> active: false;
|
||||
|
||||
@@ -97,12 +100,13 @@ export component Button inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// A square button carrying a single glyph.
|
||||
// A square button carrying a single icon.
|
||||
//
|
||||
// Square because it has no label to size against: a toolbar affordance whose
|
||||
// width tracked its glyph would jitter as the glyph changed.
|
||||
// width tracked its drawing would jitter as the drawing changed.
|
||||
export component IconButton inherits Rectangle {
|
||||
in property <string> glyph;
|
||||
/// A name from the [`Icon`] vocabulary.
|
||||
in property <string> icon;
|
||||
in property <bool> enabled: true;
|
||||
/// Sustained state — a toggle that is currently on, not a press.
|
||||
in property <bool> active: false;
|
||||
@@ -131,14 +135,15 @@ export component IconButton inherits Rectangle {
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.glyph;
|
||||
color: root.active ? Theme.active : Theme.ink;
|
||||
font-size: Theme.text;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
Icon {
|
||||
name: root.icon;
|
||||
ink: root.active ? Theme.active : Theme.ink;
|
||||
// Half the button, near enough: the icon carries the whole meaning of
|
||||
// the control, so it wants more of the square than a glyph set at
|
||||
// body size used to take, but it must not touch the border.
|
||||
size: root.height / 2;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +169,11 @@ export component FilterChip inherits Rectangle {
|
||||
in property <bool> active: false;
|
||||
/// Images behind this term, or -1 where the count is not known.
|
||||
in property <int> count: -1;
|
||||
/// A mark before the label, naming what the term filters on — a star for a
|
||||
/// rating, a tick for picks. Empty for chips whose word says it already.
|
||||
/// Drawn rather than prefixed to `label`, because a symbol inside a string
|
||||
/// is exactly the thing [`Icon`] exists to stop (see icons.slint).
|
||||
in property <string> icon;
|
||||
|
||||
callback clicked();
|
||||
|
||||
@@ -195,6 +205,16 @@ export component FilterChip inherits Rectangle {
|
||||
padding-right: Theme.gap-sm;
|
||||
spacing: 4px;
|
||||
|
||||
if root.icon != "": Icon {
|
||||
name: root.icon;
|
||||
// Takes the label's colour, including the inversion on an active
|
||||
// chip: a mark that stayed light on the near-white fill would be
|
||||
// the one invisible thing in the row.
|
||||
ink: root.active ? Theme.ground : Theme.ink;
|
||||
size: 11px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.label;
|
||||
// Dark on the active fill, which is near-white — the same
|
||||
@@ -221,17 +241,26 @@ export component FilterChip inherits Rectangle {
|
||||
// The arrow beside a row that opens into something: a disclosure triangle on
|
||||
// a section, an "into this folder" marker in the picker.
|
||||
//
|
||||
// **Fixed width, and that is the whole point.** The glyphs differ in advance
|
||||
// width, so a row that sized to its own arrow would shift its label sideways
|
||||
// as it opened and closed — the one movement that makes a static list look
|
||||
// like it is being redrawn. Rotating a single glyph would need a transform on
|
||||
// a Text; two characters render identically and cost nothing.
|
||||
export component Disclosure inherits Text {
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
vertical-alignment: center;
|
||||
horizontal-alignment: center;
|
||||
// **Fixed width, and that is the whole point.** The drawings differ in extent,
|
||||
// so a row that sized to its own arrow would shift its label sideways as it
|
||||
// opened and closed — the one movement that makes a static list look like it
|
||||
// is being redrawn. The box stays 14px whatever is in it, and the icon centres
|
||||
// inside.
|
||||
export component Disclosure inherits Rectangle {
|
||||
/// A name from the [`Icon`] vocabulary — `chevron-right` closed,
|
||||
/// `chevron-down` open, or `arrow-up` for a row that leads back out.
|
||||
in property <string> icon: "chevron-right";
|
||||
|
||||
width: 14px;
|
||||
horizontal-stretch: 0;
|
||||
|
||||
Icon {
|
||||
name: root.icon;
|
||||
ink: Theme.ink-faint;
|
||||
size: 10px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// A collapsible group with a header that reports whether anything inside has
|
||||
@@ -305,7 +334,9 @@ export component Section inherits Rectangle {
|
||||
padding-right: Theme.gap-sm;
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
Disclosure { text: root.expanded ? "▾" : "▸"; }
|
||||
Disclosure {
|
||||
icon: root.expanded ? "chevron-down" : "chevron-right";
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.title;
|
||||
@@ -609,6 +640,79 @@ export component Field inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// A horizontal progress bar with two modes.
|
||||
//
|
||||
// Determinate where a real denominator exists (thumbnails: we know how many
|
||||
// cells we asked for). Indeterminate where one does not — a directory walk
|
||||
// discovers its own extent, so any percentage would be invented, and inventing
|
||||
// one is worse than admitting the work is unbounded.
|
||||
//
|
||||
// Lives here rather than in library.slint, where it started, because the grid
|
||||
// is no longer the only view that reports progress: the shell draws one across
|
||||
// the top of every view and the settings page draws one per running job.
|
||||
export component ProgressBar inherits Rectangle {
|
||||
in property <float> fraction: 0;
|
||||
in property <bool> indeterminate: false;
|
||||
|
||||
height: 3px;
|
||||
background: Theme.rule;
|
||||
// The sweep below is positioned outside these bounds for half its cycle.
|
||||
// Without clipping it paints over whatever sits beside the bar, which at
|
||||
// the top of the shell is the whole window.
|
||||
clip: true;
|
||||
|
||||
// Determinate: a bar proportional to real progress.
|
||||
Rectangle {
|
||||
x: 0;
|
||||
width: parent.width * clamp(root.fraction, 0, 1);
|
||||
height: parent.height;
|
||||
background: Theme.active-dim;
|
||||
visible: !root.indeterminate;
|
||||
}
|
||||
|
||||
// Indeterminate: a sweep that says "working" without claiming a position.
|
||||
Rectangle {
|
||||
width: parent.width * 25%;
|
||||
height: parent.height;
|
||||
background: Theme.active-dim;
|
||||
visible: root.indeterminate;
|
||||
|
||||
x: root.indeterminate ? -self.width : 0;
|
||||
animate x {
|
||||
duration: 1200ms;
|
||||
iteration-count: -1;
|
||||
easing: ease-in-out;
|
||||
}
|
||||
states [
|
||||
running when root.indeterminate: { x: parent.width; }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// One background job, as the interface sees it.
|
||||
//
|
||||
// Built in Rust by `activity.rs`, which is the only thing that knows a scan
|
||||
// from a download. Everything here is already a sentence or a number ready to
|
||||
// draw: the page decides where a row goes, never what it means.
|
||||
export struct ActivityRow {
|
||||
// "Scanning Photos", "Keeping 40 photographs offline".
|
||||
title: string,
|
||||
// Whatever the job last said about itself — counts, a byte figure, or the
|
||||
// error where it failed.
|
||||
detail: string,
|
||||
// 0..1, meaningless unless `determinate`.
|
||||
fraction: float,
|
||||
// Whether this job knows its own extent. A scan does not.
|
||||
determinate: bool,
|
||||
running: bool,
|
||||
// Kept apart from `running`: a finished job and a failed one are both
|
||||
// stopped, and only one of them is worth the user's attention.
|
||||
failed: bool,
|
||||
// Moves bytes over the network, so it is one of the "transfers" the user
|
||||
// asks about when the connection is slow (FR-NC-6c).
|
||||
transfer: bool,
|
||||
}
|
||||
|
||||
// What a view says when it has nothing to show.
|
||||
//
|
||||
// Not in the S3 brief, but `app.slint` and `library.slint` had the same two
|
||||
|
||||
Reference in New Issue
Block a user