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)]
142#[allow(deprecated)]
143mod tests {
144 use super::*;
145 use crate::settings::SettingsSection;
146 use crate::{Action, Destination, Shell};
147
148 #[derive(Clone, Copy, PartialEq, Debug)]
149 enum Screen {
150 A,
151 B,
152 }
153
154 fn shell() -> Shell<Screen> {
155 Shell::from_items(vec![
156 Action::new("new", crate::icons::plus(), "new-entry").into(),
157 Destination::new(Screen::A, crate::icons::info(), "a").into(),
158 Destination::new(Screen::B, crate::icons::star(), "b").into(),
159 ])
160 }
161
162 fn frames(
164 ctx: &egui::Context,
165 shell: &mut Shell<Screen>,
166 page: &mut RailPage,
167 at: Option<egui::Pos2>,
168 ) {
169 let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(900.0, 600.0));
170 let mut frame = |events: Vec<egui::Event>| {
171 let input = egui::RawInput {
172 screen_rect: Some(rect),
173 events,
174 ..Default::default()
175 };
176 let mut output = ctx.run_ui(input, |ui| {
177 shell.ui(ui, |ui, _| {
178 egui::CentralPanel::default().show(ui, |ui| page.ui(ui));
179 })
180 });
181 output.textures_delta.clear();
182 };
183 frame(vec![]);
184 frame(vec![]);
185 if let Some(pos) = at {
186 let button = |pressed| egui::Event::PointerButton {
187 pos,
188 button: egui::PointerButton::Primary,
189 pressed,
190 modifiers: egui::Modifiers::NONE,
191 };
192 frame(vec![egui::Event::PointerMoved(pos)]);
193 frame(vec![button(true)]);
194 frame(vec![button(false)]);
195 frame(vec![]);
196 }
197 }
198
199 #[test]
200 fn the_page_lists_the_rail_in_its_order() {
201 let ctx = egui::Context::default();
202 let (mut shell, mut page) = (shell(), RailPage::new());
203 frames(&ctx, &mut shell, &mut page, None);
204 assert_eq!(page.rows.len(), 3);
205 for pair in page.rows.windows(2) {
206 assert!(pair[0].bottom() <= pair[1].top(), "{pair:?}");
207 }
208 }
209
210 #[test]
211 fn clearing_a_box_hides_the_item() {
212 let ctx = egui::Context::default();
213 let (mut shell, mut page) = (shell(), RailPage::new());
214 frames(&ctx, &mut shell, &mut page, None);
215 let row = page.rows[0];
217 frames(
218 &ctx,
219 &mut shell,
220 &mut page,
221 Some(row.left_center() + egui::vec2(30.0, 0.0)),
222 );
223 assert_eq!(shell.rail_order().hidden, ["new"]);
224 }
225
226 #[test]
227 fn the_order_of_entries_is_their_keys() {
228 let ctx = egui::Context::default();
229 let (mut shell, mut page) = (shell(), RailPage::new());
230 frames(&ctx, &mut shell, &mut page, None);
231 let mut entries = published(&ctx).unwrap();
232 move_to(&mut entries, 0, 2, true);
233 entries[1].shown = false;
234 let order = order_of(&entries);
235 assert_eq!(order.order, ["a", "b", "new"]);
236 assert_eq!(order.hidden, ["b"]);
237 }
238
239 #[test]
240 fn without_a_shell_the_page_lists_nothing() {
241 let mut page = RailPage::new();
242 crate::test_support::render([600.0, 400.0], |ui| page.ui(ui));
243 assert!(page.rows.is_empty());
244 }
245}