| ... | ... | @@ -67,6 +67,12 @@ pub struct Tree<'a> { |
| 67 | 67 | rows: Vec<Entry>, |
| 68 | 68 | /// The section or group held down, which a drag moves. |
| 69 | 69 | held: Option<Entry>, |
| 70 | /// The notebook row held down, which a drag moves among the notebooks. |
| 71 | held_notebook: Option<Entry>, |
| 72 | /// The notebook dragged, by index, and the place among the others it would land in. |
| 73 | lifted_notebook: Option<(usize, usize)>, |
| 74 | /// Each notebook's rows but the dragged one's, in order. |
| 75 | blocks: Vec<[f32; 4]>, |
| 70 | 76 | /// The row dragged, which the tree leaves out for a gap where it would land. |
| 71 | 77 | lifted: Option<(&'a Entry, &'a Landing)>, |
| 72 | 78 | /// The dragged row as it was built, for drawing it over the others. |
| ... | ... | @@ -95,6 +101,19 @@ struct Ghost { |
| 95 | 101 | bold: bool, |
| 96 | 102 | } |
| 97 | 103 | |
| 104 | impl Ghost { |
| 105 | fn of(row: &Row) -> Self { |
| 106 | Self { |
| 107 | label: row.label.to_owned(), |
| 108 | icon: row.icon, |
| 109 | depth: row.depth, |
| 110 | dim: row.dim, |
| 111 | selected: row.selected, |
| 112 | bold: row.bold, |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 98 | 117 | impl<'a> Tree<'a> { |
| 99 | 118 | fn new( |
| 100 | 119 | theme: &'a Theme, |
| ... | ... | @@ -110,6 +129,9 @@ impl<'a> Tree<'a> { |
| 110 | 129 | action: None, |
| 111 | 130 | rows: Vec::new(), |
| 112 | 131 | held: None, |
| 132 | held_notebook: None, |
| 133 | lifted_notebook: None, |
| 134 | blocks: Vec::new(), |
| 113 | 135 | lifted: None, |
| 114 | 136 | ghost: None, |
| 115 | 137 | gap: None, |
| ... | ... | @@ -118,7 +140,8 @@ impl<'a> Tree<'a> { |
| 118 | 140 | } |
| 119 | 141 | } |
| 120 | 142 | |
| 121 | | /// A section or group row, for dragging one onto or between the others. |
| 143 | /// A section or group row, for dragging one onto or between the others; or a notebook's, |
| 144 | /// with an empty `path`, among the notebooks. |
| 122 | 145 | #[derive(Clone, PartialEq)] |
| 123 | 146 | pub struct Entry { |
| 124 | 147 | pub notebook: usize, |
| ... | ... | @@ -212,32 +235,46 @@ fn sidebar( |
| 212 | 235 | ..Spec::default() |
| 213 | 236 | }, |
| 214 | 237 | ); |
| 238 | let mut place = 0; |
| 215 | 239 | for (index, library) in notebooks.iter().enumerate() { |
| 216 | 240 | let key = library.key(""); |
| 217 | 241 | let unfolded = !folded.contains(&key); |
| 218 | 242 | let renamed = (tree.renaming.as_ref()).is_some_and(|renaming| renaming.notebook(library)); |
| 219 | | let (row, fold) = tree_row( |
| 220 | | ui, |
| 221 | | tree, |
| 222 | | ui.id(("notebook", index)), |
| 223 | | Row { |
| 224 | | label: &library.name, |
| 225 | | icon: Leading::Notebook( |
| 226 | | if library.in_icloud() { |
| 227 | | art::NOTEBOOK_ICLOUD |
| 228 | | } else { |
| 229 | | art::NOTEBOOK |
| 230 | | }, |
| 231 | | library.color(), |
| 232 | | ), |
| 233 | | depth: 0, |
| 234 | | dim: library.notebook.is_err(), |
| 235 | | fold: Some(unfolded), |
| 236 | | selected: None, |
| 237 | | renamed, |
| 238 | | bold: tree.unread.contains(&key), |
| 243 | let row = Row { |
| 244 | label: &library.name, |
| 245 | icon: Leading::Notebook( |
| 246 | if library.in_icloud() { |
| 247 | art::NOTEBOOK_ICLOUD |
| 248 | } else { |
| 249 | art::NOTEBOOK |
| 250 | }, |
| 251 | library.color(), |
| 252 | ), |
| 253 | depth: 0, |
| 254 | dim: library.notebook.is_err(), |
| 255 | fold: Some(unfolded), |
| 256 | selected: None, |
| 257 | renamed, |
| 258 | bold: tree.unread.contains(&key), |
| 259 | }; |
| 260 | if let Some((lifted, slot)) = tree.lifted_notebook { |
| 261 | if lifted == index { |
| 262 | tree.ghost = Some(Ghost::of(&row)); |
| 263 | continue; |
| 264 | } |
| 265 | opening(ui, tree, ("notebook gap", &library.location), slot == place); |
| 266 | place += 1; |
| 267 | } |
| 268 | let block = ui.open( |
| 269 | ("notebook block", index), |
| 270 | Spec { |
| 271 | axis: Axis::Y, |
| 272 | size: [fill(), ui::children()], |
| 273 | ..Spec::default() |
| 239 | 274 | }, |
| 240 | 275 | ); |
| 276 | let id = ui.id(("notebook", index)); |
| 277 | let (row, fold) = tree_row(ui, tree, id, row); |
| 241 | 278 | let unsigned = library.notebook.is_err() |
| 242 | 279 | && crate::library::server_address(&library.location).is_some(); |
| 243 | 280 | if row.clicked && unsigned { |
| ... | ... | @@ -251,6 +288,18 @@ fn sidebar( |
| 251 | 288 | if let Some(point) = row.context { |
| 252 | 289 | tree.action = Some(Action::Menu(Target::Notebook(Arc::clone(library)), point)); |
| 253 | 290 | } |
| 291 | if row.dragging |
| 292 | && !renamed |
| 293 | && let Some(rect) = ui.rect(id) |
| 294 | { |
| 295 | tree.held_notebook = Some(Entry { |
| 296 | notebook: index, |
| 297 | path: String::new(), |
| 298 | group: false, |
| 299 | rect, |
| 300 | id, |
| 301 | }); |
| 302 | } |
| 254 | 303 | if let Some(catalog) = library.catalog() |
| 255 | 304 | && unfolding(ui, ("notebook rows", index), unfolded) |
| 256 | 305 | { |
| ... | ... | @@ -258,6 +307,11 @@ fn sidebar( |
| 258 | 307 | ui.close(); |
| 259 | 308 | ui.close(); |
| 260 | 309 | } |
| 310 | ui.close(); |
| 311 | tree.blocks.extend(ui.rect(block)); |
| 312 | } |
| 313 | if let Some((_, slot)) = tree.lifted_notebook { |
| 314 | opening(ui, tree, ("notebook gap", ""), slot == place); |
| 261 | 315 | } |
| 262 | 316 | ui.close(); |
| 263 | 317 | ui.open( |
| ... | ... | @@ -549,14 +603,7 @@ fn lifts(tree: &mut Tree, notebook: usize, path: &str, row: &Row) -> bool { |
| 549 | 603 | .lifted |
| 550 | 604 | .is_some_and(|(lifted, _)| lifted.notebook == notebook && lifted.path == path); |
| 551 | 605 | if lifted { |
| 552 | | tree.ghost = Some(Ghost { |
| 553 | | label: row.label.to_owned(), |
| 554 | | icon: row.icon, |
| 555 | | depth: row.depth, |
| 556 | | dim: row.dim, |
| 557 | | selected: row.selected, |
| 558 | | bold: row.bold, |
| 559 | | }); |
| 606 | tree.ghost = Some(Ghost::of(row)); |
| 560 | 607 | } |
| 561 | 608 | lifted |
| 562 | 609 | } |
| ... | ... | @@ -568,14 +615,20 @@ fn gap(ui: &mut Ui, tree: &mut Tree, folder: &str, before: Option<&str>) { |
| 568 | 615 | return; |
| 569 | 616 | }; |
| 570 | 617 | let here = !landing.into && landing.folder == folder && landing.before.as_deref() == before; |
| 571 | | let id = ui.id(("gap", before)); |
| 618 | opening(ui, tree, ("gap", before), here); |
| 619 | } |
| 620 | |
| 621 | /// A gap, `part`, that eases open a row tall where a dragged row would land, `here`, and |
| 622 | /// shut elsewhere. |
| 623 | fn opening(ui: &mut Ui, tree: &mut Tree, part: impl std::hash::Hash + Copy, here: bool) { |
| 624 | let id = ui.id(part); |
| 572 | 625 | let height = ui.animate(id, if here { ROW } else { 0.0 }); |
| 573 | 626 | if here { |
| 574 | 627 | tree.gap = ui.rect(id); |
| 575 | 628 | } |
| 576 | 629 | if height > 0.0 { |
| 577 | 630 | ui.leaf( |
| 578 | | ("gap", before), |
| 631 | part, |
| 579 | 632 | Spec { |
| 580 | 633 | size: [fill(), px(height)], |
| 581 | 634 | ..Spec::default() |
| ... | ... | @@ -836,6 +889,14 @@ impl crate::State { |
| 836 | 889 | Dragged::Entry { entry, landing, .. } => Some((entry, landing)), |
| 837 | 890 | _ => None, |
| 838 | 891 | }); |
| 892 | tree.lifted_notebook = (self.drag.as_ref()) |
| 893 | .filter(|drag| drag.lifted) |
| 894 | .and_then(|drag| match &drag.what { |
| 895 | Dragged::Notebook(entry) => { |
| 896 | Some((entry.notebook, drag.slot.unwrap_or(entry.notebook))) |
| 897 | } |
| 898 | _ => None, |
| 899 | }); |
| 839 | 900 | sidebar( |
| 840 | 901 | &mut self.ui, |
| 841 | 902 | &mut tree, |
| ... | ... | @@ -848,7 +909,9 @@ impl crate::State { |
| 848 | 909 | let Tree { |
| 849 | 910 | action, |
| 850 | 911 | rows, |
| 851 | | mut held, |
| 912 | held, |
| 913 | held_notebook, |
| 914 | blocks, |
| 852 | 915 | ghost, |
| 853 | 916 | gap, |
| 854 | 917 | .. |
| ... | ... | @@ -856,8 +919,13 @@ impl crate::State { |
| 856 | 919 | let corner = self.ui.rect(rows_id).unwrap_or_default(); |
| 857 | 920 | let found = ghost.is_some(); |
| 858 | 921 | let (ghost_held, settled) = self.lifted_row(theme, ghost, gap, &rows, corner); |
| 859 | | held = held.or(ghost_held); |
| 860 | | self.drag_entries(held, &rows, gap, settled || !found, corner, theme); |
| 922 | let settled = settled || !found; |
| 923 | let (held, held_notebook) = match ghost_held { |
| 924 | Some(entry) if entry.path.is_empty() => (held, Some(entry)), |
| 925 | ghost_held => (held.or(ghost_held), held_notebook), |
| 926 | }; |
| 927 | self.drag_entries(held, &rows, gap, settled, corner, theme); |
| 928 | self.drag_notebooks(held_notebook, &blocks, gap, settled); |
| 861 | 929 | self.ui.close(); |
| 862 | 930 | self.ui.close(); |
| 863 | 931 | match action { |
| ... | ... | @@ -976,30 +1044,25 @@ impl crate::State { |
| 976 | 1044 | rows: &[Entry], |
| 977 | 1045 | corner: [f32; 4], |
| 978 | 1046 | ) -> (Option<Entry>, bool) { |
| 979 | | let ( |
| 980 | | Some(ghost), |
| 981 | | Some( |
| 982 | | drag @ crate::menus::Drag { |
| 983 | | what: Dragged::Entry { entry, landing, .. }, |
| 984 | | .. |
| 985 | | }, |
| 986 | | ), |
| 987 | | ) = (ghost, &self.drag) |
| 988 | | else { |
| 1047 | let (Some(ghost), Some(drag)) = (ghost, &self.drag) else { |
| 989 | 1048 | return (None, true); |
| 990 | 1049 | }; |
| 1050 | let (entry, onto) = match &drag.what { |
| 1051 | Dragged::Entry { entry, landing, .. } => ( |
| 1052 | entry, |
| 1053 | (rows.iter()) |
| 1054 | .find(|row| landing.into && row.path == landing.folder) |
| 1055 | .map(|row| row.rect), |
| 1056 | ), |
| 1057 | Dragged::Notebook(entry) => (entry, None), |
| 1058 | _ => return (None, true), |
| 1059 | }; |
| 991 | 1060 | let key = self.ui.id("lifted"); |
| 992 | 1061 | let (top, settled) = if drag.live() { |
| 993 | 1062 | let top = drag.corner(self.pointer)[1] - corner[1]; |
| 994 | 1063 | (self.ui.hold(key, top), false) |
| 995 | 1064 | } else { |
| 996 | | let place = gap |
| 997 | | .or_else(|| { |
| 998 | | rows.iter() |
| 999 | | .find(|row| landing.into && row.path == landing.folder) |
| 1000 | | .map(|row| row.rect) |
| 1001 | | }) |
| 1002 | | .unwrap_or(entry.rect); |
| 1065 | let place = gap.or(onto).unwrap_or(entry.rect); |
| 1003 | 1066 | let target = place[1] - corner[1]; |
| 1004 | 1067 | let top = self.ui.animate(key, target); |
| 1005 | 1068 | (top, (top - target).abs() < 0.5) |
| ... | ... | @@ -1164,6 +1227,66 @@ impl crate::State { |
| 1164 | 1227 | .push(crate::Command::Structure(library, change)); |
| 1165 | 1228 | } |
| 1166 | 1229 | |
| 1230 | /// Moves a notebook among the others by dragging its row, `held`: the others' rows, |
| 1231 | /// `blocks`, open a `gap` where it would land, and let go it takes that place, which the |
| 1232 | /// next launch keeps. The drag ends once the row, let go, is `settled`. |
| 1233 | fn drag_notebooks( |
| 1234 | &mut self, |
| 1235 | held: Option<Entry>, |
| 1236 | blocks: &[[f32; 4]], |
| 1237 | gap: Option<[f32; 4]>, |
| 1238 | settled: bool, |
| 1239 | ) { |
| 1240 | let mine = |what: &Dragged| matches!(what, Dragged::Notebook(_)); |
| 1241 | self.settle(mine, settled); |
| 1242 | let held = held.map(|entry| { |
| 1243 | let rect = entry.rect; |
| 1244 | (Dragged::Notebook(entry), rect) |
| 1245 | }); |
| 1246 | let dropped = self.follow_drag(held, mine); |
| 1247 | let pointer = self.pointer; |
| 1248 | let Some( |
| 1249 | drag @ crate::menus::Drag { |
| 1250 | what: Dragged::Notebook(_), |
| 1251 | .. |
| 1252 | }, |
| 1253 | ) = &mut self.drag |
| 1254 | else { |
| 1255 | return; |
| 1256 | }; |
| 1257 | if drag.cancelled { |
| 1258 | drag.slot = None; |
| 1259 | } else if drag.live() { |
| 1260 | // Where the rows stand with no gap open, as `drag_entries` finds them. |
| 1261 | let spans: Vec<[f32; 2]> = blocks |
| 1262 | .iter() |
| 1263 | .map(|&[_, top, _, bottom]| { |
| 1264 | let shut = gap |
| 1265 | .filter(|gap| top >= gap[3] - 0.5) |
| 1266 | .map_or(0.0, |[_, start, _, end]| end - start); |
| 1267 | [top - shut, bottom - top] |
| 1268 | }) |
| 1269 | .collect(); |
| 1270 | let middle = drag.corner(pointer)[1] + ROW / 2.0; |
| 1271 | drag.slot = Some(ui::drop_slot(&spans, usize::MAX, middle)); |
| 1272 | } |
| 1273 | let (true, Dragged::Notebook(entry)) = (dropped, &mut drag.what) else { |
| 1274 | return; |
| 1275 | }; |
| 1276 | let from = entry.notebook; |
| 1277 | let Some(to) = drag |
| 1278 | .slot |
| 1279 | .filter(|to| *to != from && from < self.notebooks.len()) |
| 1280 | else { |
| 1281 | return; |
| 1282 | }; |
| 1283 | // It eases into its new place from where it was let go. |
| 1284 | entry.notebook = to; |
| 1285 | let moved = self.notebooks.remove(from); |
| 1286 | self.notebooks.insert(to, moved); |
| 1287 | self.save_settings(); |
| 1288 | } |
| 1289 | |
| 1167 | 1290 | /// Opens what the open panel chose: a notebook folder, a notebook's table of contents, |
| 1168 | 1291 | /// or a section, in its notebook where it has one. |
| 1169 | 1292 | pub(crate) fn open_path(&mut self, path: &std::path::Path) { |