Skip to main content

qframe/widgets/tree/
edit.rs

1//! Reordering a tree's nodes among their siblings, by dragging or with the keys, and the context
2//! menu of a node.
3//!
4//! Siblings are a stack of rows, so a drag works like the tabs of a [`TabRail`](super::super::TabRail):
5//! the landing place is the resting sibling under the pointer ([`drop_target`]), the siblings make
6//! room while the drag lasts ([`preview_order`]), a ghost follows the pointer and the move is the
7//! same `from`/`to` pair [`TabEdit::Move`] applies.
8
9use crate::event::{Event, KeyEvent, MouseButton, MouseEvent, MouseKind};
10use crate::geometry::Rect;
11use crate::keymap::Key;
12use crate::widget::{EventCx, PaintCx, Widget};
13
14use super::super::TabEdit;
15use super::super::click::Click;
16use super::super::context_item;
17use super::super::context_menu::{self, ContextMenu};
18use super::super::edge_scroll::{Edge, EdgeScroll, Zone};
19use super::super::rows::RowScroll;
20use super::super::tab_model::{Direction, drop_target};
21use super::drop::{Aim, Spring};
22use super::{Flat, Tree, TreeNode};
23
24/// A move of one node among its siblings that a [reorderable](Tree::reorderable) tree asks for.
25/// The tree never changes the application's nodes; [`apply`](Self::apply) does the bookkeeping on
26/// the application's own list of siblings.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct TreeMove {
29    /// The key of the node that moves.
30    pub key: String,
31    /// The key of its parent, `None` for a top-level node. The node keeps this parent.
32    pub parent: Option<String>,
33    /// The node's position among its siblings before the move, counted from 0.
34    pub from: usize,
35    /// Its position after the move.
36    pub to: usize,
37}
38
39impl TreeMove {
40    /// Moves the sibling at `from` so that its position becomes `to`, the list the application
41    /// keeps for [`parent`](Self::parent)'s children (or its top-level nodes). Positions out of
42    /// range are ignored.
43    pub fn apply<T>(&self, siblings: &mut Vec<T>) {
44        let mut unused = 0;
45        TabEdit::Move { from: self.from, to: self.to }.apply(siblings, &mut unused);
46    }
47}
48
49/// How a drag lays the tree out: the dragged node folded (its children travel with it and would
50/// only hide the siblings) and, while it is over a landing place, its siblings in the order the
51/// drop would give.
52pub(super) struct Arrange<'k> {
53    pub(super) key: &'k str,
54    pub(super) parent: Option<&'k str>,
55    pub(super) order: Option<(usize, usize)>,
56}
57
58/// A pointer press on a row of a reorderable or droppable tree that may become a drag.
59#[derive(Debug, Clone)]
60struct Press {
61    key: String,
62    /// The nodes a drag from this press carries: the pressed node, or the selection it is in.
63    keys: Vec<String>,
64    start: (i32, i32),
65    pointer: (i32, i32),
66    dragging: bool,
67    /// Whether the press kept several selected rows that a click without a drag reduces to this
68    /// one.
69    reduce: bool,
70}
71
72/// Pointer state of a reorderable tree, kept in its memory.
73#[derive(Debug, Default)]
74struct TreePointer {
75    pressed: Option<Press>,
76    /// Scrolling while the dragged node rests against the top or bottom row.
77    edge: EdgeScroll,
78}
79
80/// A node being dragged, for painting.
81pub(super) struct Drag {
82    /// The pressed node.
83    pub(super) key: String,
84    /// Every node the drag carries.
85    pub(super) keys: Vec<String>,
86    pub(super) pointer: (i32, i32),
87}
88
89/// The node whose context menu is open, kept in the tree's memory.
90#[derive(Debug, Default)]
91struct MenuNode(Option<String>);
92
93impl<Msg: 'static> Tree<Msg> {
94    /// The siblings of `key` with its parent key: the parent's children, or the top-level nodes.
95    pub(super) fn siblings(&self, key: &str) -> Option<(Option<&str>, &[TreeNode])> {
96        fn find<'a>(
97            nodes: &'a [TreeNode],
98            parent: Option<&'a str>,
99            key: &str,
100        ) -> Option<(Option<&'a str>, &'a [TreeNode])> {
101            if nodes.iter().any(|node| node.key == key) {
102                return Some((parent, nodes));
103            }
104            nodes.iter().find_map(|node| find(&node.children, Some(&node.key), key))
105        }
106        find(&self.roots, None, key)
107    }
108
109    /// Asks to move `key` from sibling position `from` to `to`.
110    pub(super) fn move_node(&self, cx: &mut EventCx<'_, Msg>, key: &str, parent: Option<&str>, from: usize, to: usize) {
111        if from != to
112            && let Some(message) = &self.on_move
113        {
114            cx.emit(message(TreeMove { key: key.to_owned(), parent: parent.map(str::to_owned), from, to }));
115        }
116    }
117
118    /// Ctrl+Shift+↑/↓ move the selected node one place among its siblings; true when used.
119    pub(super) fn move_key(&self, cx: &mut EventCx<'_, Msg>, key: &KeyEvent) -> bool {
120        let mods = key.chord.mods;
121        let up = key.chord.key == Key::Up;
122        if self.on_move.is_none() || !mods.ctrl || !mods.shift || mods.alt || !(up || key.chord.key == Key::Down) {
123            return false;
124        }
125        let Some(selected) = self.selected.as_deref() else {
126            return true;
127        };
128        let Some((parent, siblings)) = self.siblings(selected) else {
129            return true;
130        };
131        let Some(from) = siblings.iter().position(|node| node.key == selected) else {
132            return true;
133        };
134        let to = if up { from.saturating_sub(1) } else { (from + 1).min(siblings.len() - 1) };
135        self.move_node(cx, selected, parent, from, to);
136        true
137    }
138
139    /// The resting layout of a drag of `key`: the node folded, the siblings in their order.
140    pub(super) fn resting(&self, key: &str) -> Vec<Flat<'_>> {
141        let parent = self.siblings(key).and_then(|(parent, _)| parent);
142        self.flatten_with(Some(&Arrange { key, parent, order: None }))
143    }
144
145    /// Where a drag of `key` with the pointer at `pointer` would land: the dragged node's sibling
146    /// position, the landing position and the parent, computed on the resting layout so the target
147    /// stays put while the siblings make room.
148    pub(super) fn landing(&self, key: &str, pointer: (i32, i32), area: Rect, offset: usize) -> Option<(usize, usize)> {
149        let flat = self.resting(key);
150        let row = flat.iter().position(|row| row.node.key == key)?;
151        let parent = flat[row].parent;
152        let from = flat[row].index;
153        let visible = usize::from(area.height);
154        let slots: Vec<(usize, Rect)> = flat
155            .iter()
156            .enumerate()
157            .skip(offset)
158            .take(visible)
159            .filter(|(_, other)| other.parent == parent)
160            .map(|(at, other)| {
161                let y = area.y + i32::try_from(at - offset).unwrap_or(0);
162                (other.index, Rect::new(area.x, y, area.width, 1))
163            })
164            .collect();
165        Some((from, drop_target(&slots, from, pointer, Direction::Down)))
166    }
167
168    /// The node being dragged right now, if any.
169    pub(super) fn drag(&self, cx: &mut PaintCx<'_>) -> Option<Drag> {
170        if self.on_move.is_none() && self.dropping.is_none() {
171            return None;
172        }
173        let press = cx.memory::<TreePointer>().pressed.clone()?;
174        (press.dragging && self.siblings(&press.key).is_some()).then_some(Drag {
175            key: press.key,
176            keys: press.keys,
177            pointer: press.pointer,
178        })
179    }
180
181    /// Pointer input of a reorderable or droppable tree on row `index` of `flat` (the row under the
182    /// pointer, if any). A press selects the row and holds it; moving a row's height makes it a
183    /// drag, and the release drops it into the node under the pointer or among its siblings. A
184    /// press and release without a drag opens, closes or activates the row like Enter. Returns
185    /// `None` when the event is not the tree's to take.
186    pub(super) fn drag_pointer(
187        &self,
188        cx: &mut EventCx<'_, Msg>,
189        mouse: &MouseEvent,
190        flat: &[Flat<'_>],
191        index: Option<usize>,
192    ) -> Option<bool> {
193        let at = (mouse.x, mouse.y);
194        match mouse.kind {
195            MouseKind::Down(MouseButton::Left) => {
196                let index = index?;
197                if self.modified_press(cx, flat, index, mouse.mods) {
198                    return Some(true);
199                }
200                let key = flat[index].node.key.clone();
201                if self.double_press(cx, &key) {
202                    self.select_one(cx, flat, index);
203                    self.open_or_activate(cx, index, flat[index].node);
204                    return Some(true);
205                }
206                // A press on one of several selected rows keeps them all, so they can be dragged
207                // together; a click without a drag reduces them to this row on release.
208                let reduce = self.is_among_many(&key);
209                if reduce {
210                    self.select(cx, flat, index);
211                } else {
212                    self.select_one(cx, flat, index);
213                }
214                cx.capture_pointer();
215                *cx.memory::<Spring>() = Spring::default();
216                let keys = self.carried(&key);
217                let press = Press { key, keys, start: at, pointer: at, dragging: false, reduce };
218                *cx.memory::<TreePointer>() = TreePointer { pressed: Some(press), edge: EdgeScroll::default() };
219                Some(true)
220            }
221            MouseKind::Drag(MouseButton::Left) => {
222                let memory = cx.memory::<TreePointer>();
223                let press = memory.pressed.as_mut()?;
224                press.pointer = at;
225                let starts = !press.dragging && (at.1 - press.start.1).abs() >= 1;
226                if starts {
227                    press.dragging = true;
228                }
229                let dragging = press.dragging;
230                let keys = press.keys.clone();
231                // Reordering moves one node: dragging one of several selected ones carries it alone.
232                let carried_alone = starts && press.reduce && self.dropping.is_none();
233                if carried_alone {
234                    press.reduce = false;
235                }
236                if starts {
237                    // A press that became a drag is not the first half of a double click.
238                    self.forget_press(cx);
239                }
240                if carried_alone {
241                    self.choose(cx, keys.clone());
242                }
243                self.edge_scroll(cx, mouse.y, flat.len(), dragging);
244                if dragging && self.dropping.is_some() {
245                    let offset = cx.memory::<RowScroll>().offset;
246                    let aim = self.aim(&keys, at, Self::rows_area(cx.area(), flat.len()), offset);
247                    self.wake_for_spring(cx, &aim);
248                }
249                Some(true)
250            }
251            MouseKind::Up(MouseButton::Left) => {
252                let memory = cx.memory::<TreePointer>();
253                memory.edge = EdgeScroll::default();
254                let press = memory.pressed.take()?;
255                let area = cx.area();
256                *cx.memory::<Spring>() = Spring::default();
257                if press.dragging {
258                    let offset = cx.memory::<RowScroll>().offset;
259                    let aim = self.aim(&press.keys, at, Self::rows_area(area, flat.len()), offset);
260                    self.release(cx, press.keys, aim, mouse.mods.ctrl);
261                } else if let Some(row) = flat.iter().position(|row| row.node.key == press.key) {
262                    if press.reduce {
263                        self.select_one(cx, flat, row);
264                    }
265                    if self.activate_on == Click::Single {
266                        self.open_or_activate(cx, row, flat[row].node);
267                    }
268                }
269                Some(true)
270            }
271            _ => None,
272        }
273    }
274
275    /// Opens the closed node a drag rests on once it has rested long enough, and keeps the pointer
276    /// waking until then, no later than the edge scrolling needs it.
277    fn wake_for_spring(&self, cx: &mut EventCx<'_, Msg>, aim: &Aim) {
278        let now = cx.now();
279        let edge = cx.memory::<TreePointer>().edge.due();
280        match (self.spring(cx, aim), edge) {
281            (Some(wait), Some(due)) => cx.repeat_pointer(wait.min(due.saturating_sub(now))),
282            (Some(wait), None) => cx.repeat_pointer(wait),
283            // The edge scrolling keeps its own wakeups; without it nothing is left to wait for.
284            (None, Some(_)) => {}
285            (None, None) => cx.stop_pointer_repeat(),
286        }
287    }
288
289    /// The rows of `area` with `total` rows: the scrollbar column is not a landing place.
290    pub(super) fn rows_area(area: Rect, total: usize) -> Rect {
291        let overflows = total > usize::from(area.height);
292        Rect::new(area.x, area.y, area.width.saturating_sub(u16::from(overflows)), area.height)
293    }
294
295    /// Scrolls one row while a dragged node rests on the top or bottom row or past them.
296    fn edge_scroll(&self, cx: &mut EventCx<'_, Msg>, y: i32, total: usize, dragging: bool) {
297        let area = cx.area();
298        let last = area.bottom() - 1;
299        let zone = if y <= area.y {
300            Some(Zone { edge: Edge::Back, beyond: u16::try_from(area.y - y).unwrap_or(u16::MAX) })
301        } else if y >= last {
302            Some(Zone { edge: Edge::Forward, beyond: u16::try_from(y - last).unwrap_or(u16::MAX) })
303        } else {
304            None
305        };
306        let visible = usize::from(area.height);
307        let mut edge = cx.memory::<TreePointer>().edge;
308        edge.drive(cx, zone.filter(|_| dragging), |cx, edge| {
309            let memory = cx.memory::<RowScroll>();
310            memory.offset = match edge {
311                Edge::Back => memory.offset.checked_sub(1)?,
312                Edge::Forward => Some(memory.offset + 1).filter(|next| *next + visible <= total)?,
313            };
314            Some(memory.offset)
315        });
316        cx.memory::<TreePointer>().edge = edge;
317    }
318
319    /// The context menu of `key` with its messages replaced by positions, and the messages.
320    fn menu_for(&self, key: &str) -> Option<(ContextMenu<usize>, Vec<Msg>)> {
321        let items = self.menu.as_ref()?;
322        let (items, messages) = context_item::keyed(items(key));
323        Some((ContextMenu::new(items), messages))
324    }
325
326    fn open_menu(&self, cx: &mut EventCx<'_, Msg>, key: &str, anchor: Rect, keyboard: bool) {
327        let Some((menu, _)) = self.menu_for(key) else {
328            return;
329        };
330        cx.memory::<MenuNode>().0 = Some(key.to_owned());
331        cx.with_messages(|cx: &mut EventCx<'_, usize>| menu.open(cx, anchor, keyboard));
332    }
333
334    /// Offers `event` to the context menu, when the tree has one; true when the menu used it.
335    ///
336    /// A right press on a row opens the menu of that node at the pointer; the menu key or
337    /// Shift+F10 opens the menu of the selected node below its row, scrolling it into view first.
338    /// While the menu is open it takes the keys, the wheel and presses on itself and sends the
339    /// message of the chosen entry; a left press anywhere else closes it and is not used, so the
340    /// tree still acts on it.
341    pub(super) fn menu_event(&self, cx: &mut EventCx<'_, Msg>, event: &Event, flat: &[Flat<'_>]) -> bool {
342        if self.menu.is_none() || flat.is_empty() {
343            return false;
344        }
345        let right_press = match event {
346            Event::Mouse(mouse) if mouse.kind == MouseKind::Down(MouseButton::Right) => Some((mouse.x, mouse.y)),
347            _ => None,
348        };
349        if context_menu::is_open_in(cx) {
350            let node = cx.memory::<MenuNode>().0.clone().filter(|key| flat.iter().any(|row| row.node.key == *key));
351            let elsewhere = right_press.is_some_and(|(x, y)| !context_menu::contains(cx, x, y));
352            // A right press beside the menu, or a node that went away, closes it; the press may open
353            // the menu of another node below.
354            match node.and_then(|key| self.menu_for(&key)).filter(|_| !elsewhere) {
355                Some((menu, messages)) => {
356                    let (used, chosen) = cx.with_messages(|cx| menu.event(cx, event));
357                    if let Some(message) = chosen.last().and_then(|chosen| messages.into_iter().nth(*chosen)) {
358                        cx.emit(message);
359                    }
360                    return used;
361                }
362                None => {
363                    cx.with_messages(|cx: &mut EventCx<'_, usize>| ContextMenu::<usize>::close(cx));
364                }
365            }
366        }
367        let area = cx.area();
368        let visible = usize::from(area.height);
369        match event {
370            Event::Mouse(_) => {
371                let Some((x, y)) = right_press else {
372                    return false;
373                };
374                let offset = cx.memory::<RowScroll>().offset;
375                let row = usize::try_from(y - area.y).ok().map(|r| offset + r).filter(|i| *i < flat.len());
376                let Some(row) = row.filter(|_| x < Self::rows_area(area, flat.len()).right()) else {
377                    return false;
378                };
379                // The menu of a selected row acts on the whole selection, which the application
380                // knows; outside the selection the row becomes the selection first, so the menu
381                // never acts on rows the user did not mean.
382                if self.is_multi() && !self.is_chosen(&flat[row].node.key) {
383                    self.select_one(cx, flat, row);
384                }
385                self.open_menu(cx, &flat[row].node.key, Rect::new(x, y, 1, 1), false);
386                true
387            }
388            Event::Key(key) if context_menu::is_menu_key(key) => {
389                let Some(row) = self.selected_index(flat) else {
390                    return false;
391                };
392                let memory = cx.memory::<RowScroll>();
393                if row < memory.offset {
394                    memory.offset = row;
395                } else if visible > 0 && row >= memory.offset + visible {
396                    memory.offset = row + 1 - visible;
397                }
398                let y = area.y + i32::try_from(row - memory.offset).unwrap_or(0);
399                let anchor = Rect::new(area.x, y, Self::rows_area(area, flat.len()).width, 1);
400                self.open_menu(cx, &flat[row].node.key, anchor, true);
401                true
402            }
403            _ => false,
404        }
405    }
406
407    /// The node whose context menu is open in the tree being painted; its row stays raised as if
408    /// hovered, so it is clear what the menu acts on.
409    pub(super) fn menu_node(&self, cx: &mut PaintCx<'_>) -> Option<String> {
410        if self.menu.is_none() || !context_menu::is_open(cx) {
411            return None;
412        }
413        cx.memory::<MenuNode>().0.clone()
414    }
415
416    /// Paints the open context menu; called from `paint_overlay`.
417    pub(super) fn paint_menu(&self, cx: &mut PaintCx<'_>, anchor: Rect) {
418        if let Some((menu, _)) = self.menu_node(cx).and_then(|key| self.menu_for(&key)) {
419            menu.paint_overlay(cx, anchor);
420        }
421    }
422}