mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-31 21:48:53 -06:00
Albums: drop beside a row to place it by hand; Sort A–Z goes back to by name
Dropping on the edge of a row places the album there (album.reorder) with a line showing where; a folder's middle still means inside. A closed folder opens only when the drop would go inside it. Sort Contents A–Z and Sort Albums A–Z (album.sort) appear once a folder is ordered by hand. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
8036f98aca
commit
085c25b10b
@@ -234,18 +234,27 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
if ui.button(crate::i18n::tr("Create Folder…")).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::NewAlbum { name: String::new(), folder: true, parent: None });
|
||||
}
|
||||
// only once the albums were put in an order by hand
|
||||
if app.session.catalog.album_children_are_ordered(None) {
|
||||
ui.separator();
|
||||
if ui.button(crate::i18n::tr("Sort Albums A–Z")).on_hover_text(crate::i18n::tr("Go back to listing them by name")).clicked() {
|
||||
let _ = app.run("album.sort", json!({}));
|
||||
}
|
||||
}
|
||||
});
|
||||
// the album just made: the folders down to it open, once (also when the section is shut)
|
||||
let reveal = app.ui.reveal_album.take();
|
||||
if albums_open {
|
||||
let albums: Vec<Album> = app.session.catalog.albums().cloned().collect();
|
||||
let kids: AlbumKids =
|
||||
app.session.catalog.album_children_by_parent().into_iter().map(|(k, v)| (k, v.into_iter().cloned().collect())).collect();
|
||||
let cat = &app.session.catalog;
|
||||
let mut open_to = Vec::new();
|
||||
let mut cur = reveal.map(AlbumId).and_then(|id| albums.iter().find(|a| a.id == id)).and_then(|a| a.parent);
|
||||
let mut cur = reveal.map(AlbumId).and_then(|id| cat.album(id)).and_then(|a| a.parent);
|
||||
while let Some(p) = cur.filter(|p| !open_to.contains(p) && open_to.len() < 64) {
|
||||
open_to.push(p);
|
||||
cur = albums.iter().find(|a| a.id == p).and_then(|a| a.parent);
|
||||
cur = cat.album(p).and_then(|a| a.parent);
|
||||
}
|
||||
albums_tree(app, ui, &albums, None, 0.0, &open_to);
|
||||
albums_tree(app, ui, &kids, None, 0.0, &open_to);
|
||||
top_level_drop_zone(app, ui);
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
@@ -670,10 +679,12 @@ fn browse_all_photos(app: &mut LightcraftApp, on: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The albums inside each folder (`None`: the top level), in the order they are listed.
|
||||
type AlbumKids = std::collections::HashMap<Option<AlbumId>, Vec<Album>>;
|
||||
|
||||
/// `open_to`: folders forced open this frame (the way down to an album that was just made).
|
||||
fn albums_tree(app: &mut LightcraftApp, ui: &mut egui::Ui, all: &[Album], parent: Option<AlbumId>, indent: f32, open_to: &[AlbumId]) {
|
||||
let mut kids: Vec<&Album> = all.iter().filter(|a| a.parent == parent).collect();
|
||||
kids.sort_by_key(|a| (!a.folder, a.name.to_lowercase()));
|
||||
fn albums_tree(app: &mut LightcraftApp, ui: &mut egui::Ui, all: &AlbumKids, parent: Option<AlbumId>, indent: f32, open_to: &[AlbumId]) {
|
||||
let Some(kids) = all.get(&parent) else { return };
|
||||
for a in kids {
|
||||
if a.folder {
|
||||
let open_id = egui::Id::new(("folder-open", a.id.0));
|
||||
@@ -684,11 +695,11 @@ fn albums_tree(app: &mut LightcraftApp, ui: &mut egui::Ui, all: &[Album], parent
|
||||
}
|
||||
let resp = row_sensed(app, ui, &format!("folder:{}", a.id.0), Icon::Folder, &a.name, None, None, false, indent, Sense::click_and_drag());
|
||||
// a folder is no source, so its row folds it too; the triangle is the same click, aimed
|
||||
let has_children = all.iter().any(|c| c.parent == Some(a.id));
|
||||
let has_children = all.get(&Some(a.id)).is_some_and(|v| !v.is_empty());
|
||||
if resp.drag_started() {
|
||||
app.ui.dragging_album = Some(a.id.0);
|
||||
}
|
||||
if album_drag_over(app, ui, &resp, a, has_children.then_some(&mut open)) {
|
||||
if album_drag_over(app, ui, &resp, a, has_children.then_some(&mut open), indent) {
|
||||
ui.data_mut(|d| d.insert_temp(open_id, true));
|
||||
}
|
||||
let mut toggled = resp.clicked();
|
||||
@@ -719,6 +730,7 @@ fn albums_tree(app: &mut LightcraftApp, ui: &mut egui::Ui, all: &[Album], parent
|
||||
if resp.drag_started() {
|
||||
app.ui.dragging_album = Some(a.id.0);
|
||||
}
|
||||
album_drag_over(app, ui, &resp, a, None, indent);
|
||||
if !a.is_smart() {
|
||||
drop_target(app, ui, &resp, a);
|
||||
}
|
||||
@@ -752,10 +764,10 @@ fn can_drop_album(app: &LightcraftApp, dragged: AlbumId, target: Option<AlbumId>
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the dragged album (`album.move`, one undo step) and ends the drag; the folders down to it
|
||||
/// open so it shows in its new place.
|
||||
fn drop_album(app: &mut LightcraftApp, ui: &egui::Ui, dragged: AlbumId, target: Option<AlbumId>) {
|
||||
match app.run("album.move", json!({"id": dragged.0, "parent": target.map(|t| t.0)})) {
|
||||
/// Ends a drag that ran `result`: the folders down to the album open so it shows in its new place.
|
||||
fn finish_drop(app: &mut LightcraftApp, ui: &egui::Ui, dragged: AlbumId, result: Result<serde_json::Value, String>) {
|
||||
forget_hover(app, ui.ctx());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
app.ui.reveal_album = Some(dragged.0);
|
||||
ui.ctx().request_repaint();
|
||||
@@ -765,24 +777,103 @@ fn drop_album(app: &mut LightcraftApp, ui: &egui::Ui, dragged: AlbumId, target:
|
||||
app.ui.dragging_album = None;
|
||||
}
|
||||
|
||||
/// A folder row while an album is dragged: outlined under the pointer when the drop is allowed,
|
||||
/// opened (`open`, when it has something inside) once the pointer rests on it, and the album moved
|
||||
/// in on release. Returns whether it opened the folder.
|
||||
fn album_drag_over(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response, folder: &Album, open: Option<&mut bool>) -> bool {
|
||||
let hover_id = egui::Id::new(("album-hover", folder.id.0));
|
||||
/// Forgets how long the pointer rested on each folder (a new drag starts counting again).
|
||||
fn forget_hover(app: &LightcraftApp, ctx: &egui::Context) {
|
||||
let ids: Vec<u64> = app.session.catalog.albums().filter(|a| a.folder).map(|a| a.id.0).collect();
|
||||
ctx.data_mut(|d| {
|
||||
for id in ids {
|
||||
d.remove_temp::<f64>(egui::Id::new(("album-hover", id)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Moves the dragged album into a folder (`album.move`, one undo step; `None`: the top level).
|
||||
fn drop_album(app: &mut LightcraftApp, ui: &egui::Ui, dragged: AlbumId, target: Option<AlbumId>) {
|
||||
let r = app.run("album.move", json!({"id": dragged.0, "parent": target.map(|t| t.0)}));
|
||||
finish_drop(app, ui, dragged, r);
|
||||
}
|
||||
|
||||
/// Places the dragged album in `parent` before the sibling `before` (`None`: last of its kind), by
|
||||
/// hand (`album.reorder`, one undo step).
|
||||
fn place_album(app: &mut LightcraftApp, ui: &egui::Ui, dragged: AlbumId, parent: Option<AlbumId>, before: Option<AlbumId>) {
|
||||
let r = app.run("album.reorder", json!({"id": dragged.0, "parent": parent.map(|p| p.0), "before": before.map(|b| b.0)}));
|
||||
finish_drop(app, ui, dragged, r);
|
||||
}
|
||||
|
||||
/// What dropping the dragged album on a row would do.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum AlbumDrop {
|
||||
/// Into the folder.
|
||||
Into(AlbumId),
|
||||
/// Beside the row: in `parent`, before `before` (`None`: last of its kind); the line is drawn
|
||||
/// above the row (`above`) or below it.
|
||||
Place { parent: Option<AlbumId>, before: Option<AlbumId>, above: bool },
|
||||
}
|
||||
|
||||
/// What dropping `dragged` at `frac` (0 = top edge, 1 = bottom edge) of the row of `target` does,
|
||||
/// if anything. An album over a folder goes inside it; a folder over a folder goes inside by its
|
||||
/// middle and beside it by its edges; an album over an album goes before it by its top half and
|
||||
/// after it by its bottom half; a folder over an album does nothing. Placing where it already
|
||||
/// is, or inside itself, is nothing too. `expanded`: `target` is a folder shown open.
|
||||
fn album_drop_at(app: &LightcraftApp, dragged: AlbumId, target: &Album, frac: f32, expanded: bool) -> Option<AlbumDrop> {
|
||||
let cat = &app.session.catalog;
|
||||
let d = cat.album(dragged)?;
|
||||
let into = || can_drop_album(app, dragged, Some(target.id)).then_some(AlbumDrop::Into(target.id));
|
||||
if d.folder != target.folder {
|
||||
return if target.folder { into() } else { None };
|
||||
}
|
||||
if target.id == dragged {
|
||||
return None;
|
||||
}
|
||||
let after = frac >= if target.folder { 0.7 } else { 0.5 };
|
||||
// the middle of a folder, and the bottom of an open one (its first child is next), mean inside
|
||||
if target.folder && frac >= 0.3 && (!after || expanded) {
|
||||
return into();
|
||||
}
|
||||
let parent = target.parent;
|
||||
if parent.is_some_and(|p| is_within(app, p, dragged)) {
|
||||
return None;
|
||||
}
|
||||
let sibs = cat.album_children(parent);
|
||||
let at = sibs.iter().position(|a| a.id == target.id)?;
|
||||
let was = sibs.iter().position(|a| a.id == dragged);
|
||||
if !after {
|
||||
(was.map(|w| w + 1) != Some(at)).then_some(AlbumDrop::Place { parent, before: Some(target.id), above: true })
|
||||
} else {
|
||||
let next = sibs.iter().skip(at + 1).find(|a| a.id != dragged).filter(|a| a.folder == target.folder).map(|a| a.id);
|
||||
(was != Some(at + 1)).then_some(AlbumDrop::Place { parent, before: next, above: false })
|
||||
}
|
||||
}
|
||||
|
||||
/// A row while an album is dragged: a folder outlined under the pointer when the drop would go
|
||||
/// inside it, a line above or below the row when it would go beside it, and the move or placement
|
||||
/// done on release. A closed folder with something inside (`open`) opens once the pointer rests on
|
||||
/// it; returns whether it did.
|
||||
fn album_drag_over(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Response, row: &Album, open: Option<&mut bool>, indent: f32) -> bool {
|
||||
let hover_id = egui::Id::new(("album-hover", row.id.0));
|
||||
let Some(dragged) = app.ui.dragging_album.map(AlbumId) else { return false };
|
||||
if !pointer_over(ui, resp.rect) {
|
||||
let pos = ui.input(|i| i.pointer.latest_pos()).filter(|_| pointer_over(ui, resp.rect));
|
||||
let Some(pos) = pos else {
|
||||
if ui.data(|d| d.get_temp::<f64>(hover_id)).is_some() {
|
||||
ui.data_mut(|d| d.remove_temp::<f64>(hover_id));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let now = ui.input(|i| i.time);
|
||||
let since = ui.data_mut(|d| *d.get_temp_mut_or_insert_with(hover_id, || now));
|
||||
};
|
||||
let expanded = open.as_deref().copied().unwrap_or(false);
|
||||
let frac = ((pos.y - resp.rect.top()) / resp.rect.height().max(1.0)).clamp(0.0, 1.0);
|
||||
let drop = album_drop_at(app, dragged, row, frac, expanded);
|
||||
let mut opened = false;
|
||||
if let Some(open) = open
|
||||
// only a drop that goes inside opens the folder: on its edges the rows would shift under the pointer
|
||||
let inside = matches!(drop, Some(AlbumDrop::Into(_)));
|
||||
if !inside && ui.data(|d| d.get_temp::<f64>(hover_id)).is_some() {
|
||||
ui.data_mut(|d| d.remove_temp::<f64>(hover_id));
|
||||
}
|
||||
if inside
|
||||
&& let Some(open) = open
|
||||
&& !*open
|
||||
{
|
||||
let now = ui.input(|i| i.time);
|
||||
let since = ui.data_mut(|d| *d.get_temp_mut_or_insert_with(hover_id, || now));
|
||||
if now - since >= HOVER_OPEN_SECS {
|
||||
*open = true;
|
||||
opened = true;
|
||||
@@ -790,12 +881,25 @@ fn album_drag_over(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Resp
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
if can_drop_album(app, dragged, Some(folder.id)) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.painter().rect_stroke(resp.rect.shrink2(vec2(8.0, 1.0)), 4.0, egui::Stroke::new(1.5, t.accent), egui::StrokeKind::Inside);
|
||||
if ui.input(|i| i.pointer.primary_released()) {
|
||||
drop_album(app, ui, dragged, Some(folder.id));
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let released = ui.input(|i| i.pointer.primary_released());
|
||||
match drop {
|
||||
Some(AlbumDrop::Into(folder)) => {
|
||||
ui.painter().rect_stroke(resp.rect.shrink2(vec2(8.0, 1.0)), 4.0, egui::Stroke::new(1.5, t.accent), egui::StrokeKind::Inside);
|
||||
if released {
|
||||
drop_album(app, ui, dragged, Some(folder));
|
||||
}
|
||||
}
|
||||
Some(AlbumDrop::Place { parent, before, above }) => {
|
||||
let y = if above { resp.rect.top() + 0.5 } else { resp.rect.bottom() - 0.5 };
|
||||
let (x0, x1) = (resp.rect.left() + 18.0 + indent, ui.clip_rect().right() - 8.0);
|
||||
ui.painter().circle_stroke(pos2(x0, y), 3.0, egui::Stroke::new(1.5, t.accent));
|
||||
ui.painter().line_segment([pos2(x0 + 3.0, y), pos2(x1.max(x0 + 3.0), y)], egui::Stroke::new(2.0, t.accent));
|
||||
if released {
|
||||
place_album(app, ui, dragged, parent, before);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
opened
|
||||
}
|
||||
@@ -827,13 +931,7 @@ pub fn album_drag_feedback(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
ctx.input(|i| (i.pointer.primary_released(), i.pointer.primary_down(), i.pointer.latest_pos(), i.key_pressed(egui::Key::Escape)));
|
||||
if released || !down || esc {
|
||||
app.ui.dragging_album = None;
|
||||
// forget how long the pointer rested on each folder
|
||||
let ids: Vec<u64> = app.session.catalog.albums().filter(|a| a.folder).map(|a| a.id.0).collect();
|
||||
ctx.data_mut(|d| {
|
||||
for id in ids {
|
||||
d.remove_temp::<f64>(egui::Id::new(("album-hover", id)));
|
||||
}
|
||||
});
|
||||
forget_hover(app, ctx);
|
||||
return;
|
||||
}
|
||||
let (Some(pos), Some(name)) = (pos, app.session.catalog.album(AlbumId(id)).map(|a| a.name.clone())) else { return };
|
||||
@@ -883,6 +981,12 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
||||
}
|
||||
}
|
||||
});
|
||||
if app.session.catalog.album_children_are_ordered(Some(a.id))
|
||||
&& ui.button(crate::i18n::tr("Sort Contents A–Z")).on_hover_text(crate::i18n::tr("Go back to listing them by name")).clicked()
|
||||
{
|
||||
let _ = app.run("album.sort", json!({"parent": a.id.0}));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
if !a.folder && !a.is_smart() && ui.button(crate::i18n::tr("Add Selected Photos")).clicked() {
|
||||
|
||||
@@ -427,6 +427,19 @@ impl AlbumTree {
|
||||
self.h.step();
|
||||
self.h.step();
|
||||
}
|
||||
/// Drag `id` to `frac` of the way down the row of `onto` (0 = its top edge, 1 = its bottom edge).
|
||||
fn drag_to(&mut self, id: u64, onto: u64, frac: f32) {
|
||||
let (from, r) = (self.row(id).center(), self.row(onto));
|
||||
self.drag(from, &[(egui::pos2(r.left() + 100.0, r.top() + frac * r.height()), 3)]);
|
||||
}
|
||||
fn id_of(&self, name: &str) -> u64 {
|
||||
self.h.app.session.catalog.albums().find(|a| a.name == name).unwrap_or_else(|| panic!("no album {name}")).id.0
|
||||
}
|
||||
/// The names inside `parent`, folders or albums only.
|
||||
fn kids(&self, parent: Option<u64>, folders: bool) -> Vec<String> {
|
||||
let cat = &self.h.app.session.catalog;
|
||||
cat.album_children(parent.map(lightcraft_catalog::AlbumId)).iter().filter(|a| a.folder == folders).map(|a| a.name.clone()).collect()
|
||||
}
|
||||
fn drag_row(&mut self, id: u64, onto: u64) {
|
||||
let (from, to) = (self.row(id).center(), self.row(onto).center());
|
||||
self.drag(from, &[(to, 3)]);
|
||||
@@ -446,9 +459,10 @@ fn dragging_an_album_onto_a_folder_moves_it() {
|
||||
assert!(t.h.app.widgets.iter().any(|(w, _)| *w == format!("source:album:{}", t.loose)));
|
||||
t.drag_row(t.sub, t.archive);
|
||||
assert_eq!(t.parent(t.sub), Some(t.archive), "a folder into a folder");
|
||||
// refused: onto a plain album, onto itself, into its own subfolder
|
||||
// refused: onto itself, into its own subfolder; onto a plain album it only goes beside it
|
||||
t.drag_row(t.best, t.loose);
|
||||
assert_eq!(t.parent(t.best), Some(t.trips), "a plain album is no destination");
|
||||
assert_eq!(t.parent(t.best), Some(t.archive), "a plain album is no destination: Best lands beside Loose, not inside");
|
||||
assert_eq!(t.parent(t.loose), Some(t.archive));
|
||||
let (from, aside) = (t.row(t.archive).center(), t.row(t.archive).center() + egui::vec2(40.0, 0.0));
|
||||
t.drag(from, &[(aside, 3)]);
|
||||
assert_eq!(t.parent(t.archive), None, "not onto itself");
|
||||
@@ -483,6 +497,109 @@ fn an_album_drag_ignores_other_buttons_and_esc_cancels_it() {
|
||||
assert_eq!(t.parent(t.loose), Some(t.archive));
|
||||
}
|
||||
|
||||
/// Given the top-level albums Garden, Loose, Night Sky, Portfolio listed by name, when an album is
|
||||
/// dropped on the top or bottom half of another, then it is placed before or after it and the
|
||||
/// list is ordered by hand from then on.
|
||||
#[test]
|
||||
fn dropping_on_the_edge_of_an_album_places_it_there() {
|
||||
let mut t = album_tree();
|
||||
let id = |t: &AlbumTree, name: &str| t.h.app.session.catalog.albums().find(|a| a.name == name).unwrap().id.0;
|
||||
let (garden, night, portfolio) = (id(&t, "Garden"), id(&t, "Night Sky"), id(&t, "Portfolio"));
|
||||
assert_eq!(t.kids(None, false), ["Garden", "Loose", "Night Sky", "Portfolio"]);
|
||||
t.drag_to(portfolio, garden, 0.2);
|
||||
assert_eq!(t.kids(None, false), ["Portfolio", "Garden", "Loose", "Night Sky"], "before Garden");
|
||||
t.drag_to(garden, night, 0.8);
|
||||
assert_eq!(t.kids(None, false), ["Portfolio", "Loose", "Night Sky", "Garden"], "after Night Sky");
|
||||
assert!(t.h.app.session.catalog.album_children_are_ordered(None));
|
||||
// one undo step per drop
|
||||
t.h.app.session.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(t.kids(None, false), ["Portfolio", "Garden", "Loose", "Night Sky"]);
|
||||
assert!(t.h.app.ui.dragging_album.is_none());
|
||||
}
|
||||
|
||||
/// Dropping beside an album in another folder moves the album there, at that place.
|
||||
#[test]
|
||||
fn dropping_beside_an_album_in_another_folder_moves_it_there() {
|
||||
let mut t = album_tree();
|
||||
t.drag_to(t.loose, t.best, 0.2);
|
||||
assert_eq!(t.parent(t.loose), Some(t.trips));
|
||||
assert_eq!(t.kids(Some(t.trips), false), ["Loose", "Best"]);
|
||||
}
|
||||
|
||||
/// Where nothing would change, nothing is done: no edit, so no hand order appears.
|
||||
#[test]
|
||||
fn dropping_where_it_already_is_changes_nothing() {
|
||||
let mut t = album_tree();
|
||||
let garden = t.id_of("Garden");
|
||||
assert_eq!(t.kids(None, false)[..2], ["Garden", "Loose"]);
|
||||
t.drag_to(t.loose, garden, 0.8);
|
||||
t.drag_to(garden, t.loose, 0.2);
|
||||
assert!(!t.h.app.session.catalog.album_children_are_ordered(None), "Loose already follows Garden");
|
||||
// a real placement orders the albums; the same drop again then changes nothing
|
||||
t.drag_to(t.loose, garden, 0.2);
|
||||
assert!(t.h.app.session.catalog.album_children_are_ordered(None));
|
||||
let snapshot = t.h.app.session.catalog.to_snapshot();
|
||||
t.drag_to(t.loose, garden, 0.2);
|
||||
assert_eq!(t.h.app.session.catalog.to_snapshot(), snapshot);
|
||||
}
|
||||
|
||||
/// The bottom of an open folder is inside it; from the bottom edge of a closed one a folder goes
|
||||
/// after it (last of the folders); a folder over an album does nothing.
|
||||
#[test]
|
||||
fn folders_go_after_a_closed_folder_and_inside_an_open_one() {
|
||||
let mut t = album_tree();
|
||||
t.drag_to(t.archive, t.trips, 0.9);
|
||||
assert_eq!(t.parent(t.archive), Some(t.trips), "the bottom of an open folder is inside it");
|
||||
let travel = t.id_of("Travel 2026");
|
||||
assert_eq!(t.kids(None, true), ["Travel 2026", "Trips"]);
|
||||
let r = t.h.request("ui.clickWidget", json!({"id": format!("albumToggle:{}", t.trips)}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
t.h.step();
|
||||
t.h.step();
|
||||
t.drag_to(travel, t.trips, 0.9);
|
||||
assert_eq!(t.kids(None, true), ["Trips", "Travel 2026"], "after Trips, the last folder");
|
||||
assert_eq!(t.parent(travel), None);
|
||||
// folders are not placed among albums
|
||||
t.drag_to(t.trips, t.loose, 0.5);
|
||||
assert_eq!(t.kids(None, true), ["Trips", "Travel 2026"]);
|
||||
}
|
||||
|
||||
/// Resting on a closed folder's edge (where the drop goes beside it) does not open it: the rows
|
||||
/// would shift under the pointer. Only a drop that goes inside opens it.
|
||||
#[test]
|
||||
fn hovering_a_folders_edge_does_not_open_it() {
|
||||
let mut t = album_tree();
|
||||
let r = t.h.request("ui.clickWidget", json!({"id": format!("albumToggle:{}", t.trips)}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
t.h.step();
|
||||
t.h.step();
|
||||
let (from, r) = (t.row(t.archive).center(), t.row(t.trips));
|
||||
t.drag(from, &[(egui::pos2(r.left() + 100.0, r.top() + 0.9 * r.height()), 90)]);
|
||||
assert_eq!(t.parent(t.archive), None, "dropped after Trips, not inside it");
|
||||
assert_eq!(t.kids(None, true), ["Travel 2026", "Trips", "Archive"]);
|
||||
}
|
||||
|
||||
/// Folders are placed among folders by their edges; their middle still means "inside"; an album
|
||||
/// over a folder is always "inside"; where nothing would change nothing is done or ordered.
|
||||
#[test]
|
||||
fn folders_are_placed_by_their_edges_and_entered_by_their_middle() {
|
||||
let mut t = album_tree();
|
||||
assert_eq!(t.kids(None, true), ["Archive", "Travel 2026", "Trips"]);
|
||||
t.drag_to(t.trips, t.archive, 0.1);
|
||||
assert_eq!(t.kids(None, true), ["Trips", "Archive", "Travel 2026"], "Trips before Archive");
|
||||
assert_eq!(t.parent(t.trips), None);
|
||||
t.drag_to(t.sub, t.archive, 0.5);
|
||||
assert_eq!(t.parent(t.sub), Some(t.archive), "the middle of a folder is inside it");
|
||||
// an album anywhere over a folder row goes inside, even at its edge
|
||||
t.drag_to(t.loose, t.archive, 0.05);
|
||||
assert_eq!(t.parent(t.loose), Some(t.archive));
|
||||
// already in place: no change, so no hand order is made
|
||||
let mut t = album_tree();
|
||||
let travel = t.h.app.session.catalog.albums().find(|a| a.name == "Travel 2026").unwrap().id.0;
|
||||
t.drag_to(travel, t.trips, 0.1);
|
||||
assert!(!t.h.app.session.catalog.album_children_are_ordered(None), "Travel 2026 already sits right before Trips");
|
||||
}
|
||||
|
||||
/// Dragging out of a folder: the drop zone under the tree puts the album back at the top level;
|
||||
/// it only shows when there is somewhere to go, and the move is one undo step.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user