# Conflicts:
#	crates/ui-egui/src/headless.rs
#	crates/ui-egui/src/lib.rs
#	crates/ui-egui/src/state.rs
#	docs/parity-checklist.md
#	docs/whats-new.md
This commit is contained in:
Brandon Thomas
2026-10-14 22:32:47 -04:00
20 changed files with 148 additions and 3 deletions
+4 -1
View File
@@ -43,6 +43,9 @@ fn prefs(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(v) = i.get("perCamera").and_then(Value::as_bool) {
d.per_camera = v;
}
if let Some(v) = i.get("showOnCard").and_then(Value::as_bool) {
d.show_on_card = v;
}
if let Some(v) = i.get("copyright").and_then(Value::as_str) {
d.copyright = v.trim().to_string();
}
@@ -107,7 +110,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Library Preferences",
[],
None,
"{import?: {rawPreset?: presetId|\"default\", otherPreset?: presetId|\"default\", perCamera?: bool, cameras?: [{camera: \"Make Model\", preset: presetId|null}], copyright?: text, creator?: text (given to imported photos without one)}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default), forgetLocalDays?: n (forget untouched Local photos of folders not browsed for n days; 0 = never)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, forgetLocalDays, persistent}",
"{import?: {rawPreset?: presetId|\"default\", otherPreset?: presetId|\"default\", perCamera?: bool, cameras?: [{camera: \"Make Model\", preset: presetId|null}], copyright?: text, creator?: text (given to imported photos without one), showOnCard?: bool (open the import review when a camera or card is connected)}, camera?: {camera, preset?, remove?: bool}, cacheMb?: n (0 = default), forgetLocalDays?: n (forget untouched Local photos of folders not browsed for n days; 0 = never)} — develop defaults applied on import (raws / other images / per camera) and the thumbnail cache size, saved with the library → {xmp, import, cacheMb, forgetLocalDays, persistent}",
always,
prefs
)]
+38
View File
@@ -104,6 +104,18 @@ impl DeviceWatch {
s.list.clone()
}
/// Like [`DeviceWatch::get`], but `None` until the first scan has landed, so an empty list
/// means no cards rather than not looked yet.
pub fn get_scanned(self: &Arc<Self>) -> Option<Vec<Device>> {
let _ = self.get();
let s = self.state();
#[cfg(not(target_arch = "wasm32"))]
let scanned = s.scanned.is_some();
#[cfg(target_arch = "wasm32")]
let scanned = false;
scanned.then(|| s.list.clone())
}
/// Scan now on this thread, and serve the result to later [`DeviceWatch::get`] calls.
pub fn scan_now(&self) -> Vec<Device> {
let list = (self.scanner)();
@@ -148,6 +160,12 @@ pub fn devices() -> Vec<Device> {
global().get()
}
/// [`devices`], but `None` until the first scan lands. The import prompt uses it so a card that was
/// already in when LightCraft started isn't taken for a new one.
pub fn devices_scanned() -> Option<Vec<Device>> {
global().get_scanned()
}
/// Scan the mounted volumes now (blocking), for explicit requests.
pub fn devices_now() -> Vec<Device> {
global().scan_now()
@@ -285,4 +303,24 @@ mod tests {
assert!(watch.scan_now().is_empty());
assert_eq!(changes.load(Ordering::SeqCst), 2);
}
#[test]
fn get_scanned_waits_for_the_first_scan() {
let gate = Arc::new(std::sync::atomic::AtomicBool::new(false));
let g = gate.clone();
let watch = DeviceWatch::new(
move || {
while !g.load(Ordering::SeqCst) {
std::thread::sleep(Duration::from_millis(5));
}
vec![card("NIKON Z 8")]
},
Duration::from_secs(60),
);
assert_eq!(watch.get_scanned(), None, "nothing scanned yet");
assert_eq!(watch.get_scanned(), None);
gate.store(true, Ordering::SeqCst);
assert!(until(|| watch.get_scanned().is_some()));
assert_eq!(watch.get_scanned(), Some(vec![card("NIKON Z 8")]));
}
}
+2
View File
@@ -310,6 +310,8 @@ pub struct ImportDefaults {
pub auto_folder: Option<String>,
pub auto_copy: bool,
pub auto_album: Option<String>,
/// Open the import review when a camera or memory card is connected.
pub show_on_card: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
+2 -1
View File
@@ -108,7 +108,7 @@ fn preferences_persist_with_the_library() {
s.execute("library.xmpPreferences", &json!({"autoWrite": true})).unwrap();
s.execute(
"library.preferences",
&json!({"import": {"rawPreset": "user.rawlook", "perCamera": true}, "camera": {"camera": "Canon EOS R5", "preset": "user.canonlook"}, "cacheMb": 512}),
&json!({"import": {"rawPreset": "user.rawlook", "perCamera": true, "showOnCard": true}, "camera": {"camera": "Canon EOS R5", "preset": "user.canonlook"}, "cacheMb": 512}),
)
.unwrap();
s.close_library().unwrap();
@@ -118,6 +118,7 @@ fn preferences_persist_with_the_library() {
assert!(s.xmp.auto_write);
assert_eq!(s.import_defaults.raw_preset.as_deref(), Some("user.rawlook"));
assert!(s.import_defaults.per_camera);
assert!(s.import_defaults.show_on_card);
assert_eq!(s.import_defaults.cameras.len(), 1);
assert_eq!(s.import_defaults.preset_for(true, "Canon EOS R5"), Some("user.canonlook"));
assert_eq!(s.cache_mb, 512);
+1
View File
@@ -1405,6 +1405,7 @@
"Show overlay (O)": "Überlagerung anzeigen (O)",
"Show ratings, flags and edit badges": "Bewertungen, Markierungen und Bearbeitungssymbole anzeigen",
"Show the Navigator while zoomed in": "Navigator beim Hineinzoomen anzeigen",
"Show the import review when a camera or card is connected": "Importfenster öffnen, wenn eine Kamera oder Karte angeschlossen wird",
"Show the template tags; click one to insert it": "Vorlagenplatzhalter anzeigen; zum Einfügen anklicken",
"Show/Hide Mask": "Maske ein-/ausblenden",
"Show/hide My Photos panel": "Bereich „Meine Fotos“ ein-/ausblenden",
+1
View File
@@ -1418,6 +1418,7 @@
"Show overlay (O)": "Mostrar superposición (O)",
"Show ratings, flags and edit badges": "Mostrar valoraciones, marcas e indicadores de edición",
"Show the Navigator while zoomed in": "Mostrar el Navegador al ampliar",
"Show the import review when a camera or card is connected": "Abrir la importación al conectar una cámara o tarjeta",
"Show the template tags; click one to insert it": "Mostrar las etiquetas de plantilla; hacer clic en una para insertarla",
"Show/Hide Mask": "Mostrar/ocultar máscara",
"Show/hide My Photos panel": "Mostrar/ocultar panel Mis fotos",
+1
View File
@@ -1341,6 +1341,7 @@
"Show overlay (O)": "Afficher l'incrustation (O)",
"Show ratings, flags and edit badges": "Afficher les notes, les drapeaux et les pastilles de retouche",
"Show the Navigator while zoomed in": "Afficher le Navigateur lors du zoom",
"Show the import review when a camera or card is connected": "Ouvrir l'importation lorsqu'un appareil photo ou une carte est connecté",
"Show the template tags; click one to insert it": "Afficher les balises du modèle ; cliquez sur l'une d'elles pour l'insérer",
"Show/Hide Mask": "Afficher/masquer le masque",
"Show/hide My Photos panel": "Afficher/masquer le panneau Mes photos",
+1
View File
@@ -1287,6 +1287,7 @@
"Show overlay (O)": "重ね表示(O)",
"Show ratings, flags and edit badges": "評価・フラグ・編集済みマークを表示",
"Show the Navigator while zoomed in": "拡大時にナビゲーターを表示",
"Show the import review when a camera or card is connected": "カメラやメモリーカードを接続したときに読み込み画面を表示",
"Show the template tags; click one to insert it": "テンプレートのタグを表示(クリックで挿入)",
"Show/Hide Mask": "マスクの表示/非表示",
"Show/hide My Photos panel": "マイフォトパネルを表示/非表示",
+1
View File
@@ -1364,6 +1364,7 @@
"Show overlay (O)": "Mostrar sobreposição (O)",
"Show ratings, flags and edit badges": "Mostrar avaliações, marcações e indicadores de edição",
"Show the Navigator while zoomed in": "Mostrar o Navegador quando ampliado",
"Show the import review when a camera or card is connected": "Abrir a importação ao conectar uma câmera ou cartão",
"Show the template tags; click one to insert it": "Mostrar os marcadores do modelo; clique em um para inseri-lo",
"Show/Hide Mask": "Mostrar/ocultar máscara",
"Show/hide My Photos panel": "Mostrar/ocultar painel Minhas fotos",
+1
View File
@@ -1132,6 +1132,7 @@
"Show overlay (O)": "Показать наложение (O)",
"Show ratings, flags and edit badges": "Показывать оценки, флаги и значки правок",
"Show the Navigator while zoomed in": "Показывать навигатор при увеличении",
"Show the import review when a camera or card is connected": "Открывать импорт при подключении камеры или карты",
"Show the template tags; click one to insert it": "Показать теги шаблона; щёлкните по тегу, чтобы вставить его",
"Show/Hide Mask": "Показать/скрыть маску",
"Show/hide My Photos panel": "Показать/скрыть панель «Мои фото»",
+1
View File
@@ -1355,6 +1355,7 @@
"Show overlay (O)": "Показати накладання (O)",
"Show ratings, flags and edit badges": "Показати оцінки, прапорці й позначки змін",
"Show the Navigator while zoomed in": "Показувати навігатор під час збільшення",
"Show the import review when a camera or card is connected": "Відкривати імпорт під час підключення камери або карти",
"Show the template tags; click one to insert it": "Показати теги шаблону; клацніть тег, щоб вставити його",
"Show/Hide Mask": "Показати/приховати маску",
"Show/hide My Photos panel": "Показати/приховати панель моїх фото",
+1
View File
@@ -1349,6 +1349,7 @@
"Show overlay (O)": "显示叠加 (O)",
"Show ratings, flags and edit badges": "显示星级、旗标和编辑标记",
"Show the Navigator while zoomed in": "放大时显示导航器",
"Show the import review when a camera or card is connected": "连接相机或存储卡时显示导入窗口",
"Show the template tags; click one to insert it": "显示模板标签;单击即可插入",
"Show/Hide Mask": "显示/隐藏蒙版",
"Show/hide My Photos panel": "显示/隐藏我的照片面板",
+1
View File
@@ -1349,6 +1349,7 @@
"Show overlay (O)": "顯示疊加(O)",
"Show ratings, flags and edit badges": "顯示評分、旗標與編輯標記",
"Show the Navigator while zoomed in": "放大時顯示導覽器",
"Show the import review when a camera or card is connected": "連接相機或記憶卡時顯示匯入視窗",
"Show the template tags; click one to insert it": "顯示範本標籤;點擊即可插入",
"Show/Hide Mask": "顯示/隱藏遮色片",
"Show/hide My Photos panel": "顯示/隱藏我的照片面板",
+42
View File
@@ -3101,6 +3101,48 @@ mod tests {
assert!(heights.iter().any(|h| (h - first.height()).abs() > 20.0), "the tabs differ in height: {heights:?}");
}
/// Settings → Import → "Show the import review when a camera or card is connected": a card that
/// was already in doesn't ask, a new one opens Import from Device (copying), and a busy window
/// asks once it's free.
#[test]
fn a_connected_card_opens_the_import_review() {
let mut h = demo([1200.0, 900.0]);
let t = Duration::from_secs(10);
let root = std::env::temp_dir().join(format!("lc-card-{}", std::process::id()));
let dcim = root.join("DCIM/100NIKON");
std::fs::create_dir_all(&dcim).unwrap();
let img = lightcraft_raster::Rgba8::from_fn(8, 8, |x, y| [(x * 9) as u8, (y * 12) as u8, 80, 255]);
let o = lightcraft_engine::export::ExportOptions { format: lightcraft_engine::export::ExportFormat::Png, ..Default::default() };
std::fs::write(dcim.join("DSC_0001.png"), lightcraft_engine::export::encode_image(&img, &o).unwrap()).unwrap();
let card = lightcraft_engine::devices::Device {
name: "NIKON Z 8".into(),
path: root.join("DCIM").to_string_lossy().to_string(),
root: root.to_string_lossy().to_string(),
};
h.app.ui.dialog = None;
// first look: the card was already in
crate::import::cards_seen(&mut h.app, vec![card.clone()]);
assert!(h.app.scan.is_none() && h.app.ui.dialog.is_none(), "a card that was already in doesn't ask");
crate::import::cards_seen(&mut h.app, vec![card.clone()]);
assert!(h.app.scan.is_none());
// taken out, then put back while another dialog is open: it waits
crate::import::cards_seen(&mut h.app, Vec::new());
h.app.ui.dialog = Some(crate::state::Dialog::About);
crate::import::cards_seen(&mut h.app, vec![card.clone()]);
assert!(h.app.scan.is_none(), "a busy window isn't interrupted");
h.app.ui.dialog = None;
crate::import::cards_seen(&mut h.app, vec![card.clone()]);
assert!(h.app.scan.is_some(), "the card is scanned once the window is free");
assert!(h.step_until(t, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. }))));
let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") };
assert!(opts.copy, "a card is copied into the library");
// with the option off nothing is remembered, so turning it on again starts with a first look
assert!(!h.app.session.import_defaults.show_on_card);
h.step();
assert!(h.app.ui.known_cards.is_none());
let _ = std::fs::remove_dir_all(&root);
}
/// The folder scan runs in the background: the request returns at once and the review opens
/// when the scan finishes (a folder on a network share must not freeze the window).
/// Clicking the folder being read again keeps the running read (and its progress).
+22
View File
@@ -444,6 +444,28 @@ pub(crate) fn busy_synchronizing(app: &LightcraftApp) -> Result<(), String> {
if app.sync_run.is_some() { Err(crate::i18n::tr("A folder is being synchronized").to_string()) } else { Ok(()) }
}
/// Settings → Import → "Show the import review when a camera or card is connected", called with
/// each device scan while it's on. The first look only notes the cards already in. A card that
/// shows up after that opens Import from Device once nothing else is open, and a card that is
/// taken out and put back in asks again.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn cards_seen(app: &mut LightcraftApp, now: Vec<lightcraft_engine::devices::Device>) {
let Some(known) = app.ui.known_cards.as_mut() else {
app.ui.known_cards = Some(now.into_iter().map(|d| d.root).collect());
return;
};
known.retain(|root| now.iter().any(|d| d.root == *root));
let Some(card) = now.into_iter().find(|d| !known.contains(&d.root)) else { return };
// busy: ask when the window is free (the card isn't noted until then)
if app.import.is_some() || app.scan.is_some() || app.sync_run.is_some() || app.lightroom.is_some() || app.ui.dialog.is_some() {
return;
}
known.push(card.root);
if let Err(e) = app.run("file.addFromDevice", json!({"path": card.path})) {
log::warn!("import from {}: {e}", card.name);
}
}
/// Scan `paths` in the background, then open the review dialog (see [`poll_scan`]).
pub fn open(app: &mut LightcraftApp, paths: Vec<String>) -> Result<Value, String> {
if app.scan.is_some() {
+10
View File
@@ -1138,6 +1138,16 @@ impl LightcraftApp {
}
self.ui.sidecars_waiting = waiting;
}
// Settings → Import: open the import review when a camera or card is connected
#[cfg(not(target_arch = "wasm32"))]
if self.session.import_defaults.show_on_card {
if let Some(now) = lightcraft_engine::devices::devices_scanned() {
import::cards_seen(self, now);
}
ctx.request_repaint_after(lightcraft_engine::devices::MAX_AGE);
} else {
self.ui.known_cards = None;
}
self.session.persist_if_dirty();
panels::denoise::pump(self, ctx);
model_setup::pump(self, ctx);
+5
View File
@@ -365,6 +365,11 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
}
});
if !cfg!(target_arch = "wasm32") {
heading(ui, t, crate::i18n::tr("Cameras and Cards"));
let mut on_card = d.show_on_card;
if check(ui, "settings.importOnCard", &mut on_card, "Show the import review when a camera or card is connected") {
let _ = app.run("library.preferences", json!({"import": {"showOnCard": on_card}}));
}
heading(ui, t, crate::i18n::tr("Auto Import"));
hint(ui, t, crate::i18n::tr("Photos that arrive in this folder (tethering, a scanner, a sync app) are added as soon as they're complete."));
row(ui, t, crate::i18n::tr("Watched folder"), |ui| {
+5
View File
@@ -734,6 +734,10 @@ pub struct UiState {
pub sidecar_retry_at: f64,
#[serde(skip)]
pub sidecars_waiting: usize,
/// The cards and cameras already seen (volume roots) while Settings → Import → "Show the
/// import review when a camera or card is connected" is on; `None` = not looked yet.
#[serde(skip)]
pub known_cards: Option<Vec<String>>,
/// The develop control whose slider is being dragged (geometry sliders show a grid).
#[serde(skip)]
pub dragging_control: Option<String>,
@@ -1273,6 +1277,7 @@ impl Default for UiState {
auto_import_at: 0.0,
sidecar_retry_at: 0.0,
sidecars_waiting: 0,
known_cards: None,
search: String::new(),
focus_search: false,
renaming_mask: None,
+2 -1
View File
@@ -76,7 +76,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
| LR-IMP-ADD-DIALOG | Add photos/folders | P0 | ✅ | `cmd:file.addPhotos`, `cmd:library.importPreview`, `cmd:library.import` (`mode`, `album`, `albumName`, `preset`, `keywords`), `crates/ui-egui/src/import.rs` | review dialog (Import Photos): the scanned source (a scanned folder is not added to Local), candidate grid with thumbnails and checkboxes (duplicates by path/content marked and unchecked; with several file types, a toggle per type checks or unchecks all of its files, e.g. the raws without the JPEGs beside them, issue #344) filling a resizable window (issue #337), transfer mode (add in place / copy / move, each explained), album (existing/new), preset, keywords; the folder scan runs on a background thread, shown in the activity stack with ✕ to cancel (a NAS folder no longer freezes the window); the import itself (probing, sidecars, copying / moving files) runs on a worker thread and its batches join the library between frames, shown in the activity stack with ✕ to cancel; one undo step. Drag-and-drop goes through the same background import. File dialogs list the importable extensions (`lightcraft_engine::import::EXTENSIONS`) in any letter case on Linux, where GTK and the desktop portal match globs case-sensitively (`apps/lightcraft/src/dialog_filter.rs`, issue #342; 0.4.0 hid `.JPG` / `.NEF`, issue #705). The folder scan, Browse Folder, drag-and-drop and the sidecar lookup match extensions in any case (`crates/engine/src/tests_import.rs`) |
| LR-IMP-DRAGDROP | Drop files/folders to import | P0 | ✅ | `crates/ui-egui/src/lib.rs` (dropped files → `cmd:library.import`) | dropping onto a specific album not supported |
| LR-IMP-DUPES | Skip duplicates by content | P1 | ✅ | `crates/engine/src/import.rs`, `crates/engine/src/tests_import.rs` | |
| LR-IMP-DEVICE | Import from camera/card | P1 | ✅ | `cmd:library.devices`, `cmd:file.addFromDevice`, `crates/engine/src/devices.rs` | mounted volumes with a DCIM folder (macOS /Volumes, Linux /media and /run/media, Windows drive letters); File → Import from Device → the import review, copying into the library by default; menus serve the last scan and rescan in the background (never blocking a frame; hot-plugs repaint); copies are verified (a new file, synced, checked against the content hash the scan computed — byte compare with the source when there is none, issue #134; a bad copy is removed and reported as failed, never counted as imported; taken names get -1, -2…, never replaced — issue #96, `crates/engine/src/import_move.rs`); no PTP/MTP (cameras that don't mount as a disk) |
| LR-IMP-DEVICE | Import from camera/card | P1 | ✅ | `cmd:library.devices`, `cmd:file.addFromDevice`, `crates/engine/src/devices.rs` | mounted volumes with a DCIM folder (macOS /Volumes, Linux /media and /run/media, Windows drive letters); File → Import from Device → the import review, copying into the library by default; menus serve the last scan and rescan in the background (never blocking a frame; hot-plugs repaint); copies are verified (a new file, synced, checked against the content hash the scan computed — byte compare with the source when there is none, issue #134; a bad copy is removed and reported as failed, never counted as imported; taken names get -1, -2…, never replaced — issue #96, `crates/engine/src/import_move.rs`); no PTP/MTP (cameras that don't mount as a disk); Settings → Import → Cameras and Cards can open the import review when a camera or card is connected (off by default; a card that was already in doesn't ask, and an open dialog or running import is finished first; `crates/ui-egui/src/import.rs`) |
| LR-IMP-AUTO | Watched-folder auto import | P2 | ✅ | `cmd:library.autoImport`, `cmd:library.autoImportScan`, `crates/ui-egui/src/panels/settings.rs` | Settings ▸ Import ▸ Auto Import: a watched folder whose new photos are added (in place or copied) once complete, into an optional album; scanned every 3 s |
| LR-IMP-PRESET | Preset on import | P2 | ✅ | `cmd:library.import` (`preset`) | chosen in the import review; one History entry |
| LR-IMP-RAWDEFAULT | Raw defaults | P1 | ✅ | `cmd:library.preferences`, `crates/engine/src/import.rs`, `crates/ui-egui/src/panels/settings.rs` | LightCraft default / a preset / per camera (make + model); non-raw default too; the preset look counts as unedited and Reset returns to it |
@@ -814,6 +814,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
| 2026-10-13 | minor | LR-EDIT-DETAIL-NR, LR-EDIT-DETAIL-CNR: noise-reduction radii in the original's pixels, so previews at any size and smaller exports look like the full-size render scaled down (issue #960); status unchanged |
| 2026-10-13 | minor | KEYC-RATING: backtick cycles the flag (`photo.cycleFlag`), ⌘↑ / ⌘↓ raise / lower it (`photo.increaseFlag`, `photo.decreaseFlag`); status unchanged (🟡) |
| 2026-10-13 | patch | LR-MASK-LUMRANGE / LR-MASK-COLORRANGE: range drags keep the grabbed endpoint; colour samples use the render's enabled sections and HDR/SDR settings; regression coverage, status unchanged |
| 2026-10-13 | minor | LR-IMP-DEVICE: Settings → Import → Cameras and Cards can open the import review when a camera or card is connected; status unchanged (✅) |
| 2026-10-11 | minor | LR-MERGE-HDR: HDR and panorama merges of raws without colour matrices (CR2, NEF, ARW…) write the colour they open with as DNG tags (issue #965); status unchanged |
| 2026-10-12 | minor | LR-META-XMP: Read Metadata from File no longer auto-writes the sidecar it has just read, which added `lc:settings` and hid another app's later `crs:` edits (#934); status unchanged (✅) |
| 2026-10-12 | patch | LR-IMP-CAMERA-COVERAGE: CR2 white level from the active area, not the saturated masked rows above it (issue #1272); status unchanged (🟡) |
+7
View File
@@ -81,6 +81,11 @@
search by lens. LightCraft now reads the lens range Nikon keeps in its own maker note, the way Lightroom shows a
lens it doesn't have a name for. Newly imported photos show it.
### Import when a card is connected
- Settings → Import → Cameras and Cards has an option to open the import review as soon as a camera or memory card
is connected, the same review File → Import from Device opens. It's off by default. A card that's already in when
LightCraft starts doesn't ask, and if a dialog is open or an import is running it waits until that's done.
### Hard-edged Heal and Clone spots
- A spot with Feather at 0 no longer draws black pixels exactly on its radius. This also fixes spots at 0% Opacity
changing pixels along their edge. The hard edge now leaves pixels on the radius unchanged.
@@ -198,6 +203,8 @@
"LightCraft stopped unexpectedly" and why, instead of "could not open its window", and the next start renders
on the CPU and explains how to turn the GPU back on.
## 2026-10-13
### Slider drags on large raws
- Dragging a slider no longer re-renders the edited photo's filmstrip thumbnail on every frame; it follows once you
let go. On a 64 MP raw the first of those renders decoded the whole file again, which held up the loupe's