dr-decode exposes four entry points rather than one decode, because callers differ sharply in what they need (ARCH §3.2): culling wants a preview, the grid wants metadata, only develop and export touch sensor data. Fusing them forces a full decode where a header read suffices, which is why Lightroom stalls ~2s per image while culling. Smoke-tested against 1,852 real Canon CR2 files (EOS 6D, ~27MB each): metadata 0.2ms from a 256KB header read, no full decode preview ~250ms 5472x3648, downscaled to 2048 for display jpeg 3.0ms Two findings worth recording: rawler 0.7.2's CR2 decoder implements only full_image; thumbnail_image and preview_image are unimplemented trait defaults returning None. So every rung of the preview ladder resolves to a full-resolution decode at ~250ms — 5x over NFR-P13's 50ms budget. CR2 does carry smaller IFDs, so the fix is our own IFD walk or an upstream contribution. The ladder is written now so that fixing it is a decoder change, not a change to every caller. Recorded in milestone-v0.1 risks. Preview.downscale_to bounds memory: a 5472x3648 RGBA preview is 79.8MB, which exhausts a phone's budget after a handful of images. Box-filtered so downscaled thumbnails do not alias. Also fixed a RefCell double-borrow that panicked on first navigation — `*x.borrow_mut() = *x.borrow() + 1` holds both borrows at once. Verified with 10,000 programmatic navigations. 58 tests passing. Traceability 20.3% (29/143).
255 lines
8.1 KiB
Plaintext
255 lines
8.1 KiB
Plaintext
import { Theme } from "theme.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.
|
|
component StatusBar inherits Rectangle {
|
|
in property <string> adapter;
|
|
in property <string> backend;
|
|
in property <string> layout-class;
|
|
in property <int> fps;
|
|
in property <string> filename;
|
|
in property <string> position;
|
|
|
|
height: 28px;
|
|
background: Theme.surface;
|
|
|
|
HorizontalLayout {
|
|
padding-left: Theme.gap;
|
|
padding-right: Theme.gap;
|
|
spacing: Theme.gap;
|
|
alignment: start;
|
|
|
|
Text {
|
|
text: root.backend;
|
|
color: Theme.accent;
|
|
font-size: Theme.text-sm;
|
|
font-weight: 700;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
Text {
|
|
text: root.filename;
|
|
color: Theme.ink;
|
|
font-size: Theme.text-sm;
|
|
font-weight: 600;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
|
|
Text {
|
|
text: root.position;
|
|
color: Theme.ink-faint;
|
|
font-size: Theme.text-sm;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
Rectangle { horizontal-stretch: 1; }
|
|
|
|
Text {
|
|
text: root.layout-class;
|
|
color: Theme.ink-faint;
|
|
font-size: Theme.text-sm;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
Text {
|
|
text: root.fps + " fps";
|
|
color: root.fps >= 55 ? Theme.ink-dim : Theme.accent;
|
|
font-size: Theme.text-sm;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
y: parent.height - 1px;
|
|
height: 1px;
|
|
background: Theme.rule;
|
|
}
|
|
}
|
|
|
|
// A placeholder panel standing in for the adjustment controls that FR-DEV-3a
|
|
// will generate from operation descriptors.
|
|
component SidePanel inherits Rectangle {
|
|
in property <string> camera;
|
|
in property <string> exposure;
|
|
in property <string> dimensions;
|
|
|
|
background: Theme.surface;
|
|
|
|
VerticalLayout {
|
|
padding: Theme.gap;
|
|
spacing: Theme.gap;
|
|
alignment: start;
|
|
|
|
Text {
|
|
text: "IMAGE";
|
|
color: Theme.accent;
|
|
font-size: Theme.text-sm;
|
|
font-weight: 700;
|
|
letter-spacing: 1.2px;
|
|
}
|
|
|
|
Text {
|
|
text: root.camera == "" ? "—" : root.camera;
|
|
color: Theme.ink;
|
|
font-size: Theme.text;
|
|
wrap: word-wrap;
|
|
}
|
|
|
|
Text {
|
|
text: root.exposure;
|
|
color: Theme.ink-dim;
|
|
font-size: Theme.text-sm;
|
|
}
|
|
|
|
Text {
|
|
text: root.dimensions;
|
|
color: Theme.ink-faint;
|
|
font-size: Theme.text-sm;
|
|
}
|
|
|
|
Rectangle { height: Theme.gap; }
|
|
|
|
Text {
|
|
text: "← → to navigate";
|
|
color: Theme.ink-faint;
|
|
font-size: Theme.text-sm;
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: 1px;
|
|
background: Theme.rule;
|
|
}
|
|
}
|
|
|
|
export component AppWindow inherits Window {
|
|
title: "DarkRoom";
|
|
background: Theme.ground;
|
|
preferred-width: 1100px;
|
|
preferred-height: 720px;
|
|
min-width: 360px;
|
|
min-height: 320px;
|
|
|
|
// Set from Rust each frame: the compute output, delivered as a texture.
|
|
in property <image> canvas;
|
|
in property <string> adapter: "detecting…";
|
|
in property <string> backend: "—";
|
|
in property <int> fps: 0;
|
|
|
|
// Current image, for the status strip and empty state.
|
|
in property <string> filename: "";
|
|
in property <string> camera: "";
|
|
in property <string> exposure: "";
|
|
in property <string> dimensions: "";
|
|
in property <int> index: 0;
|
|
in property <int> total: 0;
|
|
in property <string> load-error: "";
|
|
|
|
callback next-image();
|
|
callback prev-image();
|
|
|
|
// FR-UI-1: layout class follows window width, not device type. A narrow
|
|
// desktop window gets the compact layout, exactly as a tablet would.
|
|
//
|
|
// Set from Rust rather than derived from `root.width` here: a property
|
|
// read inside the layout and also feeding it creates a binding loop,
|
|
// which Slint warns about and which can panic at runtime.
|
|
in property <bool> expanded: true;
|
|
in property <string> layout-class: "expanded";
|
|
|
|
callback canvas-resized(int, int);
|
|
callback window-resized(length);
|
|
|
|
// One-way: report width outward, never read layout back into it.
|
|
changed width => { root.window-resized(self.width); }
|
|
|
|
VerticalLayout {
|
|
StatusBar {
|
|
adapter: root.adapter;
|
|
backend: root.backend;
|
|
layout-class: root.layout-class;
|
|
fps: root.fps;
|
|
filename: root.filename;
|
|
position: root.total > 0 ? root.index + 1 + " / " + root.total : "";
|
|
}
|
|
|
|
HorizontalLayout {
|
|
// The canvas: compute output composited directly. No CPU
|
|
// round-trip anywhere in this path (ARCH §6.1).
|
|
canvas-area := Rectangle {
|
|
horizontal-stretch: 1;
|
|
background: Theme.ground;
|
|
clip: true;
|
|
|
|
Image {
|
|
width: 100%;
|
|
height: 100%;
|
|
source: root.canvas;
|
|
image-fit: contain;
|
|
visible: root.total > 0 && root.load-error == "";
|
|
}
|
|
|
|
// Empty and error states say what happened rather than
|
|
// showing a blank canvas.
|
|
if root.total == 0 || root.load-error != "": VerticalLayout {
|
|
alignment: center;
|
|
spacing: Theme.gap;
|
|
Text {
|
|
text: root.load-error != "" ? "Could not load image" : "No images";
|
|
color: Theme.ink-dim;
|
|
font-size: Theme.text-lg;
|
|
horizontal-alignment: center;
|
|
}
|
|
Text {
|
|
text: root.load-error != "" ? root.load-error
|
|
: "Pass a folder or file on the command line.";
|
|
color: Theme.ink-faint;
|
|
font-size: Theme.text-sm;
|
|
horizontal-alignment: center;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
|
|
// Arrow keys and space step through the folder.
|
|
FocusScope {
|
|
key-pressed(event) => {
|
|
if (event.text == Key.RightArrow || event.text == " ") {
|
|
root.next-image();
|
|
return accept;
|
|
}
|
|
if (event.text == Key.LeftArrow) {
|
|
root.prev-image();
|
|
return accept;
|
|
}
|
|
return reject;
|
|
}
|
|
}
|
|
|
|
// Report size changes so the render target can be resized to
|
|
// match. Width and height are tracked separately because
|
|
// Slint has no single "geometry changed" hook.
|
|
property <int> px-w: Math.round(self.width / 1px);
|
|
property <int> px-h: Math.round(self.height / 1px);
|
|
changed px-w => { root.canvas-resized(self.px-w, self.px-h); }
|
|
changed px-h => { root.canvas-resized(self.px-w, self.px-h); }
|
|
}
|
|
|
|
// Always instantiated, width collapsed to zero in compact mode.
|
|
// A conditional `if` here creates a binding loop — the layout
|
|
// depends on `expanded`, which derives from the window width,
|
|
// which the layout then influences. Slint flags it, and it can
|
|
// panic at runtime.
|
|
SidePanel {
|
|
width: root.expanded ? 260px : 0px;
|
|
visible: root.expanded;
|
|
camera: root.camera;
|
|
exposure: root.exposure;
|
|
dimensions: root.dimensions;
|
|
}
|
|
}
|
|
}
|
|
}
|