1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! The context menu of one row of a widget that owns its rows: which row it acts on, opening it
//! with the right button or with the menu key, and drawing it.
//!
//! A single [`ContextMenu`] wrapped around a list of rows opens for the area, not for the row that
//! was pressed, so its entries would act on whatever the cursor rests on instead. A file manager
//! deleting the wrong file is that mistake at its worst. So a row widget keeps the row its menu
//! belongs to and builds the entries for that row alone; [`Tree`](super::Tree), [`Table`](super::Table)
//! and [`CardGrid`](super::CardGrid) all work this way, and this module is what they share.
use crate::event::{Event, MouseButton, MouseKind};
use crate::geometry::Rect;
use crate::widget::{EventCx, PaintCx, Widget};
use super::context_item::{self, ContextItem};
use super::context_menu::{self, ContextMenu};
/// Builds the menu entries of the row `index`.
pub(crate) type RowMenuItems<Msg> = Box<dyn Fn(usize) -> Vec<ContextItem<Msg>>>;
/// The row whose context menu is open, kept in the widget's memory.
#[derive(Debug, Default)]
pub(crate) struct MenuRow(Option<usize>);
/// A row whose menu is about to open, and where it opens.
pub(crate) struct RowAnchor {
/// The row the menu acts on.
pub(crate) row: usize,
/// The cell or the row the menu unfolds from.
pub(crate) at: Rect,
/// Whether the keyboard opened it, which highlights its first entry.
pub(crate) keyboard: bool,
}
/// The menu of `row` with its messages replaced by positions, and the messages.
fn menu_for<Msg>(items: &RowMenuItems<Msg>, row: usize) -> (ContextMenu<usize>, Vec<Msg>) {
let (entries, messages) = context_item::keyed(items(row));
(ContextMenu::new(entries), messages)
}
/// Opens the menu of `anchor`'s row and remembers whose it is.
fn open<Msg: 'static>(cx: &mut EventCx<'_, Msg>, items: &RowMenuItems<Msg>, anchor: &RowAnchor) -> bool {
let (menu, _) = menu_for(items, anchor.row);
cx.memory::<MenuRow>().0 = Some(anchor.row);
let at = anchor.at;
let keyboard = anchor.keyboard;
cx.with_messages(|cx: &mut EventCx<'_, usize>| menu.open(cx, at, keyboard));
true
}
/// Offers `event` to the row menu of a widget with `count` rows; true when the menu used it.
///
/// A right press asks `press` for the row under the pointer and where its menu unfolds from; the
/// menu key or Shift+F10 asks `key` for the row the keys are on. Both may move the selection or
/// scroll first, so the menu never acts on a row that is not shown or not meant. While the menu is
/// open it takes the keys, the wheel and presses on itself and sends the message of the chosen
/// entry; a press beside it closes it and is left to the widget, which still acts on it.
pub(crate) fn event<Msg: 'static>(
cx: &mut EventCx<'_, Msg>,
event: &Event,
items: Option<&RowMenuItems<Msg>>,
count: usize,
press: impl FnOnce(&mut EventCx<'_, Msg>, i32, i32) -> Option<RowAnchor>,
key: impl FnOnce(&mut EventCx<'_, Msg>) -> Option<RowAnchor>,
) -> bool {
let Some(items) = items else { return false };
if count == 0 {
return false;
}
let right_press = match event {
Event::Mouse(mouse) if mouse.kind == MouseKind::Down(MouseButton::Right) => Some((mouse.x, mouse.y)),
_ => None,
};
if context_menu::is_open_in(cx) {
let row = cx.memory::<MenuRow>().0.filter(|row| *row < count);
let elsewhere = right_press.is_some_and(|(x, y)| !context_menu::contains(cx, x, y));
// A right press beside the menu, or a row that went away, closes it; the press may open the
// menu of another row below it.
match row.filter(|_| !elsewhere) {
Some(row) => {
let (menu, messages) = menu_for(items, row);
let (used, chosen) = cx.with_messages(|cx| menu.event(cx, event));
if let Some(message) = chosen.last().and_then(|chosen| messages.into_iter().nth(*chosen)) {
cx.emit(message);
}
return used;
}
None => {
cx.with_messages(|cx: &mut EventCx<'_, usize>| ContextMenu::<usize>::close(cx));
}
}
}
let anchor = match event {
Event::Mouse(_) => match right_press {
Some((x, y)) => press(cx, x, y),
None => None,
},
Event::Key(pressed) if context_menu::is_menu_key(pressed) => key(cx),
_ => None,
};
anchor.is_some_and(|anchor| open(cx, items, &anchor))
}
/// The row whose menu is open in the widget being painted, whose surface stays raised as if
/// hovered so it is clear what the menu acts on.
pub(crate) fn open_row<Msg>(cx: &mut PaintCx<'_>, items: Option<&RowMenuItems<Msg>>) -> Option<usize> {
if items.is_none() || !context_menu::is_open(cx) {
return None;
}
cx.memory::<MenuRow>().0
}
/// Paints the open row menu; called from the widget's `paint_overlay`.
pub(crate) fn paint<Msg: 'static>(cx: &mut PaintCx<'_>, items: Option<&RowMenuItems<Msg>>, anchor: Rect) {
let Some(row) = open_row(cx, items) else { return };
let Some(items) = items else { return };
let (menu, _) = menu_for(items, row);
menu.paint_overlay(cx, anchor);
}