[demo] add frame time graph to demo app

This commit is contained in:
Emil Ernerfeldt 2020-10-01 22:25:44 +02:00
parent 1156525ce9
commit 6fcfb52aa0
18 changed files with 371 additions and 130 deletions

51
egui/src/align.rs Normal file
View file

@ -0,0 +1,51 @@
use crate::math::{pos2, Rect};
/// left/center/right or top/center/bottom alignment for e.g. anchors and `Layout`s.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Align {
/// Left/Top
Min,
/// Note: requires a bounded/known available_width.
Center,
/// Right/Bottom
/// Note: requires a bounded/known available_width.
Max,
}
impl Default for Align {
fn default() -> Align {
Align::Min
}
}
pub type Align2 = (Align, Align);
pub const LEFT_BOTTOM: Align2 = (Align::Min, Align::Max);
pub const LEFT_CENTER: Align2 = (Align::Min, Align::Center);
pub const LEFT_TOP: Align2 = (Align::Min, Align::Min);
pub const CENTER_BOTTOM: Align2 = (Align::Center, Align::Max);
pub const CENTER_CENTER: Align2 = (Align::Center, Align::Center);
pub const CENTER_TOP: Align2 = (Align::Center, Align::Min);
pub const RIGHT_BOTTOM: Align2 = (Align::Max, Align::Max);
pub const RIGHT_CENTER: Align2 = (Align::Max, Align::Center);
pub const RIGHT_TOP: Align2 = (Align::Max, Align::Min);
/// Used e.g. to anchor a piece of text to a part of the rectangle.
/// Give a position within the rect, specified by the aligns
pub(crate) fn anchor_rect(rect: Rect, anchor: (Align, Align)) -> Rect {
let x = match anchor.0 {
Align::Min => rect.left(),
Align::Center => rect.left() - 0.5 * rect.width(),
Align::Max => rect.left() - rect.width(),
};
let y = match anchor.1 {
Align::Min => rect.top(),
Align::Center => rect.top() - 0.5 * rect.height(),
Align::Max => rect.top() - rect.height(),
};
Rect::from_min_size(pos2(x, y), rect.size())
}

View file

