1#[cfg(feature = "settings")]
2use crate::settings::{Setting, SettingType, SettingValue};
3use bevy::prelude::*;
4
5#[derive(Component, Clone)]
7pub struct SettingsScreenConfig {
8 pub title: String,
9 pub sections: Vec<SettingsSection>,
10 pub allow_dismissal: bool,
11 pub back_button_text: String,
12 pub navigation_enabled: bool,
13 pub mobile_layout: bool,
14}
15
16impl Default for SettingsScreenConfig {
17 fn default() -> Self {
18 Self {
19 title: "Settings".to_string(),
20 sections: vec![],
21 allow_dismissal: true,
22 back_button_text: "Back".to_string(),
23 navigation_enabled: true,
24 mobile_layout: false,
25 }
26 }
27}
28
29impl SettingsScreenConfig {
30 pub fn new(title: impl Into<String>) -> Self {
31 Self {
32 title: title.into(),
33 ..Default::default()
34 }
35 }
36
37 pub fn with_sections(mut self, sections: Vec<SettingsSection>) -> Self {
38 self.sections = sections;
39 self
40 }
41
42 pub fn add_section(mut self, section: SettingsSection) -> Self {
43 self.sections.push(section);
44 self
45 }
46
47 pub fn with_back_button_text(mut self, text: impl Into<String>) -> Self {
48 self.back_button_text = text.into();
49 self
50 }
51
52 pub fn with_navigation(mut self, enabled: bool) -> Self {
53 self.navigation_enabled = enabled;
54 self
55 }
56
57 pub fn mobile_layout(mut self, mobile: bool) -> Self {
58 self.mobile_layout = mobile;
59 self
60 }
61
62 pub fn no_dismissal(mut self) -> Self {
63 self.allow_dismissal = false;
64 self
65 }
66}
67
68#[derive(Clone)]
70pub struct SettingsSection {
71 pub title: String,
72 pub settings: Vec<ScreenSettingsItem>,
73}
74
75impl SettingsSection {
76 pub fn new(title: impl Into<String>) -> Self {
77 Self {
78 title: title.into(),
79 settings: vec![],
80 }
81 }
82
83 pub fn with_settings(mut self, settings: Vec<ScreenSettingsItem>) -> Self {
84 self.settings = settings;
85 self
86 }
87
88 pub fn add_setting(mut self, setting: ScreenSettingsItem) -> Self {
89 self.settings.push(setting);
90 self
91 }
92}
93
94#[derive(Clone)]
97pub struct ScreenSettingsItem {
98 pub id: String,
99 pub label: String,
100 #[cfg(feature = "settings")]
101 pub setting_type: SettingType,
102 #[cfg(not(feature = "settings"))]
103 pub setting_type: ScreenOnlySettingType,
104 #[cfg(feature = "settings")]
105 pub current_value: SettingValue,
106 #[cfg(not(feature = "settings"))]
107 pub current_value: ScreenSettingValue,
108 pub navigation_index: Option<usize>,
109}
110
111#[cfg(not(feature = "settings"))]
113#[derive(Clone)]
114pub enum ScreenOnlySettingType {
115 Toggle,
116 FloatRange {
117 min: f32,
118 max: f32,
119 step: f32,
120 },
121 IntRange {
122 min: i32,
123 max: i32,
124 step: i32,
125 },
126 Selection {
127 options: Vec<String>,
128 },
129 Text {
130 max_length: Option<usize>,
131 },
132 Custom {
133 display_fn: fn(&ScreenSettingValue) -> String,
134 },
135}
136
137#[cfg(not(feature = "settings"))]
139#[derive(Debug, Clone)]
140pub enum ScreenSettingValue {
141 Bool(bool),
142 Float(f32),
143 Int(i32),
144 String(String),
145 Selection(usize),
146}
147
148impl ScreenSettingsItem {
149 #[cfg(feature = "settings")]
150 pub fn new(
151 id: impl Into<String>,
152 label: impl Into<String>,
153 setting_type: SettingType,
154 current_value: SettingValue,
155 ) -> Self {
156 Self {
157 id: id.into(),
158 label: label.into(),
159 setting_type,
160 current_value,
161 navigation_index: None,
162 }
163 }
164
165 #[cfg(not(feature = "settings"))]
166 pub fn new(
167 id: impl Into<String>,
168 label: impl Into<String>,
169 setting_type: ScreenOnlySettingType,
170 current_value: ScreenSettingValue,
171 ) -> Self {
172 Self {
173 id: id.into(),
174 label: label.into(),
175 setting_type,
176 current_value,
177 navigation_index: None,
178 }
179 }
180
181 pub fn with_navigation_index(mut self, index: usize) -> Self {
182 self.navigation_index = Some(index);
183 self
184 }
185
186 #[cfg(feature = "settings")]
188 pub fn with_value(mut self, value: SettingValue) -> Self {
189 self.current_value = value;
190 self
191 }
192
193 #[cfg(not(feature = "settings"))]
194 pub fn with_value(mut self, value: ScreenSettingValue) -> Self {
195 self.current_value = value;
196 self
197 }
198}
199
200#[cfg(feature = "settings")]
202impl From<&Setting> for ScreenSettingsItem {
203 fn from(setting: &Setting) -> Self {
204 Self {
205 id: setting.id.clone(),
206 label: setting.label.clone(),
207 setting_type: setting.setting_type.clone(),
208 current_value: setting.value.clone(),
209 navigation_index: setting.tab_index,
210 }
211 }
212}
213
214#[cfg(feature = "settings")]
215impl ScreenSettingsItem {
216 pub fn from_setting(setting: &Setting) -> Self {
218 setting.into()
219 }
220
221 pub fn toggle(id: impl Into<String>, label: impl Into<String>, current_value: bool) -> Self {
223 Self::new(
224 id,
225 label,
226 SettingType::Toggle,
227 SettingValue::Bool(current_value),
228 )
229 }
230
231 pub fn slider(
232 id: impl Into<String>,
233 label: impl Into<String>,
234 current_value: f32,
235 min: f32,
236 max: f32,
237 step: f32,
238 ) -> Self {
239 Self::new(
240 id,
241 label,
242 SettingType::FloatRange { min, max, step },
243 SettingValue::Float(current_value),
244 )
245 }
246
247 pub fn int_slider(
248 id: impl Into<String>,
249 label: impl Into<String>,
250 current_value: i32,
251 min: i32,
252 max: i32,
253 step: i32,
254 ) -> Self {
255 Self::new(
256 id,
257 label,
258 SettingType::IntRange { min, max, step },
259 SettingValue::Int(current_value),
260 )
261 }
262
263 pub fn selection(
264 id: impl Into<String>,
265 label: impl Into<String>,
266 options: Vec<String>,
267 current_index: usize,
268 ) -> Self {
269 Self::new(
270 id,
271 label,
272 SettingType::Selection { options },
273 SettingValue::Selection(current_index),
274 )
275 }
276
277 pub fn text(
278 id: impl Into<String>,
279 label: impl Into<String>,
280 current_text: String,
281 max_length: Option<usize>,
282 ) -> Self {
283 Self::new(
284 id,
285 label,
286 SettingType::Text { max_length },
287 SettingValue::String(current_text),
288 )
289 }
290
291 pub fn custom(
293 id: impl Into<String>,
294 label: impl Into<String>,
295 current_value: SettingValue,
296 setting_type: SettingType,
297 ) -> Self {
298 Self::new(id, label, setting_type, current_value)
299 }
300}
301
302#[derive(Event)]
304pub enum SettingsScreenEvent {
305 ValueChanged {
307 entity: Entity,
308 setting_id: String,
309 #[cfg(feature = "settings")]
310 value: SettingValue,
311 #[cfg(not(feature = "settings"))]
312 value: ScreenSettingValue,
313 },
314 Dismissed { entity: Entity },
316 Navigate { direction: NavigationDirection },
318}
319
320#[derive(Debug, Clone)]
322pub enum NavigationDirection {
323 Up,
324 Down,
325 Left,
326 Right,
327 Select,
328}
329
330impl SettingsSection {
332 #[cfg(feature = "settings")]
334 pub fn audio_section() -> Self {
335 Self::new("Audio Settings")
336 .add_setting(ScreenSettingsItem::slider(
337 "master_volume",
338 "Master Volume",
339 1.0,
340 0.0,
341 1.0,
342 0.1,
343 ))
344 .add_setting(ScreenSettingsItem::slider(
345 "music_volume",
346 "Music Volume",
347 0.8,
348 0.0,
349 1.0,
350 0.1,
351 ))
352 .add_setting(ScreenSettingsItem::slider(
353 "sfx_volume",
354 "Sound Effects",
355 1.0,
356 0.0,
357 1.0,
358 0.1,
359 ))
360 .add_setting(ScreenSettingsItem::toggle(
361 "audio_enabled",
362 "Enable Audio",
363 true,
364 ))
365 }
366
367 #[cfg(not(feature = "settings"))]
368 pub fn audio_section() -> Self {
369 Self::new("Audio Settings")
370 .add_setting(ScreenSettingsItem::new(
371 "master_volume",
372 "Master Volume",
373 ScreenOnlySettingType::FloatRange {
374 min: 0.0,
375 max: 1.0,
376 step: 0.1,
377 },
378 ScreenSettingValue::Float(1.0),
379 ))
380 .add_setting(ScreenSettingsItem::new(
381 "audio_enabled",
382 "Enable Audio",
383 ScreenOnlySettingType::Toggle,
384 ScreenSettingValue::Bool(true),
385 ))
386 }
387
388 #[cfg(feature = "input")]
389 pub fn input_section() -> Self {
390 Self::new("Input Settings").add_setting(ScreenSettingsItem::custom(
391 "configure_players",
392 "Configure Players",
393 #[cfg(feature = "settings")]
394 SettingValue::String("Configure".to_string()),
395 #[cfg(not(feature = "settings"))]
396 ScreenSettingValue::String("Configure".to_string()),
397 #[cfg(feature = "settings")]
398 SettingType::Custom {
399 validator: |_| true,
400 display_fn: |_| "Configure Input Devices →".to_string(),
401 },
402 #[cfg(not(feature = "settings"))]
403 ScreenOnlySettingType::Custom {
404 display_fn: |_| "Configure Input Devices →".to_string(),
405 },
406 ))
407 }
408
409 #[cfg(feature = "settings")]
410 pub fn graphics_section() -> Self {
411 Self::new("Graphics Settings")
412 .add_setting(ScreenSettingsItem::selection(
413 "resolution",
414 "Resolution",
415 vec![
416 "1920x1080".to_string(),
417 "1680x1050".to_string(),
418 "1440x900".to_string(),
419 "1280x720".to_string(),
420 ],
421 0,
422 ))
423 .add_setting(ScreenSettingsItem::toggle(
424 "fullscreen",
425 "Fullscreen",
426 false,
427 ))
428 .add_setting(ScreenSettingsItem::toggle("vsync", "V-Sync", true))
429 }
430
431 #[cfg(feature = "settings")]
432 pub fn gameplay_section() -> Self {
433 Self::new("Gameplay Settings")
434 .add_setting(ScreenSettingsItem::selection(
435 "difficulty",
436 "Difficulty",
437 vec![
438 "Easy".to_string(),
439 "Normal".to_string(),
440 "Hard".to_string(),
441 "Expert".to_string(),
442 ],
443 1,
444 ))
445 .add_setting(ScreenSettingsItem::toggle("auto_save", "Auto Save", true))
446 }
447
448 #[cfg(feature = "settings")]
450 pub fn from_settings(title: impl Into<String>, settings: &[&Setting]) -> Self {
451 let mut section = Self::new(title);
452 for setting in settings {
453 section = section.add_setting(ScreenSettingsItem::from_setting(setting));
454 }
455 section
456 }
457}
458
459impl SettingsScreenConfig {
461 #[cfg(all(feature = "settings", feature = "input"))]
463 pub fn game_settings_with_input(title: impl Into<String>) -> Self {
464 Self::new(title)
465 .add_section(SettingsSection::audio_section())
466 .add_section(SettingsSection::graphics_section())
467 .add_section(SettingsSection::input_section())
468 .add_section(SettingsSection::gameplay_section())
469 }
470
471 #[cfg(feature = "settings")]
473 pub fn game_settings(title: impl Into<String>) -> Self {
474 Self::new(title)
475 .add_section(SettingsSection::audio_section())
476 .add_section(SettingsSection::graphics_section())
477 .add_section(SettingsSection::gameplay_section())
478 }
479
480 pub fn audio_only(title: impl Into<String>) -> Self {
482 Self::new(title).add_section(SettingsSection::audio_section())
483 }
484
485 #[cfg(feature = "settings")]
487 pub fn educational_game(title: impl Into<String>) -> Self {
488 let learning_section = SettingsSection::new("Learning Settings")
489 .add_setting(ScreenSettingsItem::toggle(
490 "hints_enabled",
491 "Show Hints",
492 true,
493 ))
494 .add_setting(ScreenSettingsItem::selection(
495 "feedback_level",
496 "Feedback Level",
497 vec![
498 "Minimal".to_string(),
499 "Standard".to_string(),
500 "Detailed".to_string(),
501 ],
502 1,
503 ))
504 .add_setting(ScreenSettingsItem::toggle(
505 "progress_tracking",
506 "Track Progress",
507 true,
508 ));
509
510 Self::new(title)
511 .add_section(SettingsSection::audio_section())
512 .add_section(learning_section)
513 .add_section(SettingsSection::gameplay_section())
514 }
515
516 #[cfg(feature = "settings")]
518 pub fn from_component_settings(title: impl Into<String>, settings: &[&Setting]) -> Self {
519 use std::collections::HashMap;
520
521 let mut categories: HashMap<String, Vec<&Setting>> = HashMap::new();
522
523 for setting in settings {
524 let category = setting
525 .category
526 .clone()
527 .unwrap_or_else(|| "General".to_string());
528 categories.entry(category).or_default().push(setting);
529 }
530
531 let mut config = Self::new(title);
532 for (category_name, category_settings) in categories {
533 let section = SettingsSection::from_settings(category_name, &category_settings);
534 config = config.add_section(section);
535 }
536
537 config
538 }
539}