Move the filter bar and the timeline onto the library's Slint global
The date-range fields and their band on the capture-time axis, the timeline's bars, labels, scrub/pinch/pan/zoom callbacks and its sweep/current-bucket/anchored state, and the filter bar's people chips, mode, eyes-open toggle and gesture reference move from AppWindow onto the `Library` global. Rust reaches them through window.global::<Library>() rather than window.set_/get_/on_ on the root, as the earlier commits on this branch did for the grid's cells, selection, ratings and keywords.
This commit is contained in:
@@ -112,7 +112,7 @@ fn wire_selection(
|
||||
// Where the window starts, so the press is recorded as the ordinal
|
||||
// it is rather than as a row that stops meaning this photograph on
|
||||
// the next scroll.
|
||||
let offset = w.get_library_offset().max(0) as usize;
|
||||
let offset = w.global::<Library>().get_library_offset().max(0) as usize;
|
||||
select_row(&w, &ctl, &ids, offset, row as usize, ctrl_held, shift_held);
|
||||
|
||||
// TRACES: FR-UI-2 | FR-UI-4 | FR-CAT-7
|
||||
|
||||
@@ -107,77 +107,87 @@ pub(super) fn wire_filter_ratings_and_people(window: &AppWindow, ctl: &Rc<Librar
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_filter_person_cleared(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
ctl.filter
|
||||
.borrow_mut()
|
||||
.people
|
||||
.retain(|p| *p != id.max(0) as u64);
|
||||
drop_eyes_open_without_people(&w, &ctl);
|
||||
push_people_chips(&w, &ctl);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_filter_person_cleared(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
ctl.filter
|
||||
.borrow_mut()
|
||||
.people
|
||||
.retain(|p| *p != id.max(0) as u64);
|
||||
drop_eyes_open_without_people(&w, &ctl);
|
||||
push_people_chips(&w, &ctl);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_people_listed(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
push_people_roster(&w, &ctl);
|
||||
});
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_people_listed(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
push_people_roster(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_filter_person_toggled(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let person = id.max(0) as u64;
|
||||
{
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
if let Some(at) = f.people.iter().position(|p| *p == person) {
|
||||
f.people.remove(at);
|
||||
} else {
|
||||
f.people.push(person);
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_filter_person_toggled(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let person = id.max(0) as u64;
|
||||
{
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
if let Some(at) = f.people.iter().position(|p| *p == person) {
|
||||
f.people.remove(at);
|
||||
} else {
|
||||
f.people.push(person);
|
||||
}
|
||||
}
|
||||
}
|
||||
drop_eyes_open_without_people(&w, &ctl);
|
||||
push_people_chips(&w, &ctl);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
drop_eyes_open_without_people(&w, &ctl);
|
||||
push_people_chips(&w, &ctl);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_filter_eyes_open_toggled(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let on = {
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.eyes_open = !f.eyes_open;
|
||||
f.eyes_open
|
||||
};
|
||||
w.set_library_filter_eyes_open(on);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_filter_people_mode_toggled(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
{
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.people_mode = match f.people_mode {
|
||||
library::PeopleMode::Any => library::PeopleMode::All,
|
||||
library::PeopleMode::All => library::PeopleMode::Any,
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_filter_eyes_open_toggled(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let on = {
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.eyes_open = !f.eyes_open;
|
||||
f.eyes_open
|
||||
};
|
||||
}
|
||||
push_people_chips(&w, &ctl);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
w.global::<Library>().set_library_filter_eyes_open(on);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_filter_people_mode_toggled(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
{
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.people_mode = match f.people_mode {
|
||||
library::PeopleMode::Any => library::PeopleMode::All,
|
||||
library::PeopleMode::All => library::PeopleMode::Any,
|
||||
};
|
||||
}
|
||||
push_people_chips(&w, &ctl);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
@@ -227,57 +237,59 @@ pub(super) fn wire_filter_dates(window: &AppWindow, ctl: &Rc<LibraryController>)
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_toggle_date_range(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// Whether the *controls* are showing, which is not the same as
|
||||
// whether a range is set — and reading it from the filter was why
|
||||
// pressing this did nothing. The fields are how a range gets set,
|
||||
// so requiring one before they appear is a door locked from the
|
||||
// inside.
|
||||
let on = !w.get_library_range_active();
|
||||
if on {
|
||||
// Seeded from the span the axis is drawn over, which is what
|
||||
// the user is looking at when they ask for "this range".
|
||||
//
|
||||
// Only a seed. If there is no catalog yet, or nothing in it
|
||||
// carries a capture date — dates are read from EXIF as
|
||||
// thumbnails load, so a freshly opened library has none — the
|
||||
// fields simply open empty and wait to be typed into. This
|
||||
// used to `return` in both cases, which meant the button
|
||||
// silently did nothing on exactly the libraries where a person
|
||||
// is most likely to be looking for a date.
|
||||
let seeded = {
|
||||
let borrow = ctl.catalog.borrow();
|
||||
borrow
|
||||
.as_ref()
|
||||
.and_then(|catalog| catalog_span(catalog, &ctl))
|
||||
.map(|span| {
|
||||
zoomed_span(
|
||||
span,
|
||||
*ctl.timeline_zoom.borrow(),
|
||||
*ctl.timeline_centre.borrow(),
|
||||
)
|
||||
})
|
||||
};
|
||||
if let Some(span) = seeded {
|
||||
// Through the same days-inclusive conversion a drag uses,
|
||||
// so a seeded range and a dragged one are the same kind of
|
||||
// thing — and so `show_range` reads this one back out as
|
||||
// the two days it covers.
|
||||
let (from, to) = dragged_range(span, 0.0, 1.0);
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_toggle_date_range(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// Whether the *controls* are showing, which is not the same as
|
||||
// whether a range is set — and reading it from the filter was why
|
||||
// pressing this did nothing. The fields are how a range gets set,
|
||||
// so requiring one before they appear is a door locked from the
|
||||
// inside.
|
||||
let on = !w.global::<Library>().get_library_range_active();
|
||||
if on {
|
||||
// Seeded from the span the axis is drawn over, which is what
|
||||
// the user is looking at when they ask for "this range".
|
||||
//
|
||||
// Only a seed. If there is no catalog yet, or nothing in it
|
||||
// carries a capture date — dates are read from EXIF as
|
||||
// thumbnails load, so a freshly opened library has none — the
|
||||
// fields simply open empty and wait to be typed into. This
|
||||
// used to `return` in both cases, which meant the button
|
||||
// silently did nothing on exactly the libraries where a person
|
||||
// is most likely to be looking for a date.
|
||||
let seeded = {
|
||||
let borrow = ctl.catalog.borrow();
|
||||
borrow
|
||||
.as_ref()
|
||||
.and_then(|catalog| catalog_span(catalog, &ctl))
|
||||
.map(|span| {
|
||||
zoomed_span(
|
||||
span,
|
||||
*ctl.timeline_zoom.borrow(),
|
||||
*ctl.timeline_centre.borrow(),
|
||||
)
|
||||
})
|
||||
};
|
||||
if let Some(span) = seeded {
|
||||
// Through the same days-inclusive conversion a drag uses,
|
||||
// so a seeded range and a dragged one are the same kind of
|
||||
// thing — and so `show_range` reads this one back out as
|
||||
// the two days it covers.
|
||||
let (from, to) = dragged_range(span, 0.0, 1.0);
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.captured_from = Some(from);
|
||||
f.captured_to = Some(to);
|
||||
}
|
||||
} else {
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.captured_from = Some(from);
|
||||
f.captured_to = Some(to);
|
||||
f.captured_from = None;
|
||||
f.captured_to = None;
|
||||
}
|
||||
} else {
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.captured_from = None;
|
||||
f.captured_to = None;
|
||||
}
|
||||
w.set_library_range_active(on);
|
||||
show_range(&w, &ctl);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
w.global::<Library>().set_library_range_active(on);
|
||||
show_range(&w, &ctl);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-6 | FR-UI-2
|
||||
@@ -298,39 +310,41 @@ pub(super) fn wire_filter_dates(window: &AppWindow, ctl: &Rc<LibraryController>)
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_timeline_range_changed(move |a, b| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let Some(catalog) = borrow.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(full) = catalog_span(catalog, &ctl) else {
|
||||
return;
|
||||
};
|
||||
let span = zoomed_span(
|
||||
full,
|
||||
*ctl.timeline_zoom.borrow(),
|
||||
*ctl.timeline_centre.borrow(),
|
||||
);
|
||||
drop(borrow);
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_timeline_range_changed(move |a, b| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let Some(catalog) = borrow.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(full) = catalog_span(catalog, &ctl) else {
|
||||
return;
|
||||
};
|
||||
let span = zoomed_span(
|
||||
full,
|
||||
*ctl.timeline_zoom.borrow(),
|
||||
*ctl.timeline_centre.borrow(),
|
||||
);
|
||||
drop(borrow);
|
||||
|
||||
let (from, to) = dragged_range(span, a, b);
|
||||
{
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.captured_from = Some(from);
|
||||
f.captured_to = Some(to);
|
||||
}
|
||||
let (from, to) = dragged_range(span, a, b);
|
||||
{
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.captured_from = Some(from);
|
||||
f.captured_to = Some(to);
|
||||
}
|
||||
|
||||
// A dragged range cannot be mistyped, so any standing complaint
|
||||
// about the fields is now about a range that no longer exists.
|
||||
w.set_library_range_invalid(false);
|
||||
w.set_library_range_active(true);
|
||||
show_range(&w, &ctl);
|
||||
// Reloads the window, and `load_window` redraws the timeline —
|
||||
// which is what puts the band back under the finger that dropped
|
||||
// it, at the whole days it was snapped to.
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
// A dragged range cannot be mistyped, so any standing complaint
|
||||
// about the fields is now about a range that no longer exists.
|
||||
w.global::<Library>().set_library_range_invalid(false);
|
||||
w.global::<Library>().set_library_range_active(true);
|
||||
show_range(&w, &ctl);
|
||||
// Reloads the window, and `load_window` redraws the timeline —
|
||||
// which is what puts the band back under the finger that dropped
|
||||
// it, at the whole days it was snapped to.
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-6
|
||||
@@ -343,39 +357,41 @@ pub(super) fn wire_filter_dates(window: &AppWindow, ctl: &Rc<LibraryController>)
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_range_edited(move |from, to| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_range_edited(move |from, to| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
|
||||
// Both ends, or neither. A range with one end parsed and the other
|
||||
// silently dropped is a filter nobody asked for, and the grid
|
||||
// going empty is a poor way to find out a date was mistyped.
|
||||
let (Some(from), Some(to)) = (
|
||||
dr_types::parse_date(from.as_str()),
|
||||
dr_types::parse_date(to.as_str()),
|
||||
) else {
|
||||
w.set_library_range_invalid(true);
|
||||
return;
|
||||
};
|
||||
// Both ends, or neither. A range with one end parsed and the other
|
||||
// silently dropped is a filter nobody asked for, and the grid
|
||||
// going empty is a poor way to find out a date was mistyped.
|
||||
let (Some(from), Some(to)) = (
|
||||
dr_types::parse_date(from.as_str()),
|
||||
dr_types::parse_date(to.as_str()),
|
||||
) else {
|
||||
w.global::<Library>().set_library_range_invalid(true);
|
||||
return;
|
||||
};
|
||||
|
||||
// Typed the other way round is a slip, not an error: the user has
|
||||
// said which two days they mean and there is exactly one range
|
||||
// between them.
|
||||
let (from, to) = if from <= to { (from, to) } else { (to, from) };
|
||||
// Typed the other way round is a slip, not an error: the user has
|
||||
// said which two days they mean and there is exactly one range
|
||||
// between them.
|
||||
let (from, to) = if from <= to { (from, to) } else { (to, from) };
|
||||
|
||||
{
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.captured_from = Some(from);
|
||||
// Inclusive of the last day. "To the 5th" means the whole of
|
||||
// the 5th — `parse_date` returns its midnight, and a range
|
||||
// ending there would exclude every photograph taken on the day
|
||||
// the user named. The same rule `dragged_range` applies.
|
||||
f.captured_to = Some(to + 86_400 - 1);
|
||||
}
|
||||
w.set_library_range_invalid(false);
|
||||
w.set_library_range_active(true);
|
||||
show_range(&w, &ctl);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
{
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
f.captured_from = Some(from);
|
||||
// Inclusive of the last day. "To the 5th" means the whole of
|
||||
// the 5th — `parse_date` returns its midnight, and a range
|
||||
// ending there would exclude every photograph taken on the day
|
||||
// the user named. The same rule `dragged_range` applies.
|
||||
f.captured_to = Some(to + 86_400 - 1);
|
||||
}
|
||||
w.global::<Library>().set_library_range_invalid(false);
|
||||
w.global::<Library>().set_library_range_active(true);
|
||||
show_range(&w, &ctl);
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,7 +527,9 @@ fn drop_eyes_open_without_people(window: &AppWindow, ctl: &Rc<LibraryController>
|
||||
let mut f = ctl.filter.borrow_mut();
|
||||
if f.people.is_empty() && f.eyes_open {
|
||||
f.eyes_open = false;
|
||||
window.set_library_filter_eyes_open(false);
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_filter_eyes_open(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +537,7 @@ pub(super) fn push_people_chips(window: &AppWindow, ctl: &Rc<LibraryController>)
|
||||
let ids = ctl.filter.borrow().people.clone();
|
||||
if ids.is_empty() {
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_filter_people(slint::ModelRc::new(slint::VecModel::from(
|
||||
Vec::<PersonChip>::new(),
|
||||
)));
|
||||
@@ -563,11 +582,15 @@ pub(super) fn push_people_chips(window: &AppWindow, ctl: &Rc<LibraryController>)
|
||||
})
|
||||
.collect();
|
||||
|
||||
window.set_library_filter_people(slint::ModelRc::new(slint::VecModel::from(chips)));
|
||||
window.set_library_filter_people_all(matches!(
|
||||
ctl.filter.borrow().people_mode,
|
||||
library::PeopleMode::All
|
||||
));
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_filter_people(slint::ModelRc::new(slint::VecModel::from(chips)));
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_filter_people_all(matches!(
|
||||
ctl.filter.borrow().people_mode,
|
||||
library::PeopleMode::All
|
||||
));
|
||||
push_people_roster(window, ctl);
|
||||
}
|
||||
|
||||
@@ -632,7 +655,9 @@ fn push_people_roster(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
rows.sort_by_key(|(unnamed, faces, _)| (*unnamed, -faces));
|
||||
|
||||
let rows: Vec<PersonChip> = rows.into_iter().map(|(_, _, chip)| chip).collect();
|
||||
window.set_library_people(slint::ModelRc::new(slint::VecModel::from(rows)));
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_people(slint::ModelRc::new(slint::VecModel::from(rows)));
|
||||
}
|
||||
|
||||
pub(super) fn refilter(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
@@ -698,10 +723,14 @@ fn show_range(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
let text = |t: Option<i64>| -> slint::SharedString {
|
||||
t.map(dr_types::format_date).unwrap_or_default().into()
|
||||
};
|
||||
window.set_library_range_from(text(f.captured_from));
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_range_from(text(f.captured_from));
|
||||
// The stored end is the last second of the closing day; naming that day is
|
||||
// what the user typed and what they should read back.
|
||||
window.set_library_range_to(text(f.captured_to.map(|t| t - 86_400 + 1)));
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_range_to(text(f.captured_to.map(|t| t - 86_400 + 1)));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -184,18 +184,20 @@ pub fn wire<F>(
|
||||
// table is a compiled-in constant, so there is nothing to be fresh about
|
||||
// and nothing to recompute — a callback to fill it would only be a way for
|
||||
// it to be empty the first time the sheet opens.
|
||||
window.set_library_gestures(slint::ModelRc::new(slint::VecModel::from(
|
||||
crate::gestures::rows()
|
||||
.into_iter()
|
||||
.map(|r| GestureRow {
|
||||
heading: r.heading.into(),
|
||||
title: r.title.into(),
|
||||
touch: r.touch.into(),
|
||||
pointer: r.pointer.into(),
|
||||
keys: r.keys.into(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)));
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_gestures(slint::ModelRc::new(slint::VecModel::from(
|
||||
crate::gestures::rows()
|
||||
.into_iter()
|
||||
.map(|r| GestureRow {
|
||||
heading: r.heading.into(),
|
||||
title: r.title.into(),
|
||||
touch: r.touch.into(),
|
||||
pointer: r.pointer.into(),
|
||||
keys: r.keys.into(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)));
|
||||
|
||||
// Shared rather than moved: a click and `Return` both open an image, and
|
||||
// they are two callbacks.
|
||||
@@ -621,12 +623,13 @@ fn wire_grid_sync_and_load(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
let centre = *ctl.timeline_centre.borrow();
|
||||
if let Some(full) = catalog_span(catalog, &ctl) {
|
||||
let (from, to) = zoomed_span(full, zoom, centre);
|
||||
w.set_library_current_bucket(when as i32);
|
||||
w.set_library_current_fraction(
|
||||
w.global::<Library>()
|
||||
.set_library_current_bucket(when as i32);
|
||||
w.global::<Library>().set_library_current_fraction(
|
||||
((when - from) as f64 / (to - from).max(1) as f64)
|
||||
.clamp(0.0, 1.0) as f32,
|
||||
);
|
||||
w.set_library_timeline_anchored(true);
|
||||
w.global::<Library>().set_library_timeline_anchored(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,9 @@ pub fn open(
|
||||
window.set_library_status("Starting…".into());
|
||||
// Always visible: two folders one letter apart are easy to confuse, and a
|
||||
// scan of the wrong one is indistinguishable from a broken scan.
|
||||
window.set_library_root_label(library_root_label(&conn.account).into());
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_root_label(library_root_label(&conn.account).into());
|
||||
|
||||
// An empty filter would walk the whole tree and match nothing, which looks
|
||||
// exactly like a broken scan. Say so instead.
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::rc::Rc;
|
||||
use slint::ComponentHandle;
|
||||
|
||||
use crate::library;
|
||||
use crate::AppWindow;
|
||||
use crate::{AppWindow, Library};
|
||||
|
||||
use super::controller::{stop, LibraryController};
|
||||
use super::open::schedule_reload;
|
||||
@@ -302,7 +302,7 @@ pub(super) fn start_sweep(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
Ok(m) => m,
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => return,
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||
w.set_library_sweep_total(0);
|
||||
w.global::<Library>().set_library_sweep_total(0);
|
||||
job.fail("stopped without finishing");
|
||||
stop(&ctl_cb.sweep_timer);
|
||||
return;
|
||||
@@ -311,13 +311,16 @@ pub(super) fn start_sweep(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
|
||||
match msg {
|
||||
library::SweepMessage::Total(n) => {
|
||||
w.set_library_sweep_total(n as i32);
|
||||
w.set_library_sweep_done(0);
|
||||
w.global::<Library>().set_library_sweep_total(n as i32);
|
||||
w.global::<Library>().set_library_sweep_done(0);
|
||||
job.total(n);
|
||||
}
|
||||
library::SweepMessage::Progress { done, dated } => {
|
||||
w.set_library_sweep_done(done as i32);
|
||||
job.progress(done, w.get_library_sweep_total() as usize);
|
||||
w.global::<Library>().set_library_sweep_done(done as i32);
|
||||
job.progress(
|
||||
done,
|
||||
w.global::<Library>().get_library_sweep_total() as usize,
|
||||
);
|
||||
// Rebuild as it goes: the histogram growing while the
|
||||
// sweep runs is the visible sign it is working.
|
||||
if dated > 0 {
|
||||
@@ -330,7 +333,7 @@ pub(super) fn start_sweep(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
library::SweepMessage::Finished { dated } => {
|
||||
log::info!("sweep finished: {dated} dated");
|
||||
job.finish(format!("{dated} dated"));
|
||||
w.set_library_sweep_total(0);
|
||||
w.global::<Library>().set_library_sweep_total(0);
|
||||
{
|
||||
let borrow = ctl_cb.catalog.borrow();
|
||||
if let Some(catalog) = borrow.as_ref() {
|
||||
|
||||
@@ -95,8 +95,12 @@ pub(super) fn refresh_timeline(window: &AppWindow, catalog: &Catalog, ctl: &Rc<L
|
||||
None => {
|
||||
// No dated images yet. An empty histogram is honest — EXIF is read
|
||||
// as thumbnails load, so this populates as the user browses.
|
||||
window.set_library_timeline(slint::ModelRc::new(slint::VecModel::from(vec![])));
|
||||
window.set_library_timeline_label(slint::SharedString::new());
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_timeline(slint::ModelRc::new(slint::VecModel::from(vec![])));
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_timeline_label(slint::SharedString::new());
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -206,14 +210,27 @@ pub(super) fn refresh_timeline(window: &AppWindow, catalog: &Catalog, ctl: &Rc<L
|
||||
buckets.get(i).map(|b| b.start)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
window.set_library_current_bucket(lit as i32);
|
||||
window.set_library_current_fraction(fraction);
|
||||
window.set_library_range_from_fraction(band_from);
|
||||
window.set_library_range_to_fraction(band_to);
|
||||
window.set_library_timeline_anchored(current.is_some());
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_current_bucket(lit as i32);
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_current_fraction(fraction);
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_range_from_fraction(band_from);
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_range_to_fraction(band_to);
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_timeline_anchored(current.is_some());
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_timeline_label(format!("{} – {}", format_date(from), format_date(to)).into());
|
||||
window.set_library_timeline(slint::ModelRc::new(slint::VecModel::from(bars)));
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_timeline(slint::ModelRc::new(slint::VecModel::from(bars)));
|
||||
}
|
||||
|
||||
/// Whether this bucket size is fine enough for month labels to mean anything.
|
||||
@@ -904,7 +921,9 @@ fn push_filter(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
.global::<Library>()
|
||||
.set_library_local_only(filter.local_only);
|
||||
ctl.local_only.set(filter.local_only);
|
||||
window.set_library_filter_eyes_open(filter.eyes_open);
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_filter_eyes_open(filter.eyes_open);
|
||||
push_people_chips(window, ctl);
|
||||
}
|
||||
|
||||
@@ -1019,30 +1038,32 @@ pub(super) fn wire_timeline(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_timeline_pan(move |fraction| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let Some(catalog) = borrow.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(full) = catalog_span(catalog, &ctl) else {
|
||||
return;
|
||||
};
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_timeline_pan(move |fraction| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let Some(catalog) = borrow.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(full) = catalog_span(catalog, &ctl) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// A fraction of the *visible* span, so dragging half the axis
|
||||
// moves half a span's worth of time whatever the zoom — and a
|
||||
// small movement produces a small shift rather than nothing.
|
||||
let zoom = *ctl.timeline_zoom.borrow();
|
||||
let (from, to) = zoomed_span(full, zoom, *ctl.timeline_centre.borrow());
|
||||
let shift = ((to - from) as f64 * fraction as f64) as i64;
|
||||
if shift == 0 {
|
||||
return;
|
||||
}
|
||||
// A fraction of the *visible* span, so dragging half the axis
|
||||
// moves half a span's worth of time whatever the zoom — and a
|
||||
// small movement produces a small shift rather than nothing.
|
||||
let zoom = *ctl.timeline_zoom.borrow();
|
||||
let (from, to) = zoomed_span(full, zoom, *ctl.timeline_centre.borrow());
|
||||
let shift = ((to - from) as f64 * fraction as f64) as i64;
|
||||
if shift == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let centre = ctl.timeline_centre.borrow().unwrap_or((from + to) / 2) + shift;
|
||||
*ctl.timeline_centre.borrow_mut() = Some(centre.clamp(full.0, full.1));
|
||||
refresh_timeline(&w, catalog, &ctl);
|
||||
});
|
||||
let centre = ctl.timeline_centre.borrow().unwrap_or((from + to) / 2) + shift;
|
||||
*ctl.timeline_centre.borrow_mut() = Some(centre.clamp(full.0, full.1));
|
||||
refresh_timeline(&w, catalog, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
// The wheel zooms the axis: a sidebar is a scale, not a list.
|
||||
@@ -1052,11 +1073,13 @@ pub(super) fn wire_timeline(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_timeline_zoom(move |delta| {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
apply_zoom(&w, &ctl, delta);
|
||||
}
|
||||
});
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_timeline_zoom(move |delta| {
|
||||
if let Some(w) = weak.upgrade() {
|
||||
apply_zoom(&w, &ctl, delta);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Dragging the histogram moves the grid through time.
|
||||
@@ -1067,25 +1090,27 @@ pub(super) fn wire_timeline(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_scrub_fraction(move |f| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let Some(catalog) = borrow.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(full) = catalog_span(catalog, &ctl) else {
|
||||
return;
|
||||
};
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_scrub_fraction(move |f| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let Some(catalog) = borrow.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(full) = catalog_span(catalog, &ctl) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (from, to) = zoomed_span(
|
||||
full,
|
||||
*ctl.timeline_zoom.borrow(),
|
||||
*ctl.timeline_centre.borrow(),
|
||||
);
|
||||
let when = instant_at((from, to), f);
|
||||
drop(borrow);
|
||||
scrub_to(&w, &ctl, when);
|
||||
});
|
||||
let (from, to) = zoomed_span(
|
||||
full,
|
||||
*ctl.timeline_zoom.borrow(),
|
||||
*ctl.timeline_centre.borrow(),
|
||||
);
|
||||
let when = instant_at((from, to), f);
|
||||
drop(borrow);
|
||||
scrub_to(&w, &ctl, when);
|
||||
});
|
||||
}
|
||||
|
||||
// Pinch, for tablet: no wheel there, so this is the only way to reach the
|
||||
@@ -1097,30 +1122,32 @@ pub(super) fn wire_timeline(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_library_timeline_pinch(move |ratio| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if !(0.01..=100.0).contains(&ratio) {
|
||||
return;
|
||||
}
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_timeline_pinch(move |ratio| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if !(0.01..=100.0).contains(&ratio) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut accum = ctl.pinch_accum.borrow_mut();
|
||||
*accum *= ratio;
|
||||
let mut accum = ctl.pinch_accum.borrow_mut();
|
||||
*accum *= ratio;
|
||||
|
||||
// Whole doublings out of the accumulated ratio, remainder carried.
|
||||
//
|
||||
// `log2` rather than repeated halving: one update can carry a
|
||||
// large ratio — a fast spread, or a trackpad reporting coarsely —
|
||||
// and stepping once per update would turn an 8× pinch into a
|
||||
// single level instead of three.
|
||||
let steps = accum.log2().trunc() as i32;
|
||||
if steps == 0 {
|
||||
return;
|
||||
}
|
||||
*accum /= (2.0f32).powi(steps);
|
||||
drop(accum);
|
||||
// Whole doublings out of the accumulated ratio, remainder carried.
|
||||
//
|
||||
// `log2` rather than repeated halving: one update can carry a
|
||||
// large ratio — a fast spread, or a trackpad reporting coarsely —
|
||||
// and stepping once per update would turn an 8× pinch into a
|
||||
// single level instead of three.
|
||||
let steps = accum.log2().trunc() as i32;
|
||||
if steps == 0 {
|
||||
return;
|
||||
}
|
||||
*accum /= (2.0f32).powi(steps);
|
||||
drop(accum);
|
||||
|
||||
apply_zoom(&w, &ctl, steps);
|
||||
});
|
||||
apply_zoom(&w, &ctl, steps);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
let window_size = *ctl.window.borrow();
|
||||
let offset = (*ctl.offset.borrow()).min(total.saturating_sub(window_size.min(total)));
|
||||
*ctl.offset.borrow_mut() = offset;
|
||||
window.set_library_offset(offset as i32);
|
||||
window.global::<Library>().set_library_offset(offset as i32);
|
||||
|
||||
// The axis the scrubber draws. A `MIN`/`MAX` and a `GROUP BY` over the
|
||||
// whole scope — 5.7 ms on 24,000 images — and the bars do not change as the
|
||||
@@ -308,7 +308,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
Some((lo, hi)) => (lo.min(t), hi.max(t)),
|
||||
})
|
||||
});
|
||||
window.set_library_window_label(
|
||||
window.global::<Library>().set_library_window_label(
|
||||
match span {
|
||||
Some((lo, hi)) => format!("{} – {}", format_date(lo), format_date(hi)),
|
||||
// Nothing here has a date yet: EXIF is read as thumbnails load, so
|
||||
|
||||
Reference in New Issue
Block a user