Implement rotating text

Closes https://github.com/emilk/egui/issues/428
This commit is contained in:
Emil Ernerfeldt 2021-09-05 09:06:53 +02:00
parent 6902151a96
commit 14c989fdfa
12 changed files with 112 additions and 69 deletions

View file

@ -6,7 +6,7 @@ use crate::{
use epaint::{ use epaint::{
mutex::Mutex, mutex::Mutex,
text::{Fonts, Galley, TextStyle}, text::{Fonts, Galley, TextStyle},
Shape, Stroke, Shape, Stroke, TextShape,
}; };
/// Helper to paint shapes and text to a specific region on a specific layer. /// 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. /// It is up to the caller to make sure there is room for this.
/// Can be used for free painting. /// Can be used for free painting.
/// NOTE: all coordinates are screen coordinates! /// NOTE: all coordinates are screen coordinates!
pub fn add(&self, mut shape: Shape) -> ShapeIdx { pub fn add(&self, shape: impl Into<Shape>) -> ShapeIdx {
if self.fade_to_color == Some(Color32::TRANSPARENT) { if self.fade_to_color == Some(Color32::TRANSPARENT) {
self.paint_list.lock().add(self.clip_rect, Shape::Noop) self.paint_list.lock().add(self.clip_rect, Shape::Noop)
} else { } else {
let mut shape = shape.into();
self.transform_shape(&mut shape); self.transform_shape(&mut shape);
self.paint_list.lock().add(self.clip_rect, shape) self.paint_list.lock().add(self.clip_rect, shape)
} }
@ -399,11 +400,9 @@ impl Painter {
text_color: Color32, text_color: Color32,
) { ) {
if !galley.is_empty() { if !galley.is_empty() {
self.add(Shape::Text { self.add(TextShape {
pos,
galley,
underline: Stroke::none(),
override_text_color: Some(text_color), override_text_color: Some(text_color),
..TextShape::new(pos, galley)
}); });
} }
} }

View file

@ -83,11 +83,12 @@ impl Widget for Hyperlink {
Stroke::none() Stroke::none()
}; };
ui.painter().add(Shape::Text { ui.painter().add(epaint::TextShape {
pos, pos,
galley, galley,
override_text_color: Some(color), override_text_color: Some(color),
underline, underline,
angle: 0.0,
}); });
response.on_hover_text(url) response.on_hover_text(url)

View file

@ -251,11 +251,12 @@ impl Label {
Stroke::none() Stroke::none()
}; };
ui.painter().add(Shape::Text { ui.painter().add(epaint::TextShape {
pos, pos,
galley, galley,
override_text_color: Some(text_color), override_text_color: Some(text_color),
underline, underline,
angle: 0.0,
}); });
} }

View file

