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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
//! The one list-editor row component (UX.md SC4).
//!
//! Every list in Settings whose items are edited one at a time (triggers,
//! snippets, custom actions, coprocesses, observer scripts, dynamic profile
//! sources, CLI agents, agent commands, prompts) draws each row's buttons
//! through [`row_actions`]: reorder ↑↓, Duplicate, Edit, and a Delete that
//! always asks first. A list passes only the buttons its data supports and
//! applies the returned [`RowAction`] after its row loop, so the row never
//! mutates the list it is iterating.
//!
//! `consistency_tests` fails if a list draws its own Edit or Delete button.
use crate::delete_confirm::{PendingDelete, confirm_delete_button};
/// Which buttons a list's rows offer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RowButtons {
/// ↑ and ↓ (the list's order matters to the user).
pub reorder: bool,
/// Duplicate (the list can hold two copies of an item).
pub duplicate: bool,
/// Edit.
pub edit: bool,
/// Delete, armed by a first click and confirmed by a second.
pub delete: bool,
}
impl RowButtons {
/// Edit and Delete.
pub const EDIT_DELETE: Self = Self {
reorder: false,
duplicate: false,
edit: true,
delete: true,
};
/// Every button.
pub const ALL: Self = Self {
reorder: true,
duplicate: true,
edit: true,
delete: true,
};
}
/// What a row's buttons asked for this frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowAction {
MoveUp(usize),
MoveDown(usize),
Duplicate(usize),
Edit(usize),
/// Delete, already confirmed.
Delete(usize),
}
/// One row's identity for the shared buttons.
pub struct Row<'a> {
/// Index of the row in its list.
pub index: usize,
/// Number of rows in the list (disables ↓ on the last).
pub len: usize,
/// Name of the list, scoping the armed-delete state.
pub list: &'static str,
/// Stable key of the item (an id or name), so an armed delete never
/// moves to another item when the list shifts.
pub key: &'a str,
/// Caption of the delete button ("Delete", "Remove").
pub delete_label: &'static str,
}
/// Draw a row's buttons, right to left from the current cursor: Delete,
/// Edit, Duplicate, ↓, ↑. Call inside a right-to-left layout so they sit at
/// the row's end. Returns the action clicked, if any.
pub fn row_actions(
ui: &mut egui::Ui,
pending: &mut PendingDelete,
row: Row<'_>,
buttons: RowButtons,
) -> Option<RowAction> {
let mut action = None;
if buttons.delete && confirm_delete_button(ui, pending, row.list, row.key, row.delete_label) {
action = Some(RowAction::Delete(row.index));
}
if buttons.edit && ui.small_button("Edit").clicked() {
action = Some(RowAction::Edit(row.index));
}
if buttons.duplicate
&& ui
.small_button("Duplicate")
.on_hover_text("Add a copy of this item below it")
.clicked()
{
action = Some(RowAction::Duplicate(row.index));
}
if buttons.reorder {
let last = row.index + 1 >= row.len;
// Nerd Font arrows (U+F063/U+F062): the classic arrows U+2191/U+2193
// are carried by NO font in the egui stack (egui's subsetted
// Ubuntu-Light, emoji-icon-font, SymbolsNerdFontMono) and render as □.
if ui
.add_enabled(!last, egui::Button::new("\u{f063}").small())
.on_hover_text("Move down")
.clicked()
{
action = Some(RowAction::MoveDown(row.index));
}
if ui
.add_enabled(row.index > 0, egui::Button::new("\u{f062}").small())
.on_hover_text("Move up")
.clicked()
{
action = Some(RowAction::MoveUp(row.index));
}
}
action
}
/// Apply a reorder or duplicate to `items`. Edit and Delete are left to the
/// list, which owns its edit form and delete side effects. Returns whether
/// the list changed.
pub fn apply_move_or_duplicate<T: Clone>(
items: &mut Vec<T>,
action: RowAction,
rename_copy: impl FnOnce(&mut T),
) -> bool {
match action {
RowAction::MoveUp(i) if i > 0 && i < items.len() => {
items.swap(i, i - 1);
true
}
RowAction::MoveDown(i) if i + 1 < items.len() => {
items.swap(i, i + 1);
true
}
RowAction::Duplicate(i) if i < items.len() => {
let mut copy = items[i].clone();
rename_copy(&mut copy);
items.insert(i + 1, copy);
true
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn moves_and_duplicates_apply_in_place() {
let mut items = vec!["a".to_string(), "b".to_string(), "c".to_string()];
assert!(apply_move_or_duplicate(
&mut items,
RowAction::MoveUp(1),
|_| {}
));
assert_eq!(items, ["b", "a", "c"]);
assert!(apply_move_or_duplicate(
&mut items,
RowAction::MoveDown(1),
|_| {}
));
assert_eq!(items, ["b", "c", "a"]);
assert!(apply_move_or_duplicate(
&mut items,
RowAction::Duplicate(0),
|s| s.push_str(" copy")
));
assert_eq!(items, ["b", "b copy", "c", "a"]);
}
#[test]
fn out_of_range_moves_are_ignored() {
let mut items = vec![1, 2];
assert!(!apply_move_or_duplicate(
&mut items,
RowAction::MoveUp(0),
|_| {}
));
assert!(!apply_move_or_duplicate(
&mut items,
RowAction::MoveDown(1),
|_| {}
));
assert!(!apply_move_or_duplicate(
&mut items,
RowAction::Edit(0),
|_| {}
));
assert_eq!(items, [1, 2]);
}
}