konnektoren_bevy/screens/
about.rs

1use crate::{
2    theme::KonnektorenTheme,
3    ui::{
4        responsive::{ResponsiveFontSize, ResponsiveInfo, ResponsiveSpacing},
5        widgets::{ResponsiveText, ThemedButton},
6    },
7};
8use bevy::prelude::*;
9use bevy_egui::{
10    egui::{self, Color32, Widget},
11    EguiContextPass, EguiContexts,
12};
13use chrono::Utc;
14
15/// Plugin for reusable about screen functionality
16pub struct AboutPlugin;
17
18impl Plugin for AboutPlugin {
19    fn build(&self, app: &mut App) {
20        app.add_event::<AboutDismissed>()
21            .add_systems(Update, (check_about_config, handle_about_completion))
22            .add_systems(EguiContextPass, render_about_ui);
23    }
24}
25
26/// Configuration for the about screen
27#[derive(Component, Clone)]
28pub struct AboutConfig {
29    /// Main title of the application/game
30    pub app_title: String,
31    /// Subtitle describing the application
32    pub subtitle: String,
33    /// Version information (optional)
34    pub version: Option<String>,
35    /// Main description text
36    pub description: String,
37    /// Why choose us section content
38    pub why_choose_us: Option<String>,
39    /// List of features with titles and descriptions
40    pub features: Vec<(String, String)>,
41    /// List of technologies used
42    pub technologies: Vec<(String, String)>,
43    /// Beta/status message (optional)
44    pub status_message: Option<(String, Color32)>, // (message, color)
45    /// Website links
46    pub websites: Vec<WebsiteLink>,
47    /// Copyright holder name
48    pub copyright_holder: String,
49    /// Allow manual dismissal (back button/escape)
50    pub manual_dismissal: bool,
51    /// Custom extension widget renderer
52    pub extension_widget: Option<fn(&mut egui::Ui, &KonnektorenTheme, &ResponsiveInfo)>,
53    /// Additional custom sections
54    pub custom_sections: Vec<CustomSection>,
55    /// Button text for dismissal
56    pub dismiss_button_text: String,
57}
58
59/// Website link configuration
60#[derive(Clone)]
61pub struct WebsiteLink {
62    pub title: String,
63    pub description: String,
64    pub url: String,
65    pub icon: Option<String>, // Optional emoji or icon
66}
67
68/// Custom section for extending the about screen
69#[derive(Clone)]
70pub struct CustomSection {
71    pub title: String,
72    pub renderer: fn(&mut egui::Ui, &KonnektorenTheme, &ResponsiveInfo),
73}
74
75impl Default for AboutConfig {
76    fn default() -> Self {
77        Self {
78            app_title: "Konnektoren".to_string(),
79            subtitle: "Educational Platform".to_string(),
80            version: Some("Beta".to_string()),
81            description: "Welcome to Konnektoren, an innovative educational platform designed to make learning engaging and effective.".to_string(),
82            why_choose_us: Some("We offer interactive experiences designed for different proficiency levels, making learning both fun and effective.".to_string()),
83            features: vec![
84                ("🎮 Interactive Experience".to_string(), "Engaging activities that make learning enjoyable".to_string()),
85                ("📊 Progress Tracking".to_string(), "Monitor your improvement and see your strengths".to_string()),
86                ("🏆 Achievements".to_string(), "Earn rewards as you master new concepts".to_string()),
87                ("📱 Cross-Platform".to_string(), "Access on desktop or in your web browser".to_string()),
88            ],
89            technologies: vec![
90                ("Rust".to_string(), "Safe, fast, and modern programming language".to_string()),
91                ("Bevy".to_string(), "Data-driven game engine".to_string()),
92                ("egui".to_string(), "Immediate mode GUI library".to_string()),
93                ("WebAssembly".to_string(), "For web browser support".to_string()),
94            ],
95            status_message: Some(("Currently in Beta - Thank you for being part of our journey!".to_string(), egui::Color32::from_rgb(255, 193, 7))),
96            websites: vec![
97                WebsiteLink {
98                    title: "Konnektoren Web App".to_string(),
99                    description: "Visit our main web application".to_string(),
100                    url: "https://konnektoren.help".to_string(),
101                    icon: Some("🌐".to_string()),
102                },
103            ],
104            copyright_holder: "Konnektoren".to_string(),
105            manual_dismissal: true,
106            extension_widget: None,
107            custom_sections: vec![],
108            dismiss_button_text: "← Back".to_string(),
109        }
110    }
111}
112
113impl AboutConfig {
114    pub fn new(app_title: impl Into<String>) -> Self {
115        Self {
116            app_title: app_title.into(),
117            ..Default::default()
118        }
119    }
120
121    pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
122        self.subtitle = subtitle.into();
123        self
124    }
125
126    pub fn with_version(mut self, version: impl Into<String>) -> Self {
127        self.version = Some(version.into());
128        self
129    }
130
131    pub fn with_description(mut self, description: impl Into<String>) -> Self {
132        self.description = description.into();
133        self
134    }
135
136    pub fn with_why_choose_us(mut self, text: impl Into<String>) -> Self {
137        self.why_choose_us = Some(text.into());
138        self
139    }
140
141    pub fn with_features(mut self, features: Vec<(String, String)>) -> Self {
142        self.features = features;
143        self
144    }
145
146    pub fn add_feature(mut self, title: impl Into<String>, description: impl Into<String>) -> Self {
147        self.features.push((title.into(), description.into()));
148        self
149    }
150
151    pub fn with_technologies(mut self, technologies: Vec<(String, String)>) -> Self {
152        self.technologies = technologies;
153        self
154    }
155
156    pub fn add_technology(
157        mut self,
158        name: impl Into<String>,
159        description: impl Into<String>,
160    ) -> Self {
161        self.technologies.push((name.into(), description.into()));
162        self
163    }
164
165    pub fn with_status_message(mut self, message: impl Into<String>, color: egui::Color32) -> Self {
166        self.status_message = Some((message.into(), color));
167        self
168    }
169
170    pub fn with_websites(mut self, websites: Vec<WebsiteLink>) -> Self {
171        self.websites = websites;
172        self
173    }
174
175    pub fn add_website(
176        mut self,
177        title: impl Into<String>,
178        description: impl Into<String>,
179        url: impl Into<String>,
180    ) -> Self {
181        self.websites.push(WebsiteLink {
182            title: title.into(),
183            description: description.into(),
184            url: url.into(),
185            icon: Some("🌐".to_string()),
186        });
187        self
188    }
189
190    pub fn with_copyright_holder(mut self, holder: impl Into<String>) -> Self {
191        self.copyright_holder = holder.into();
192        self
193    }
194
195    pub fn with_extension_widget(
196        mut self,
197        widget: fn(&mut egui::Ui, &KonnektorenTheme, &ResponsiveInfo),
198    ) -> Self {
199        self.extension_widget = Some(widget);
200        self
201    }
202
203    pub fn add_custom_section(
204        mut self,
205        title: impl Into<String>,
206        renderer: fn(&mut egui::Ui, &KonnektorenTheme, &ResponsiveInfo),
207    ) -> Self {
208        self.custom_sections.push(CustomSection {
209            title: title.into(),
210            renderer,
211        });
212        self
213    }
214
215    pub fn with_dismiss_button_text(mut self, text: impl Into<String>) -> Self {
216        self.dismiss_button_text = text.into();
217        self
218    }
219
220    /// Create a default game-focused about config
221    pub fn for_game(title: impl Into<String>) -> Self {
222        Self {
223            app_title: title.into(),
224            subtitle: "Educational Game".to_string(),
225            description: "An engaging educational game designed to make learning fun and effective through interactive gameplay.".to_string(),
226            features: vec![
227                ("🎮 Interactive Gameplay".to_string(), "Fun challenges that test your knowledge".to_string()),
228                ("🗺️ Challenge Map".to_string(), "Visual progression system to track your learning journey".to_string()),
229                ("📊 Progress Tracking".to_string(), "Monitor your improvement and see your strengths".to_string()),
230                ("🏆 Achievements".to_string(), "Earn rewards as you master new concepts".to_string()),
231                ("📱 Cross-Platform".to_string(), "Play on desktop or in your web browser".to_string()),
232            ],
233            ..Default::default()
234        }
235    }
236}
237
238/// Component marking an active about screen
239#[derive(Component)]
240pub struct ActiveAbout {
241    config: AboutConfig,
242    navigation_state: NavigationState,
243}
244
245/// Navigation state for keyboard/gamepad support
246#[derive(Clone)]
247pub struct NavigationState {
248    pub current_index: usize,
249    pub max_index: usize,
250    pub enabled: bool,
251}
252
253impl Default for NavigationState {
254    fn default() -> Self {
255        Self {
256            current_index: 0,
257            max_index: 0,
258            enabled: true,
259        }
260    }
261}
262
263/// Event sent when about screen should be dismissed
264#[derive(Event)]
265pub struct AboutDismissed {
266    pub entity: Entity,
267}
268
269/// System to check for new about configurations and set them up
270#[allow(clippy::type_complexity)]
271fn check_about_config(
272    mut commands: Commands,
273    query: Query<(Entity, &AboutConfig), (Without<ActiveAbout>, Changed<AboutConfig>)>,
274    existing_about: Query<Entity, With<ActiveAbout>>,
275) {
276    for (entity, config) in query.iter() {
277        info!("Setting up about screen for entity {:?}", entity);
278
279        // Clean up any existing about screens first
280        for existing_entity in existing_about.iter() {
281            info!("Cleaning up existing about screen: {:?}", existing_entity);
282            commands.entity(existing_entity).remove::<ActiveAbout>();
283        }
284
285        // Calculate navigation indices
286        let mut nav_state = NavigationState {
287            max_index: config.websites.len(),
288            ..Default::default()
289        };
290        if config.manual_dismissal {
291            nav_state.max_index += 1;
292        }
293
294        commands.entity(entity).insert(ActiveAbout {
295            config: config.clone(),
296            navigation_state: nav_state,
297        });
298    }
299}
300
301/// System to render about UI
302fn render_about_ui(
303    mut contexts: EguiContexts,
304    theme: Res<KonnektorenTheme>,
305    responsive: Res<ResponsiveInfo>,
306    mut query: Query<(Entity, &mut ActiveAbout)>,
307    mut dismiss_events: EventWriter<AboutDismissed>,
308    input: Res<ButtonInput<KeyCode>>,
309) {
310    if query.is_empty() {
311        return;
312    }
313
314    let ctx = contexts.ctx_mut();
315
316    // Only render the first (most recent) about screen to avoid widget ID conflicts
317    if let Some((entity, mut about)) = query.iter_mut().next() {
318        // Check dismissal first with separate borrow
319        let should_dismiss = about.config.manual_dismissal && input.just_pressed(KeyCode::Escape);
320        if should_dismiss {
321            dismiss_events.write(AboutDismissed { entity });
322            return;
323        }
324
325        // Destructure to get separate borrows
326        let ActiveAbout {
327            config,
328            navigation_state,
329        } = &mut *about;
330
331        egui::CentralPanel::default()
332            .frame(egui::Frame::NONE.fill(theme.base_100))
333            .show(ctx, |ui| {
334                render_about_content(
335                    ui,
336                    config,
337                    navigation_state,
338                    &theme,
339                    &responsive,
340                    entity,
341                    &mut dismiss_events,
342                );
343            });
344    }
345}
346
347/// Render about screen content
348fn render_about_content(
349    ui: &mut egui::Ui,
350    config: &AboutConfig,
351    nav_state: &mut NavigationState,
352    theme: &KonnektorenTheme,
353    responsive: &ResponsiveInfo,
354    entity: Entity,
355    dismiss_events: &mut EventWriter<AboutDismissed>,
356) {
357    ui.vertical_centered(|ui| {
358        let max_width = if responsive.is_mobile() {
359            ui.available_width() * 0.95
360        } else {
361            800.0_f32.min(ui.available_width() * 0.9)
362        };
363
364        ui.set_max_width(max_width);
365
366        let top_spacing = responsive.spacing(ResponsiveSpacing::Large);
367        ui.add_space(top_spacing);
368
369        // Header section
370        render_header(ui, config, theme, responsive);
371
372        // Main scrollable content
373        let scroll_height = ui.available_height() - 80.0;
374        egui::ScrollArea::vertical()
375            .max_height(scroll_height)
376            .auto_shrink([false; 2])
377            .show(ui, |ui| {
378                render_content_sections(ui, config, nav_state, theme, responsive);
379            });
380
381        // Back button at bottom
382        if config.manual_dismissal {
383            ui.add_space(responsive.spacing(ResponsiveSpacing::Large));
384            render_dismiss_button(
385                ui,
386                config,
387                theme,
388                responsive,
389                nav_state,
390                entity,
391                dismiss_events,
392            );
393        }
394
395        ui.add_space(responsive.spacing(ResponsiveSpacing::Medium));
396    });
397}
398
399fn render_header(
400    ui: &mut egui::Ui,
401    config: &AboutConfig,
402    theme: &KonnektorenTheme,
403    responsive: &ResponsiveInfo,
404) {
405    ui.vertical_centered(|ui| {
406        // Title
407        ResponsiveText::new(
408            &format!("About {}", config.app_title),
409            ResponsiveFontSize::Title,
410            theme.primary,
411        )
412        .responsive(responsive)
413        .strong()
414        .ui(ui);
415
416        ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
417
418        // Version badge
419        if let Some(version) = &config.version {
420            let badge_frame = egui::Frame {
421                inner_margin: egui::Margin::symmetric(12, 6),
422                corner_radius: egui::CornerRadius::same(16),
423                fill: theme.secondary,
424                ..Default::default()
425            };
426
427            badge_frame.show(ui, |ui| {
428                ResponsiveText::new(version, ResponsiveFontSize::Small, theme.secondary_content)
429                    .responsive(responsive)
430                    .strong()
431                    .ui(ui);
432            });
433
434            ui.add_space(responsive.spacing(ResponsiveSpacing::Medium));
435        }
436
437        // Subtitle
438        ResponsiveText::new(
439            &config.subtitle,
440            ResponsiveFontSize::Large,
441            theme.base_content,
442        )
443        .responsive(responsive)
444        .ui(ui);
445
446        ui.add_space(responsive.spacing(ResponsiveSpacing::Large));
447    });
448}
449
450fn render_content_sections(
451    ui: &mut egui::Ui,
452    config: &AboutConfig,
453    nav_state: &NavigationState,
454    theme: &KonnektorenTheme,
455    responsive: &ResponsiveInfo,
456) {
457    let section_spacing = responsive.spacing(ResponsiveSpacing::Large);
458
459    // Main description
460    render_section(ui, theme, responsive, "About This Platform", |ui| {
461        ResponsiveText::new(
462            &config.description,
463            ResponsiveFontSize::Medium,
464            theme.base_content,
465        )
466        .responsive(responsive)
467        .ui(ui);
468    });
469
470    ui.add_space(section_spacing);
471
472    // Why choose us section
473    if let Some(why_choose_us) = &config.why_choose_us {
474        render_section(ui, theme, responsive, "Why Choose Us?", |ui| {
475            ResponsiveText::new(
476                why_choose_us,
477                ResponsiveFontSize::Medium,
478                theme.base_content,
479            )
480            .responsive(responsive)
481            .ui(ui);
482        });
483        ui.add_space(section_spacing);
484    }
485
486    // Features section
487    if !config.features.is_empty() {
488        render_section(ui, theme, responsive, "Features", |ui| {
489            for (title, description) in &config.features {
490                render_feature_item(ui, theme, responsive, title, description);
491            }
492        });
493        ui.add_space(section_spacing);
494    }
495
496    // Custom extension widget
497    if let Some(extension_widget) = config.extension_widget {
498        extension_widget(ui, theme, responsive);
499        ui.add_space(section_spacing);
500    }
501
502    // Custom sections
503    for custom_section in &config.custom_sections {
504        render_section(ui, theme, responsive, &custom_section.title, |ui| {
505            (custom_section.renderer)(ui, theme, responsive);
506        });
507        ui.add_space(section_spacing);
508    }
509
510    // Technology section
511    if !config.technologies.is_empty() {
512        render_section(ui, theme, responsive, "Built With", |ui| {
513            for (tech_name, description) in &config.technologies {
514                render_tech_item(ui, theme, responsive, tech_name, description);
515            }
516        });
517        ui.add_space(section_spacing);
518    }
519
520    // Status message section
521    if let Some((message, color)) = &config.status_message {
522        render_section(ui, theme, responsive, "Status", |ui| {
523            ResponsiveText::new(message, ResponsiveFontSize::Medium, *color)
524                .responsive(responsive)
525                .ui(ui);
526        });
527        ui.add_space(section_spacing * 1.5);
528    }
529
530    // Website links
531    if !config.websites.is_empty() {
532        ui.vertical_centered(|ui| {
533            ResponsiveText::new(
534                "Visit Our Websites",
535                ResponsiveFontSize::Large,
536                theme.secondary,
537            )
538            .responsive(responsive)
539            .strong()
540            .ui(ui);
541
542            ui.add_space(responsive.spacing(ResponsiveSpacing::Medium));
543
544            for (index, website) in config.websites.iter().enumerate() {
545                render_website_link(ui, theme, responsive, nav_state, website, index + 1);
546                ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
547            }
548        });
549        ui.add_space(section_spacing * 2.0);
550    }
551
552    // Copyright notice
553    ui.vertical_centered(|ui| {
554        let current_year = Utc::now().format("%Y").to_string();
555        ResponsiveText::new(
556            &format!(
557                "© {} {}. All rights reserved.",
558                current_year, config.copyright_holder
559            ),
560            ResponsiveFontSize::Small,
561            theme.accent,
562        )
563        .responsive(responsive)
564        .ui(ui);
565    });
566
567    ui.add_space(responsive.spacing(ResponsiveSpacing::XLarge));
568}
569
570fn render_section<F>(
571    ui: &mut egui::Ui,
572    theme: &KonnektorenTheme,
573    responsive: &ResponsiveInfo,
574    title: &str,
575    content: F,
576) where
577    F: FnOnce(&mut egui::Ui),
578{
579    let margin = if responsive.is_mobile() { 12 } else { 16 };
580    let frame = egui::Frame {
581        inner_margin: egui::Margin::same(margin),
582        corner_radius: egui::CornerRadius::same(8),
583        fill: theme.base_200,
584        stroke: egui::Stroke::new(1.0, theme.accent.linear_multiply(0.3)),
585        ..Default::default()
586    };
587
588    frame.show(ui, |ui| {
589        ResponsiveText::new(title, ResponsiveFontSize::Large, theme.primary)
590            .responsive(responsive)
591            .strong()
592            .ui(ui);
593
594        ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
595
596        content(ui);
597    });
598}
599
600fn render_feature_item(
601    ui: &mut egui::Ui,
602    theme: &KonnektorenTheme,
603    responsive: &ResponsiveInfo,
604    title: &str,
605    description: &str,
606) {
607    if responsive.is_mobile() {
608        ui.vertical(|ui| {
609            ResponsiveText::new(title, ResponsiveFontSize::Medium, theme.secondary)
610                .responsive(responsive)
611                .strong()
612                .ui(ui);
613            ResponsiveText::new(description, ResponsiveFontSize::Small, theme.base_content)
614                .responsive(responsive)
615                .ui(ui);
616        });
617    } else {
618        ui.horizontal(|ui| {
619            ResponsiveText::new(title, ResponsiveFontSize::Medium, theme.secondary)
620                .responsive(responsive)
621                .strong()
622                .ui(ui);
623            ui.label(" - ");
624            ResponsiveText::new(description, ResponsiveFontSize::Medium, theme.base_content)
625                .responsive(responsive)
626                .ui(ui);
627        });
628    }
629
630    ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
631}
632
633fn render_tech_item(
634    ui: &mut egui::Ui,
635    theme: &KonnektorenTheme,
636    responsive: &ResponsiveInfo,
637    tech_name: &str,
638    description: &str,
639) {
640    ui.horizontal(|ui| {
641        let tech_badge = egui::Frame {
642            inner_margin: egui::Margin::symmetric(8, 4),
643            corner_radius: egui::CornerRadius::same(4),
644            fill: theme.info,
645            ..Default::default()
646        };
647
648        tech_badge.show(ui, |ui| {
649            ResponsiveText::new(tech_name, ResponsiveFontSize::Small, theme.primary_content)
650                .responsive(responsive)
651                .strong()
652                .ui(ui);
653        });
654
655        ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
656
657        ResponsiveText::new(description, ResponsiveFontSize::Medium, theme.base_content)
658            .responsive(responsive)
659            .ui(ui);
660    });
661
662    ui.add_space(responsive.spacing(ResponsiveSpacing::XSmall));
663}
664
665fn render_website_link(
666    ui: &mut egui::Ui,
667    theme: &KonnektorenTheme,
668    responsive: &ResponsiveInfo,
669    nav_state: &NavigationState,
670    website: &WebsiteLink,
671    nav_index: usize,
672) {
673    let is_focused = nav_state.enabled && nav_state.current_index == nav_index;
674
675    let link_frame = egui::Frame {
676        inner_margin: egui::Margin::same(12),
677        corner_radius: egui::CornerRadius::same(8),
678        fill: if is_focused {
679            theme.base_300.linear_multiply(1.2)
680        } else {
681            theme.base_300
682        },
683        stroke: egui::Stroke::new(
684            if is_focused { 2.0 } else { 1.0 },
685            if is_focused {
686                theme.primary
687            } else {
688                theme.accent
689            },
690        ),
691        ..Default::default()
692    };
693
694    link_frame.show(ui, |ui| {
695        ui.vertical_centered(|ui| {
696            ResponsiveText::new(&website.title, ResponsiveFontSize::Medium, theme.primary)
697                .responsive(responsive)
698                .strong()
699                .ui(ui);
700
701            ResponsiveText::new(
702                &website.description,
703                ResponsiveFontSize::Small,
704                theme.base_content,
705            )
706            .responsive(responsive)
707            .ui(ui);
708
709            ui.add_space(responsive.spacing(ResponsiveSpacing::Small));
710
711            let button_text = if let Some(icon) = &website.icon {
712                format!("{} {}", icon, website.url)
713            } else {
714                website.url.clone()
715            };
716
717            let url_button = ThemedButton::new(&button_text, theme).responsive(responsive);
718
719            if ui.add(url_button).clicked() {
720                open_url(&website.url);
721            }
722        });
723    });
724}
725
726fn render_dismiss_button(
727    ui: &mut egui::Ui,
728    config: &AboutConfig,
729    theme: &KonnektorenTheme,
730    responsive: &ResponsiveInfo,
731    nav_state: &NavigationState,
732    entity: Entity,
733    dismiss_events: &mut EventWriter<AboutDismissed>,
734) {
735    ui.vertical_centered(|ui| {
736        let _is_focused = nav_state.enabled && nav_state.current_index == 0;
737
738        let back_button = ThemedButton::new(&config.dismiss_button_text, theme)
739            .responsive(responsive)
740            .width(if responsive.is_mobile() { 200.0 } else { 250.0 });
741
742        if ui.add(back_button).clicked() {
743            dismiss_events.write(AboutDismissed { entity });
744        }
745    });
746}
747
748/// System to handle about completion
749fn handle_about_completion(
750    mut commands: Commands,
751    mut dismiss_events: EventReader<AboutDismissed>,
752) {
753    for event in dismiss_events.read() {
754        info!("Dismissing about screen for entity {:?}", event.entity);
755        commands.entity(event.entity).remove::<ActiveAbout>();
756    }
757}
758
759/// Helper function to open URLs
760fn open_url(url: &str) {
761    #[cfg(target_arch = "wasm32")]
762    {
763        use wasm_bindgen::prelude::*;
764
765        #[wasm_bindgen]
766        extern "C" {
767            #[wasm_bindgen(js_namespace = ["window"], js_name = open)]
768            fn window_open(url: &str);
769        }
770
771        window_open(url);
772    }
773
774    #[cfg(not(target_arch = "wasm32"))]
775    {
776        if let Err(e) = std::process::Command::new("xdg-open")
777            .arg(url)
778            .spawn()
779            .or_else(|_| std::process::Command::new("open").arg(url).spawn())
780            .or_else(|_| std::process::Command::new("start").arg(url).spawn())
781        {
782            error!("Failed to open URL: {}", e);
783        }
784    }
785}
786
787/// Helper trait for easy about screen setup
788pub trait AboutScreenExt {
789    /// Add an about screen with the given configuration
790    fn spawn_about(&mut self, config: AboutConfig) -> Entity;
791
792    /// Add a simple about screen with just a title
793    fn spawn_simple_about(&mut self, title: impl Into<String>) -> Entity;
794
795    /// Add a game-focused about screen
796    fn spawn_game_about(&mut self, title: impl Into<String>) -> Entity;
797}
798
799impl AboutScreenExt for Commands<'_, '_> {
800    fn spawn_about(&mut self, config: AboutConfig) -> Entity {
801        self.spawn((Name::new("About Screen"), config)).id()
802    }
803
804    fn spawn_simple_about(&mut self, title: impl Into<String>) -> Entity {
805        self.spawn_about(AboutConfig::new(title))
806    }
807
808    fn spawn_game_about(&mut self, title: impl Into<String>) -> Entity {
809        self.spawn_about(AboutConfig::for_game(title))
810    }
811}