From fa43d16c416889fd749aca9a3985c1317da24093 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 24 Jan 2022 14:32:36 +0100 Subject: [PATCH] Choose your own font and size (#1154) * Refactor text layout: don't need &Fonts in all functions * Replace indexing in Fonts with member function * Wrap Fonts in a Mutex * Remove mutex for Font::glyph_info_cache * Remove RwLock around Font::characters * Put FontsImpl and GalleyCache behind the same Mutex * Round font sizes to whole pixels before deduplicating them * Make TextStyle !Copy * Implement user-named TextStyle:s * round font size earlier * Cache fonts based on family and size * Move TextStyle into egui and Style * Remove body_text_style * Query graphics about max texture size and use that as font atlas size * Recreate texture atlas when it is getting full --- CHANGELOG.md | 10 + eframe/CHANGELOG.md | 2 +- eframe/examples/custom_font.rs | 4 +- eframe/examples/file_dialog.rs | 2 +- egui-winit/CHANGELOG.md | 5 +- egui-winit/src/epi.rs | 3 +- egui-winit/src/lib.rs | 15 +- egui/src/containers/collapsing_header.rs | 2 +- egui/src/containers/scroll_area.rs | 2 +- egui/src/containers/window.rs | 5 +- egui/src/context.rs | 63 +- egui/src/data/input.rs | 13 + egui/src/input_state.rs | 7 +- egui/src/introspection.rs | 46 +- egui/src/layout.rs | 1 - egui/src/lib.rs | 10 +- egui/src/menu.rs | 3 +- egui/src/painter.rs | 27 +- egui/src/style.rs | 201 +++++- egui/src/ui.rs | 23 +- egui/src/widget_text.rs | 105 ++- egui/src/widgets/drag_value.rs | 2 +- egui/src/widgets/label.rs | 4 +- egui/src/widgets/plot/items/mod.rs | 8 +- egui/src/widgets/plot/legend.rs | 15 +- egui/src/widgets/plot/mod.rs | 5 +- egui/src/widgets/slider.rs | 4 +- egui/src/widgets/text_edit/builder.rs | 34 +- egui_demo_lib/benches/benchmark.rs | 45 +- egui_demo_lib/src/apps/demo/code_editor.rs | 4 +- egui_demo_lib/src/apps/demo/code_example.rs | 3 +- egui_demo_lib/src/apps/demo/font_book.rs | 47 +- .../src/apps/demo/misc_demo_window.rs | 22 +- egui_demo_lib/src/apps/demo/plot_demo.rs | 11 +- egui_demo_lib/src/apps/demo/scrolling.rs | 10 +- egui_demo_lib/src/apps/http_app.rs | 4 +- .../src/easy_mark/easy_mark_editor.rs | 4 +- .../src/easy_mark/easy_mark_highlighter.rs | 42 +- .../src/easy_mark/easy_mark_viewer.rs | 11 +- egui_demo_lib/src/frame_history.rs | 2 +- egui_demo_lib/src/syntax_highlighting.rs | 66 +- egui_demo_lib/src/wrap_app.rs | 2 +- egui_glium/src/epi_backend.rs | 1 + egui_glium/src/lib.rs | 8 +- egui_glium/src/painter.rs | 9 + egui_glow/CHANGELOG.md | 4 +- egui_glow/src/epi_backend.rs | 1 + egui_glow/src/lib.rs | 17 +- egui_glow/src/painter.rs | 9 + egui_web/CHANGELOG.md | 6 +- egui_web/src/backend.rs | 2 + egui_web/src/glow_wrapping.rs | 4 + egui_web/src/painter.rs | 3 + egui_web/src/webgl1.rs | 15 + egui_web/src/webgl2.rs | 15 + emath/src/align.rs | 2 - epaint/CHANGELOG.md | 7 +- epaint/src/color.rs | 10 +- epaint/src/lib.rs | 2 +- epaint/src/shape.rs | 6 +- epaint/src/text/font.rs | 80 +-- epaint/src/text/fonts.rs | 617 +++++++++++------- epaint/src/text/mod.rs | 2 +- epaint/src/text/text_layout.rs | 94 ++- epaint/src/text/text_layout_types.rs | 28 +- epaint/src/texture_atlas.rs | 33 +- epi/src/lib.rs | 2 +- 67 files changed, 1231 insertions(+), 640 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7545fa6e..3e5dd7fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w ## Unreleased ### Added โญ +* Much improved font selection ([#1154](https://github.com/emilk/egui/pull/1154)): + * You can now select any font size and family using `RichText::size` amd `RichText::family` and the new `FontId`. + * Easily change text styles with `Style::text_styles`. + * Added `Ui::text_style_height`. + * Added `TextStyle::resolve`. * `Context::load_texture` to convert an image into a texture which can be displayed using e.g. `ui.image(texture, size)` ([#1110](https://github.com/emilk/egui/pull/1110)). * Added `Ui::add_visible` and `Ui::add_visible_ui`. * Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)) @@ -23,6 +28,10 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * For integrations: * `FontImage` has been replaced by `TexturesDelta` (found in `Output`), describing what textures were loaded and freed each frame ([#1110](https://github.com/emilk/egui/pull/1110)). * The painter must support partial texture updates ([#1149](https://github.com/emilk/egui/pull/1149)). + * Added `RawInput::max_texture_side` which should be filled in with e.g. `GL_MAX_TEXTURE_SIZE` ([#1154](https://github.com/emilk/egui/pull/1154)). +* Replaced `Style::body_text_style` with more generic `Style::text_styles` ([#1154](https://github.com/emilk/egui/pull/1154)). +* `TextStyle` is no longer `Copy` ([#1154](https://github.com/emilk/egui/pull/1154)). +* Replaced `TextEdit::text_style` with `TextEdit::font` ([#1154](https://github.com/emilk/egui/pull/1154)). ### Fixed ๐Ÿ› * Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)) @@ -533,6 +542,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * Optimization: coarse culling in the tessellator * CHANGED: switch argument order of `ui.checkbox` and `ui.radio` + ## 0.1.4 - 2020-09-08 This is when I started the CHANGELOG.md, after almost two years of development. Better late than never. diff --git a/eframe/CHANGELOG.md b/eframe/CHANGELOG.md index d28f9a0a..45ed02fe 100644 --- a/eframe/CHANGELOG.md +++ b/eframe/CHANGELOG.md @@ -10,7 +10,7 @@ NOTE: [`egui_web`](../egui_web/CHANGELOG.md), [`egui-winit`](../egui-winit/CHANG * The default web painter is now `egui_glow` (instead of WebGL) ([#1020](https://github.com/emilk/egui/pull/1020)). * Fix horizontal scrolling direction on Linux. * Added `App::on_exit_event` ([#1038](https://github.com/emilk/egui/pull/1038)) -* Shift-scroll will now result in horizontal scrolling on all platforms ((#1136)[https://github.com/emilk/egui/pull/1136]). +* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)). ## 0.16.0 - 2021-12-29 diff --git a/eframe/examples/custom_font.rs b/eframe/examples/custom_font.rs index 28af4c06..8ba2ced4 100644 --- a/eframe/examples/custom_font.rs +++ b/eframe/examples/custom_font.rs @@ -35,14 +35,14 @@ impl epi::App for MyApp { // Put my font first (highest priority) for proportional text: fonts - .fonts_for_family + .families .entry(egui::FontFamily::Proportional) .or_default() .insert(0, "my_font".to_owned()); // Put my font as last fallback for monospace: fonts - .fonts_for_family + .families .entry(egui::FontFamily::Monospace) .or_default() .push("my_font".to_owned()); diff --git a/eframe/examples/file_dialog.rs b/eframe/examples/file_dialog.rs index b7e17e47..f4b2f5a9 100644 --- a/eframe/examples/file_dialog.rs +++ b/eframe/examples/file_dialog.rs @@ -82,7 +82,7 @@ impl MyApp { screen_rect.center(), Align2::CENTER_CENTER, text, - TextStyle::Heading, + TextStyle::Heading.resolve(&ctx.style()), Color32::WHITE, ); } diff --git a/egui-winit/CHANGELOG.md b/egui-winit/CHANGELOG.md index 55d50bda..258e6a85 100644 --- a/egui-winit/CHANGELOG.md +++ b/egui-winit/CHANGELOG.md @@ -4,9 +4,10 @@ All notable changes to the `egui-winit` integration will be noted in this file. ## Unreleased -* Fix horizontal scrolling direction on Linux. +* Fixed horizontal scrolling direction on Linux. * Replaced `std::time::Instant` with `instant::Instant` for WebAssembly compatability ([#1023](https://github.com/emilk/egui/pull/1023)) -* Shift-scroll will now result in horizontal scrolling on all platforms ((#1136)[https://github.com/emilk/egui/pull/1136]). +* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)). +* Require knowledge about max texture side (e.g. `GL_MAX_TEXTURE_SIZE`)) ([#1154](https://github.com/emilk/egui/pull/1154)). ## 0.16.0 - 2021-12-29 diff --git a/egui-winit/src/epi.rs b/egui-winit/src/epi.rs index 869921ab..46f0eed1 100644 --- a/egui-winit/src/epi.rs +++ b/egui-winit/src/epi.rs @@ -198,6 +198,7 @@ pub struct EpiIntegration { impl EpiIntegration { pub fn new( integration_name: &'static str, + max_texture_side: usize, window: &winit::window::Window, repaint_signal: std::sync::Arc, persistence: crate::epi::Persistence, @@ -223,7 +224,7 @@ impl EpiIntegration { frame, persistence, egui_ctx, - egui_winit: crate::State::new(window), + egui_winit: crate::State::new(max_texture_side, window), app, quit: false, }; diff --git a/egui-winit/src/lib.rs b/egui-winit/src/lib.rs index 69c15c1b..a3a98150 100644 --- a/egui-winit/src/lib.rs +++ b/egui-winit/src/lib.rs @@ -129,17 +129,22 @@ pub struct State { } impl State { - /// Initialize with the native `pixels_per_point` (dpi scaling). - pub fn new(window: &winit::window::Window) -> Self { - Self::from_pixels_per_point(native_pixels_per_point(window)) + /// Initialize with: + /// * `max_texture_side`: e.g. `GL_MAX_TEXTURE_SIZE` + /// * the native `pixels_per_point` (dpi scaling). + pub fn new(max_texture_side: usize, window: &winit::window::Window) -> Self { + Self::from_pixels_per_point(max_texture_side, native_pixels_per_point(window)) } - /// Initialize with a given dpi scaling. - pub fn from_pixels_per_point(pixels_per_point: f32) -> Self { + /// Initialize with: + /// * `max_texture_side`: e.g. `GL_MAX_TEXTURE_SIZE` + /// * the given `pixels_per_point` (dpi scaling). + pub fn from_pixels_per_point(max_texture_side: usize, pixels_per_point: f32) -> Self { Self { start_time: instant::Instant::now(), egui_input: egui::RawInput { pixels_per_point: Some(pixels_per_point), + max_texture_side, ..Default::default() }, pointer_pos_in_points: None, diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index 33d27301..652d9956 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -1,7 +1,7 @@ use std::hash::Hash; use crate::*; -use epaint::{Shape, TextStyle}; +use epaint::Shape; #[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/egui/src/containers/scroll_area.rs b/egui/src/containers/scroll_area.rs index cf9f5e04..83c7e3b4 100644 --- a/egui/src/containers/scroll_area.rs +++ b/egui/src/containers/scroll_area.rs @@ -374,7 +374,7 @@ impl ScrollArea { /// ``` /// # egui::__run_test_ui(|ui| { /// let text_style = egui::TextStyle::Body; - /// let row_height = ui.fonts()[text_style].row_height(); + /// let row_height = ui.text_style_height(&text_style); /// // let row_height = ui.spacing().interact_size.y; // if you are adding buttons instead of labels. /// let total_rows = 10_000; /// egui::ScrollArea::vertical().show_rows(ui, row_height, total_rows, |ui, row_range| { diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index 1a7d2382..c6d2cae8 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -296,7 +296,8 @@ impl<'open> Window<'open> { .and_then(|window_interaction| { // Calculate roughly how much larger the window size is compared to the inner rect let title_bar_height = if with_title_bar { - title.font_height(ctx) + title_content_spacing + let style = ctx.style(); + title.font_height(&ctx.fonts(), &style) + title_content_spacing } else { 0.0 }; @@ -764,7 +765,7 @@ fn show_title_bar( ) -> TitleBar { let inner_response = ui.horizontal(|ui| { let height = title - .font_height(ui.ctx()) + .font_height(&ui.fonts(), ui.style()) .max(ui.spacing().interact_size.y); ui.set_min_height(height); diff --git a/egui/src/context.rs b/egui/src/context.rs index a982ebfa..d7701ead 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -62,7 +62,7 @@ impl ContextImpl { self.input = input.begin_frame(new_raw_input); self.frame_state.begin_frame(&self.input); - self.update_fonts_mut(self.input.pixels_per_point()); + self.update_fonts_mut(); // Ensure we register the background area so panels and background ui can catch clicks: let screen_rect = self.input.screen_rect(); @@ -77,27 +77,21 @@ impl ContextImpl { } /// Load fonts unless already loaded. - fn update_fonts_mut(&mut self, pixels_per_point: f32) { - let new_font_definitions = self.memory.new_font_definitions.take(); + fn update_fonts_mut(&mut self) { + let pixels_per_point = self.input.pixels_per_point(); + let max_texture_side = self.input.raw.max_texture_side; - let pixels_per_point_changed = match &self.fonts { - None => true, - Some(current_fonts) => { - (current_fonts.pixels_per_point() - pixels_per_point).abs() > 1e-3 - } - }; - - if self.fonts.is_none() || new_font_definitions.is_some() || pixels_per_point_changed { - self.fonts = Some(Fonts::new( - pixels_per_point, - new_font_definitions.unwrap_or_else(|| { - self.fonts - .as_ref() - .map(|font| font.definitions().clone()) - .unwrap_or_default() - }), - )); + if let Some(font_definitions) = self.memory.new_font_definitions.take() { + let fonts = Fonts::new(pixels_per_point, max_texture_side, font_definitions); + self.fonts = Some(fonts); } + + let fonts = self.fonts.get_or_insert_with(|| { + let font_definitions = FontDefinitions::default(); + Fonts::new(pixels_per_point, max_texture_side, font_definitions) + }); + + fonts.begin_frame(pixels_per_point, max_texture_side); } } @@ -521,7 +515,7 @@ impl Context { pub fn set_fonts(&self, font_definitions: FontDefinitions) { if let Some(current_fonts) = &*self.fonts_mut() { // NOTE: this comparison is expensive since it checks TTF data for equality - if current_fonts.definitions() == &font_definitions { + if current_fonts.lock().fonts.definitions() == &font_definitions { return; // no change - save us from reloading font textures } } @@ -700,8 +694,6 @@ impl Context { self.request_repaint(); } - self.fonts().end_frame(); - { let ctx_impl = &mut *self.write(); ctx_impl @@ -953,16 +945,6 @@ impl Context { self.style_ui(ui); }); - CollapsingHeader::new("๐Ÿ”  Fonts") - .default_open(false) - .show(ui, |ui| { - let mut font_definitions = self.fonts().definitions().clone(); - font_definitions.ui(ui); - let font_image_size = self.fonts().font_image_size(); - crate::introspection::font_texture_ui(ui, font_image_size); - self.set_fonts(font_definitions); - }); - CollapsingHeader::new("โœ’ Painting") .default_open(true) .show(ui, |ui| { @@ -1039,6 +1021,13 @@ impl Context { .show(ui, |ui| { self.texture_ui(ui); }); + + CollapsingHeader::new("๐Ÿ”  Font texture") + .default_open(false) + .show(ui, |ui| { + let font_image_size = self.fonts().font_image_size(); + crate::introspection::font_texture_ui(ui, font_image_size); + }); } /// Show stats about the allocated textures. @@ -1080,8 +1069,12 @@ impl Context { size *= (max_preview_size.x / size.x).min(1.0); size *= (max_preview_size.y / size.y).min(1.0); ui.image(texture_id, size).on_hover_ui(|ui| { - // show full size on hover - ui.image(texture_id, Vec2::new(w as f32, h as f32)); + // show larger on hover + let max_size = 0.5 * ui.ctx().input().screen_rect().size(); + let mut size = Vec2::new(w as f32, h as f32); + size *= max_size.x / size.x.max(max_size.x); + size *= max_size.y / size.y.max(max_size.y); + ui.image(texture_id, size); }); ui.label(format!("{} x {}", w, h)); diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index 6de259a2..21234b65 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -28,6 +28,13 @@ pub struct RawInput { /// Set this the first frame, whenever it changes, or just on every frame. pub pixels_per_point: Option, + /// Maximum size of one side of the font texture. + /// + /// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`. + /// + /// The default is a very small (but very portable) 2048. + pub max_texture_side: usize, + /// Monotonically increasing time, in seconds. Relative to whatever. Used for animations. /// If `None` is provided, egui will assume a time delta of `predicted_dt` (default 1/60 seconds). pub time: Option, @@ -62,6 +69,7 @@ impl Default for RawInput { Self { screen_rect: None, pixels_per_point: None, + max_texture_side: 2048, time: None, predicted_dt: 1.0 / 60.0, modifiers: Modifiers::default(), @@ -81,6 +89,7 @@ impl RawInput { RawInput { screen_rect: self.screen_rect.take(), pixels_per_point: self.pixels_per_point.take(), + max_texture_side: self.max_texture_side, time: self.time.take(), predicted_dt: self.predicted_dt, modifiers: self.modifiers, @@ -95,6 +104,7 @@ impl RawInput { let Self { screen_rect, pixels_per_point, + max_texture_side, time, predicted_dt, modifiers, @@ -105,6 +115,7 @@ impl RawInput { self.screen_rect = screen_rect.or(self.screen_rect); self.pixels_per_point = pixels_per_point.or(self.pixels_per_point); + self.max_texture_side = max_texture_side; // use latest self.time = time; // use latest time self.predicted_dt = predicted_dt; // use latest dt self.modifiers = modifiers; // use latest @@ -357,6 +368,7 @@ impl RawInput { let Self { screen_rect, pixels_per_point, + max_texture_side, time, predicted_dt, modifiers, @@ -370,6 +382,7 @@ impl RawInput { .on_hover_text( "Also called HDPI factor.\nNumber of physical pixels per each logical pixel.", ); + ui.label(format!("max_texture_side: {}", max_texture_side)); if let Some(time) = time { ui.label(format!("time: {:.3} s", time)); } else { diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index 25e266ae..6395b54f 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -700,7 +700,12 @@ impl InputState { events, } = self; - ui.style_mut().body_text_style = epaint::TextStyle::Monospace; + ui.style_mut() + .text_styles + .get_mut(&crate::TextStyle::Body) + .unwrap() + .family = crate::FontFamily::Monospace; + ui.collapsing("Raw Input", |ui| raw.ui(ui)); crate::containers::CollapsingHeader::new("๐Ÿ–ฑ Pointer") diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 04525119..eb6b7c4a 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -1,6 +1,27 @@ -//! uis for egui types. +//! Showing UI:s for egui/epaint types. use crate::*; +pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) { + let families = ui.fonts().families(); + ui.horizontal(|ui| { + for alternative in families { + let text = alternative.to_string(); + ui.radio_value(font_family, alternative, text); + } + }); +} + +pub fn font_id_ui(ui: &mut Ui, font_id: &mut FontId) { + let families = ui.fonts().families(); + ui.horizontal(|ui| { + ui.add(Slider::new(&mut font_id.size, 4.0..=40.0).max_decimals(0)); + for alternative in families { + let text = alternative.to_string(); + ui.radio_value(&mut font_id.family, alternative, text); + } + }); +} + // Show font texture in demo Ui pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Response { use epaint::Mesh; @@ -55,33 +76,16 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo .response } -impl Widget for &mut epaint::text::FontDefinitions { - fn ui(self, ui: &mut Ui) -> Response { - ui.vertical(|ui| { - for (text_style, (_family, size)) in self.family_and_size.iter_mut() { - // TODO: radio button for family - ui.add( - Slider::new(size, 4.0..=40.0) - .max_decimals(0) - .text(format!("{:?}", text_style)), - ); - } - crate::reset_button(ui, self); - }) - .response - } -} - impl Widget for &epaint::stats::PaintStats { fn ui(self, ui: &mut Ui) -> Response { ui.vertical(|ui| { ui.label( "egui generates intermediate level shapes like circles and text. \ - These are later tessellated into triangles.", + These are later tessellated into triangles.", ); ui.add_space(10.0); - ui.style_mut().body_text_style = TextStyle::Monospace; + ui.style_mut().override_text_style = Some(TextStyle::Monospace); let epaint::stats::PaintStats { shapes, @@ -124,7 +128,7 @@ impl Widget for &epaint::stats::PaintStats { } } -pub fn label(ui: &mut Ui, alloc_info: &epaint::stats::AllocInfo, what: &str) -> Response { +fn label(ui: &mut Ui, alloc_info: &epaint::stats::AllocInfo, what: &str) -> Response { ui.add(Label::new(alloc_info.format(what)).wrap(false)) } diff --git a/egui/src/layout.rs b/egui/src/layout.rs index 433242ca..a9642c86 100644 --- a/egui/src/layout.rs +++ b/egui/src/layout.rs @@ -77,7 +77,6 @@ impl Region { /// Layout direction, one of `LeftToRight`, `RightToLeft`, `TopDown`, `BottomUp`. #[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 Direction { LeftToRight, RightToLeft, diff --git a/egui/src/lib.rs b/egui/src/lib.rs index 59528e7e..a3cacda8 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -364,7 +364,7 @@ mod frame_state; pub(crate) mod grid; mod id; mod input_state; -mod introspection; +pub mod introspection; pub mod layers; mod layout; mod memory; @@ -385,7 +385,7 @@ pub use epaint::emath; pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rect, Vec2}; pub use epaint::{ color, mutex, - text::{FontData, FontDefinitions, FontFamily, TextStyle}, + text::{FontData, FontDefinitions, FontFamily, FontId}, textures::TexturesDelta, AlphaImage, ClippedMesh, Color32, ColorImage, ImageData, Rgba, Shape, Stroke, TextureHandle, TextureId, @@ -394,7 +394,7 @@ pub use epaint::{ pub mod text { pub use epaint::text::{ FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TextFormat, - TextStyle, TAB_SIZE, + TAB_SIZE, }; } @@ -414,7 +414,7 @@ pub use { painter::Painter, response::{InnerResponse, Response}, sense::Sense, - style::{Style, Visuals}, + style::{FontSelection, Style, TextStyle, Visuals}, text::{Galley, TextFormat}, ui::Ui, widget_text::{RichText, WidgetText}, @@ -511,7 +511,7 @@ macro_rules! egui_assert { // ---------------------------------------------------------------------------- -/// egui supports around 1216 emojis in total. +/// The default egui fonts supports around 1216 emojis in total. /// Here are some of the most useful: /// โˆžโŠ—โŽ—โŽ˜โŽ™โโดโตโถโท /// โฉโชโญโฎโธโนโบโ– โ–ถ๐Ÿ“พ๐Ÿ”€๐Ÿ”๐Ÿ”ƒ diff --git a/egui/src/menu.rs b/egui/src/menu.rs index c2860187..4499ec05 100644 --- a/egui/src/menu.rs +++ b/egui/src/menu.rs @@ -414,7 +414,8 @@ impl SubMenuButton { let button_padding = ui.spacing().button_padding; let total_extra = button_padding + button_padding; let text_available_width = ui.available_width() - total_extra.x; - let text_galley = text.into_galley(ui, Some(true), text_available_width, text_style); + let text_galley = + text.into_galley(ui, Some(true), text_available_width, text_style.clone()); let icon_available_width = text_available_width - text_galley.size().x; let icon_galley = icon.into_galley(ui, Some(true), icon_available_width, text_style); diff --git a/egui/src/painter.rs b/egui/src/painter.rs index 6348a1f8..111e130e 100644 --- a/egui/src/painter.rs +++ b/egui/src/painter.rs @@ -1,11 +1,11 @@ use crate::{ emath::{Align2, Pos2, Rect, Vec2}, layers::{LayerId, PaintList, ShapeIdx}, - Color32, Context, + Color32, Context, FontId, }; use epaint::{ mutex::{Arc, RwLockReadGuard, RwLockWriteGuard}, - text::{Fonts, Galley, TextStyle}, + text::{Fonts, Galley}, CircleShape, RectShape, Shape, Stroke, TextShape, }; @@ -30,6 +30,7 @@ pub struct Painter { } impl Painter { + /// Create a painter to a specific layer within a certain clip rectangle. pub fn new(ctx: Context, layer_id: LayerId, clip_rect: Rect) -> Self { Self { ctx, @@ -39,6 +40,7 @@ impl Painter { } } + /// Redirect where you are painting. #[must_use] pub fn with_layer_id(self, layer_id: LayerId) -> Self { Self { @@ -49,7 +51,7 @@ impl Painter { } } - /// redirect + /// Redirect where you are painting. pub fn set_layer_id(&mut self, layer_id: LayerId) { self.layer_id = layer_id; } @@ -194,12 +196,11 @@ impl Painter { #[allow(clippy::needless_pass_by_value)] pub fn debug_rect(&mut self, rect: Rect, color: Color32, text: impl ToString) { self.rect_stroke(rect, 0.0, (1.0, color)); - let text_style = TextStyle::Monospace; self.text( rect.min, Align2::LEFT_TOP, text.to_string(), - text_style, + FontId::monospace(14.0), color, ); } @@ -217,7 +218,7 @@ impl Painter { color: Color32, text: impl ToString, ) -> Rect { - let galley = self.layout_no_wrap(text.to_string(), TextStyle::Monospace, color); + let galley = self.layout_no_wrap(text.to_string(), FontId::monospace(14.0), color); let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size())); let frame_rect = rect.expand(2.0); self.add(Shape::rect_filled( @@ -324,7 +325,7 @@ impl Painter { impl Painter { /// Lay out and paint some text. /// - /// To center the text at the given position, use `anchor: (Center, Center)`. + /// To center the text at the given position, use `Align2::CENTER_CENTER`. /// /// To find out the size of text before painting it, use /// [`Self::layout`] or [`Self::layout_no_wrap`]. @@ -336,10 +337,10 @@ impl Painter { pos: Pos2, anchor: Align2, text: impl ToString, - text_style: TextStyle, + font_id: FontId, text_color: Color32, ) -> Rect { - let galley = self.layout_no_wrap(text.to_string(), text_style, text_color); + let galley = self.layout_no_wrap(text.to_string(), font_id, text_color); let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size())); self.galley(rect.min, galley); rect @@ -352,11 +353,11 @@ impl Painter { pub fn layout( &self, text: String, - text_style: TextStyle, + font_id: FontId, color: crate::Color32, wrap_width: f32, ) -> Arc { - self.fonts().layout(text, text_style, color, wrap_width) + self.fonts().layout(text, font_id, color, wrap_width) } /// Will line break at `\n`. @@ -366,10 +367,10 @@ impl Painter { pub fn layout_no_wrap( &self, text: String, - text_style: TextStyle, + font_id: FontId, color: crate::Color32, ) -> Arc { - self.fonts().layout(text, text_style, color, f32::INFINITY) + self.fonts().layout(text, font_id, color, f32::INFINITY) } /// Paint text that has already been layed out in a [`Galley`]. diff --git a/egui/src/style.rs b/egui/src/style.rs index ed7d8cc8..084f7772 100644 --- a/egui/src/style.rs +++ b/egui/src/style.rs @@ -2,8 +2,123 @@ #![allow(clippy::if_same_then_else)] -use crate::{color::*, emath::*, Response, RichText, WidgetText}; -use epaint::{Shadow, Stroke, TextStyle}; +use crate::{color::*, emath::*, FontFamily, FontId, Response, RichText, WidgetText}; +use epaint::{mutex::Arc, Shadow, Stroke}; +use std::collections::BTreeMap; + +// ---------------------------------------------------------------------------- + +/// Alias for a [`FontId`] (font of a certain size). +/// +/// The font is found via look-up in [`Style::text_styles`]. +/// You can use [`TextStyle::resolve`] to do this lookup. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum TextStyle { + /// Used when small text is needed. + Small, + + /// Normal labels. Easily readable, doesn't take up too much space. + Body, + + /// Same size as [`Self::Body]`, but used when monospace is important (for aligning number, code snippets, etc). + Monospace, + + /// Buttons. Maybe slightly bigger than [`Self::Body]`. + /// Signifies that he item is interactive. + Button, + + /// Heading. Probably larger than [`Self::Body]`. + Heading, + + /// A user-chosen style, found in [`Style::text_styles`]. + /// ``` + /// egui::TextStyle::Name("footing".into()); + /// ```` + Name(Arc), +} + +impl std::fmt::Display for TextStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Small => "Small".fmt(f), + Self::Body => "Body".fmt(f), + Self::Monospace => "Monospace".fmt(f), + Self::Button => "Button".fmt(f), + Self::Heading => "Heading".fmt(f), + Self::Name(name) => (*name).fmt(f), + } + } +} + +impl TextStyle { + /// Look up this [`TextStyle`] in [`Style::text_styles`]. + pub fn resolve(&self, style: &Style) -> FontId { + style.text_styles.get(self).cloned().unwrap_or_else(|| { + panic!( + "Failed to find {:?} in Style::text_styles. Available styles:\n{:#?}", + self, + style.text_styles() + ) + }) + } +} + +// ---------------------------------------------------------------------------- + +/// A way to select [`FontId`], either by picking one directly or by using a [`TextStyle`]. +pub enum FontSelection { + /// Default text style - will use [`TextStyle::Body`], unless + /// [`Style::override_font_id`] or [`Style::override_text_style`] is set. + Default, + + /// Directly select size and font family + FontId(FontId), + + /// Use a [`TextStyle`] to look up the [`FontId`] in [`Style::text_styles`]. + Style(TextStyle), +} + +impl Default for FontSelection { + #[inline] + fn default() -> Self { + Self::Default + } +} + +impl FontSelection { + pub fn resolve(self, style: &Style) -> FontId { + match self { + Self::Default => { + if let Some(override_font_id) = &style.override_font_id { + override_font_id.clone() + } else if let Some(text_style) = &style.override_text_style { + text_style.resolve(style) + } else { + TextStyle::Body.resolve(style) + } + } + Self::FontId(font_id) => font_id, + Self::Style(text_style) => text_style.resolve(style), + } + } +} + +impl From for FontSelection { + #[inline(always)] + fn from(font_id: FontId) -> Self { + Self::FontId(font_id) + } +} + +impl From for FontSelection { + #[inline(always)] + fn from(text_style: TextStyle) -> Self { + Self::Style(text_style) + } +} + +// ---------------------------------------------------------------------------- /// Specifies the look and feel of egui. /// @@ -15,15 +130,23 @@ use epaint::{Shadow, Stroke, TextStyle}; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Style { - /// Default `TextStyle` for normal text (i.e. for `Label` and `TextEdit`). - pub body_text_style: TextStyle, - /// If set this will change the default [`TextStyle`] for all widgets. /// /// On most widgets you can also set an explicit text style, /// which will take precedence over this. pub override_text_style: Option, + /// If set this will change the font family and size for all widgets. + /// + /// On most widgets you can also set an explicit text style, + /// which will take precedence over this. + pub override_font_id: Option, + + /// The [`FontFamily`] and size you want to use for a specific [`TextStyle`]. + /// + /// The most convenient way to look something up in this is to use [`TextStyle::resolve`]. + pub text_styles: BTreeMap, + /// If set, labels buttons wtc will use this to determine whether or not /// to wrap the text at the right edge of the `Ui` they are in. /// By default this is `None`. @@ -77,6 +200,11 @@ impl Style { pub fn noninteractive(&self) -> &WidgetVisuals { &self.visuals.widgets.noninteractive } + + /// All known text styles. + pub fn text_styles(&self) -> Vec { + self.text_styles.keys().cloned().collect() + } } /// Controls the sizes and distances between widgets. @@ -356,11 +484,35 @@ pub struct DebugOptions { // ---------------------------------------------------------------------------- +/// The default text styles of the default egui theme. +pub fn default_text_styles() -> BTreeMap { + let mut text_styles = BTreeMap::new(); + text_styles.insert( + TextStyle::Small, + FontId::new(10.0, FontFamily::Proportional), + ); + text_styles.insert(TextStyle::Body, FontId::new(14.0, FontFamily::Proportional)); + text_styles.insert( + TextStyle::Button, + FontId::new(14.0, FontFamily::Proportional), + ); + text_styles.insert( + TextStyle::Heading, + FontId::new(20.0, FontFamily::Proportional), + ); + text_styles.insert( + TextStyle::Monospace, + FontId::new(14.0, FontFamily::Monospace), + ); + text_styles +} + impl Default for Style { fn default() -> Self { Self { - body_text_style: TextStyle::Body, + override_font_id: None, override_text_style: None, + text_styles: default_text_styles(), wrap: None, spacing: Spacing::default(), interaction: Interaction::default(), @@ -565,8 +717,9 @@ use crate::{widgets::*, Ui}; impl Style { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { - body_text_style, + override_font_id, override_text_style, + text_styles, wrap: _, spacing, interaction, @@ -579,11 +732,14 @@ impl Style { visuals.light_dark_radio_buttons(ui); crate::Grid::new("_options").show(ui, |ui| { - ui.label("Default body text style:"); + ui.label("Override font id:"); ui.horizontal(|ui| { - for &style in &[TextStyle::Body, TextStyle::Monospace] { - let text = crate::RichText::new(format!("{:?}", style)).text_style(style); - ui.radio_value(body_text_style, style, text); + ui.radio_value(override_font_id, None, "None"); + if ui.radio(override_font_id.is_some(), "override").clicked() { + *override_font_id = Some(FontId::default()); + } + if let Some(override_font_id) = override_font_id { + crate::introspection::font_id_ui(ui, override_font_id); } }); ui.end_row(); @@ -592,12 +748,14 @@ impl Style { crate::ComboBox::from_id_source("Override text style") .selected_text(match override_text_style { None => "None".to_owned(), - Some(override_text_style) => format!("{:?}", override_text_style), + Some(override_text_style) => override_text_style.to_string(), }) .show_ui(ui, |ui| { ui.selectable_value(override_text_style, None, "None"); - for style in TextStyle::all() { - let text = crate::RichText::new(format!("{:?}", style)).text_style(style); + let all_text_styles = ui.style().text_styles(); + for style in all_text_styles { + let text = + crate::RichText::new(style.to_string()).text_style(style.clone()); ui.selectable_value(override_text_style, Some(style), text); } }); @@ -612,6 +770,7 @@ impl Style { ui.end_row(); }); + ui.collapsing("๐Ÿ”  Text Styles", |ui| text_styles_ui(ui, text_styles)); ui.collapsing("๐Ÿ“ Spacing", |ui| spacing.ui(ui)); ui.collapsing("โ˜ Interaction", |ui| interaction.ui(ui)); ui.collapsing("๐ŸŽจ Visuals", |ui| visuals.ui(ui)); @@ -626,6 +785,20 @@ impl Style { } } +fn text_styles_ui(ui: &mut Ui, text_styles: &mut BTreeMap) -> Response { + ui.vertical(|ui| { + crate::Grid::new("text_styles").show(ui, |ui| { + for (text_style, font_id) in text_styles.iter_mut() { + ui.label(RichText::new(text_style.to_string()).font(font_id.clone())); + crate::introspection::font_id_ui(ui, font_id); + ui.end_row(); + } + }); + crate::reset_button_with(ui, text_styles, default_text_styles()); + }) + .response +} + impl Spacing { pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 0d5ea817..dad5fabc 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -133,7 +133,7 @@ impl Ui { /// Example: /// ``` /// # egui::__run_test_ui(|ui| { - /// ui.style_mut().body_text_style = egui::TextStyle::Heading; + /// ui.style_mut().override_text_style = Some(egui::TextStyle::Heading); /// # }); /// ``` pub fn style_mut(&mut self) -> &mut Style { @@ -359,6 +359,11 @@ impl Ui { self.ctx().fonts() } + /// The height of text of this text style + pub fn text_style_height(&self, style: &TextStyle) -> f32 { + self.fonts().row_height(&style.resolve(self.style())) + } + /// Screen-space rectangle for clipping what we paint in this ui. /// This is used, for instance, to avoid painting outside a window that is smaller than its contents. #[inline] @@ -1086,6 +1091,16 @@ impl Ui { /// Shortcut for `add(Label::new(text))` /// /// See also [`Label`]. + /// + /// ### Example + /// ``` + /// # egui::__run_test_ui(|ui| { + /// use egui::{RichText, FontId, Color32}; + /// ui.label("Normal text"); + /// ui.label(RichText::new("Large text").font(FontId::proportional(40.0))); + /// ui.label(RichText::new("Red text").color(Color32::RED)); + /// # }); + /// ``` #[inline] pub fn label(&mut self, text: impl Into) -> Response { Label::new(text).ui(self) @@ -1256,12 +1271,12 @@ impl Ui { pub fn radio_value( &mut self, current_value: &mut Value, - selected_value: Value, + alternative: Value, text: impl Into, ) -> Response { - let mut response = self.radio(*current_value == selected_value, text); + let mut response = self.radio(*current_value == alternative, text); if response.clicked() { - *current_value = selected_value; + *current_value = alternative; response.mark_changed(); } response diff --git a/egui/src/widget_text.rs b/egui/src/widget_text.rs index 14365f25..4e5659cd 100644 --- a/egui/src/widget_text.rs +++ b/egui/src/widget_text.rs @@ -1,17 +1,30 @@ use epaint::mutex::Arc; use crate::{ - style::WidgetVisuals, text::LayoutJob, Align, Color32, Context, Galley, Pos2, Style, TextStyle, - Ui, Visuals, + style::WidgetVisuals, text::LayoutJob, Align, Color32, FontFamily, FontSelection, Galley, Pos2, + Style, TextStyle, Ui, Visuals, }; /// Text and optional style choices for it. /// /// The style choices (font, color) are applied to the entire text. /// For more detailed control, use [`crate::text::LayoutJob`] instead. +/// +/// A `RichText` can be used in most widgets and helper functions, e.g. [`Ui::label`] and [`Ui::button`]. +/// +/// ### Example +/// ``` +/// use egui::{RichText, Color32}; +/// +/// RichText::new("Plain"); +/// RichText::new("colored").color(Color32::RED); +/// RichText::new("Large and underlined").size(20.0).underline(); +/// ``` #[derive(Clone, Default, PartialEq)] pub struct RichText { text: String, + size: Option, + family: Option, text_style: Option, background_color: Color32, text_color: Option, @@ -64,6 +77,35 @@ impl RichText { &self.text } + /// Select the font size (in points). + /// This overrides the value from [`Self::text_style`]. + #[inline] + pub fn size(mut self, size: f32) -> Self { + self.size = Some(size); + self + } + + /// Select the font family. + /// + /// This overrides the value from [`Self::text_style`]. + /// + /// Only the families available in [`crate::FontDefinitions::families`] may be used. + #[inline] + pub fn family(mut self, family: FontFamily) -> Self { + self.family = Some(family); + self + } + + /// Select the font and size. + /// This overrides the value from [`Self::text_style`]. + #[inline] + pub fn font(mut self, font_id: crate::FontId) -> Self { + let crate::FontId { size, family } = font_id; + self.size = Some(size); + self.family = Some(family); + self + } + /// Override the [`TextStyle`]. #[inline] pub fn text_style(mut self, text_style: TextStyle) -> Self { @@ -170,24 +212,33 @@ impl RichText { } /// Read the font height of the selected text style. - pub fn font_height(&self, ctx: &Context) -> f32 { - let text_style = self - .text_style - .or(ctx.style().override_text_style) - .unwrap_or(ctx.style().body_text_style); - ctx.fonts().row_height(text_style) + pub fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 { + let mut font_id = self.text_style.as_ref().map_or_else( + || FontSelection::Default.resolve(style), + |text_style| text_style.resolve(style), + ); + + if let Some(size) = self.size { + font_id.size = size; + } + if let Some(family) = &self.family { + font_id.family = family.clone(); + } + fonts.row_height(&font_id) } fn into_text_job( self, style: &Style, - default_text_style: TextStyle, + fallback_font: FontSelection, default_valign: Align, ) -> WidgetTextJob { let text_color = self.get_text_color(&style.visuals); let Self { text, + size, + family, text_style, background_color, text_color: _, // already used by `get_text_color` @@ -204,9 +255,21 @@ impl RichText { let line_color = text_color.unwrap_or_else(|| style.visuals.text_color()); let text_color = text_color.unwrap_or(crate::Color32::TEMPORARY_COLOR); - let text_style = text_style - .or(style.override_text_style) - .unwrap_or(default_text_style); + let font_id = { + let mut font_id = text_style + .or_else(|| style.override_text_style.clone()) + .map_or_else( + || fallback_font.resolve(style), + |text_style| text_style.resolve(style), + ); + if let Some(size) = size { + font_id.size = size; + } + if let Some(family) = family { + font_id.family = family; + } + font_id + }; let mut background_color = background_color; if code { @@ -230,7 +293,7 @@ impl RichText { }; let text_format = crate::text::TextFormat { - style: text_style, + font_id, color: text_color, background: background_color, italics, @@ -270,6 +333,7 @@ impl RichText { #[derive(Clone)] pub enum WidgetText { RichText(RichText), + /// Use this [`LayoutJob`] when laying out the text. /// /// Only [`LayoutJob::text`] and [`LayoutJob::sections`] are guaranteed to be respected. @@ -280,6 +344,7 @@ pub enum WidgetText { /// If you want all parts of the `LayoutJob` respected, then convert it to a /// [`Galley`] and use [`Self::Galley`] instead. LayoutJob(LayoutJob), + /// Use exactly this galley when painting the text. Galley(Arc), } @@ -438,10 +503,10 @@ impl WidgetText { } } - pub(crate) fn font_height(&self, ctx: &Context) -> f32 { + pub(crate) fn font_height(&self, fonts: &epaint::Fonts, style: &Style) -> f32 { match self { - Self::RichText(text) => text.font_height(ctx), - Self::LayoutJob(job) => job.font_height(&*ctx.fonts()), + Self::RichText(text) => text.font_height(fonts, style), + Self::LayoutJob(job) => job.font_height(fonts), Self::Galley(galley) => { if let Some(row) = galley.rows.first() { row.height() @@ -455,11 +520,11 @@ impl WidgetText { pub fn into_text_job( self, style: &Style, - default_text_style: TextStyle, + fallback_font: FontSelection, default_valign: Align, ) -> WidgetTextJob { match self { - Self::RichText(text) => text.into_text_job(style, default_text_style, default_valign), + Self::RichText(text) => text.into_text_job(style, fallback_font, default_valign), Self::LayoutJob(job) => WidgetTextJob { job, job_has_color: true, @@ -482,7 +547,7 @@ impl WidgetText { ui: &Ui, wrap: Option, available_width: f32, - default_text_style: TextStyle, + fallback_font: impl Into, ) -> WidgetTextGalley { let wrap = wrap.unwrap_or_else(|| ui.wrap_text()); let wrap_width = if wrap { available_width } else { f32::INFINITY }; @@ -490,7 +555,7 @@ impl WidgetText { match self { Self::RichText(text) => { let valign = ui.layout().vertical_align(); - let mut text_job = text.into_text_job(ui.style(), default_text_style, valign); + let mut text_job = text.into_text_job(ui.style(), fallback_font.into(), valign); text_job.job.wrap_width = wrap_width; WidgetTextGalley { galley: ui.fonts().layout_job(text_job.job), diff --git a/egui/src/widgets/drag_value.rs b/egui/src/widgets/drag_value.rs index 6eb4e2dc..0c788184 100644 --- a/egui/src/widgets/drag_value.rs +++ b/egui/src/widgets/drag_value.rs @@ -195,7 +195,7 @@ impl<'a> Widget for DragValue<'a> { TextEdit::singleline(&mut value_text) .id(kb_edit_id) .desired_width(button_width) - .text_style(TextStyle::Monospace), + .font(TextStyle::Monospace), ); if let Ok(parsed_value) = value_text.parse() { let parsed_value = clamp_to_range(parsed_value, clamp_range); diff --git a/egui/src/widgets/label.rs b/egui/src/widgets/label.rs index b819e08b..5513fb20 100644 --- a/egui/src/widgets/label.rs +++ b/egui/src/widgets/label.rs @@ -2,6 +2,8 @@ use crate::{widget_text::WidgetTextGalley, *}; /// Static text. /// +/// Usually it is more convenient to use [`Ui::label`]. +/// /// ``` /// # egui::__run_test_ui(|ui| { /// ui.label("Equivalent"); @@ -84,7 +86,7 @@ impl Label { let valign = ui.layout().vertical_align(); let mut text_job = self .text - .into_text_job(ui.style(), ui.style().body_text_style, valign); + .into_text_job(ui.style(), FontSelection::Default, valign); let should_wrap = self.wrap.unwrap_or_else(|| ui.wrap_text()); let available_width = ui.available_width(); diff --git a/egui/src/widgets/plot/items/mod.rs b/egui/src/widgets/plot/items/mod.rs index 90df4992..ee5d9f34 100644 --- a/egui/src/widgets/plot/items/mod.rs +++ b/egui/src/widgets/plot/items/mod.rs @@ -1621,13 +1621,15 @@ fn add_rulers_and_text( text }); + let font_id = TextStyle::Body.resolve(plot.ui.style()); + let corner_value = elem.corner_value(); shapes.push(Shape::text( &*plot.ui.fonts(), plot.transform.position_from_value(&corner_value) + vec2(3.0, -2.0), Align2::LEFT_BOTTOM, text, - TextStyle::Body, + font_id, plot.ui.visuals().text_color(), )); } @@ -1677,12 +1679,14 @@ pub(super) fn rulers_at_value( } }; + let font_id = TextStyle::Body.resolve(plot.ui.style()); + shapes.push(Shape::text( &*plot.ui.fonts(), pointer + vec2(3.0, -2.0), Align2::LEFT_BOTTOM, text, - TextStyle::Body, + font_id, plot.ui.visuals().text_color(), )); } diff --git a/egui/src/widgets/plot/legend.rs b/egui/src/widgets/plot/legend.rs index 3f41c400..cbb5a676 100644 --- a/egui/src/widgets/plot/legend.rs +++ b/egui/src/widgets/plot/legend.rs @@ -29,7 +29,7 @@ impl Corner { } /// The configuration for a plot legend. -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, PartialEq)] pub struct Legend { pub text_style: TextStyle, pub background_alpha: f32, @@ -82,16 +82,18 @@ impl LegendEntry { } } - fn ui(&mut self, ui: &mut Ui, text: String) -> Response { + fn ui(&mut self, ui: &mut Ui, text: String, text_style: &TextStyle) -> Response { let Self { color, checked, hovered, } = self; - let galley = - ui.fonts() - .layout_delayed_color(text, ui.style().body_text_style, f32::INFINITY); + let font_id = text_style.resolve(ui.style()); + + let galley = ui + .fonts() + .layout_delayed_color(text, font_id, f32::INFINITY); let icon_size = galley.size().y; let icon_spacing = icon_size / 5.0; @@ -236,7 +238,6 @@ impl Widget for &mut LegendWidget { let mut legend_ui = ui.child_ui(legend_rect, layout); legend_ui .scope(|ui| { - ui.style_mut().body_text_style = config.text_style; let background_frame = Frame { margin: vec2(8.0, 4.0), corner_radius: ui.style().visuals.window_corner_radius, @@ -249,7 +250,7 @@ impl Widget for &mut LegendWidget { .show(ui, |ui| { entries .iter_mut() - .map(|(name, entry)| entry.ui(ui, name.clone())) + .map(|(name, entry)| entry.ui(ui, name.clone(), &config.text_style)) .reduce(|r1, r2| r1.union(r2)) .unwrap() }) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 23f60bc6..4a2f86c3 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -683,7 +683,8 @@ impl PreparedPlot { let Self { transform, .. } = self; let bounds = transform.bounds(); - let text_style = TextStyle::Body; + + let font_id = TextStyle::Body.resolve(ui.style()); let base: i64 = 10; let basef = base as f64; @@ -741,7 +742,7 @@ impl PreparedPlot { let color = color_from_alpha(ui, text_alpha); let text = emath::round_to_decimals(value_main, 5).to_string(); // hack - let galley = ui.painter().layout_no_wrap(text, text_style, color); + let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color); let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y); diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index 5f38a68c..e3906259 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -466,10 +466,8 @@ impl<'a> Slider<'a> { } fn add_contents(&mut self, ui: &mut Ui) -> Response { - let text_style = TextStyle::Button; let perpendicular = ui - .fonts() - .row_height(text_style) + .text_style_height(&TextStyle::Body) .at_least(ui.spacing().interact_size.y); let slider_response = self.allocate_slider_space(ui, perpendicular); self.slider_ui(ui, &slider_response); diff --git a/egui/src/widgets/text_edit/builder.rs b/egui/src/widgets/text_edit/builder.rs index 0c06260a..0f2d2b67 100644 --- a/egui/src/widgets/text_edit/builder.rs +++ b/egui/src/widgets/text_edit/builder.rs @@ -52,7 +52,7 @@ pub struct TextEdit<'t> { hint_text: WidgetText, id: Option, id_source: Option, - text_style: Option, + font_selection: FontSelection, text_color: Option, layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc>, password: bool, @@ -97,7 +97,7 @@ impl<'t> TextEdit<'t> { hint_text: Default::default(), id: None, id_source: None, - text_style: None, + font_selection: Default::default(), text_color: None, layouter: None, password: false, @@ -117,7 +117,7 @@ impl<'t> TextEdit<'t> { /// - monospaced font /// - focus lock pub fn code_editor(self) -> Self { - self.text_style(TextStyle::Monospace).lock_focus(true) + self.font(TextStyle::Monospace).lock_focus(true) } /// Use if you want to set an explicit `Id` for this widget. @@ -144,11 +144,17 @@ impl<'t> TextEdit<'t> { self } - pub fn text_style(mut self, text_style: TextStyle) -> Self { - self.text_style = Some(text_style); + /// Pick a [`FontId`] or [`TextStyle`]. + pub fn font(mut self, font_selection: impl Into) -> Self { + self.font_selection = font_selection.into(); self } + #[deprecated = "Use .font(โ€ฆ) instead"] + pub fn text_style(self, text_style: TextStyle) -> Self { + self.font(text_style) + } + pub fn text_color(mut self, text_color: Color32) -> Self { self.text_color = Some(text_color); self @@ -330,7 +336,7 @@ impl<'t> TextEdit<'t> { hint_text, id, id_source, - text_style, + font_selection, text_color, layouter, password, @@ -350,10 +356,9 @@ impl<'t> TextEdit<'t> { .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()); let prev_text = text.as_ref().to_owned(); - let text_style = text_style - .or(ui.style().override_text_style) - .unwrap_or_else(|| ui.style().body_text_style); - let row_height = ui.fonts().row_height(text_style); + + let font_id = font_selection.resolve(ui.style()); + let row_height = ui.fonts().row_height(&font_id); const MIN_WIDTH: f32 = 24.0; // Never make a `TextEdit` more narrow than this. let available_width = ui.available_width().at_least(MIN_WIDTH); let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width); @@ -363,12 +368,13 @@ impl<'t> TextEdit<'t> { desired_width.min(available_width) }; + let font_id_clone = font_id.clone(); let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| { let text = mask_if_password(password, text); ui.fonts().layout_job(if multiline { - LayoutJob::simple(text, text_style, text_color, wrap_width) + LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width) } else { - LayoutJob::simple_singleline(text, text_style, text_color) + LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color) }) }; @@ -543,9 +549,9 @@ impl<'t> TextEdit<'t> { if text.as_ref().is_empty() && !hint_text.is_empty() { let hint_text_color = ui.visuals().weak_text_color(); let galley = if multiline { - hint_text.into_galley(ui, Some(true), desired_size.x, text_style) + hint_text.into_galley(ui, Some(true), desired_size.x, font_id) } else { - hint_text.into_galley(ui, Some(false), f32::INFINITY, text_style) + hint_text.into_galley(ui, Some(false), f32::INFINITY, font_id) }; galley.paint_with_fallback_color(&painter, response.rect.min, hint_text_color); } diff --git a/egui_demo_lib/benches/benchmark.rs b/egui_demo_lib/benches/benchmark.rs index d1d549bd..584afb75 100644 --- a/egui_demo_lib/benches/benchmark.rs +++ b/egui_demo_lib/benches/benchmark.rs @@ -86,35 +86,50 @@ pub fn criterion_benchmark(c: &mut Criterion) { { let pixels_per_point = 1.0; + let max_texture_side = 8 * 1024; let wrap_width = 512.0; - let text_style = egui::TextStyle::Body; + let font_id = egui::FontId::default(); let color = egui::Color32::WHITE; - let fonts = - egui::epaint::text::Fonts::new(pixels_per_point, egui::FontDefinitions::default()); - c.bench_function("text_layout_uncached", |b| { - b.iter(|| { - use egui::epaint::text::{layout, LayoutJob}; + let fonts = egui::epaint::text::Fonts::new( + pixels_per_point, + max_texture_side, + egui::FontDefinitions::default(), + ); + { + let mut locked_fonts = fonts.lock(); + c.bench_function("text_layout_uncached", |b| { + b.iter(|| { + use egui::epaint::text::{layout, LayoutJob}; - let job = LayoutJob::simple( + let job = LayoutJob::simple( + LOREM_IPSUM_LONG.to_owned(), + font_id.clone(), + color, + wrap_width, + ); + layout(&mut locked_fonts.fonts, job.into()) + }) + }); + } + c.bench_function("text_layout_cached", |b| { + b.iter(|| { + fonts.layout( LOREM_IPSUM_LONG.to_owned(), - egui::TextStyle::Body, + font_id.clone(), color, wrap_width, - ); - layout(&fonts, job.into()) + ) }) }); - c.bench_function("text_layout_cached", |b| { - b.iter(|| fonts.layout(LOREM_IPSUM_LONG.to_owned(), text_style, color, wrap_width)) - }); - let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), text_style, color, wrap_width); + let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, color, wrap_width); let mut tessellator = egui::epaint::Tessellator::from_options(Default::default()); let mut mesh = egui::epaint::Mesh::default(); let text_shape = TextShape::new(egui::Pos2::ZERO, galley); + let font_image_size = fonts.font_image_size(); c.bench_function("tessellate_text", |b| { b.iter(|| { - tessellator.tessellate_text(fonts.font_image_size(), text_shape.clone(), &mut mesh); + tessellator.tessellate_text(font_image_size, text_shape.clone(), &mut mesh); mesh.clear(); }) }); diff --git a/egui_demo_lib/src/apps/demo/code_editor.rs b/egui_demo_lib/src/apps/demo/code_editor.rs index 6784fb51..107d66b8 100644 --- a/egui_demo_lib/src/apps/demo/code_editor.rs +++ b/egui_demo_lib/src/apps/demo/code_editor.rs @@ -71,7 +71,7 @@ impl super::View for CodeEditor { ui.collapsing("Theme", |ui| { ui.group(|ui| { theme.ui(ui); - theme.store_in_memory(ui.ctx()); + theme.clone().store_in_memory(ui.ctx()); }); }); @@ -85,7 +85,7 @@ impl super::View for CodeEditor { egui::ScrollArea::vertical().show(ui, |ui| { ui.add( egui::TextEdit::multiline(code) - .text_style(egui::TextStyle::Monospace) // for cursor height + .font(egui::TextStyle::Monospace) // for cursor height .code_editor() .desired_rows(10) .lock_focus(true) diff --git a/egui_demo_lib/src/apps/demo/code_example.rs b/egui_demo_lib/src/apps/demo/code_example.rs index 783e1ed4..5c62e606 100644 --- a/egui_demo_lib/src/apps/demo/code_example.rs +++ b/egui_demo_lib/src/apps/demo/code_example.rs @@ -98,7 +98,8 @@ impl CodeExample { ); ui.horizontal(|ui| { - let indentation = 8.0 * ui.fonts()[egui::TextStyle::Monospace].glyph_width(' '); + let font_id = egui::TextStyle::Monospace.resolve(ui.style()); + let indentation = 8.0 * ui.fonts().glyph_width(&font_id, ' '); let item_spacing = ui.spacing_mut().item_spacing; ui.add_space(indentation - item_spacing.x); diff --git a/egui_demo_lib/src/apps/demo/font_book.rs b/egui_demo_lib/src/apps/demo/font_book.rs index fe1ce499..3a5a2ef6 100644 --- a/egui_demo_lib/src/apps/demo/font_book.rs +++ b/egui_demo_lib/src/apps/demo/font_book.rs @@ -2,15 +2,15 @@ use std::collections::BTreeMap; pub struct FontBook { filter: String, - text_style: egui::TextStyle, - named_chars: BTreeMap>, + font_id: egui::FontId, + named_chars: BTreeMap>, } impl Default for FontBook { fn default() -> Self { Self { filter: Default::default(), - text_style: egui::TextStyle::Button, + font_id: egui::FontId::proportional(20.0), named_chars: Default::default(), } } @@ -34,7 +34,7 @@ impl super::View for FontBook { ui.label(format!( "The selected font supports {} characters.", self.named_chars - .get(&self.text_style) + .get(&self.font_id.family) .map(|map| map.len()) .unwrap_or_default() )); @@ -51,13 +51,7 @@ impl super::View for FontBook { ui.separator(); - egui::ComboBox::from_label("Text style") - .selected_text(format!("{:?}", self.text_style)) - .show_ui(ui, |ui| { - for style in egui::TextStyle::all() { - ui.selectable_value(&mut self.text_style, style, format!("{:?}", style)); - } - }); + egui::introspection::font_id_ui(ui, &mut self.font_id); ui.horizontal(|ui| { ui.label("Filter:"); @@ -68,16 +62,11 @@ impl super::View for FontBook { } }); - let text_style = self.text_style; let filter = &self.filter; - let named_chars = self.named_chars.entry(text_style).or_insert_with(|| { - ui.fonts()[text_style] - .characters() - .iter() - .filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control()) - .map(|&chr| (chr, char_name(chr))) - .collect() - }); + let named_chars = self + .named_chars + .entry(self.font_id.family.clone()) + .or_insert_with(|| available_characters(ui, self.font_id.family.clone())); ui.separator(); @@ -88,12 +77,14 @@ impl super::View for FontBook { for (&chr, name) in named_chars { if filter.is_empty() || name.contains(filter) || *filter == chr.to_string() { let button = egui::Button::new( - egui::RichText::new(chr.to_string()).text_style(text_style), + egui::RichText::new(chr.to_string()).font(self.font_id.clone()), ) .frame(false); let tooltip_ui = |ui: &mut egui::Ui| { - ui.label(egui::RichText::new(chr.to_string()).text_style(text_style)); + ui.label( + egui::RichText::new(chr.to_string()).font(self.font_id.clone()), + ); ui.label(format!("{}\nU+{:X}\n\nClick to copy", name, chr as u32)); }; @@ -107,6 +98,18 @@ impl super::View for FontBook { } } +fn available_characters(ui: &egui::Ui, family: egui::FontFamily) -> BTreeMap { + ui.fonts() + .lock() + .fonts + .font(&egui::FontId::new(10.0, family)) // size is arbitrary for getting the characters + .characters() + .iter() + .filter(|chr| !chr.is_whitespace() && !chr.is_ascii_control()) + .map(|&chr| (chr, char_name(chr))) + .collect() +} + fn char_name(chr: char) -> String { special_char_name(chr) .map(|s| s.to_owned()) diff --git a/egui_demo_lib/src/apps/demo/misc_demo_window.rs b/egui_demo_lib/src/apps/demo/misc_demo_window.rs index 9308d335..785a9473 100644 --- a/egui_demo_lib/src/apps/demo/misc_demo_window.rs +++ b/egui_demo_lib/src/apps/demo/misc_demo_window.rs @@ -140,7 +140,7 @@ impl Widgets { ui.horizontal_wrapped(|ui| { // Trick so we don't have to add spaces in the text below: - let width = ui.fonts()[TextStyle::Body].glyph_width(' '); + let width = ui.fonts().glyph_width(&TextStyle::Body.resolve(ui.style()), ' '); ui.spacing_mut().item_spacing.x = width; ui.label(RichText::new("Text can have").color(Color32::from_rgb(110, 255, 110))); @@ -418,7 +418,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { "This is a demonstration of ", first_row_indentation, TextFormat { - style: TextStyle::Body, color: default_color, ..Default::default() }, @@ -427,7 +426,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { "the egui text layout engine. ", 0.0, TextFormat { - style: TextStyle::Body, color: strong_color, ..Default::default() }, @@ -436,7 +434,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { "It supports ", 0.0, TextFormat { - style: TextStyle::Body, color: default_color, ..Default::default() }, @@ -445,7 +442,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { "different ", 0.0, TextFormat { - style: TextStyle::Body, color: Color32::from_rgb(110, 255, 110), ..Default::default() }, @@ -454,7 +450,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { "colors, ", 0.0, TextFormat { - style: TextStyle::Body, color: Color32::from_rgb(128, 140, 255), ..Default::default() }, @@ -463,7 +458,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { "backgrounds, ", 0.0, TextFormat { - style: TextStyle::Body, color: default_color, background: Color32::from_rgb(128, 32, 32), ..Default::default() @@ -473,7 +467,7 @@ fn text_layout_ui(ui: &mut egui::Ui) { "mixing ", 0.0, TextFormat { - style: TextStyle::Heading, + font_id: FontId::proportional(20.0), color: default_color, ..Default::default() }, @@ -482,7 +476,7 @@ fn text_layout_ui(ui: &mut egui::Ui) { "fonts, ", 0.0, TextFormat { - style: TextStyle::Monospace, + font_id: FontId::monospace(14.0), color: default_color, ..Default::default() }, @@ -491,7 +485,7 @@ fn text_layout_ui(ui: &mut egui::Ui) { "raised text, ", 0.0, TextFormat { - style: TextStyle::Small, + font_id: FontId::proportional(8.0), color: default_color, valign: Align::TOP, ..Default::default() @@ -501,7 +495,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { "with ", 0.0, TextFormat { - style: TextStyle::Body, color: default_color, ..Default::default() }, @@ -510,7 +503,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { "underlining", 0.0, TextFormat { - style: TextStyle::Body, color: default_color, underline: Stroke::new(1.0, Color32::LIGHT_BLUE), ..Default::default() @@ -520,7 +512,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { " and ", 0.0, TextFormat { - style: TextStyle::Body, color: default_color, ..Default::default() }, @@ -529,7 +520,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { "strikethrough", 0.0, TextFormat { - style: TextStyle::Body, color: default_color, strikethrough: Stroke::new(2.0, Color32::RED.linear_multiply(0.5)), ..Default::default() @@ -539,7 +529,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { ". Of course, ", 0.0, TextFormat { - style: TextStyle::Body, color: default_color, ..Default::default() }, @@ -548,7 +537,6 @@ fn text_layout_ui(ui: &mut egui::Ui) { "you can", 0.0, TextFormat { - style: TextStyle::Body, color: default_color, strikethrough: Stroke::new(1.0, strong_color), ..Default::default() @@ -558,7 +546,7 @@ fn text_layout_ui(ui: &mut egui::Ui) { " mix these!", 0.0, TextFormat { - style: TextStyle::Small, + font_id: FontId::proportional(8.0), color: Color32::LIGHT_BLUE, background: Color32::from_rgb(128, 0, 0), underline: Stroke::new(1.0, strong_color), diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index a172c218..4039c06f 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -261,9 +261,10 @@ impl Widget for &mut LegendDemo { egui::Grid::new("settings").show(ui, |ui| { ui.label("Text style:"); ui.horizontal(|ui| { - TextStyle::all().for_each(|style| { - ui.selectable_value(&mut config.text_style, style, format!("{:?}", style)); - }); + let all_text_styles = ui.style().text_styles(); + for style in all_text_styles { + ui.selectable_value(&mut config.text_style, style.clone(), style.to_string()); + } }); ui.end_row(); @@ -284,7 +285,9 @@ impl Widget for &mut LegendDemo { ui.end_row(); }); - let legend_plot = Plot::new("legend_demo").legend(*config).data_aspect(1.0); + let legend_plot = Plot::new("legend_demo") + .legend(config.clone()) + .data_aspect(1.0); legend_plot .show(ui, |plot_ui| { plot_ui.line(LegendDemo::line_with_slope(0.5).name("lines")); diff --git a/egui_demo_lib/src/apps/demo/scrolling.rs b/egui_demo_lib/src/apps/demo/scrolling.rs index 7c69a726..f2499bd0 100644 --- a/egui_demo_lib/src/apps/demo/scrolling.rs +++ b/egui_demo_lib/src/apps/demo/scrolling.rs @@ -81,7 +81,7 @@ fn huge_content_lines(ui: &mut egui::Ui) { ui.add_space(4.0); let text_style = TextStyle::Body; - let row_height = ui.fonts()[text_style].row_height(); + let row_height = ui.text_style_height(&text_style); let num_rows = 10_000; ScrollArea::vertical().auto_shrink([false; 2]).show_rows( ui, @@ -101,8 +101,8 @@ fn huge_content_painter(ui: &mut egui::Ui) { ui.label("A lot of rows, but only the visible ones are painted, so performance is still good:"); ui.add_space(4.0); - let text_style = TextStyle::Body; - let row_height = ui.fonts()[text_style].row_height() + ui.spacing().item_spacing.y; + let font_id = TextStyle::Body.resolve(ui.style()); + let row_height = ui.fonts().row_height(&font_id) + ui.spacing().item_spacing.y; let num_rows = 10_000; ScrollArea::vertical() @@ -130,7 +130,7 @@ fn huge_content_painter(ui: &mut egui::Ui) { pos2(x, y), Align2::LEFT_TOP, text, - text_style, + font_id.clone(), ui.visuals().text_color(), ); used_rect = used_rect.union(text_rect); @@ -265,7 +265,7 @@ impl super::View for ScrollStickTo { ui.add_space(4.0); let text_style = TextStyle::Body; - let row_height = ui.fonts()[text_style].row_height(); + let row_height = ui.text_style_height(&text_style); ScrollArea::vertical().stick_to_bottom().show_rows( ui, row_height, diff --git a/egui_demo_lib/src/apps/http_app.rs b/egui_demo_lib/src/apps/http_app.rs index c3e53b11..9ae72a96 100644 --- a/egui_demo_lib/src/apps/http_app.rs +++ b/egui_demo_lib/src/apps/http_app.rs @@ -216,7 +216,7 @@ fn selectable_text(ui: &mut egui::Ui, mut text: &str) { ui.add( egui::TextEdit::multiline(&mut text) .desired_width(f32::INFINITY) - .text_style(egui::TextStyle::Monospace), + .font(egui::TextStyle::Monospace), ); } @@ -257,7 +257,7 @@ impl ColoredText { let mut text = self.0.text.as_str(); ui.add( egui::TextEdit::multiline(&mut text) - .text_style(egui::TextStyle::Monospace) + .font(egui::TextStyle::Monospace) .desired_width(f32::INFINITY) .layouter(&mut layouter), ); diff --git a/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 42198959..aa1468d9 100644 --- a/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -88,7 +88,7 @@ impl EasyMarkEditor { let response = if self.highlight_editor { let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| { - let mut layout_job = highlighter.highlight(ui.visuals(), easymark); + let mut layout_job = highlighter.highlight(ui.style(), easymark); layout_job.wrap_width = wrap_width; ui.fonts().layout_job(layout_job) }; @@ -96,7 +96,7 @@ impl EasyMarkEditor { ui.add( egui::TextEdit::multiline(code) .desired_width(f32::INFINITY) - .text_style(egui::TextStyle::Monospace) // for cursor height + .font(egui::TextStyle::Monospace) // for cursor height .layouter(&mut layouter), ) } else { diff --git a/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs b/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs index 67c2c859..45caeeff 100644 --- a/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs +++ b/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs @@ -5,23 +5,23 @@ use crate::easy_mark::easy_mark_parser; /// In practice, the highlighter is fast enough not to need any caching. #[derive(Default)] pub struct MemoizedEasymarkHighlighter { - visuals: egui::Visuals, + style: egui::Style, code: String, output: egui::text::LayoutJob, } impl MemoizedEasymarkHighlighter { - pub fn highlight(&mut self, visuals: &egui::Visuals, code: &str) -> egui::text::LayoutJob { - if (&self.visuals, self.code.as_str()) != (visuals, code) { - self.visuals = visuals.clone(); + pub fn highlight(&mut self, egui_style: &egui::Style, code: &str) -> egui::text::LayoutJob { + if (&self.style, self.code.as_str()) != (egui_style, code) { + self.style = egui_style.clone(); self.code = code.to_owned(); - self.output = highlight_easymark(visuals, code); + self.output = highlight_easymark(egui_style, code); } self.output.clone() } } -pub fn highlight_easymark(visuals: &egui::Visuals, mut text: &str) -> egui::text::LayoutJob { +pub fn highlight_easymark(egui_style: &egui::Style, mut text: &str) -> egui::text::LayoutJob { let mut job = egui::text::LayoutJob::default(); let mut style = easy_mark_parser::Style::default(); let mut start_of_line = true; @@ -33,7 +33,7 @@ pub fn highlight_easymark(visuals: &egui::Visuals, mut text: &str) -> egui::text &text[..end], 0.0, format_from_style( - visuals, + egui_style, &easy_mark_parser::Style { code: true, ..Default::default() @@ -50,7 +50,7 @@ pub fn highlight_easymark(visuals: &egui::Visuals, mut text: &str) -> egui::text let end = text[1..] .find(&['`', '\n'][..]) .map_or_else(|| text.len(), |i| i + 2); - job.append(&text[..end], 0.0, format_from_style(visuals, &style)); + job.append(&text[..end], 0.0, format_from_style(egui_style, &style)); text = &text[end..]; style.code = false; continue; @@ -77,7 +77,7 @@ pub fn highlight_easymark(visuals: &egui::Visuals, mut text: &str) -> egui::text skip = 1; if style.strong { // Include the character that i ending ths style: - job.append(&text[..skip], 0.0, format_from_style(visuals, &style)); + job.append(&text[..skip], 0.0, format_from_style(egui_style, &style)); text = &text[skip..]; skip = 0; } @@ -86,7 +86,7 @@ pub fn highlight_easymark(visuals: &egui::Visuals, mut text: &str) -> egui::text skip = 1; if style.small { // Include the character that i ending ths style: - job.append(&text[..skip], 0.0, format_from_style(visuals, &style)); + job.append(&text[..skip], 0.0, format_from_style(egui_style, &style)); text = &text[skip..]; skip = 0; } @@ -95,7 +95,7 @@ pub fn highlight_easymark(visuals: &egui::Visuals, mut text: &str) -> egui::text skip = 1; if style.raised { // Include the character that i ending ths style: - job.append(&text[..skip], 0.0, format_from_style(visuals, &style)); + job.append(&text[..skip], 0.0, format_from_style(egui_style, &style)); text = &text[skip..]; skip = 0; } @@ -114,12 +114,16 @@ pub fn highlight_easymark(visuals: &egui::Visuals, mut text: &str) -> egui::text .map_or_else(|| text.len(), |i| (skip + i).max(1)); if line_end <= end { - job.append(&text[..line_end], 0.0, format_from_style(visuals, &style)); + job.append( + &text[..line_end], + 0.0, + format_from_style(egui_style, &style), + ); text = &text[line_end..]; start_of_line = true; style = Default::default(); } else { - job.append(&text[..end], 0.0, format_from_style(visuals, &style)); + job.append(&text[..end], 0.0, format_from_style(egui_style, &style)); text = &text[end..]; start_of_line = false; } @@ -129,17 +133,17 @@ pub fn highlight_easymark(visuals: &egui::Visuals, mut text: &str) -> egui::text } fn format_from_style( - visuals: &egui::Visuals, + egui_style: &egui::Style, emark_style: &easy_mark_parser::Style, ) -> egui::text::TextFormat { use egui::{Align, Color32, Stroke, TextStyle}; let color = if emark_style.strong || emark_style.heading { - visuals.strong_text_color() + egui_style.visuals.strong_text_color() } else if emark_style.quoted { - visuals.weak_text_color() + egui_style.visuals.weak_text_color() } else { - visuals.text_color() + egui_style.visuals.text_color() }; let text_style = if emark_style.heading { @@ -153,7 +157,7 @@ fn format_from_style( }; let background = if emark_style.code { - visuals.code_bg_color + egui_style.visuals.code_bg_color } else { Color32::TRANSPARENT }; @@ -177,7 +181,7 @@ fn format_from_style( }; egui::text::TextFormat { - style: text_style, + font_id: text_style.resolve(egui_style), color, background, italics: emark_style.italics, diff --git a/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs b/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs index 56b379f1..ffe79d40 100644 --- a/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs +++ b/egui_demo_lib/src/easy_mark/easy_mark_viewer.rs @@ -18,7 +18,7 @@ pub fn easy_mark_it<'em>(ui: &mut Ui, items: impl Iterator(ui: &mut Ui, items: impl Iterator) { - let row_height = ui.fonts()[TextStyle::Body].row_height(); + let row_height = ui.text_style_height(&TextStyle::Body); let one_indent = row_height / 2.0; match item { @@ -134,7 +134,7 @@ fn rich_text_from_style(text: &str, style: &easy_mark::Style) -> RichText { } fn bullet_point(ui: &mut Ui, width: f32) -> Response { - let row_height = ui.fonts()[TextStyle::Body].row_height(); + let row_height = ui.text_style_height(&TextStyle::Body); let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover()); ui.painter().circle_filled( rect.center(), @@ -145,7 +145,8 @@ fn bullet_point(ui: &mut Ui, width: f32) -> Response { } fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response { - let row_height = ui.fonts()[TextStyle::Body].row_height(); + let font_id = TextStyle::Body.resolve(ui.style()); + let row_height = ui.fonts().row_height(&font_id); let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover()); let text = format!("{}.", number); let text_color = ui.visuals().strong_text_color(); @@ -153,7 +154,7 @@ fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response { rect.right_center(), Align2::RIGHT_CENTER, text, - TextStyle::Body, + font_id, text_color, ); response diff --git a/egui_demo_lib/src/frame_history.rs b/egui_demo_lib/src/frame_history.rs index a1ac01e3..5af9d24c 100644 --- a/egui_demo_lib/src/frame_history.rs +++ b/egui_demo_lib/src/frame_history.rs @@ -98,7 +98,7 @@ impl FrameHistory { pos2(rect.left(), y), egui::Align2::LEFT_BOTTOM, text, - TextStyle::Monospace, + TextStyle::Monospace.resolve(ui.style()), color, )); } diff --git a/egui_demo_lib/src/syntax_highlighting.rs b/egui_demo_lib/src/syntax_highlighting.rs index c8c08fb1..f472cc44 100644 --- a/egui_demo_lib/src/syntax_highlighting.rs +++ b/egui_demo_lib/src/syntax_highlighting.rs @@ -13,7 +13,7 @@ pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) { ui.add( egui::TextEdit::multiline(&mut code) - .text_style(egui::TextStyle::Monospace) // for cursor height + .font(egui::TextStyle::Monospace) // for cursor height .code_editor() .desired_rows(1) .lock_focus(true) @@ -116,7 +116,7 @@ impl SyntectTheme { } } -#[derive(Clone, Copy, Hash, PartialEq)] +#[derive(Clone, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct CodeTheme { @@ -158,15 +158,15 @@ impl CodeTheme { } } - pub fn store_in_memory(&self, ctx: &egui::Context) { + pub fn store_in_memory(self, ctx: &egui::Context) { if self.dark_mode { ctx.memory() .data - .insert_persisted(egui::Id::new("dark"), *self); + .insert_persisted(egui::Id::new("dark"), self); } else { ctx.memory() .data - .insert_persisted(egui::Id::new("light"), *self); + .insert_persisted(egui::Id::new("light"), self); } } } @@ -201,34 +201,34 @@ impl CodeTheme { #[cfg(not(feature = "syntect"))] impl CodeTheme { pub fn dark() -> Self { - let text_style = egui::TextStyle::Monospace; + let font_id = egui::FontId::monospace(12.0); use egui::{Color32, TextFormat}; Self { dark_mode: true, formats: enum_map::enum_map![ - TokenType::Comment => TextFormat::simple(text_style, Color32::from_gray(120)), - TokenType::Keyword => TextFormat::simple(text_style, Color32::from_rgb(255, 100, 100)), - TokenType::Literal => TextFormat::simple(text_style, Color32::from_rgb(87, 165, 171)), - TokenType::StringLiteral => TextFormat::simple(text_style, Color32::from_rgb(109, 147, 226)), - TokenType::Punctuation => TextFormat::simple(text_style, Color32::LIGHT_GRAY), - TokenType::Whitespace => TextFormat::simple(text_style, Color32::TRANSPARENT), + TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::from_gray(120)), + TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(255, 100, 100)), + TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(87, 165, 171)), + TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(109, 147, 226)), + TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::LIGHT_GRAY), + TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT), ], } } pub fn light() -> Self { - let text_style = egui::TextStyle::Monospace; + let font_id = egui::FontId::monospace(12.0); use egui::{Color32, TextFormat}; Self { dark_mode: false, #[cfg(not(feature = "syntect"))] formats: enum_map::enum_map![ - TokenType::Comment => TextFormat::simple(text_style, Color32::GRAY), - TokenType::Keyword => TextFormat::simple(text_style, Color32::from_rgb(235, 0, 0)), - TokenType::Literal => TextFormat::simple(text_style, Color32::from_rgb(153, 134, 255)), - TokenType::StringLiteral => TextFormat::simple(text_style, Color32::from_rgb(37, 203, 105)), - TokenType::Punctuation => TextFormat::simple(text_style, Color32::DARK_GRAY), - TokenType::Whitespace => TextFormat::simple(text_style, Color32::TRANSPARENT), + TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::GRAY), + TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(235, 0, 0)), + TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(153, 134, 255)), + TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(37, 203, 105)), + TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::DARK_GRAY), + TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT), ], } } @@ -259,7 +259,7 @@ impl CodeTheme { // (TokenType::Whitespace, "whitespace"), ] { let format = &mut self.formats[tt]; - ui.style_mut().override_text_style = Some(format.style); + ui.style_mut().override_font_id = Some(format.font_id.clone()); ui.visuals_mut().override_text_color = Some(format.color); ui.radio_value(&mut selected_tt, tt, tt_name); } @@ -325,7 +325,7 @@ impl Highligher { // Fallback: LayoutJob::simple( code.into(), - egui::TextStyle::Monospace, + egui::FontId::monospace(14.0), if theme.dark_mode { egui::Color32::LIGHT_GRAY } else { @@ -371,7 +371,7 @@ impl Highligher { leading_space: 0.0, byte_range: as_byte_range(text, range), format: TextFormat { - style: egui::TextStyle::Monospace, + font_id: egui::FontId::monospace(14.0), color: text_color, italics, underline, @@ -412,7 +412,7 @@ impl Highligher { while !text.is_empty() { if text.starts_with("//") { let end = text.find('\n').unwrap_or_else(|| text.len()); - job.append(&text[..end], 0.0, theme.formats[TokenType::Comment]); + job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone()); text = &text[end..]; } else if text.starts_with('"') { let end = text[1..] @@ -420,7 +420,11 @@ impl Highligher { .map(|i| i + 2) .or_else(|| text.find('\n')) .unwrap_or_else(|| text.len()); - job.append(&text[..end], 0.0, theme.formats[TokenType::StringLiteral]); + job.append( + &text[..end], + 0.0, + theme.formats[TokenType::StringLiteral].clone(), + ); text = &text[end..]; } else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) { let end = text[1..] @@ -432,19 +436,27 @@ impl Highligher { } else { TokenType::Literal }; - job.append(word, 0.0, theme.formats[tt]); + job.append(word, 0.0, theme.formats[tt].clone()); text = &text[end..]; } else if text.starts_with(|c: char| c.is_ascii_whitespace()) { let end = text[1..] .find(|c: char| !c.is_ascii_whitespace()) .map_or_else(|| text.len(), |i| i + 1); - job.append(&text[..end], 0.0, theme.formats[TokenType::Whitespace]); + job.append( + &text[..end], + 0.0, + theme.formats[TokenType::Whitespace].clone(), + ); text = &text[end..]; } else { let mut it = text.char_indices(); it.next(); let end = it.next().map_or(text.len(), |(idx, _chr)| idx); - job.append(&text[..end], 0.0, theme.formats[TokenType::Punctuation]); + job.append( + &text[..end], + 0.0, + theme.formats[TokenType::Punctuation].clone(), + ); text = &text[end..]; } } diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index d1322543..e277e58c 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -190,7 +190,7 @@ impl WrapApp { screen_rect.center(), Align2::CENTER_CENTER, text, - TextStyle::Heading, + TextStyle::Heading.resolve(&ctx.style()), Color32::WHITE, ); } diff --git a/egui_glium/src/epi_backend.rs b/egui_glium/src/epi_backend.rs index 73ae74d3..b22a5075 100644 --- a/egui_glium/src/epi_backend.rs +++ b/egui_glium/src/epi_backend.rs @@ -47,6 +47,7 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { let mut painter = crate::Painter::new(&display); let mut integration = egui_winit::epi::EpiIntegration::new( "egui_glium", + painter.max_texture_side(), display.gl_window().window(), repaint_signal, persistence, diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index c43578dd..50426289 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -111,10 +111,14 @@ pub struct EguiGlium { impl EguiGlium { pub fn new(display: &glium::Display) -> Self { + let painter = crate::Painter::new(display); Self { egui_ctx: Default::default(), - egui_winit: egui_winit::State::new(display.gl_window().window()), - painter: crate::Painter::new(display), + egui_winit: egui_winit::State::new( + painter.max_texture_side(), + display.gl_window().window(), + ), + painter, shapes: Default::default(), textures_delta: Default::default(), } diff --git a/egui_glium/src/painter.rs b/egui_glium/src/painter.rs index 384b74da..b176a2f7 100644 --- a/egui_glium/src/painter.rs +++ b/egui_glium/src/painter.rs @@ -16,6 +16,7 @@ use { }; pub struct Painter { + max_texture_side: usize, program: glium::Program, textures: AHashMap>, @@ -27,6 +28,9 @@ pub struct Painter { impl Painter { pub fn new(facade: &dyn glium::backend::Facade) -> Painter { + use glium::CapabilitiesSource as _; + let max_texture_side = facade.get_capabilities().max_texture_size as _; + let program = program! { facade, 120 => { @@ -49,6 +53,7 @@ impl Painter { .expect("Failed to compile shader"); Painter { + max_texture_side, program, textures: Default::default(), #[cfg(feature = "epi")] @@ -56,6 +61,10 @@ impl Painter { } } + pub fn max_texture_side(&self) -> usize { + self.max_texture_side + } + /// Main entry-point for painting a frame. /// You should call `target.clear_color(..)` before /// and `target.finish()` after this. diff --git a/egui_glow/CHANGELOG.md b/egui_glow/CHANGELOG.md index 6ff8ea75..45ddd2db 100644 --- a/egui_glow/CHANGELOG.md +++ b/egui_glow/CHANGELOG.md @@ -4,8 +4,8 @@ All notable changes to the `egui_glow` integration will be noted in this file. ## Unreleased * `EguiGlow::run` no longer returns the shapes to paint, but stores them internally until you call `EguiGlow::paint` ([#1110](https://github.com/emilk/egui/pull/1110)). -* Added `set_texture_filter` method to `Painter` ((#1041)[https://github.com/emilk/egui/pull/1041]). -* Fix failure to run in Chrome ((#1092)[https://github.com/emilk/egui/pull/1092]). +* Added `set_texture_filter` method to `Painter` ([#1041](https://github.com/emilk/egui/pull/1041)). +* Fix failure to run in Chrome ([#1092](https://github.com/emilk/egui/pull/1092)). ## 0.16.0 - 2021-12-29 diff --git a/egui_glow/src/epi_backend.rs b/egui_glow/src/epi_backend.rs index 86ad9d66..0e2cac48 100644 --- a/egui_glow/src/epi_backend.rs +++ b/egui_glow/src/epi_backend.rs @@ -63,6 +63,7 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { .unwrap(); let mut integration = egui_winit::epi::EpiIntegration::new( "egui_glow", + painter.max_texture_side(), gl_window.window(), repaint_signal, persistence, diff --git a/egui_glow/src/lib.rs b/egui_glow/src/lib.rs index 8e150b88..5a8c85e4 100644 --- a/egui_glow/src/lib.rs +++ b/egui_glow/src/lib.rs @@ -123,14 +123,19 @@ impl EguiGlow { gl_window: &glutin::WindowedContext, gl: &glow::Context, ) -> Self { + let painter = crate::Painter::new(gl, None, "") + .map_err(|error| { + crate::misc_util::glow_print_error(format!( + "error occurred in initializing painter:\n{}", + error + )); + }) + .unwrap(); + Self { egui_ctx: Default::default(), - egui_winit: egui_winit::State::new(gl_window.window()), - painter: crate::Painter::new(gl, None, "") - .map_err(|error| { - eprintln!("some error occurred in initializing painter\n{}", error); - }) - .unwrap(), + egui_winit: egui_winit::State::new(painter.max_texture_side(), gl_window.window()), + painter, shapes: Default::default(), textures_delta: Default::default(), } diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs index baa97869..1e461838 100644 --- a/egui_glow/src/painter.rs +++ b/egui_glow/src/painter.rs @@ -24,6 +24,8 @@ const FRAG_SRC: &str = include_str!("shader/fragment.glsl"); /// This struct must be destroyed with [`Painter::destroy`] before dropping, to ensure OpenGL /// objects have been properly deleted and are not leaked. pub struct Painter { + max_texture_side: usize, + program: glow::Program, u_screen_size: glow::UniformLocation, u_sampler: glow::UniformLocation, @@ -90,6 +92,8 @@ impl Painter { ) -> Result { check_for_gl_error(gl, "before Painter::new"); + let max_texture_side = unsafe { gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE) } as usize; + let support_vao = crate::misc_util::supports_vao(gl); let shader_version = ShaderVersion::get(gl); let is_webgl_1 = shader_version == ShaderVersion::Es100; @@ -203,6 +207,7 @@ impl Painter { check_for_gl_error(gl, "after Painter::new"); Ok(Painter { + max_texture_side, program, u_screen_size, u_sampler, @@ -223,6 +228,10 @@ impl Painter { } } + pub fn max_texture_side(&self) -> usize { + self.max_texture_side + } + unsafe fn prepare_painting( &mut self, [width_in_pixels, height_in_pixels]: [u32; 2], diff --git a/egui_web/CHANGELOG.md b/egui_web/CHANGELOG.md index d64a79dc..3964e70e 100644 --- a/egui_web/CHANGELOG.md +++ b/egui_web/CHANGELOG.md @@ -6,9 +6,9 @@ All notable changes to the `egui_web` integration will be noted in this file. ## Unreleased * The default painter is now glow instead of WebGL ([#1020](https://github.com/emilk/egui/pull/1020)). * Made the WebGL painter opt-in ([#1020](https://github.com/emilk/egui/pull/1020)). -* Fix glow failure Chrome ((#1092)[https://github.com/emilk/egui/pull/1092]). -* Shift-scroll will now result in horizontal scrolling on all platforms ((#1136)[https://github.com/emilk/egui/pull/1136]). -* Update `epi::IntegrationInfo::web_location_hash` on `hashchange` event ([#1140](https://github.com/emilk/egui/pull/1140)). +* Fixed glow failure on Chromium ([#1092](https://github.com/emilk/egui/pull/1092)). +* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)). +* Updated `epi::IntegrationInfo::web_location_hash` on `hashchange` event ([#1140](https://github.com/emilk/egui/pull/1140)). ## 0.16.0 - 2021-12-29 diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 7158ef55..05b7f366 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -143,6 +143,8 @@ impl AppRunner { textures_delta: Default::default(), }; + runner.input.raw.max_texture_side = runner.painter.max_texture_side(); + { runner .app diff --git a/egui_web/src/glow_wrapping.rs b/egui_web/src/glow_wrapping.rs index 35ab6de2..b6f87dd1 100644 --- a/egui_web/src/glow_wrapping.rs +++ b/egui_web/src/glow_wrapping.rs @@ -40,6 +40,10 @@ impl WrappedGlowPainter { } impl crate::Painter for WrappedGlowPainter { + fn max_texture_side(&self) -> usize { + self.painter.max_texture_side() + } + fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { self.painter.set_texture(&self.glow_ctx, tex_id, delta); } diff --git a/egui_web/src/painter.rs b/egui_web/src/painter.rs index 2688d2a5..16bc8e49 100644 --- a/egui_web/src/painter.rs +++ b/egui_web/src/painter.rs @@ -1,6 +1,9 @@ use wasm_bindgen::prelude::JsValue; pub trait Painter { + /// Max size of one side of a texture. + fn max_texture_side(&self) -> usize; + fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta); fn free_texture(&mut self, tex_id: egui::TextureId); diff --git a/egui_web/src/webgl1.rs b/egui_web/src/webgl1.rs index 973cec78..41553c62 100644 --- a/egui_web/src/webgl1.rs +++ b/egui_web/src/webgl1.rs @@ -289,6 +289,21 @@ impl epi::NativeTexture for WebGlPainter { } impl crate::Painter for WebGlPainter { + fn max_texture_side(&self) -> usize { + if let Ok(max_texture_side) = self + .gl + .get_parameter(web_sys::WebGlRenderingContext::MAX_TEXTURE_SIZE) + { + if let Some(max_texture_side) = max_texture_side.as_f64() { + return max_texture_side as usize; + } + } + + crate::console_error("Failed to query max texture size"); + + 2048 + } + fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { match &delta.image { egui::ImageData::Color(image) => { diff --git a/egui_web/src/webgl2.rs b/egui_web/src/webgl2.rs index 778ff109..848d355c 100644 --- a/egui_web/src/webgl2.rs +++ b/egui_web/src/webgl2.rs @@ -273,6 +273,21 @@ impl epi::NativeTexture for WebGl2Painter { } impl crate::Painter for WebGl2Painter { + fn max_texture_side(&self) -> usize { + if let Ok(max_texture_side) = self + .gl + .get_parameter(web_sys::WebGl2RenderingContext::MAX_TEXTURE_SIZE) + { + if let Some(max_texture_side) = max_texture_side.as_f64() { + return max_texture_side as usize; + } + } + + crate::console_error("Failed to query max texture size"); + + 2048 + } + fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { match &delta.image { egui::ImageData::Color(image) => { diff --git a/emath/src/align.rs b/emath/src/align.rs index 41801e36..016b7d71 100644 --- a/emath/src/align.rs +++ b/emath/src/align.rs @@ -5,7 +5,6 @@ use crate::*; /// left/center/right or top/center/bottom alignment for e.g. anchors and layouts. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] pub enum Align { /// Left or top. Min, @@ -146,7 +145,6 @@ impl Default for Align { /// Two-dimension alignment, e.g. [`Align2::LEFT_TOP`]. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] pub struct Align2(pub [Align; 2]); impl Align2 { diff --git a/epaint/CHANGELOG.md b/epaint/CHANGELOG.md index 2c0d3410..503a2128 100644 --- a/epaint/CHANGELOG.md +++ b/epaint/CHANGELOG.md @@ -4,8 +4,11 @@ All notable changes to the epaint crate will be documented in this file. ## Unreleased +* Much improved font selection ([#1154](https://github.com/emilk/egui/pull/1154)): + * Replaced `TextStyle` with `FontId` which lets you pick any font size and font family. + * Replaced `Fonts::font_image` with `font_image_delta` for partial font atlas updates. +* Added `ImageData` and `TextureManager` for loading images into textures ([#1110](https://github.com/emilk/egui/pull/1110)). * Added `Shape::dashed_line_many` ([#1027](https://github.com/emilk/egui/pull/1027)). -* Add `ImageData` and `TextureManager` for loading images into textures ([#1110](https://github.com/emilk/egui/pull/1110)). ## 0.16.0 - 2021-12-29 @@ -17,5 +20,5 @@ All notable changes to the epaint crate will be documented in this file. ## 0.15.0 - 2021-10-24 * `Fonts::layout_job`: New text layout engine allowing mixing fonts, colors and styles, with underlining and strikethrough. * New `CircleShape`, `PathShape`, `RectShape` and `TextShape` used in `enum Shape`. -* Add support for rotated text (see `TextShape`). +* Added support for rotated text (see `TextShape`). * Added `"convert_bytemuck"` feature. diff --git a/epaint/src/color.rs b/epaint/src/color.rs index 3f53ba23..2c506cb9 100644 --- a/epaint/src/color.rs +++ b/epaint/src/color.rs @@ -485,9 +485,9 @@ pub fn gamma_u8_from_linear_f32(l: f32) -> u8 { if l <= 0.0 { 0 } else if l <= 0.0031308 { - (3294.6 * l).round() as u8 + fast_round(3294.6 * l) } else if l <= 1.0 { - (269.025 * l.powf(1.0 / 2.4) - 14.025).round() as u8 + fast_round(269.025 * l.powf(1.0 / 2.4) - 14.025) } else { 255 } @@ -497,7 +497,11 @@ pub fn gamma_u8_from_linear_f32(l: f32) -> u8 { /// Useful for alpha-channel. #[inline(always)] pub fn linear_u8_from_linear_f32(a: f32) -> u8 { - (a * 255.0).round() as u8 // rust does a saturating cast since 1.45 + fast_round(a * 255.0) +} + +fn fast_round(r: f32) -> u8 { + (r + 0.5).floor() as _ // rust does a saturating cast since 1.45 } #[test] diff --git a/epaint/src/lib.rs b/epaint/src/lib.rs index 07d34d1f..2207398d 100644 --- a/epaint/src/lib.rs +++ b/epaint/src/lib.rs @@ -112,7 +112,7 @@ pub use { stats::PaintStats, stroke::Stroke, tessellator::{tessellate_shapes, TessellationOptions, Tessellator}, - text::{Fonts, Galley, TextStyle}, + text::{FontFamily, FontId, Fonts, Galley}, texture_atlas::TextureAtlas, texture_handle::TextureHandle, textures::TextureManager, diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index 792bfebc..84a363cf 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -1,5 +1,5 @@ use crate::{ - text::{Fonts, Galley, TextStyle}, + text::{FontId, Fonts, Galley}, Color32, Mesh, Stroke, }; use emath::*; @@ -126,10 +126,10 @@ impl Shape { pos: Pos2, anchor: Align2, text: impl ToString, - text_style: TextStyle, + font_id: FontId, color: Color32, ) -> Self { - let galley = fonts.layout_no_wrap(text.to_string(), text_style, color); + let galley = fonts.layout_no_wrap(text.to_string(), font_id, color); let rect = anchor.anchor_rect(Rect::from_min_size(pos, galley.size())); Self::galley(rect.min, galley) } diff --git a/epaint/src/text/font.rs b/epaint/src/text/font.rs index f0eaaa97..aab77be1 100644 --- a/epaint/src/text/font.rs +++ b/epaint/src/text/font.rs @@ -1,6 +1,5 @@ use crate::{ mutex::{Arc, Mutex, RwLock}, - text::TextStyle, TextureAtlas, }; use ahash::AHashMap; @@ -59,7 +58,7 @@ impl Default for GlyphInfo { pub struct FontImpl { ab_glyph_font: ab_glyph::FontArc, /// Maximum character height - scale_in_pixels: f32, + scale_in_pixels: u32, height_in_points: f32, // move each character by this much (hack) y_offset: f32, @@ -73,20 +72,13 @@ impl FontImpl { atlas: Arc>, pixels_per_point: f32, ab_glyph_font: ab_glyph::FontArc, - scale_in_points: f32, + scale_in_pixels: u32, y_offset: f32, ) -> FontImpl { - assert!(scale_in_points > 0.0); + assert!(scale_in_pixels > 0); assert!(pixels_per_point > 0.0); - let scale_in_pixels = pixels_per_point * scale_in_points; - - // Round to an even number of physical pixels to get even kerning. - // See https://github.com/emilk/egui/issues/382 - let scale_in_pixels = scale_in_pixels.round(); - let scale_in_points = scale_in_pixels / pixels_per_point; - - let height_in_points = scale_in_points; + let height_in_points = scale_in_pixels as f32 / pixels_per_point; // TODO: use v_metrics for line spacing ? // let v = rusttype_font.v_metrics(Scale::uniform(scale_in_pixels)); @@ -162,7 +154,7 @@ impl FontImpl { &mut self.atlas.lock(), &self.ab_glyph_font, glyph_id, - self.scale_in_pixels, + self.scale_in_pixels as f32, self.y_offset, self.pixels_per_point, ); @@ -180,7 +172,7 @@ impl FontImpl { ) -> f32 { use ab_glyph::{Font as _, ScaleFont}; self.ab_glyph_font - .as_scaled(self.scale_in_pixels) + .as_scaled(self.scale_in_pixels as f32) .kern(last_glyph_id, glyph_id) / self.pixels_per_point } @@ -202,23 +194,21 @@ type FontIndex = usize; // TODO: rename? /// Wrapper over multiple `FontImpl` (e.g. a primary + fallbacks for emojis) pub struct Font { - text_style: TextStyle, fonts: Vec>, /// Lazily calculated. - characters: RwLock>>, + characters: Option>, replacement_glyph: (FontIndex, GlyphInfo), pixels_per_point: f32, row_height: f32, - glyph_info_cache: RwLock>, + glyph_info_cache: AHashMap, } impl Font { - pub fn new(text_style: TextStyle, fonts: Vec>) -> Self { + pub fn new(fonts: Vec>) -> Self { if fonts.is_empty() { return Self { - text_style, fonts, - characters: RwLock::new(None), + characters: None, replacement_glyph: Default::default(), pixels_per_point: 1.0, row_height: 0.0, @@ -230,9 +220,8 @@ impl Font { let row_height = fonts[0].row_height(); let mut slf = Self { - text_style, fonts, - characters: RwLock::new(None), + characters: None, replacement_glyph: Default::default(), pixels_per_point, row_height, @@ -260,26 +249,20 @@ impl Font { slf.glyph_info(c); } slf.glyph_info('ยฐ'); - slf.glyph_info(crate::text::PASSWORD_REPLACEMENT_CHAR); // password replacement character + slf.glyph_info(crate::text::PASSWORD_REPLACEMENT_CHAR); slf } /// All supported characters - pub fn characters(&self) -> BTreeSet { - if self.characters.read().is_none() { + pub fn characters(&mut self) -> &BTreeSet { + self.characters.get_or_insert_with(|| { let mut characters = BTreeSet::new(); for font in &self.fonts { characters.extend(font.characters()); } - self.characters.write().replace(characters); - } - self.characters.read().clone().unwrap() - } - - #[inline(always)] - pub fn text_style(&self) -> TextStyle { - self.text_style + characters + }) } #[inline(always)] @@ -295,35 +278,30 @@ impl Font { pub fn uv_rect(&self, c: char) -> UvRect { self.glyph_info_cache - .read() .get(&c) .map(|gi| gi.1.uv_rect) .unwrap_or_default() } /// Width of this character in points. - pub fn glyph_width(&self, c: char) -> f32 { + pub fn glyph_width(&mut self, c: char) -> f32 { self.glyph_info(c).1.advance_width } /// `\n` will (intentionally) show up as the replacement character. - fn glyph_info(&self, c: char) -> (FontIndex, GlyphInfo) { - { - if let Some(font_index_glyph_info) = self.glyph_info_cache.read().get(&c) { - return *font_index_glyph_info; - } + fn glyph_info(&mut self, c: char) -> (FontIndex, GlyphInfo) { + if let Some(font_index_glyph_info) = self.glyph_info_cache.get(&c) { + return *font_index_glyph_info; } let font_index_glyph_info = self.glyph_info_no_cache_or_fallback(c); let font_index_glyph_info = font_index_glyph_info.unwrap_or(self.replacement_glyph); - self.glyph_info_cache - .write() - .insert(c, font_index_glyph_info); + self.glyph_info_cache.insert(c, font_index_glyph_info); font_index_glyph_info } #[inline] - pub(crate) fn glyph_info_and_font_impl(&self, c: char) -> (Option<&FontImpl>, GlyphInfo) { + pub(crate) fn glyph_info_and_font_impl(&mut self, c: char) -> (Option<&FontImpl>, GlyphInfo) { if self.fonts.is_empty() { return (None, self.replacement_glyph.1); } @@ -332,12 +310,10 @@ impl Font { (Some(font_impl), glyph_info) } - fn glyph_info_no_cache_or_fallback(&self, c: char) -> Option<(FontIndex, GlyphInfo)> { + fn glyph_info_no_cache_or_fallback(&mut self, c: char) -> Option<(FontIndex, GlyphInfo)> { for (font_index, font_impl) in self.fonts.iter().enumerate() { if let Some(glyph_info) = font_impl.glyph_info(c) { - self.glyph_info_cache - .write() - .insert(c, (font_index, glyph_info)); + self.glyph_info_cache.insert(c, (font_index, glyph_info)); return Some((font_index, glyph_info)); } } @@ -379,11 +355,11 @@ fn allocate_glyph( if v > 0.0 { let px = glyph_pos.0 + x as usize; let py = glyph_pos.1 + y as usize; - image[(px, py)] = (v * 255.0).round() as u8; + image[(px, py)] = fast_round(v * 255.0); } }); - let offset_in_pixels = vec2(bb.min.x as f32, scale_in_pixels as f32 + bb.min.y as f32); + let offset_in_pixels = vec2(bb.min.x as f32, scale_in_pixels + bb.min.y as f32); let offset = offset_in_pixels / pixels_per_point + y_offset * Vec2::Y; UvRect { offset, @@ -407,3 +383,7 @@ fn allocate_glyph( uv_rect, } } + +fn fast_round(r: f32) -> u8 { + (r + 0.5).floor() as _ // rust does a saturating cast since 1.45 +} diff --git a/epaint/src/text/fonts.rs b/epaint/src/text/fonts.rs index c363a8d7..6ca3eb41 100644 --- a/epaint/src/text/fonts.rs +++ b/epaint/src/text/fonts.rs @@ -1,63 +1,122 @@ use std::collections::BTreeMap; use crate::{ - mutex::{Arc, Mutex}, + mutex::{Arc, Mutex, MutexGuard}, text::{ font::{Font, FontImpl}, Galley, LayoutJob, }, TextureAtlas, }; +use emath::NumExt as _; -// TODO: rename -/// One of a few categories of styles of text, e.g. body, button or heading. -#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +// ---------------------------------------------------------------------------- + +/// How to select a sized font. +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -pub enum TextStyle { - /// Used when small text is needed. - Small, - /// Normal labels. Easily readable, doesn't take up too much space. - Body, - /// Buttons. Maybe slightly bigger than `Body`. - Button, - /// Heading. Probably larger than `Body`. - Heading, - /// Same size as `Body`, but used when monospace is important (for aligning number, code snippets, etc). - Monospace, +pub struct FontId { + /// Height in points. + pub size: f32, + + /// What font family to use. + pub family: FontFamily, + // TODO: weight (bold), italics, โ€ฆ } -impl TextStyle { - pub fn all() -> impl ExactSizeIterator { - [ - TextStyle::Small, - TextStyle::Body, - TextStyle::Button, - TextStyle::Heading, - TextStyle::Monospace, - ] - .iter() - .copied() +impl Default for FontId { + #[inline] + fn default() -> Self { + Self { + size: 14.0, + family: FontFamily::Proportional, + } } } -/// Which style of font: [`Monospace`][`FontFamily::Monospace`] or [`Proportional`][`FontFamily::Proportional`]. -#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] -pub enum FontFamily { - /// A font where each character is the same width (`w` is the same width as `i`). - Monospace, - /// A font where some characters are wider than other (e.g. 'w' is wider than 'i'). - Proportional, +impl FontId { + #[inline] + pub fn new(size: f32, family: FontFamily) -> Self { + Self { size, family } + } + + #[inline] + pub fn proportional(size: f32) -> Self { + Self::new(size, FontFamily::Proportional) + } + + #[inline] + pub fn monospace(size: f32) -> Self { + Self::new(size, FontFamily::Monospace) + } } +#[allow(clippy::derive_hash_xor_eq)] +impl std::hash::Hash for FontId { + #[inline(always)] + fn hash(&self, state: &mut H) { + let Self { size, family } = self; + crate::f32_hash(state, *size); + family.hash(state); + } +} + +// ---------------------------------------------------------------------------- + +/// Font of unknown size. +/// +/// Which style of font: [`Monospace`][`FontFamily::Monospace`], [`Proportional`][`FontFamily::Proportional`], +/// or by user-chosen name. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum FontFamily { + /// A font where some characters are wider than other (e.g. 'w' is wider than 'i'). + /// + /// Proportional fonts are easier to read and should be the preferred choice in most situations. + Proportional, + + /// A font where each character is the same width (`w` is the same width as `i`). + /// + /// Useful for code snippets, or when you need to align numbers or text. + Monospace, + + /// One of the names in [`FontDefinitions::families`]. + /// + /// ``` + /// # use epaint::FontFamily; + /// // User-chosen names: + /// FontFamily::Name("arial".into()); + /// FontFamily::Name("serif".into()); + /// ``` + Name(Arc), +} + +impl Default for FontFamily { + #[inline] + fn default() -> Self { + FontFamily::Proportional + } +} + +impl std::fmt::Display for FontFamily { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Monospace => "Monospace".fmt(f), + Self::Proportional => "Proportional".fmt(f), + Self::Name(name) => (*name).fmt(f), + } + } +} + +// ---------------------------------------------------------------------------- + /// A `.ttf` or `.otf` file and a font face index. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct FontData { /// The content of a `.ttf` or `.otf` file. pub font: std::borrow::Cow<'static, [u8]>, + /// Which font face in the file to use. /// When in doubt, use `0`. pub index: u32, @@ -97,28 +156,12 @@ fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontAr /// /// Often you would start with [`FontDefinitions::default()`] and then add/change the contents. /// +/// This is how you install your own custom fonts: /// ``` -/// # use {epaint::text::{FontDefinitions, TextStyle, FontFamily}}; +/// # use {epaint::text::{FontDefinitions, FontFamily, FontData}}; /// # struct FakeEguiCtx {}; /// # impl FakeEguiCtx { fn set_fonts(&self, _: FontDefinitions) {} } -/// # let ctx = FakeEguiCtx {}; -/// let mut fonts = FontDefinitions::default(); -/// -/// // Large button text: -/// fonts.family_and_size.insert( -/// TextStyle::Button, -/// (FontFamily::Proportional, 32.0) -/// ); -/// -/// ctx.set_fonts(fonts); -/// ``` -/// -/// You can also install your own custom fonts: -/// ``` -/// # use {epaint::text::{FontDefinitions, TextStyle, FontFamily, FontData}}; -/// # struct FakeEguiCtx {}; -/// # impl FakeEguiCtx { fn set_fonts(&self, _: FontDefinitions) {} } -/// # let ctx = FakeEguiCtx {}; +/// # let egui_ctx = FakeEguiCtx {}; /// let mut fonts = FontDefinitions::default(); /// /// // Install my own font (maybe supporting non-latin characters): @@ -126,14 +169,14 @@ fn ab_glyph_font_from_font_data(name: &str, data: &FontData) -> ab_glyph::FontAr /// FontData::from_static(include_bytes!("../../fonts/Ubuntu-Light.ttf"))); // .ttf and .otf supported /// /// // Put my font first (highest priority): -/// fonts.fonts_for_family.get_mut(&FontFamily::Proportional).unwrap() +/// fonts.families.get_mut(&FontFamily::Proportional).unwrap() /// .insert(0, "my_font".to_owned()); /// /// // Put my font as last fallback for monospace: -/// fonts.fonts_for_family.get_mut(&FontFamily::Monospace).unwrap() +/// fonts.families.get_mut(&FontFamily::Monospace).unwrap() /// .push("my_font".to_owned()); /// -/// ctx.set_fonts(fonts); +/// egui_ctx.set_fonts(fonts); /// ``` #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -150,10 +193,8 @@ pub struct FontDefinitions { /// When looking for a character glyph `epaint` will start with /// the first font and then move to the second, and so on. /// So the first font is the primary, and then comes a list of fallbacks in order of priority. - pub fonts_for_family: BTreeMap>, - - /// The [`FontFamily`] and size you want to use for a specific [`TextStyle`]. - pub family_and_size: BTreeMap, + // TODO: per font size-modifier. + pub families: BTreeMap>, } impl Default for FontDefinitions { @@ -161,7 +202,7 @@ impl Default for FontDefinitions { #[allow(unused)] let mut font_data: BTreeMap = BTreeMap::new(); - let mut fonts_for_family = BTreeMap::new(); + let mut families = BTreeMap::new(); #[cfg(feature = "default_fonts")] { @@ -185,7 +226,7 @@ impl Default for FontDefinitions { FontData::from_static(include_bytes!("../../fonts/emoji-icon-font.ttf")), ); - fonts_for_family.insert( + families.insert( FontFamily::Monospace, vec![ "Hack".to_owned(), @@ -194,7 +235,7 @@ impl Default for FontDefinitions { "emoji-icon-font".to_owned(), ], ); - fonts_for_family.insert( + families.insert( FontFamily::Proportional, vec![ "Ubuntu-Light".to_owned(), @@ -206,49 +247,240 @@ impl Default for FontDefinitions { #[cfg(not(feature = "default_fonts"))] { - fonts_for_family.insert(FontFamily::Monospace, vec![]); - fonts_for_family.insert(FontFamily::Proportional, vec![]); + families.insert(FontFamily::Monospace, vec![]); + families.insert(FontFamily::Proportional, vec![]); } - let mut family_and_size = BTreeMap::new(); - family_and_size.insert(TextStyle::Small, (FontFamily::Proportional, 10.0)); - family_and_size.insert(TextStyle::Body, (FontFamily::Proportional, 14.0)); - family_and_size.insert(TextStyle::Button, (FontFamily::Proportional, 14.0)); - family_and_size.insert(TextStyle::Heading, (FontFamily::Proportional, 20.0)); - family_and_size.insert(TextStyle::Monospace, (FontFamily::Monospace, 14.0)); - Self { font_data, - fonts_for_family, - family_and_size, + families, } } } +// ---------------------------------------------------------------------------- + /// The collection of fonts used by `epaint`. /// /// Required in order to paint text. -pub struct Fonts { - pixels_per_point: f32, - definitions: FontDefinitions, - fonts: BTreeMap, - atlas: Arc>, - galley_cache: Mutex, -} +/// Create one and reuse. Cheap to clone. +/// +/// You need to call [`Self::begin_frame`] and [`Self::font_image_delta`] once every frame. +/// +/// Wrapper for `Arc>`. +pub struct Fonts(Arc>); impl Fonts { /// Create a new [`Fonts`] for text layout. /// This call is expensive, so only create one [`Fonts`] and then reuse it. - pub fn new(pixels_per_point: f32, definitions: FontDefinitions) -> Self { + /// + /// * `pixels_per_point`: how many physical pixels per logical "point". + /// * `max_texture_side`: largest supported texture size (one side). + pub fn new( + pixels_per_point: f32, + max_texture_side: usize, + definitions: FontDefinitions, + ) -> Self { + let fonts_and_cache = FontsAndCache { + fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions), + galley_cache: Default::default(), + }; + Self(Arc::new(Mutex::new(fonts_and_cache))) + } + + /// Call at the start of each frame with the latest known + /// `pixels_per_point` and `max_texture_side`. + /// + /// Call after painting the previous frame, but before using [`Fonts`] for the new frame. + /// + /// This function will react to changes in `pixels_per_point` and `max_texture_side`, + /// as well as notice when the font atlas is getting full, and handle that. + pub fn begin_frame(&self, pixels_per_point: f32, max_texture_side: usize) { + let mut fonts_and_cache = self.0.lock(); + + let pixels_per_point_changed = + (fonts_and_cache.fonts.pixels_per_point - pixels_per_point).abs() > 1e-3; + let max_texture_side_changed = fonts_and_cache.fonts.max_texture_side != max_texture_side; + let font_atlas_almost_full = fonts_and_cache.fonts.atlas.lock().fill_ratio() > 0.8; + let needs_recreate = + pixels_per_point_changed || max_texture_side_changed || font_atlas_almost_full; + + if needs_recreate { + let definitions = fonts_and_cache.fonts.definitions.clone(); + + *fonts_and_cache = FontsAndCache { + fonts: FontsImpl::new(pixels_per_point, max_texture_side, definitions), + galley_cache: Default::default(), + }; + } + + fonts_and_cache.galley_cache.flush_cache(); + } + + /// Call at the end of each frame (before painting) to get the change to the font texture since last call. + pub fn font_image_delta(&self) -> Option { + self.lock().fonts.atlas.lock().take_delta() + } + + /// Access the underlying [`FontsAndCache`]. + #[doc(hidden)] + #[inline] + pub fn lock(&self) -> MutexGuard<'_, FontsAndCache> { + self.0.lock() + } + + #[inline] + pub fn pixels_per_point(&self) -> f32 { + self.lock().fonts.pixels_per_point + } + + #[inline] + pub fn max_texture_side(&self) -> usize { + self.lock().fonts.max_texture_side + } + + /// Current size of the font image. + /// Pass this to [`crate::Tessellator`]. + pub fn font_image_size(&self) -> [usize; 2] { + self.lock().fonts.atlas.lock().size() + } + + /// Width of this character in points. + #[inline] + pub fn glyph_width(&self, font_id: &FontId, c: char) -> f32 { + self.lock().fonts.glyph_width(font_id, c) + } + + /// Height of one row of text in points + #[inline] + pub fn row_height(&self, font_id: &FontId) -> f32 { + self.lock().fonts.row_height(font_id) + } + + /// List of all known font families. + pub fn families(&self) -> Vec { + self.lock() + .fonts + .definitions + .families + .keys() + .cloned() + .collect() + } + + /// Layout some text. + /// + /// This is the most advanced layout function. + /// See also [`Self::layout`], [`Self::layout_no_wrap`] and + /// [`Self::layout_delayed_color`]. + /// + /// The implementation uses memoization so repeated calls are cheap. + #[inline] + pub fn layout_job(&self, job: LayoutJob) -> Arc { + self.lock().layout_job(job) + } + + pub fn num_galleys_in_cache(&self) -> usize { + self.lock().galley_cache.num_galleys_in_cache() + } + + /// How full is the font atlas? + /// + /// This increases as new fonts and/or glyphs are used, + /// but can also decrease in a call to [`Self::begin_frame`]. + pub fn font_atlas_fill_ratio(&self) -> f32 { + self.lock().fonts.atlas.lock().fill_ratio() + } + + /// Will wrap text at the given width and line break at `\n`. + /// + /// The implementation uses memoization so repeated calls are cheap. + pub fn layout( + &self, + text: String, + font_id: FontId, + color: crate::Color32, + wrap_width: f32, + ) -> Arc { + let job = LayoutJob::simple(text, font_id, color, wrap_width); + self.layout_job(job) + } + + /// Will line break at `\n`. + /// + /// The implementation uses memoization so repeated calls are cheap. + pub fn layout_no_wrap( + &self, + text: String, + font_id: FontId, + color: crate::Color32, + ) -> Arc { + let job = LayoutJob::simple(text, font_id, color, f32::INFINITY); + self.layout_job(job) + } + + /// Like [`Self::layout`], made for when you want to pick a color for the text later. + /// + /// The implementation uses memoization so repeated calls are cheap. + pub fn layout_delayed_color( + &self, + text: String, + font_id: FontId, + wrap_width: f32, + ) -> Arc { + self.layout_job(LayoutJob::simple( + text, + font_id, + crate::Color32::TEMPORARY_COLOR, + wrap_width, + )) + } +} + +// ---------------------------------------------------------------------------- + +pub struct FontsAndCache { + pub fonts: FontsImpl, + galley_cache: GalleyCache, +} + +impl FontsAndCache { + fn layout_job(&mut self, job: LayoutJob) -> Arc { + self.galley_cache.layout(&mut self.fonts, job) + } +} + +// ---------------------------------------------------------------------------- + +/// The collection of fonts used by `epaint`. +/// +/// Required in order to paint text. +pub struct FontsImpl { + pixels_per_point: f32, + max_texture_side: usize, + definitions: FontDefinitions, + atlas: Arc>, + font_impl_cache: FontImplCache, + sized_family: ahash::AHashMap<(u32, FontFamily), Font>, +} + +impl FontsImpl { + /// Create a new [`FontsImpl`] for text layout. + /// This call is expensive, so only create one [`FontsImpl`] and then reuse it. + pub fn new( + pixels_per_point: f32, + max_texture_side: usize, + definitions: FontDefinitions, + ) -> Self { assert!( 0.0 < pixels_per_point && pixels_per_point < 100.0, "pixels_per_point out of range: {}", pixels_per_point ); - // We want an atlas big enough to be able to include all the Emojis in the `TextStyle::Heading`, - // so we can show the Emoji picker demo window. - let mut atlas = TextureAtlas::new([2048, 64]); + let texture_width = max_texture_side.at_most(16 * 1024); + let initial_height = 512; + let mut atlas = TextureAtlas::new([texture_width, initial_height]); { // Make the top left pixel fully white: @@ -259,31 +491,16 @@ impl Fonts { let atlas = Arc::new(Mutex::new(atlas)); - let mut font_impl_cache = FontImplCache::new(atlas.clone(), pixels_per_point, &definitions); - - let fonts = definitions - .family_and_size - .iter() - .map(|(&text_style, &(family, scale_in_points))| { - let fonts = &definitions.fonts_for_family.get(&family); - let fonts = fonts.unwrap_or_else(|| { - panic!("FontFamily::{:?} is not bound to any fonts", family) - }); - let fonts: Vec> = fonts - .iter() - .map(|font_name| font_impl_cache.font_impl(font_name, scale_in_points)) - .collect(); - - (text_style, Font::new(text_style, fonts)) - }) - .collect(); + let font_impl_cache = + FontImplCache::new(atlas.clone(), pixels_per_point, &definitions.font_data); Self { pixels_per_point, + max_texture_side, definitions, - fonts, atlas, - galley_cache: Default::default(), + font_impl_cache, + sized_family: Default::default(), } } @@ -292,110 +509,41 @@ impl Fonts { self.pixels_per_point } + #[inline] pub fn definitions(&self) -> &FontDefinitions { &self.definitions } - #[inline(always)] - pub fn round_to_pixel(&self, point: f32) -> f32 { - (point * self.pixels_per_point).round() / self.pixels_per_point - } + /// Get the right font implementation from size and [`FontFamily`]. + pub fn font(&mut self, font_id: &FontId) -> &mut Font { + let FontId { size, family } = font_id; + let scale_in_pixels = self.font_impl_cache.scale_as_pixels(*size); - #[inline(always)] - pub fn floor_to_pixel(&self, point: f32) -> f32 { - (point * self.pixels_per_point).floor() / self.pixels_per_point - } + self.sized_family + .entry((scale_in_pixels, family.clone())) + .or_insert_with(|| { + let fonts = &self.definitions.families.get(family); + let fonts = fonts.unwrap_or_else(|| { + panic!("FontFamily::{:?} is not bound to any fonts", family) + }); - /// Call each frame to get the change to the font texture since last call. - pub fn font_image_delta(&self) -> Option { - self.atlas.lock().take_delta() - } + let fonts: Vec> = fonts + .iter() + .map(|font_name| self.font_impl_cache.font_impl(scale_in_pixels, font_name)) + .collect(); - /// Current size of the font image - pub fn font_image_size(&self) -> [usize; 2] { - self.atlas.lock().size() + Font::new(fonts) + }) } /// Width of this character in points. - pub fn glyph_width(&self, text_style: TextStyle, c: char) -> f32 { - self.fonts[&text_style].glyph_width(c) + fn glyph_width(&mut self, font_id: &FontId, c: char) -> f32 { + self.font(font_id).glyph_width(c) } /// Height of one row of text. In points - pub fn row_height(&self, text_style: TextStyle) -> f32 { - self.fonts[&text_style].row_height() - } - - /// Layout some text. - /// This is the most advanced layout function. - /// See also [`Self::layout`], [`Self::layout_no_wrap`] and - /// [`Self::layout_delayed_color`]. - /// - /// The implementation uses memoization so repeated calls are cheap. - pub fn layout_job(&self, job: LayoutJob) -> Arc { - self.galley_cache.lock().layout(self, job) - } - - /// Will wrap text at the given width and line break at `\n`. - /// - /// The implementation uses memoization so repeated calls are cheap. - pub fn layout( - &self, - text: String, - text_style: TextStyle, - color: crate::Color32, - wrap_width: f32, - ) -> Arc { - let job = LayoutJob::simple(text, text_style, color, wrap_width); - self.layout_job(job) - } - - /// Will line break at `\n`. - /// - /// The implementation uses memoization so repeated calls are cheap. - pub fn layout_no_wrap( - &self, - text: String, - text_style: TextStyle, - color: crate::Color32, - ) -> Arc { - let job = LayoutJob::simple(text, text_style, color, f32::INFINITY); - self.layout_job(job) - } - - /// Like [`Self::layout`], made for when you want to pick a color for the text later. - /// - /// The implementation uses memoization so repeated calls are cheap. - pub fn layout_delayed_color( - &self, - text: String, - text_style: TextStyle, - wrap_width: f32, - ) -> Arc { - self.layout_job(LayoutJob::simple( - text, - text_style, - crate::Color32::TEMPORARY_COLOR, - wrap_width, - )) - } - - 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 { - type Output = Font; - - #[inline(always)] - fn index(&self, text_style: TextStyle) -> &Font { - &self.fonts[&text_style] + fn row_height(&mut self, font_id: &FontId) -> f32 { + self.font(font_id).row_height() } } @@ -415,7 +563,7 @@ struct GalleyCache { } impl GalleyCache { - fn layout(&mut self, fonts: &Fonts, job: LayoutJob) -> Arc { + fn layout(&mut self, fonts: &mut FontsImpl, job: LayoutJob) -> Arc { let hash = crate::util::hash(&job); // TODO: even faster hasher? match self.cache.entry(hash) { @@ -441,7 +589,7 @@ impl GalleyCache { } /// Must be called once per frame to clear the [`Galley`] cache. - pub fn end_frame(&mut self) { + pub fn flush_cache(&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 @@ -457,19 +605,17 @@ struct FontImplCache { pixels_per_point: f32, ab_glyph_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 - cache: Vec<(String, f32, Arc)>, + /// Map font pixel sizes and names to the cached `FontImpl`. + cache: ahash::AHashMap<(u32, String), Arc>, } impl FontImplCache { pub fn new( atlas: Arc>, pixels_per_point: f32, - definitions: &super::FontDefinitions, + font_data: &BTreeMap, ) -> Self { - let ab_glyph_fonts = definitions - .font_data + let ab_glyph_fonts = font_data .iter() .map(|(name, font_data)| (name.clone(), ab_glyph_font_from_font_data(name, font_data))) .collect(); @@ -482,42 +628,47 @@ impl FontImplCache { } } - pub fn ab_glyph_font(&self, font_name: &str) -> ab_glyph::FontArc { - self.ab_glyph_fonts - .get(font_name) - .unwrap_or_else(|| panic!("No font data found for {:?}", font_name)) - .clone() + #[inline] + pub fn scale_as_pixels(&self, scale_in_points: f32) -> u32 { + let scale_in_pixels = self.pixels_per_point * scale_in_points; + + // Round to an even number of physical pixels to get even kerning. + // See https://github.com/emilk/egui/issues/382 + scale_in_pixels.round() as u32 } - pub fn font_impl(&mut self, font_name: &str, scale_in_points: f32) -> Arc { - for entry in &self.cache { - if (entry.0.as_str(), entry.1) == (font_name, scale_in_points) { - return entry.2.clone(); - } - } + pub fn font_impl(&mut self, scale_in_pixels: u32, font_name: &str) -> Arc { + let scale_in_pixels = if font_name == "emoji-icon-font" { + (scale_in_pixels as f32 * 0.8).round() as u32 // TODO: remove font scale HACK! + } else { + scale_in_pixels + }; let y_offset = if font_name == "emoji-icon-font" { - scale_in_points * 0.235 // TODO: remove font alignment hack + let scale_in_points = scale_in_pixels as f32 / self.pixels_per_point; + scale_in_points * 0.29375 // TODO: remove font alignment hack } else { 0.0 }; let y_offset = y_offset - 3.0; // Tweaked to make text look centered in buttons and text edit fields - let scale_in_points = if font_name == "emoji-icon-font" { - scale_in_points * 0.8 // TODO: remove HACK! - } else { - scale_in_points - }; - - let font_impl = Arc::new(FontImpl::new( - self.atlas.clone(), - self.pixels_per_point, - self.ab_glyph_font(font_name), - scale_in_points, - y_offset, - )); self.cache - .push((font_name.to_owned(), scale_in_points, font_impl.clone())); - font_impl + .entry((scale_in_pixels, font_name.to_owned())) + .or_insert_with(|| { + let ab_glyph_font = self + .ab_glyph_fonts + .get(font_name) + .unwrap_or_else(|| panic!("No font data found for {:?}", font_name)) + .clone(); + + Arc::new(FontImpl::new( + self.atlas.clone(), + self.pixels_per_point, + ab_glyph_font, + scale_in_pixels, + y_offset, + )) + }) + .clone() } } diff --git a/epaint/src/text/mod.rs b/epaint/src/text/mod.rs index ee2b5c07..32d4070a 100644 --- a/epaint/src/text/mod.rs +++ b/epaint/src/text/mod.rs @@ -10,7 +10,7 @@ mod text_layout_types; pub const TAB_SIZE: usize = 4; pub use { - fonts::{FontData, FontDefinitions, FontFamily, Fonts, TextStyle}, + fonts::{FontData, FontDefinitions, FontFamily, FontId, Fonts, FontsImpl}, text_layout::layout, text_layout_types::*, }; diff --git a/epaint/src/text/text_layout.rs b/epaint/src/text/text_layout.rs index 8ea82e64..a70127c7 100644 --- a/epaint/src/text/text_layout.rs +++ b/epaint/src/text/text_layout.rs @@ -1,9 +1,41 @@ use std::ops::RangeInclusive; -use super::{Fonts, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals}; +use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, Row, RowVisuals}; use crate::{mutex::Arc, Color32, Mesh, Stroke, Vertex}; use emath::*; +// ---------------------------------------------------------------------------- + +/// Represents GUI scale and convenience methods for rounding to pixels. +#[derive(Clone, Copy)] +struct PointScale { + pub pixels_per_point: f32, +} + +impl PointScale { + #[inline(always)] + pub fn new(pixels_per_point: f32) -> Self { + Self { pixels_per_point } + } + + #[inline(always)] + pub fn pixels_per_point(&self) -> f32 { + self.pixels_per_point + } + + #[inline(always)] + pub fn round_to_pixel(&self, point: f32) -> f32 { + (point * self.pixels_per_point).round() / self.pixels_per_point + } + + #[inline(always)] + pub fn floor_to_pixel(&self, point: f32) -> f32 { + (point * self.pixels_per_point).floor() / self.pixels_per_point + } +} + +// ---------------------------------------------------------------------------- + /// Temporary storage before line-wrapping. #[derive(Default, Clone)] struct Paragraph { @@ -16,14 +48,16 @@ struct Paragraph { /// Layout text into a [`Galley`]. /// -/// In most cases you should use [`Fonts::layout_job`] instead +/// In most cases you should use [`crate::Fonts::layout_job`] instead /// since that memoizes the input, making subsequent layouting of the same text much faster. -pub fn layout(fonts: &Fonts, job: Arc) -> Galley { +pub fn layout(fonts: &mut FontsImpl, job: Arc) -> Galley { let mut paragraphs = vec![Paragraph::default()]; for (section_index, section) in job.sections.iter().enumerate() { layout_section(fonts, &job, section_index as u32, section, &mut paragraphs); } + let point_scale = PointScale::new(fonts.pixels_per_point()); + let mut rows = rows_from_paragraphs(paragraphs, job.wrap_width); let justify = job.justify && job.wrap_width.is_finite(); @@ -33,15 +67,15 @@ pub fn layout(fonts: &Fonts, job: Arc) -> Galley { for (i, row) in rows.iter_mut().enumerate() { let is_last_row = i + 1 == num_rows; let justify_row = justify && !row.ends_with_newline && !is_last_row; - halign_and_jusitfy_row(fonts, row, job.halign, job.wrap_width, justify_row); + halign_and_jusitfy_row(point_scale, row, job.halign, job.wrap_width, justify_row); } } - galley_from_rows(fonts, job, rows) + galley_from_rows(point_scale, job, rows) } fn layout_section( - fonts: &Fonts, + fonts: &mut FontsImpl, job: &LayoutJob, section_index: u32, section: &LayoutSection, @@ -52,7 +86,7 @@ fn layout_section( byte_range, format, } = section; - let font = &fonts[format.style]; + let font = fonts.font(&format.font_id); let font_height = font.row_height(); let mut paragraph = out_paragraphs.last_mut().unwrap(); @@ -213,7 +247,7 @@ fn line_break(paragraph: &Paragraph, wrap_width: f32, out_rows: &mut Vec) { } fn halign_and_jusitfy_row( - fonts: &Fonts, + point_scale: PointScale, row: &mut Row, halign: Align, wrap_width: f32, @@ -278,7 +312,7 @@ fn halign_and_jusitfy_row( // Add an integral number of pixels between each glyph, // and add the balance to the spaces: - extra_x_per_glyph = fonts.floor_to_pixel(extra_x_per_glyph); + extra_x_per_glyph = point_scale.floor_to_pixel(extra_x_per_glyph); extra_x_per_space = (target_width - original_width @@ -290,7 +324,7 @@ fn halign_and_jusitfy_row( for glyph in &mut row.glyphs { glyph.pos.x += translate_x; - glyph.pos.x = fonts.round_to_pixel(glyph.pos.x); + glyph.pos.x = point_scale.round_to_pixel(glyph.pos.x); translate_x += extra_x_per_glyph; if glyph.chr.is_whitespace() { translate_x += extra_x_per_space; @@ -303,7 +337,7 @@ fn halign_and_jusitfy_row( } /// Calculate the Y positions and tessellate the text. -fn galley_from_rows(fonts: &Fonts, job: Arc, mut rows: Vec) -> Galley { +fn galley_from_rows(point_scale: PointScale, job: Arc, mut rows: Vec) -> Galley { let mut first_row_min_height = job.first_row_min_height; let mut cursor_y = 0.0; let mut min_x: f32 = 0.0; @@ -314,13 +348,13 @@ fn galley_from_rows(fonts: &Fonts, job: Arc, mut rows: Vec) -> G for glyph in &row.glyphs { row_height = row_height.max(glyph.size.y); } - row_height = fonts.round_to_pixel(row_height); + row_height = point_scale.round_to_pixel(row_height); // Now positions each glyph: for glyph in &mut row.glyphs { let format = &job.sections[glyph.section_index as usize].format; glyph.pos.y = cursor_y + format.valign.to_factor() * (row_height - glyph.size.y); - glyph.pos.y = fonts.round_to_pixel(glyph.pos.y); + glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y); } row.rect.min.y = cursor_y; @@ -329,7 +363,7 @@ fn galley_from_rows(fonts: &Fonts, job: Arc, mut rows: Vec) -> G min_x = min_x.min(row.rect.min.x); max_x = max_x.max(row.rect.max.x); cursor_y += row_height; - cursor_y = fonts.round_to_pixel(cursor_y); + cursor_y = point_scale.round_to_pixel(cursor_y); } let format_summary = format_summary(&job); @@ -339,7 +373,7 @@ fn galley_from_rows(fonts: &Fonts, job: Arc, mut rows: Vec) -> G let mut num_indices = 0; for row in &mut rows { - row.visuals = tessellate_row(fonts, &job, &format_summary, row); + row.visuals = tessellate_row(point_scale, &job, &format_summary, row); mesh_bounds = mesh_bounds.union(row.visuals.mesh_bounds); num_vertices += row.visuals.mesh.vertices.len(); num_indices += row.visuals.mesh.indices.len(); @@ -375,7 +409,7 @@ fn format_summary(job: &LayoutJob) -> FormatSummary { } fn tessellate_row( - fonts: &Fonts, + point_scale: PointScale, job: &LayoutJob, format_summary: &FormatSummary, row: &mut Row, @@ -394,11 +428,11 @@ fn tessellate_row( } let glyph_vertex_start = mesh.vertices.len(); - tessellate_glyphs(fonts, job, row, &mut mesh); + tessellate_glyphs(point_scale, job, row, &mut mesh); let glyph_vertex_end = mesh.vertices.len(); if format_summary.any_underline { - add_row_hline(fonts, row, &mut mesh, |glyph| { + add_row_hline(point_scale, row, &mut mesh, |glyph| { let format = &job.sections[glyph.section_index as usize].format; let stroke = format.underline; let y = glyph.logical_rect().bottom(); @@ -407,7 +441,7 @@ fn tessellate_row( } if format_summary.any_strikethrough { - add_row_hline(fonts, row, &mut mesh, |glyph| { + add_row_hline(point_scale, row, &mut mesh, |glyph| { let format = &job.sections[glyph.section_index as usize].format; let stroke = format.strikethrough; let y = glyph.logical_rect().center().y; @@ -469,13 +503,13 @@ fn add_row_backgrounds(job: &LayoutJob, row: &Row, mesh: &mut Mesh) { end_run(run_start.take(), last_rect.right()); } -fn tessellate_glyphs(fonts: &Fonts, job: &LayoutJob, row: &Row, mesh: &mut Mesh) { +fn tessellate_glyphs(point_scale: PointScale, job: &LayoutJob, row: &Row, mesh: &mut Mesh) { for glyph in &row.glyphs { let uv_rect = glyph.uv_rect; if !uv_rect.is_nothing() { let mut left_top = glyph.pos + uv_rect.offset; - left_top.x = fonts.round_to_pixel(left_top.x); - left_top.y = fonts.round_to_pixel(left_top.y); + left_top.x = point_scale.round_to_pixel(left_top.x); + left_top.y = point_scale.round_to_pixel(left_top.y); let rect = Rect::from_min_max(left_top, left_top + uv_rect.size); let uv = Rect::from_min_max( @@ -523,14 +557,14 @@ fn tessellate_glyphs(fonts: &Fonts, job: &LayoutJob, row: &Row, mesh: &mut Mesh) /// Add a horizontal line over a row of glyphs with a stroke and y decided by a callback. fn add_row_hline( - fonts: &Fonts, + point_scale: PointScale, row: &Row, mesh: &mut Mesh, stroke_and_y: impl Fn(&Glyph) -> (Stroke, f32), ) { let mut end_line = |start: Option<(Stroke, Pos2)>, stop_x: f32| { if let Some((stroke, start)) = start { - add_hline(fonts, [start, pos2(stop_x, start.y)], stroke, mesh); + add_hline(point_scale, [start, pos2(stop_x, start.y)], stroke, mesh); } }; @@ -559,14 +593,14 @@ fn add_row_hline( end_line(line_start.take(), last_right_x); } -fn add_hline(fonts: &Fonts, [start, stop]: [Pos2; 2], stroke: Stroke, mesh: &mut Mesh) { +fn add_hline(point_scale: PointScale, [start, stop]: [Pos2; 2], stroke: Stroke, mesh: &mut Mesh) { let antialiased = true; if antialiased { let mut path = crate::tessellator::Path::default(); // TODO: reuse this to avoid re-allocations. path.add_line_segment([start, stop]); let options = crate::tessellator::TessellationOptions::from_pixels_per_point( - fonts.pixels_per_point(), + point_scale.pixels_per_point(), ); path.stroke_open(stroke, &options, mesh); } else { @@ -574,12 +608,12 @@ fn add_hline(fonts: &Fonts, [start, stop]: [Pos2; 2], stroke: Stroke, mesh: &mut assert_eq!(start.y, stop.y); - let min_y = fonts.round_to_pixel(start.y - 0.5 * stroke.width); - let max_y = fonts.round_to_pixel(min_y + stroke.width); + let min_y = point_scale.round_to_pixel(start.y - 0.5 * stroke.width); + let max_y = point_scale.round_to_pixel(min_y + stroke.width); let rect = Rect::from_min_max( - pos2(fonts.round_to_pixel(start.x), min_y), - pos2(fonts.round_to_pixel(stop.x), max_y), + pos2(point_scale.round_to_pixel(start.x), min_y), + pos2(point_scale.round_to_pixel(stop.x), max_y), ); mesh.add_colored_rect(rect, stroke.color); diff --git a/epaint/src/text/text_layout_types.rs b/epaint/src/text/text_layout_types.rs index ea572d74..0b608df0 100644 --- a/epaint/src/text/text_layout_types.rs +++ b/epaint/src/text/text_layout_types.rs @@ -3,7 +3,7 @@ use std::ops::Range; use super::{cursor::*, font::UvRect}; -use crate::{mutex::Arc, Color32, Mesh, Stroke, TextStyle}; +use crate::{mutex::Arc, Color32, FontId, Mesh, Stroke}; use emath::*; /// Describes the task of laying out text. @@ -14,14 +14,14 @@ use emath::*; /// /// ## Example: /// ``` -/// use epaint::{Color32, text::{LayoutJob, TextFormat}, TextStyle}; +/// use epaint::{Color32, text::{LayoutJob, TextFormat}, FontFamily, FontId}; /// /// let mut job = LayoutJob::default(); /// job.append( /// "Hello ", /// 0.0, /// TextFormat { -/// style: TextStyle::Body, +/// font_id: FontId::new(14.0, FontFamily::Proportional), /// color: Color32::WHITE, /// ..Default::default() /// }, @@ -30,7 +30,7 @@ use emath::*; /// "World!", /// 0.0, /// TextFormat { -/// style: TextStyle::Monospace, +/// font_id: FontId::new(14.0, FontFamily::Monospace), /// color: Color32::BLACK, /// ..Default::default() /// }, @@ -90,12 +90,12 @@ impl Default for LayoutJob { impl LayoutJob { /// Break on `\n` and at the given wrap width. #[inline] - pub fn simple(text: String, text_style: TextStyle, color: Color32, wrap_width: f32) -> Self { + pub fn simple(text: String, font_id: FontId, color: Color32, wrap_width: f32) -> Self { Self { sections: vec![LayoutSection { leading_space: 0.0, byte_range: 0..text.len(), - format: TextFormat::simple(text_style, color), + format: TextFormat::simple(font_id, color), }], text, wrap_width, @@ -106,12 +106,12 @@ impl LayoutJob { /// Does not break on `\n`, but shows the replacement character instead. #[inline] - pub fn simple_singleline(text: String, text_style: TextStyle, color: Color32) -> Self { + pub fn simple_singleline(text: String, font_id: FontId, color: Color32) -> Self { Self { sections: vec![LayoutSection { leading_space: 0.0, byte_range: 0..text.len(), - format: TextFormat::simple(text_style, color), + format: TextFormat::simple(font_id, color), }], text, wrap_width: f32::INFINITY, @@ -156,7 +156,7 @@ impl LayoutJob { pub fn font_height(&self, fonts: &crate::Fonts) -> f32 { let mut max_height = 0.0_f32; for section in &self.sections { - max_height = max_height.max(fonts.row_height(section.format.style)); + max_height = max_height.max(fonts.row_height(§ion.format.font_id)); } max_height } @@ -213,10 +213,10 @@ impl std::hash::Hash for LayoutSection { // ---------------------------------------------------------------------------- -#[derive(Copy, Clone, Debug, Hash, PartialEq)] +#[derive(Clone, Debug, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct TextFormat { - pub style: TextStyle, + pub font_id: FontId, /// Text color pub color: Color32, pub background: Color32, @@ -233,7 +233,7 @@ impl Default for TextFormat { #[inline] fn default() -> Self { Self { - style: TextStyle::Body, + font_id: FontId::default(), color: Color32::GRAY, background: Color32::TRANSPARENT, italics: false, @@ -246,9 +246,9 @@ impl Default for TextFormat { impl TextFormat { #[inline] - pub fn simple(style: TextStyle, color: Color32) -> Self { + pub fn simple(font_id: FontId, color: Color32) -> Self { Self { - style, + font_id, color, ..Default::default() } diff --git a/epaint/src/texture_atlas.rs b/epaint/src/texture_atlas.rs index 8e5e1c52..2d0a86b2 100644 --- a/epaint/src/texture_atlas.rs +++ b/epaint/src/texture_atlas.rs @@ -39,15 +39,20 @@ pub struct TextureAtlas { /// Used for when allocating new rectangles. cursor: (usize, usize), row_height: usize, + + /// Set when someone requested more space than was available. + overflowed: bool, } impl TextureAtlas { pub fn new(size: [usize; 2]) -> Self { + assert!(size[0] >= 1024, "Tiny texture atlas"); Self { image: AlphaImage::new(size), dirty: Rectu::EVERYTHING, cursor: (0, 0), row_height: 0, + overflowed: false, } } @@ -55,6 +60,20 @@ impl TextureAtlas { self.image.size } + fn max_height(&self) -> usize { + // the initial width is likely the max texture side size + self.image.width() + } + + /// When this get high, it might be time to clear and start over! + pub fn fill_ratio(&self) -> f32 { + if self.overflowed { + 1.0 + } else { + (self.cursor.1 + self.row_height) as f32 / self.max_height() as f32 + } + } + /// Call to get the change to the image since last call. pub fn take_delta(&mut self) -> Option { let dirty = std::mem::replace(&mut self.dirty, Rectu::NOTHING); @@ -92,7 +111,15 @@ impl TextureAtlas { } self.row_height = self.row_height.max(h); - if resize_to_min_height(&mut self.image, self.cursor.1 + self.row_height) { + + let required_height = self.cursor.1 + self.row_height; + + if required_height > self.max_height() { + // This is a bad place to be - we need to start reusing space :/ + eprintln!("epaint texture atlas overflowed!"); + self.cursor = (0, self.image.height() / 3); // Restart a bit down - the top of the atlas has too many important things in it + self.overflowed = true; // this will signal the user that we need to recreate the texture atlas next frame. + } else if resize_to_min_height(&mut self.image, required_height) { self.dirty = Rectu::EVERYTHING; } @@ -108,8 +135,8 @@ impl TextureAtlas { } } -fn resize_to_min_height(image: &mut AlphaImage, min_height: usize) -> bool { - while min_height >= image.height() { +fn resize_to_min_height(image: &mut AlphaImage, required_height: usize) -> bool { + while required_height >= image.height() { image.size[1] *= 2; // double the height } diff --git a/epi/src/lib.rs b/epi/src/lib.rs index 95fce001..a7a15b49 100644 --- a/epi/src/lib.rs +++ b/epi/src/lib.rs @@ -285,7 +285,7 @@ impl Frame { Self(Arc::new(Mutex::new(frame_data))) } - /// Convenience to access the underlying `backend::FrameData`. + /// Access the underlying [`backend::FrameData`]. #[doc(hidden)] #[inline] pub fn lock(&self) -> std::sync::MutexGuard<'_, backend::FrameData> {