corundum_framework_gui/settings/
rail.rs1use crate::shell::{RailEntry, RailOrder, move_to, published, request};
4use crate::tr;
5
6#[derive(Default)]
16pub struct RailPage {
17 #[cfg(test)]
19 rows: Vec<egui::Rect>,
20}
21
22struct RowDrag(usize);
24
25impl RailPage {
26 pub fn new() -> RailPage {
27 RailPage::default()
28 }
29}
30
31impl super::SettingsSection for RailPage {
32 fn label(&self, ctx: &egui::Context) -> String {
33 tr!(ctx, "corundum-settings-rail")
34 }
35
36 fn ui(&mut self, ui: &mut egui::Ui) {
37 ui.heading(tr!(ui, "corundum-settings-rail"));
38 ui.separator();
39 ui.label(tr!(ui, "corundum-settings-rail-intro"));
40 ui.add_space(8.0);
41 let Some(mut entries) = published(ui.ctx()) else {
42 return;
43 };
44 if let Some(changed) = self.rows_ui(ui, &entries) {
45 match changed {
46 Change::Moved(from, to, after) => {
47 move_to(&mut entries, from, to, after);
48 }
49 Change::Toggled(i) => entries[i].shown = !entries[i].shown,
50 }
51 request(ui.ctx(), order_of(&entries));
52 }
53 ui.add_space(8.0);
54 if ui.button(tr!(ui, "corundum-settings-rail-reset")).clicked() {
55 request(ui.ctx(), RailOrder::default());
56 }
57 }
58}
59
60enum Change {
62 Moved(usize, usize, bool),
64 Toggled(usize),
66}
67
68impl RailPage {
69 fn rows_ui(&mut self, ui: &mut egui::Ui, entries: &[RailEntry]) -> Option<Change> {
70 let ctx = ui.ctx().clone();
71 let mut change = None;
72 #[cfg(test)]
73 self.rows.clear();
74 for (n, entry) in entries.iter().enumerate() {
75 let row = ui.horizontal(|ui| {
76 ui.dnd_drag_source(
77 egui::Id::unique(("corundum_rail_row", n)),
78 RowDrag(n),
79 |ui| {
80 ui.add(crate::icons::image_as(
81 ui,
82 crate::icons::grip_vertical(),
83 16.0,
84 crate::theme::Tone::Muted,
85 crate::theme::Swatch::Neutral,
86 ));
87 },
88 )
89 .response
90 .on_hover_text(tr!(&ctx, "corundum-settings-rail-drag"));
91 let mut shown = entry.shown;
92 let image = crate::icons::image(ui, entry.icon.clone(), 16.0);
93 if ui
94 .checkbox(&mut shown, (image, tr!(&ctx, entry.label)))
95 .on_hover_text(tr!(&ctx, "corundum-settings-rail-show"))
96 .changed()
97 {
98 change = Some(Change::Toggled(n));
99 }
100 });
101 let row = row.response;
102 #[cfg(test)]
103 self.rows.push(row.rect);
104 let below = ctx
106 .pointer_interact_pos()
107 .is_some_and(|p| p.y > row.rect.center().y);
108 if row.dnd_hover_payload::<RowDrag>().is_some() {
109 let y = if below {
110 row.rect.bottom()
111 } else {
112 row.rect.top()
113 };
114 ui.painter().hline(
115 row.rect.x_range(),
116 y,
117 egui::Stroke::new(2.0, ui.visuals().selection.stroke.color),
118 );
119 }
120 if let Some(drag) = row.dnd_release_payload::<RowDrag>() {
121 change = Some(Change::Moved(drag.0, n, below));
122 }
123 }
124 change
125 }
126}
127
128fn order_of(entries: &[RailEntry]) -> RailOrder {
130 RailOrder {
131 order: entries.iter().map(|e| e.key.into()).collect(),
132 hidden: entries
133 .iter()
134 .filter(|e| !e.shown)
135 .map(|e| e.key.into())
136 .collect(),
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::settings::SettingsSection;
144 use crate::{Action, Destination, Shell};
145
146 #[derive(Clone, Copy, PartialEq, Debug)]
147 enum Screen {
148 A,
149 B,
150 }
151
152 fn shell() -> Shell<Screen> {
153 Shell::from_items(vec![
154 Action::new("new", crate::icons::plus(), "new-entry").into(),
155 Destination::new(Screen::A, crate::icons::info(), "a").into(),
156 Destination::new(Screen::B, crate::icons::star(), "b").into(),
157 ])
158 }
159
160 fn frames(
162 ctx: &egui::Context,
163 shell: &mut Shell<Screen>,
164 page: &mut RailPage,
165 at: Option<egui::Pos2>,
166 ) {
167 let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(900.0, 600.0));
168 let mut frame = |events: Vec<egui::Event>| {
169 let input = egui::RawInput {
170 screen_rect: Some(rect),
171 events,
172 ..Default::default()
173 };
174 let mut output = ctx.run_ui(input, |ui| {
175 shell.ui(ui, |ui, _| {
176 egui::CentralPanel::default().show(ui, |ui| page.ui(ui));
177 })
178 });
179 output.textures_delta.clear();
180 };
181 frame(vec![]);
182 frame(vec![]);
183 if let Some(pos) = at {
184 let button = |pressed| egui::Event::PointerButton {
185 pos,
186 button: egui::PointerButton::Primary,
187 pressed,
188 modifiers: egui::Modifiers::NONE,
189 };
190 frame(vec![egui::Event::PointerMoved(pos)]);
191 frame(vec![button(true)]);
192 frame(vec![button(false)]);
193 frame(vec![]);
194 }
195 }
196
197 #[test]
198 fn the_page_lists_the_rail_in_its_order() {
199 let ctx = egui::Context::default();
200 let (mut shell, mut page) = (shell(), RailPage::new());
201 frames(&ctx, &mut shell, &mut page, None);
202 assert_eq!(page.rows.len(), 3);
203 for pair in page.rows.windows(2) {
204 assert!(pair[0].bottom() <= pair[1].top(), "{pair:?}");
205 }
206 }
207
208 #[test]
209 fn clearing_a_box_hides_the_item() {
210 let ctx = egui::Context::default();
211 let (mut shell, mut page) = (shell(), RailPage::new());
212 frames(&ctx, &mut shell, &mut page, None);
213 let row = page.rows[0];
215 frames(
216 &ctx,
217 &mut shell,
218 &mut page,
219 Some(row.left_center() + egui::vec2(30.0, 0.0)),
220 );
221 assert_eq!(shell.rail_order().hidden, ["new"]);
222 }
223
224 #[test]
225 fn the_order_of_entries_is_their_keys() {
226 let ctx = egui::Context::default();
227 let (mut shell, mut page) = (shell(), RailPage::new());
228 frames(&ctx, &mut shell, &mut page, None);
229 let mut entries = published(&ctx).unwrap();
230 move_to(&mut entries, 0, 2, true);
231 entries[1].shown = false;
232 let order = order_of(&entries);
233 assert_eq!(order.order, ["a", "b", "new"]);
234 assert_eq!(order.hidden, ["b"]);
235 }
236
237 #[test]
238 fn without_a_shell_the_page_lists_nothing() {
239 let mut page = RailPage::new();
240 crate::test_support::render([600.0, 400.0], |ui| page.ui(ui));
241 assert!(page.rows.is_empty());
242 }
243}