@ -30,11 +30,9 @@ pub trait Backend {
None None
} }
/// excludes painting /// Seconds of cpu usage (in seconds) of UI code on the previous frame.
fn cpu_time(&self) -> f32; /// Zero if this is the first frame.
fn cpu_usage(&self) -> Option<f32>;
/// Smoothed frames per second
fn fps(&self) -> f32;
/// Local time. Used for the clock in the demo app. /// Local time. Used for the clock in the demo app.
fn seconds_since_midnight(&self) -> Option<f64> { fn seconds_since_midnight(&self) -> Option<f64> {

View file

@ -85,6 +85,16 @@ impl Resize {
self.min_size = min_size.into(); self.min_size = min_size.into();
self self
} }
/// Won't shrink to smaller than this
pub fn min_width(mut self, min_width: f32) -> Self {
self.min_size.x = min_width;
self
}
/// Won't shrink to smaller than this
pub fn min_height(mut self, min_height: f32) -> Self {
self.min_size.y = min_height;
self
}
/// Won't expand to larger than this /// Won't expand to larger than this
pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self { pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {

View file

@ -77,6 +77,17 @@ impl<'open> Window<'open> {
self self
} }
/// Set minimum width of the window.
pub fn min_width(mut self, min_width: f32) -> Self {
self.resize = self.resize.min_width(min_width);
self
}
/// Set minimum height of the window.
pub fn min_height(mut self, min_height: f32) -> Self {
self.resize = self.resize.min_height(min_height);
self
}
/// Set initial position of the window. /// Set initial position of the window.
pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self { pub fn default_pos(mut self, default_pos: impl Into<Pos2>) -> Self {
self.area = self.area.default_pos(default_pos); self.area = self.area.default_pos(default_pos);

View file

@ -47,6 +47,120 @@ impl Default for RunMode {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
struct FrameHistory {
frame_times: History<f32>,
}
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 {
pub fn on_new_frame(&mut self, now: f64, previus_frame_time: Option<f32>) {
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 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: {}",
self.frame_times.total_count()
));
ui.label(format!(
"Mean CPU usage per frame: {:.2} ms / frame",
1e3 * self.frame_times.average().unwrap_or_default()
))
.tooltip_text(
"Includes Egui layout and tesselation time.\n\
Does not include GPU usage, nor overhead for sending data to GPU.",
);
CollapsingHeader::new("CPU usage history")
.default_open(false)
.show(ui, |ui| {
self.graph(ui);
});
}
fn graph(&mut self, ui: &mut Ui) {
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 rect = ui.allocate_space(vec2(ui.available_finite().width(), height));
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.range_x());
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);
}
}
// ----------------------------------------------------------------------------
/// Special input to the demo-app. /// Special input to the demo-app.
#[derive(Default)] #[derive(Default)]
pub struct DemoEnvironment { pub struct DemoEnvironment {
@ -62,6 +176,7 @@ pub struct DemoEnvironment {
/// ///
/// Implements `egui::app::App` so it can be used with /// Implements `egui::app::App` so it can be used with
/// [`egui_glium`](https://crates.io/crates/egui_glium) and [`egui_web`](https://crates.io/crates/egui_web). /// [`egui_glium`](https://crates.io/crates/egui_glium) and [`egui_web`](https://crates.io/crates/egui_web).
// TODO: split into `DemoWindows` and `app::DemoApp`
#[derive(Default)] #[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
@ -72,7 +187,10 @@ pub struct DemoApp {
open_windows: OpenWindows, open_windows: OpenWindows,
demo_window: DemoWindow, demo_window: DemoWindow,
fractal_clock: FractalClock, fractal_clock: FractalClock,
num_frames_painted: u64,
#[cfg_attr(feature = "serde", serde(skip))]
frame_history: FrameHistory,
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
color_test: ColorTest, color_test: ColorTest,
show_color_test: bool, show_color_test: bool,
@ -195,15 +313,18 @@ impl DemoApp {
}); });
} }
// TODO: give cpu_usage and web_info via `struct BackendInfo`
fn backend_ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) { fn backend_ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) {
self.frame_history
.on_new_frame(ui.input().time, backend.cpu_usage());
let is_web = backend.web_info().is_some(); let is_web = backend.web_info().is_some();
if is_web { if is_web {
ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL."); ui.label("Egui is an immediate mode GUI written in Rust, compiled to WebAssembly, rendered with WebGL.");
ui.label( ui.label(
"Everything you see is rendered as textured triangles. There is no DOM. There are no HTML elements." "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 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.label("This is also work in progress, and not ready for production... yet :)");
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Project home page:"); ui.label("Project home page:");
@ -219,16 +340,28 @@ impl DemoApp {
ui.separator(); ui.separator();
ui.add( self.run_mode_ui(ui);
label!(
"CPU usage: {:.2} ms / frame (excludes painting)", if self.run_mode == RunMode::Continuous {
1e3 * backend.cpu_time() ui.label(format!(
) "Repainting the UI each frame. FPS: {:.1}",
.text_style(TextStyle::Monospace), self.frame_history.fps()
); ));
} else {
ui.label("Only running UI code when there are animations or input");
}
ui.separator(); ui.separator();
self.frame_history.ui(ui);
ui.separator();
ui.checkbox(
"Show color blend test (debug backend painter)",
&mut self.show_color_test,
);
}
fn run_mode_ui(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let run_mode = &mut self.run_mode; let run_mode = &mut self.run_mode;
ui.label("Run mode:"); ui.label("Run mode:");
@ -237,32 +370,17 @@ impl DemoApp {
ui.radio_value("Reactive", run_mode, RunMode::Reactive) ui.radio_value("Reactive", run_mode, RunMode::Reactive)
.tooltip_text("Repaint when there are animations or input (e.g. mouse movement)"); .tooltip_text("Repaint when there are animations or input (e.g. mouse movement)");
}); });
if self.run_mode == RunMode::Continuous {
ui.add(
label!("Repainting the UI each frame. FPS: {:.1}", backend.fps())
.text_style(TextStyle::Monospace),
);
} else {
ui.label("Only running UI code when there are animations or input");
}
self.num_frames_painted += 1;
ui.label(format!("Total frames painted: {}", self.num_frames_painted));
ui.separator();
ui.checkbox(
"Show color blend test (debug backend painter)",
&mut self.show_color_test,
);
} }
} }
impl app::App for DemoApp { impl app::App for DemoApp {
fn ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) { fn ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) {
Window::new("Backend").scroll(false).show(ui.ctx(), |ui| { Window::new("Backend")
self.backend_ui(ui, backend); .min_width(360.0)
}); .scroll(false)
.show(ui.ctx(), |ui| {
self.backend_ui(ui, backend);
});
let Self { let Self {
show_color_test, show_color_test,

View file

@ -1,40 +1,84 @@
use std::collections::VecDeque; use std::collections::VecDeque;
/// This struct tracks recent values of some time series. /// This struct tracks recent values of some time series.
///
/// One use is to show a log of recent events,
/// or show a graph over recent events.
///
/// It has both a maximum length and a maximum storage time.
/// Elements are dropped when either max length or max age is reached.
///
/// Time difference between values can be zero, but never negative.
///
/// This can be used for things like smoothed averages (for e.g. FPS) /// This can be used for things like smoothed averages (for e.g. FPS)
/// or for smoothed velocity (e.g. mouse pointer speed). /// or for smoothed velocity (e.g. mouse pointer speed).
/// All times are in seconds. /// All times are in seconds.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MovementTracker<T> { pub struct History<T> {
/// In elements, i.e. of `values.len()`
max_len: usize, max_len: usize,
max_age: f64,
/// (time, value) pais /// In seconds
max_age: f64, // TODO: f32
/// Total number of elements seen ever
total_count: u64,
/// (time, value) pairs, oldest front, newest back.
/// Time difference between values can be zero, but never negative.
values: VecDeque<(f64, T)>, values: VecDeque<(f64, T)>,
} }
impl<T> MovementTracker<T> impl<T> History<T>
where where
T: Copy, T: Copy,
{ {
pub fn new(max_len: usize, max_age: f64) -> Self { pub fn new(max_len: usize, max_age: f64) -> Self {
Self::from_max_len_age(max_len, max_age)
}
pub fn from_max_len_age(max_len: usize, max_age: f64) -> Self {
Self { Self {
max_len, max_len,
max_age, max_age,
total_count: 0,
values: Default::default(), values: Default::default(),
} }
} }
pub fn max_len(&self) -> usize {
self.max_len
}
pub fn max_age(&self) -> f32 {
self.max_age as f32
}
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.values.is_empty() self.values.is_empty()
} }
/// Current number of values kept in history
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.values.len() self.values.len()
} }
/// Amount of time contained from start to end in this `MovementTracker` /// Total number of values seen.
pub fn dt(&self) -> f32 { /// Includes those that have been discarded due to `max_len` or `max_age`.
pub fn total_count(&self) -> u64 {
self.total_count
}
pub fn latest(&self) -> Option<T> {
self.values.back().map(|(_, value)| *value)
}
pub fn latest_mut(&mut self) -> Option<&mut T> {
self.values.back_mut().map(|(_, value)| value)
}
/// Amount of time contained from start to end in this `History`.
pub fn duration(&self) -> f32 {
if let (Some(front), Some(back)) = (self.values.front(), self.values.back()) { if let (Some(front), Some(back)) = (self.values.front(), self.values.back()) {
(back.0 - front.0) as f32 (back.0 - front.0) as f32
} else { } else {
@ -42,6 +86,13 @@ where
} }
} }
/// `(time, value)` pairs
/// Time difference between values can be zero, but never negative.
// TODO: impl IntoIter
pub fn iter<'a>(&'a self) -> impl Iterator<Item = (f64, T)> + 'a {
self.values.iter().map(|(time, value)| (*time, *value))
}
pub fn values<'a>(&'a self) -> impl Iterator<Item = T> + 'a { pub fn values<'a>(&'a self) -> impl Iterator<Item = T> + 'a {
self.values.iter().map(|(_time, value)| *value) self.values.iter().map(|(_time, value)| *value)
} }
@ -53,13 +104,14 @@ where
/// Values must be added with a monotonically increasing time, or at least not decreasing. /// Values must be added with a monotonically increasing time, or at least not decreasing.
pub fn add(&mut self, now: f64, value: T) { pub fn add(&mut self, now: f64, value: T) {
if let Some((last_time, _)) = self.values.back() { if let Some((last_time, _)) = self.values.back() {
debug_assert!(now >= *last_time, "Time shouldn't go backwards"); debug_assert!(now >= *last_time, "Time shouldn't move backwards");
} }
self.total_count += 1;
self.values.push_back((now, value)); self.values.push_back((now, value));
self.flush(now); self.flush(now);
} }
/// Mean time difference between values in this `MovementTracker`. /// Mean time difference between values in this `History`.
pub fn mean_time_interval(&self) -> Option<f32> { pub fn mean_time_interval(&self) -> Option<f32> {
if let (Some(first), Some(last)) = (self.values.front(), self.values.back()) { if let (Some(first), Some(last)) = (self.values.front(), self.values.back()) {
let n = self.len(); let n = self.len();
@ -88,7 +140,7 @@ where
} }
} }
impl<T> MovementTracker<T> impl<T> History<T>
where where
T: Copy, T: Copy,
T: std::iter::Sum, T: std::iter::Sum,
@ -108,7 +160,7 @@ where
} }
} }
impl<T, Vel> MovementTracker<T> impl<T, Vel> History<T>
where where
T: Copy, T: Copy,
T: std::ops::Sub<Output = Vel>, T: std::ops::Sub<Output = Vel>,

