431 lines
14 KiB
Rust
431 lines
14 KiB
Rust
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<Box<dyn Demo>>,
|
|
}
|
|
|
|
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<Box<dyn Demo>>) -> Self {
|
|
Self { demos }
|
|
}
|
|
|
|
pub fn checkboxes(&mut self, ui: &mut Ui, open: &mut BTreeSet<String>) {
|
|
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<String>) {
|
|
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<String>, 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::<super::paint_bezier::PaintBezier>::default(),
|
|
Box::<super::code_editor::CodeEditor>::default(),
|
|
Box::<super::code_example::CodeExample>::default(),
|
|
Box::<super::dancing_strings::DancingStrings>::default(),
|
|
Box::<super::drag_and_drop::DragAndDropDemo>::default(),
|
|
Box::<super::extra_viewport::ExtraViewport>::default(),
|
|
Box::<super::font_book::FontBook>::default(),
|
|
Box::<super::frame_demo::FrameDemo>::default(),
|
|
Box::<super::highlighting::Highlighting>::default(),
|
|
Box::<super::interactive_container::InteractiveContainerDemo>::default(),
|
|
Box::<super::MiscDemoWindow>::default(),
|
|
Box::<super::modals::Modals>::default(),
|
|
Box::<super::multi_touch::MultiTouch>::default(),
|
|
Box::<super::painting::Painting>::default(),
|
|
Box::<super::panels::Panels>::default(),
|
|
Box::<super::popups::PopupsDemo>::default(),
|
|
Box::<super::scene::SceneDemo>::default(),
|
|
Box::<super::screenshot::Screenshot>::default(),
|
|
Box::<super::scrolling::Scrolling>::default(),
|
|
Box::<super::sliders::Sliders>::default(),
|
|
Box::<super::strip_demo::StripDemo>::default(),
|
|
Box::<super::table_demo::TableDemo>::default(),
|
|
Box::<super::text_edit::TextEditDemo>::default(),
|
|
Box::<super::text_layout::TextLayoutDemo>::default(),
|
|
Box::<super::tooltips::Tooltips>::default(),
|
|
Box::<super::undo_redo::UndoRedoDemo>::default(),
|
|
Box::<super::widget_gallery::WidgetGallery>::default(),
|
|
Box::<super::window_options::WindowOptions>::default(),
|
|
]),
|
|
tests: DemoGroup::new(vec![
|
|
Box::<super::tests::ClipboardTest>::default(),
|
|
Box::<super::tests::CursorTest>::default(),
|
|
Box::<super::tests::GridTest>::default(),
|
|
Box::<super::tests::IdTest>::default(),
|
|
Box::<super::tests::InputEventHistory>::default(),
|
|
Box::<super::tests::InputTest>::default(),
|
|
Box::<super::tests::LayoutTest>::default(),
|
|
Box::<super::tests::ManualLayoutTest>::default(),
|
|
Box::<super::tests::SvgTest>::default(),
|
|
Box::<super::tests::TessellationTest>::default(),
|
|
Box::<super::tests::WindowResizeTest>::default(),
|
|
]),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DemoGroups {
|
|
pub fn checkboxes(&mut self, ui: &mut Ui, open: &mut BTreeSet<String>) {
|
|
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<String>) {
|
|
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<String>,
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|