| ... | @@ -67,6 +67,12 @@ pub struct Tree<'a> { | ... | @@ -67,6 +67,12 @@ pub struct Tree<'a> { |
| 67 | rows: Vec<Entry>, | 67 | rows: Vec<Entry>, |
| 68 | /// The section or group held down, which a drag moves. | 68 | /// The section or group held down, which a drag moves. |
| 69 | held: Option<Entry>, | 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 | /// The row dragged, which the tree leaves out for a gap where it would land. | 76 | /// The row dragged, which the tree leaves out for a gap where it would land. |
| 71 | lifted: Option<(&'a Entry, &'a Landing)>, | 77 | lifted: Option<(&'a Entry, &'a Landing)>, |
| 72 | /// The dragged row as it was built, for drawing it over the others. | 78 | /// The dragged row as it was built, for drawing it over the others. |
| ... | @@ -95,6 +101,19 @@ struct Ghost { | ... | @@ -95,6 +101,19 @@ struct Ghost { |
| 95 | bold: bool, | 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 | impl<'a> Tree<'a> { | 117 | impl<'a> Tree<'a> { |
| 99 | fn new( | 118 | fn new( |
| 100 | theme: &'a Theme, | 119 | theme: &'a Theme, |
| ... | @@ -110,6 +129,9 @@ impl<'a> Tree<'a> { | ... | @@ -110,6 +129,9 @@ impl<'a> Tree<'a> { |
| 110 | action: None, | 129 | action: None, |
| 111 | rows: Vec::new(), | 130 | rows: Vec::new(), |
| 112 | held: None, | 131 | held: None, |
| | 132 | held_notebook: None, |
| | 133 | lifted_notebook: None, |
| | 134 | blocks: Vec::new(), |
| 113 | lifted: None, | 135 | lifted: None, |
| 114 | ghost: None, | 136 | ghost: None, |
| 115 | gap: None, | 137 | gap: None, |
| ... | @@ -118,7 +140,8 @@ impl<'a> Tree<'a> { | ... | @@ -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 | #[derive(Clone, PartialEq)] | 145 | #[derive(Clone, PartialEq)] |
| 123 | pub struct Entry { | 146 | pub struct Entry { |
| 124 | pub notebook: usize, | 147 | pub notebook: usize, |
| ... | @@ -212,32 +235,46 @@ fn sidebar( | ... | @@ -212,32 +235,46 @@ fn sidebar( |
| 212 | ..Spec::default() | 235 | ..Spec::default() |
| 213 | }, | 236 | }, |
| 214 | ); | 237 | ); |
| | 238 | let mut place = 0; |
| 215 | for (index, library) in notebooks.iter().enumerate() { | 239 | for (index, library) in notebooks.iter().enumerate() { |
| 216 | let key = library.key(""); | 240 | let key = library.key(""); |
| 217 | let unfolded = !folded.contains(&key); | 241 | let unfolded = !folded.contains(&key); |
| 218 | let renamed = (tree.renaming.as_ref()).is_some_and(|renaming| renaming.notebook(library)); | 242 | let renamed = (tree.renaming.as_ref()).is_some_and(|renaming| renaming.notebook(library)); |
| 219 | let (row, fold) = tree_row( | 243 | let row = Row { |
| 220 | ui, | 244 | label: &library.name, |
| 221 | tree, | 245 | icon: Leading::Notebook( |
| 222 | ui.id(("notebook", index)), | 246 | if library.in_icloud() { |
| 223 | Row { | 247 | art::NOTEBOOK_ICLOUD |
| 224 | label: &library.name, | 248 | } else { |
| 225 | icon: Leading::Notebook( | 249 | art::NOTEBOOK |
| 226 | if library.in_icloud() { | 250 | }, |
| 227 | art::NOTEBOOK_ICLOUD | 251 | library.color(), |
| 228 | } else { | 252 | ), |
| 229 | art::NOTEBOOK | 253 | depth: 0, |
| 230 | }, | 254 | dim: library.notebook.is_err(), |
| 231 | library.color(), | 255 | fold: Some(unfolded), |
| 232 | ), | 256 | selected: None, |
| 233 | depth: 0, | 257 | renamed, |
| 234 | dim: library.notebook.is_err(), | 258 | bold: tree.unread.contains(&key), |
| 235 | fold: Some(unfolded), | 259 | }; |
| 236 | selected: None, | 260 | if let Some((lifted, slot)) = tree.lifted_notebook { |
| 237 | renamed, | 261 | if lifted == index { |
| 238 | bold: tree.unread.contains(&key), | 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 | let unsigned = library.notebook.is_err() | 278 | let unsigned = library.notebook.is_err() |
| 242 | && crate::library::server_address(&library.location).is_some(); | 279 | && crate::library::server_address(&library.location).is_some(); |
| 243 | if row.clicked && unsigned { | 280 | if row.clicked && unsigned { |
| ... | @@ -251,6 +288,18 @@ fn sidebar( | ... | @@ -251,6 +288,18 @@ fn sidebar( |
| 251 | if let Some(point) = row.context { | 288 | if let Some(point) = row.context { |
| 252 | tree.action = Some(Action::Menu(Target::Notebook(Arc::clone(library)), point)); | 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 | if let Some(catalog) = library.catalog() | 303 | if let Some(catalog) = library.catalog() |
| 255 | && unfolding(ui, ("notebook rows", index), unfolded) | 304 | && unfolding(ui, ("notebook rows", index), unfolded) |
| 256 | { | 305 | { |
| ... | @@ -258,6 +307,11 @@ fn sidebar( | ... | @@ -258,6 +307,11 @@ fn sidebar( |
| 258 | ui.close(); | 307 | ui.close(); |
| 259 | ui.close(); | 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 | ui.close(); | 316 | ui.close(); |
| 263 | ui.open( | 317 | ui.open( |
| ... | @@ -549,14 +603,7 @@ fn lifts(tree: &mut Tree, notebook: usize, path: &str, row: &Row) -> bool { | ... | @@ -549,14 +603,7 @@ fn lifts(tree: &mut Tree, notebook: usize, path: &str, row: &Row) -> bool { |
| 549 | .lifted | 603 | .lifted |
| 550 | .is_some_and(|(lifted, _)| lifted.notebook == notebook && lifted.path == path); | 604 | .is_some_and(|(lifted, _)| lifted.notebook == notebook && lifted.path == path); |
| 551 | if lifted { | 605 | if lifted { |
| 552 | tree.ghost = Some(Ghost { | 606 | tree.ghost = Some(Ghost::of(row)); |
| 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 | }); | | |
| 560 | } | 607 | } |
| 561 | lifted | 608 | lifted |
| 562 | } | 609 | } |
| ... | @@ -568,14 +615,20 @@ fn gap(ui: &mut Ui, tree: &mut Tree, folder: &str, before: Option<&str>) { | ... | @@ -568,14 +615,20 @@ fn gap(ui: &mut Ui, tree: &mut Tree, folder: &str, before: Option<&str>) { |
| 568 | return; | 615 | return; |
| 569 | }; | 616 | }; |
| 570 | let here = !landing.into && landing.folder == folder && landing.before.as_deref() == before; | 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 | let height = ui.animate(id, if here { ROW } else { 0.0 }); | 625 | let height = ui.animate(id, if here { ROW } else { 0.0 }); |
| 573 | if here { | 626 | if here { |
| 574 | tree.gap = ui.rect(id); | 627 | tree.gap = ui.rect(id); |
| 575 | } | 628 | } |
| 576 | if height > 0.0 { | 629 | if height > 0.0 { |
| 577 | ui.leaf( | 630 | ui.leaf( |
| 578 | ("gap", before), | 631 | part, |
| 579 | Spec { | 632 | Spec { |
| 580 | size: [fill(), px(height)], | 633 | size: [fill(), px(height)], |
| 581 | ..Spec::default() | 634 | ..Spec::default() |
| ... | @@ -836,6 +889,14 @@ impl crate::State { | ... | @@ -836,6 +889,14 @@ impl crate::State { |
| 836 | Dragged::Entry { entry, landing, .. } => Some((entry, landing)), | 889 | Dragged::Entry { entry, landing, .. } => Some((entry, landing)), |
| 837 | _ => None, | 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 | sidebar( | 900 | sidebar( |
| 840 | &mut self.ui, | 901 | &mut self.ui, |
| 841 | &mut tree, | 902 | &mut tree, |
| ... | @@ -848,7 +909,9 @@ impl crate::State { | ... | @@ -848,7 +909,9 @@ impl crate::State { |
| 848 | let Tree { | 909 | let Tree { |
| 849 | action, | 910 | action, |
| 850 | rows, | 911 | rows, |
| 851 | mut held, | 912 | held, |
| | 913 | held_notebook, |
| | 914 | blocks, |
| 852 | ghost, | 915 | ghost, |
| 853 | gap, | 916 | gap, |
| 854 | .. | 917 | .. |
| ... | @@ -856,8 +919,13 @@ impl crate::State { | ... | @@ -856,8 +919,13 @@ impl crate::State { |
| 856 | let corner = self.ui.rect(rows_id).unwrap_or_default(); | 919 | let corner = self.ui.rect(rows_id).unwrap_or_default(); |
| 857 | let found = ghost.is_some(); | 920 | let found = ghost.is_some(); |
| 858 | let (ghost_held, settled) = self.lifted_row(theme, ghost, gap, &rows, corner); | 921 | let (ghost_held, settled) = self.lifted_row(theme, ghost, gap, &rows, corner); |
| 859 | held = held.or(ghost_held); | 922 | let settled = settled || !found; |
| 860 | self.drag_entries(held, &rows, gap, settled || !found, corner, theme); | 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 | self.ui.close(); | 929 | self.ui.close(); |
| 862 | self.ui.close(); | 930 | self.ui.close(); |
| 863 | match action { | 931 | match action { |
| ... | @@ -976,30 +1044,25 @@ impl crate::State { | ... | @@ -976,30 +1044,25 @@ impl crate::State { |
| 976 | rows: &[Entry], | 1044 | rows: &[Entry], |
| 977 | corner: [f32; 4], | 1045 | corner: [f32; 4], |
| 978 | ) -> (Option<Entry>, bool) { | 1046 | ) -> (Option<Entry>, bool) { |
| 979 | let ( | 1047 | let (Some(ghost), Some(drag)) = (ghost, &self.drag) else { |
| 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 { | | |
| 989 | return (None, true); | 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 | let key = self.ui.id("lifted"); | 1060 | let key = self.ui.id("lifted"); |
| 992 | let (top, settled) = if drag.live() { | 1061 | let (top, settled) = if drag.live() { |
| 993 | let top = drag.corner(self.pointer)[1] - corner[1]; | 1062 | let top = drag.corner(self.pointer)[1] - corner[1]; |
| 994 | (self.ui.hold(key, top), false) | 1063 | (self.ui.hold(key, top), false) |
| 995 | } else { | 1064 | } else { |
| 996 | let place = gap | 1065 | let place = gap.or(onto).unwrap_or(entry.rect); |
| 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); | | |
| 1003 | let target = place[1] - corner[1]; | 1066 | let target = place[1] - corner[1]; |
| 1004 | let top = self.ui.animate(key, target); | 1067 | let top = self.ui.animate(key, target); |
| 1005 | (top, (top - target).abs() < 0.5) | 1068 | (top, (top - target).abs() < 0.5) |
| ... | @@ -1164,6 +1227,66 @@ impl crate::State { | ... | @@ -1164,6 +1227,66 @@ impl crate::State { |
| 1164 | .push(crate::Command::Structure(library, change)); | 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 | /// Opens what the open panel chose: a notebook folder, a notebook's table of contents, | 1290 | /// Opens what the open panel chose: a notebook folder, a notebook's table of contents, |
| 1168 | /// or a section, in its notebook where it has one. | 1291 | /// or a section, in its notebook where it has one. |
| 1169 | pub(crate) fn open_path(&mut self, path: &std::path::Path) { | 1292 | pub(crate) fn open_path(&mut self, path: &std::path::Path) { |