View file

@ -1,6 +1,6 @@
//! The input needed by Egui. //! The input needed by Egui.
use crate::math::*; use crate::{math::*, History};
/// If mouse moves more than this, it is no longer a click (but maybe a drag) /// If mouse moves more than this, it is no longer a click (but maybe a drag)
const MAX_CLICK_DIST: f32 = 6.0; const MAX_CLICK_DIST: f32 = 6.0;
@ -128,7 +128,7 @@ pub struct MouseInput {
/// Recent movement of the mouse. /// Recent movement of the mouse.
/// Used for calculating velocity of mouse pointer. /// Used for calculating velocity of mouse pointer.
pub pos_tracker: MovementTracker<Pos2>, pos_history: History<Pos2>,
} }
impl Default for MouseInput { impl Default for MouseInput {
@ -145,7 +145,7 @@ impl Default for MouseInput {
press_origin: None, press_origin: None,
delta: Vec2::zero(), delta: Vec2::zero(),
velocity: Vec2::zero(), velocity: Vec2::zero(),
pos_tracker: MovementTracker::new(1000, 0.1), pos_history: History::new(1000, 0.1),
} }
} }
} }
@ -295,20 +295,20 @@ impl MouseInput {
if pressed { if pressed {
// Start of a drag: we want to track the velocity for during the drag // Start of a drag: we want to track the velocity for during the drag
// and ignore any incoming movement // and ignore any incoming movement
self.pos_tracker.clear(); self.pos_history.clear();
} }
if let Some(mouse_pos) = new.mouse_pos { if let Some(mouse_pos) = new.mouse_pos {
self.pos_tracker.add(new.time, mouse_pos); self.pos_history.add(new.time, mouse_pos);
} else { } else {
// we do not clear the `mouse_tracker` here, because it is exactly when a finger has // we do not clear the `mouse_tracker` here, because it is exactly when a finger has
// released from the touch screen that we may want to assign a velocity to whatever // released from the touch screen that we may want to assign a velocity to whatever
// the user tried to throw // the user tried to throw
} }
self.pos_tracker.flush(new.time); self.pos_history.flush(new.time);
let velocity = if self.pos_tracker.len() >= 3 && self.pos_tracker.dt() > 0.01 { let velocity = if self.pos_history.len() >= 3 && self.pos_history.duration() > 0.01 {
self.pos_tracker.velocity().unwrap_or_default() self.pos_history.velocity().unwrap_or_default()
} else { } else {
Vec2::default() Vec2::default()
}; };
@ -325,7 +325,7 @@ impl MouseInput {
press_origin, press_origin,
delta, delta,
velocity, velocity,
pos_tracker: self.pos_tracker, pos_history: self.pos_history,
} }
} }
} }
@ -411,7 +411,7 @@ impl MouseInput {
press_origin, press_origin,
delta, delta,
velocity, velocity,
pos_tracker: _, pos_history: _,
} = self; } = self;
ui.label(format!("down: {}", down)); ui.label(format!("down: {}", down));

