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 /// [`egui_glium`](https://crates.io/crates/egui_glium) and [`egui_web`](https://crates.io/crates/egui_web). #[derive(Default, serde::Deserialize, serde::Serialize)] #[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 { fn name(&self) -> &str { "✨ Egui Demo" } fn load(&mut self, storage: &dyn epi::Storage) { *self = epi::get_value(storage, epi::APP_KEY).unwrap_or_default() } fn save(&mut self, storage: &mut dyn epi::Storage) { 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(); } } }