use std::collections::BTreeSet; use super::About; use crate::Demo; use crate::View as _; use crate::is_mobile; use egui::containers::menu; use egui::style::StyleModifier; use egui::{Modifiers, ScrollArea, Ui}; // ---------------------------------------------------------------------------- struct DemoGroup { demos: Vec>, } impl std::ops::Add for DemoGroup { type Output = Self; fn add(self, other: Self) -> Self { let mut demos = self.demos; demos.extend(other.demos); Self { demos } } } impl DemoGroup { pub fn new(demos: Vec>) -> Self { Self { demos } } pub fn checkboxes(&mut self, ui: &mut Ui, open: &mut BTreeSet) { let Self { demos } = self; for demo in demos { if demo.is_enabled(ui.ctx()) { let mut is_open = open.contains(demo.name()); ui.toggle_value(&mut is_open, demo.name()); set_open(open, demo.name(), is_open); } } } pub fn windows(&mut self, ui: &mut Ui, open: &mut BTreeSet) { let Self { demos } = self; for demo in demos { let mut is_open = open.contains(demo.name()); demo.show(ui, &mut is_open); set_open(open, demo.name(), is_open); } } pub fn logic(&mut self, ctx: &egui::Context) { for demo in &mut self.demos { demo.logic(ctx); } } } fn set_open(open: &mut BTreeSet, key: &'static str, is_open: bool) { if is_open { if !open.contains(key) { open.insert(key.to_owned()); } } else { open.remove(key); } } // ---------------------------------------------------------------------------- pub struct DemoGroups { about: About, demos: DemoGroup, tests: DemoGroup, } impl Default for DemoGroups { fn default() -> Self { Self { about: About::default(), demos: DemoGroup::new(vec![ Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), ]), tests: DemoGroup::new(vec![ Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), Box::::default(), ]), } } } impl DemoGroups { pub fn about_egui_checkbox(&mut self, ui: &mut Ui, open: &mut BTreeSet) { let Self { about, .. } = self; let mut is_open = open.contains(about.name()); ui.toggle_value(&mut is_open, about.name()); set_open(open, about.name(), is_open); } pub fn checkboxes(&mut self, ui: &mut Ui, open: &mut BTreeSet) { let Self { about: _, demos, tests, } = self; ui.vertical_centered(|ui| { ui.strong("Demos"); }); demos.checkboxes(ui, open); ui.separator(); ui.vertical_centered(|ui| { ui.strong("Tests"); }); tests.checkboxes(ui, open); } pub fn windows(&mut self, ui: &mut Ui, open: &mut BTreeSet) { let Self { about, demos, tests, } = self; { let mut is_open = open.contains(about.name()); about.show(ui, &mut is_open); set_open(open, about.name(), is_open); } demos.windows(ui, open); tests.windows(ui, open); } pub fn logic(&mut self, ctx: &egui::Context) { self.demos.logic(ctx); self.tests.logic(ctx); } } // ---------------------------------------------------------------------------- /// A menu bar in which you can select different demo windows to show. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct DemoWindows { #[cfg_attr(feature = "serde", serde(skip))] groups: DemoGroups, open: BTreeSet, } impl Default for DemoWindows { fn default() -> Self { let mut open = BTreeSet::new(); // Explains egui very well set_open(&mut open, About::default().name(), true); // Explains egui very well set_open( &mut open, super::code_example::CodeExample::default().name(), true, ); // Shows off the features set_open( &mut open, super::widget_gallery::WidgetGallery::default().name(), true, ); Self { groups: Default::default(), open, } } } impl DemoWindows { /// Show the app ui (menu bar and windows). pub fn ui(&mut self, ui: &mut egui::Ui) { if is_mobile(ui.ctx()) { self.mobile_ui(ui); } else { self.desktop_ui(ui); } } /// Run background logic for all demos. /// /// Called every frame, even when hidden, so demos can keep working in the background. pub fn logic(&mut self, ctx: &egui::Context) { self.groups.logic(ctx); } fn about_is_open(&self) -> bool { self.open.contains(About::default().name()) } fn mobile_ui(&mut self, ui: &mut egui::Ui) { if self.about_is_open() { let mut close = false; egui::ScrollArea::vertical() .auto_shrink(false) .show(ui, |ui| { self.groups.about.ui(ui); ui.add_space(12.0); ui.vertical_centered_justified(|ui| { if ui .button(egui::RichText::new("Continue to the demo!").size(20.0)) .clicked() { close = true; } }); }); if close { set_open(&mut self.open, About::default().name(), false); } } else { self.mobile_top_bar(ui); self.groups.windows(ui, &mut self.open); } } fn mobile_top_bar(&mut self, ui: &mut egui::Ui) { egui::Panel::top("menu_bar").show(ui, |ui| { menu::MenuBar::new() .config(menu::MenuConfig::new().style(StyleModifier::default())) .ui(ui, |ui| { let font_size = 16.5; ui.menu_button(egui::RichText::new("⏷ demos").size(font_size), |ui| { self.demo_list_ui(ui); }); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { use egui::special_emojis::GITHUB; ui.hyperlink_to( egui::RichText::new("🦋").size(font_size), "https://bsky.app/profile/ernerfeldt.bsky.social", ); ui.hyperlink_to( egui::RichText::new(GITHUB).size(font_size), "https://github.com/emilk/egui", ); }); }); }); } fn desktop_ui(&mut self, ui: &mut egui::Ui) { egui::Panel::right("egui_demo_panel") .resizable(false) .default_size(160.0) .min_size(160.0) .show(ui, |ui| { ui.vertical_centered_justified(|ui| { ui.add_space(4.0); ui.add( egui::Image::new(egui::include_image!("../../data/egui-logo.svg")) .max_height(32.0) .tint(ui.visuals().strong_text_color()), ); ui.add_space(4.0); self.groups.about_egui_checkbox(ui, &mut self.open); }); ui.add_space(4.0); ui.separator(); self.demo_list_ui(ui); }); egui::Panel::top("menu_bar").show(ui, |ui| { menu::MenuBar::new().ui(ui, |ui| { file_menu_button(ui); }); }); self.groups.windows(ui, &mut self.open); } fn demo_list_ui(&mut self, ui: &mut egui::Ui) { ScrollArea::vertical().show(ui, |ui| { ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { self.groups.checkboxes(ui, &mut self.open); ui.separator(); if ui.button("Organize windows").clicked() { ui.memory_mut(|mem| mem.reset_areas()); } }); }); } } // ---------------------------------------------------------------------------- fn file_menu_button(ui: &mut Ui) { let organize_shortcut = egui::KeyboardShortcut::new(Modifiers::CTRL | Modifiers::SHIFT, egui::Key::O); let reset_shortcut = egui::KeyboardShortcut::new(Modifiers::CTRL | Modifiers::SHIFT, egui::Key::R); // NOTE: we must check the shortcuts OUTSIDE of the actual "File" menu, // or else they would only be checked if the "File" menu was actually open! if ui.input_mut(|i| i.consume_shortcut(&organize_shortcut)) { ui.memory_mut(|mem| mem.reset_areas()); } if ui.input_mut(|i| i.consume_shortcut(&reset_shortcut)) { ui.memory_mut(|mem| *mem = Default::default()); } ui.menu_button("File", |ui| { ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // On the web the browser controls the zoom #[cfg(not(target_arch = "wasm32"))] { egui::gui_zoom::zoom_menu_buttons(ui); ui.weak(format!( "Current zoom: {:.0}%", 100.0 * ui.ctx().zoom_factor() )) .on_hover_text("The UI zoom level, on top of the operating system's default value"); ui.separator(); } if ui .add( egui::Button::new("Organize Windows") .shortcut_text(ui.ctx().format_shortcut(&organize_shortcut)), ) .clicked() { ui.memory_mut(|mem| mem.reset_areas()); } if ui .add( egui::Button::new("Reset egui memory") .shortcut_text(ui.ctx().format_shortcut(&reset_shortcut)), ) .on_hover_text("Forget scroll, positions, sizes etc") .clicked() { ui.memory_mut(|mem| *mem = Default::default()); } }); } #[cfg(test)] mod tests { use crate::{Demo as _, demo::demo_app_windows::DemoGroups}; use egui::Vec2; use egui_kittest::{HarnessBuilder, OsThreshold, SnapshotOptions, SnapshotResults}; #[test] fn demos_should_match_snapshot() { let DemoGroups { demos, tests, about: _, } = DemoGroups::default(); let demos = demos + tests; let mut results = SnapshotResults::new(); for mut demo in demos.demos { // Widget Gallery needs to be customized (to set a specific date) and has its own test if demo.name() == crate::WidgetGallery::default().name() { continue; } let name = remove_leading_emoji(demo.name()); let mut harness = HarnessBuilder::default() .with_size(Vec2::splat(2048.0)) .build_ui(|ui| { egui_extras::install_image_loaders(ui); demo.show(ui, &mut true); }); // Resize to fit every window, plus some margin: harness.set_size(harness.ctx.globally_used_rect().max.to_vec2() + Vec2::splat(16.0)); // Run the app for some more frames... harness.run_ok(); let mut options = SnapshotOptions::default(); if name == "Bézier Curve" { // The Bézier Curve demo needs a threshold of 2.1 to pass on linux: options = options.threshold(OsThreshold::new(0.0_f32).linux(2.1)); } results.add(harness.try_snapshot_options(format!("demos/{name}"), &options)); } } fn remove_leading_emoji(full_name: &str) -> &str { if let Some((start, name)) = full_name.split_once(' ') && start.len() <= 4 && start.bytes().next().is_some_and(|byte| byte >= 128) { return name; } full_name } }