Skip to main content

qframe/widgets/
row_pointer.rs

1//! The pointer on the rows of a [`Table`](super::Table) or the cards of a
2//! [`CardGrid`](super::CardGrid): selecting one row, several or a range, opening with one click or
3//! two, drawing a box over the free space, and dragging the selection onto another row.
4//!
5//! Each of these is an option the widget is asked for, and a widget asked for none of them does
6//! what it always did: a press selects the row under it and opens it. The two widgets lay their
7//! rows out differently, one under another or in columns, so each says where its rows are through
8//! [`PickedRows`] and this module does the rest the same way for both.
9
10use crate::event::{MouseButton, MouseEvent, MouseKind};
11use crate::geometry::Rect;
12use crate::widget::{EventCx, PaintCx};
13
14use super::click::{Click, LastPress};
15use super::select_box::SelectBox;
16
17/// A drop of dragged rows onto another row that a droppable [`Table`](super::Table) or
18/// [`CardGrid`](super::CardGrid) asks for, such as files onto a folder. The widget never moves the
19/// application's rows; the application does.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct RowDrop {
22    /// The rows that move, by index, in their order: the pressed row alone, or the whole
23    /// selection when the drag started on one of its rows.
24    pub rows: Vec<usize>,
25    /// The row they were dropped on.
26    pub into: usize,
27}
28
29/// Builds a message from a whole new selection.
30type ChooseMessage<Msg> = Box<dyn Fn(Vec<usize>) -> Msg>;
31
32/// Builds a message from a drop.
33type DropMessage<Msg> = Box<dyn Fn(RowDrop) -> Msg>;
34
35/// Tells whether the row of an index takes drops.
36type DropFilter = Box<dyn Fn(usize) -> bool>;
37
38/// Where a point is on a widget's rows.
39pub(crate) enum Spot {
40    /// On the row of this index.
41    Row(usize),
42    /// Among the rows but on none of them: below the last one, or between two cards.
43    Free,
44    /// Not among the rows at all: a header, a scrollbar.
45    Outside,
46}
47
48/// What a widget whose rows the pointer picks tells the picking: where its rows are, and what
49/// moving the cursor to one and opening one mean for it.
50pub(crate) trait PickedRows<Msg> {
51    /// Where `(x, y)` is on the rows as they were painted last.
52    fn spot(&self, cx: &mut EventCx<'_, Msg>, x: i32, y: i32) -> Spot;
53    /// The rows `rect` covers, in their order.
54    fn covered(&self, cx: &mut EventCx<'_, Msg>, rect: Rect) -> Vec<usize>;
55    /// The row the cursor is on.
56    fn cursor(&self) -> Option<usize>;
57    /// Moves the cursor to row `index`.
58    fn select(&self, cx: &mut EventCx<'_, Msg>, index: usize);
59    /// Opens row `index`, pressed at `at`.
60    fn open(&self, cx: &mut EventCx<'_, Msg>, index: usize, at: (i32, i32));
61}
62
63/// How a widget's rows answer the pointer, as its options set it. Without any option a press
64/// selects the row and opens it.
65pub(crate) struct Picking<Msg> {
66    /// How many clicks open a row.
67    pub(crate) activate_on: Click,
68    /// The rows selected together, when several can be.
69    pub(crate) chosen: Vec<usize>,
70    /// Asks for a whole new selection; set when several rows can be selected.
71    pub(crate) on_choose: Option<ChooseMessage<Msg>>,
72    /// Whether a drag over the free space draws a box that selects.
73    pub(crate) box_select: bool,
74    /// Asks to move dragged rows, and tells which rows take them.
75    pub(crate) dropping: Option<(DropMessage<Msg>, DropFilter)>,
76    /// Asks to copy dragged rows, for a drop released with Ctrl held.
77    pub(crate) copy_drop: Option<DropMessage<Msg>>,
78}
79
80impl<Msg> Default for Picking<Msg> {
81    fn default() -> Self {
82        Self {
83            activate_on: Click::Single,
84            chosen: Vec::new(),
85            on_choose: None,
86            box_select: false,
87            dropping: None,
88            copy_drop: None,
89        }
90    }
91}
92
93/// A press on a row that may become a drag.
94#[derive(Debug, Clone)]
95struct RowPress {
96    index: usize,
97    start: (i32, i32),
98    pointer: (i32, i32),
99    dragging: bool,
100    /// The rows a drag from this press carries.
101    carried: Vec<usize>,
102    /// Whether the press kept several selected rows, which a click without a drag reduces to
103    /// this one.
104    reduce: bool,
105}
106
107/// What the held button is doing.
108#[derive(Debug, Clone)]
109enum Held {
110    Row(RowPress),
111    Box(SelectBox<usize>),
112}
113
114/// The pointer's state on a widget's rows, kept in its memory.
115#[derive(Debug, Default)]
116struct PointerRows {
117    presses: LastPress<usize>,
118    held: Option<Held>,
119    /// Where a Shift+click range starts: the row last clicked without Shift.
120    anchor: Option<usize>,
121}
122
123impl<Msg: 'static> Picking<Msg> {
124    /// Whether several rows can be selected.
125    pub(crate) fn is_multi(&self) -> bool {
126        self.on_choose.is_some()
127    }
128
129    /// Whether row `index` is one of the rows selected together.
130    pub(crate) fn is_chosen(&self, index: usize) -> bool {
131        self.is_multi() && self.chosen.contains(&index)
132    }
133
134    /// Reports `rows` as the new selection, when it differs from the current one.
135    fn choose(&self, cx: &mut EventCx<'_, Msg>, rows: Vec<usize>) {
136        if let Some(message) = &self.on_choose
137            && rows != self.chosen
138        {
139            cx.emit(message(rows));
140        }
141    }
142
143    /// Moves the cursor to row `index` and makes it the whole selection: a plain click.
144    pub(crate) fn select_one(&self, cx: &mut EventCx<'_, Msg>, rows: &impl PickedRows<Msg>, index: usize) {
145        rows.select(cx, index);
146        if self.is_multi() {
147            cx.memory::<PointerRows>().anchor = Some(index);
148            self.choose(cx, vec![index]);
149        }
150    }
151
152    /// Adds row `index` to the selection or takes it out, and moves the cursor there: Ctrl+click
153    /// and Space. False when several rows cannot be selected.
154    pub(crate) fn toggle(&self, cx: &mut EventCx<'_, Msg>, rows: &impl PickedRows<Msg>, index: usize) -> bool {
155        if !self.is_multi() {
156            return false;
157        }
158        let mut chosen = self.chosen.clone();
159        match chosen.iter().position(|row| *row == index) {
160            Some(at) => {
161                chosen.remove(at);
162            }
163            None => chosen.push(index),
164        }
165        rows.select(cx, index);
166        cx.memory::<PointerRows>().anchor = Some(index);
167        self.choose(cx, chosen);
168        true
169    }
170
171    /// Selects the rows from the last plain or Ctrl click to row `index`: Shift+click. The start
172    /// stays, so a second Shift+click reshapes the same range.
173    fn select_range(&self, cx: &mut EventCx<'_, Msg>, rows: &impl PickedRows<Msg>, index: usize) {
174        let start = cx.memory::<PointerRows>().anchor.or_else(|| rows.cursor()).unwrap_or(index);
175        cx.memory::<PointerRows>().anchor = Some(start);
176        rows.select(cx, index);
177        self.choose(cx, (start.min(index)..=start.max(index)).collect());
178    }
179
180    /// Whether dragged `carried` rows can be dropped on row `into`.
181    pub(crate) fn takes_drop(&self, carried: &[usize], into: usize) -> bool {
182        self.dropping.as_ref().is_some_and(|(_, accepts)| accepts(into)) && !carried.contains(&into)
183    }
184
185    /// Offers a pointer event to the picking. `None` when it is not the picking's to take, such as
186    /// a press outside the rows, so the widget handles it itself.
187    pub(crate) fn mouse(
188        &self,
189        cx: &mut EventCx<'_, Msg>,
190        mouse: &MouseEvent,
191        rows: &impl PickedRows<Msg>,
192    ) -> Option<bool> {
193        match mouse.kind {
194            MouseKind::Down(MouseButton::Left) => self.press(cx, mouse, rows),
195            MouseKind::Drag(MouseButton::Left) => self.drag(cx, (mouse.x, mouse.y), rows),
196            MouseKind::Up(MouseButton::Left) => self.release(cx, mouse, rows),
197            _ => None,
198        }
199    }
200
201    fn press(&self, cx: &mut EventCx<'_, Msg>, mouse: &MouseEvent, rows: &impl PickedRows<Msg>) -> Option<bool> {
202        let at = (mouse.x, mouse.y);
203        let mods = mouse.mods;
204        let index = match rows.spot(cx, mouse.x, mouse.y) {
205            Spot::Outside => return None,
206            Spot::Free if self.box_select && self.is_multi() => {
207                let drawn = SelectBox::new(at, mods.ctrl && !mods.alt, &self.chosen);
208                let covered = rows.covered(cx, drawn.rect());
209                self.choose(cx, drawn.selection(covered));
210                let memory = cx.memory::<PointerRows>();
211                memory.presses.forget();
212                memory.held = Some(Held::Box(drawn));
213                cx.capture_pointer();
214                return Some(true);
215            }
216            Spot::Free => return None,
217            Spot::Row(index) => index,
218        };
219        if self.is_multi() && !mods.alt && mods.ctrl != mods.shift {
220            cx.memory::<PointerRows>().presses.forget();
221            if mods.ctrl {
222                self.toggle(cx, rows, index);
223            } else {
224                self.select_range(cx, rows, index);
225            }
226            return Some(true);
227        }
228        let now = cx.now();
229        if self.activate_on == Click::Double && cx.memory::<PointerRows>().presses.press(index, now) {
230            rows.select(cx, index);
231            rows.open(cx, index, at);
232            return Some(true);
233        }
234        // A press on one of several selected rows keeps them all, so they can be dragged together;
235        // a click without a drag reduces them to this row on release.
236        let reduce = self.dropping.is_some() && self.chosen.len() > 1 && self.is_chosen(index);
237        if reduce {
238            rows.select(cx, index);
239        } else {
240            self.select_one(cx, rows, index);
241        }
242        if self.dropping.is_none() {
243            if self.activate_on == Click::Single {
244                rows.open(cx, index, at);
245            }
246            return Some(true);
247        }
248        let carried = if reduce { sorted(&self.chosen) } else { vec![index] };
249        let press = RowPress { index, start: at, pointer: at, dragging: false, carried, reduce };
250        cx.memory::<PointerRows>().held = Some(Held::Row(press));
251        cx.capture_pointer();
252        Some(true)
253    }
254
255    fn drag(&self, cx: &mut EventCx<'_, Msg>, at: (i32, i32), rows: &impl PickedRows<Msg>) -> Option<bool> {
256        let PointerRows { presses, held, .. } = cx.memory::<PointerRows>();
257        match held.as_mut()? {
258            Held::Box(drawn) => {
259                drawn.stretch(at);
260                let drawn = drawn.clone();
261                let covered = rows.covered(cx, drawn.rect());
262                self.choose(cx, drawn.selection(covered));
263            }
264            Held::Row(press) => {
265                press.pointer = at;
266                if !press.dragging && at != press.start {
267                    press.dragging = true;
268                    // A press that became a drag is not the first half of a double click.
269                    presses.forget();
270                }
271            }
272        }
273        Some(true)
274    }
275
276    fn release(&self, cx: &mut EventCx<'_, Msg>, mouse: &MouseEvent, rows: &impl PickedRows<Msg>) -> Option<bool> {
277        let at = (mouse.x, mouse.y);
278        match cx.memory::<PointerRows>().held.take()? {
279            Held::Box(mut drawn) => {
280                drawn.stretch(at);
281                let covered = rows.covered(cx, drawn.rect());
282                self.choose(cx, drawn.selection(covered));
283            }
284            Held::Row(press) if press.dragging => {
285                let Spot::Row(into) = rows.spot(cx, mouse.x, mouse.y) else { return Some(true) };
286                if !self.takes_drop(&press.carried, into) {
287                    return Some(true);
288                }
289                let drop = RowDrop { rows: press.carried, into };
290                // A terminal that does not report Ctrl with the pointer simply moves.
291                let message = match (&self.copy_drop, &self.dropping) {
292                    (Some(copy), _) if mouse.mods.ctrl => copy(drop),
293                    (_, Some((moving, _))) => moving(drop),
294                    (_, None) => return Some(true),
295                };
296                cx.emit(message);
297            }
298            Held::Row(press) => {
299                if press.reduce {
300                    self.select_one(cx, rows, press.index);
301                }
302                if self.activate_on == Click::Single {
303                    rows.open(cx, press.index, at);
304                }
305            }
306        }
307        Some(true)
308    }
309}
310
311/// `rows` in their order.
312fn sorted(rows: &[usize]) -> Vec<usize> {
313    let mut rows = rows.to_vec();
314    rows.sort_unstable();
315    rows
316}
317
318/// The rows being dragged and where the pointer is, while a drag lasts.
319pub(crate) fn dragged(cx: &mut PaintCx<'_>) -> Option<((i32, i32), Vec<usize>)> {
320    match &cx.memory::<PointerRows>().held {
321        Some(Held::Row(press)) if press.dragging => Some((press.pointer, press.carried.clone())),
322        _ => None,
323    }
324}
325
326/// The selection box being drawn, while it is.
327pub(crate) fn drawn_box(cx: &mut PaintCx<'_>) -> Option<Rect> {
328    match &cx.memory::<PointerRows>().held {
329        Some(Held::Box(drawn)) => Some(drawn.rect()),
330        _ => None,
331    }
332}