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}