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