mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 14:07:34 -06:00
LRC-LIB-LIBFOLDERS: a count never covers a name in a narrow panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
59a5d00c60
commit
e2043f44df
@@ -1,4 +1,5 @@
|
||||
use super::albums::MARK_W;
|
||||
use super::sections::visible_right;
|
||||
use super::{Align2, Icon, LightcraftApp, Rect, Sense, Tokens, paint, pos2, register, vec2};
|
||||
|
||||
/// How wide the sidebar's content needs to be, from the widest row of the last frame: rows are
|
||||
@@ -22,6 +23,10 @@ pub(super) const SCROLL_TO_ROW: &str = "left-scroll-to-row";
|
||||
/// removed, they open again.
|
||||
pub(super) const LIBFOLDER_REVEALED: &str = "libfolder-revealed";
|
||||
|
||||
/// The least of its name a row shows before its count gives way (about three letters and an
|
||||
/// ellipsis).
|
||||
const MIN_COUNT_NAME_ROOM: f32 = 36.0;
|
||||
|
||||
/// A row says how wide it needs to be.
|
||||
pub(super) fn note_width(ui: &egui::Ui, w: f32) {
|
||||
ui.data_mut(|d| {
|
||||
@@ -30,6 +35,20 @@ pub(super) fn note_width(ui: &egui::Ui, w: f32) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Where a row's count (and label dot) go when the visible `edge` leaves too little room for the
|
||||
/// name: after the name (at least a bit of it, [`MIN_COUNT_NAME_ROOM`], or all of a shorter name)
|
||||
/// and wholly past the edge, never cut in two by it, reached by scrolling sideways. `None` when
|
||||
/// they stay at the edge, which a row with room (or with neither count nor dot, which has
|
||||
/// nothing to keep whole) always does. A row asks for that much width, drawn or not, so from the
|
||||
/// next frame on the content reaches it and the clamp to the row no longer bites; the name's
|
||||
/// share is at most what [`row_need`] already asks for, so scrolling to the count never narrows
|
||||
/// the content and sends it back.
|
||||
fn count_past_edge(label_left: f32, name_w: f32, mark_w: f32, count_w: Option<f32>, has_mark: bool, edge: f32) -> Option<f32> {
|
||||
let w = count_w.unwrap_or(0.0);
|
||||
let after_name = label_left + name_w.min(MIN_COUNT_NAME_ROOM) + 8.0 + mark_w + w + 18.0;
|
||||
(after_name > edge && (count_w.is_some() || has_mark)).then(|| after_name.max(edge + 4.0 + mark_w + w + 18.0))
|
||||
}
|
||||
|
||||
/// One level of a sidebar tree when the panel has room: callers pass `indent` in these steps.
|
||||
pub(crate) const INDENT_STEP: f32 = 16.0;
|
||||
|
||||
@@ -175,8 +194,13 @@ fn draw_row(app: &mut LightcraftApp, ui: &mut egui::Ui, row: Row) -> egui::Respo
|
||||
let count_color = |n: usize| if n == 0 { t.text_dim.gamma_multiply(0.55) } else { t.text_dim };
|
||||
let count_galley = count.filter(|_| app.ui.show_counts).map(|n| ui.painter().layout_no_wrap(n.to_string(), t.font(12.5), count_color(n)));
|
||||
let label_left = r.left() + 42.0 + indent;
|
||||
let count_left = count_galley.as_ref().map_or(edge - 18.0, |g| edge - 18.0 - g.size().x);
|
||||
let measure = |s: &str| ui.painter().layout_no_wrap(s.to_string(), font.clone(), color).size().x;
|
||||
let full_w = measure(label);
|
||||
// a label's dot sits just before the count
|
||||
let count_w = count_galley.as_ref().map(|g| g.size().x);
|
||||
let past_edge = count_past_edge(label_left, full_w, mark_w, count_w, mark.is_some(), edge);
|
||||
let edge = past_edge.map_or(edge, |e| e.min(r.right()));
|
||||
let count_left = edge - 18.0 - count_w.unwrap_or(0.0);
|
||||
if let Some(c) = mark {
|
||||
let dot = Rect::from_center_size(pos2(count_left - MARK_W / 2.0, r.center().y), vec2(8.0, 8.0));
|
||||
ui.painter().circle_filled(dot.center(), 4.0, c);
|
||||
@@ -184,18 +208,20 @@ fn draw_row(app: &mut LightcraftApp, ui: &mut egui::Ui, row: Row) -> egui::Respo
|
||||
}
|
||||
// the name gives way to the count: cut with an ellipsis, in full on hover
|
||||
let room = count_left - mark_w - 8.0 - label_left;
|
||||
let measure = |s: &str| ui.painter().layout_no_wrap(s.to_string(), font.clone(), color).size().x;
|
||||
let full_w = measure(label);
|
||||
let shown = if full_w <= room { label.to_string() } else { crate::widgets::elide_head(label, room.max(0.0), measure) };
|
||||
let label_rect = ui.painter().text(pos2(label_left, r.center().y), Align2::LEFT_CENTER, &shown, font.clone(), color);
|
||||
register(ui, format!("label:{id}"), label_rect);
|
||||
let resp = if shown != label { resp.on_hover_text(label) } else { resp };
|
||||
note_width(ui, row_need(indent, full_w, mark_w, count_galley.as_ref().map(|g| g.size().x)));
|
||||
let mut needed = row_need(indent, full_w, mark_w, count_galley.as_ref().map(|g| g.size().x));
|
||||
if let Some(galley) = count_galley {
|
||||
let rect = Rect::from_min_size(pos2(edge - 18.0 - galley.size().x, r.center().y - galley.size().y / 2.0), galley.size());
|
||||
register(ui, format!("count:{id}"), rect);
|
||||
ui.painter().galley(rect.min, galley, t.text_dim);
|
||||
}
|
||||
if let Some(e) = past_edge {
|
||||
needed = needed.max(e - r.left());
|
||||
}
|
||||
note_width(ui, needed);
|
||||
resp
|
||||
}
|
||||
|
||||
@@ -216,7 +242,14 @@ pub(super) fn out_of_sight(app: &LightcraftApp, ui: &mut egui::Ui, id: &str, lab
|
||||
let painter = ui.painter();
|
||||
let name_w = painter.layout_no_wrap(label.to_string(), t.font(13.5), t.text_label).size().x;
|
||||
let count_w = count.filter(|_| app.ui.show_counts).map(|n| painter.layout_no_wrap(n.to_string(), t.font(12.5), t.text_dim).size().x);
|
||||
note_width(ui, row_need(indent, name_w, if mark { MARK_W } else { 0.0 }, count_w));
|
||||
let mark_w = if mark { MARK_W } else { 0.0 };
|
||||
let mut needed = row_need(indent, name_w, mark_w, count_w);
|
||||
// a row that drawn would keep its count past the edge asks for the same width as when drawn
|
||||
let edge = r.right().min(visible_right(ui));
|
||||
if let Some(e) = count_past_edge(r.left() + 42.0 + indent, name_w, mark_w, count_w, mark, edge) {
|
||||
needed = needed.max(e - r.left());
|
||||
}
|
||||
note_width(ui, needed);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -229,7 +262,9 @@ fn scroll_asked(ui: &egui::Ui, id: &str) -> bool {
|
||||
/// room for its name up to a share of a panel, so one very long name does not make everything
|
||||
/// scroll: depth does that.
|
||||
fn row_need(indent: f32, name_w: f32, mark_w: f32, count_w: Option<f32>) -> f32 {
|
||||
42.0 + indent + name_w.min(MAX_NAME_NEED) + 18.0 + mark_w + count_w.map_or(0.0, |w| w + 16.0)
|
||||
// (a colour dot with no count keeps the gap after the name that a count's 16 pt includes)
|
||||
let after = count_w.map_or(if mark_w > 0.0 { 8.0 } else { 0.0 }, |w| w + 16.0);
|
||||
42.0 + indent + name_w.min(MAX_NAME_NEED) + 18.0 + mark_w + after
|
||||
}
|
||||
|
||||
/// The disclosure chevron at the left of a tree row (`indent` is the row's): right-pointing
|
||||
|
||||
@@ -51,7 +51,7 @@ pub(super) fn section(
|
||||
|
||||
/// Where what the panel shows of the sidebar's content ends (screen x): the content is wider
|
||||
/// than the panel while the sidebar scrolls sideways.
|
||||
fn visible_right(ui: &egui::Ui) -> f32 {
|
||||
pub(super) fn visible_right(ui: &egui::Ui) -> f32 {
|
||||
ui.data(|d| d.get_temp(egui::Id::unique("left-visible-right"))).unwrap_or(f32::MAX)
|
||||
}
|
||||
|
||||
|
||||
@@ -1600,6 +1600,61 @@ fn a_chain_of_single_folders_starts_open() {
|
||||
assert!(!has(&h, "source:libfolder:/Users/me/Pictures/2024"), "the branching folder waits to be opened");
|
||||
}
|
||||
|
||||
/// In a panel too narrow for a deep row, the count gives way: it is drawn after the name, never
|
||||
/// over it or over the folder's icon.
|
||||
#[test]
|
||||
fn a_count_never_covers_the_name_of_a_deep_row() {
|
||||
// fourteen levels, names wider than the room a row keeps for its name
|
||||
let names: Vec<String> = (1..=14).map(|i| format!("level-{i:02}")).collect();
|
||||
let mut paths = Vec::new();
|
||||
let mut dir = String::new();
|
||||
for name in &names {
|
||||
dir.push('/');
|
||||
dir.push_str(name);
|
||||
paths.push(format!("{dir}/1.jpg"));
|
||||
}
|
||||
// (enough photos at the bottom for counts five digits wide all the way up)
|
||||
paths.extend((0..12_000).map(|i| format!("{dir}/more-{i}.jpg")));
|
||||
let refs: Vec<&str> = paths.iter().map(String::as_str).collect();
|
||||
// (tall enough for every row of the chain to be drawn: rows below the fold are not)
|
||||
let mut h = folders_app_sized(&refs, [1400.0, 1500.0]);
|
||||
let left = widget(&h, "panel:left_panel");
|
||||
drag(&mut h, left.right() - 2.0, -1400.0);
|
||||
assert_eq!(h.app.ui.left_width, crate::state::LEFT_WIDTH.min, "the panel at its narrowest");
|
||||
let mut path = String::new();
|
||||
for name in &names[..names.len() - 1] {
|
||||
path.push('/');
|
||||
path.push_str(name);
|
||||
click(&mut h, &format!("libraryFolderToggle:{path}"));
|
||||
}
|
||||
h.settle(SETTLE);
|
||||
let deep = dir.as_str();
|
||||
let (label, count) = (widget(&h, &format!("label:libfolder:{deep}")), widget(&h, &format!("count:libfolder:{deep}")));
|
||||
assert!(label.width() > 0.0, "the name is shown: {label:?}");
|
||||
assert!(count.left() >= label.right(), "the count comes after the name: {label:?} {count:?}");
|
||||
// a row with room keeps its count at the panel's visible edge
|
||||
let panel = widget(&h, "panel:left_panel");
|
||||
assert!(widget(&h, "count:all").right() <= panel.right());
|
||||
// and no count is cut in two by that edge (5 digits wide here): it is whole, inside or past it
|
||||
let mut past = 0;
|
||||
for (id, r) in h.app.widgets.iter().filter(|(w, _)| w.starts_with("count:libfolder:")) {
|
||||
assert!(r.right() <= panel.right() || r.left() >= panel.right(), "{id} straddles the edge: {r:?} / {panel:?}");
|
||||
past += usize::from(r.left() >= panel.right());
|
||||
}
|
||||
assert!(past > 0, "the deepest rows' counts are past the edge");
|
||||
// at every width, also where the content is only a little wider than the panel (a count that
|
||||
// gave way used to be held back by the row's own end there, and was cut by the edge)
|
||||
for width in (200..=330).step_by(2) {
|
||||
let r = h.request("ui.set", json!({"leftWidth": width}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
let panel = widget(&h, "panel:left_panel");
|
||||
for (id, r) in h.app.widgets.iter().filter(|(w, _)| w.starts_with("count:libfolder:")) {
|
||||
assert!(r.right() <= panel.right() || r.left() >= panel.right(), "at {width} pt {id} straddles the edge: {r:?} / {panel:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removing a folder from the library asks first, then moves its photos to Recently Deleted.
|
||||
#[test]
|
||||
fn removing_a_folder_from_the_library_asks_first() {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -157,6 +157,11 @@
|
||||
the full count took about 14 s instead of about 23 s on a heavily loaded machine; the rest is the text rules of
|
||||
the albums themselves. The counts are the same as before, including for albums that loop through each other.
|
||||
|
||||
### A count never covers a name in the sidebar
|
||||
- In the left panel, the count of a deeply indented row (a folder, keyword or album) no longer prints over its icon
|
||||
and name when the panel is narrow. It follows the name, wholly past the panel's edge, and scrolling sideways
|
||||
reaches it; so does the colour dot of a labelled folder. A row with room for its name keeps its count at the edge, as before.
|
||||
|
||||
### Sharper resampling in HDR and panorama merges
|
||||
- Merged photos (HDR and panorama) are resampled with a sharper filter (Lanczos-3 instead of bilinear). Moving a
|
||||
photo by half a pixel used to lose a third of its finest detail; now it keeps most of it, without halos around
|
||||
|
||||
Reference in New Issue
Block a user