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::{GraphicsBatch, GraphicsTarget},
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 dyn GraphicsTarget<Vertex>) {
194 if let Some(layout) = self.make_text_layout(context) {
195 let matrix = if self.screen_space {
196 graphics.state().main_camera.screen_matrix()
197 } else {
198 graphics.state().main_camera.world_matrix()
199 }
200 .into_col_array();
201 context
202 .text_renderer
203 .include(context.fonts.values(), &layout);
204 graphics.state_mut().stream.batch_optimized(GraphicsBatch {
205 shader: context.shader(self.shader.as_ref()),
206 uniforms: self
207 .uniforms
208 .iter()
209 .map(|(k, v)| (k.clone(), v.to_owned()))
210 .chain(std::iter::once((
211 "u_projection_view".into(),
212 GlowUniformValue::M4(matrix),
213 )))
214 .chain(std::iter::once(("u_image".into(), GlowUniformValue::I1(0))))
215 .collect(),
216 textures: if let Some(texture) = context.fonts_texture() {
217 vec![(texture, GlowTextureFiltering::Linear)]
218 } else {
219 vec![]
220 },
221 blending: GlowBlending::Alpha,
222 scissor: Default::default(),
223 wireframe: context.wireframe,
224 });
225 let transform = context.top_transform() * transform_to_matrix(self.transform);
226 graphics.state_mut().stream.transformed(
227 |stream| {
228 context.text_renderer.render_to_stream(stream);
229 },
230 |vertex| {
231 let point = transform.mul_point(Vec2::from(vertex.position));
232 vertex.position[0] = point.x;
233 vertex.position[1] = point.y;
234 },
235 );
236 }
237 }
238}