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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! 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`.
///
/// A widget handed a closure for its entries keeps it boxed for as long as it lives, so it outlives
/// every frame; one that keeps its entries in its own data borrows them instead, so a row's menu
/// is read from where its labels are rather than from a copy of them.
pub(crate) type RowMenuItems<'a, Msg> = dyn Fn(usize) -> Vec<ContextItem<Msg>> + 'a;
/// 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
}
/// Opens the menu of `anchor`'s row as the row's own action, for a widget whose activation is its
/// menu. A row whose menu has no entries opens nothing, as with the right button.
pub(crate) fn open_as_action<Msg: 'static>(
cx: &mut EventCx<'_, Msg>,
items: Option<&RowMenuItems<'_, Msg>>,
anchor: &RowAnchor,
) -> bool {
items.is_some_and(|items| open(cx, items, anchor))
}
/// 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);
}