View file

@ -1,4 +1,4 @@
use crate::{math::*, style::Style}; use crate::{math::*, style::Style, Align};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -17,44 +17,6 @@ impl Default for Direction {
} }
} }
/// left/center/right or top/center/bottom alignment for e.g. anchors and `Layout`s.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Align {
/// Left/Top
Min,
/// Note: requires a bounded/known available_width.
Center,
/// Right/Bottom
/// Note: requires a bounded/known available_width.
Max,
}
impl Default for Align {
fn default() -> Align {
Align::Min
}
}
/// Used e.g. to anchor a piece of text to a part of the rectangle.
/// Give a position within the rect, specified by the aligns
pub(crate) fn anchor_rect(rect: Rect, anchor: (Align, Align)) -> Rect {
let x = match anchor.0 {
Align::Min => rect.left(),
Align::Center => rect.left() - 0.5 * rect.width(),
Align::Max => rect.left() - rect.width(),
};
let y = match anchor.1 {
Align::Min => rect.top(),
Align::Center => rect.top() - 0.5 * rect.height(),
Align::Max => rect.top() - rect.height(),
};
Rect::from_min_size(pos2(x, y), rect.size())
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
/// The layout of a `Ui`, e.g. horizontal left-aligned. /// The layout of a `Ui`, e.g. horizontal left-aligned.

View file

@ -44,12 +44,14 @@
rust_2018_idioms, rust_2018_idioms,
)] )]
pub mod align;
mod animation_manager; mod animation_manager;
pub mod app; pub mod app;
pub(crate) mod cache; pub(crate) mod cache;
pub mod containers; pub mod containers;
mod context; mod context;
pub mod demos; pub mod demos;
mod history;
mod id; mod id;
mod input; mod input;
mod introspection; mod introspection;
@ -66,9 +68,11 @@ mod ui;
pub mod widgets; pub mod widgets;
pub use { pub use {
align::Align,
containers::*, containers::*,
context::Context, context::Context,
demos::DemoApp, demos::DemoApp,
history::History,
id::Id, id::Id,
input::*, input::*,
layers::*, layers::*,

View file

@ -4,13 +4,12 @@ use std::ops::{Add, Mul, RangeInclusive};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
mod movement_tracker;
mod pos2; mod pos2;
mod rect; mod rect;
pub mod smart_aim; pub mod smart_aim;
mod vec2; mod vec2;
pub use {movement_tracker::*, pos2::*, rect::*, vec2::*}; pub use {pos2::*, rect::*, vec2::*};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View file

@ -147,6 +147,10 @@ impl Rect {
self.min.y..=self.max.y self.min.y..=self.max.y
} }
pub fn bottom_up_range(&self) -> RangeInclusive<f32> {
self.max.y..=self.min.y
}
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.max.x < self.min.x || self.max.y < self.min.y self.max.x < self.min.x || self.max.y < self.min.y
} }

