spitfire_draw/
text.rs

1use crate::{
2    context::DrawContext,
3    utils::{Drawable, ShaderRef, Vertex, transform_to_matrix},
4};
5use fontdue::layout::{
6    CoordinateSystem, HorizontalAlign, Layout, LayoutSettings, TextStyle, VerticalAlign,
7};
8use spitfire_fontdue::TextRenderer;
9use spitfire_glow::{
10    graphics::{Graphics, GraphicsBatch},
11    renderer::{GlowBlending, GlowTextureFiltering, GlowUniformValue},
12};
13use std::{borrow::Cow, collections::HashMap};
14use vek::{Quaternion, Rect, Rgba, Transform, Vec2, Vec3};
15
16pub struct Text {
17    pub shader: Option<ShaderRef>,
18    pub font: Cow<'static, str>,
19    pub size: f32,
20    pub text: Cow<'static, str>,
21    pub tint: Rgba<f32>,
22    pub horizontal_align: HorizontalAlign,
23    pub vertical_align: VerticalAlign,
24    pub width: Option<f32>,
25    pub height: Option<f32>,
26    pub uniforms: HashMap<Cow<'static, str>, GlowUniformValue>,
27    pub transform: Transform<f32, f32, f32>,
28    pub blending: Option<GlowBlending>,
29    pub screen_space: bool,
30}
31
32impl Default for Text {
33    fn default() -> Self {
34        Self {
35            shader: Default::default(),
36            font: Default::default(),
37            size: 32.0,
38            text: Default::default(),
39            tint: Rgba::white(),
40            horizontal_align: HorizontalAlign::Left,
41            vertical_align: VerticalAlign::Top,
42            width: Default::default(),
43            height: Default::default(),
44            uniforms: Default::default(),
45            transform: Default::default(),
46            blending: Default::default(),
47            screen_space: Default::default(),
48        }
49    }
50}
51
52impl Text {
53    pub fn new(shader: ShaderRef) -> Self {
54        Self {
55            shader: Some(shader),
56            ..Default::default()
57        }
58    }
59
60    pub fn shader(mut self, value: ShaderRef) -> Self {
61        self.shader = Some(value);
62        self
63    }
64
65    pub fn font(mut self, value: impl Into<Cow<'static, str>>) -> Self {
66        self.font = value.into();
67        self
68    }
69
70    pub fn size(mut self, value: f32) -> Self {
71        self.size = value;
72        self
73    }
74
75    pub fn text(mut self, value: impl Into<Cow<'static, str>>) -> Self {
76        self.text = value.into();
77        self
78    }
79
80    pub fn tint(mut self, value: Rgba<f32>) -> Self {
81        self.tint = value;
82        self
83    }
84
85    pub fn horizontal_align(mut self, value: HorizontalAlign) -> Self {
86        self.horizontal_align = value;
87        self
88    }
89
90    pub fn vertical_align(mut self, value: VerticalAlign) -> Self {
91        self.vertical_align = value;
92        self
93    }
94
95    pub fn width(mut self, value: f32) -> Self {
96        self.width = Some(value);
97        self
98    }
99
100    pub fn height(mut self, value: f32) -> Self {
101        self.height = Some(value);
102        self
103    }
104
105    pub fn uniform(mut self, key: Cow<'static, str>, value: GlowUniformValue) -> Self {
106        self.uniforms.insert(key, value);
107        self
108    }
109
110    pub fn transform(mut self, value: Transform<f32, f32, f32>) -> Self {
111        self.transform = value;
112        self
113    }
114
115    pub fn position(mut self, value: Vec2<f32>) -> Self {
116        self.transform.position = value.into();
117        self
118    }
119
120    pub fn orientation(mut self, value: Quaternion<f32>) -> Self {
121        self.transform.orientation = value;
122        self
123    }
124
125    pub fn rotation(mut self, angle_radians: f32) -> Self {
126        self.transform.orientation = Quaternion::rotation_z(angle_radians);
127        self
128    }
129
130    pub fn scale(mut self, value: Vec2<f32>) -> Self {
131        self.transform.scale = Vec3::new(value.x, value.y, 1.0);
132        self
133    }
134
135    pub fn blending(mut self, value: GlowBlending) -> Self {
136        self.blending = Some(value);
137        self
138    }
139
140    pub fn screen_space(mut self, value: bool) -> Self {
141        self.screen_space = value;
142        self
143    }
144
145    pub fn get_local_space_bounding_box(
146        &self,
147        context: &DrawContext,
148        compact: bool,
149    ) -> Option<Rect<f32, f32>> {
150        let layout = self.make_text_layout(context)?;
151        let aabb = TextRenderer::measure(&layout, context.fonts.values(), compact);
152        if aabb.iter().all(|v| v.is_finite()) {
153            Some(Rect::new(
154                aabb[0],
155                aabb[1],
156                aabb[2] - aabb[0],
157                aabb[3] - aabb[1],
158            ))
159        } else {
160            None
161        }
162    }
163
164    fn make_text_layout(&self, context: &DrawContext) -> Option<Layout<Rgba<f32>>> {
165        if let Some(index) = context.fonts.index_of(&self.font) {
166            let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
167            layout.reset(&LayoutSettings {
168                x: 0.0,
169                y: 0.0,
170                max_width: self.width,
171                max_height: self.height,
172                horizontal_align: self.horizontal_align,
173                vertical_align: self.vertical_align,
174                ..Default::default()
175            });
176            layout.append(
177                context.fonts.values(),
178                &TextStyle {
179                    text: &self.text,
180                    px: self.size,
181                    font_index: index,
182                    user_data: self.tint,
183                },
184            );
185            Some(layout)
186        } else {
187            None
188        }
189    }
190}
191
192impl Drawable for Text {
193    fn draw(&self, context: &mut DrawContext, graphics: &mut Graphics<Vertex>) {
194        if let Some(layout) = self.make_text_layout(context) {
195            context
196                .text_renderer
197                .include(context.fonts.values(), &layout);
198            graphics.stream.batch_optimized(GraphicsBatch {
199                shader: context.shader(self.shader.as_ref()),
200                uniforms: self
201                    .uniforms
202                    .iter()
203                    .map(|(k, v)| (k.clone(), v.to_owned()))
204                    .chain(std::iter::once((
205                        "u_projection_view".into(),
206                        GlowUniformValue::M4(
207                            if self.screen_space {
208                                graphics.main_camera.screen_matrix()
209                            } else {
210                                graphics.main_camera.world_matrix()
211                            }
212                            .into_col_array(),
213                        ),
214                    )))
215                    .chain(std::iter::once(("u_image".into(), GlowUniformValue::I1(0))))
216                    .collect(),
217                textures: if let Some(texture) = context.fonts_texture() {
218                    vec![(texture, GlowTextureFiltering::Linear)]
219                } else {
220                    vec![]
221                },
222                blending: GlowBlending::Alpha,
223                scissor: Default::default(),
224                wireframe: context.wireframe,
225            });
226            let transform = context.top_transform() * transform_to_matrix(self.transform);
227            graphics.stream.transformed(
228                |stream| {
229                    context.text_renderer.render_to_stream(stream);
230                },
231                |vertex| {
232                    let point = transform.mul_point(Vec2::from(vertex.position));
233                    vertex.position[0] = point.x;
234                    vertex.position[1] = point.y;
235                },
236            );
237        }
238    }
239}