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