konnektoren_bevy/screens/settings/
component_ui.rs

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/// Events for component-based settings
18#[derive(Event)]
19pub enum ComponentSettingsEvent {
20    /// Settings screen dismissed
21    Dismissed { entity: Entity },
22}
23
24/// Component that marks an active component-based settings screen
25#[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/// Navigation state for component-based settings
34#[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/// Component to mark a setting that needs to be updated
52#[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                        &current_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}