epaint/shapes/
text_shape.rs

1use std::sync::Arc;
2
3use emath::{Align2, Rot2};
4
5use crate::*;
6
7/// How to paint some text on screen.
8///
9/// This needs to be recreated if `pixels_per_point` (dpi scale) changes.
10#[derive(Clone, Debug, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
12pub struct TextShape {
13    /// Where the origin of [`Self::galley`] is.
14    ///
15    /// Usually the top left corner of the first character.
16    pub pos: Pos2,
17
18    /// The laid out text, from [`Fonts::layout_job`].
19    pub galley: Arc<Galley>,
20
21    /// Add this underline to the whole text.
22    /// You can also set an underline when creating the galley.
23    pub underline: Stroke,
24
25    /// Any [`Color32::PLACEHOLDER`] in the galley will be replaced by the given color.
26    /// Affects everything: backgrounds, glyphs, strikethrough, underline, etc.
27    pub fallback_color: Color32,
28
29    /// If set, the text color in the galley will be ignored and replaced
30    /// with the given color.
31    ///
32    /// This only affects the glyphs and will NOT replace background color nor strikethrough/underline color.
33    pub override_text_color: Option<Color32>,
34
35    /// If set, the text will be rendered with the given opacity in gamma space
36    /// Affects everything: backgrounds, glyphs, strikethrough, underline, etc.
37    pub opacity_factor: f32,
38
39    /// Rotate text by this many radians clockwise.
40    /// The pivot is `pos` (the upper left corner of the text).
41    pub angle: f32,
42}
43
44impl TextShape {
45    /// The given fallback color will be used for any uncolored part of the galley (using [`Color32::PLACEHOLDER`]).
46    ///
47    /// Any non-placeholder color in the galley takes precedence over this fallback color.
48    #[inline]
49    pub fn new(pos: Pos2, galley: Arc<Galley>, fallback_color: Color32) -> Self {
50        Self {
51            pos,
52            galley,
53            underline: Stroke::NONE,
54            fallback_color,
55            override_text_color: None,
56            opacity_factor: 1.0,
57            angle: 0.0,
58        }
59    }
60
61    /// The visual bounding rectangle
62    #[inline]
63    pub fn visual_bounding_rect(&self) -> Rect {
64        self.galley
65            .mesh_bounds
66            .rotate_bb(emath::Rot2::from_angle(self.angle))
67            .translate(self.pos.to_vec2())
68    }
69
70    #[inline]
71    pub fn with_underline(mut self, underline: Stroke) -> Self {
72        self.underline = underline;
73        self
74    }
75
76    /// Use the given color for the text, regardless of what color is already in the galley.
77    #[inline]
78    pub fn with_override_text_color(mut self, override_text_color: Color32) -> Self {
79        self.override_text_color = Some(override_text_color);
80        self
81    }
82
83    /// Set text rotation to `angle` radians clockwise.
84    /// The pivot is `pos` (the upper left corner of the text).
85    #[inline]
86    pub fn with_angle(mut self, angle: f32) -> Self {
87        self.angle = angle;
88        self
89    }
90
91    /// Set the text rotation to the `angle` radians clockwise.
92    /// The pivot is determined by the given `anchor` point on the text bounding box.
93    #[inline]
94    pub fn with_angle_and_anchor(mut self, angle: f32, anchor: Align2) -> Self {
95        self.angle = angle;
96        let a0 = anchor.pos_in_rect(&self.galley.rect).to_vec2();
97        let a1 = Rot2::from_angle(angle) * a0;
98        self.pos += a0 - a1;
99        self
100    }
101
102    /// Render text with this opacity in gamma space
103    #[inline]
104    pub fn with_opacity_factor(mut self, opacity_factor: f32) -> Self {
105        self.opacity_factor = opacity_factor;
106        self
107    }
108
109    /// Move the shape by this many points, in-place.
110    pub fn transform(&mut self, transform: emath::TSTransform) {
111        let Self {
112            pos,
113            galley,
114            underline,
115            fallback_color: _,
116            override_text_color: _,
117            opacity_factor: _,
118            angle: _,
119        } = self;
120
121        *pos = transform * *pos;
122        underline.width *= transform.scaling;
123
124        let Galley {
125            job: _,
126            rows,
127            elided: _,
128            rect,
129            mesh_bounds,
130            num_vertices: _,
131            num_indices: _,
132            pixels_per_point: _,
133            intrinsic_size,
134        } = Arc::make_mut(galley);
135
136        *rect = transform.scaling * *rect;
137        *mesh_bounds = transform.scaling * *mesh_bounds;
138        *intrinsic_size = transform.scaling * *intrinsic_size;
139
140        for text::PlacedRow { pos, row } in rows {
141            *pos *= transform.scaling;
142
143            let text::Row {
144                section_index_at_start: _,
145                glyphs: _, // TODO(emilk): would it make sense to transform these?
146                size,
147                visuals,
148                ends_with_newline: _,
149            } = Arc::make_mut(row);
150
151            *size *= transform.scaling;
152
153            let text::RowVisuals {
154                mesh,
155                mesh_bounds,
156                glyph_index_start: _,
157                glyph_vertex_range: _,
158            } = visuals;
159
160            *mesh_bounds = transform.scaling * *mesh_bounds;
161
162            for v in &mut mesh.vertices {
163                v.pos *= transform.scaling;
164            }
165        }
166    }
167}
168
169impl From<TextShape> for Shape {
170    #[inline(always)]
171    fn from(shape: TextShape) -> Self {
172        Self::Text(shape)
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::{super::*, *};
179    use crate::text::FontDefinitions;
180    use emath::almost_equal;
181
182    #[test]
183    fn text_bounding_box_under_rotation() {
184        let fonts = Fonts::new(
185            1.0,
186            1024,
187            AlphaFromCoverage::default(),
188            FontDefinitions::default(),
189        );
190        let font = FontId::monospace(12.0);
191
192        let mut t = crate::Shape::text(
193            &fonts,
194            Pos2::ZERO,
195            emath::Align2::CENTER_CENTER,
196            "testing123",
197            font,
198            Color32::BLACK,
199        );
200
201        let size_orig = t.visual_bounding_rect().size();
202
203        // 90 degree rotation
204        if let Shape::Text(ts) = &mut t {
205            ts.angle = std::f32::consts::PI / 2.0;
206        }
207
208        let size_rot = t.visual_bounding_rect().size();
209
210        // make sure the box is actually rotated
211        assert!(almost_equal(size_orig.x, size_rot.y, 1e-4));
212        assert!(almost_equal(size_orig.y, size_rot.x, 1e-4));
213    }
214}