@ -1,5 +1,6 @@
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use egui::epaint::TextShape;
use egui_demo_lib::LOREM_IPSUM_LONG; use egui_demo_lib::LOREM_IPSUM_LONG;
pub fn criterion_benchmark(c: &mut Criterion) { 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 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 tessellator = egui::epaint::Tessellator::from_options(Default::default());
let mut mesh = egui::epaint::Mesh::default(); let mut mesh = egui::epaint::Mesh::default();
let text_shape = TextShape::new(egui::Pos2::ZERO, galley);
c.bench_function("tessellate_text", |b| { c.bench_function("tessellate_text", |b| {
b.iter(|| { b.iter(|| {
tessellator.tessellate_text( tessellator.tessellate_text(fonts.texture().size(), text_shape.clone(), &mut mesh);
fonts.texture().size(),
egui::Pos2::ZERO,
&galley,
Default::default(),
None,
&mut mesh,
);
mesh.clear(); mesh.clear();
}) })
}); });

View file

@ -150,6 +150,21 @@ impl Rect {
Rect::from_min_size(self.min + amnt, self.size()) 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. /// The intersection of two `Rect`, i.e. the area covered by both.
#[must_use] #[must_use]
pub fn intersect(self, other: Rect) -> Self { pub fn intersect(self, other: Rect) -> Self {

View file

@ -33,6 +33,7 @@ impl Rot2 {
/// The identity rotation: nothing rotates /// The identity rotation: nothing rotates
pub const IDENTITY: Self = Self { s: 0.0, c: 1.0 }; 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. /// A 𝞃/4 = 90° rotation means rotating the X axis to the Y axis.
pub fn from_angle(angle: f32) -> Self { pub fn from_angle(angle: f32) -> Self {
let (s, c) = angle.sin_cos(); let (s, c) = angle.sin_cos();

View file

@ -183,7 +183,7 @@ impl Vec2 {
self.y.atan2(self.x) 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 zero gives the unit X axis.
/// * An angle of 𝞃/4 = 90° gives the unit Y axis. /// * An angle of 𝞃/4 = 90° gives the unit Y axis.
/// ///

View file

@ -93,7 +93,7 @@ pub use {
color::{Color32, Rgba}, color::{Color32, Rgba},
mesh::{Mesh, Mesh16, Vertex}, mesh::{Mesh, Mesh16, Vertex},
shadow::Shadow, shadow::Shadow,
shape::Shape, shape::{Shape, TextShape},
stats::PaintStats, stats::PaintStats,
stroke::Stroke, stroke::Stroke,
tessellator::{TessellationOptions, Tessellator}, tessellator::{TessellationOptions, Tessellator},

View file

@ -40,22 +40,7 @@ pub enum Shape {
fill: Color32, fill: Color32,
stroke: Stroke, stroke: Stroke,
}, },
Text { Text(TextShape),
/// Top left corner of the first character..
pos: Pos2,
/// The layed out text.
galley: std::sync::Arc<Galley>,
/// 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<Color32>,
},
Mesh(Mesh), Mesh(Mesh),
} }
@ -181,15 +166,57 @@ impl Shape {
} }
pub fn galley(pos: Pos2, galley: std::sync::Arc<Galley>) -> Self { pub fn galley(pos: Pos2, galley: std::sync::Arc<Galley>) -> 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<Galley>,
/// 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<Color32>,
/// 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<Galley>) -> Self {
Self {
pos, pos,
galley, galley,
override_text_color: None,
underline: Stroke::none(), underline: Stroke::none(),
override_text_color: None,
angle: 0.0,
} }
} }
} }
impl Into<Shape> for TextShape {
#[inline(always)]
fn into(self) -> Shape {
Shape::Text(self)
}
}
// ----------------------------------------------------------------------------
/// Creates equally spaced filled circles from a line. /// Creates equally spaced filled circles from a line.
fn points_from_line( fn points_from_line(
line: &[Pos2], line: &[Pos2],
@ -294,8 +321,8 @@ impl Shape {
Shape::Rect { rect, .. } => { Shape::Rect { rect, .. } => {
*rect = rect.translate(delta); *rect = rect.translate(delta);
} }
Shape::Text { pos, .. } => { Shape::Text(text_shape) => {
*pos += delta; text_shape.pos += delta;
} }
Shape::Mesh(mesh) => { Shape::Mesh(mesh) => {
mesh.translate(delta); mesh.translate(delta);

View file

@ -24,17 +24,13 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) {
adjust_color(fill); adjust_color(fill);
adjust_color(&mut stroke.color); adjust_color(&mut stroke.color);
} }
Shape::Text { Shape::Text(text_shape) => {
galley, if let Some(override_text_color) = &mut text_shape.override_text_color {
override_text_color,
..
} => {
if let Some(override_text_color) = override_text_color {
adjust_color(override_text_color); adjust_color(override_text_color);
} }
if !galley.is_empty() { if !text_shape.galley.is_empty() {
let galley = std::sync::Arc::make_mut(galley); let galley = std::sync::Arc::make_mut(&mut text_shape.galley);
for row in &mut galley.rows { for row in &mut galley.rows {
for vertex in &mut row.visuals.mesh.vertices { for vertex in &mut row.visuals.mesh.vertices {
adjust_color(&mut vertex.color); adjust_color(&mut vertex.color);

View file

@ -198,8 +198,8 @@ impl PaintStats {
Shape::Path { points, .. } => { Shape::Path { points, .. } => {
self.shape_path += AllocInfo::from_slice(points); self.shape_path += AllocInfo::from_slice(points);
} }
Shape::Text { galley, .. } => { Shape::Text(text_shape) => {
self.shape_text += AllocInfo::from_galley(galley); self.shape_text += AllocInfo::from_galley(&text_shape.galley);
} }
Shape::Mesh(mesh) => { Shape::Mesh(mesh) => {
self.shape_mesh += AllocInfo::from_mesh(mesh); self.shape_mesh += AllocInfo::from_mesh(mesh);

View file

@ -617,16 +617,12 @@ impl Tessellator {
}; };
self.tessellate_rect(&rect, out); self.tessellate_rect(&rect, out);
} }
Shape::Text { Shape::Text(text_shape) => {
pos,
galley,
underline,
override_text_color,
} => {
if options.debug_paint_text_rects { if options.debug_paint_text_rects {
self.tessellate_rect( self.tessellate_rect(
&PaintRect { &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, corner_radius: 2.0,
fill: Default::default(), fill: Default::default(),
stroke: (0.5, Color32::GREEN).into(), stroke: (0.5, Color32::GREEN).into(),
@ -634,7 +630,7 @@ impl Tessellator {
out, 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); path.stroke_closed(stroke, self.options, out);
} }
pub fn tessellate_text( pub fn tessellate_text(&mut self, tex_size: [usize; 2], text_shape: TextShape, out: &mut Mesh) {
&mut self, let TextShape {
tex_size: [usize; 2], pos: galley_pos,
galley_pos: Pos2, galley,
galley: &super::Galley, underline,
underline: Stroke, override_text_color,
override_text_color: Option<Color32>, angle,
out: &mut Mesh, } = text_shape;
) {
if galley.is_empty() { if galley.is_empty() {
return; 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 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 { for row in &galley.rows {
if row.visuals.mesh.is_empty() { if row.visuals.mesh.is_empty() {
continue; 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) { if self.options.coarse_tessellation_culling && !self.clip_rect.intersects(row_rect) {
// culling individual lines of text is important, since a single `Shape::Text` // culling individual lines of text is important, since a single `Shape::Text`
@ -724,7 +726,7 @@ impl Tessellator {
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, vertex)| { .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 let Some(override_text_color) = override_text_color {
if row.visuals.glyph_vertex_range.contains(&i) { 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 { Vertex {
pos: galley_pos + vertex.pos.to_vec2(), pos: galley_pos + offset,
uv: (vertex.uv.to_vec2() * uv_normalizer).to_pos2(), uv: (uv.to_vec2() * uv_normalizer).to_pos2(),
color, color,
} }
}), }),