1#[cfg(feature = "settings")]
2use crate::settings::{Setting, SettingChanged, SettingType, SettingValue};
3use crate::{
4 theme::KonnektorenTheme,
5 ui::{
6 responsive::{ResponsiveFontSize, ResponsiveInfo, ResponsiveSpacing},
7 widgets::{ResponsiveText, ThemedButton},
8 },
9};
10use bevy::prelude::*;
11use bevy_egui::{
12 egui::{self, Widget},
13 EguiContexts,
14};
15use std::collections::HashMap;
16
17#[derive(Event)]
19pub enum ComponentSettingsEvent {
20 Dismissed { entity: Entity },
22}
23
24#[derive(Component)]
26pub struct ActiveComponentSettings {
27 pub title: String,
28 pub allow_dismissal: bool,
29 pub back_button_text: String,
30 pub navigation_state: ComponentSettingsNavigationState,
31}
32
33#[derive(Clone)]
35pub struct ComponentSettingsNavigationState {
36 pub current_index: usize,
37 pub max_index: usize,
38 pub enabled: bool,
39}
40
41impl Default for ComponentSettingsNavigationState {
42 fn default() -> Self {
43 Self {
44 current_index: 0,
45 max_index: 0,
46 enabled: true,
47 }
48 }
49}
50
51#[derive(Component)]
53pub struct PendingSettingUpdate {
54 new_value: SettingValue,
55}
56
57#[cfg(feature = "settings")]
58pub fn process_pending_setting_updates(
59 mut settings_query: Query<(Entity, &mut Setting, &PendingSettingUpdate)>,
60 mut commands: Commands,
61) {
62 for (entity, mut setting, update) in settings_query.iter_mut() {
63 let old_value = setting.value.clone();
64 setting.value = update.new_value.clone();
65
66 commands
67 .entity(entity)
68 .insert(SettingChanged { old_value })
69 .remove::<PendingSettingUpdate>();
70 }
71}
72
73#[cfg(feature = "settings")]
74pub fn check_component_settings(
75 mut commands: Commands,
76 settings_query: Query<&Setting>,
77 existing_config: Query<Entity, With<ActiveComponentSettings>>,
78) {
79 if settings_query.is_empty() || !existing_config.is_empty() {
80 return;
81 }
82
83 let max_index = settings_query
84 .iter()
85 .filter_map(|setting| setting.tab_index)
86 .max()
87 .unwrap_or(0)
88 + 1;
89
90 commands.spawn((
91 Name::new("Component-Based Settings Screen"),
92 ActiveComponentSettings {
93 title: "Settings".to_string(),
94 allow_dismissal: true,
95 back_button_text: "Back".to_string(),
96 navigation_state: ComponentSettingsNavigationState {
97 max_index,
98 ..Default::default()
99 },
100 },
101 ));
102}
103
104#[cfg(feature = "settings")]
105#[allow(clippy::too_many_arguments)]
106pub fn render_component_settings_ui(
107 mut contexts: EguiContexts,
108 theme: Res<KonnektorenTheme>,
109 responsive: Res<ResponsiveInfo>,
110 mut config_query: Query<&mut ActiveComponentSettings>,
111 settings_query: Query<(Entity, &Setting)>,
112 mut settings_events: EventWriter<ComponentSettingsEvent>,
113 mut commands: Commands,
114 input: Res<ButtonInput<KeyCode>>,
115) {
116 if config_query.is_empty() {
117 return;
118 }
119
120 let ctx = contexts.ctx_mut();
121
122 if let Ok(mut config) = config_query.single_mut() {
123 let should_dismiss = config.allow_dismissal && input.just_pressed(KeyCode::Escape);
124 if should_dismiss {
125 settings_events.write(ComponentSettingsEvent::Dismissed {
126 entity: Entity::PLACEHOLDER,
127 });
128 return;
129 }
130
131 egui::CentralPanel::default()
132 .frame(egui::Frame::NONE.fill(theme.base_100))
133 .show(ctx, |ui| {
134 render_component_settings_content(
135 ui,
136 &mut config,
137 &theme,
138 &responsive,
139 &settings_query,
140 &mut settings_events,
141 &mut commands,
142 );
143 });
144 }
145}
146
147#[cfg(feature = "settings")]
148fn render_component_settings_content(
149 ui: &mut egui::Ui,
150 config: &mut ActiveComponentSettings,
151 theme: &KonnektorenTheme,
152 responsive: &ResponsiveInfo,
153 settings_query: &Query<(Entity, &Setting)>,
154 settings_events: &mut EventWriter<ComponentSettingsEvent>,
155 commands: &mut Commands,
156) {
157 ui.vertical_centered(|ui| {
158 let max_width = if responsive.is_mobile() {
159 ui.available_width() * 0.95
160 } else {
161 800.0_f32.min(ui.available_width() * 0.9)
162 };
163
164 ui.set_max_width(max_width);
165
166 ui.add_space(responsive.spacing(ResponsiveSpacing::Large));
167 ResponsiveText::new(&config.title, ResponsiveFontSize::Header, theme.primary)
168 .responsive(responsive)
169 .strong()
170 .ui(ui);
171
172 ui.add_space(responsive.spacing(ResponsiveSpacing::Large));
173
174 let mut categories: HashMap<String, Vec<(Entity, &Setting)>> = HashMap::new();
175
176 for (entity, setting) in settings_query.iter() {
177 let category = setting
178 .category
179 .clone()
180 .unwrap_or_else(|| "General".to_string());
181 categories
182 .entry(category)
183 .or_default()
184 .push((entity, setting));
185 }
186
187 for settings_list in categories.values_mut() {
188 settings_list.sort_by_key(|(_, setting)| setting.tab_index.unwrap_or(usize::MAX));
189 }
190
191 let scroll_height = ui.available_height() - 80.0;
192 egui::ScrollArea::vertical()
193 .max_height(scroll_height)
194 .auto_shrink([false; 2])
195 .show(ui, |ui| {
196 if responsive.is_mobile() {
197 render_mobile_component_layout(
198 ui,
199 theme,
200 responsive,
201 &categories,
202 &config.navigation_state,
203 commands,
204 );
205 } else {
206 render_desktop_component_layout(
207 ui,
208 theme,
209 &categories,
210 &config.navigation_state,
211 commands,
212 );
213 }
214 });
215
216 if config.allow_dismissal {
217 ui.add_space(responsive.spacing(ResponsiveSpacing::Large));
218 let back_button = ThemedButton::new(&config.back_button_text, theme)
219 .responsive(responsive)
220 .width(if responsive.is_mobile() { 200.0 } else { 150.0 });
221
222 if ui.add(back_button).clicked() {
223 settings_events.write(ComponentSettingsEvent::Dismissed {
224 entity: Entity::PLACEHOLDER,
225 });
226 }
227 }
228
229 ui.add_space(responsive.spacing(ResponsiveSpacing::Medium));
230 });
231}
232
233#[cfg(feature = "settings")]
234fn render_mobile_component_layout(
235 ui: &mut egui::Ui,
236 theme: &KonnektorenTheme,
237 responsive: &ResponsiveInfo,
238 categories: &HashMap<String, Vec<(Entity, &Setting)>>,
239 _nav_state: &ComponentSettingsNavigationState,
240 commands: &mut Commands,
241) {
242 let section_spacing = responsive.spacing(ResponsiveSpacing::Large);
243
244 for (category_name, settings) in categories.iter() {
245 ResponsiveText::new(category_name, ResponsiveFontSize::Large, theme.secondary)
246 .responsive(responsive)
247 .strong()
248 .ui(ui);
249
250 ui.add_space(responsive.spacing(ResponsiveSpacing::Medium));
251
252 for (entity, setting) in settings.iter() {
253 render_mobile_component_setting_item(ui, *entity, setting, theme, responsive, commands);
254 ui.add_space(responsive.spacing(ResponsiveSpacing::Medium));
255 }
256
257 ui.add_space(section_spacing);
258 ui.separator();
259 ui.add_space(section_spacing);
260 }
261}
262
263#[cfg(feature = "settings")]
264fn render_desktop_component_layout(
265 ui: &mut egui::Ui,
266 theme: &KonnektorenTheme,
267 categories: &HashMap<String, Vec<(Entity, &Setting)>>,
268 _nav_state: &ComponentSettingsNavigationState,
269 commands: &mut Commands,
270) {
271 for (category_name, settings) in categories.iter() {
272 ResponsiveText::new(category_name, ResponsiveFontSize::Large, theme.secondary)
273 .strong()
274 .ui(ui);
275
276 ui.add_space(10.0);
277
278 egui::Grid::new(format!("component_settings_grid_{}", category_name))
279 .num_columns(2)
280 .spacing([30.0, 15.0])
281 .show(ui, |ui| {
282 for (entity, setting) in settings.iter() {
283 ResponsiveText::new(
284 &setting.label,
285 ResponsiveFontSize::Medium,
286 theme.base_content,
287 )
288 .ui(ui);
289
290 render_desktop_component_setting_control(ui, *entity, setting, theme, commands);
291 ui.end_row();
292 }
293 });
294
295 ui.add_space(20.0);
296 }
297}
298
299#[cfg(feature = "settings")]
300fn render_mobile_component_setting_item(
301 ui: &mut egui::Ui,
302 entity: Entity,
303 setting: &Setting,
304 theme: &KonnektorenTheme,
305 responsive: &ResponsiveInfo,
306 commands: &mut Commands,
307) {
308 ui.vertical_centered(|ui| {
309 ResponsiveText::new(
310 &setting.label,
311 ResponsiveFontSize::Medium,
312 theme.base_content,
313 )
314 .responsive(responsive)
315 .strong()
316 .ui(ui);
317
318 if let Some(description) = &setting.description {
319 ui.add_space(responsive.spacing(ResponsiveSpacing::XSmall));
320 ResponsiveText::new(description, ResponsiveFontSize::Small, theme.accent)
321 .responsive(responsive)
322 .ui(ui);
323 }
324
325 ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
326
327 render_component_setting_control(ui, entity, setting, theme, responsive, commands);
328 });
329}
330
331#[cfg(feature = "settings")]
332fn render_desktop_component_setting_control(
333 ui: &mut egui::Ui,
334 entity: Entity,
335 setting: &Setting,
336 theme: &KonnektorenTheme,
337 commands: &mut Commands,
338) {
339 render_component_setting_control(
340 ui,
341 entity,
342 setting,
343 theme,
344 &ResponsiveInfo::default(),
345 commands,
346 );
347}
348
349#[cfg(feature = "settings")]
350fn render_component_setting_control(
351 ui: &mut egui::Ui,
352 entity: Entity,
353 setting: &Setting,
354 theme: &KonnektorenTheme,
355 responsive: &ResponsiveInfo,
356 commands: &mut Commands,
357) {
358 if !setting.enabled {
359 ui.add_enabled(false, egui::Label::new("Disabled"));
360 return;
361 }
362
363 match &setting.setting_type {
364 SettingType::Toggle => {
365 if let Some(value) = setting.value.as_bool() {
366 let button_text = if value { "ON" } else { "OFF" };
367 let button = ThemedButton::new(button_text, theme).responsive(responsive);
368
369 if ui.add(button).clicked() {
370 update_component_setting_value(entity, SettingValue::Bool(!value), commands);
371 }
372 }
373 }
374
375 SettingType::FloatRange { min, max, step } => {
376 if let Some(current_value) = setting.value.as_float() {
377 ui.horizontal(|ui| {
378 let dec_button = ThemedButton::new("-", theme)
379 .responsive(responsive)
380 .width(30.0);
381
382 if ui.add(dec_button).clicked() {
383 let new_value = (current_value - step).max(*min);
384 update_component_setting_value(
385 entity,
386 SettingValue::Float(new_value),
387 commands,
388 );
389 }
390
391 ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
392
393 ResponsiveText::new(
394 &format!("{:.1}", current_value),
395 ResponsiveFontSize::Medium,
396 theme.base_content,
397 )
398 .responsive(responsive)
399 .ui(ui);
400
401 ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
402
403 let inc_button = ThemedButton::new("+", theme)
404 .responsive(responsive)
405 .width(30.0);
406
407 if ui.add(inc_button).clicked() {
408 let new_value = (current_value + step).min(*max);
409 update_component_setting_value(
410 entity,
411 SettingValue::Float(new_value),
412 commands,
413 );
414 }
415 });
416 }
417 }
418
419 SettingType::IntRange { min, max, step } => {
420 if let Some(current_value) = setting.value.as_int() {
421 ui.horizontal(|ui| {
422 let dec_button = ThemedButton::new("-", theme)
423 .responsive(responsive)
424 .width(30.0);
425
426 if ui.add(dec_button).clicked() {
427 let new_value = (current_value - step).max(*min);
428 update_component_setting_value(
429 entity,
430 SettingValue::Int(new_value),
431 commands,
432 );
433 }
434
435 ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
436
437 ResponsiveText::new(
438 ¤t_value.to_string(),
439 ResponsiveFontSize::Medium,
440 theme.base_content,
441 )
442 .responsive(responsive)
443 .ui(ui);
444
445 ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
446
447 let inc_button = ThemedButton::new("+", theme)
448 .responsive(responsive)
449 .width(30.0);
450
451 if ui.add(inc_button).clicked() {
452 let new_value = (current_value + step).min(*max);
453 update_component_setting_value(
454 entity,
455 SettingValue::Int(new_value),
456 commands,
457 );
458 }
459 });
460 }
461 }
462
463 SettingType::Selection { options } => {
464 if let Some(current_index) = setting.value.as_selection() {
465 if responsive.is_mobile() {
466 ui.horizontal(|ui| {
467 let left_button = ThemedButton::new("◀", theme)
468 .responsive(responsive)
469 .width(40.0);
470
471 if ui.add(left_button).clicked() {
472 let new_index = if current_index > 0 {
473 current_index - 1
474 } else {
475 options.len() - 1
476 };
477 update_component_setting_value(
478 entity,
479 SettingValue::Selection(new_index),
480 commands,
481 );
482 }
483
484 ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
485
486 let binding = String::new();
487 let current_option = options.get(current_index).unwrap_or(&binding);
488 ResponsiveText::new(
489 current_option,
490 ResponsiveFontSize::Medium,
491 theme.primary,
492 )
493 .responsive(responsive)
494 .ui(ui);
495
496 ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
497
498 let right_button = ThemedButton::new("▶", theme)
499 .responsive(responsive)
500 .width(40.0);
501
502 if ui.add(right_button).clicked() {
503 let new_index = (current_index + 1) % options.len();
504 update_component_setting_value(
505 entity,
506 SettingValue::Selection(new_index),
507 commands,
508 );
509 }
510 });
511 } else {
512 ui.horizontal_wrapped(|ui| {
513 for (index, option) in options.iter().enumerate() {
514 let is_selected = index == current_index;
515 let mut button =
516 ThemedButton::new(option, theme).responsive(responsive);
517
518 if is_selected {
519 button = button.with_style(|btn| {
520 btn.fill(theme.primary)
521 .stroke(egui::Stroke::new(2.0, theme.primary))
522 });
523 }
524
525 if ui.add(button).clicked() && !is_selected {
526 update_component_setting_value(
527 entity,
528 SettingValue::Selection(index),
529 commands,
530 );
531 }
532 }
533 });
534 }
535 }
536 }
537
538 SettingType::Text { max_length: _ } => {
539 if let Some(current_text) = setting.value.as_string() {
540 let mut text = current_text.to_string();
541 if ui.text_edit_singleline(&mut text).changed() {
542 update_component_setting_value(entity, SettingValue::String(text), commands);
543 }
544 }
545 }
546
547 SettingType::Custom { display_fn, .. } => {
548 let display_text = display_fn(&setting.value);
549 ui.label(display_text);
550 }
551 }
552}
553
554#[cfg(feature = "settings")]
555fn update_component_setting_value(
556 entity: Entity,
557 new_value: SettingValue,
558 commands: &mut Commands,
559) {
560 commands
561 .entity(entity)
562 .insert(PendingSettingUpdate { new_value });
563}
564
565pub fn cleanup_component_settings(
566 mut commands: Commands,
567 mut settings_events: EventReader<ComponentSettingsEvent>,
568 config_query: Query<Entity, With<ActiveComponentSettings>>,
569) {
570 for _event in settings_events.read() {
571 for entity in config_query.iter() {
572 commands.entity(entity).despawn();
573 }
574 }
575}