mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 12:08:10 -06:00
Sidebar: dragging albums or photos near its top or bottom edge scrolls it
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
5fefe91640
commit
8ff45f5237
@@ -179,6 +179,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
let counts = app.caches.counts(&app.session.catalog);
|
||||
let (total, picks, deleted) = (counts.total, counts.picks, counts.deleted);
|
||||
egui::ScrollArea::both().id_salt("left-scroll").auto_shrink([false, false]).show_viewport(ui, |ui, viewport| {
|
||||
drag_auto_scroll(app, ui);
|
||||
// rows are as wide as the widest one needs (last frame), at least the panel
|
||||
let wide = viewport.width().max(content_width(ui.ctx()));
|
||||
ui.set_min_width(wide);
|
||||
@@ -751,6 +752,36 @@ fn pointer_over(ui: &egui::Ui, rect: Rect) -> bool {
|
||||
ui.input(|i| i.pointer.latest_pos()).is_some_and(|p| rect.contains(p) && ui.clip_rect().contains(p))
|
||||
}
|
||||
|
||||
/// How close to the top or bottom edge of the sidebar (points) a dragged album starts scrolling it.
|
||||
const SCROLL_EDGE: f32 = 36.0;
|
||||
/// The fastest the sidebar scrolls for a drag (points per second).
|
||||
const SCROLL_MAX_SPEED: f32 = 700.0;
|
||||
|
||||
/// How fast the sidebar scrolls (points per second, positive = down) for a dragged item with the
|
||||
/// pointer at `y`, the visible part being `top..bottom`: none in the middle, faster the nearer to
|
||||
/// the edge, the top speed at and past it.
|
||||
pub fn auto_scroll_speed(y: f32, top: f32, bottom: f32) -> f32 {
|
||||
let depth = |dist: f32| ((SCROLL_EDGE - dist) / SCROLL_EDGE).clamp(0.0, 1.0);
|
||||
SCROLL_MAX_SPEED * (depth(bottom - y) - depth(y - top))
|
||||
}
|
||||
|
||||
/// While an album, or photos from the grid, are dragged over the sidebar: scrolls it when the
|
||||
/// pointer is near its top or bottom edge, so rows beyond the visible part can be reached. Call
|
||||
/// inside the scroll area.
|
||||
fn drag_auto_scroll(app: &LightcraftApp, ui: &egui::Ui) {
|
||||
if app.ui.dragging_album.is_none() && app.ui.dragging_photos.is_none() {
|
||||
return;
|
||||
}
|
||||
let view = ui.clip_rect();
|
||||
let Some(p) = ui.input(|i| i.pointer.latest_pos()).filter(|p| p.x >= view.left() && p.x <= view.right()) else { return };
|
||||
let speed = auto_scroll_speed(p.y, view.top(), view.bottom());
|
||||
if speed != 0.0 {
|
||||
// a positive delta moves the content down: the view goes up
|
||||
ui.scroll_with_delta_animation(vec2(0.0, -speed * ui.input(|i| i.stable_dt).min(0.1)), egui::style::ScrollAnimation::none());
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// How long a dragged album must rest on a closed folder before it opens.
|
||||
const HOVER_OPEN_SECS: f64 = 0.6;
|
||||
|
||||
|
||||
@@ -596,6 +596,78 @@ fn folders_are_placed_by_their_edges_and_entered_by_their_middle() {
|
||||
assert!(!t.h.app.session.catalog.album_children_are_ordered(None), "Travel 2026 already sits right before Trips");
|
||||
}
|
||||
|
||||
/// Holding a dragged album near the top or bottom edge of the sidebar scrolls it that way; in the
|
||||
/// middle, or beside the sidebar, nothing scrolls. (Each drag ends with Esc: nothing is dropped.)
|
||||
#[test]
|
||||
fn a_dragged_album_scrolls_the_sidebar_at_its_edges() {
|
||||
let mut t = album_tree();
|
||||
for i in 0..60 {
|
||||
t.h.app.session.execute("album.create", &json!({"name": format!("Z{i:02}")})).unwrap();
|
||||
}
|
||||
t.h.step();
|
||||
t.h.step();
|
||||
let last = t.id_of("Z59");
|
||||
let panel = widget(&t.h, "panel:left_panel");
|
||||
let (cx, top, bottom) = (panel.center().x, panel.top(), panel.bottom());
|
||||
let start = t.row(last).top();
|
||||
assert!(start > bottom, "the last album starts below the visible sidebar: {start} vs {bottom}");
|
||||
// a row in the middle of the visible sidebar: where a drag starts
|
||||
let pick = |t: &AlbumTree| {
|
||||
let rows = t.h.app.widgets.iter().filter(|(w, r)| w.starts_with("source:album:") && r.top() > top + 120.0 && r.bottom() < bottom - 120.0);
|
||||
rows.map(|(_, r)| r.center()).next().expect("a visible album row")
|
||||
};
|
||||
let esc = |pressed| egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed, repeat: false, modifiers: Default::default() };
|
||||
let hold = |t: &mut AlbumTree, to: egui::Pos2| {
|
||||
let from = pick(t);
|
||||
t.drag_with(from, &[(to, 90)], vec![esc(true), esc(false)]);
|
||||
};
|
||||
hold(&mut t, egui::pos2(cx, panel.center().y));
|
||||
assert_eq!(t.row(last).top(), start, "no scrolling in the middle");
|
||||
hold(&mut t, egui::pos2(panel.right() + 200.0, bottom - 6.0));
|
||||
assert_eq!(t.row(last).top(), start, "no scrolling beside the sidebar");
|
||||
hold(&mut t, egui::pos2(cx, bottom - 6.0));
|
||||
let scrolled = t.row(last).top();
|
||||
assert!(scrolled < start - 100.0, "the bottom edge scrolls down: {start} -> {scrolled}");
|
||||
hold(&mut t, egui::pos2(cx, top + 70.0));
|
||||
let back = t.row(last).top();
|
||||
assert!(back > scrolled + 100.0, "the top edge scrolls back up: {scrolled} -> {back}");
|
||||
assert!(t.h.app.ui.dragging_album.is_none());
|
||||
}
|
||||
|
||||
/// Photos dragged from the grid scroll the sidebar the same way, to reach an album further down.
|
||||
#[test]
|
||||
fn dragged_photos_scroll_the_sidebar_at_its_edges() {
|
||||
let mut t = album_tree();
|
||||
for i in 0..60 {
|
||||
t.h.app.session.execute("album.create", &json!({"name": format!("Z{i:02}")})).unwrap();
|
||||
}
|
||||
t.h.step();
|
||||
t.h.step();
|
||||
let (last, panel) = (t.id_of("Z59"), widget(&t.h, "panel:left_panel"));
|
||||
let start = t.row(last).top();
|
||||
let first = t.h.app.session.visible_cloned()[0].0;
|
||||
let thumb = widget(&t.h, &format!("thumb:{first}")).center();
|
||||
t.drag(thumb, &[(egui::pos2(panel.center().x, panel.bottom() - 6.0), 90)]);
|
||||
assert!(t.row(last).top() < start - 100.0, "scrolled down: {start} -> {}", t.row(last).top());
|
||||
assert!(t.h.app.ui.dragging_photos.is_none(), "the drag ended");
|
||||
}
|
||||
|
||||
/// How fast the sidebar scrolls for a pointer at `y` (+ down) is zero in the middle, ramps up
|
||||
/// within the edge band and stays at the top speed past the edge.
|
||||
#[test]
|
||||
fn auto_scroll_speed_ramps_up_at_the_edges() {
|
||||
use crate::panels::left::auto_scroll_speed;
|
||||
let (top, bottom) = (100.0, 700.0);
|
||||
assert_eq!(auto_scroll_speed(400.0, top, bottom), 0.0);
|
||||
assert_eq!(auto_scroll_speed(top + 60.0, top, bottom), 0.0, "outside the band");
|
||||
let (slow, fast, past) =
|
||||
(auto_scroll_speed(bottom - 30.0, top, bottom), auto_scroll_speed(bottom - 5.0, top, bottom), auto_scroll_speed(bottom + 80.0, top, bottom));
|
||||
assert!(0.0 < slow && slow < fast && fast <= past, "{slow} {fast} {past}");
|
||||
assert_eq!(past, auto_scroll_speed(bottom + 400.0, top, bottom), "capped");
|
||||
let up = auto_scroll_speed(top + 5.0, top, bottom);
|
||||
assert!(up < 0.0 && (up + fast).abs() < 1e-3, "the same ramp upwards: {up} vs {fast}");
|
||||
}
|
||||
|
||||
/// Dragging out of a folder: the "Albums" header is the way back to the top level (it stays in
|
||||
/// view however long the tree is), only offered for an album that sits in a folder, and the move
|
||||
/// is one undo step. The drop does not fold the section.
|
||||
|
||||
Reference in New Issue
Block a user