diff --git a/egui/src/align.rs b/egui/src/align.rs new file mode 100644 index 00000000..4324e7e1 --- /dev/null +++ b/egui/src/align.rs @@ -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()) +} diff --git a/egui/src/app.rs b/egui/src/app.rs index 6330b3df..ac10e3de 100644 --- a/egui/src/app.rs +++ b/egui/src/app.rs @@ -30,11 +30,9 @@ pub trait Backend { None } - /// excludes painting - fn cpu_time(&self) -> f32; - - /// Smoothed frames per second - fn fps(&self) -> f32; + /// Seconds of cpu usage (in seconds) of UI code on the previous frame. + /// Zero if this is the first frame. + fn cpu_usage(&self) -> Option; /// Local time. Used for the clock in the demo app. fn seconds_since_midnight(&self) -> Option { diff --git a/egui/src/containers/resize.rs b/egui/src/containers/resize.rs index 3c7ecf36..eab9f6a0 100644 --- a/egui/src/containers/resize.rs +++ b/egui/src/containers/resize.rs @@ -85,6 +85,16 @@ impl Resize { self.min_size = min_size.into(); 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 pub fn max_size(mut self, max_size: impl Into) -> Self { diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index 830008c7..08e4b9d8 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -77,6 +77,17 @@ impl<'open> Window<'open> { 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. pub fn default_pos(mut self, default_pos: impl Into) -> Self { self.area = self.area.default_pos(default_pos); diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index ef2d0327..31564f5a 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -47,6 +47,120 @@ impl Default for RunMode { // ---------------------------------------------------------------------------- +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 { + 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 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. #[derive(Default)] pub struct DemoEnvironment { @@ -62,6 +176,7 @@ pub struct DemoEnvironment { /// /// 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). +// TODO: split into `DemoWindows` and `app::DemoApp` #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] @@ -72,7 +187,10 @@ pub struct DemoApp { open_windows: OpenWindows, demo_window: DemoWindow, fractal_clock: FractalClock, - num_frames_painted: u64, + + #[cfg_attr(feature = "serde", serde(skip))] + frame_history: FrameHistory, + #[cfg_attr(feature = "serde", serde(skip))] color_test: ColorTest, 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) { + self.frame_history + .on_new_frame(ui.input().time, backend.cpu_usage()); + let is_web = backend.web_info().is_some(); 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." - ); - ui.label("This is not JavaScript. This is Rust, running at 60 FPS. This is the web page, reinvented with game tech."); + "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:"); @@ -219,16 +340,28 @@ impl DemoApp { ui.separator(); - ui.add( - label!( - "CPU usage: {:.2} ms / frame (excludes painting)", - 1e3 * backend.cpu_time() - ) - .text_style(TextStyle::Monospace), - ); + self.run_mode_ui(ui); + + 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"); + } 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| { let run_mode = &mut self.run_mode; ui.label("Run mode:"); @@ -237,32 +370,17 @@ impl DemoApp { ui.radio_value("Reactive", run_mode, RunMode::Reactive) .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 { fn ui(&mut self, ui: &mut Ui, backend: &mut dyn app::Backend) { - Window::new("Backend").scroll(false).show(ui.ctx(), |ui| { - self.backend_ui(ui, backend); - }); + Window::new("Backend") + .min_width(360.0) + .scroll(false) + .show(ui.ctx(), |ui| { + self.backend_ui(ui, backend); + }); let Self { show_color_test, diff --git a/egui/src/math/movement_tracker.rs b/egui/src/history.rs similarity index 60% rename from egui/src/math/movement_tracker.rs rename to egui/src/history.rs index 1c3236c9..67bfb55a 100644 --- a/egui/src/math/movement_tracker.rs +++ b/egui/src/history.rs @@ -1,40 +1,84 @@ use std::collections::VecDeque; /// 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) /// or for smoothed velocity (e.g. mouse pointer speed). /// All times are in seconds. #[derive(Clone, Debug)] -pub struct MovementTracker { +pub struct History { + /// In elements, i.e. of `values.len()` 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)>, } -impl MovementTracker +impl History where T: Copy, { 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 { max_len, max_age, + total_count: 0, 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 { self.values.is_empty() } + /// Current number of values kept in history pub fn len(&self) -> usize { self.values.len() } - /// Amount of time contained from start to end in this `MovementTracker` - pub fn dt(&self) -> f32 { + /// Total number of values seen. + /// 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 { + 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()) { (back.0 - front.0) as f32 } 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 + 'a { + self.values.iter().map(|(time, value)| (*time, *value)) + } + pub fn values<'a>(&'a self) -> impl Iterator + 'a { 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. pub fn add(&mut self, now: f64, value: T) { 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.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 { if let (Some(first), Some(last)) = (self.values.front(), self.values.back()) { let n = self.len(); @@ -88,7 +140,7 @@ where } } -impl MovementTracker +impl History where T: Copy, T: std::iter::Sum, @@ -108,7 +160,7 @@ where } } -impl MovementTracker +impl History where T: Copy, T: std::ops::Sub, diff --git a/egui/src/input.rs b/egui/src/input.rs index 5aaf14a9..9c338feb 100644 --- a/egui/src/input.rs +++ b/egui/src/input.rs @@ -1,6 +1,6 @@ //! 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) const MAX_CLICK_DIST: f32 = 6.0; @@ -128,7 +128,7 @@ pub struct MouseInput { /// Recent movement of the mouse. /// Used for calculating velocity of mouse pointer. - pub pos_tracker: MovementTracker, + pos_history: History, } impl Default for MouseInput { @@ -145,7 +145,7 @@ impl Default for MouseInput { press_origin: None, delta: 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 { // Start of a drag: we want to track the velocity for during the drag // and ignore any incoming movement - self.pos_tracker.clear(); + self.pos_history.clear(); } 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 { // 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 // the user tried to throw } - self.pos_tracker.flush(new.time); - let velocity = if self.pos_tracker.len() >= 3 && self.pos_tracker.dt() > 0.01 { - self.pos_tracker.velocity().unwrap_or_default() + self.pos_history.flush(new.time); + let velocity = if self.pos_history.len() >= 3 && self.pos_history.duration() > 0.01 { + self.pos_history.velocity().unwrap_or_default() } else { Vec2::default() }; @@ -325,7 +325,7 @@ impl MouseInput { press_origin, delta, velocity, - pos_tracker: self.pos_tracker, + pos_history: self.pos_history, } } } @@ -411,7 +411,7 @@ impl MouseInput { press_origin, delta, velocity, - pos_tracker: _, + pos_history: _, } = self; ui.label(format!("down: {}", down)); diff --git a/egui/src/layout.rs b/egui/src/layout.rs index 20dbc739..1004f177 100644 --- a/egui/src/layout.rs +++ b/egui/src/layout.rs @@ -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. diff --git a/egui/src/lib.rs b/egui/src/lib.rs index b015f05b..1593cf63 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -44,12 +44,14 @@ rust_2018_idioms, )] +pub mod align; mod animation_manager; pub mod app; pub(crate) mod cache; pub mod containers; mod context; pub mod demos; +mod history; mod id; mod input; mod introspection; @@ -66,9 +68,11 @@ mod ui; pub mod widgets; pub use { + align::Align, containers::*, context::Context, demos::DemoApp, + history::History, id::Id, input::*, layers::*, diff --git a/egui/src/math/mod.rs b/egui/src/math/mod.rs index cd68c38c..c6ddff28 100644 --- a/egui/src/math/mod.rs +++ b/egui/src/math/mod.rs @@ -4,13 +4,12 @@ use std::ops::{Add, Mul, RangeInclusive}; // ---------------------------------------------------------------------------- -mod movement_tracker; mod pos2; mod rect; pub mod smart_aim; mod vec2; -pub use {movement_tracker::*, pos2::*, rect::*, vec2::*}; +pub use {pos2::*, rect::*, vec2::*}; // ---------------------------------------------------------------------------- diff --git a/egui/src/math/rect.rs b/egui/src/math/rect.rs index 152d5fcb..0b43e709 100644 --- a/egui/src/math/rect.rs +++ b/egui/src/math/rect.rs @@ -147,6 +147,10 @@ impl Rect { self.min.y..=self.max.y } + pub fn bottom_up_range(&self) -> RangeInclusive { + self.max.y..=self.min.y + } + pub fn is_empty(&self) -> bool { self.max.x < self.min.x || self.max.y < self.min.y } diff --git a/egui/src/paint/command.rs b/egui/src/paint/command.rs index 0753af76..5de3da94 100644 --- a/egui/src/paint/command.rs +++ b/egui/src/paint/command.rs @@ -1,6 +1,9 @@ use { - super::{font::Galley, fonts::TextStyle, Srgba, Triangles}, - crate::math::{Pos2, Rect}, + super::{font::Galley, fonts::TextStyle, Fonts, Srgba, Triangles}, + crate::{ + align::{anchor_rect, Align}, + math::{Pos2, Rect}, + }, }; // TODO: rename, e.g. `paint::Cmd`? @@ -86,6 +89,25 @@ impl PaintCmd { stroke: stroke.into(), } } + + pub fn text( + fonts: &Fonts, + pos: Pos2, + anchor: (Align, Align), + text: impl Into, + 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)] diff --git a/egui/src/painter.rs b/egui/src/painter.rs index e6139072..0eed68d8 100644 --- a/egui/src/painter.rs +++ b/egui/src/painter.rs @@ -1,11 +1,12 @@ use std::sync::Arc; use crate::{ - anchor_rect, color, + align::{anchor_rect, Align, LEFT_TOP}, + color, layers::PaintCmdIdx, math::{Pos2, Rect, Vec2}, 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. @@ -116,18 +117,16 @@ impl Painter { impl Painter { pub fn debug_rect(&mut self, rect: Rect, color: Srgba, text: impl Into) { self.rect_stroke(rect, 0.0, (1.0, color)); - let anchor = (Align::Min, Align::Min); 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) { let text = text.into(); - let anchor = (Align::Min, Align::Min); let text_style = TextStyle::Monospace; let font = &self.fonts()[text_style]; 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 { rect: rect.expand(2.0), corner_radius: 0.0, diff --git a/egui/src/style.rs b/egui/src/style.rs index c397c877..261dc8a3 100644 --- a/egui/src/style.rs +++ b/egui/src/style.rs @@ -23,11 +23,18 @@ pub struct Style { } impl Style { - // TODO: rename style.interact() to maybe... `style.response_visuals` ? - /// Use this style for interactive things + // TODO: rename style.interact() to maybe... `style.interactive` ? + /// 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 { self.visuals.widgets.style(response) } + + /// Style to use for non-interactive widgets. + pub fn noninteractive(&self) -> &WidgetVisuals { + &self.visuals.widgets.noninteractive + } } #[derive(Clone, Debug)] @@ -116,6 +123,10 @@ pub struct Visuals { } impl Visuals { + pub fn noninteractive(&self) -> &WidgetVisuals { + &self.widgets.noninteractive + } + pub fn text_color(&self) -> Srgba { self.widgets.noninteractive.text_color() } diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 3f4b17ba..4069cd4b 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -478,6 +478,11 @@ impl Ui { self.add(label.into().heading()) } + /// Shortcut for `add(Label::new(text).monospace())` + pub fn monospace(&mut self, label: impl Into