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