From b3d10165077022b0463dbbddcf77d01987c33de3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 2 Jan 2021 01:01:01 +0100 Subject: [PATCH] [demo] Move backend into an optional panel of WrapApp --- egui_demo_lib/benches/benchmark.rs | 6 +- egui_demo_lib/src/apps/demo/app.rs | 308 +------------------- egui_demo_lib/src/apps/demo/demo_windows.rs | 4 +- egui_demo_lib/src/apps/http_app.rs | 10 +- egui_demo_lib/src/frame_history.rs | 126 ++++++++ egui_demo_lib/src/lib.rs | 3 +- egui_demo_lib/src/wrap_app.rs | 229 ++++++++++++++- 7 files changed, 354 insertions(+), 332 deletions(-) create mode 100644 egui_demo_lib/src/frame_history.rs diff --git a/egui_demo_lib/benches/benchmark.rs b/egui_demo_lib/benches/benchmark.rs index a00cec18..6f961ece 100644 --- a/egui_demo_lib/benches/benchmark.rs +++ b/egui_demo_lib/benches/benchmark.rs @@ -10,7 +10,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { c.bench_function("demo_windows_minimal", |b| { b.iter(|| { ctx.begin_frame(raw_input.clone()); - demo_windows.ui(&ctx, |_ui| {}); + demo_windows.ui(&ctx); ctx.end_frame() }) }); @@ -24,7 +24,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { c.bench_function("demo_windows_full", |b| { b.iter(|| { ctx.begin_frame(raw_input.clone()); - demo_windows.ui(&ctx, |_ui| {}); + demo_windows.ui(&ctx); ctx.end_frame() }) }); @@ -35,7 +35,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { ctx.memory().all_collpasing_are_open = true; // expand the demo window with everything let mut demo_windows = egui_demo_lib::DemoWindows::default(); ctx.begin_frame(raw_input.clone()); - demo_windows.ui(&ctx, |_ui| {}); + demo_windows.ui(&ctx); let (_, paint_commands) = ctx.end_frame(); c.bench_function("tessellate", |b| { diff --git a/egui_demo_lib/src/apps/demo/app.rs b/egui_demo_lib/src/apps/demo/app.rs index 10dd55db..69d1f764 100644 --- a/egui_demo_lib/src/apps/demo/app.rs +++ b/egui_demo_lib/src/apps/demo/app.rs @@ -1,174 +1,3 @@ -use egui::{util::History, *}; - -// ---------------------------------------------------------------------------- - -/// How often we repaint the demo app by default -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum RunMode { - /// This is the default for the demo. - /// - /// If this is selected, Egui is only updated if are input events - /// (like mouse movements) or there are some animations in the GUI. - /// - /// Reactive mode saves CPU. - /// - /// The downside is that the UI can become out-of-date if something it is supposed to monitor changes. - /// For instance, a GUI for a thermostat need to repaint each time the temperature changes. - /// To ensure the UI is up to date you need to call `egui::Context::request_repaint()` each - /// time such an event happens. You can also chose to call `request_repaint()` once every second - /// or after every single frame - this is called `Continuous` mode, - /// and for games and interactive tools that need repainting every frame anyway, this should be the default. - Reactive, - - /// This will call `egui::Context::request_repaint()` at the end of each frame - /// to request the backend to repaint as soon as possible. - /// - /// On most platforms this will mean that Egui will run at the display refresh rate of e.g. 60 Hz. - /// - /// For this demo it is not any reason to do so except to - /// demonstrate how quickly Egui runs. - /// - /// For games or other interactive apps, this is probably what you want to do. - /// It will guarantee that Egui is always up-to-date. - Continuous, -} - -/// Default for demo is Reactive since -/// 1) We want to use minimal CPU -/// 2) There are no external events that could invalidate the UI -/// so there are no events to miss. -impl Default for RunMode { - fn default() -> Self { - RunMode::Reactive - } -} - -// ---------------------------------------------------------------------------- - -struct FrameHistory { - frame_times: History, -} - -impl Default for FrameHistory { - fn default() -> Self { - let max_age: f64 = 1.0; - Self { - frame_times: History::from_max_len_age((max_age * 300.0).round() as usize, max_age), - } - } -} - -impl FrameHistory { - // Called first - pub fn on_new_frame(&mut self, now: f64, previus_frame_time: Option) { - let previus_frame_time = previus_frame_time.unwrap_or_default(); - if let Some(latest) = self.frame_times.latest_mut() { - *latest = previus_frame_time; // rewrite history now that we know - } - self.frame_times.add(now, previus_frame_time); // projected - } - - fn mean_frame_time(&self) -> f32 { - self.frame_times.average().unwrap_or_default() - } - - fn fps(&self) -> f32 { - 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() - } - - fn ui(&mut self, ui: &mut Ui) { - ui.label(format!( - "Total frames painted (including this one): {}", - self.frame_times.total_count() - )); - - ui.label(format!( - "Mean CPU usage per frame: {:.2} ms / frame", - 1e3 * self.mean_frame_time() - )) - .on_hover_text( - "Includes Egui layout and tessellation time.\n\ - Does not include GPU usage, nor overhead for sending data to GPU.", - ); - egui::warn_if_debug_build(ui); - - egui::CollapsingHeader::new("📊 CPU usage history") - .default_open(false) - .show(ui, |ui| { - self.graph(ui); - }); - } - - fn graph(&mut self, ui: &mut Ui) -> Response { - let graph_top_cpu_usage = 0.010; - ui.label("Egui CPU usage history"); - - let history = &self.frame_times; - - // TODO: we should not use `slider_width` as default graph width. - let height = ui.style().spacing.slider_width; - let size = vec2(ui.available_size_before_wrap_finite().x, height); - let response = ui.allocate_response(size, Sense::hover()); - let rect = response.rect; - let style = ui.style().noninteractive(); - - let mut cmds = vec![PaintCmd::Rect { - rect, - corner_radius: style.corner_radius, - fill: ui.style().visuals.dark_bg_color, - stroke: ui.style().noninteractive().bg_stroke, - }]; - - let rect = rect.shrink(4.0); - let line_stroke = Stroke::new(1.0, Srgba::additive_luminance(128)); - - if let Some(mouse_pos) = ui.input().mouse.pos { - if rect.contains(mouse_pos) { - let y = mouse_pos.y; - cmds.push(PaintCmd::line_segment( - [pos2(rect.left(), y), pos2(rect.right(), y)], - line_stroke, - )); - let cpu_usage = remap(y, rect.bottom_up_range(), 0.0..=graph_top_cpu_usage); - let text = format!("{:.1} ms", 1e3 * cpu_usage); - cmds.push(PaintCmd::text( - ui.fonts(), - pos2(rect.left(), y), - align::LEFT_BOTTOM, - text, - TextStyle::Monospace, - color::WHITE, - )); - } - } - - let circle_color = Srgba::additive_luminance(196); - let radius = 2.0; - let right_side_time = ui.input().time; // Time at right side of screen - - for (time, cpu_usage) in history.iter() { - let age = (right_side_time - time) as f32; - let x = remap(age, history.max_age()..=0.0, rect.x_range()); - let y = remap_clamp(cpu_usage, 0.0..=graph_top_cpu_usage, rect.bottom_up_range()); - - cmds.push(PaintCmd::line_segment( - [pos2(x, rect.bottom()), pos2(x, y)], - line_stroke, - )); - - if cpu_usage < graph_top_cpu_usage { - cmds.push(PaintCmd::circle_filled(pos2(x, y), radius, circle_color)); - } - } - - ui.painter().extend(cmds); - - response - } -} - -// ---------------------------------------------------------------------------- - /// Demonstrates how to make an app using Egui. /// /// Implements `epi::App` so it can be used with @@ -177,107 +6,6 @@ impl FrameHistory { #[serde(default)] pub struct DemoApp { demo_windows: super::DemoWindows, - - backend_window_open: bool, - - #[serde(skip)] // go back to `Reactive` mode each time we start - run_mode: RunMode, - - /// current slider value for current gui scale (backend demo only) - pixels_per_point: Option, - - #[serde(skip)] - frame_history: FrameHistory, -} - -impl DemoApp { - fn backend_ui(&mut self, ui: &mut Ui, frame: &mut epi::Frame<'_>) { - let is_web = frame.is_web(); - - if is_web { - ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."); - ui.label( - "Everything you see is rendered as textured triangles. There is no DOM. There are no HTML elements. \ - This is not JavaScript. This is Rust, running at 60 FPS. This is the web page, reinvented with game tech."); - ui.label("This is also work in progress, and not ready for production... yet :)"); - ui.horizontal(|ui| { - ui.label("Project home page:"); - ui.hyperlink("https://github.com/emilk/egui"); - }); - ui.separator(); - } - - self.run_mode_ui(ui); - - ui.separator(); - - self.frame_history.ui(ui); - - if !is_web { - // web browsers have their own way of zooming, which egui_web respects - ui.separator(); - if let Some(new_pixels_per_point) = self.pixels_per_point_ui(ui, frame.info()) { - frame.set_pixels_per_point(new_pixels_per_point); - } - } - - if !is_web { - ui.separator(); - if ui.button("Quit").clicked { - frame.quit(); - } - } - } - - fn pixels_per_point_ui(&mut self, ui: &mut Ui, info: &epi::IntegrationInfo) -> Option { - self.pixels_per_point = self - .pixels_per_point - .or(info.native_pixels_per_point) - .or_else(|| Some(ui.ctx().pixels_per_point())); - if let Some(pixels_per_point) = &mut self.pixels_per_point { - ui.add( - egui::Slider::f32(pixels_per_point, 0.5..=5.0) - .logarithmic(true) - .text("Scale (physical pixels per point)"), - ); - if let Some(native_pixels_per_point) = info.native_pixels_per_point { - if ui - .button(format!( - "Reset scale to native value ({:.1})", - native_pixels_per_point - )) - .clicked - { - *pixels_per_point = native_pixels_per_point; - } - } - if !ui.ctx().is_using_mouse() { - // We wait until mouse release to activate: - return Some(*pixels_per_point); - } - } - None - } - - fn run_mode_ui(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - let run_mode = &mut self.run_mode; - ui.label("Run mode:"); - ui.radio_value(run_mode, RunMode::Continuous, "Continuous") - .on_hover_text("Repaint everything each frame"); - ui.radio_value(run_mode, RunMode::Reactive, "Reactive") - .on_hover_text("Repaint when there are animations or input (e.g. mouse movement)"); - }); - - if self.run_mode == RunMode::Continuous { - ui.label(format!( - "Repainting the UI each frame. FPS: {:.1}", - self.frame_history.fps() - )); - } else { - ui.label("Only running UI code when there are animations or input"); - } - } } impl epi::App for DemoApp { @@ -293,39 +21,7 @@ impl epi::App for DemoApp { epi::set_value(storage, epi::APP_KEY, self); } - fn update(&mut self, ctx: &CtxRef, frame: &mut epi::Frame<'_>) { - self.frame_history - .on_new_frame(ctx.input().time, frame.info().cpu_usage); - - let mean_frame_time = self.frame_history.mean_frame_time(); - - let Self { - demo_windows, - backend_window_open, - .. - } = self; - - demo_windows.ui(ctx, |ui| { - ui.separator(); - ui.checkbox(backend_window_open, "💻 Backend"); - - ui.label(format!("{:.2} ms / frame", 1e3 * mean_frame_time)) - .on_hover_text("CPU usage."); - }); - - let mut backend_window_open = self.backend_window_open; - egui::Window::new("💻 Backend") - .min_width(360.0) - .scroll(false) - .open(&mut backend_window_open) - .show(ctx, |ui| { - self.backend_ui(ui, frame); - }); - self.backend_window_open = backend_window_open; - - if self.run_mode == RunMode::Continuous { - // Tell the backend to repaint as soon as possible - ctx.request_repaint(); - } + fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { + self.demo_windows.ui(ctx); } } diff --git a/egui_demo_lib/src/apps/demo/demo_windows.rs b/egui_demo_lib/src/apps/demo/demo_windows.rs index 4ec216d3..36f147cb 100644 --- a/egui_demo_lib/src/apps/demo/demo_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_windows.rs @@ -53,7 +53,7 @@ pub struct DemoWindows { impl DemoWindows { /// Show the app ui (menu bar and windows). /// `sidebar_ui` can be used to optionally show some things in the sidebar - pub fn ui(&mut self, ctx: &CtxRef, sidebar_ui: impl FnOnce(&mut Ui)) { + pub fn ui(&mut self, ctx: &CtxRef) { egui::SidePanel::left("side_panel", 190.0).show(ctx, |ui| { ui.heading("✒ Egui Demo"); @@ -80,8 +80,6 @@ impl DemoWindows { if ui.button("Organize windows").clicked { ui.ctx().memory().reset_areas(); } - - sidebar_ui(ui); }); }); diff --git a/egui_demo_lib/src/apps/http_app.rs b/egui_demo_lib/src/apps/http_app.rs index 4feea806..259de254 100644 --- a/egui_demo_lib/src/apps/http_app.rs +++ b/egui_demo_lib/src/apps/http_app.rs @@ -72,13 +72,13 @@ impl epi::App for HttpApp { } egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("Egui Fetch Example"); + ui.heading("HTTP Fetch Example"); ui.add(egui::github_link_file!( "https://github.com/emilk/egui/blob/master/", "(source code)" )); - if let Some(url) = ui_url(ui, &mut self.url) { + if let Some(url) = ui_url(ui, frame, &mut self.url) { let repaint_signal = frame.repaint_signal(); let (sender, receiver) = std::sync::mpsc::channel(); self.in_progress = Some(receiver); @@ -108,7 +108,7 @@ impl epi::App for HttpApp { } } -fn ui_url(ui: &mut egui::Ui, url: &mut String) -> Option { +fn ui_url(ui: &mut egui::Ui, frame: &mut epi::Frame<'_>, url: &mut String) -> Option { let mut trigger_fetch = false; ui.horizontal(|ui| { @@ -117,7 +117,9 @@ fn ui_url(ui: &mut egui::Ui, url: &mut String) -> Option { trigger_fetch |= ui.button("GET").clicked; }); - ui.label("HINT: paste the url of this page into the field above!"); + if frame.is_web() { + ui.label("HINT: paste the url of this page into the field above!"); + } ui.horizontal(|ui| { if ui.button("Source code for this example").clicked { diff --git a/egui_demo_lib/src/frame_history.rs b/egui_demo_lib/src/frame_history.rs new file mode 100644 index 00000000..1b002067 --- /dev/null +++ b/egui_demo_lib/src/frame_history.rs @@ -0,0 +1,126 @@ +use egui::util::History; + +pub struct FrameHistory { + frame_times: History, +} + +impl Default for FrameHistory { + fn default() -> Self { + let max_age: f64 = 1.0; + Self { + frame_times: History::from_max_len_age((max_age * 300.0).round() as usize, max_age), + } + } +} + +impl FrameHistory { + // Called first + pub fn on_new_frame(&mut self, now: f64, previus_frame_time: Option) { + let previus_frame_time = previus_frame_time.unwrap_or_default(); + if let Some(latest) = self.frame_times.latest_mut() { + *latest = previus_frame_time; // rewrite history now that we know + } + self.frame_times.add(now, previus_frame_time); // projected + } + + pub fn mean_frame_time(&self) -> f32 { + self.frame_times.average().unwrap_or_default() + } + + pub fn fps(&self) -> f32 { + 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() + } + + pub fn ui(&mut self, ui: &mut egui::Ui) { + ui.label(format!( + "Total frames painted: {}", + self.frame_times.total_count() + )) + .on_hover_text("Includes this frame."); + + ui.label(format!( + "Mean CPU usage: {:.2} ms / frame", + 1e3 * self.mean_frame_time() + )) + .on_hover_text( + "Includes Egui layout and tessellation time.\n\ + Does not include GPU usage, nor overhead for sending data to GPU.", + ); + egui::warn_if_debug_build(ui); + + egui::CollapsingHeader::new("📊 CPU usage history") + .default_open(false) + .show(ui, |ui| { + self.graph(ui); + }); + } + + fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response { + use egui::*; + + let graph_top_cpu_usage = 0.010; + ui.label("Egui CPU usage history"); + + let history = &self.frame_times; + + // TODO: we should not use `slider_width` as default graph width. + let height = ui.style().spacing.slider_width; + let size = vec2(ui.available_size_before_wrap_finite().x, height); + let response = ui.allocate_response(size, Sense::hover()); + let rect = response.rect; + let style = ui.style().noninteractive(); + + let mut cmds = vec![PaintCmd::Rect { + rect, + corner_radius: style.corner_radius, + fill: ui.style().visuals.dark_bg_color, + stroke: ui.style().noninteractive().bg_stroke, + }]; + + let rect = rect.shrink(4.0); + let line_stroke = Stroke::new(1.0, Srgba::additive_luminance(128)); + + if let Some(mouse_pos) = ui.input().mouse.pos { + if rect.contains(mouse_pos) { + let y = mouse_pos.y; + cmds.push(PaintCmd::line_segment( + [pos2(rect.left(), y), pos2(rect.right(), y)], + line_stroke, + )); + let cpu_usage = remap(y, rect.bottom_up_range(), 0.0..=graph_top_cpu_usage); + let text = format!("{:.1} ms", 1e3 * cpu_usage); + cmds.push(PaintCmd::text( + ui.fonts(), + pos2(rect.left(), y), + align::LEFT_BOTTOM, + text, + TextStyle::Monospace, + color::WHITE, + )); + } + } + + let circle_color = Srgba::additive_luminance(196); + let radius = 2.0; + let right_side_time = ui.input().time; // Time at right side of screen + + for (time, cpu_usage) in history.iter() { + let age = (right_side_time - time) as f32; + let x = remap(age, history.max_age()..=0.0, rect.x_range()); + let y = remap_clamp(cpu_usage, 0.0..=graph_top_cpu_usage, rect.bottom_up_range()); + + cmds.push(PaintCmd::line_segment( + [pos2(x, rect.bottom()), pos2(x, y)], + line_stroke, + )); + + if cpu_usage < graph_top_cpu_usage { + cmds.push(PaintCmd::circle_filled(pos2(x, y), radius, circle_color)); + } + } + + ui.painter().extend(cmds); + + response + } +} diff --git a/egui_demo_lib/src/lib.rs b/egui_demo_lib/src/lib.rs index 10dc2fbe..bfb8c5ae 100644 --- a/egui_demo_lib/src/lib.rs +++ b/egui_demo_lib/src/lib.rs @@ -46,6 +46,7 @@ )] mod apps; +pub(crate) mod frame_history; mod wrap_app; pub use apps::DemoWindows; // used for tests @@ -96,7 +97,7 @@ fn test_egui_e2e() { const NUM_FRAMES: usize = 5; for _ in 0..NUM_FRAMES { ctx.begin_frame(raw_input.clone()); - demo_windows.ui(&ctx, |_ui| {}); + demo_windows.ui(&ctx); let (_output, paint_commands) = ctx.end_frame(); let paint_jobs = ctx.tessellate(paint_commands); assert!(!paint_jobs.is_empty()); diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index 322af2d0..411aad71 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -1,11 +1,4 @@ -/// Wraps many demo/test apps into one -#[derive(Default, serde::Deserialize, serde::Serialize)] -#[serde(default)] -pub struct WrapApp { - selected_anchor: String, - apps: Apps, -} - +/// All the different demo apps. #[derive(Default, serde::Deserialize, serde::Serialize)] #[serde(default)] pub struct Apps { @@ -27,6 +20,15 @@ impl Apps { } } +/// Wraps many demo/test apps into one. +#[derive(Default, serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct WrapApp { + selected_anchor: String, + apps: Apps, + backend_panel: BackendPanel, +} + impl epi::App for WrapApp { fn name(&self) -> &str { "Egui Demo Apps" @@ -51,9 +53,13 @@ impl epi::App for WrapApp { self.selected_anchor = self.apps.iter_mut().next().unwrap().0.to_owned(); } - egui::TopPanel::top("wrap_app").show(ctx, |ui| { + egui::TopPanel::top("wrap_app_top_bar").show(ctx, |ui| { // A menu-bar is a horizontal layout with some special styles applied. - egui::menu::bar(ui, |ui| { + // egui::menu::bar(ui, |ui| { + ui.horizontal_wrapped(|ui| { + ui.checkbox(&mut self.backend_panel.open, "💻 Backend"); + ui.separator(); + for (anchor, app) in self.apps.iter_mut() { if ui .selectable_label(self.selected_anchor == anchor, app.name()) @@ -67,11 +73,14 @@ impl epi::App for WrapApp { } ui.with_layout(egui::Layout::right_to_left(), |ui| { - if let Some(seconds_since_midnight) = frame.info().seconds_since_midnight { - if clock_button(ui, seconds_since_midnight).clicked { - self.selected_anchor = "clock".to_owned(); - if frame.is_web() { - ui.output().open_url = Some("#clock".to_owned()); + if false { + // TODO: fix the overlap on small screens + if let Some(seconds_since_midnight) = frame.info().seconds_since_midnight { + if clock_button(ui, seconds_since_midnight).clicked { + self.selected_anchor = "clock".to_owned(); + if frame.is_web() { + ui.output().open_url = Some("#clock".to_owned()); + } } } } @@ -81,6 +90,13 @@ impl epi::App for WrapApp { }); }); + self.backend_panel.update(ctx, frame); + if self.backend_panel.open { + egui::SidePanel::left("backend_panel", 0.0).show(ctx, |ui| { + self.backend_panel.ui(ui, frame); + }); + } + for (anchor, app) in self.apps.iter_mut() { if anchor == self.selected_anchor { app.update(ctx, frame); @@ -101,3 +117,186 @@ fn clock_button(ui: &mut egui::Ui, seconds_since_midnight: f64) -> egui::Respons ui.add(egui::Button::new(time).text_style(egui::TextStyle::Monospace)) } + +// ---------------------------------------------------------------------------- + +/// How often we repaint the demo app by default +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum RunMode { + /// This is the default for the demo. + /// + /// If this is selected, Egui is only updated if are input events + /// (like mouse movements) or there are some animations in the GUI. + /// + /// Reactive mode saves CPU. + /// + /// The downside is that the UI can become out-of-date if something it is supposed to monitor changes. + /// For instance, a GUI for a thermostat need to repaint each time the temperature changes. + /// To ensure the UI is up to date you need to call `egui::Context::request_repaint()` each + /// time such an event happens. You can also chose to call `request_repaint()` once every second + /// or after every single frame - this is called `Continuous` mode, + /// and for games and interactive tools that need repainting every frame anyway, this should be the default. + Reactive, + + /// This will call `egui::Context::request_repaint()` at the end of each frame + /// to request the backend to repaint as soon as possible. + /// + /// On most platforms this will mean that Egui will run at the display refresh rate of e.g. 60 Hz. + /// + /// For this demo it is not any reason to do so except to + /// demonstrate how quickly Egui runs. + /// + /// For games or other interactive apps, this is probably what you want to do. + /// It will guarantee that Egui is always up-to-date. + Continuous, +} + +/// Default for demo is Reactive since +/// 1) We want to use minimal CPU +/// 2) There are no external events that could invalidate the UI +/// so there are no events to miss. +impl Default for RunMode { + fn default() -> Self { + RunMode::Reactive + } +} + +// ---------------------------------------------------------------------------- + +#[derive(Default, serde::Deserialize, serde::Serialize)] +#[serde(default)] +struct BackendPanel { + open: bool, + + #[serde(skip)] // go back to `Reactive` mode each time we start + run_mode: RunMode, + + /// current slider value for current gui scale + pixels_per_point: Option, + + #[serde(skip)] + frame_history: crate::frame_history::FrameHistory, +} + +impl BackendPanel { + fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) { + self.frame_history + .on_new_frame(ctx.input().time, frame.info().cpu_usage); + + if self.run_mode == RunMode::Continuous { + // Tell the backend to repaint as soon as possible + ctx.request_repaint(); + } + } + + fn ui(&mut self, ui: &mut egui::Ui, frame: &mut epi::Frame<'_>) { + let is_web = frame.is_web(); + + if is_web { + ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."); + ui.label( + "Everything you see is rendered as textured triangles. There is no DOM. There are no HTML elements. \ + This is not JavaScript. This is Rust, running at 60 FPS. This is the web page, reinvented with game tech."); + ui.label("This is also work in progress, and not ready for production... yet :)"); + ui.horizontal(|ui| { + ui.label("Project home page:"); + ui.hyperlink("https://github.com/emilk/egui"); + }); + ui.separator(); + } + + self.run_mode_ui(ui); + + ui.separator(); + + self.frame_history.ui(ui); + + if !is_web { + // web browsers have their own way of zooming, which egui_web respects + ui.separator(); + if let Some(new_pixels_per_point) = self.pixels_per_point_ui(ui, frame.info()) { + frame.set_pixels_per_point(new_pixels_per_point); + } + } + + if !is_web { + ui.separator(); + if ui + .button("📱 Phone Size") + .on_hover_text("Resize the window to be small like a phone.") + .clicked + { + frame.set_window_size(egui::Vec2::new(375.0, 812.0)); // iPhone 12 mini + } + if ui.button("Quit").clicked { + frame.quit(); + } + } + } + + fn pixels_per_point_ui( + &mut self, + ui: &mut egui::Ui, + info: &epi::IntegrationInfo, + ) -> Option { + #![allow(clippy::float_cmp)] + + self.pixels_per_point = self + .pixels_per_point + .or(info.native_pixels_per_point) + .or_else(|| Some(ui.ctx().pixels_per_point())); + + let pixels_per_point = self.pixels_per_point.as_mut()?; + + ui.horizontal(|ui| { + ui.style_mut().spacing.slider_width = 90.0; + ui.add( + egui::Slider::f32(pixels_per_point, 0.5..=5.0) + .logarithmic(true) + .text("Scale"), + ) + .on_hover_text("Physical pixels per point."); + if let Some(native_pixels_per_point) = info.native_pixels_per_point { + let button = egui::Button::new("Reset") + .enabled(*pixels_per_point != native_pixels_per_point); + if ui + .add(button) + .on_hover_text(format!( + "Reset scale to native value ({:.1})", + native_pixels_per_point + )) + .clicked + { + *pixels_per_point = native_pixels_per_point; + } + } + }); + + // We wait until mouse release to activate: + if ui.ctx().is_using_mouse() { + None + } else { + Some(*pixels_per_point) + } + } + + fn run_mode_ui(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + let run_mode = &mut self.run_mode; + ui.label("Mode:"); + ui.radio_value(run_mode, RunMode::Continuous, "Continuous") + .on_hover_text("Repaint everything each frame"); + ui.radio_value(run_mode, RunMode::Reactive, "Reactive") + .on_hover_text("Repaint when there are animations or input (e.g. mouse movement)"); + }); + + if self.run_mode == RunMode::Continuous { + ui.label(format!( + "Repainting the UI each frame. FPS: {:.1}", + self.frame_history.fps() + )); + } else { + ui.label("Only running UI code when there are animations or input"); + } + } +}