diff --git a/Cargo.lock b/Cargo.lock index eb234a9e..be7f1a25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -840,6 +840,7 @@ dependencies = [ "ahash", "atomic_refcell", "emath", + "ordered-float", "parking_lot", "rusttype", "serde", @@ -1618,6 +1619,15 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "ordered-float" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "766f840da25490628d8e63e529cd21c014f6600c6b8517add12a6fa6167a6218" +dependencies = [ + "num-traits", +] + [[package]] name = "osmesa-sys" version = "0.1.2" diff --git a/README.md b/README.md index 539b2265..c942eead 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,9 @@ [![Build Status](https://github.com/emilk/egui/workflows/CI/badge.svg)](https://github.com/emilk/egui/actions?workflow=CI) ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) ![Apache](https://img.shields.io/badge/license-Apache-blue.svg) -**dependencies**: [`rusttype`](https://crates.io/crates/rusttype) [`atomic_refcell`](https://crates.io/crates/atomic_refcell) [`ahash`](https://crates.io/crates/ahash) -egui is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and in your favorite game engine (or will soon). +egui is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations) (or will soon). egui aims to be the easiest-to-use Rust GUI libary, and the simplest way to make a web app in Rust. @@ -80,7 +79,7 @@ ui.label(format!("Hello '{}', age {}", name, age)); * Extensible: [easy to write your own widgets for egui](https://github.com/emilk/egui/blob/master/egui_demo_lib/src/apps/demo/toggle_switch.rs) * Modular: You should be able to use small parts of egui and combine them in new ways * Safe: there is no `unsafe` code in egui -* Minimal dependencies: [`rusttype`](https://crates.io/crates/rusttype), [`atomic_refcell`](https://crates.io/crates/atomic_refcell) and [`ahash`](https://crates.io/crates/ahash). +* Minimal dependencies: [`ahash`](https://crates.io/crates/ahash) [`atomic_refcell`](https://crates.io/crates/atomic_refcell) [`ordered-float`](https://crates.io/crates/) [`rusttype`](https://crates.io/crates/rusttype). egui is *not* a framework. egui is a library you call into, not an environment you program for. diff --git a/egui/src/context.rs b/egui/src/context.rs index fe1bc9b1..d348adcc 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -582,6 +582,8 @@ impl Context { self.memory() .end_frame(&self.input, &self.frame_state().used_ids); + self.fonts().end_frame(); + let mut output: Output = std::mem::take(&mut self.output()); if self.repaint_requests.load(SeqCst) > 0 { self.repaint_requests.fetch_sub(1, SeqCst); @@ -765,7 +767,7 @@ impl Context { "Wants keyboard input: {}", self.wants_keyboard_input() )) - .on_hover_text("Is egui currently listening for text input"); + .on_hover_text("Is egui currently listening for text input?"); ui.label(format!( "keyboard focus widget: {}", self.memory() @@ -776,7 +778,14 @@ impl Context { .map(Id::short_debug_format) .unwrap_or_default() )) - .on_hover_text("Is egui currently listening for text input"); + .on_hover_text("Is egui currently listening for text input?"); + ui.advance_cursor(16.0); + + ui.label(format!( + "There are {} text galleys in the layout cache", + self.fonts().num_galleys_in_cache() + )) + .on_hover_text("This is approximately the number of text strings on screen"); ui.advance_cursor(16.0); CollapsingHeader::new("📥 Input") diff --git a/epaint/Cargo.toml b/epaint/Cargo.toml index 56e78e16..77f5e09e 100644 --- a/epaint/Cargo.toml +++ b/epaint/Cargo.toml @@ -26,6 +26,7 @@ emath = { version = "0.10.0", path = "../emath" } ahash = { version = "0.7", features = ["std"], default-features = false } atomic_refcell = { version = "0.1", optional = true } # Used instead of parking_lot when you are always using epaint in a single thread. About as fast as parking_lot. Panics on multi-threaded use. +ordered-float = "2" parking_lot = { version = "0.11", optional = true } # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios. rusttype = "0.9" serde = { version = "1", features = ["derive"], optional = true } diff --git a/epaint/src/text/fonts.rs b/epaint/src/text/fonts.rs index d7b5a5a2..4aeef40e 100644 --- a/epaint/src/text/fonts.rs +++ b/epaint/src/text/fonts.rs @@ -1,5 +1,5 @@ use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, hash::{Hash, Hasher}, sync::Arc, }; @@ -183,6 +183,8 @@ pub struct Fonts { /// Copy of the texture in the texture atlas. /// This is so we can return a reference to it (the texture atlas is behind a lock). buffered_texture: Mutex>, + + galley_cache: Mutex, } impl Fonts { @@ -241,6 +243,7 @@ impl Fonts { fonts, atlas, buffered_texture: Default::default(), //atlas.lock().texture().clone(); + galley_cache: Default::default(), } } @@ -286,7 +289,14 @@ impl Fonts { /// Most often you probably want `\n` to produce a new row, /// and so [`Self::layout_no_wrap`] may be a better choice. pub fn layout_single_line(&self, text_style: TextStyle, text: String) -> Galley { - self.fonts[&text_style].layout_single_line(text) + self.galley_cache.lock().layout( + &self.fonts, + LayoutJob { + text_style, + text, + layout_params: LayoutParams::SingleLine, + }, + ) } /// Always returns at least one row. @@ -315,12 +325,27 @@ impl Fonts { first_row_indentation: f32, max_width_in_points: f32, ) -> Galley { - self.fonts[&text_style].layout_multiline_with_indentation_and_max_width( - text, - first_row_indentation, - max_width_in_points, + self.galley_cache.lock().layout( + &self.fonts, + LayoutJob { + text_style, + text, + layout_params: LayoutParams::Multiline { + first_row_indentation: first_row_indentation.into(), + max_width_in_points: max_width_in_points.into(), + }, + }, ) } + + pub fn num_galleys_in_cache(&self) -> usize { + self.galley_cache.lock().num_galleys_in_cache() + } + + /// Must be called once per frame to clear the [`Galley`] cache. + pub fn end_frame(&self) { + self.galley_cache.lock().end_frame() + } } impl std::ops::Index for Fonts { @@ -333,10 +358,89 @@ impl std::ops::Index for Fonts { // ---------------------------------------------------------------------------- +#[derive(Clone, Copy, Eq, PartialEq, Hash)] +enum LayoutParams { + SingleLine, + Multiline { + first_row_indentation: ordered_float::OrderedFloat, + max_width_in_points: ordered_float::OrderedFloat, + }, +} + +#[derive(Clone, Eq, PartialEq, Hash)] +struct LayoutJob { + text_style: TextStyle, + layout_params: LayoutParams, + text: String, +} + +struct CachedGalley { + /// When it was last used + last_used: u32, + galley: Galley, // TODO: use an Arc instead! +} + +#[derive(Default)] +struct GalleyCache { + /// Frame counter used to do garbage collection on the cache + generation: u32, + cache: HashMap, +} + +impl GalleyCache { + fn layout(&mut self, fonts: &BTreeMap, job: LayoutJob) -> Galley { + if let Some(cached) = self.cache.get_mut(&job) { + cached.last_used = self.generation; + cached.galley.clone() + } else { + let LayoutJob { + text_style, + layout_params, + text, + } = job.clone(); + let font = &fonts[&text_style]; + let galley = match layout_params { + LayoutParams::SingleLine => font.layout_single_line(text), + LayoutParams::Multiline { + first_row_indentation, + max_width_in_points, + } => font.layout_multiline_with_indentation_and_max_width( + text, + first_row_indentation.into_inner(), + max_width_in_points.into_inner(), + ), + }; + self.cache.insert( + job, + CachedGalley { + last_used: self.generation, + galley: galley.clone(), + }, + ); + galley + } + } + + pub fn num_galleys_in_cache(&self) -> usize { + self.cache.len() + } + + /// Must be called once per frame to clear the [`Galley`] cache. + pub fn end_frame(&mut self) { + let current_generation = self.generation; + self.cache.retain(|_key, cached| { + cached.last_used == current_generation // only keep those that were used this frame + }); + self.generation = self.generation.wrapping_add(1); + } +} + +// ---------------------------------------------------------------------------- + struct FontImplCache { atlas: Arc>, pixels_per_point: f32, - rusttype_fonts: std::collections::BTreeMap>>, + rusttype_fonts: BTreeMap>>, /// Map font names and size to the cached `FontImpl`. /// Can't have f32 in a HashMap or BTreeMap, so let's do a linear search