use std::sync::Arc; use emath::{Align2, Rot2}; use crate::*; /// How to paint some text on screen. /// /// This needs to be recreated if `pixels_per_point` (dpi scale) changes. #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct TextShape { /// Where the origin of [`Self::galley`] is. /// /// Usually the top left corner of the first character. pub pos: Pos2, /// The laid out text, from [`Fonts::layout_job`]. pub galley: Arc, /// Add this underline to the whole text. /// You can also set an underline when creating the galley. pub underline: Stroke, /// Any [`Color32::PLACEHOLDER`] in the galley will be replaced by the given color. /// Affects everything: backgrounds, glyphs, strikethrough, underline, etc. pub fallback_color: Color32, /// If set, the text color in the galley will be ignored and replaced /// with the given color. /// /// This only affects the glyphs and will NOT replace background color nor strikethrough/underline color. pub override_text_color: Option, /// If set, the text will be rendered with the given opacity in gamma space /// Affects everything: backgrounds, glyphs, strikethrough, underline, etc. pub opacity_factor: f32, /// Rotate text by this many radians clockwise. /// The pivot is `pos` (the upper left corner of the text). pub angle: f32, } impl TextShape { /// The given fallback color will be used for any uncolored part of the galley (using [`Color32::PLACEHOLDER`]). /// /// Any non-placeholder color in the galley takes precedence over this fallback color. #[inline] pub fn new(pos: Pos2, galley: Arc, fallback_color: Color32) -> Self { Self { pos, galley, underline: Stroke::NONE, fallback_color, override_text_color: None, opacity_factor: 1.0, angle: 0.0, } } /// The visual bounding rectangle #[inline] pub fn visual_bounding_rect(&self) -> Rect { self.galley .mesh_bounds .rotate_bb(emath::Rot2::from_angle(self.angle)) .translate(self.pos.to_vec2()) } #[inline] pub fn with_underline(mut self, underline: Stroke) -> Self { self.underline = underline; self } /// Use the given color for the text, regardless of what color is already in the galley. #[inline] pub fn with_override_text_color(mut self, override_text_color: Color32) -> Self { self.override_text_color = Some(override_text_color); self } /// Set text rotation to `angle` radians clockwise. /// The pivot is `pos` (the upper left corner of the text). #[inline] pub fn with_angle(mut self, angle: f32) -> Self { self.angle = angle; self } /// Set the text rotation to the `angle` radians clockwise. /// The pivot is determined by the given `anchor` point on the text bounding box. #[inline] pub fn with_angle_and_anchor(mut self, angle: f32, anchor: Align2) -> Self { self.angle = angle; let a0 = anchor.pos_in_rect(&self.galley.rect).to_vec2(); let a1 = Rot2::from_angle(angle) * a0; self.pos += a0 - a1; self } /// Render text with this opacity in gamma space #[inline] pub fn with_opacity_factor(mut self, opacity_factor: f32) -> Self { self.opacity_factor = opacity_factor; self } /// Move the shape by this many points, in-place. pub fn transform(&mut self, transform: emath::TSTransform) { let Self { pos, galley, underline, fallback_color: _, override_text_color: _, opacity_factor: _, angle: _, } = self; *pos = transform * *pos; underline.width *= transform.scaling; let Galley { job: _, rows, elided: _, rect, mesh_bounds, num_vertices: _, num_indices: _, pixels_per_point: _, } = Arc::make_mut(galley); *rect = transform.scaling * *rect; *mesh_bounds = transform.scaling * *mesh_bounds; for text::PlacedRow { pos, row } in rows { *pos *= transform.scaling; let text::Row { section_index_at_start: _, glyphs: _, // TODO(emilk): would it make sense to transform these? size, visuals, ends_with_newline: _, } = Arc::make_mut(row); *size *= transform.scaling; let text::RowVisuals { mesh, mesh_bounds, glyph_index_start: _, glyph_vertex_range: _, } = visuals; *mesh_bounds = transform.scaling * *mesh_bounds; for v in &mut mesh.vertices { v.pos *= transform.scaling; } } } } impl From for Shape { #[inline(always)] fn from(shape: TextShape) -> Self { Self::Text(shape) } } #[cfg(test)] mod tests { use super::{super::*, *}; use crate::text::FontDefinitions; use emath::almost_equal; #[test] fn text_bounding_box_under_rotation() { let fonts = Fonts::new(1.0, 1024, FontDefinitions::default()); let font = FontId::monospace(12.0); let mut t = crate::Shape::text( &fonts, Pos2::ZERO, emath::Align2::CENTER_CENTER, "testing123", font, Color32::BLACK, ); let size_orig = t.visual_bounding_rect().size(); // 90 degree rotation if let Shape::Text(ts) = &mut t { ts.angle = std::f32::consts::PI / 2.0; } let size_rot = t.visual_bounding_rect().size(); // make sure the box is actually rotated assert!(almost_equal(size_orig.x, size_rot.y, 1e-4)); assert!(almost_equal(size_orig.y, size_rot.x, 1e-4)); } }