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::{Context, 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, ctx: &Context, open: &mut BTreeSet) { let Self { demos } = self; for demo in demos { let mut is_open = open.contains(demo.name()); demo.show(ctx, &mut is_open); set_open(open, demo.name(), is_open); } } } 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 checkboxes(&mut self, ui: &mut Ui, open: &mut BTreeSet) { let Self { about, demos, tests, } = self; { let mut is_open = open.contains(about.name()); ui.toggle_value(&mut is_open, about.name()); set_open(open, about.name(), is_open); } ui.separator(); demos.checkboxes(ui, open); ui.separator(); tests.checkboxes(ui, open); } pub fn windows(&mut self, ctx: &Context, open: &mut BTreeSet) { let Self { about, demos, tests, } = self; { let mut is_open = open.contains(about.name()); about.show(ctx, &mut is_open); set_open(open, about.name(), is_open); } demos.windows(ctx, open); tests.windows(ctx, open); } } // ---------------------------------------------------------------------------- /// 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, ctx: &Context) { if is_mobile(ctx) { self.mobile_ui(ctx); } else { self.desktop_ui(ctx); } } fn about_is_open(&self) -> bool { self.open.contains(About::default().name()) } fn mobile_ui(&mut self, ctx: &Context) { if self.about_is_open() { let mut close = false; egui::CentralPanel::default().show(ctx, |ui| { 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(ctx); self.groups.windows(ctx, &mut self.open); } } fn mobile_top_bar(&mut self, ctx: &Context) { egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { menu::Bar::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, ctx: &Context) { egui::SidePanel::right("egui_demo_panel") .resizable(false) .default_width(160.0) .min_width(160.0) .show(ctx, |ui| { ui.add_space(4.0); ui.vertical_centered(|ui| { ui.heading("✒ egui demos"); }); ui.separator(); use egui::special_emojis::GITHUB; ui.hyperlink_to( format!("{GITHUB} egui on GitHub"), "https://github.com/emilk/egui", ); ui.hyperlink_to( "@ernerfeldt.bsky.social", "https://bsky.app/profile/ernerfeldt.bsky.social", ); ui.separator(); self.demo_list_ui(ui); }); egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { menu::Bar::new().ui(ui, |ui| { file_menu_button(ui); }); }); self.groups.windows(ctx, &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.ctx().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.ctx().memory_mut(|mem| mem.reset_areas()); } if ui.input_mut(|i| i.consume_shortcut(&reset_shortcut)) { ui.ctx().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.ctx().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.ctx().memory_mut(|mem| *mem = Default::default()); } }); } #[cfg(test)] mod tests { use crate::{Demo as _, demo::demo_app_windows::DemoGroups}; use egui_kittest::kittest::{NodeT as _, Queryable as _}; use egui_kittest::{Harness, 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 = Harness::new(|ctx| { egui_extras::install_image_loaders(ctx); demo.show(ctx, &mut true); }); let window = harness.queryable_node().children().next().unwrap(); // TODO(lucasmerlin): Windows should probably have a label? //let window = harness.get_by_label(name); let size = window.rect().size(); harness.set_size(size); // Run the app for some more frames... harness.run_ok(); let mut options = SnapshotOptions::default(); // The Bézier Curve demo needs a threshold of 2.1 to pass on linux if name == "Bézier Curve" { options.threshold = 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(' ') { if start.len() <= 4 && start.bytes().next().is_some_and(|byte| byte >= 128) { return name; } } full_name } }