Remember where the photographer was
Opening the application was always a fresh arrival at the beginning of the library, whatever you had been doing when you closed it. What is written down is the view, the scope, the rating filter and the photograph on screen -- the open one in develop, the first visible one in the grid. Not just a scroll position: a position without the filter that produced it names a row of a list that no longer exists. Restoring them has an order for the same reason -- scope, then filter, then position, then the view -- because each step changes what an ordinal *means*. Addressed by remote path and collection UUID, never by an ordinal or a row id. `images.id` and `collections.id` are local to one catalog, and a grid ordinal is local to one ordering; a record naming either would land somewhere arbitrary on a second device and after any filter change on this one. Where the ordinal is needed, `library::ordinal_of_path` computes it through the grid's own `ORDER BY`, taken verbatim by a window function rather than spelled a second time as an inequality -- which is the mistake `grid_order_for` already warns about, and which a manually ordered collection would make unreadable. Every failure degrades rather than reports. A collection this device has not merged leaves the scope at the whole library; a photograph that has since been deleted falls back to when it was taken, which puts the grid in the right week; a torn file yields no place and the library opens at the top. Reopening develop is the one thing that requires an exact match, because a canvas on a path that no longer resolves is a filename over an empty frame. The record lives in `dr-types` beside `Settings` and the store lives here beside `SettingsStore`, for the reason `dr-types`' manifest gives: a JSON serialiser in `core/` would be paid for by every crate there. Two files and two lifetimes, though -- resetting preferences must not forget where you were. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+470
-2
@@ -192,6 +192,46 @@ pub struct LibraryController {
|
||||
/// and read here. Shared rather than passed per call because a rescan, a
|
||||
/// scrub and a drop all reload the window and must all honour it.
|
||||
scope: RefCell<Option<dr_types::CollectionId>>,
|
||||
/// TRACES: FR-UI-8
|
||||
/// How to open a photograph in develop.
|
||||
///
|
||||
/// Held so a restored place that was left in develop can put the user back
|
||||
/// there. The closure is the grid's own — `wire` is handed it and stashes a
|
||||
/// clone — rather than a second route into the develop view, which would be
|
||||
/// a second place for "persist the outgoing edit first" to be forgotten.
|
||||
open_image: RefCell<Option<OpenImage>>,
|
||||
/// Where this library's position is written, once one is open.
|
||||
///
|
||||
/// `None` before a library has been opened, and on the local-files path,
|
||||
/// which has no library to have a position in.
|
||||
place_store: RefCell<Option<crate::place::PlaceStore>>,
|
||||
/// What this device calls itself in a place it writes.
|
||||
///
|
||||
/// [`dr_thumbs::ThumbStore::client_id`], so the place and the thumbnail
|
||||
/// shards a device publishes carry the same name — which is what makes
|
||||
/// `.darkroom-derived/` readable by a human wondering which machine put
|
||||
/// what there. Informational only: see [`crate::place`] on why the
|
||||
/// timestamp, not the device, decides an exchange.
|
||||
place_device: RefCell<String>,
|
||||
/// Coalesces the writes a flick would otherwise make one of per event.
|
||||
place_timer: RefCell<Option<slint::Timer>>,
|
||||
/// Whether a place is being applied right now.
|
||||
///
|
||||
/// A restore moves the scope, the filter and the viewport, and every one of
|
||||
/// those is something [`note_place`] otherwise treats as the photographer
|
||||
/// having moved. Without this a restore would close its own handover latch
|
||||
/// and, worse, write the record back out under a fresh timestamp — so the
|
||||
/// device that had just *adopted* a place from the tablet would claim to be
|
||||
/// the newer of the two.
|
||||
applying_place: std::cell::Cell<bool>,
|
||||
/// Whether the photographer has moved since this library was opened.
|
||||
///
|
||||
/// The latch that decides whether a place arriving from another device may
|
||||
/// still be applied. A handover is only welcome before the user has started
|
||||
/// working: a grid that jumped somewhere else *while being scrolled*, because
|
||||
/// a network round-trip finally landed, would be worse than never handing
|
||||
/// over at all.
|
||||
place_untouched: std::cell::Cell<bool>,
|
||||
/// TRACES: FR-CAT-15
|
||||
/// Whether the grid is listing the trash rather than the library.
|
||||
///
|
||||
@@ -388,6 +428,12 @@ impl LibraryController {
|
||||
sync_timer: RefCell::new(None),
|
||||
session: RefCell::new(None),
|
||||
scope: RefCell::new(None),
|
||||
open_image: RefCell::new(None),
|
||||
place_store: RefCell::new(None),
|
||||
place_device: RefCell::new(String::new()),
|
||||
place_timer: RefCell::new(None),
|
||||
place_untouched: std::cell::Cell::new(true),
|
||||
applying_place: std::cell::Cell::new(false),
|
||||
viewing_trash: std::cell::Cell::new(false),
|
||||
coll_ctl: RefCell::new(None),
|
||||
timeline_zoom: RefCell::new(0),
|
||||
@@ -806,6 +852,11 @@ impl LibraryController {
|
||||
/// or a drop that altered the collection being shown.
|
||||
pub fn reload(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
load_window(window, ctl);
|
||||
// TRACES: FR-UI-8
|
||||
// The scope changed, or the collection under it did. Either way this is the
|
||||
// entry point every such change comes through, which is why the record is
|
||||
// taken here rather than at each of the callers.
|
||||
note_place(window, ctl);
|
||||
}
|
||||
|
||||
/// Open a library: show the grid, start a scan, then fill in thumbnails.
|
||||
@@ -834,6 +885,18 @@ pub fn open(
|
||||
let filter = account.format_filter();
|
||||
*ctl.session.borrow_mut() = Some((conn.clone(), filter.clone()));
|
||||
|
||||
// TRACES: FR-UI-8
|
||||
// Where this library's position is kept, and what this device signs it
|
||||
// with. Before the catalog opens, because `adopt_catalog` is what reads the
|
||||
// record back and it is the next thing to run.
|
||||
*ctl.place_store.borrow_mut() = Some(crate::place::PlaceStore::open_at(library::place_path(
|
||||
&conn.account,
|
||||
)));
|
||||
*ctl.place_device.borrow_mut() = device_id(&conn.account);
|
||||
// A fresh library is one nobody has moved in yet, so a place arriving from
|
||||
// another device may still be applied — see `place_untouched`.
|
||||
ctl.place_untouched.set(true);
|
||||
|
||||
window.set_show_library(true);
|
||||
window.set_library_open(true);
|
||||
window.set_library_scanning(true);
|
||||
@@ -2028,6 +2091,14 @@ fn open_catalog_for_offline(
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-UI-8
|
||||
/// The grid's own route into the develop view, as something that can be held.
|
||||
///
|
||||
/// Named because it is threaded through the controller as well as passed to
|
||||
/// `wire`, and a `RefCell<Option<Rc<dyn Fn(String)>>>` written out twice is a
|
||||
/// signature nobody reads.
|
||||
type OpenImage = Rc<dyn Fn(String)>;
|
||||
|
||||
/// How long a run of geometry changes has to stop for before the window is
|
||||
/// reloaded against it.
|
||||
///
|
||||
@@ -2208,7 +2279,43 @@ fn adopt_catalog(
|
||||
// membership — the same order the scan's completion uses.
|
||||
crate::collections_ui::refresh_tree(window, coll_ctl, &cat);
|
||||
*ctl.catalog.borrow_mut() = Some(cat);
|
||||
load_window(window, ctl);
|
||||
|
||||
// TRACES: FR-UI-8
|
||||
// Where the photographer was, before the first window is read rather than
|
||||
// after. The scope and the filter both change what the grid's query
|
||||
// returns, so restoring them afterwards would mean reading the top of the
|
||||
// whole library, drawing it, and then reading again — a visible jump on
|
||||
// every launch, and on a remote library a screenful of thumbnails fetched
|
||||
// for photographs nobody asked to see.
|
||||
//
|
||||
// The sidebar is refreshed above rather than below for the same ordering
|
||||
// reason: a scope is restored by naming a row of a tree that has to exist.
|
||||
let stored = ctl.place_store.borrow().as_ref().and_then(|s| s.load());
|
||||
match stored {
|
||||
Some(place) => apply_place(window, ctl, coll_ctl, &place),
|
||||
None => load_window(window, ctl),
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-UI-8
|
||||
/// What this device signs a place with.
|
||||
///
|
||||
/// The thumbnail store's client id, which is already this device's name among
|
||||
/// the clients sharing a library — it is what qualifies a shard's remote
|
||||
/// filename. Reusing it means the place and the shards a machine publishes
|
||||
/// carry one name in `.darkroom-derived/`, rather than two that have to be
|
||||
/// correlated by hand.
|
||||
///
|
||||
/// Empty where the store will not open. The field is informational, so an
|
||||
/// unsigned place is worth strictly more than no place at all.
|
||||
fn device_id(account: &Account) -> String {
|
||||
match dr_thumbs::ThumbStore::open(&library::thumbs_dir(account)) {
|
||||
Ok(store) => store.client_id().to_string(),
|
||||
Err(e) => {
|
||||
log::debug!("no device id for the place record: {e}");
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop the open catalog, so the next `show_catalog_now` opens the file
|
||||
@@ -4806,6 +4913,338 @@ fn report_position(window: &AppWindow, ctl: &Rc<LibraryController>, row: usize)
|
||||
window.set_total(window.get_library_total());
|
||||
}
|
||||
|
||||
// --- where the photographer was (FR-UI-8) --------------------------------
|
||||
|
||||
/// How long a movement has to settle before it is written down.
|
||||
///
|
||||
/// A flick reports a scroll several times per screenful, and a place is a file
|
||||
/// write — one per event would put disk I/O inside the gesture NFR-P5 governs,
|
||||
/// for a record only the *last* of which is ever read. Longer than
|
||||
/// [`GEOMETRY_SETTLE`] because nothing on screen waits for this: the reload a
|
||||
/// geometry change schedules is visible, and this is not.
|
||||
const PLACE_SETTLE: std::time::Duration = std::time::Duration::from_millis(750);
|
||||
|
||||
/// The photographer moved. Write it down, once they stop.
|
||||
///
|
||||
/// Replacing the timer rather than adding one is what makes this a debounce:
|
||||
/// a `slint::Timer` is stopped by being dropped, so the previous pending write
|
||||
/// goes with the old handle.
|
||||
fn note_place(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
if ctl.applying_place.get() {
|
||||
// A restore is not a movement — see `applying_place`.
|
||||
return;
|
||||
}
|
||||
|
||||
// The handover latch closes on the first movement — see `place_untouched`.
|
||||
ctl.place_untouched.set(false);
|
||||
|
||||
if ctl.place_store.borrow().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let timer = slint::Timer::default();
|
||||
let weak = window.as_weak();
|
||||
let held = ctl.clone();
|
||||
timer.start(slint::TimerMode::SingleShot, PLACE_SETTLE, move || {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
write_place(&w, &held);
|
||||
}
|
||||
});
|
||||
*ctl.place_timer.borrow_mut() = Some(timer);
|
||||
}
|
||||
|
||||
/// Write the place out now, without waiting for a settle.
|
||||
///
|
||||
/// For the moments there may be no "later": leaving the grid for develop, and
|
||||
/// coming back. Both are single deliberate acts rather than runs of events, and
|
||||
/// both change the one field a debounce is most likely to lose — which view the
|
||||
/// user is in.
|
||||
fn write_place(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
if ctl.applying_place.get() {
|
||||
return;
|
||||
}
|
||||
// Opening a photograph is as much "the photographer has started" as
|
||||
// scrolling is, and this is the path that records it — so the handover
|
||||
// latch closes here too, not only in `note_place`. Without it a record
|
||||
// arriving from another device could pull someone out of the image they had
|
||||
// just opened.
|
||||
ctl.place_untouched.set(false);
|
||||
|
||||
let Some(place) = current_place(window, ctl) else {
|
||||
return;
|
||||
};
|
||||
if let Some(store) = ctl.place_store.borrow().as_ref() {
|
||||
store.save(&place);
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the photographer is, as a record that can be written down.
|
||||
///
|
||||
/// `None` where there is nothing worth recording: no library open, or the
|
||||
/// launch screen in front of everything. A place is never written from the
|
||||
/// launch screen — the user is *choosing* a library there, and recording "you
|
||||
/// were at the launch screen" would make the next launch skip the position it
|
||||
/// had just been asked to keep.
|
||||
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() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Which view, and — the same question in both — which photograph.
|
||||
//
|
||||
// In the grid it is the first one visible, which `resume_at` already
|
||||
// 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 at = if in_library {
|
||||
ctl.resume_at.get()
|
||||
} else {
|
||||
window.get_index().max(0) as usize
|
||||
};
|
||||
|
||||
// Resolved through the loaded window rather than by a query: the model has
|
||||
// the path and the capture time side by side already, and this runs on a
|
||||
// settle after scrolling — the one moment a query is least welcome.
|
||||
let offset = *ctl.offset.borrow();
|
||||
let row = at.checked_sub(offset);
|
||||
let path = row
|
||||
.and_then(|r| ctl.paths.borrow().get(r).cloned())
|
||||
.unwrap_or_default();
|
||||
let captured_at = row.and_then(|r| ctl.captured_at.borrow().get(r).copied().flatten());
|
||||
|
||||
// The scope, in the vocabulary both devices share. A collection this device
|
||||
// cannot name is recorded as the whole library rather than as a name
|
||||
// nothing can resolve — see `collections::uuid_of`.
|
||||
let scope = if ctl.viewing_trash.get() {
|
||||
PlaceScope::Trash
|
||||
} else {
|
||||
match *ctl.scope.borrow() {
|
||||
None => PlaceScope::Library,
|
||||
Some(id) => {
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let uuid = borrow
|
||||
.as_ref()
|
||||
.and_then(|c| dr_catalog::collections::uuid_of(c.connection(), id).ok())
|
||||
.flatten();
|
||||
match uuid {
|
||||
Some(uuid) => PlaceScope::Collection { uuid },
|
||||
None => PlaceScope::Library,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Some(Place {
|
||||
at: library::now_secs(),
|
||||
device: ctl.place_device.borrow().clone(),
|
||||
screen: if in_library {
|
||||
Screen::Library
|
||||
} else {
|
||||
Screen::Develop
|
||||
},
|
||||
scope,
|
||||
path,
|
||||
captured_at,
|
||||
filter: (&*ctl.filter.borrow()).into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Put the photographer back where a record says they were.
|
||||
///
|
||||
/// # The order is the whole function
|
||||
///
|
||||
/// Scope, then filter, then position, then — last of all — the view. Each step
|
||||
/// changes what an ordinal *means*: the grid is a window over a query, and a
|
||||
/// row resolved before the scope was narrowed names a photograph in a different
|
||||
/// list. Resolving the path last is what makes the restored position address the
|
||||
/// grid the user is actually about to see.
|
||||
///
|
||||
/// # Nothing here is allowed to fail loudly
|
||||
///
|
||||
/// Every step degrades to "the state we would have been in anyway". A collection
|
||||
/// this device has not merged yet leaves the scope at the whole library; a
|
||||
/// photograph that has been deleted falls back to when it was taken; a capture
|
||||
/// time that resolves to nothing leaves the grid at the top. A place is a
|
||||
/// convenience, and an application that would not open because one was stale
|
||||
/// would be trading something that matters for something that does not.
|
||||
pub(crate) fn apply_place(
|
||||
window: &AppWindow,
|
||||
ctl: &Rc<LibraryController>,
|
||||
coll_ctl: &Rc<crate::collections_ui::CollectionsController>,
|
||||
place: &dr_types::Place,
|
||||
) {
|
||||
use dr_types::{PlaceScope, Screen};
|
||||
|
||||
ctl.applying_place.set(true);
|
||||
|
||||
// --- the scope --------------------------------------------------------
|
||||
//
|
||||
// Through the same callback the sidebar fires, rather than by setting the
|
||||
// two controllers' fields by hand. Both of them hold a scope, and so does
|
||||
// the header's label and the reorderable flag; there is exactly one piece
|
||||
// of code that keeps those four in step and this is not the place to write
|
||||
// a second.
|
||||
let selected = match &place.scope {
|
||||
PlaceScope::Library => 0,
|
||||
PlaceScope::Trash => -1,
|
||||
PlaceScope::Collection { uuid } => {
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let found = borrow
|
||||
.as_ref()
|
||||
.and_then(|c| dr_catalog::collections::id_for_uuid(c.connection(), uuid).ok())
|
||||
.flatten();
|
||||
drop(borrow);
|
||||
match found {
|
||||
Some(id) => id.0 as i32,
|
||||
// Recorded on another device, and this one has not merged the
|
||||
// collection yet. The whole library is the honest fallback: an
|
||||
// empty grid under a name nothing can resolve reads as data
|
||||
// loss.
|
||||
None => {
|
||||
log::info!("the stored place names a collection this device does not have yet");
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
window.invoke_collection_select(selected);
|
||||
|
||||
// --- the filter -------------------------------------------------------
|
||||
*ctl.filter.borrow_mut() = (&place.filter).into();
|
||||
push_filter(window, ctl);
|
||||
|
||||
// --- the position -----------------------------------------------------
|
||||
let resolved = resolve_place(window, ctl, place);
|
||||
|
||||
// The filter and the scope have both moved the window to the top; this is
|
||||
// what puts it back, and it has to run after them for that reason.
|
||||
let total = window.get_library_total().max(0) as usize;
|
||||
let resolved = resolved.filter(|(at, _)| *at < total);
|
||||
match resolved {
|
||||
Some((at, _)) => {
|
||||
ctl.resume_at.set(at);
|
||||
// Moves the capture-time marker with it — see `resume_position`.
|
||||
resume_position(window, ctl, coll_ctl, None);
|
||||
}
|
||||
None => refilter(window, ctl),
|
||||
}
|
||||
|
||||
// --- and the view -----------------------------------------------------
|
||||
//
|
||||
// Only where the photograph itself was found. A capture time that merely
|
||||
// landed in the right week is a good answer for the grid and no answer at
|
||||
// all for a canvas: develop would open on a path that no longer resolves
|
||||
// and show a filename over an empty frame.
|
||||
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_library_roll_centre(true);
|
||||
let offset = *ctl.offset.borrow();
|
||||
if let Some(row) = at.checked_sub(offset) {
|
||||
window.set_library_roll_current(row as i32);
|
||||
}
|
||||
open(place.path.clone());
|
||||
// After the open, which resets the readout to "1 of 1" on its
|
||||
// way in — the same ordering `report_position` documents.
|
||||
window.set_index(at as i32);
|
||||
window.set_total(window.get_library_total());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last, so nothing between here and the top is mistaken for the
|
||||
// photographer having moved.
|
||||
ctl.applying_place.set(false);
|
||||
}
|
||||
|
||||
/// The library ordinal a record names, or `None` if it names nothing here.
|
||||
///
|
||||
/// Two ways, and the second is why a place survives its subject. The path is
|
||||
/// exact and is tried first. Failing that — the photograph was deleted, moved,
|
||||
/// or filtered out by the very filter this place restored — the capture time
|
||||
/// puts the grid in the right week, which is close enough that the user
|
||||
/// recognises where they are.
|
||||
fn resolve_place(
|
||||
window: &AppWindow,
|
||||
ctl: &Rc<LibraryController>,
|
||||
place: &dr_types::Place,
|
||||
) -> Option<(usize, Found)> {
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let catalog = borrow.as_ref()?;
|
||||
|
||||
if !place.path.is_empty() {
|
||||
match library::ordinal_of_path(
|
||||
catalog,
|
||||
*ctl.scope.borrow(),
|
||||
&ctl.filter.borrow(),
|
||||
ctl.viewing_trash.get(),
|
||||
&place.path,
|
||||
) {
|
||||
Ok(Some(at)) => return Some((at, Found::Photograph)),
|
||||
Ok(None) => {}
|
||||
Err(e) => log::debug!("resolving the stored place: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
let when = place.captured_at?;
|
||||
drop(borrow);
|
||||
// Through the scrubber's own route, so "an instant, as an ordinal" has one
|
||||
// spelling. It sets the viewport and the loaded window together, which is
|
||||
// exactly what a restore needs, and it anchors the timeline marker on the
|
||||
// way.
|
||||
scrub_to(window, ctl, when);
|
||||
// Where it landed, read back rather than recomputed: `scrub_to` sets the
|
||||
// offset to the ordinal it resolved and `load_window` then clamps it against
|
||||
// the end of the library, so the clamped value is the one the grid is
|
||||
// actually showing.
|
||||
Some((*ctl.offset.borrow(), Found::Thereabouts))
|
||||
}
|
||||
|
||||
/// How exactly a place was resolved.
|
||||
///
|
||||
/// The distinction is what stops a restore reopening develop on the wrong
|
||||
/// photograph. Landing in the right week is a perfectly good answer for a grid
|
||||
/// — the user recognises where they are — and no answer at all for a canvas,
|
||||
/// which would open on a path that no longer resolves and show a filename over
|
||||
/// an empty frame.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Found {
|
||||
/// The photograph itself is still in this grid, at this ordinal.
|
||||
Photograph,
|
||||
/// It is not, so this is where its capture time falls instead.
|
||||
Thereabouts,
|
||||
}
|
||||
|
||||
/// Put the whole rating filter on screen.
|
||||
///
|
||||
/// The filter bar is drawn from half a dozen properties, each of which is set
|
||||
/// where its own control is handled — which is right while the user is moving
|
||||
/// them one at a time, and leaves nothing that can restore all of them at once.
|
||||
/// A restored place that narrowed the grid without moving the stars would show
|
||||
/// a filtered library under a bar claiming it was not filtered, and the user's
|
||||
/// first act would be to try to clear a filter the bar says is off.
|
||||
fn push_filter(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
let filter = ctl.filter.borrow().clone();
|
||||
window.set_library_filter_min_rating(filter.min_rating as i32);
|
||||
window.set_library_filter_unjudged(filter.unjudged);
|
||||
// The bar's own numbering, from `on_library_filter_flag_changed`. It has no
|
||||
// code for `Unflagged` because it cannot produce one — "nothing has judged
|
||||
// this" is the `unjudged` chip, over both stars and flags — so a place can
|
||||
// never carry one either, and it falls in with "no flag constraint".
|
||||
window.set_library_filter_flag(match filter.flag {
|
||||
Some(dr_types::FlagState::Pick) => 1,
|
||||
Some(dr_types::FlagState::Reject) => 2,
|
||||
_ => 0,
|
||||
});
|
||||
window.set_library_local_only(filter.local_only);
|
||||
ctl.local_only.set(filter.local_only);
|
||||
push_people_chips(window, ctl);
|
||||
}
|
||||
|
||||
/// Bring the grid back to where the photographer left it, and — coming out of
|
||||
/// develop — put the keyboard cursor on the photograph they were editing.
|
||||
///
|
||||
@@ -4952,7 +5391,12 @@ pub fn wire<F>(
|
||||
|
||||
// Shared rather than moved: a click and `Return` both open an image, and
|
||||
// they are two callbacks.
|
||||
let on_open_image = Rc::new(on_open_image);
|
||||
let on_open_image: OpenImage = Rc::new(on_open_image);
|
||||
// TRACES: FR-UI-8
|
||||
// And a third caller, which is not a callback at all: a place recorded in
|
||||
// develop reopens it at launch. Through the same closure, so the edit-saving
|
||||
// and identity bookkeeping it does are not something a restore can skip.
|
||||
*ctl.open_image.borrow_mut() = Some(on_open_image.clone());
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
@@ -4988,6 +5432,11 @@ pub fn wire<F>(
|
||||
w.set_library_roll_centre(true);
|
||||
on_open_image(path);
|
||||
report_position(&w, &ctl, i as usize);
|
||||
// TRACES: FR-UI-8
|
||||
// Written now rather than on a settle: which view you are in is
|
||||
// the field a debounce is most likely to lose, and quitting
|
||||
// straight from develop is exactly the case worth getting right.
|
||||
write_place(&w, &ctl);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -5012,6 +5461,10 @@ pub fn wire<F>(
|
||||
w.set_library_roll_current(i);
|
||||
on_open_image(path);
|
||||
report_position(&w, &ctl, i as usize);
|
||||
// TRACES: FR-UI-8
|
||||
// Debounced, unlike the two entries into develop: the roll is walked
|
||||
// frame by frame and every step is a different photograph.
|
||||
note_place(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5056,6 +5509,7 @@ pub fn wire<F>(
|
||||
w.set_library_roll_centre(true);
|
||||
on_open(path);
|
||||
report_position(&w, &ctl, row.unwrap_or(0));
|
||||
write_place(&w, &ctl);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -5270,6 +5724,11 @@ pub fn wire<F>(
|
||||
// is only in the Flickable, which is about to be destroyed.
|
||||
ctl.resume_at.set(first_visible);
|
||||
|
||||
// TRACES: FR-UI-8
|
||||
// And where the *next launch* will look for it. Debounced: a flick
|
||||
// reports several of these per screenful.
|
||||
note_place(&w, &ctl);
|
||||
|
||||
// The same position where a rebuilt grid will look for it.
|
||||
//
|
||||
// `scroll-to` is read by `seek()`, which runs on a `scroll-token`
|
||||
@@ -5488,6 +5947,10 @@ pub fn wire<F>(
|
||||
resume_position(&w, &ctl, &coll_ctl, Some(open));
|
||||
|
||||
w.set_show_library(true);
|
||||
// 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.
|
||||
write_place(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6206,6 +6669,11 @@ fn refilter(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
*ctl.offset.borrow_mut() = 0;
|
||||
ctl.requested.borrow_mut().clear();
|
||||
load_window(window, ctl);
|
||||
// TRACES: FR-UI-8
|
||||
// A narrowed grid is part of where you are: coming back to five-star frames
|
||||
// of one person and finding the whole library is the same loss of place as
|
||||
// coming back to the top of it.
|
||||
note_place(window, ctl);
|
||||
}
|
||||
|
||||
fn stop(slot: &RefCell<Option<slint::Timer>>) {
|
||||
|
||||
Reference in New Issue
Block a user