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