Implement rotating text
Closes https://github.com/emilk/egui/issues/428
This commit is contained in:
parent
6902151a96
commit
14c989fdfa
12 changed files with 112 additions and 69 deletions
|
@ -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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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.
|
||||||
///
|
///
|
||||||
|
|
|
@ -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},
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
Loading…
Reference in a new issue