View file

@ -1,6 +1,9 @@
use { use {
super::{font::Galley, fonts::TextStyle, Srgba, Triangles}, super::{font::Galley, fonts::TextStyle, Fonts, Srgba, Triangles},
crate::math::{Pos2, Rect}, crate::{
align::{anchor_rect, Align},
math::{Pos2, Rect},
},
}; };
// TODO: rename, e.g. `paint::Cmd`? // TODO: rename, e.g. `paint::Cmd`?
@ -86,6 +89,25 @@ impl PaintCmd {
stroke: stroke.into(), stroke: stroke.into(),
} }
} }
pub fn text(
fonts: &Fonts,
pos: Pos2,
anchor: (Align, Align),
text: impl Into<String>,
text_style: TextStyle,
color: Srgba,
) -> Self {
let font = &fonts[text_style];
let galley = font.layout_multiline(text.into(), f32::INFINITY);
let rect = anchor_rect(Rect::from_min_size(pos, galley.size), anchor);
Self::Text {
pos: rect.min,
galley,
text_style,
color,
}
}
} }
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]

View file

@ -1,11 +1,12 @@
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::{
anchor_rect, color, align::{anchor_rect, Align, LEFT_TOP},
color,
layers::PaintCmdIdx, layers::PaintCmdIdx,
math::{Pos2, Rect, Vec2}, math::{Pos2, Rect, Vec2},
paint::{font, Fonts, PaintCmd, Stroke, TextStyle}, paint::{font, Fonts, PaintCmd, Stroke, TextStyle},
Align, Context, Layer, Srgba, Context, Layer, Srgba,
}; };
/// Helper to paint shapes and text to a specific region on a specific layer. /// Helper to paint shapes and text to a specific region on a specific layer.
@ -116,18 +117,16 @@ impl Painter {
impl Painter { impl Painter {
pub fn debug_rect(&mut self, rect: Rect, color: Srgba, text: impl Into<String>) { pub fn debug_rect(&mut self, rect: Rect, color: Srgba, text: impl Into<String>) {
self.rect_stroke(rect, 0.0, (1.0, color)); self.rect_stroke(rect, 0.0, (1.0, color));
let anchor = (Align::Min, Align::Min);
let text_style = TextStyle::Monospace; let text_style = TextStyle::Monospace;
self.text(rect.min, anchor, text.into(), text_style, color); self.text(rect.min, LEFT_TOP, text.into(), text_style, color);
} }
pub fn error(&self, pos: Pos2, text: impl Into<String>) { pub fn error(&self, pos: Pos2, text: impl Into<String>) {
let text = text.into(); let text = text.into();
let anchor = (Align::Min, Align::Min);
let text_style = TextStyle::Monospace; let text_style = TextStyle::Monospace;
let font = &self.fonts()[text_style]; let font = &self.fonts()[text_style];
let galley = font.layout_multiline(text, f32::INFINITY); let galley = font.layout_multiline(text, f32::INFINITY);
let rect = anchor_rect(Rect::from_min_size(pos, galley.size), anchor); let rect = anchor_rect(Rect::from_min_size(pos, galley.size), LEFT_TOP);
self.add(PaintCmd::Rect { self.add(PaintCmd::Rect {
rect: rect.expand(2.0), rect: rect.expand(2.0),
corner_radius: 0.0, corner_radius: 0.0,

View file

@ -23,11 +23,18 @@ pub struct Style {
} }
impl Style { impl Style {
// TODO: rename style.interact() to maybe... `style.response_visuals` ? // TODO: rename style.interact() to maybe... `style.interactive` ?
/// Use this style for interactive things /// Use this style for interactive things.
/// Note that you must already have a response,
/// i.e. you must allocate space and interact BEFORE painting the widget!
pub fn interact(&self, response: &Response) -> &WidgetVisuals { pub fn interact(&self, response: &Response) -> &WidgetVisuals {
self.visuals.widgets.style(response) self.visuals.widgets.style(response)
} }
/// Style to use for non-interactive widgets.
pub fn noninteractive(&self) -> &WidgetVisuals {
&self.visuals.widgets.noninteractive
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -116,6 +123,10 @@ pub struct Visuals {
} }
impl Visuals { impl Visuals {
pub fn noninteractive(&self) -> &WidgetVisuals {
&self.widgets.noninteractive
}
pub fn text_color(&self) -> Srgba { pub fn text_color(&self) -> Srgba {
self.widgets.noninteractive.text_color() self.widgets.noninteractive.text_color()
} }

View file

@ -478,6 +478,11 @@ impl Ui {
self.add(label.into().heading()) self.add(label.into().heading())
} }
/// Shortcut for `add(Label::new(text).monospace())`
pub fn monospace(&mut self, label: impl Into<Label>) -> Response {
self.add(label.into().monospace())
}
/// Shortcut for `add(Hyperlink::new(url))` /// Shortcut for `add(Hyperlink::new(url))`
pub fn hyperlink(&mut self, url: impl Into<String>) -> Response { pub fn hyperlink(&mut self, url: impl Into<String>) -> Response {
self.add(Hyperlink::new(url)) self.add(Hyperlink::new(url))
@ -616,10 +621,10 @@ impl Ui {
impl Ui { impl Ui {
pub fn collapsing<R>( pub fn collapsing<R>(
&mut self, &mut self,
text: impl Into<String>, heading: impl Into<String>,
add_contents: impl FnOnce(&mut Ui) -> R, add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> { ) -> Option<R> {
CollapsingHeader::new(text).show(self, add_contents) CollapsingHeader::new(heading).show(self, add_contents)
} }
/// Create a child ui at the current cursor. /// Create a child ui at the current cursor.

View file

@ -65,6 +65,10 @@ impl Label {
self.text_style(TextStyle::Heading) self.text_style(TextStyle::Heading)
} }
pub fn monospace(self) -> Self {
self.text_style(TextStyle::Monospace)
}
pub fn text_color(mut self, text_color: Srgba) -> Self { pub fn text_color(mut self, text_color: Srgba) -> Self {
self.text_color = Some(text_color); self.text_color = Some(text_color);
self self

View file

@ -14,7 +14,7 @@ const EGUI_MEMORY_KEY: &str = "egui";
const WINDOW_KEY: &str = "window"; const WINDOW_KEY: &str = "window";
pub struct GliumBackend { pub struct GliumBackend {
frame_times: egui::MovementTracker<f32>, previous_frame_time: Option<f32>,
quit: bool, quit: bool,
painter: Painter, painter: Painter,
} }
@ -22,7 +22,7 @@ pub struct GliumBackend {
impl GliumBackend { impl GliumBackend {
pub fn new(painter: Painter) -> Self { pub fn new(painter: Painter) -> Self {
Self { Self {
frame_times: egui::MovementTracker::new(1000, 1.0), previous_frame_time: None,
quit: false, quit: false,
painter, painter,
} }
@ -30,12 +30,8 @@ impl GliumBackend {
} }
impl Backend for GliumBackend { impl Backend for GliumBackend {
fn cpu_time(&self) -> f32 { fn cpu_usage(&self) -> Option<f32> {
self.frame_times.average().unwrap_or_default() self.previous_frame_time
}
fn fps(&self) -> f32 {
1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
} }
fn seconds_since_midnight(&self) -> Option<f64> { fn seconds_since_midnight(&self) -> Option<f64> {
@ -100,7 +96,7 @@ pub fn run(title: &str, mut storage: FileStorage, mut app: impl App + 'static) -
let (output, paint_jobs) = ctx.end_frame(); let (output, paint_jobs) = ctx.end_frame();
let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32; let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32;
runner.frame_times.add(raw_input.time, frame_time); runner.previous_frame_time = Some(frame_time);
runner runner
.painter .painter
@ -127,7 +123,7 @@ pub fn run(title: &str, mut storage: FileStorage, mut app: impl App + 'static) -
glutin::event::Event::WindowEvent { event, .. } => { glutin::event::Event::WindowEvent { event, .. } => {
input_to_egui(event, clipboard.as_mut(), &mut raw_input, control_flow); input_to_egui(event, clipboard.as_mut(), &mut raw_input, control_flow);
display.gl_window().window().request_redraw(); // TODO: maybe only on some events? display.gl_window().window().request_redraw(); // TODO: ask Egui if the events warrants a repaint instead
} }
glutin::event::Event::LoopDestroyed => { glutin::event::Event::LoopDestroyed => {
egui::app::set_value( egui::app::set_value(

View file

@ -10,7 +10,7 @@ pub use egui::{
pub struct WebBackend { pub struct WebBackend {
ctx: Arc<egui::Context>, ctx: Arc<egui::Context>,
painter: webgl::Painter, painter: webgl::Painter,
frame_times: egui::MovementTracker<f32>, previous_frame_time: Option<f32>,
frame_start: Option<f64>, frame_start: Option<f64>,
last_save_time: Option<f64>, last_save_time: Option<f64>,
} }
@ -22,7 +22,7 @@ impl WebBackend {
Ok(Self { Ok(Self {
ctx, ctx,
painter: webgl::Painter::new(canvas_id)?, painter: webgl::Painter::new(canvas_id)?,
frame_times: egui::MovementTracker::new(1000, 1.0), previous_frame_time: None,
frame_start: None, frame_start: None,
last_save_time: None, last_save_time: None,
}) })
@ -50,7 +50,7 @@ impl WebBackend {
self.auto_save(); self.auto_save();
let now = now_sec(); let now = now_sec();
self.frame_times.add(now, (now - frame_start) as f32); self.previous_frame_time = Some((now - frame_start) as f32);
Ok((output, paint_jobs)) Ok((output, paint_jobs))
} }
@ -87,13 +87,8 @@ impl Backend for WebBackend {
}) })
} }
/// excludes painting fn cpu_usage(&self) -> Option<f32> {
fn cpu_time(&self) -> f32 { self.previous_frame_time
self.frame_times.average().unwrap_or_default()
}
fn fps(&self) -> f32 {
1.0 / self.frame_times.mean_time_interval().unwrap_or_default()
} }
fn seconds_since_midnight(&self) -> Option<f64> { fn seconds_since_midnight(&self) -> Option<f64> {