Skip to main content

qframe/widgets/tree/
drop.rs

1//! Dropping dragged nodes into another node, such as files into a folder.
2//!
3//! A drag carries the selection. The node under the pointer is the target when the application
4//! accepts drops on it; a node cannot take itself or one of its own descendants, and a folder
5//! cannot take nodes that are already all in it. Every decision is made on the layout the drag
6//! started from, so painting a target never moves the rows the pointer is deciding on.
7
8use std::time::Duration;
9
10use crate::geometry::Rect;
11use crate::widget::EventCx;
12
13use super::super::edge_scroll::DELAY;
14use super::{Flat, Tree, TreeNode};
15
16/// A drop of nodes into another node that a [droppable](Tree::droppable) tree asks for. The tree
17/// never changes the application's nodes; the application moves them.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct TreeDrop {
20    /// The keys of the nodes that move, in the order they have in the tree. A node inside another
21    /// node that moves is left out: it travels inside it.
22    pub keys: Vec<String>,
23    /// The key of the node they move into, or `None` for the top level (a drop on the free space
24    /// below the last row).
25    pub into: Option<String>,
26}
27
28/// Builds a message from a drop.
29type DropMessage<Msg> = Box<dyn Fn(TreeDrop) -> Msg>;
30
31/// Tells whether the node with a key takes drops.
32type DropFilter = Box<dyn Fn(&str) -> bool>;
33
34/// What a droppable tree does with a drop.
35pub(super) struct Dropping<Msg> {
36    pub(super) message: DropMessage<Msg>,
37    pub(super) accepts: DropFilter,
38}
39
40impl<Msg> Dropping<Msg> {
41    pub(super) fn new(message: impl Fn(TreeDrop) -> Msg + 'static, accepts: impl Fn(&str) -> bool + 'static) -> Self {
42        Self { message: Box::new(message), accepts: Box::new(accepts) }
43    }
44}
45
46/// What the pointer of a drag is over.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub(super) enum Aim {
49    /// A node, or the top level, that takes the dragged nodes.
50    Into(Option<String>),
51    /// A node that takes drops but not these: the dragged nodes themselves, one of their
52    /// descendants, or the node they are all in already.
53    Refused(String),
54    /// A place among the dragged node's siblings, as the sibling positions a reorder moves
55    /// between; `None` while the pointer is outside the tree.
56    Reorder(Option<(usize, usize)>),
57    /// Nothing that takes the drag.
58    Nothing,
59}
60
61/// A closed node that opens when a drag rests on it, kept in the tree's memory.
62#[derive(Debug, Default)]
63pub(super) struct Spring {
64    /// The node the drag rests on and when it opens; `None` once it has.
65    pending: Option<(String, Option<Duration>)>,
66}
67
68impl<Msg: 'static> Tree<Msg> {
69    /// The node with `key`, anywhere in the tree.
70    fn find(&self, key: &str) -> Option<&TreeNode> {
71        fn walk<'a>(nodes: &'a [TreeNode], key: &str) -> Option<&'a TreeNode> {
72            nodes.iter().find_map(|node| if node.key == key { Some(node) } else { walk(&node.children, key) })
73        }
74        walk(&self.roots, key)
75    }
76
77    /// The nodes a press on `key` drags: the whole selection of a droppable tree when `key` is in
78    /// it, in tree order and without the nodes inside other dragged nodes; otherwise `key` alone.
79    pub(super) fn carried(&self, key: &str) -> Vec<String> {
80        fn walk(nodes: &[TreeNode], chosen: &[String], out: &mut Vec<String>) {
81            for node in nodes {
82                if chosen.contains(&node.key) {
83                    out.push(node.key.clone());
84                } else {
85                    walk(&node.children, chosen, out);
86                }
87            }
88        }
89        if self.dropping.is_none() || !self.is_multi() || !self.is_chosen(key) {
90            return vec![key.to_owned()];
91        }
92        let mut out = Vec::new();
93        walk(&self.roots, &self.chosen, &mut out);
94        out
95    }
96
97    /// Whether a drag of `keys` reorders siblings rather than only dropping into nodes: a
98    /// reorderable tree dragging one node.
99    pub(super) fn reorders(&self, keys: &[String]) -> bool {
100        self.on_move.is_some() && keys.len() == 1
101    }
102
103    /// The rows a drag of `keys` decides on: the dragged node folded among its resting siblings
104    /// when the drag reorders, the tree as it is otherwise.
105    pub(super) fn drag_layout(&self, keys: &[String]) -> Vec<Flat<'_>> {
106        match keys {
107            [key] if self.reorders(keys) => self.resting(key),
108            _ => self.flatten(),
109        }
110    }
111
112    /// Whether `into` cannot take `keys`: it is one of them or inside one of them, or they are
113    /// all in it already.
114    fn refuses(&self, keys: &[String], into: Option<&str>) -> bool {
115        fn holds(node: &TreeNode, key: &str) -> bool {
116            node.children.iter().any(|child| child.key == key || holds(child, key))
117        }
118        let parent = |key: &str| self.siblings(key).map(|(parent, _)| parent);
119        if keys.iter().all(|key| parent(key) == Some(into)) {
120            return true;
121        }
122        into.is_some_and(|into| {
123            keys.iter().any(|key| key == into || self.find(key).is_some_and(|node| holds(node, into)))
124        })
125    }
126
127    /// What a drag of `keys` with the pointer at `pointer` is over, with the tree's rows in `area`
128    /// scrolled by `offset`. A node that takes drops wins over a place among siblings, except the
129    /// dragged node's own row, which is where a reorder leaves it.
130    pub(super) fn aim(&self, keys: &[String], pointer: (i32, i32), area: Rect, offset: usize) -> Aim {
131        if let Some(dropping) = &self.dropping
132            && area.contains(pointer.0, pointer.1)
133        {
134            let flat = self.drag_layout(keys);
135            let index = offset + usize::try_from(pointer.1 - area.y).unwrap_or(0);
136            match flat.get(index) {
137                Some(row) => {
138                    let key = &row.node.key;
139                    let own_slot = self.reorders(keys) && keys.first() == Some(key);
140                    if (dropping.accepts)(key) && !own_slot {
141                        return if self.refuses(keys, Some(key)) {
142                            Aim::Refused(key.clone())
143                        } else {
144                            Aim::Into(Some(key.clone()))
145                        };
146                    }
147                }
148                None if !self.refuses(keys, None) => return Aim::Into(None),
149                None => {}
150            }
151        }
152        match keys {
153            [key] if self.reorders(keys) => Aim::Reorder(self.landing(key, pointer, area, offset)),
154            _ => Aim::Nothing,
155        }
156    }
157
158    /// Sends the drop or the reorder a drag of `keys` released at `aim` asks for.
159    pub(super) fn release(&self, cx: &mut EventCx<'_, Msg>, keys: Vec<String>, aim: Aim) {
160        match aim {
161            Aim::Into(into) => {
162                if let Some(dropping) = &self.dropping {
163                    cx.emit((dropping.message)(TreeDrop { keys, into }));
164                }
165            }
166            Aim::Reorder(Some((from, to))) => {
167                let Some(key) = keys.first() else { return };
168                let parent = self.siblings(key).and_then(|(parent, _)| parent.map(str::to_owned));
169                self.move_node(cx, key, parent.as_deref(), from, to);
170            }
171            Aim::Reorder(None) | Aim::Refused(_) | Aim::Nothing => {}
172        }
173    }
174
175    /// Opens a closed node a drag rests on for [`DELAY`], the wait desktop file managers give a
176    /// folder before it springs open, so a drop can reach nodes inside it. Passing over a node on
177    /// the way does not open it. Returns when the pointer should be woken next for it, since
178    /// terminals send nothing while the pointer is held still.
179    pub(super) fn spring(&self, cx: &mut EventCx<'_, Msg>, aim: &Aim) -> Option<Duration> {
180        let now = cx.now();
181        let node = match aim {
182            Aim::Into(Some(key)) => self.find(key).filter(|node| node.expandable && !node.expanded),
183            _ => None,
184        };
185        let Some(node) = node else {
186            cx.memory::<Spring>().pending = None;
187            return None;
188        };
189        let pending = cx.memory::<Spring>().pending.clone();
190        match pending {
191            Some((key, due)) if key == node.key => {
192                let due = due?;
193                if now < due {
194                    return Some(due - now);
195                }
196                cx.memory::<Spring>().pending = Some((key, None));
197                self.expand(cx, node, true);
198                None
199            }
200            _ => {
201                cx.memory::<Spring>().pending = Some((node.key.clone(), Some(now + DELAY)));
202                Some(DELAY)
203            }
204        }
205    }
206}