mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-11 16:31:32 -06:00
Merge PR #2162: LR-VIEW-PHOTOGRID: the active photo's selection ring shows on light themes and differs from the other selected photos by weight
docs/parity-checklist.md: kept the newer header date and both sides' change clauses; revision rows are the union.
This commit is contained in:
@@ -221,7 +221,7 @@ fn theme_preview(ui: &mut egui::Ui, kind: crate::theme::ThemeKind, width: f32) {
|
||||
let thumb = cell.shrink(2.5);
|
||||
painter.rect_filled(thumb, 0.0, Color32::from_rgb(0x6f + 8 * i as u8, 0x80, 0x8f));
|
||||
if selected {
|
||||
painter.rect_stroke(thumb, 0.0, Stroke::new(1.0, p.pick), StrokeKind::Outside);
|
||||
crate::panels::grid::selection_ring(&painter, &p, thumb, lightcraft_engine::SelectionState::Active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! pan) and **Survey** (the selected photos tiled). Rating, flag and label keys act on the active
|
||||
//! photo only here, and with Auto Advance on they move to the next candidate / photo.
|
||||
|
||||
use egui::{Align2, Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2};
|
||||
use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2};
|
||||
use lightcraft_catalog::{Flag, PhotoId};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -286,7 +286,7 @@ fn photo_tile(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, slot: Slo
|
||||
display = super::detail::fit_rect(img_area, texture_aspect, zoom, native, ppp, app.ui.pan);
|
||||
p.image(tex.tex.id(), display, uv, Color32::WHITE);
|
||||
} else {
|
||||
p.rect_filled(img.intersect(img_area), 0.0, Tokens::get(ui.ctx()).skeleton);
|
||||
p.rect_filled(img.intersect(img_area), 0.0, t.skeleton);
|
||||
}
|
||||
if photo.flag == Flag::Reject {
|
||||
p.rect_filled(display, 0.0, Color32::from_black_alpha(110));
|
||||
@@ -296,7 +296,7 @@ fn photo_tile(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, slot: Slo
|
||||
}
|
||||
let active = app.session.selection.active == Some(id);
|
||||
if active {
|
||||
p.rect_stroke(display.intersect(img_area.shrink(1.0)), 0.0, Stroke::new(2.0, Color32::WHITE), StrokeKind::Outside);
|
||||
super::grid::selection_ring(&p, &t, display.intersect(img_area.shrink(3.0)), lightcraft_engine::SelectionState::Active);
|
||||
}
|
||||
// Compare and Reference are where sharpness is judged: as in the loupe, say when what is on
|
||||
// screen isn't the render on its way (a thumbnail, the last render after an edit, a magnified
|
||||
|
||||
@@ -97,7 +97,6 @@ pub(crate) fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) {
|
||||
crate::widgets::announce(&resp, film_spoken(app.session.catalog.photo(*id).map_or("", |p| p.file_name.as_str()), failed));
|
||||
register(ui, format!("film:{}", id.0), cr);
|
||||
let state = app.session.selection.state_of(*id);
|
||||
let sel = state == lightcraft_engine::SelectionState::Active;
|
||||
let p = ui.painter();
|
||||
let label = app.session.catalog.photo(*id).and_then(|ph| ph.label);
|
||||
let selected = state != lightcraft_engine::SelectionState::NotSelected;
|
||||
@@ -129,11 +128,7 @@ pub(crate) fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) {
|
||||
let s = (img_area.width() / tw as f32).min(img_area.height() / th as f32);
|
||||
let fr = Rect::from_center_size(img_area.center(), vec2(tw as f32 * s, th as f32 * s));
|
||||
p.image(tex.tex.id(), fr, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
if sel {
|
||||
p.rect_stroke(fr, 0.0, Stroke::new(1.5, Color32::WHITE), StrokeKind::Outside);
|
||||
} else if state == lightcraft_engine::SelectionState::Selected {
|
||||
p.rect_stroke(fr, 0.0, Stroke::new(1.5, Color32::from_gray(170)), StrokeKind::Outside);
|
||||
}
|
||||
crate::panels::grid::selection_ring(p, &t, fr, state);
|
||||
if app.ui.settings.film_badges
|
||||
&& let Some(ph) = app.session.catalog.photo(*id)
|
||||
{
|
||||
|
||||
@@ -651,6 +651,25 @@ pub fn layout(aspects: &[f32], groups: &[(usize, usize)], avail_w: f32, target:
|
||||
out
|
||||
}
|
||||
|
||||
/// The ring round a selected photo, drawn outside `photo`. The active one is 3 pt in
|
||||
/// [`Tokens::ring_active`] with a 1 pt line in [`Tokens::ring_inner`] over its inner edge, against
|
||||
/// the photo (so the ring reads on a white photo and on a black one). The others are a single 1.5 pt line in
|
||||
/// [`Tokens::ring_selected`]; the two are told apart by weight, not by lightness alone.
|
||||
pub(crate) fn selection_ring(p: &egui::Painter, t: &Tokens, photo: Rect, state: lightcraft_engine::SelectionState) {
|
||||
use lightcraft_engine::SelectionState;
|
||||
match state {
|
||||
SelectionState::Active => {
|
||||
// both on the same rect, so no seam opens between them at a fractional scale
|
||||
p.rect_stroke(photo, 0.0, Stroke::new(3.0, t.ring_active), StrokeKind::Outside);
|
||||
p.rect_stroke(photo, 0.0, Stroke::new(1.0, t.ring_inner), StrokeKind::Outside);
|
||||
}
|
||||
SelectionState::Selected => {
|
||||
p.rect_stroke(photo, 0.0, Stroke::new(1.5, t.ring_selected), StrokeKind::Outside);
|
||||
}
|
||||
SelectionState::NotSelected => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// A date header: "Wednesday, 30 September 2026 · 12 photos". Clicking it selects the group
|
||||
/// (Cmd/Shift: adds to the selection).
|
||||
fn group_header(app: &mut LightcraftApp, ui: &mut egui::Ui, run: &DateRun, ids: &[PhotoId], r: Rect, pinned: bool) {
|
||||
@@ -772,7 +791,6 @@ fn cell(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, r: Rect, square
|
||||
let selected = state != lightcraft_engine::SelectionState::NotSelected;
|
||||
let spoken = cell_description(app, &photo);
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, selected, &spoken));
|
||||
let active = state == lightcraft_engine::SelectionState::Active;
|
||||
let p = ui.painter();
|
||||
let img_rect = if square {
|
||||
let base = if selected { t.cell_selected } else { t.cell };
|
||||
@@ -781,7 +799,7 @@ fn cell(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId, r: Rect, square
|
||||
} else {
|
||||
r
|
||||
};
|
||||
paint_cell_thumbnail(app, p, &t, id, img_rect, ThumbFlags { square, selected, active, onscreen, ppp });
|
||||
paint_cell_thumbnail(app, p, &t, id, img_rect, ThumbFlags { square, state, onscreen, ppp });
|
||||
// labels and badges
|
||||
paint_cell_caption(app, p, &t, &photo, r, square);
|
||||
paint_cell_badges(app, p, &t, &photo, img_rect, BadgeFlags { square, selected, hovered: resp.hovered() });
|
||||
@@ -814,7 +832,7 @@ fn cell_description(app: &LightcraftApp, photo: &lightcraft_catalog::Photo) -> S
|
||||
}
|
||||
|
||||
fn paint_cell_thumbnail(app: &mut LightcraftApp, p: &egui::Painter, t: &Tokens, id: PhotoId, img_rect: Rect, flags: ThumbFlags) {
|
||||
let ThumbFlags { square, selected, active, onscreen, ppp } = flags;
|
||||
let ThumbFlags { square, state, onscreen, ppp } = flags;
|
||||
let size = thumb_px(img_rect.width().max(img_rect.height()), ppp);
|
||||
request_thumb(app, id, size, if onscreen { 10 } else { 5 });
|
||||
if let Some(tex) = app.renderer.thumb(id) {
|
||||
@@ -822,22 +840,12 @@ fn paint_cell_thumbnail(app: &mut LightcraftApp, p: &egui::Painter, t: &Tokens,
|
||||
// Preserve the decoded preview's ratio when it differs from the catalog dimensions.
|
||||
let fit = super::detail::fit_texture_rect(img_rect, [tw, th]);
|
||||
p.image(tex.tex.id(), fit, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
if active {
|
||||
p.rect_stroke(fit.expand(if square { 2.0 } else { 0.0 }), 0.0, Stroke::new(2.0, Color32::WHITE), StrokeKind::Outside);
|
||||
} else if selected {
|
||||
p.rect_stroke(fit, 0.0, Stroke::new(2.0, Color32::from_gray(170)), StrokeKind::Outside);
|
||||
}
|
||||
selection_ring(p, t, fit, state);
|
||||
} else {
|
||||
let ph = img_rect.shrink(if square { 20.0 } else { 0.0 });
|
||||
p.rect_filled(ph, 0.0, t.skeleton);
|
||||
// not drawn yet (or unreadable): the selection is still visible
|
||||
if active {
|
||||
p.rect_stroke(ph, 0.0, Stroke::new(2.0, Color32::WHITE), StrokeKind::Outside);
|
||||
} else if selected {
|
||||
// a light theme's skeleton is itself mid-grey: the ring takes the dim text colour
|
||||
let ring = if t.skeleton.r() > 0x80 { t.text_dim } else { Color32::from_gray(170) };
|
||||
p.rect_stroke(ph, 0.0, Stroke::new(2.0, ring), StrokeKind::Outside);
|
||||
}
|
||||
selection_ring(p, t, ph, state);
|
||||
if app.renderer.failure(Slot::Thumb(id)).is_some() {
|
||||
// unreadable / missing file
|
||||
p.text(ph.center(), Align2::CENTER_CENTER, "!", t.semibold(18.0), t.text);
|
||||
@@ -983,8 +991,7 @@ fn cell_interaction(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Res
|
||||
|
||||
struct ThumbFlags {
|
||||
square: bool,
|
||||
selected: bool,
|
||||
active: bool,
|
||||
state: lightcraft_engine::SelectionState,
|
||||
onscreen: bool,
|
||||
ppp: f32,
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ fn card(app: &mut LightcraftApp, ui: &mut egui::Ui, person: &Person, r: Rect, ed
|
||||
p.rect_filled(pill, 4.0, Color32::from_black_alpha(175));
|
||||
p.galley(pill.min + vec2(5.0, 3.0), g, Color32::WHITE);
|
||||
if selected {
|
||||
p.rect_stroke(face, 3.0, Stroke::new(2.0, Color32::WHITE), StrokeKind::Outside);
|
||||
super::grid::selection_ring(p, &t, face, lightcraft_engine::SelectionState::Active);
|
||||
} else if resp.hovered() {
|
||||
p.rect_stroke(face, 3.0, Stroke::new(1.0, t.text_dim), StrokeKind::Outside);
|
||||
}
|
||||
|
||||
@@ -318,3 +318,44 @@ fn info_panel_label_in_compare_labels_the_active_photo_and_shows_it() {
|
||||
assert_eq!(photo(&h, cand).rating, 3);
|
||||
assert_eq!(photo(&h, sel).rating, 0, "the select is not rated with it");
|
||||
}
|
||||
|
||||
/// Survey frames its active tile in the theme's active ring, which must not be the white that
|
||||
/// vanishes on a light theme's canvas.
|
||||
#[test]
|
||||
fn survey_rings_the_active_tile_in_the_theme_colour() {
|
||||
use egui::epaint::Shape;
|
||||
let mut h = demo();
|
||||
h.app.ui.settings.appearance_mode = crate::state::AppearanceMode::Light;
|
||||
let vis: Vec<u64> = h.app.session.visible_cloned().iter().map(|p| p.0).collect();
|
||||
h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[0], vis[1], vis[2]], "active": vis[1]}}), T);
|
||||
let r = h.request("engine.execute", json!({"command": "view.survey"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
let t = crate::theme::Tokens::get(&h.view.ctx);
|
||||
assert_ne!(t.ring_active, egui::Color32::WHITE);
|
||||
// the filmstrip below rings its active cell the same way: only the tile counts here
|
||||
let films: Vec<egui::Rect> = h.app.widgets.iter().filter(|(w, _)| w.starts_with("film:")).map(|(_, r)| *r).collect();
|
||||
let rings: Vec<(f32, egui::Color32)> = h
|
||||
.view
|
||||
.painted_shapes()
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
Shape::Rect(rs)
|
||||
if rs.stroke_kind == egui::StrokeKind::Outside
|
||||
&& rs.stroke.color == t.ring_active
|
||||
&& !films.iter().any(|f| f.intersects(rs.rect)) =>
|
||||
{
|
||||
Some((rs.stroke.width, rs.stroke.color))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(rings, vec![(3.0, t.ring_active)], "one active tile, ringed once");
|
||||
let white = h
|
||||
.view
|
||||
.painted_shapes()
|
||||
.iter()
|
||||
.filter(|s| matches!(s, Shape::Rect(rs) if rs.stroke.width == 2.0 && rs.stroke.color == egui::Color32::WHITE))
|
||||
.count();
|
||||
assert_eq!(white, 0, "no white 2 pt frame");
|
||||
}
|
||||
|
||||
@@ -59,3 +59,43 @@ fn filmstrip_stack_badge_expands_and_collapses_the_stack() {
|
||||
assert!(h.app.session.catalog.stack(StackId(1)).unwrap().collapsed, "a second click collapses it");
|
||||
assert!(!h.app.session.visible_cloned().contains(&member));
|
||||
}
|
||||
|
||||
/// The filmstrip rings its selected photos by the same rule as the grid, in the theme's colours:
|
||||
/// the active photo's ring is heavier than the other selected photo's, and a light theme gets no
|
||||
/// fixed white or grey ring.
|
||||
#[test]
|
||||
fn the_filmstrip_marks_the_active_photo_heavier_than_the_other_selected_one() {
|
||||
use egui::epaint::Shape;
|
||||
let mut session = lightcraft_engine::Session::with_demo();
|
||||
let ids = session.visible_cloned();
|
||||
let (a, b, c) = (ids[0], ids[1], ids[2]);
|
||||
let app = LightcraftApp::new(session, Services { png: None, ..Default::default() });
|
||||
let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
|
||||
h.app.ui.settings.appearance_mode = crate::state::AppearanceMode::Light;
|
||||
let r = h.request("ui.set", json!({"view": "detail"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
let r = h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [a.0, b.0]}}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
let t = crate::theme::Tokens::get(&h.view.ctx);
|
||||
assert!(crate::theme::current(&h.view.ctx).is_some_and(|k| !k.is_dark()));
|
||||
let active = h.app.session.selection.active.expect("active");
|
||||
let other = if active == a { b } else { a };
|
||||
let shapes = h.view.painted_shapes();
|
||||
let strokes = |id: lightcraft_catalog::PhotoId| -> Vec<(f32, egui::Color32)> {
|
||||
let cell = h.app.widgets.iter().find(|(w, _)| *w == format!("film:{}", id.0)).map(|(_, r)| *r).expect("cell drawn");
|
||||
shapes
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
Shape::Rect(rs) if rs.stroke.width > 0.0 && rs.stroke_kind == egui::StrokeKind::Outside && cell.contains_rect(rs.rect) => {
|
||||
Some((rs.stroke.width, rs.stroke.color))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let (on_active, on_other, on_unselected) = (strokes(active), strokes(other), strokes(c));
|
||||
assert_eq!(on_active, vec![(3.0, t.ring_active), (1.0, t.ring_inner)]);
|
||||
assert_eq!(on_other, vec![(1.5, t.ring_selected)]);
|
||||
assert!(on_unselected.is_empty(), "{on_unselected:?}");
|
||||
}
|
||||
|
||||
@@ -133,27 +133,56 @@ fn grid_headers_selection_scrolling_stacks_and_shape_changes() {
|
||||
}
|
||||
|
||||
/// Selected photos whose thumbnails have not loaded (or can't) are still marked selected in the
|
||||
/// justified grid (issue #298): the active one white, the others grey.
|
||||
/// justified grid (issue #298), in a light theme too (a white ring vanished on the light grid):
|
||||
/// the ring of the active photo is heavier than the others' and both are the theme's colours, not
|
||||
/// fixed white and grey.
|
||||
#[test]
|
||||
fn unloaded_thumbnails_show_their_selection() {
|
||||
use egui::epaint::Shape;
|
||||
let mut h = grid(ViewMode::PhotoGrid);
|
||||
h.app.ui.settings.appearance_mode = crate::state::AppearanceMode::Light;
|
||||
h.step();
|
||||
h.request("engine.execute", json!({"command": "library.selectAll"}), T);
|
||||
let img = h.snapshot(SETTLE);
|
||||
assert_eq!(h.view.ctx.pixels_per_point(), 1.0);
|
||||
h.settle(SETTLE);
|
||||
let t = crate::theme::Tokens::get(&h.view.ctx);
|
||||
assert!(crate::theme::current(&h.view.ctx).is_some_and(|k| !k.is_dark()), "light theme");
|
||||
let active = h.app.session.selection.active.expect("active");
|
||||
let canvas = h.app.canvas_rect.expect("grid drawn");
|
||||
let shapes = h.view.painted_shapes();
|
||||
// the strokes drawn around a photo's cell in paint order (the widest first): (width, colour)
|
||||
let ring = |r: egui::Rect| -> Vec<(f32, egui::Color32)> {
|
||||
shapes
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
Shape::Rect(rs)
|
||||
if rs.stroke.width > 0.0 && rs.stroke_kind == egui::StrokeKind::Outside && (rs.rect.center() - r.center()).length() < 0.5 =>
|
||||
{
|
||||
Some((rs.stroke.width, rs.stroke.color))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let mut checked = 0;
|
||||
let mut saw_active = false;
|
||||
for (w, r) in h.app.widgets.iter().filter(|(w, _)| w.starts_with("thumb:")) {
|
||||
if !canvas.shrink(4.0).contains_rect(*r) || r.min.x < 6.0 {
|
||||
if !canvas.shrink(4.0).contains_rect(*r) {
|
||||
continue;
|
||||
}
|
||||
let id = w.trim_start_matches("thumb:").parse::<u64>().unwrap();
|
||||
let want = if id == active.0 { egui::Color32::WHITE } else { egui::Color32::from_gray(170) };
|
||||
let px = img[((r.min.x - 1.0) as usize, r.center().y as usize)];
|
||||
assert_eq!(px, want, "photo {id}");
|
||||
let strokes = ring(*r);
|
||||
if id == active.0 {
|
||||
assert_ne!(t.ring_active, egui::Color32::WHITE, "the light theme's ring must not vanish on its grid");
|
||||
assert_eq!(strokes, vec![(3.0, t.ring_active), (1.0, t.ring_inner)], "photo {id}: active ring");
|
||||
saw_active = true;
|
||||
} else {
|
||||
assert_eq!(strokes.iter().map(|(w, _)| *w).sum::<f32>(), 1.5, "photo {id}: selected ring {strokes:?}");
|
||||
assert_eq!(strokes.iter().map(|(_, c)| *c).collect::<Vec<_>>(), vec![t.ring_selected], "photo {id}");
|
||||
}
|
||||
checked += 1;
|
||||
}
|
||||
assert!(checked >= 3, "checked {checked}");
|
||||
assert!(saw_active, "the active photo (top left) was checked");
|
||||
}
|
||||
|
||||
/// A folder of unedited raws on a slow share: the grid must not decode every raw to draw a 512 px
|
||||
|
||||
@@ -47,6 +47,15 @@ pub struct Tokens {
|
||||
/// A thumbnail's place while its picture is still being made: static, apart from the grid, the
|
||||
/// cells and the canvas in every theme.
|
||||
pub skeleton: Color32,
|
||||
/// The ring round the active photo (outer line, 3 pt): 3:1 or more against the grid and the
|
||||
/// square grid's cells. The photo itself is parted from it by [`Tokens::ring_inner`].
|
||||
pub ring_active: Color32,
|
||||
/// The thin line (1 pt) between the active photo and its ring: the ring's opposite, so the ring
|
||||
/// reads on a photo of any brightness.
|
||||
pub ring_inner: Color32,
|
||||
/// The ring round the other selected photos (1.5 pt, against the active one's 3 pt): 3:1 or
|
||||
/// more against the grid.
|
||||
pub ring_selected: Color32,
|
||||
pub divider: Color32,
|
||||
pub inset: Color32,
|
||||
pub field: Color32,
|
||||
@@ -129,6 +138,9 @@ impl Default for Tokens {
|
||||
cell: Color32::from_rgb(0x1c, 0x1c, 0x1c),
|
||||
cell_selected: Color32::from_rgb(0x2d, 0x2d, 0x2d),
|
||||
skeleton: Color32::from_rgb(0x25, 0x25, 0x25),
|
||||
ring_active: Color32::from_rgb(0xff, 0xff, 0xff),
|
||||
ring_inner: Color32::from_rgb(0x00, 0x00, 0x00),
|
||||
ring_selected: Color32::from_rgb(0xc0, 0xc0, 0xc0),
|
||||
divider: Color32::from_rgb(0x1c, 0x1c, 0x1c),
|
||||
inset: Color32::from_rgb(0x23, 0x23, 0x23),
|
||||
field: Color32::from_rgb(0x22, 0x22, 0x22),
|
||||
@@ -189,6 +201,9 @@ impl Tokens {
|
||||
..base
|
||||
},
|
||||
ThemeKind::Silver => Tokens {
|
||||
ring_active: rgb(0x141414),
|
||||
ring_inner: rgb(0xffffff),
|
||||
ring_selected: rgb(0x4a4a4a),
|
||||
chrome: rgb(0xe6e6e6),
|
||||
canvas: rgb(0xcfcfcf),
|
||||
grid_bg: rgb(0xd9d9d9),
|
||||
@@ -220,6 +235,9 @@ impl Tokens {
|
||||
..base
|
||||
},
|
||||
ThemeKind::Paper => Tokens {
|
||||
ring_active: rgb(0x141414),
|
||||
ring_inner: rgb(0xffffff),
|
||||
ring_selected: rgb(0x4a4a4a),
|
||||
chrome: rgb(0xfafafa),
|
||||
canvas: rgb(0xececec),
|
||||
grid_bg: rgb(0xf2f2f2),
|
||||
@@ -458,8 +476,36 @@ pub fn apply_kind(ctx: &egui::Context, kind: ThemeKind) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::button::contrast;
|
||||
use crate::i18n::{Locale, set_language};
|
||||
|
||||
/// The selection ring is a mark on the grid: 3:1 or more against what it is drawn over, in
|
||||
/// every theme. The active photo's ring reads on the grid and on a selected or unselected
|
||||
/// square-grid cell; the other selected photos' ring on the grid.
|
||||
#[test]
|
||||
fn selection_rings_stand_out_from_the_grid_in_every_theme() {
|
||||
for kind in ThemeKind::ALL {
|
||||
let t = Tokens::for_kind(kind);
|
||||
for (what, surface) in [("grid", t.grid_bg), ("cell", t.cell), ("selected cell", t.cell_selected)] {
|
||||
assert!(contrast(t.ring_active, surface) >= 3.0, "{kind:?}: active ring on the {what}: {:.2}", contrast(t.ring_active, surface));
|
||||
}
|
||||
// the selected ring also sits on a selected cell (square grid, filmstrip), label-tinted or not
|
||||
let mut selected_cells = vec![("grid", t.grid_bg), ("selected cell", t.cell_selected)];
|
||||
for label in lightcraft_catalog::ColorLabel::ALL {
|
||||
selected_cells.push(("labelled selected cell", label_background(t.cell_selected, Some(label), true)));
|
||||
}
|
||||
for (what, surface) in selected_cells {
|
||||
assert!(
|
||||
contrast(t.ring_selected, surface) >= 3.0,
|
||||
"{kind:?}: selected ring on the {what}: {:.2}",
|
||||
contrast(t.ring_selected, surface)
|
||||
);
|
||||
}
|
||||
// the thin inner line of the active ring is the ring's opposite: it parts the ring from the photo's edge
|
||||
assert!(contrast(t.ring_inner, t.ring_active) >= 3.0, "{kind:?}: inner line against the active ring");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every theme scrolls 100 points a wheel notch, and a zoom notch stays egui's stock ~1.22x
|
||||
/// (`exp(40 / 200)`) instead of growing with the longer scroll.
|
||||
#[test]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -168,6 +168,13 @@
|
||||
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.
|
||||
|
||||
### Selection rings you can see in the light themes
|
||||
- The active photo's white outline vanished on Silver and Paper's light grid, and told the active photo from the other
|
||||
selected ones by lightness alone. Now the active photo has a heavier ring (3 pt, with a thin line against the photo so
|
||||
it also shows on a white one) and the other selected photos a lighter one (1.5 pt), in colours each theme picks to
|
||||
stand out from its grid. The filmstrip rings its photos the same way, and so do Compare, Reference and Survey (where
|
||||
the ring is the only sign of the active photo) and the People view's chosen person.
|
||||
|
||||
### 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