diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..035ec83a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Egui Changelog + +## Unreleased + +* Color picker +* Unicode characters in labels (limited by [what the default font supports](https://fonts.google.com/specimen/Comfortaa#glyphs)) + +## 0.1.4 - 2020-09-08 + +This is when I started the CHANGELOG.md, after almost two years of development. Better late than never. + +* Widgets: label, text button, hyperlink, checkbox, radio button, slider, draggable value, text editing +* Layouts: horizontal, vertical, columns +* Text input: very basic, multiline, copy/paste +* Windows: move, resize, name, minimize and close. Automatically sized and positioned. +* Regions: resizing, vertical scrolling, collapsing headers (sections) +* Rendering: Anti-aliased rendering of lines, circles, text and convex polygons. +* Tooltips on hover diff --git a/TODO.md b/TODO.md index 1d798b8e..09b56e30 100644 --- a/TODO.md +++ b/TODO.md @@ -34,9 +34,11 @@ TODO-list for the Egui project. If you looking for something to do, look here. * [ ] Distinguish between touch input and mouse input * [ ] Get modifier keys * [ ] Keyboard shortcuts + * [ ] Copy, paste, undo, ... * [ ] Text - * [ ] Unicode - * [ ] Shared mutable expanding texture map? + * [/] Unicode + * [x] Shared mutable expanding texture map + * [ ] Text editing of unicode * [ ] Change text style/color and continue in same layout * [ ] Menu bar (File, Edit, etc) * [ ] Sub-menus diff --git a/egui/src/context.rs b/egui/src/context.rs index a9071a46..dcca8de5 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -110,9 +110,10 @@ impl Context { .expect("No fonts available until first call to Context::begin_frame()`") } + /// 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) -> &paint::Texture { + pub fn texture(&self) -> Arc { self.fonts().texture() } diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index fc7a1987..8eed65fc 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -491,6 +491,9 @@ impl Widgets { ); }); + ui.add(label!("Some non-latin characters: Ευρηκα τ = 2×π")) + .tooltip_text("The current font supports only a few non-latin characters and Egui does not currently support right-to-left text."); + ui.horizontal(|ui| { ui.radio_value("First", &mut self.radio, 0); ui.radio_value("Second", &mut self.radio, 1); diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 467292c8..99de597d 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -13,6 +13,9 @@ impl Texture { self.width, self.height )); + if self.width <= 1 || self.height <= 1 { + return; + } let mut size = vec2(self.width as f32, self.height as f32); if size.x > ui.available().width() { size *= ui.available().width() / size.x; @@ -40,8 +43,8 @@ impl Texture { let v = remap_clamp(pos.y, rect.range_y(), 0.0..=self.height as f32 - 1.0).round(); let texel_radius = 32.0; - let u = clamp(u, texel_radius..=self.width as f32 - 1.0 - texel_radius); - let v = clamp(v, texel_radius..=self.height as f32 - 1.0 - texel_radius); + let u = u.max(texel_radius); + let v = v.max(texel_radius); let top_left = Vertex { pos: zoom_rect.min, diff --git a/egui/src/paint/font.rs b/egui/src/paint/font.rs index c9561283..8e0f8547 100644 --- a/egui/src/paint/font.rs +++ b/egui/src/paint/font.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use { ahash::AHashMap, - parking_lot::Mutex, + parking_lot::{Mutex, RwLock}, rusttype::{point, Scale}, }; @@ -176,13 +176,13 @@ pub struct GlyphInfo { } /// The interface uses points as the unit for everything. -#[derive(Clone)] pub struct Font { font: rusttype::Font<'static>, /// Maximum character height scale_in_pixels: f32, pixels_per_point: f32, - glyph_infos: AHashMap, // TODO: see if we can optimize if we switch to a binary search + replacement_glyph_info: GlyphInfo, + glyph_infos: RwLock>, atlas: Arc>, } @@ -199,22 +199,40 @@ impl Font { let font = rusttype::Font::try_from_bytes(font_data).expect("Error constructing Font"); let scale_in_pixels = pixels_per_point * scale_in_points; - let mut font = Font { + 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, }; - /// Printable ASCII characters [32, 126], which excludes control codes. + 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.add_char(c); + font.glyph_info(c); } - font.add_char(REPLACEMENT_CHAR); - font.add_char('°'); + font.glyph_info('°'); font } @@ -233,86 +251,38 @@ impl Font { } pub fn uv_rect(&self, c: char) -> Option { - self.glyph_infos.get(&c).and_then(|gi| gi.uv_rect) + self.glyph_infos.read().get(&c).and_then(|gi| gi.uv_rect) } - fn glyph_info_or_none(&self, c: char) -> Option<&GlyphInfo> { - self.glyph_infos.get(&c) - } - - fn glyph_info_or_replacemnet(&self, c: char) -> &GlyphInfo { - self.glyph_info_or_none(c) - .unwrap_or_else(|| self.glyph_info_or_none(REPLACEMENT_CHAR).unwrap()) - } - - fn add_char(&mut self, c: char) { - if self.glyph_infos.contains_key(&c) { - return; + fn glyph_info(&self, c: char) -> GlyphInfo { + if c == '\n' { + // Hack: else we show '\n' as '?' (REPLACEMENT_CHAR) + return self.glyph_info(' '); } - let glyph = self.font.glyph(c); - assert_ne!( - glyph.id().0, - 0, - "Failed to find a glyph for the character '{}'", - c - ); - let glyph = glyph.scaled(Scale::uniform(self.scale_in_pixels)); - let glyph = glyph.positioned(point(0.0, 0.0)); + { + if let Some(glyph_info) = self.glyph_infos.read().get(&c) { + return *glyph_info; + } + } - 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 mut atlas_lock = self.atlas.lock(); - let glyph_pos = atlas_lock.allocate((glyph_width, glyph_height)); - - let texture = atlas_lock.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 = - self.scale_in_pixels as f32 + bb.min.y as f32 - 4.0 * self.pixels_per_point; // TODO: use font.v_metrics - Some(UvRect { - offset: vec2( - bb.min.x as f32 / self.pixels_per_point, - offset_y_in_pixels / self.pixels_per_point, - ), - size: vec2(glyph_width as f32, glyph_height as f32) / self.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 / self.pixels_per_point; - - self.glyph_infos.insert( + // Add new character: + let glyph_info = allocate_glyph( + &mut self.atlas.lock(), c, - GlyphInfo { - id: glyph.id(), - advance_width: advance_width_in_points, - uv_rect, - }, + &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 frament. + /// 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 { @@ -397,7 +367,7 @@ impl Font { let mut last_glyph_id = None; for c in text.chars() { - let glyph = self.glyph_info_or_replacemnet(c); + let glyph = self.glyph_info(c); if let Some(last_glyph_id) = last_glyph_id { cursor_x_in_points += @@ -500,3 +470,62 @@ impl Font { out_lines } } + +fn allocate_glyph( + atlas: &mut TextureAtlas, + c: char, + font: &rusttype::Font<'static>, + scale_in_pixels: f32, + pixels_per_point: f32, +) -> Option { + 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 6ea9b30e..2aa6df97 100644 --- a/egui/src/paint/fonts.rs +++ b/egui/src/paint/fonts.rs @@ -63,7 +63,10 @@ impl FontDefinitions { pub struct Fonts { definitions: FontDefinitions, fonts: BTreeMap, - texture: Texture, + atlas: Arc>, + /// 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>, } impl Fonts { @@ -82,22 +85,25 @@ impl Fonts { return; } - let mut atlas = TextureAtlas::new(512, 8); // TODO: better default? + let mut atlas = TextureAtlas::new(512, 16); // TODO: better default? // Make the top left four pixels fully white: - let pos = atlas.allocate((2, 2)); - assert_eq!(pos, (0, 0)); - atlas.texture_mut()[(0, 0)] = 255; - atlas.texture_mut()[(0, 1)] = 255; - atlas.texture_mut()[(1, 0)] = 255; - atlas.texture_mut()[(1, 1)] = 255; + { + let pos = atlas.allocate((2, 2)); + assert_eq!(pos, (0, 0)); + let tex = atlas.texture_mut(); + tex[(0, 0)] = 255; + tex[(0, 1)] = 255; + tex[(1, 0)] = 255; + tex[(1, 1)] = 255; + } let atlas = Arc::new(Mutex::new(atlas)); // TODO: figure out a way to make the wasm smaller despite including a font. Zip it? - let monospae_typeface_data = include_bytes!("../../fonts/ProggyClean.ttf"); // Use 13 for this. NOTHING ELSE. + let monospace_typeface_data = include_bytes!("../../fonts/ProggyClean.ttf"); // Use 13 for this. NOTHING ELSE. - // let monospae_typeface_data = include_bytes!("../../fonts/Roboto-Regular.ttf"); + // let monospace_typeface_data = include_bytes!("../../fonts/Roboto-Regular.ttf"); let variable_typeface_data = include_bytes!("../../fonts/Comfortaa-Regular.ttf"); // Funny, hard to read @@ -112,7 +118,7 @@ impl Fonts { .into_iter() .map(|(text_style, (family, size))| { let typeface_data: &[u8] = match family { - FontFamily::Monospace => monospae_typeface_data, + FontFamily::Monospace => monospace_typeface_data, FontFamily::VariableWidth => variable_typeface_data, }; @@ -122,15 +128,28 @@ impl Fonts { ) }) .collect(); - self.texture = atlas.lock().texture().clone(); - let mut hasher = ahash::AHasher::default(); - self.texture.pixels.hash(&mut hasher); - self.texture.version = hasher.finish(); + { + 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(); + } + + self.buffered_texture = Default::default(); //atlas.lock().texture().clone(); + self.atlas = atlas; } - pub fn texture(&self) -> &Texture { - &self.texture + pub fn texture(&self) -> Arc { + let atlas = self.atlas.lock(); + let mut buffered_texture = self.buffered_texture.lock(); + if buffered_texture.version != atlas.texture().version { + *buffered_texture = Arc::new(atlas.texture().clone()); + } + + buffered_texture.clone() } } diff --git a/egui_glium/src/backend.rs b/egui_glium/src/backend.rs index a4a07bf0..45d9a2ed 100644 --- a/egui_glium/src/backend.rs +++ b/egui_glium/src/backend.rs @@ -105,7 +105,7 @@ pub fn run( let frame_time = (Instant::now() - egui_start).as_secs_f64() as f32; runner.frame_times.add(raw_input.time, frame_time); - painter.paint_jobs(&display, paint_jobs, ctx.texture()); + painter.paint_jobs(&display, paint_jobs, &ctx.texture()); if runner.quit { *control_flow = glutin::event_loop::ControlFlow::Exit diff --git a/egui_glium/src/painter.rs b/egui_glium/src/painter.rs index 162cf392..8f6e24ba 100644 --- a/egui_glium/src/painter.rs +++ b/egui_glium/src/painter.rs @@ -258,7 +258,7 @@ impl Painter { u_sampler: &self.texture, }; - // Emilib outputs colors with premultiplied alpha: + // Egui outputs colors with premultiplied alpha: let blend_func = glium::BlendingFunction::Addition { source: glium::LinearBlendingFactor::One, destination: glium::LinearBlendingFactor::OneMinusSourceAlpha, diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 2f119d29..68c97461 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -59,7 +59,7 @@ impl WebBackend { self.painter.paint_jobs( bg_color, paint_jobs, - self.ctx.texture(), + &self.ctx.texture(), self.ctx.pixels_per_point(), ) }