diff --git a/Cargo.lock b/Cargo.lock index fe02a86dde8e..bc17eaf6bfcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +[[package]] +name = "ahash" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" + [[package]] name = "ahash" version = "0.4.5" @@ -519,10 +525,10 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" name = "egui" version = "0.2.0" dependencies = [ - "ahash", + "ahash 0.4.5", "criterion", + "fontdue", "parking_lot", - "rusttype", "serde", "serde_json", ] @@ -574,6 +580,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fontdue" +version = "0.3.2" +source = "git+https://github.com/mooman219/fontdue#3e2f51fe70fd7ab21ff70652ff8c2798f6e9fc65" +dependencies = [ + "hashbrown", + "ttf-parser 0.8.2", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -716,6 +731,16 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177" +[[package]] +name = "hashbrown" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" +dependencies = [ + "ahash 0.3.8", + "autocfg", +] + [[package]] name = "hermit-abi" version = "0.1.17" @@ -1143,7 +1168,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" dependencies = [ - "ttf-parser", + "ttf-parser 0.6.2", ] [[package]] @@ -1525,6 +1550,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" +[[package]] +name = "ttf-parser" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d973cfa0e6124166b50a1105a67c85de40bbc625082f35c0f56f84cb1fb0a827" + [[package]] name = "unicode-width" version = "0.1.8" diff --git a/egui/Cargo.toml b/egui/Cargo.toml index 214eaf484cd4..92aa18ce8003 100644 --- a/egui/Cargo.toml +++ b/egui/Cargo.toml @@ -17,7 +17,7 @@ include = [ "**/*.rs", "Cargo.toml", "fonts/ProggyClean.ttf", "fonts/Comfortaa-R [dependencies] ahash = { version = "0.4", features = ["std"], default-features = false } parking_lot = "0.11" -rusttype = "0.9" +fontdue = { version = "0.3", git = "https://github.com/mooman219/fontdue" } serde = { version = "1", features = ["derive"], optional = true } serde_json = { version = "1", optional = true } diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index 978d170686a1..91e2b9b2a95a 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -169,8 +169,9 @@ struct Prepared { impl CollapsingHeader { fn begin(self, ui: &mut Ui) -> Prepared { - assert!( - ui.layout().dir() == Direction::Vertical, + assert_eq!( + ui.layout().dir(), + Direction::Vertical, "Horizontal collapsing is unimplemented" ); let Self { @@ -186,14 +187,14 @@ impl CollapsingHeader { let available = ui.available_finite(); let text_pos = available.min + vec2(ui.style().spacing.indent, 0.0); - let galley = label.layout_width(ui, available.right() - text_pos.x); - let text_max_x = text_pos.x + galley.size.x; + let layout = label.layout_width(ui, available.right() - text_pos.x); + let text_max_x = text_pos.x + layout.size.x; let desired_width = text_max_x - available.left(); let desired_width = desired_width.max(available.width()); let mut desired_size = vec2( desired_width, - galley.size.y + 2.0 * ui.style().spacing.button_padding.y, + layout.size.y + 2.0 * ui.style().spacing.button_padding.y, ); desired_size = desired_size.at_least(ui.style().spacing.interact_size); let rect = ui.allocate_space(desired_size); @@ -201,7 +202,7 @@ impl CollapsingHeader { let header_response = ui.interact(rect, id, Sense::click()); let text_pos = pos2( text_pos.x, - header_response.rect.center().y - galley.size.y / 2.0, + header_response.rect.center().y - layout.size.y / 2.0, ); let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open); @@ -226,9 +227,9 @@ impl CollapsingHeader { } let painter = ui.painter(); - painter.galley( + painter.layout( text_pos, - galley, + layout, label.text_style_or_default(ui.style()), ui.style().interact(&header_response).text_color(), ); diff --git a/egui/src/containers/window.rs b/egui/src/containers/window.rs index 77ce625807f5..0ae6af48ffcc 100644 --- a/egui/src/containers/window.rs +++ b/egui/src/containers/window.rs @@ -2,9 +2,7 @@ use std::sync::Arc; -use crate::{paint::*, widgets::*, *}; - -use super::*; +use crate::{paint::fonts::GlyphLayout, paint::*, widgets::*, *}; /// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default). /// @@ -219,7 +217,7 @@ impl<'open> Window<'open> { // First interact (move etc) to avoid frame delay: let last_frame_outer_rect = area.state().rect(); let interaction = if possible.movable || possible.resizable { - let title_bar_height = title_label.font_height(ctx.fonts(), &ctx.style()) + let title_bar_height = title_label.font_height(&ctx.fonts().lock(), &ctx.style()) + 1.0 * ctx.style().spacing.item_spacing.y; // this could be better let margins = 2.0 * frame.margin + vec2(0.0, title_bar_height); @@ -599,7 +597,7 @@ fn paint_frame_interaction( struct TitleBar { title_label: Label, - title_galley: font::Galley, + title_layout: GlyphLayout, title_rect: Rect, rect: Rect, } @@ -613,7 +611,7 @@ fn show_title_bar( collapsible: bool, ) -> TitleBar { let (title_bar, response) = ui.horizontal(|ui| { - ui.set_min_height(title_label.font_height(ui.fonts(), ui.style())); + ui.set_min_height(title_label.font_height(&ui.fonts().lock(), ui.style())); let item_spacing = ui.style().spacing.item_spacing; let button_size = ui.style().spacing.icon_width; @@ -630,8 +628,8 @@ fn show_title_bar( collapsing_header::paint_icon(ui, openness, &collapse_button_response); } - let title_galley = title_label.layout(ui); - let title_rect = ui.allocate_space(title_galley.size); + let title_layout = title_label.layout(ui); + let title_rect = ui.allocate_space(title_layout.size); if show_close_button { // Reserve space for close button which will be added later (once we know our full width): @@ -649,7 +647,7 @@ fn show_title_bar( TitleBar { title_label, - title_galley, + title_layout, title_rect, rect: Rect::invalid(), // Will be filled in later } @@ -685,7 +683,7 @@ impl TitleBar { // TODO: pick style for title based on move interaction self.title_label - .paint_galley(ui, self.title_rect.min, self.title_galley); + .paint_layout(ui, self.title_rect.min, self.title_layout); if let Some(content_response) = &content_response { // paint separator between title and content: diff --git a/egui/src/context.rs b/egui/src/context.rs index 2463fa1c4130..64af66563e92 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -25,7 +25,7 @@ struct Options { /// Controls the tessellator. paint_options: paint::PaintOptions, /// Font sizes etc. - font_definitions: FontDefinitions, + font_configuration: FontConfiguration, } /// Thi is the first thing you need when working with Egui. @@ -39,7 +39,7 @@ struct Options { pub struct Context { options: Mutex<Options>, /// None until first call to `begin_frame`. - fonts: Option<Arc<Fonts>>, + fonts: Option<Arc<Mutex<Fonts>>>, memory: Arc<Mutex<Memory>>, animation_manager: Arc<Mutex<AnimationManager>>, @@ -110,24 +110,24 @@ impl Context { /// Not valid until first call to `begin_frame()` /// That's because since we don't know the proper `pixels_per_point` until then. - pub fn fonts(&self) -> &Fonts { - &*self - .fonts + pub fn fonts(&self) -> Arc<Mutex<Fonts>> { + self.fonts .as_ref() .expect("No fonts available until first call to Context::begin_frame()`") + .clone() } /// The Egui texture, containing font characters etc.. /// Not valid until first call to `begin_frame()` /// That's because since we don't know the proper `pixels_per_point` until then. pub fn texture(&self) -> Arc<paint::Texture> { - self.fonts().texture() + self.fonts().lock().texture() } /// Will become active at the start of the next frame. /// `pixels_per_point` will be ignored (overwritten at start of each frame with the contents of input) - pub fn set_fonts(&self, font_definitions: FontDefinitions) { - lock(&self.options, "options").font_definitions = font_definitions; + pub fn set_fonts(&self, font_configuration: FontConfiguration) { + lock(&self.options, "options").font_configuration = font_configuration; } pub fn style(&self) -> Arc<Style> { @@ -183,14 +183,16 @@ impl Context { self.used_ids.lock().clear(); self.input = std::mem::take(&mut self.input).begin_frame(new_raw_input); - let mut font_definitions = lock(&self.options, "options").font_definitions.clone(); - font_definitions.pixels_per_point = self.input.pixels_per_point(); + let mut font_configuration = lock(&self.options, "options").font_configuration.clone(); + font_configuration.pixels_per_point = self.input.pixels_per_point(); let same_as_current = match &self.fonts { None => false, - Some(fonts) => *fonts.definitions() == font_definitions, + Some(fonts) => *fonts.lock().configuration() == font_configuration, }; if !same_as_current { - self.fonts = Some(Arc::new(Fonts::from_definitions(font_definitions))); + self.fonts = Some(Arc::new(Mutex::new(Fonts::from_definitions( + font_configuration, + )))); } } @@ -523,10 +525,11 @@ impl Context { CollapsingHeader::new("Fonts") .default_open(false) .show(ui, |ui| { - let mut font_definitions = self.fonts().definitions().clone(); - font_definitions.ui(ui); - self.fonts().texture().ui(ui); - self.set_fonts(font_definitions); + let mut font_configuration = self.fonts().lock().configuration().clone(); + font_configuration.ui(ui); + let texture = self.fonts().lock().texture(); + texture.ui(ui); + self.set_fonts(font_configuration); }); CollapsingHeader::new("Painting") diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index 6bc5c5f389f4..9373b6d38228 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -129,7 +129,7 @@ impl FrameHistory { ui.fonts(), pos2(rect.left(), y), align::LEFT_BOTTOM, - text, + &text, TextStyle::Monospace, color::WHITE, )); diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index e864d19ff42f..1895b266a0a8 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -54,18 +54,18 @@ impl Texture { } } -impl paint::FontDefinitions { +impl paint::FontConfiguration { pub fn ui(&mut self, ui: &mut Ui) { - for (text_style, (_family, size)) in self.fonts.iter_mut() { + for (text_style, definition) in self.definitions.iter_mut() { // TODO: radio button for family ui.add( - Slider::f32(size, 4.0..=40.0) + Slider::f32(&mut definition.scale_in_points, 4.0..=40.0) .precision(0) .text(format!("{:?}", text_style)), ); } if ui.button("Reset fonts").clicked { - *self = paint::FontDefinitions::with_pixels_per_point(self.pixels_per_point); + *self = paint::FontConfiguration::with_pixels_per_point(self.pixels_per_point); } } } diff --git a/egui/src/paint/command.rs b/egui/src/paint/command.rs index 968b7bc35546..dc0fdb986bf7 100644 --- a/egui/src/paint/command.rs +++ b/egui/src/paint/command.rs @@ -1,8 +1,13 @@ +use std::sync::Arc; + +use parking_lot::Mutex; + use { - super::{font::Galley, fonts::TextStyle, Fonts, Srgba, Triangles}, + super::{fonts::TextStyle, Fonts, Srgba, Triangles}, crate::{ align::{anchor_rect, Align}, math::{Pos2, Rect}, + paint::fonts::GlyphLayout, }, }; @@ -38,9 +43,8 @@ pub enum PaintCmd { Text { /// Top left corner of the first character. pos: Pos2, - /// The layed out text - galley: Galley, - text_style: TextStyle, // TODO: Font? + layout: GlyphLayout, + text_style: TextStyle, color: Srgba, }, Triangles(Triangles), @@ -91,19 +95,18 @@ impl PaintCmd { } pub fn text( - fonts: &Fonts, + fonts: Arc<Mutex<Fonts>>, pos: Pos2, anchor: (Align, Align), - text: impl Into<String>, + text: &str, text_style: TextStyle, color: Srgba, ) -> Self { - let font = &fonts[text_style]; - let galley = font.layout_multiline(text.into(), f32::INFINITY); - let rect = anchor_rect(Rect::from_min_size(pos, galley.size), anchor); + let layout = fonts.lock().layout_multiline(text_style, text, None); + let rect = anchor_rect(Rect::from_min_size(pos, layout.size), anchor); Self::Text { pos: rect.min, - galley, + layout, text_style, color, } diff --git a/egui/src/paint/font.rs b/egui/src/paint/font.rs deleted file mode 100644 index 8e0f854767da..000000000000 --- a/egui/src/paint/font.rs +++ /dev/null @@ -1,531 +0,0 @@ -use std::sync::Arc; - -use { - ahash::AHashMap, - parking_lot::{Mutex, RwLock}, - rusttype::{point, Scale}, -}; - -use crate::math::{vec2, Vec2}; - -use super::texture_atlas::TextureAtlas; - -#[derive(Clone, Copy, Debug, Default)] -pub struct GalleyCursor { - /// character count in whole galley - pub char_idx: usize, - /// line number - pub line: usize, - /// character count on this line - pub column: usize, -} - -/// A collection of text locked into place. -#[derive(Clone, Debug, Default)] -pub struct Galley { - /// The full text - pub text: String, - - /// Lines of text, from top to bottom. - /// The number of chars in all lines sum up to text.chars().count() - pub lines: Vec<Line>, - - // Optimization: calculate once and reuse. - pub size: Vec2, -} - -/// A typeset piece of text on a single line. -#[derive(Clone, Debug)] -pub struct Line { - /// The start of each character, probably starting at zero. - /// The last element is the end of the last character. - /// x_offsets.len() == text.chars().count() + 1 - /// This is never empty. - /// Unit: points. - pub x_offsets: Vec<f32>, - - /// Top of the line, offset within the Galley. - /// Unit: points. - pub y_min: f32, - - /// Bottom of the line, offset within the Galley. - /// Unit: points. - pub y_max: f32, - - /// If true, the last char on this line is '\n' - pub ends_with_newline: bool, -} - -impl Galley { - pub fn sanity_check(&self) { - let mut char_count = 0; - for line in &self.lines { - line.sanity_check(); - char_count += line.char_count(); - } - assert_eq!(char_count, self.text.chars().count()); - } - - /// If given a char index after the first line, the end of the last character is returned instead. - /// Returns a Vec2 rather than a Pos2 as this is an offset into the galley. *shrug* - pub fn char_start_pos(&self, char_idx: usize) -> Vec2 { - let mut char_count = 0; - for line in &self.lines { - let line_char_count = line.char_count(); - if char_count <= char_idx && char_idx < char_count + line_char_count { - let line_char_offset = char_idx - char_count; - return vec2(line.x_offsets[line_char_offset], line.y_min); - } - char_count += line_char_count; - } - - if let Some(last) = self.lines.last() { - vec2(last.max_x(), last.y_min) - } else { - // Empty galley - vec2(0.0, 0.0) - } - } - - /// Character offset at the given position within the galley - pub fn char_at(&self, pos: Vec2) -> GalleyCursor { - let mut best_y_dist = f32::INFINITY; - let mut cursor = GalleyCursor::default(); - - let mut char_count = 0; - for (line_nr, line) in self.lines.iter().enumerate() { - let y_dist = (line.y_min - pos.y).abs().min((line.y_max - pos.y).abs()); - if y_dist < best_y_dist { - best_y_dist = y_dist; - let mut column = line.char_at(pos.x); - if column == line.char_count() && line.ends_with_newline { - // handle the case where line ends with a \n and we click after it. - // We should return the position BEFORE the \n! - column -= 1; - } - cursor = GalleyCursor { - char_idx: char_count + column, - line: line_nr, - column, - } - } - char_count += line.char_count(); - } - cursor - } -} - -impl Line { - pub fn sanity_check(&self) { - assert!(!self.x_offsets.is_empty()); - } - - pub fn char_count(&self) -> usize { - assert!(!self.x_offsets.is_empty()); - self.x_offsets.len() - 1 - } - - pub fn min_x(&self) -> f32 { - *self.x_offsets.first().unwrap() - } - - pub fn max_x(&self) -> f32 { - *self.x_offsets.last().unwrap() - } - - /// Closest char at the desired x coordinate. returns something in the range `[0, char_count()]` - pub fn char_at(&self, desired_x: f32) -> usize { - for (i, char_x_bounds) in self.x_offsets.windows(2).enumerate() { - let char_center_x = 0.5 * (char_x_bounds[0] + char_x_bounds[1]); - if desired_x < char_center_x { - return i; - } - } - self.char_count() - } -} - -// ---------------------------------------------------------------------------- - -// const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character. -// const REPLACEMENT_CHAR: char = '\u{FFFD}'; // � REPLACEMENT CHARACTER -const REPLACEMENT_CHAR: char = '?'; - -#[derive(Clone, Copy, Debug)] -pub struct UvRect { - /// X/Y offset for nice rendering (unit: points). - pub offset: Vec2, - pub size: Vec2, - - /// Top left corner UV in texture. - pub min: (u16, u16), - - /// Bottom right corner (exclusive). - pub max: (u16, u16), -} - -#[derive(Clone, Copy, Debug)] -pub struct GlyphInfo { - id: rusttype::GlyphId, - - /// Unit: points. - pub advance_width: f32, - - /// Texture coordinates. None for space. - pub uv_rect: Option<UvRect>, -} - -/// The interface uses points as the unit for everything. -pub struct Font { - font: rusttype::Font<'static>, - /// Maximum character height - scale_in_pixels: f32, - pixels_per_point: f32, - replacement_glyph_info: GlyphInfo, - glyph_infos: RwLock<AHashMap<char, GlyphInfo>>, - atlas: Arc<Mutex<TextureAtlas>>, -} - -impl Font { - pub fn new( - atlas: Arc<Mutex<TextureAtlas>>, - font_data: &'static [u8], - scale_in_points: f32, - pixels_per_point: f32, - ) -> Font { - assert!(scale_in_points > 0.0); - assert!(pixels_per_point > 0.0); - - let font = rusttype::Font::try_from_bytes(font_data).expect("Error constructing Font"); - let scale_in_pixels = pixels_per_point * scale_in_points; - - let replacement_glyph_info = allocate_glyph( - &mut atlas.lock(), - REPLACEMENT_CHAR, - &font, - scale_in_pixels, - pixels_per_point, - ) - .unwrap_or_else(|| { - panic!( - "Failed to find replacement character {:?}", - REPLACEMENT_CHAR - ) - }); - - let font = Font { - font, - scale_in_pixels, - pixels_per_point, - replacement_glyph_info, - glyph_infos: Default::default(), - atlas, - }; - - font.glyph_infos - .write() - .insert(REPLACEMENT_CHAR, font.replacement_glyph_info); - - // Preload the printable ASCII characters [32, 126] (which excludes control codes): - const FIRST_ASCII: usize = 32; // 32 == space - const LAST_ASCII: usize = 126; - for c in (FIRST_ASCII..=LAST_ASCII).map(|c| c as u8 as char) { - font.glyph_info(c); - } - font.glyph_info('°'); - - font - } - - pub fn round_to_pixel(&self, point: f32) -> f32 { - (point * self.pixels_per_point).round() / self.pixels_per_point - } - - /// Height of one line of text. In points - /// TODO: rename height ? - pub fn line_spacing(&self) -> f32 { - self.scale_in_pixels / self.pixels_per_point - } - pub fn height(&self) -> f32 { - self.scale_in_pixels / self.pixels_per_point - } - - pub fn uv_rect(&self, c: char) -> Option<UvRect> { - self.glyph_infos.read().get(&c).and_then(|gi| gi.uv_rect) - } - - fn glyph_info(&self, c: char) -> GlyphInfo { - if c == '\n' { - // Hack: else we show '\n' as '?' (REPLACEMENT_CHAR) - return self.glyph_info(' '); - } - - { - if let Some(glyph_info) = self.glyph_infos.read().get(&c) { - return *glyph_info; - } - } - - // Add new character: - let glyph_info = allocate_glyph( - &mut self.atlas.lock(), - c, - &self.font, - self.scale_in_pixels, - self.pixels_per_point, - ); - // debug_assert!(glyph_info.is_some(), "Failed to find {:?}", c); - let glyph_info = glyph_info.unwrap_or(self.replacement_glyph_info); - self.glyph_infos.write().insert(c, glyph_info); - glyph_info - } - - /// Typeset the given text onto one line. - /// Assumes there are no \n in the text. - /// Always returns exactly one fragment. - pub fn layout_single_line(&self, text: String) -> Galley { - let x_offsets = self.layout_single_line_fragment(&text); - let line = Line { - x_offsets, - y_min: 0.0, - y_max: self.height(), - ends_with_newline: false, - }; - let width = line.max_x(); - let size = vec2(width, self.height()); - let galley = Galley { - text, - lines: vec![line], - size, - }; - galley.sanity_check(); - galley - } - - pub fn layout_multiline(&self, text: String, max_width_in_points: f32) -> Galley { - let line_spacing = self.line_spacing(); - let mut cursor_y = 0.0; - let mut lines = Vec::new(); - - let mut paragraph_start = 0; - - while paragraph_start < text.len() { - let next_newline = text[paragraph_start..].find('\n'); - let paragraph_end = next_newline - .map(|newline| paragraph_start + newline + 1) - .unwrap_or_else(|| text.len()); - - assert!(paragraph_start < paragraph_end); - let paragraph_text = &text[paragraph_start..paragraph_end]; - let mut paragraph_lines = - self.layout_paragraph_max_width(paragraph_text, max_width_in_points); - assert!(!paragraph_lines.is_empty()); - - for line in &mut paragraph_lines { - line.y_min += cursor_y; - line.y_max += cursor_y; - } - cursor_y = paragraph_lines.last().unwrap().y_max; - cursor_y += line_spacing * 0.4; // extra spacing between paragraphs. less hacky - - lines.append(&mut paragraph_lines); - - paragraph_start = paragraph_end; - } - - if text.is_empty() || text.ends_with('\n') { - // Add an empty last line for correct visuals etc: - lines.push(Line { - x_offsets: vec![0.0], - y_min: cursor_y, - y_max: cursor_y + line_spacing, - ends_with_newline: text.ends_with('\n'), - }); - } - - let mut widest_line = 0.0; - for line in &lines { - widest_line = line.max_x().max(widest_line); - } - let size = vec2(widest_line, lines.last().unwrap().y_max); - - let galley = Galley { text, lines, size }; - galley.sanity_check(); - galley - } - - /// Typeset the given text onto one line. - /// Assumes there are no \n in the text. - /// Return `x_offsets`, one longer than the number of characters in the text. - fn layout_single_line_fragment(&self, text: &str) -> Vec<f32> { - let scale_in_pixels = Scale::uniform(self.scale_in_pixels); - - let mut x_offsets = Vec::with_capacity(text.chars().count() + 1); - x_offsets.push(0.0); - - let mut cursor_x_in_points = 0.0f32; - let mut last_glyph_id = None; - - for c in text.chars() { - let glyph = self.glyph_info(c); - - if let Some(last_glyph_id) = last_glyph_id { - cursor_x_in_points += - self.font - .pair_kerning(scale_in_pixels, last_glyph_id, glyph.id) - / self.pixels_per_point - } - cursor_x_in_points += glyph.advance_width; - cursor_x_in_points = self.round_to_pixel(cursor_x_in_points); - last_glyph_id = Some(glyph.id); - - x_offsets.push(cursor_x_in_points); - } - - x_offsets - } - - /// A paragraph is text with no line break character in it. - /// The text will be linebreaked by the given `max_width_in_points`. - pub fn layout_paragraph_max_width(&self, text: &str, max_width_in_points: f32) -> Vec<Line> { - let full_x_offsets = self.layout_single_line_fragment(text); - - let mut line_start_x = full_x_offsets[0]; - - { - #![allow(clippy::float_cmp)] - assert_eq!(line_start_x, 0.0); - } - - let mut cursor_y = 0.0; - let mut line_start_idx = 0; - - // start index of the last space. A candidate for a new line. - let mut last_space = None; - - let mut out_lines = vec![]; - - for (i, (x, chr)) in full_x_offsets.iter().skip(1).zip(text.chars()).enumerate() { - let line_width = x - line_start_x; - - if line_width > max_width_in_points { - if let Some(last_space_idx) = last_space { - let include_trailing_space = true; - let line = if include_trailing_space { - Line { - x_offsets: full_x_offsets[line_start_idx..=last_space_idx + 1] - .iter() - .map(|x| x - line_start_x) - .collect(), - y_min: cursor_y, - y_max: cursor_y + self.height(), - ends_with_newline: false, // we'll fix this later - } - } else { - Line { - x_offsets: full_x_offsets[line_start_idx..=last_space_idx] - .iter() - .map(|x| x - line_start_x) - .collect(), - y_min: cursor_y, - y_max: cursor_y + self.height(), - ends_with_newline: false, // we'll fix this later - } - }; - line.sanity_check(); - out_lines.push(line); - - line_start_idx = last_space_idx + 1; - line_start_x = full_x_offsets[line_start_idx]; - last_space = None; - cursor_y += self.line_spacing(); - cursor_y = self.round_to_pixel(cursor_y); - } - } - - const NON_BREAKING_SPACE: char = '\u{A0}'; - if chr.is_whitespace() && chr != NON_BREAKING_SPACE { - last_space = Some(i); - } - } - - if line_start_idx + 1 < full_x_offsets.len() { - let line = Line { - x_offsets: full_x_offsets[line_start_idx..] - .iter() - .map(|x| x - line_start_x) - .collect(), - y_min: cursor_y, - y_max: cursor_y + self.height(), - ends_with_newline: false, // we'll fix this later - }; - line.sanity_check(); - out_lines.push(line); - } - - if text.ends_with('\n') { - out_lines.last_mut().unwrap().ends_with_newline = true; - } - - out_lines - } -} - -fn allocate_glyph( - atlas: &mut TextureAtlas, - c: char, - font: &rusttype::Font<'static>, - scale_in_pixels: f32, - pixels_per_point: f32, -) -> Option<GlyphInfo> { - let glyph = font.glyph(c); - if glyph.id().0 == 0 { - return None; // Failed to find a glyph for the character - } - - let glyph = glyph.scaled(Scale::uniform(scale_in_pixels)); - let glyph = glyph.positioned(point(0.0, 0.0)); - - let uv_rect = if let Some(bb) = glyph.pixel_bounding_box() { - let glyph_width = bb.width() as usize; - let glyph_height = bb.height() as usize; - assert!(glyph_width >= 1); - assert!(glyph_height >= 1); - - let glyph_pos = atlas.allocate((glyph_width, glyph_height)); - - let texture = atlas.texture_mut(); - glyph.draw(|x, y, v| { - if v > 0.0 { - let px = glyph_pos.0 + x as usize; - let py = glyph_pos.1 + y as usize; - texture[(px, py)] = (v * 255.0).round() as u8; - } - }); - - let offset_y_in_pixels = scale_in_pixels as f32 + bb.min.y as f32 - 4.0 * pixels_per_point; // TODO: use font.v_metrics - Some(UvRect { - offset: vec2( - bb.min.x as f32 / pixels_per_point, - offset_y_in_pixels / pixels_per_point, - ), - size: vec2(glyph_width as f32, glyph_height as f32) / pixels_per_point, - min: (glyph_pos.0 as u16, glyph_pos.1 as u16), - max: ( - (glyph_pos.0 + glyph_width) as u16, - (glyph_pos.1 + glyph_height) as u16, - ), - }) - } else { - // No bounding box. Maybe a space? - None - }; - - let advance_width_in_points = glyph.unpositioned().h_metrics().advance_width / pixels_per_point; - - Some(GlyphInfo { - id: glyph.id(), - advance_width: advance_width_in_points, - uv_rect, - }) -} diff --git a/egui/src/paint/fonts.rs b/egui/src/paint/fonts.rs index e03109e9f62e..58c74f4375ec 100644 --- a/egui/src/paint/fonts.rs +++ b/egui/src/paint/fonts.rs @@ -4,14 +4,17 @@ use std::{ sync::Arc, }; +use ahash::AHashMap; +use fontdue::{ + layout::{CoordinateSystem, GlyphPosition, GlyphRasterConfig, LayoutSettings}, + Font, FontSettings, Metrics, +}; use parking_lot::Mutex; -use super::{ - font::Font, - texture_atlas::{Texture, TextureAtlas}, -}; +use crate::math::{vec2, Vec2}; + +use super::texture_atlas::{Texture, TextureAtlas}; -// TODO: rename #[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"))] @@ -29,113 +32,240 @@ pub enum FontFamily { VariableWidth, } +impl FontFamily { + /// Used as index for the font vector. The fonts need to be inserted in this order! + pub fn font_index(&self) -> usize { + match self { + FontFamily::Monospace => 0, + FontFamily::VariableWidth => 1, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct FontDefinition { + pub family: FontFamily, + pub scale_in_points: f32, +} + +/// Configured the typefaces that are used. This is a configuration and is not supposed to be changed while rendering. #[derive(Clone, Debug, PartialEq)] -pub struct FontDefinitions { +pub struct FontConfiguration { /// The dpi scale factor. Needed to get pixel perfect fonts. pub pixels_per_point: f32, - - pub fonts: BTreeMap<TextStyle, (FontFamily, f32)>, + pub definitions: BTreeMap<TextStyle, FontDefinition>, } -impl Default for FontDefinitions { +impl Default for FontConfiguration { fn default() -> Self { Self::with_pixels_per_point(f32::NAN) // must be set later } } -impl FontDefinitions { +impl FontConfiguration { pub fn with_pixels_per_point(pixels_per_point: f32) -> Self { - let mut fonts = BTreeMap::new(); - fonts.insert(TextStyle::Body, (FontFamily::VariableWidth, 14.0)); - fonts.insert(TextStyle::Button, (FontFamily::VariableWidth, 16.0)); - fonts.insert(TextStyle::Heading, (FontFamily::VariableWidth, 24.0)); - fonts.insert(TextStyle::Monospace, (FontFamily::Monospace, 13.0)); - + let mut definitions = BTreeMap::new(); + definitions.insert( + TextStyle::Body, + FontDefinition { + family: FontFamily::VariableWidth, + scale_in_points: 12.0, + }, + ); + definitions.insert( + TextStyle::Button, + FontDefinition { + family: FontFamily::VariableWidth, + scale_in_points: 13.0, + }, + ); + definitions.insert( + TextStyle::Heading, + FontDefinition { + family: FontFamily::VariableWidth, + scale_in_points: 20.0, + }, + ); + definitions.insert( + TextStyle::Monospace, + FontDefinition { + family: FontFamily::Monospace, + scale_in_points: 11.0, + }, + ); Self { pixels_per_point, - fonts, + definitions, } } } -/// Note: the `default()` fonts are invalid (missing `pixels_per_point`). -#[derive(Default)] +#[derive(Clone, Copy, Debug, Default)] +pub struct UvRect { + /// The size of the element in points. + pub size: Vec2, + + /// Top left corner UV in texture. + pub min: (u16, u16), + + /// Bottom right corner (exclusive). + pub max: (u16, u16), +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct GlyphInfo { + /// Glyph metrics. + pub metrics: Metrics, + + /// Texture coordinates. + pub uv_rect: UvRect, +} + +/// Glyph layout information. Units are in physical pixel and need to be scaled back to points. +#[derive(Clone, Debug, Default)] +pub struct GlyphLayout { + pub size: Vec2, + pub glyph_positions: Vec<GlyphPosition>, +} + +impl GlyphLayout { + pub fn with_capacity(capacity: usize) -> Self { + Self { + size: vec2(0.0, 0.0), + glyph_positions: Vec::with_capacity(capacity), + } + } +} + +/// Font renderer. pub struct Fonts { - definitions: FontDefinitions, - fonts: BTreeMap<TextStyle, Font>, + configuration: FontConfiguration, + fonts: Vec<Font>, + layout_engine: fontdue::layout::Layout, + glyph_infos: AHashMap<GlyphRasterConfig, GlyphInfo>, atlas: Arc<Mutex<TextureAtlas>>, /// Copy of the texture in the texture atlas. /// This is so we can return a reference to it (the texture atlas is behind a lock). buffered_texture: Mutex<Arc<Texture>>, + /// Precalculated heights for the TextStyles. + heights: BTreeMap<TextStyle, f32>, + /// Precalculated heights for the TextStyles. + line_spacings: BTreeMap<TextStyle, f32>, +} + +impl Default for Fonts { + fn default() -> Self { + Self { + configuration: Default::default(), + fonts: Default::default(), + layout_engine: fontdue::layout::Layout::new(CoordinateSystem::PositiveYDown), + glyph_infos: Default::default(), + atlas: Default::default(), + buffered_texture: Default::default(), + heights: Default::default(), + line_spacings: Default::default(), + } + } } impl Fonts { - pub fn from_definitions(definitions: FontDefinitions) -> Fonts { - let mut fonts = Self::default(); - fonts.set_definitions(definitions); + pub fn from_definitions(configuration: FontConfiguration) -> Fonts { + let mut fonts = Fonts::default(); + fonts.set_configuration(configuration); fonts } - pub fn definitions(&self) -> &FontDefinitions { - &self.definitions + pub fn configuration(&self) -> &FontConfiguration { + &self.configuration } - pub fn set_definitions(&mut self, definitions: FontDefinitions) { - if self.definitions == definitions { + pub fn set_configuration(&mut self, configuration: FontConfiguration) { + if self.configuration == configuration { return; } - let mut atlas = TextureAtlas::new(512, 16); // TODO: better default? + self.atlas = Arc::new(Mutex::new(TextureAtlas::new(512, 512))); - { - // Make the top left pixel fully white: - let pos = atlas.allocate((1, 1)); - assert_eq!(pos, (0, 0)); - atlas.texture_mut()[pos] = 255; - } + // Make the top left pixel fully white, since it's used for the UI rendering. + let pos = self.atlas.lock().allocate((1, 1)); + self.atlas.lock().texture_mut()[pos] = 255; + debug_assert_eq!(pos, (0, 0)); - let atlas = Arc::new(Mutex::new(atlas)); + // FontFamily::Monospace (Use 13 for this. NOTHING ELSE). + let monospace_typeface_data: &[u8] = include_bytes!("../../fonts/ProggyClean.ttf"); + // FontFamily::VariableWidth. + // FIXME: https://github.com/mooman219/fontdue/issues/38 + // FIXME: https://github.com/RazrFalcon/ttf-parser/issues/43 + let variable_typeface_data: &[u8] = include_bytes!("../../fonts/Comfortaa-Regular.ttf"); + //let variable_typeface_data: &[u8] = include_bytes!("../../fonts/Roboto-Regular.ttf"); - // TODO: figure out a way to make the WASM smaller despite including a font. Zip it? - let monospace_typeface_data = include_bytes!("../../fonts/ProggyClean.ttf"); // Use 13 for this. NOTHING ELSE. + // Fonts need to be added in the same order as defined in the `FontFamily.font_index()` method. + self.fonts.push( + Font::from_bytes(monospace_typeface_data, FontSettings::default()) + .expect("error constructing Font"), + ); + self.fonts.push( + Font::from_bytes(variable_typeface_data, FontSettings::default()) + .expect("error constructing Font"), + ); - // let monospace_typeface_data = include_bytes!("../../fonts/Roboto-Regular.ttf"); + self.configuration = configuration; - let variable_typeface_data = include_bytes!("../../fonts/Comfortaa-Regular.ttf"); // Funny, hard to read + let pixels_per_points = self.configuration.pixels_per_point; + for (text_style, definition) in self.configuration.definitions.clone().iter() { + let font_index = definition.family.font_index(); + let scale_in_pixels = definition.scale_in_points * pixels_per_points; - // let variable_typeface_data = include_bytes!("../../fonts/DejaVuSans.ttf"); // Basic, boring, takes up more space + // Preload the printable ASCII characters [33, 126] (which excludes control codes): + const FIRST_ASCII: usize = 33; // ! + const LAST_ASCII: usize = 126; // ~ - self.definitions = definitions.clone(); - let FontDefinitions { - pixels_per_point, - fonts, - } = definitions; - self.fonts = fonts - .into_iter() - .map(|(text_style, (family, size))| { - let typeface_data: &[u8] = match family { - FontFamily::Monospace => monospace_typeface_data, - FontFamily::VariableWidth => variable_typeface_data, + for u in FIRST_ASCII..=LAST_ASCII { + let c = std::char::from_u32(u as u32) + .unwrap_or_else(|| panic!("can't create char from u32: {}", u)); + let key = GlyphRasterConfig { + c, + px: scale_in_pixels, + font_index, }; + self.glyph_info(&key); + } - ( - text_style, - Font::new(atlas.clone(), typeface_data, size, pixels_per_point), - ) - }) - .collect(); - - { - let mut atlas = atlas.lock(); - let texture = atlas.texture_mut(); - // Make sure we seed the texture version with something unique based on the default characters: - let mut hasher = ahash::AHasher::default(); - texture.pixels.hash(&mut hasher); - texture.version = hasher.finish(); + // Precalculate the line spacings and heights (in points) + let px = scale_in_pixels; + let font = &self.fonts[font_index]; + let line_spacing = font + .horizontal_line_metrics(px) + .unwrap_or_else(|| panic!("font doesn't seem to support horizontal text layout")) + .new_line_size; + self.line_spacings + .insert(*text_style, line_spacing / pixels_per_points); + self.heights + .insert(*text_style, scale_in_pixels / pixels_per_points); } - self.buffered_texture = Default::default(); //atlas.lock().texture().clone(); - self.atlas = atlas; + // Make sure we seed the texture version with something unique based on the default characters: + let mut atlas = self.atlas.lock(); + let texture = atlas.texture_mut(); + let mut hasher = ahash::AHasher::default(); + texture.pixels.hash(&mut hasher); + texture.version = hasher.finish(); + + self.buffered_texture = Default::default(); + } + + /// Returns the `GlyphInfo` for the given `GlyphRasterConfig` key. Allocates a new Glyph if necessary. + pub fn glyph_info(&mut self, grc: &GlyphRasterConfig) -> GlyphInfo { + if let Some(glyph_info) = self.glyph_infos.get(grc) { + return *glyph_info; + } + + let glyph_info = self.allocate_glyph(grc); + + let glyph_info = + glyph_info.unwrap_or_else(|| panic!("couldn't render glyph: {:#?}", grc.c)); + self.glyph_infos.insert(*grc, glyph_info); + glyph_info } pub fn texture(&self) -> Arc<Texture> { @@ -147,12 +277,144 @@ impl Fonts { buffered_texture.clone() } -} -impl std::ops::Index<TextStyle> for Fonts { - type Output = Font; + /// Typeset the given text onto one line. Ignores hard wraps. Returns the dimension of the line. + pub fn layout_single_line(&mut self, style: TextStyle, text: &str) -> GlyphLayout { + let settings = LayoutSettings { + wrap_hard_breaks: false, + ..Default::default() + }; + + self.layout(style, text, settings) + } + + // FIXME: https://github.com/mooman219/fontdue/issues/39 + /// Typeset the given text onto multiple lines. + pub fn layout_multiline( + &mut self, + style: TextStyle, + text: &str, + max_width_in_points: Option<f32>, + ) -> GlyphLayout { + let settings = LayoutSettings { + max_width: max_width_in_points, + wrap_hard_breaks: true, + ..Default::default() + }; + + self.layout(style, text, settings) + } + + fn layout( + &mut self, + style: TextStyle, + text: &str, + mut settings: LayoutSettings, + ) -> GlyphLayout { + // We calculate the layout in physical pixel and later scale back the metrics back to points. + // This is done to get a better looking layout. + let mut layout = GlyphLayout::with_capacity(text.len()); + + if text.is_empty() { + return layout; + } + + // Convert points to pixel. + settings.max_width = if let Some(width) = settings.max_width { + Some(width * self.configuration.pixels_per_point) + } else { + None + }; + + let font_index = self.configuration.definitions[&style].family.font_index(); + let pixels_per_point = self.configuration.pixels_per_point; + let height = self.heights[&style]; + let px_height = self.heights[&style] * pixels_per_point; + + let text_style = fontdue::layout::TextStyle { + text, + px: px_height, + font_index, + }; + + self.layout_engine.layout_horizontal( + &self.fonts, + &[&text_style], + &settings, + &mut layout.glyph_positions, + ); + + let mut min_y = f32::MAX; + let mut max_y = f32::MIN; + let mut min_x = f32::MAX; + let mut max_x = f32::MIN; + + // Calculate logical points and max dimensions. + for pos in layout.glyph_positions.iter_mut() { + pos.width = (pos.width as f32 / pixels_per_point) as usize; + pos.height = (pos.height as f32 / pixels_per_point) as usize; + pos.x /= pixels_per_point; + pos.y /= pixels_per_point; + + min_x = min_x.min(pos.x); + max_x = max_x.max(pos.x + pos.width as f32); + min_y = min_y.min(pos.y); + max_y = max_y.max(pos.y + pos.height as f32); + } + + // Wait for fontdue do provide better line metrics to align the glyphs + // to the line with the current DPI setting. + + // Add 20% height as margin to each side + let width = (max_x - min_x) + 0.4 * height; + + // Add 20% height as margin to each side + let height = (max_y - min_y) + 0.4 * height; + + layout.size = vec2(width, height); + layout + } + + fn allocate_glyph(&mut self, grc: &GlyphRasterConfig) -> Option<GlyphInfo> { + let font = &self.fonts[grc.font_index]; + let (metrics, glyph_data) = font.rasterize(grc.c, grc.px); + + if glyph_data.is_empty() { + return None; + } + + let mut atlas = self.atlas.lock(); + let glyph_pos = atlas.allocate((metrics.width, metrics.height)); + let texture = atlas.texture_mut(); + + for (i, v) in glyph_data.iter().enumerate() { + if *v > 0 { + let px = glyph_pos.0 + (i % metrics.width); + let py = glyph_pos.1 + (i / metrics.width); + texture[(px, py)] = *v; + } + } + + let uv_rect = UvRect { + size: vec2(metrics.width as f32, metrics.height as f32) + / self.configuration.pixels_per_point, + min: (glyph_pos.0 as u16, glyph_pos.1 as u16), + max: ( + (glyph_pos.0 + metrics.width) as u16, + (glyph_pos.1 + metrics.height) as u16, + ), + }; + + Some(GlyphInfo { metrics, uv_rect }) + } + + /// Returns the font height in points of the given `TextStyle`. + pub fn text_style_height(&self, text_style: TextStyle) -> f32 { + self.heights[&text_style] + } - fn index(&self, text_style: TextStyle) -> &Font { - &self.fonts[&text_style] + /// Returns the line spacing in points of the given `TextStyle`. + pub fn text_style_line_spacing(&self, text_style: TextStyle) -> f32 { + self.line_spacings[&text_style] } } diff --git a/egui/src/paint/mod.rs b/egui/src/paint/mod.rs index 383435312233..de783db3e477 100644 --- a/egui/src/paint/mod.rs +++ b/egui/src/paint/mod.rs @@ -4,7 +4,6 @@ pub mod color; pub mod command; -pub mod font; pub mod fonts; pub mod tessellator; mod texture_atlas; @@ -12,7 +11,7 @@ mod texture_atlas; pub use { color::{Rgba, Srgba}, command::{PaintCmd, Stroke}, - fonts::{FontDefinitions, Fonts, TextStyle}, + fonts::{FontConfiguration, Fonts, TextStyle}, tessellator::{PaintJobs, PaintOptions, TextureId, Triangles, Vertex, WHITE_UV}, texture_atlas::Texture, }; diff --git a/egui/src/paint/tessellator.rs b/egui/src/paint/tessellator.rs index c940d827efa0..2a3b6650eaf4 100644 --- a/egui/src/paint/tessellator.rs +++ b/egui/src/paint/tessellator.rs @@ -5,6 +5,9 @@ #![allow(clippy::identity_op)] +use parking_lot::Mutex; +use std::sync::Arc; + use { super::{ color::{self, srgba, Rgba, Srgba, TRANSPARENT}, @@ -655,7 +658,7 @@ fn tessellate_paint_command( clip_rect: Rect, command: PaintCmd, options: PaintOptions, - fonts: &Fonts, + fonts: Arc<Mutex<Fonts>>, out: &mut Triangles, scratchpad_points: &mut Vec<Pos2>, scratchpad_path: &mut Path, @@ -744,59 +747,55 @@ fn tessellate_paint_command( } PaintCmd::Text { pos, - galley, + layout, text_style, color, } => { if color == TRANSPARENT { return; } - galley.sanity_check(); - - let num_chars = galley.text.chars().count(); - out.reserve_triangles(num_chars * 2); - out.reserve_vertices(num_chars * 4); - - let tex_w = fonts.texture().width as f32; - let tex_h = fonts.texture().height as f32; - - let text_offset = vec2(0.0, 1.0); // Eye-balled for buttons. TODO: why is this needed? - - let clip_rect = clip_rect.expand(2.0); // Some fudge to handle letter slightly larger than expected. - - let font = &fonts[text_style]; - let mut chars = galley.text.chars(); - for line in &galley.lines { - let line_min_y = pos.y + line.y_min + text_offset.x; - let line_max_y = line_min_y + font.height(); - let is_line_visible = - line_max_y >= clip_rect.min.y && line_min_y <= clip_rect.max.y; - - for x_offset in line.x_offsets.iter().take(line.x_offsets.len() - 1) { - let c = chars.next().unwrap(); - - if options.coarse_tessellation_culling && !is_line_visible { - // culling individual lines of text is important, since a single `PaintCmd::Text` - // can span hundreds of lines. - continue; + let num_glyphs = layout.glyph_positions.len(); + out.reserve_triangles(num_glyphs * 2); + out.reserve_vertices(num_glyphs * 4); + + let tex_w = fonts.lock().texture().width as f32; + let tex_h = fonts.lock().texture().height as f32; + + let line_height = fonts.lock().text_style_line_spacing(text_style); + + let mut was_visible = false; + for glyph in &layout.glyph_positions { + // Coarse culling the glyphs on the Y-axis. + // Could be optimized by only checking every n-th glyph. + if options.coarse_tessellation_culling { + let glyph_pos_y = pos.y + glyph.y as f32; + let is_glyph_visible = glyph_pos_y >= clip_rect.min.y - line_height + && glyph_pos_y <= clip_rect.max.y; + + if !was_visible && is_glyph_visible { + was_visible = true; } - if let Some(glyph) = font.uv_rect(c) { - let mut left_top = - pos + glyph.offset + vec2(*x_offset, line.y_min) + text_offset; - left_top.x = font.round_to_pixel(left_top.x); // Pixel-perfection. - left_top.y = font.round_to_pixel(left_top.y); // Pixel-perfection. - - let pos = Rect::from_min_max(left_top, left_top + glyph.size); - let uv = Rect::from_min_max( - pos2(glyph.min.0 as f32 / tex_w, glyph.min.1 as f32 / tex_h), - pos2(glyph.max.0 as f32 / tex_w, glyph.max.1 as f32 / tex_h), - ); - out.add_rect_with_uv(pos, uv, color); + if !is_glyph_visible { + if was_visible { + break; + } + continue; } } + + let glyph_info = fonts.lock().glyph_info(&glyph.key); + let uv_rect = glyph_info.uv_rect; + let glyph_pos = vec2(glyph.x, glyph.y); + let left_top = pos + glyph_pos; + let pos = Rect::from_min_max(left_top, left_top + uv_rect.size); + let uv = Rect::from_min_max( + pos2(uv_rect.min.0 as f32 / tex_w, uv_rect.min.1 as f32 / tex_h), + pos2(uv_rect.max.0 as f32 / tex_w, uv_rect.max.1 as f32 / tex_h), + ); + + out.add_rect_with_uv(pos, uv, color); } - assert_eq!(chars.next(), None); } } } @@ -815,7 +814,7 @@ fn tessellate_paint_command( pub fn tessellate_paint_commands( commands: Vec<(Rect, PaintCmd)>, options: PaintOptions, - fonts: &Fonts, + fonts: Arc<Mutex<Fonts>>, ) -> Vec<(Rect, Triangles)> { let mut scratchpad_points = Vec::new(); let mut scratchpad_path = Path::default(); @@ -842,7 +841,7 @@ pub fn tessellate_paint_commands( clip_rect, cmd, options, - fonts, + fonts.clone(), out, &mut scratchpad_points, &mut scratchpad_path, @@ -860,7 +859,7 @@ pub fn tessellate_paint_commands( stroke: Stroke::new(2.0, srgba(150, 255, 150, 255)), }, options, - fonts, + fonts.clone(), triangles, &mut scratchpad_points, &mut scratchpad_path, diff --git a/egui/src/painter.rs b/egui/src/painter.rs index 7154334f1d57..f26d27e5d4f5 100644 --- a/egui/src/painter.rs +++ b/egui/src/painter.rs @@ -1,11 +1,14 @@ use std::sync::Arc; +use parking_lot::Mutex; + use crate::{ align::{anchor_rect, Align, LEFT_TOP}, color, layers::PaintCmdIdx, math::{Pos2, Rect, Vec2}, - paint::{font, Fonts, PaintCmd, Stroke, TextStyle}, + paint::fonts::GlyphLayout, + paint::{Fonts, PaintCmd, Stroke, TextStyle}, Context, Layer, Srgba, }; @@ -48,7 +51,7 @@ impl Painter { } /// Available fonts - pub(crate) fn fonts(&self) -> &Fonts { + pub(crate) fn fonts(&self) -> Arc<Mutex<Fonts>> { self.ctx.fonts() } @@ -115,25 +118,23 @@ impl Painter { /// ## Debug painting impl Painter { - pub fn debug_rect(&mut self, rect: Rect, color: Srgba, text: impl Into<String>) { + pub fn debug_rect(&mut self, rect: Rect, color: Srgba, text: &str) { self.rect_stroke(rect, 0.0, (1.0, color)); let text_style = TextStyle::Monospace; - self.text(rect.min, LEFT_TOP, text.into(), text_style, color); + self.text(rect.min, LEFT_TOP, text, text_style, color); } - pub fn error(&self, pos: Pos2, text: impl Into<String>) { - let text = text.into(); + pub fn error(&self, pos: Pos2, text: &str) { let text_style = TextStyle::Monospace; - let font = &self.fonts()[text_style]; - let galley = font.layout_multiline(text, f32::INFINITY); - let rect = anchor_rect(Rect::from_min_size(pos, galley.size), LEFT_TOP); + let layout = self.fonts().lock().layout_multiline(text_style, text, None); + let rect = anchor_rect(Rect::from_min_size(pos, layout.size), LEFT_TOP); self.add(PaintCmd::Rect { rect: rect.expand(2.0), corner_radius: 0.0, fill: Srgba::black_alpha(240), stroke: Stroke::new(1.0, color::RED), }); - self.galley(rect.min, galley, text_style, color::RED); + self.layout(rect.min, layout, text_style, color::RED); } pub fn debug_arrow(&self, origin: Pos2, dir: Vec2, stroke: Stroke) { @@ -248,22 +249,21 @@ impl Painter { &self, pos: Pos2, anchor: (Align, Align), - text: impl Into<String>, + text: &str, text_style: TextStyle, text_color: Srgba, ) -> Rect { - let font = &self.fonts()[text_style]; - let galley = font.layout_multiline(text.into(), f32::INFINITY); - let rect = anchor_rect(Rect::from_min_size(pos, galley.size), anchor); - self.galley(rect.min, galley, text_style, text_color); + let layout = self.fonts().lock().layout_multiline(text_style, text, None); + let rect = anchor_rect(Rect::from_min_size(pos, layout.size), anchor); + self.layout(rect.min, layout, text_style, text_color); rect } - /// Paint text that has already been layed out in a `Galley`. - pub fn galley(&self, pos: Pos2, galley: font::Galley, text_style: TextStyle, color: Srgba) { + /// Paint text that has already been laid out in a `GlyphLayout`. + pub fn layout(&self, pos: Pos2, layout: GlyphLayout, text_style: TextStyle, color: Srgba) { self.add(PaintCmd::Text { pos, - galley, + layout, text_style, color, }); diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 7bbb5f67d1d9..78fac776171a 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -3,6 +3,7 @@ use std::{hash::Hash, sync::Arc}; use crate::{color::*, containers::*, layout::*, paint::*, widgets::*, *}; +use parking_lot::Mutex; /// Represents a region of the screen /// with a type of layout (horizontal or vertical). @@ -166,7 +167,7 @@ impl Ui { /// The `Fonts` of the `Context` associated with the `Ui`. /// Equivalent to `.ctx().fonts()`. - pub fn fonts(&self) -> &Fonts { + pub fn fonts(&self) -> Arc<Mutex<Fonts>> { self.ctx().fonts() } diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index 93a4917222f2..438ff9ad1084 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -6,7 +6,7 @@ #![allow(clippy::new_without_default)] -use crate::{layout::Direction, *}; +use crate::{layout::Direction, paint::fonts::GlyphLayout, paint::*, *}; pub mod color_picker; mod drag_value; @@ -16,8 +16,6 @@ pub(crate) mod text_edit; pub use {drag_value::DragValue, image::Image, slider::*, text_edit::*}; -use paint::*; - // ---------------------------------------------------------------------------- /// Anything implementing Widget can be added to a Ui with `Ui::add` @@ -74,7 +72,7 @@ impl Label { self } - pub fn layout(&self, ui: &Ui) -> font::Galley { + pub fn layout(&self, ui: &Ui) -> GlyphLayout { let max_width = ui.available().width(); // Prevent word-wrapping after a single letter, and other silly shit: // TODO: general "don't force labels and similar to wrap so early" @@ -82,19 +80,20 @@ impl Label { self.layout_width(ui, max_width) } - pub fn layout_width(&self, ui: &Ui, max_width: f32) -> font::Galley { + pub fn layout_width(&self, ui: &Ui, max_width: f32) -> GlyphLayout { let text_style = self.text_style_or_default(ui.style()); - let font = &ui.fonts()[text_style]; if self.multiline { - font.layout_multiline(self.text.clone(), max_width) // TODO: avoid clone + ui.fonts() + .lock() + .layout_multiline(text_style, &self.text, Some(max_width)) } else { - font.layout_single_line(self.text.clone()) // TODO: avoid clone + ui.fonts().lock().layout_single_line(text_style, &self.text) } } pub fn font_height(&self, fonts: &Fonts, style: &Style) -> f32 { let text_style = self.text_style_or_default(style); - fonts[text_style].height() + fonts.text_style_height(text_style) } // TODO: this should return a LabelLayout which has a paint method. @@ -105,12 +104,12 @@ impl Label { // TODO: a paint method for painting anywhere in a ui. // This should be the easiest method of putting text anywhere. - pub fn paint_galley(&self, ui: &mut Ui, pos: Pos2, galley: font::Galley) { + pub fn paint_layout(&self, ui: &mut Ui, pos: Pos2, layout: GlyphLayout) { let text_style = self.text_style_or_default(ui.style()); let text_color = self .text_color .unwrap_or_else(|| ui.style().visuals.text_color()); - ui.painter().galley(pos, galley, text_style, text_color); + ui.painter().layout(pos, layout, text_style, text_color); } /// Read the text style, or get the default for the current style @@ -130,9 +129,9 @@ macro_rules! label { impl Widget for Label { fn ui(self, ui: &mut Ui) -> Response { - let galley = self.layout(ui); - let rect = ui.allocate_space(galley.size); - self.paint_galley(ui, rect.min, galley); + let layout = self.layout(ui); + let rect = ui.allocate_space(layout.size); + self.paint_layout(ui, rect.min, layout); ui.interact_hover(rect) } } @@ -182,13 +181,14 @@ impl Hyperlink { impl Widget for Hyperlink { fn ui(self, ui: &mut Ui) -> Response { let Hyperlink { url, text } = self; - let color = color::LIGHT_BLUE; let text_style = ui.style().body_text_style; let id = ui.make_child_id(&url); - let font = &ui.fonts()[text_style]; - let galley = font.layout_multiline(text, ui.available().width()); - let rect = ui.allocate_space(galley.size); + let layout = + ui.fonts() + .lock() + .layout_multiline(text_style, &text, Some(ui.available().width())); + let rect = ui.allocate_space(layout.size); let response = ui.interact(rect, id, Sense::click()); if response.hovered { ui.ctx().output().cursor_icon = CursorIcon::PointingHand; @@ -197,10 +197,13 @@ impl Widget for Hyperlink { ui.ctx().output().open_url = Some(url.clone()); } + // FIXME Render the underline (render under all glyphs?) + /* let visuals = ui.style().interact(&response); - + // Render the underline if response.hovered { - // Underline: + + for line in &galley.lines { let pos = response.rect.min; let y = pos.y + line.y_max; @@ -213,9 +216,10 @@ impl Widget for Hyperlink { ); } } + */ ui.painter() - .galley(response.rect.min, galley, text_style, color); + .layout(response.rect.min, layout, text_style, color); response.on_hover_text(url) } @@ -294,9 +298,11 @@ impl Widget for Button { let button_padding = ui.style().spacing.button_padding; let id = ui.make_position_id(); - let font = &ui.fonts()[text_style]; - let galley = font.layout_multiline(text, ui.available().width()); - let mut desired_size = galley.size + 2.0 * button_padding; + let layout = + ui.fonts() + .lock() + .layout_multiline(text_style, &text, Some(ui.available().width())); + let mut desired_size = layout.size + 2.0 * button_padding; desired_size = desired_size.at_least(ui.style().spacing.interact_size); let rect = ui.allocate_space(desired_size); @@ -305,7 +311,7 @@ impl Widget for Button { // let text_cursor = response.rect.center() - 0.5 * galley.size; // centered-centered (looks bad for justified drop-down menus let text_cursor = pos2( response.rect.left() + button_padding.x, - response.rect.center().y - 0.5 * galley.size.y, + response.rect.center().y - 0.5 * layout.size.y, ); // left-centered let fill = fill.unwrap_or(visuals.bg_fill); ui.painter().rect( @@ -318,7 +324,7 @@ impl Widget for Button { .or(ui.style().visuals.override_text_color) .unwrap_or_else(|| visuals.text_color()); ui.painter() - .galley(text_cursor, galley, text_style, text_color); + .layout(text_cursor, layout, text_style, text_color); response } } @@ -359,15 +365,14 @@ impl<'a> Widget for Checkbox<'a> { let id = ui.make_position_id(); let text_style = TextStyle::Button; - let font = &ui.fonts()[text_style]; - let galley = font.layout_single_line(text); + let layout = ui.fonts().lock().layout_single_line(text_style, &text); let spacing = &ui.style().spacing; let icon_width = spacing.icon_width; let icon_spacing = ui.style().spacing.icon_spacing; let button_padding = spacing.button_padding; let mut desired_size = - button_padding + vec2(icon_width + icon_spacing, 0.0) + galley.size + button_padding; + button_padding + vec2(icon_width + icon_spacing, 0.0) + layout.size + button_padding; desired_size = desired_size.at_least(spacing.interact_size); desired_size.y = desired_size.y.max(icon_width); let rect = ui.allocate_space(desired_size); @@ -380,7 +385,7 @@ impl<'a> Widget for Checkbox<'a> { let visuals = ui.style().interact(&response); let text_cursor = pos2( response.rect.min.x + button_padding.x + icon_width + icon_spacing, - response.rect.center().y - 0.5 * galley.size.y, + response.rect.center().y - 0.5 * layout.size.y, ); let (small_icon_rect, big_icon_rect) = ui.style().spacing.icon_rectangles(response.rect); ui.painter().add(PaintCmd::Rect { @@ -407,7 +412,7 @@ impl<'a> Widget for Checkbox<'a> { .or(ui.style().visuals.override_text_color) .unwrap_or_else(|| visuals.text_color()); ui.painter() - .galley(text_cursor, galley, text_style, text_color); + .layout(text_cursor, layout, text_style, text_color); response } } @@ -446,14 +451,16 @@ impl Widget for RadioButton { } = self; let id = ui.make_position_id(); let text_style = TextStyle::Button; - let font = &ui.fonts()[text_style]; - let galley = font.layout_multiline(text, ui.available().width()); + let layout = + ui.fonts() + .lock() + .layout_multiline(text_style, &text, Some(ui.available().width())); let icon_width = ui.style().spacing.icon_width; let icon_spacing = ui.style().spacing.icon_spacing; let button_padding = ui.style().spacing.button_padding; let mut desired_size = - button_padding + vec2(icon_width + icon_spacing, 0.0) + galley.size + button_padding; + button_padding + vec2(icon_width + icon_spacing, 0.0) + layout.size + button_padding; desired_size = desired_size.at_least(ui.style().spacing.interact_size); desired_size.y = desired_size.y.max(icon_width); let rect = ui.allocate_space(desired_size); @@ -462,7 +469,7 @@ impl Widget for RadioButton { let text_cursor = pos2( response.rect.min.x + button_padding.x + icon_width + icon_spacing, - response.rect.center().y - 0.5 * galley.size.y, + response.rect.center().y - 0.5 * layout.size.y, ); let visuals = ui.style().interact(&response); @@ -492,7 +499,7 @@ impl Widget for RadioButton { let text_color = text_color .or(ui.style().visuals.override_text_color) .unwrap_or_else(|| visuals.text_color()); - painter.galley(text_cursor, galley, text_style, text_color); + painter.layout(text_cursor, layout, text_style, text_color); response } } diff --git a/egui/src/widgets/slider.rs b/egui/src/widgets/slider.rs index 6f4238245b89..d407abce1352 100644 --- a/egui/src/widgets/slider.rs +++ b/egui/src/widgets/slider.rs @@ -354,9 +354,10 @@ impl<'a> Slider<'a> { impl<'a> Widget for Slider<'a> { fn ui(mut self, ui: &mut Ui) -> Response { let text_style = TextStyle::Button; - let font = &ui.fonts()[text_style]; - let height = font - .line_spacing() + let height = ui + .fonts() + .lock() + .text_style_line_spacing(text_style) .at_least(ui.style().spacing.interact_size.y); if self.text.is_some() { diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 512cd6cad413..462f4c8eaa1a 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -96,17 +96,19 @@ impl<'t> Widget for TextEdit<'t> { let mut state = ui.memory().text_edit.get(&id).cloned().unwrap_or_default(); let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style); - let font = &ui.fonts()[text_style]; - let line_spacing = font.line_spacing(); + + let line_spacing = ui.fonts().lock().text_style_line_spacing(text_style); let available_width = ui.available().width(); - let mut galley = if multiline { - font.layout_multiline(text.clone(), available_width) + let layout = if multiline { + ui.fonts() + .lock() + .layout_multiline(text_style, text, Some(available_width)) } else { - font.layout_single_line(text.clone()) + ui.fonts().lock().layout_single_line(text_style, text) }; let desired_size = vec2( - galley.size.x.max(desired_width.min(available_width)), - galley.size.y.max(line_spacing), + layout.size.x.max(desired_width.min(available_width)), + layout.size.y.max(line_spacing), ); let rect = ui.allocate_space(desired_size); let sense = if enabled { @@ -118,9 +120,11 @@ impl<'t> Widget for TextEdit<'t> { if response.clicked && enabled { ui.memory().request_kb_focus(id); - if let Some(mouse_pos) = ui.input().mouse.pos { - state.cursor = Some(galley.char_at(mouse_pos - response.rect.min).char_idx); - } + /* FIXME find the character under the cursor + if let Some(mouse_pos) = ui.input().mouse.pos { + state.cursor = Some(layout.char_at(mouse_pos - response.rect.min).char_idx); + } + */ } else if ui.input().mouse.click || (ui.input().mouse.pressed && !response.hovered) { // User clicked somewhere else ui.memory().surrender_kb_focus(id); @@ -172,14 +176,13 @@ impl<'t> Widget for TextEdit<'t> { state.cursor = Some(cursor); // layout again to avoid frame delay: - let font = &ui.fonts()[text_style]; - galley = if multiline { - font.layout_multiline(text.clone(), available_width) + if multiline { + ui.fonts() + .lock() + .layout_multiline(text_style, text, Some(available_width)); } else { - font.layout_single_line(text.clone()) + ui.fonts().lock().layout_single_line(text_style, text); }; - - // dbg!(&galley); } let painter = ui.painter(); @@ -196,8 +199,10 @@ impl<'t> Widget for TextEdit<'t> { }); } + /* FIXME Print the cursor if ui.memory().has_kb_focus(id) { let cursor_blink_hz = ui.style().visuals.cursor_blink_hz; + let show_cursor = if 0.0 < cursor_blink_hz { ui.ctx().request_repaint(); // TODO: only when cursor blinks on or off (ui.input().time * cursor_blink_hz as f64 * 3.0).floor() as i64 % 3 != 0 @@ -205,6 +210,7 @@ impl<'t> Widget for TextEdit<'t> { true }; + if show_cursor { if let Some(cursor) = state.cursor { let cursor_pos = response.rect.min + galley.char_start_pos(cursor); @@ -215,11 +221,11 @@ impl<'t> Widget for TextEdit<'t> { } } } - + */ let text_color = text_color .or(ui.style().visuals.override_text_color) .unwrap_or_else(|| visuals.text_color()); - painter.galley(response.rect.min, galley, text_style, text_color); + painter.layout(response.rect.min, layout, text_style, text_color); ui.memory().text_edit.insert(id, state); response }