From 14c989fdfa0739d95b606058a3f8eb055aa0df21 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 5 Sep 2021 09:06:53 +0200 Subject: [PATCH] Implement rotating text Closes https://github.com/emilk/egui/issues/428 --- egui/src/painter.rs | 11 +++-- egui/src/widgets/hyperlink.rs | 3 +- egui/src/widgets/label.rs | 3 +- egui_demo_lib/benches/benchmark.rs | 11 ++--- emath/src/rect.rs | 15 +++++++ emath/src/rot2.rs | 1 + emath/src/vec2.rs | 2 +- epaint/src/lib.rs | 2 +- epaint/src/shape.rs | 67 +++++++++++++++++++++--------- epaint/src/shape_transform.rs | 12 ++---- epaint/src/stats.rs | 4 +- epaint/src/tessellator.rs | 50 ++++++++++++---------- 12 files changed, 112 insertions(+), 69 deletions(-) diff --git a/egui/src/painter.rs b/egui/src/painter.rs index 9c638eb2..ad0a2117 100644 --- a/egui/src/painter.rs +++ b/egui/src/painter.rs @@ -6,7 +6,7 @@ use crate::{ use epaint::{ mutex::Mutex, text::{Fonts, Galley, TextStyle}, - Shape, Stroke, + Shape, Stroke, TextShape, }; /// Helper to paint shapes and text to a specific region on a specific layer. @@ -154,10 +154,11 @@ impl Painter { /// It is up to the caller to make sure there is room for this. /// Can be used for free painting. /// NOTE: all coordinates are screen coordinates! - pub fn add(&self, mut shape: Shape) -> ShapeIdx { + pub fn add(&self, shape: impl Into) -> ShapeIdx { if self.fade_to_color == Some(Color32::TRANSPARENT) { self.paint_list.lock().add(self.clip_rect, Shape::Noop) } else { + let mut shape = shape.into(); self.transform_shape(&mut shape); self.paint_list.lock().add(self.clip_rect, shape) } @@ -399,11 +400,9 @@ impl Painter { text_color: Color32, ) { if !galley.is_empty() { - self.add(Shape::Text { - pos, - galley, - underline: Stroke::none(), + self.add(TextShape { override_text_color: Some(text_color), + ..TextShape::new(pos, galley) }); } } diff --git a/egui/src/widgets/hyperlink.rs b/egui/src/widgets/hyperlink.rs index 0b45e5e7..28f47edb 100644 --- a/egui/src/widgets/hyperlink.rs +++ b/egui/src/widgets/hyperlink.rs @@ -83,11 +83,12 @@ impl Widget for Hyperlink { Stroke::none() }; - ui.painter().add(Shape::Text { + ui.painter().add(epaint::TextShape { pos, galley, override_text_color: Some(color), underline, + angle: 0.0, }); response.on_hover_text(url) diff --git a/egui/src/widgets/label.rs b/egui/src/widgets/label.rs index 28c5f16d..82fcf1d9 100644 --- a/egui/src/widgets/label.rs +++ b/egui/src/widgets/label.rs @@ -251,11 +251,12 @@ impl Label { Stroke::none() }; - ui.painter().add(Shape::Text { + ui.painter().add(epaint::TextShape { pos, galley, override_text_color: Some(text_color), underline, + angle: 0.0, }); } diff --git a/egui_demo_lib/benches/benchmark.rs b/egui_demo_lib/benches/benchmark.rs index b774357e..c44210d6 100644 --- a/egui_demo_lib/benches/benchmark.rs +++ b/egui_demo_lib/benches/benchmark.rs @@ -1,5 +1,6 @@ use criterion::{criterion_group, criterion_main, Criterion}; +use egui::epaint::TextShape; use egui_demo_lib::LOREM_IPSUM_LONG; pub fn criterion_benchmark(c: &mut Criterion) { @@ -93,16 +94,10 @@ pub fn criterion_benchmark(c: &mut Criterion) { let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), text_style, 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); c.bench_function("tessellate_text", |b| { b.iter(|| { - tessellator.tessellate_text( - fonts.texture().size(), - egui::Pos2::ZERO, - &galley, - Default::default(), - None, - &mut mesh, - ); + tessellator.tessellate_text(fonts.texture().size(), text_shape.clone(), &mut mesh); mesh.clear(); }) }); diff --git a/emath/src/rect.rs b/emath/src/rect.rs index 8fa7e690..446ddf34 100644 --- a/emath/src/rect.rs +++ b/emath/src/rect.rs @@ -150,6 +150,21 @@ impl Rect { Rect::from_min_size(self.min + amnt, self.size()) } + /// Rotate the bounds (will expand the `Rect`) + #[must_use] + #[inline] + pub fn rotate_bb(self, rot: crate::Rot2) -> Self { + let a = rot * self.left_top().to_vec2(); + let b = rot * self.right_top().to_vec2(); + let c = rot * self.left_bottom().to_vec2(); + let d = rot * self.right_bottom().to_vec2(); + + Self::from_min_max( + a.min(b).min(c).min(d).to_pos2(), + a.max(b).max(c).max(d).to_pos2(), + ) + } + /// The intersection of two `Rect`, i.e. the area covered by both. #[must_use] pub fn intersect(self, other: Rect) -> Self { diff --git a/emath/src/rot2.rs b/emath/src/rot2.rs index 8786b37f..5da3ce9c 100644 --- a/emath/src/rot2.rs +++ b/emath/src/rot2.rs @@ -33,6 +33,7 @@ impl Rot2 { /// The identity rotation: nothing rotates pub const IDENTITY: Self = Self { s: 0.0, c: 1.0 }; + /// Angle is clockwise in radians. /// A 𝞃/4 = 90° rotation means rotating the X axis to the Y axis. pub fn from_angle(angle: f32) -> Self { let (s, c) = angle.sin_cos(); diff --git a/emath/src/vec2.rs b/emath/src/vec2.rs index 9593434f..d5eaaf3a 100644 --- a/emath/src/vec2.rs +++ b/emath/src/vec2.rs @@ -183,7 +183,7 @@ impl Vec2 { self.y.atan2(self.x) } - /// Create a unit vector with the given angle (in radians). + /// Create a unit vector with the given CW angle (in radians). /// * An angle of zero gives the unit X axis. /// * An angle of 𝞃/4 = 90° gives the unit Y axis. /// diff --git a/epaint/src/lib.rs b/epaint/src/lib.rs index 761d2590..79bc7c18 100644 --- a/epaint/src/lib.rs +++ b/epaint/src/lib.rs @@ -93,7 +93,7 @@ pub use { color::{Color32, Rgba}, mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, - shape::Shape, + shape::{Shape, TextShape}, stats::PaintStats, stroke::Stroke, tessellator::{TessellationOptions, Tessellator}, diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index b34c2d0e..83c795d7 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -40,22 +40,7 @@ pub enum Shape { fill: Color32, stroke: Stroke, }, - Text { - /// Top left corner of the first character.. - pos: Pos2, - - /// The layed out text. - galley: std::sync::Arc, - - /// Add this underline to the whole text. - /// You can also set an underline when creating the galley. - underline: Stroke, - - /// If set, the text color in the galley will be ignored and replaced - /// with the given color. - /// This will NOT replace background color nor strikethrough/underline color. - override_text_color: Option, - }, + Text(TextShape), Mesh(Mesh), } @@ -181,15 +166,57 @@ impl Shape { } pub fn galley(pos: Pos2, galley: std::sync::Arc) -> Self { - Self::Text { + TextShape::new(pos, galley).into() + } +} + +// ---------------------------------------------------------------------------- + +/// How to draw some text on screen. +#[derive(Clone, Debug, PartialEq)] +pub struct TextShape { + /// Top left corner of the first character. + pub pos: Pos2, + + /// The layed out text, from [`Fonts::layout_job`]. + pub galley: std::sync::Arc, + + /// Add this underline to the whole text. + /// You can also set an underline when creating the galley. + pub underline: Stroke, + + /// If set, the text color in the galley will be ignored and replaced + /// with the given color. + /// This will NOT replace background color nor strikethrough/underline color. + pub override_text_color: Option, + + /// Rotate text by this many radians clock-wise. + /// The pivot is `pos` (the upper left corner of the text). + pub angle: f32, +} + +impl TextShape { + #[inline] + pub fn new(pos: Pos2, galley: std::sync::Arc) -> Self { + Self { pos, galley, - override_text_color: None, underline: Stroke::none(), + override_text_color: None, + angle: 0.0, } } } +impl Into for TextShape { + #[inline(always)] + fn into(self) -> Shape { + Shape::Text(self) + } +} + +// ---------------------------------------------------------------------------- + /// Creates equally spaced filled circles from a line. fn points_from_line( line: &[Pos2], @@ -294,8 +321,8 @@ impl Shape { Shape::Rect { rect, .. } => { *rect = rect.translate(delta); } - Shape::Text { pos, .. } => { - *pos += delta; + Shape::Text(text_shape) => { + text_shape.pos += delta; } Shape::Mesh(mesh) => { mesh.translate(delta); diff --git a/epaint/src/shape_transform.rs b/epaint/src/shape_transform.rs index d2ba20f3..7c41a211 100644 --- a/epaint/src/shape_transform.rs +++ b/epaint/src/shape_transform.rs @@ -24,17 +24,13 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) { adjust_color(fill); adjust_color(&mut stroke.color); } - Shape::Text { - galley, - override_text_color, - .. - } => { - if let Some(override_text_color) = override_text_color { + Shape::Text(text_shape) => { + if let Some(override_text_color) = &mut text_shape.override_text_color { adjust_color(override_text_color); } - if !galley.is_empty() { - let galley = std::sync::Arc::make_mut(galley); + if !text_shape.galley.is_empty() { + let galley = std::sync::Arc::make_mut(&mut text_shape.galley); for row in &mut galley.rows { for vertex in &mut row.visuals.mesh.vertices { adjust_color(&mut vertex.color); diff --git a/epaint/src/stats.rs b/epaint/src/stats.rs index 0e69dd38..ef7c5c79 100644 --- a/epaint/src/stats.rs +++ b/epaint/src/stats.rs @@ -198,8 +198,8 @@ impl PaintStats { Shape::Path { points, .. } => { self.shape_path += AllocInfo::from_slice(points); } - Shape::Text { galley, .. } => { - self.shape_text += AllocInfo::from_galley(galley); + Shape::Text(text_shape) => { + self.shape_text += AllocInfo::from_galley(&text_shape.galley); } Shape::Mesh(mesh) => { self.shape_mesh += AllocInfo::from_mesh(mesh); diff --git a/epaint/src/tessellator.rs b/epaint/src/tessellator.rs index ce5b17fb..6caceafd 100644 --- a/epaint/src/tessellator.rs +++ b/epaint/src/tessellator.rs @@ -617,16 +617,12 @@ impl Tessellator { }; self.tessellate_rect(&rect, out); } - Shape::Text { - pos, - galley, - underline, - override_text_color, - } => { + Shape::Text(text_shape) => { if options.debug_paint_text_rects { self.tessellate_rect( &PaintRect { - rect: Rect::from_min_size(pos, galley.size).expand(0.5), + rect: Rect::from_min_size(text_shape.pos, text_shape.galley.size) + .expand(0.5), corner_radius: 2.0, fill: Default::default(), stroke: (0.5, Color32::GREEN).into(), @@ -634,7 +630,7 @@ impl Tessellator { out, ); } - self.tessellate_text(tex_size, pos, &galley, underline, override_text_color, out); + self.tessellate_text(tex_size, text_shape, out); } } } @@ -669,15 +665,15 @@ impl Tessellator { path.stroke_closed(stroke, self.options, out); } - pub fn tessellate_text( - &mut self, - tex_size: [usize; 2], - galley_pos: Pos2, - galley: &super::Galley, - underline: Stroke, - override_text_color: Option, - out: &mut Mesh, - ) { + pub fn tessellate_text(&mut self, tex_size: [usize; 2], text_shape: TextShape, out: &mut Mesh) { + let TextShape { + pos: galley_pos, + galley, + underline, + override_text_color, + angle, + } = text_shape; + if galley.is_empty() { return; } @@ -694,12 +690,18 @@ impl Tessellator { let uv_normalizer = vec2(1.0 / tex_size[0] as f32, 1.0 / tex_size[1] as f32); + let rotator = Rot2::from_angle(angle); + for row in &galley.rows { if row.visuals.mesh.is_empty() { continue; } - let row_rect = row.visuals.mesh_bounds.translate(galley_pos.to_vec2()); + let mut row_rect = row.visuals.mesh_bounds; + if angle != 0.0 { + row_rect = row_rect.rotate_bb(rotator); + } + row_rect = row_rect.translate(galley_pos.to_vec2()); if self.options.coarse_tessellation_culling && !self.clip_rect.intersects(row_rect) { // culling individual lines of text is important, since a single `Shape::Text` @@ -724,7 +726,7 @@ impl Tessellator { .iter() .enumerate() .map(|(i, vertex)| { - let mut color = vertex.color; + let Vertex { pos, uv, mut color } = *vertex; if let Some(override_text_color) = override_text_color { if row.visuals.glyph_vertex_range.contains(&i) { @@ -732,9 +734,15 @@ impl Tessellator { } } + let offset = if angle == 0.0 { + pos.to_vec2() + } else { + rotator * pos.to_vec2() + }; + Vertex { - pos: galley_pos + vertex.pos.to_vec2(), - uv: (vertex.uv.to_vec2() * uv_normalizer).to_pos2(), + pos: galley_pos + offset, + uv: (uv.to_vec2() * uv_normalizer).to_pos2(), color, } }),