// The develop view's own chrome: the strip across the top of the window and // the capture-metadata block in the column beside the canvas. // // Split out of `app.slint`, where they sat above `AppWindow` and were read as // part of the application shell. They are not: neither is instantiated // anywhere but the develop view, and the shell's job — choosing which of the // five screens is up — is easier to read without two unrelated components in // front of it. import { Theme } from "theme.slint"; import { Develop } from "session.slint"; import { Transfer } from "adjust.slint"; import { Button, PanelHeading, Label, Value, Caption, Panel } from "widgets.slint"; import { Labels, LabelMark, LabelPicker } from "labels.slint"; import { StarStrip } from "library.slint"; // Status strip — surfaces the GPU backend and adapter, which matters during // v0.1 because assumption A1 is exactly "does this compositing path work on // this hardware". Seeing the adapter at a glance makes vendor differences // obvious during the S1/S2 spikes. export component StatusBar inherits Rectangle { in property adapter; in property backend; in property layout-class; in property fps; /// FR-UI-1: the layout class, so the instrumentation can stand down on a /// strip too narrow to carry both it and the controls. in property expanded: true; in property filename; in property position; /// Only offered where there is a library to go back to — with files named /// on the command line there is no grid behind this view. in property can-return-to-library: false; /// Whether the develop column is currently shown, for the toggle's label. in property panel-visible: true; /// What the export button says. Rust owns it because the answer depends /// on settings this component does not see — the format, and whether the /// destination is this device or the server. in property export-label: "Export"; in property export-busy: false; /// What the last export did. Sits beside the button rather than in a /// dialogue: an export that succeeded needs no acknowledging, and one /// that failed needs its reason where the retry is. in property export-status; /// Whether the edit can be stepped either way (FR-DEV-5). in property can-undo: false; in property can-redo: false; /// TRACES: FR-CAT-5 /// The open photograph's colour label code, 0 for none, -1 where there /// is no library to hold one (files named on the command line). in property colour-label: -1; /// Whether the label choices are open in the strip. property picking-label: false; callback label-picked(int); /// TRACES: FR-UI-5 | FR-CULL-4 /// The open photograph's stars, or -1 where there is no library to hold /// them, and its flag: 0 none, 1 pick, 2 reject. in property rating: -1; in property flag: 0; callback rated(int); callback flagged(int); callback back-to-library(); callback open-people(); callback open-settings(); /// TRACES: FR-UI-4 /// Open the gesture reference, which the window draws. callback open-help(); callback toggle-panel(); callback export-image(); callback undo(); callback redo(); // 44px and `surface`, the same bar the library and settings draw. // // It was 28px — the control height — which was a reasonable size for a // readout and the wrong size for the thing it actually is: this strip // carries the only way out of develop, and it was half the height of the // identical control on the two screens either side of it. Three screens // whose headers do not agree read as three applications, and the one place // the user needs to find a way back was the one drawn smallest. // // Controls keep their own 28px and are centred in it, exactly as the // library header centres the same buttons: a `Button` sets a fixed height, // which a HorizontalLayout parks at the top of the row rather than // centring for it. height: 44px; background: Theme.surface; // Scrolls rather than overflowing. A layout given less width than its // children need does not shrink them — it overruns the edge and reports // the oversized minimum to whatever contains it. Collapsing the // instrumentation below buys back about 200 logical pixels, which is // enough for a tablet; it is not enough for a phone, and nothing here // should be unreachable merely because the window is narrow. Flickable { width: 100%; height: 100%; viewport-height: self.height; viewport-width: max(self.width, strip.preferred-width); strip := HorizontalLayout { width: parent.viewport-width; height: parent.viewport-height; padding-left: Theme.gap; padding-right: Theme.gap; spacing: Theme.gap; alignment: start; // Kept instantiated with `visible` rather than wrapped in an `if`: // the strip is inside the layout that `expanded` feeds, and adding a // conditional child here is the shape that has already caused binding // loops in this file (see the panel below). Button { text: "‹ Library"; y: (parent.height - self.height) / 2; visible: root.can-return-to-library; clicked => { root.back-to-library(); } } // The third screen, reachable from the second. Beside the way back // because it is the same kind of move — leaving this photograph for // somewhere else in the library — and because a mode you can only // reach from one of the other two is not a peer of them, whatever the // navigation model says. Button { text: "Identity"; y: (parent.height - self.height) / 2; visible: root.can-return-to-library; clicked => { root.open-people(); } } Value { text: root.filename; compact: true; overflow: elide; } Caption { text: root.position; } // TRACES: FR-CAT-5 | FR-UI-4 | NFR-A11Y-3 // The label, shown and set here. The button says the label's name // beside its mark, so the strip reads "Label: Green" in words; with // none it offers "Label". Pressing it opens the six choices in the // strip, which close on the one chosen. Button { text: root.colour-label > 0 ? "Label: " + Labels.names[root.colour-label] : "Label"; active: root.picking-label; visible: root.colour-label >= 0; y: (parent.height - self.height) / 2; clicked => { root.picking-label = !root.picking-label; } } if root.colour-label > 0: LabelMark { code: root.colour-label; y: (parent.height - self.height) / 2; } if root.picking-label && root.colour-label >= 0: LabelPicker { current: root.colour-label; chosen(code) => { root.label-picked(code); root.picking-label = false; } } // TRACES: FR-UI-5 | FR-CULL-4 | NFR-A11Y-3 // The judgement, shown and set on the photograph that is open — the // pointer and touch half of the rating and flag keys, which a tablet // has no number row for (FR-UI-5, amended 2026-09-19). The grid's own // stars, so a rating looks the same in both places; the star already // set clears it, as there. StarStrip { rating: max(0, root.rating); show-empty: true; can-trash: false; visible: root.rating >= 0; y: (parent.height - self.height) / 2; rate(n) => { root.rated(n); } } // Pressed again, a flag comes off: two buttons that each toggle, // rather than a third for "none" that is only ever pressed to undo // one of the other two. Button { text: root.flag == 1 ? "Picked" : "Pick"; active: root.flag == 1; visible: root.rating >= 0; y: (parent.height - self.height) / 2; clicked => { root.flagged(root.flag == 1 ? 0 : 1); } } Button { text: root.flag == 2 ? "Rejected" : "Reject"; active: root.flag == 2; visible: root.rating >= 0; y: (parent.height - self.height) / 2; clicked => { root.flagged(root.flag == 2 ? 0 : 2); } } Rectangle { horizontal-stretch: 1; } // --- instrumentation lives in Settings now ------------------------ // // The render backend, the layout class and the frame rate are // developer readouts. They cost about 200 logical pixels of a strip // that also carries the only way out of develop, the undo pair, the // panel toggle and the export button — and a `HorizontalLayout` given // less width than its children's minimums does not shrink them, it // runs off the end. // // A tablet in portrait is 768 logical pixels and was already below the // breakpoint that hid them, so it kept its header. Landscape is 1200 // and is not, so the readouts came back and the header ran off the // right-hand edge there instead — a breakpoint that hides a problem at // one size and not another is a workaround, not a fix. // // The backend, the frame rate and the layout class used to live here. // They are diagnostics — read once when something looks wrong, and // never again — and as permanent furniture in a 44px strip they cost // three slots that the controls beside them actually needed. On a // tablet in landscape the header ran off the right-hand edge, and this // was a third of the reason. // // They are in Settings under ABOUT now, next to the version, which is // where someone goes when they have a bug to report rather than a // photograph to edit. // Undo and redo, in the strip rather than in the develop column: the // panel can be put away, and the one control that takes back a // mis-drag must not go away with it. Disabled rather than hidden, so // the pair keeps its place and the keyboard shortcut has something // visible to correspond to. Button { text: "Undo"; enabled: root.can-undo; y: (parent.height - self.height) / 2; clicked => { root.undo(); } } Button { text: "Redo"; enabled: root.can-redo; y: (parent.height - self.height) / 2; clicked => { root.redo(); } } // TRACES: FR-DEV-6 // Copy and paste, in the strip beside undo for the reason undo is // here: they act on the whole edit, not on any one panel, and the // column can be put away. They sat in the column under a "SETTINGS" // heading, which read as application settings and hid them behind a // scroll and behind the panel toggle. // // The paste button names what would be pasted — "3 adjustments", and // whether the crop is staying behind — because "Paste" alone asks the // user to remember what they copied. Button { text: "Copy"; enabled: Develop.enabled; y: (parent.height - self.height) / 2; clicked => { Transfer.copy(); } } // Enabled on `armed` rather than on a non-empty summary, so pasting a // neutral copy — which clears this image — stays available. Button { text: !Transfer.armed ? "Paste" : "Paste " + Transfer.summary + (Transfer.framing-withheld ? ", no crop" : ""); enabled: Develop.enabled && Transfer.armed; y: (parent.height - self.height) / 2; clicked => { Transfer.paste(); } } // The saved half: this edit, kept under a name. Button { text: "Presets…"; enabled: Develop.enabled; y: (parent.height - self.height) / 2; clicked => { Transfer.open-presets(); } } // Show or hide the develop column. On a tablet the panel is 280px of a // screen that is mostly photograph, and the whole point of opening an // image is to look at it — so being able to put the instruments away // and bring them back is worth a control of its own. The glyph points // the way the panel will move. Button { text: root.panel-visible ? "Hide panel ›" : "‹ Panel"; active: root.panel-visible; y: (parent.height - self.height) / 2; clicked => { root.toggle-panel(); } } Caption { text: root.export-status; vertical-alignment: center; overflow: elide; } // The export itself, beside the settings that shape it. This is the // screen where a photograph is finished, so it is the screen where // one is asked for — the grid can export a selection later, but a // single finished frame is exported from in front of it. Button { text: root.export-busy ? "Exporting…" : root.export-label; enabled: !root.export-busy; y: (parent.height - self.height) / 2; clicked => { root.export-image(); } } // TRACES: FR-UI-4 // The pointer and touch way to the reference F1 opens. Beside // Settings, as in the library header, because it is about the // application rather than about the photograph — and in the same // place on both screens, so it is found where it was last seen. Button { text: "Help"; y: (parent.height - self.height) / 2; clicked => { root.open-help(); } } // Reachable from develop as well as from the grid: export defaults are // most likely to be wanted with a finished photograph on screen, which // is exactly where this bar is and the library header is not. Button { text: "Settings"; y: (parent.height - self.height) / 2; clicked => { root.open-settings(); } } } } Rectangle { y: parent.height - 1px; height: 1px; background: Theme.rule; } } /// What the file says about how the photograph was taken. /// /// A global rather than four properties on the panel, for the reason /// `session.slint` gives at length: the panel is drawn twice — once in the /// develop column and once in the portrait dock — and four lines of forwarding /// copied into the second composition is four lines that can fall behind the /// first without anything failing. /// /// Nothing here is a control. It is the one panel in the column that only /// reports, which is why this global carries no callback at all. export global Capture { in property camera; /// TRACES: FR-DEV-3 /// The lens, and whether it matched a correction profile. /// /// Here rather than beside the optical sliders, and the reason is what the /// line is *for*. `dr-lens` states the rule — an automatic correction that /// silently did nothing is worse than one the user can see is unavailable /// — and the fact it reports is a fact about the file: which lens took /// this photograph, and whether the database has heard of it. That is the /// same kind of thing as the body and the exposure, and it wants reading /// once on opening rather than hunting for under a group filter. in property lens; in property exposure; in property dimensions; } // Capture metadata. Read-only; the adjustment controls live in AdjustPanel, // which is generated from pipeline capabilities rather than written here. // // Flat, like everything else in this column. This was briefly a collapsible // section so that every panel could be put away; the sidebar as a whole now // closes from the status strip instead, which is the control that was actually // wanted, and the per-group lids only stood between the user and the controls. export component InfoPanel inherits Rectangle { background: transparent; height: panel.preferred-height; panel := Panel { // Flat: this abuts the adjust panel below it and the rule between // them is drawn by the column that stacks the two. flat: true; width: 100%; PanelHeading { text: "IMAGE"; } Value { text: Capture.camera == "" ? "—" : Capture.camera; placeholder: Capture.camera == ""; wrap: word-wrap; } Label { text: Capture.exposure; } // Dim, like the dimensions below it: this is something the file says, // not something the photographer chose. An empty string draws nothing // — a session built from pixels with no header has no lens to report, // and an empty row is honest where "Unknown" would be noise. Caption { text: Capture.lens; visible: Capture.lens != ""; wrap: word-wrap; } Caption { text: Capture.dimensions; } } }