Merge: View and Page enums in place of the six view booleans

This commit is contained in:
2026-09-20 20:32:41 +02:00
10 changed files with 178 additions and 114 deletions
+8 -5
View File
@@ -17,7 +17,7 @@ use slint::{ComponentHandle, Model as _, ModelRc, VecModel};
use crate::faces::FaceSweepMessage;
use crate::identity::{self, FaceCell, PersonRow};
use crate::{AppWindow, IdentityFace, IdentityPerson};
use crate::{AppWindow, IdentityFace, IdentityPerson, View};
/// Which model's faces the screen is looking at.
///
@@ -740,14 +740,14 @@ fn wire_navigation(
let models_present = models.clone();
window.on_identity_open(move || {
let Some(w) = weak.upgrade() else { return };
let from_library = w.get_show_library();
let from_library = w.get_active_view() == View::Library;
ctl.came_from_library.set(from_library);
w.set_identity_back_label(if from_library {
"‹ Library".into()
} else {
"‹ Develop".into()
});
w.set_show_identity(true);
w.set_active_view(View::Identity);
// A fact about the filesystem, so it is re-checked on every open
// rather than cached: the user may have just put the models there.
w.set_identity_model_missing(models_present().is_none());
@@ -768,8 +768,11 @@ fn wire_navigation(
// Back to whichever screen this was opened from. The develop
// session was never torn down — it was only hidden — so returning
// to it costs nothing and keeps the photographer's place.
w.set_show_library(ctl.came_from_library.get());
w.set_show_identity(false);
w.set_active_view(if ctl.came_from_library.get() {
View::Library
} else {
View::Develop
});
});
}
+3 -3
View File
@@ -26,7 +26,7 @@ use dr_types::{FormatFilter, ImportSettings};
use crate::activity::{Activity, ActivityLog, Kind};
use crate::import::{self, Message, Request, Upload};
use crate::AppWindow;
use crate::{AppWindow, Page};
/// How often the drain runs while an import is going.
///
@@ -344,7 +344,7 @@ where
*ctl.error.borrow_mut() = String::new();
survey(&w, &ctl, &context);
render(&w, &ctl);
w.set_show_import(true);
w.set_active_page(Page::Import);
});
}
@@ -358,7 +358,7 @@ where
if ctl.running.get() {
return;
}
w.set_show_import(false);
w.set_active_page(Page::None);
});
}
}
+2 -2
View File
@@ -14,7 +14,7 @@ use dr_sync_nextcloud::{auth, NextcloudProvider};
use slint::ComponentHandle;
use crate::launch::{LaunchModel, LaunchState};
use crate::AppWindow;
use crate::{AppWindow, View};
/// Shared launch state for the running window.
pub struct LaunchController {
@@ -257,7 +257,7 @@ fn wire_choose_folder_and_open<F>(
let Some(w) = weak.upgrade() else { return };
let session = ctl.model.borrow().session().cloned();
if let Some(s) = session {
w.set_show_launch(false);
w.set_active_view(View::Library);
on_open_library(s);
}
});
+26 -4
View File
@@ -1618,7 +1618,13 @@ fn construct_screens(
{
let controller = launch_ui::LaunchController::new();
let startup = controller.model.borrow().startup_action(!paths.is_empty());
window.set_show_launch(startup == launch::Startup::ShowLaunchScreen);
// `OpenLibrary` is overwritten moments later by `library_ui::open`
// below, once a stored session exists; `ShowLocalFiles` leaves
// `develop` as it found it, since there is no library to switch to.
window.set_active_view(match startup {
launch::Startup::ShowLaunchScreen => View::Launch,
launch::Startup::ShowLocalFiles | launch::Startup::OpenLibrary => View::Develop,
});
let library = library.clone();
let collections = collections.clone();
@@ -3404,11 +3410,27 @@ fn apply_layout_class(
/// the activity — the behaviour every application there has, and the reason
/// this answers with a bool rather than swallowing the gesture.
fn back_one_step(w: &AppWindow) -> bool {
// The Identity screen has no representation in `NavState`: nothing here
// records which view it replaced, only `identity_ui`'s own
// `came_from_library`, which this function has no reason to reach for
// over one screen. Before this refactor, back queued nothing behind
// Identity opened from the library (show-library stayed true, unread by
// anything) and queued `ToLibrary` behind Identity opened from develop —
// but `ToLibrary` only ever set `show-library`, a property
// `IdentityScreen`'s own `if` never read, so both cases looked the same
// on screen: nothing happened. Swallowing the gesture here keeps that:
// falling into the general case below would instead write `active-view`,
// which `IdentityScreen`'s `if` *does* read, and close the screen as a
// side effect nobody asked for.
if w.get_active_view() == View::Identity {
return true;
}
let state = NavState {
settings: w.get_show_settings(),
launch: w.get_show_launch(),
settings: w.get_active_page() == Page::Settings,
launch: w.get_active_view() == View::Launch,
browsing: w.get_launch_browsing(),
library: w.get_show_library(),
library: w.get_active_view() == View::Library,
mode: w.global::<Develop>().get_view_mode(),
zoomed: w.get_zoomed(),
// Files named on the command line have no grid behind them — the same
+14 -14
View File
@@ -22,7 +22,7 @@ use dr_types::FormatFilter;
use slint::{ComponentHandle, Model as _};
use crate::library::{self, ScanMessage, ThumbnailMessage};
use crate::{AppWindow, GestureRow, KeywordRow, LibraryCell, PersonChip, TimelineBar};
use crate::{AppWindow, GestureRow, KeywordRow, LibraryCell, PersonChip, TimelineBar, View};
/// A screenful before the grid has reported its geometry.
///
@@ -957,7 +957,7 @@ pub fn open(
Ok(c) => c,
Err(e) => {
window.set_library_error(format!("credentials: {e}").into());
window.set_show_library(true);
window.set_active_view(View::Library);
return;
}
};
@@ -977,7 +977,7 @@ pub fn open(
// another device may still be applied — see `place_untouched`.
ctl.place_untouched.set(true);
window.set_show_library(true);
window.set_active_view(View::Library);
window.set_library_open(true);
window.set_library_scanning(true);
window.set_library_error(slint::SharedString::new());
@@ -5391,7 +5391,7 @@ fn write_place(window: &AppWindow, ctl: &Rc<LibraryController>) {
fn current_place(window: &AppWindow, ctl: &Rc<LibraryController>) -> Option<dr_types::Place> {
use dr_types::{Place, PlaceScope, Screen};
if window.get_show_launch() || !window.get_library_open() {
if window.get_active_view() == View::Launch || !window.get_library_open() {
return None;
}
@@ -5401,7 +5401,7 @@ fn current_place(window: &AppWindow, ctl: &Rc<LibraryController>) -> Option<dr_t
// tracks; in develop it is the open one, which `report_position` keeps
// `index` holding. Both are library ordinals against the same ordering, so
// the lookup below is one piece of code rather than two.
let in_library = window.get_show_library();
let in_library = window.get_active_view() == View::Library;
let at = if in_library {
ctl.resume_at.get()
} else {
@@ -5544,7 +5544,7 @@ pub(crate) fn apply_place(
if place.screen == Screen::Develop {
if let Some((at, Found::Photograph)) = resolved {
if let Some(open) = ctl.open_image.borrow().clone() {
window.set_show_library(false);
window.set_active_view(View::Develop);
window.set_library_roll_centre(true);
let offset = *ctl.offset.borrow();
if let Some(row) = at.checked_sub(offset) {
@@ -5836,7 +5836,7 @@ pub fn wire<F>(
if let Some(path) = path {
// Leave the grid for the develop view. The status bar's
// "‹ Library" button comes back here.
w.set_show_library(false);
w.set_active_view(View::Develop);
// Which cell the develop view is now showing, so the photo
// roll opens marking it rather than marking nothing.
w.set_library_roll_current(i);
@@ -5940,7 +5940,7 @@ fn wire_grid_cursor_and_zoom(
let row = cursor.checked_sub(offset);
let path = row.and_then(|row| ctl.paths.borrow().get(row).cloned());
if let Some(path) = path {
w.set_show_library(false);
w.set_active_view(View::Develop);
// As on a click: the roll marks what is open, and centres on it
// because this too begins a session.
w.set_library_roll_current(row.unwrap_or(0) as i32);
@@ -6367,7 +6367,7 @@ fn wire_grid_routes(
let weak = window.as_weak();
window.on_library_change(move || {
if let Some(w) = weak.upgrade() {
w.set_show_launch(true);
w.set_active_view(View::Launch);
}
});
}
@@ -6401,10 +6401,11 @@ fn wire_grid_routes(
let open = w.get_index().max(0) as usize;
resume_position(&w, &ctl, &coll_ctl, Some(open));
w.set_show_library(true);
w.set_active_view(View::Library);
// TRACES: FR-UI-8
// After the flag, not before: `current_place` reads it to say which
// view the record is of, and this is the moment it becomes the grid.
// After the view is set, not before: `current_place` reads it to
// say which view the record is of, and this is the moment it
// becomes the grid.
write_place(&w, &ctl);
});
}
@@ -6640,8 +6641,7 @@ fn wire_filter_ratings_and_people(window: &AppWindow, ctl: &Rc<LibraryController
// Leaving the Identity screen for the grid is the whole point of
// the button: the answer to "who is this" is a set of photographs,
// and they are shown where photographs are shown.
w.set_show_identity(false);
w.set_show_library(true);
w.set_active_view(View::Library);
refilter(&w, &ctl);
});
}
+3 -3
View File
@@ -22,7 +22,7 @@ use crate::merge::{
self, AlignmentReport, Cancel, Decision, FillSettings, MergeDestination, MergeEvent,
MergeInput, MergeRequest,
};
use crate::{AppWindow, MergeFrameRow};
use crate::{AppWindow, MergeFrameRow, Page};
/// How often the page reads the job's channel.
const DRAIN_INTERVAL: std::time::Duration = std::time::Duration::from_millis(120);
@@ -411,7 +411,7 @@ fn wire_stop_and_leave(window: &AppWindow, ctl: &Rc<MergeController>) {
job.activity.finish_quietly();
}
*ctl.timer.borrow_mut() = None;
w.set_show_merge(false);
w.set_active_page(Page::None);
});
}
}
@@ -481,7 +481,7 @@ fn start<Fetch>(
window.set_merge_fill_note("".into());
window.set_merge_preview_filled(false);
window.set_merge_done(false);
window.set_show_merge(true);
window.set_active_page(Page::Merge);
let timer = slint::Timer::default();
{
+3 -3
View File
@@ -32,7 +32,7 @@ use dr_types::{
use slint::ComponentHandle;
use crate::settings_store::SettingsStore;
use crate::{Adjustments, AppWindow};
use crate::{Adjustments, AppWindow, Page};
use dr_sync::Connection;
/// Shared settings state for the running window.
@@ -436,7 +436,7 @@ fn wire_open_close(
*ctl.settings.borrow_mut() = ctl.store.load();
render(&w, &ctl);
on_open(&w);
w.set_show_settings(true);
w.set_active_page(Page::Settings);
});
}
@@ -444,7 +444,7 @@ fn wire_open_close(
let weak = window.as_weak();
window.on_settings_close(move || {
let Some(w) = weak.upgrade() else { return };
w.set_show_settings(false);
w.set_active_page(Page::None);
});
}
}
+65 -26
View File
@@ -32,6 +32,45 @@ export { ViewMode, GradientHandle, HandleRole, SpotHandle, SpotRole }
// panels take their wiring this way and why there is one global per family.
export { Develop, Capture, Levels, Peaking, Adjustments, Framing, Transfer, Masking, Repair, Steps }
/// Which top-level screen is on display. Exactly one at a time — replacing
/// the `show-launch` / `show-library` / `show-identity` booleans, with
/// develop as "none of the above" wired into every conditional that drew any
/// of the other three. Those conditionals compared four and five booleans at
/// once to rule out two screens being true together; comparing one value to
/// a variant cannot be wrong in that way.
///
/// `develop` is listed first, and so is the type's default: it is what the
/// old booleans meant when every one of them was false, which is the state
/// files named on the command line stay in permanently — there is no
/// library behind them to switch to — and the state the window briefly
/// holds at construction, before `lib.rs::run` chooses.
export enum View {
develop,
library,
identity,
launch,
}
/// Which page, if any, is drawn over the current `View`. `settings`,
/// `import` and `merge` are reachable from more than one `View` — Settings
/// from the library and from develop, Import and Merge from the library —
/// and are drawn outermost so the view underneath is torn down rather than
/// nested inside the one it is about to change. `none` is the ordinary case,
/// and the type's default.
///
/// Kept apart from `View` rather than folded into it as three more variants:
/// closing a page has to return to whichever view was already current, and
/// today that works because the view's own property is left untouched while
/// a page sits over it. Three more `View` variants would need a second field
/// remembering what to return to; a separate, single-valued `Page` needs
/// nothing to remember.
export enum Page {
none,
settings,
import,
merge,
}
export component AppWindow inherits Window {
title: "DarkRoom";
// The one raster asset in the UI, and the exception that the header of
@@ -184,11 +223,16 @@ export component AppWindow inherits Window {
/// remembered across photographs, which is state no screen holds.
callback inspect-toggled(float, float);
// --- active view (FR-UI-8) ---
//
// The four top-level screens share one property; see `View`. Rust is the
// only writer — nothing in this file assigns to it — so `in` is enough.
in property <View> active-view: View.develop;
// --- launch screen (FR-NC-1, FR-NC-4) ---
//
// The app opens here when no library is configured, and returns here to
// sign out or switch account.
in property <bool> show-launch: false;
in property <bool> launch-signed-in: false;
in property <string> launch-account: "";
in property <string> launch-root: "";
@@ -228,13 +272,12 @@ export component AppWindow inherits Window {
//
// Shown after a library is opened, before an image is chosen. Cells are a
// window over the catalog, not the whole of it.
in property <bool> show-library: false;
/// Whether the grid is actually on screen — the condition the `if` below
/// is written with, hoisted so Rust can read the same answer.
///
/// **`show-library` is not that answer.** It says "the library rather than
/// develop", and stays true while Settings, Import, People or the launch
/// screen replaces the window. The grid subtree is torn down in all four
/// **`active-view == View.library` is not enough on its own.** The view
/// stays `library` while Settings, Import, Merge or People replaces the
/// window as a page over it. The grid subtree is torn down in all four
/// cases, and a Flickable being destroyed passes its viewport through zero
/// on the way out — which reaches `on_library_scrolled` as a scroll to the
/// top of the library and overwrote the position the user was at. That is
@@ -243,8 +286,7 @@ export component AppWindow inherits Window {
/// So the guard has to be this, and the `if` has to be spelled from it, or
/// the two can drift apart again.
out property <bool> library-visible:
!root.show-import && !root.show-merge && !root.show-settings && !root.show-identity
&& !root.show-launch && root.show-library;
root.active-page == Page.none && root.active-view == View.library;
in-out property <[LibraryCell]> library-cells;
in property <int> library-total: 0;
// --- background activity (FR-CAT-1, FR-NC-6c) ---
@@ -396,8 +438,7 @@ export component AppWindow inherits Window {
// A third top-level screen rather than a panel: naming a cluster and
// pulling a stranger out of it are tasks with their own rhythm, and they
// need the whole window. Gated the same way the other screens are, so
// exactly one is ever up.
in property <bool> show-identity: false;
// exactly one is ever up — see `View`.
in property <[IdentityPerson]> identity-people;
in property <[IdentityFace]> identity-faces;
in property <int> identity-selected: -1;
@@ -780,14 +821,14 @@ export component AppWindow inherits Window {
// --- settings (FR-EXP-1, FR-EXP-3, FR-NC-6a) ---
//
// A page rather than an overlay, and the outermost of the view conditions
// below: it is reachable from the library and from develop, so guarding it
// with `!show-library` or `!show-launch` would make which one you came from
// decide whether it appears.
// A page rather than an overlay, and outermost among the pages drawn over
// a view: it is reachable from the library and from develop, so guarding
// it with which view was current would make which one you came from
// decide whether it appears. See `Page`.
//
// Every control saves on change (see `settings_ui.rs`), so there is no
// dirty state here and nothing to confirm on the way out.
in-out property <bool> show-settings: false;
in-out property <Page> active-page: Page.none;
// --- import (FR-CAT-10, FR-CAT-11, FR-NC-7a) -------------------------
//
@@ -798,7 +839,6 @@ export component AppWindow inherits Window {
// Unlike Settings, this page holds a *decision* rather than a set of saved
// preferences, which is why it carries `import-can-start` and a running
// state: everything here exists to be committed or abandoned.
in-out property <bool> show-import: false;
/// Whether this platform can import at all — see `dr_plat::imports_supported`.
in property <bool> import-supported: false;
@@ -829,7 +869,6 @@ export component AppWindow inherits Window {
// TRACES: FR-MRG-1 | FR-MRG-7
// The merge page, on the import page's model: a decision to be
// committed or abandoned, with a running state, over the library.
in-out property <bool> show-merge: false;
in property <string> merge-stage: "";
in property <bool> merge-running: false;
in property <float> merge-fraction: 0.0;
@@ -1266,12 +1305,12 @@ in property <bool> panel-visible: true;
}
// Settings, over everything. First in the file and first in z-order so the
// conditions below can be read as "and settings is not open".
// conditions below can be read as "and no page is open".
// Outermost with the settings page, and before it: both are reachable
// from the library and from develop, so guarding either with
// `!show-library` would make which view you came from decide whether
// from the library and from develop, so guarding either with which
// view was current would make which view you came from decide whether
// it appears.
if root.show-import: ImportPage {
if root.active-page == Page.import: ImportPage {
width: 100%;
height: 100%;
@@ -1313,7 +1352,7 @@ in property <bool> panel-visible: true;
close() => { root.import-close(); }
}
if !root.show-import && root.show-merge: MergePage {
if root.active-page == Page.merge: MergePage {
width: 100%;
height: 100%;
stage: root.merge-stage;
@@ -1346,7 +1385,7 @@ in property <bool> panel-visible: true;
close() => { root.merge-close(); }
}
if !root.show-import && !root.show-merge && root.show-settings: SettingsPage {
if root.active-page == Page.settings: SettingsPage {
width: 100%;
height: 100%;
@@ -1482,8 +1521,8 @@ in property <bool> panel-visible: true;
}
// Like the launch screen, this replaces the window rather than
// overlaying it — see `show-identity`.
if !root.show-import && !root.show-merge && !root.show-settings && root.show-identity: IdentityScreen {
// overlaying it — see `View`.
if root.active-page == Page.none && root.active-view == View.identity: IdentityScreen {
width: 100%;
height: 100%;
people: root.identity-people;
@@ -1541,7 +1580,7 @@ in property <bool> panel-visible: true;
// The launch screen replaces the whole window rather than overlaying it:
// there is no library to look at until an account is configured.
if !root.show-import && !root.show-merge && !root.show-settings && !root.show-identity && root.show-launch: LaunchScreen {
if root.active-page == Page.none && root.active-view == View.launch: LaunchScreen {
width: 100%;
height: 100%;
signed-in: root.launch-signed-in;
@@ -1835,7 +1874,7 @@ in property <bool> panel-visible: true;
}
}
if !root.show-import && !root.show-merge && !root.show-settings && !root.show-identity && !root.show-launch && !root.show-library: VerticalLayout {
if root.active-page == Page.none && root.active-view == View.develop: VerticalLayout {
width: 100%;
height: 100%;