corundum_framework_gui/settings/
mod.rs1mod appearance;
19mod layout;
20mod rail;
21mod scope;
22
23pub use appearance::{AppearanceChange, AppearanceCtx, AppearancePage, AppearanceScope};
24pub use layout::{ListItem, SettingsLayout};
25pub use rail::RailPage;
26pub use scope::{ScopeTab, scope_tabs};
27
28pub trait SettingsSection {
30 fn label(&self, ctx: &egui::Context) -> String;
33
34 fn ui(&mut self, ui: &mut egui::Ui);
36
37 fn dirty(&self) -> bool {
39 false
40 }
41}
42
43enum Heading {
45 None,
47 Message(&'static str),
49 Text(String),
51}
52
53pub struct SettingsGroup {
55 heading: Heading,
56 sections: Vec<Box<dyn SettingsSection>>,
57}
58
59impl SettingsGroup {
60 pub fn new(sections: Vec<Box<dyn SettingsSection>>) -> SettingsGroup {
62 SettingsGroup {
63 heading: Heading::None,
64 sections,
65 }
66 }
67
68 pub fn titled(mut self, id: &'static str) -> SettingsGroup {
70 self.heading = Heading::Message(id);
71 self
72 }
73
74 pub fn named(mut self, name: impl Into<String>) -> SettingsGroup {
76 self.heading = Heading::Text(name.into());
77 self
78 }
79}
80
81#[derive(Default)]
94pub struct Settings {
95 groups: Vec<SettingsGroup>,
96 layout: SettingsLayout<(usize, usize)>,
98}
99
100impl Settings {
101 pub fn new() -> Settings {
103 Settings::default()
104 }
105
106 pub fn with_group(mut self, group: SettingsGroup) -> Settings {
108 self.groups.push(group);
109 self
110 }
111
112 pub fn push_group(&mut self, group: SettingsGroup) {
115 self.groups.push(group);
116 }
117
118 pub fn open(&mut self, group: usize, section: usize) {
120 self.layout.open(Some((group, section)));
121 }
122
123 pub fn current(&self) -> Option<(usize, usize)> {
125 self.layout.current().copied()
126 }
127
128 pub fn ui(&mut self, ui: &mut egui::Ui) {
130 let items = self.items(ui.ctx());
131 let Settings { groups, layout } = self;
132 layout.ui(ui, &items, |ui, (g, s)| {
133 if let Some(section) = groups.get_mut(g).and_then(|g| g.sections.get_mut(s)) {
134 section.ui(ui);
135 }
136 });
137 }
138
139 fn items(&self, ctx: &egui::Context) -> Vec<ListItem<(usize, usize)>> {
141 let mut out = Vec::new();
142 for (g, group) in self.groups.iter().enumerate() {
143 match &group.heading {
144 Heading::None if g > 0 => out.push(ListItem::Gap),
145 Heading::None => {}
146 Heading::Message(id) => out.push(ListItem::Heading(crate::i18n::tr(ctx, id))),
147 Heading::Text(text) => out.push(ListItem::Heading(text.clone())),
148 }
149 for (s, section) in group.sections.iter().enumerate() {
150 out.push(ListItem::Entry {
151 page: (g, s),
152 label: section.label(ctx),
153 dirty: section.dirty(),
154 });
155 }
156 }
157 out
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::test_support::render;
165
166 struct Named(&'static str, bool);
167
168 impl SettingsSection for Named {
169 fn label(&self, _ctx: &egui::Context) -> String {
170 self.0.to_owned()
171 }
172
173 fn ui(&mut self, ui: &mut egui::Ui) {
174 ui.label(self.0);
175 }
176
177 fn dirty(&self) -> bool {
178 self.1
179 }
180 }
181
182 fn settings() -> Settings {
183 Settings::new()
184 .with_group(
185 SettingsGroup::new(vec![
186 Box::new(Named("Look", false)),
187 Box::new(Named("Sync", true)),
188 ])
189 .named("Example"),
190 )
191 .with_group(
192 SettingsGroup::new(vec![Box::new(Named("Appearance", false))])
193 .titled("corundum-settings"),
194 )
195 .with_group(SettingsGroup::new(vec![Box::new(Named("About", false))]))
196 }
197
198 fn labels(settings: &Settings) -> Vec<String> {
199 let ctx = egui::Context::default();
200 crate::i18n::install(
201 &ctx,
202 &crate::i18n::LanguageSettings {
203 language: crate::i18n::Language::Explicit("en".into()),
204 },
205 );
206 settings
207 .items(&ctx)
208 .into_iter()
209 .map(|item| match item {
210 ListItem::Heading(h) => format!("# {h}"),
211 ListItem::Entry { label, dirty, .. } => layout::marked(&label, dirty),
212 ListItem::Gap => "-".into(),
213 })
214 .collect()
215 }
216
217 #[test]
218 fn groups_are_listed_in_order() {
219 let mut settings = settings();
220 settings
221 .push_group(SettingsGroup::new(vec![Box::new(Named("Later", false))]).named("Late"));
222 let labels = labels(&settings);
223 assert_eq!(
224 labels,
225 [
226 "# Example",
227 "Look",
228 "Sync •",
229 "# Settings",
230 "Appearance",
231 "-",
232 "About",
233 "# Late",
234 "Later",
235 ]
236 );
237 }
238
239 #[test]
240 fn wide_it_opens_the_first_page() {
241 let mut settings = settings();
242 render([900.0, 600.0], |ui| settings.ui(ui));
243 assert_eq!(settings.current(), Some((0, 0)));
244 }
245
246 #[test]
247 fn compact_it_shows_the_list_then_the_page_opened() {
248 let mut settings = settings();
249 render([400.0, 700.0], |ui| settings.ui(ui));
250 assert_eq!(settings.current(), None);
251 settings.open(2, 0);
252 render([400.0, 700.0], |ui| settings.ui(ui));
253 assert_eq!(settings.current(), Some((2, 0)));
254 }
255}