1use glam::{Vec2, Vec3, Vec4, Mat4};
11
12use super::ui_layer::{UiLayer, UiDrawCommand, TextAlign, BorderStyle};
13use crate::glyph::batch::GlyphInstance;
14use crate::glyph::atlas::FontAtlas;
15
16pub struct UiLayerRenderer {
23 instances: Vec<GlyphInstance>,
25 rect_instances: Vec<RectInstance>,
27}
28
29#[repr(C)]
31#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
32pub struct RectInstance {
33 pub position: [f32; 2],
34 pub size: [f32; 2],
35 pub color: [f32; 4],
36}
37
38impl UiLayerRenderer {
39 pub fn new() -> Self {
40 Self {
41 instances: Vec::with_capacity(2048),
42 rect_instances: Vec::with_capacity(256),
43 }
44 }
45
46 pub fn begin(&mut self) {
48 self.instances.clear();
49 self.rect_instances.clear();
50 }
51
52 pub fn build_instances(&mut self, ui: &UiLayer, atlas: &FontAtlas) {
54 self.begin();
55
56 if !ui.enabled {
57 return;
58 }
59
60 for cmd in ui.draw_queue() {
61 match cmd {
62 UiDrawCommand::Text { text, x, y, scale, color, emission, alignment } => {
63 self.build_text_instances(
64 text, *x, *y, *scale, *color, *emission, *alignment, ui, atlas,
65 );
66 }
67 UiDrawCommand::Rect { x, y, w, h, color, filled } => {
68 if *filled {
69 self.rect_instances.push(RectInstance {
70 position: [*x, *y],
71 size: [*w, *h],
72 color: color.to_array(),
73 });
74 } else {
75 self.build_rect_outline(*x, *y, *w, *h, *color, ui, atlas);
76 }
77 }
78 UiDrawCommand::Panel { x, y, w, h, border, fill_color, border_color } => {
79 self.build_panel(*x, *y, *w, *h, *border, *fill_color, *border_color, ui, atlas);
80 }
81 UiDrawCommand::Bar { x, y, w, h, fill_pct, fill_color, bg_color, ghost_pct, ghost_color } => {
82 self.build_bar(*x, *y, *w, *h, *fill_pct, *fill_color, *bg_color, *ghost_pct, *ghost_color, ui, atlas);
83 }
84 UiDrawCommand::Sprite { lines, x, y, color } => {
85 self.build_sprite(lines, *x, *y, *color, ui, atlas);
86 }
87 }
88 }
89 }
90
91 pub fn glyph_instances(&self) -> &[GlyphInstance] {
93 &self.instances
94 }
95
96 pub fn glyph_bytes(&self) -> &[u8] {
98 bytemuck::cast_slice(&self.instances)
99 }
100
101 pub fn rect_instances(&self) -> &[RectInstance] {
103 &self.rect_instances
104 }
105
106 pub fn rect_bytes(&self) -> &[u8] {
108 bytemuck::cast_slice(&self.rect_instances)
109 }
110
111 pub fn glyph_count(&self) -> usize {
113 self.instances.len()
114 }
115
116 pub fn rect_count(&self) -> usize {
118 self.rect_instances.len()
119 }
120
121 fn build_text_instances(
124 &mut self,
125 text: &str,
126 x: f32,
127 y: f32,
128 scale: f32,
129 color: Vec4,
130 emission: f32,
131 alignment: TextAlign,
132 ui: &UiLayer,
133 atlas: &FontAtlas,
134 ) {
135 let char_w = ui.char_width * scale;
136 let char_h = ui.char_height * scale;
137 let text_width = text.chars().count() as f32 * char_w;
138
139 let start_x = match alignment {
140 TextAlign::Left => x,
141 TextAlign::Center => x - text_width * 0.5,
142 TextAlign::Right => x - text_width,
143 };
144
145 for (i, ch) in text.chars().enumerate() {
146 if ch == ' ' {
147 continue;
148 }
149 let uv = atlas.uv_for(ch);
150 let px = start_x + i as f32 * char_w + char_w * 0.5;
151 let py = y + char_h * 0.5;
152
153 self.instances.push(GlyphInstance {
154 position: [px, py, 0.0],
155 scale: [char_w, char_h],
156 rotation: 0.0,
157 color: color.to_array(),
158 emission,
159 glow_color: [color.x, color.y, color.z],
160 glow_radius: 0.0,
161 uv_offset: uv.offset(),
162 uv_size: uv.size(),
163 _pad: [0.0; 2],
164 });
165 }
166 }
167
168 fn build_rect_outline(
169 &mut self,
170 x: f32,
171 y: f32,
172 w: f32,
173 h: f32,
174 color: Vec4,
175 ui: &UiLayer,
176 atlas: &FontAtlas,
177 ) {
178 let char_w = ui.char_width;
180 let char_h = ui.char_height;
181 let cols = (w / char_w).ceil() as usize;
182 let rows = (h / char_h).ceil() as usize;
183
184 if cols < 2 || rows < 2 {
185 return;
186 }
187
188 let border = BorderStyle::Single;
189 let chars = border.chars();
190
191 self.push_char(x, y, chars[0], color, atlas, ui);
193 for c in 1..cols - 1 {
194 self.push_char(x + c as f32 * char_w, y, chars[1], color, atlas, ui);
195 }
196 self.push_char(x + (cols - 1) as f32 * char_w, y, chars[2], color, atlas, ui);
197
198 for r in 1..rows - 1 {
200 let ry = y + r as f32 * char_h;
201 self.push_char(x, ry, chars[3], color, atlas, ui);
202 self.push_char(x + (cols - 1) as f32 * char_w, ry, chars[4], color, atlas, ui);
203 }
204
205 let by = y + (rows - 1) as f32 * char_h;
207 self.push_char(x, by, chars[5], color, atlas, ui);
208 for c in 1..cols - 1 {
209 self.push_char(x + c as f32 * char_w, by, chars[6], color, atlas, ui);
210 }
211 self.push_char(x + (cols - 1) as f32 * char_w, by, chars[7], color, atlas, ui);
212 }
213
214 fn build_panel(
215 &mut self,
216 x: f32,
217 y: f32,
218 w: f32,
219 h: f32,
220 border: BorderStyle,
221 fill_color: Vec4,
222 border_color: Vec4,
223 ui: &UiLayer,
224 atlas: &FontAtlas,
225 ) {
226 let char_w = ui.char_width;
227 let char_h = ui.char_height;
228 let cols = (w / char_w).ceil() as usize;
229 let rows = (h / char_h).ceil() as usize;
230
231 if cols < 2 || rows < 2 {
232 return;
233 }
234
235 if fill_color.w > 0.0 {
237 self.rect_instances.push(RectInstance {
238 position: [x + char_w, y + char_h],
239 size: [w - char_w * 2.0, h - char_h * 2.0],
240 color: fill_color.to_array(),
241 });
242 }
243
244 let chars = border.chars();
245
246 self.push_char(x, y, chars[0], border_color, atlas, ui);
248 for c in 1..cols - 1 {
249 self.push_char(x + c as f32 * char_w, y, chars[1], border_color, atlas, ui);
250 }
251 self.push_char(x + (cols - 1) as f32 * char_w, y, chars[2], border_color, atlas, ui);
252
253 for r in 1..rows - 1 {
255 let ry = y + r as f32 * char_h;
256 self.push_char(x, ry, chars[3], border_color, atlas, ui);
257 self.push_char(x + (cols - 1) as f32 * char_w, ry, chars[4], border_color, atlas, ui);
258 }
259
260 let by = y + (rows - 1) as f32 * char_h;
262 self.push_char(x, by, chars[5], border_color, atlas, ui);
263 for c in 1..cols - 1 {
264 self.push_char(x + c as f32 * char_w, by, chars[6], border_color, atlas, ui);
265 }
266 self.push_char(x + (cols - 1) as f32 * char_w, by, chars[7], border_color, atlas, ui);
267 }
268
269 fn build_bar(
270 &mut self,
271 x: f32,
272 y: f32,
273 w: f32,
274 h: f32,
275 fill_pct: f32,
276 fill_color: Vec4,
277 bg_color: Vec4,
278 ghost_pct: Option<f32>,
279 ghost_color: Vec4,
280 ui: &UiLayer,
281 atlas: &FontAtlas,
282 ) {
283 let char_w = ui.char_width;
284 let total_chars = (w / char_w).floor() as usize;
285 if total_chars == 0 {
286 return;
287 }
288
289 let filled_chars = (fill_pct * total_chars as f32).round() as usize;
290 let ghost_chars = ghost_pct
291 .map(|g| (g * total_chars as f32).round() as usize)
292 .unwrap_or(0);
293
294 for i in 0..total_chars {
295 let cx = x + i as f32 * char_w;
296 let (ch, color) = if i < filled_chars {
297 ('█', fill_color)
298 } else if i < ghost_chars {
299 ('█', ghost_color)
300 } else {
301 ('░', bg_color)
302 };
303 self.push_char(cx, y, ch, color, atlas, ui);
304 }
305 }
306
307 fn build_sprite(
308 &mut self,
309 lines: &[String],
310 x: f32,
311 y: f32,
312 color: Vec4,
313 ui: &UiLayer,
314 atlas: &FontAtlas,
315 ) {
316 let char_w = ui.char_width;
317 let char_h = ui.char_height;
318
319 for (row, line) in lines.iter().enumerate() {
320 let ly = y + row as f32 * char_h;
321 for (col, ch) in line.chars().enumerate() {
322 if ch == ' ' {
323 continue;
324 }
325 let cx = x + col as f32 * char_w;
326 self.push_char(cx, ly, ch, color, atlas, ui);
327 }
328 }
329 }
330
331 fn push_char(
333 &mut self,
334 x: f32,
335 y: f32,
336 ch: char,
337 color: Vec4,
338 atlas: &FontAtlas,
339 ui: &UiLayer,
340 ) {
341 let uv = atlas.uv_for(ch);
342 let char_w = ui.char_width;
343 let char_h = ui.char_height;
344
345 self.instances.push(GlyphInstance {
346 position: [x + char_w * 0.5, y + char_h * 0.5, 0.0],
347 scale: [char_w, char_h],
348 rotation: 0.0,
349 color: color.to_array(),
350 emission: 0.0,
351 glow_color: [0.0, 0.0, 0.0],
352 glow_radius: 0.0,
353 uv_offset: uv.offset(),
354 uv_size: uv.size(),
355 _pad: [0.0; 2],
356 });
357 }
358}
359
360impl Default for UiLayerRenderer {
361 fn default() -> Self {
362 Self::new()
363 }
364}
365
366pub const UI_VERT_SRC: &str = r#"
371#version 330 core
372
373layout(location = 0) in vec2 v_pos;
374layout(location = 1) in vec2 v_uv;
375
376layout(location = 2) in vec3 i_position;
377layout(location = 3) in vec2 i_scale;
378layout(location = 4) in float i_rotation;
379layout(location = 5) in vec4 i_color;
380layout(location = 6) in float i_emission;
381layout(location = 7) in vec3 i_glow_color;
382layout(location = 8) in float i_glow_radius;
383layout(location = 9) in vec2 i_uv_offset;
384layout(location = 10) in vec2 i_uv_size;
385
386uniform mat4 u_view_proj;
387
388out vec2 f_uv;
389out vec4 f_color;
390out float f_emission;
391
392void main() {
393 float c = cos(i_rotation);
394 float s = sin(i_rotation);
395 vec2 rotated = vec2(
396 v_pos.x * c - v_pos.y * s,
397 v_pos.x * s + v_pos.y * c
398 ) * i_scale;
399
400 gl_Position = u_view_proj * vec4(i_position + vec3(rotated, 0.0), 1.0);
401
402 f_uv = i_uv_offset + v_uv * i_uv_size;
403 f_color = i_color;
404 f_emission = i_emission;
405}
406"#;
407
408pub const UI_FRAG_SRC: &str = r#"
411#version 330 core
412
413in vec2 f_uv;
414in vec4 f_color;
415in float f_emission;
416
417uniform sampler2D u_atlas;
418
419layout(location = 0) out vec4 o_color;
420
421void main() {
422 float alpha = texture(u_atlas, f_uv).r;
423 if (alpha < 0.05) discard;
424 o_color = vec4(f_color.rgb, alpha * f_color.a);
425}
426"#;
427
428pub const RECT_VERT_SRC: &str = r#"
430#version 330 core
431
432layout(location = 0) in vec2 v_pos; // [0,1] unit quad
433
434layout(location = 1) in vec2 i_position; // top-left corner
435layout(location = 2) in vec2 i_size; // width, height
436layout(location = 3) in vec4 i_color;
437
438uniform mat4 u_projection;
439
440out vec4 f_color;
441
442void main() {
443 vec2 world = i_position + v_pos * i_size;
444 gl_Position = u_projection * vec4(world, 0.0, 1.0);
445 f_color = i_color;
446}
447"#;
448
449pub const RECT_FRAG_SRC: &str = r#"
451#version 330 core
452
453in vec4 f_color;
454
455layout(location = 0) out vec4 o_color;
456
457void main() {
458 o_color = f_color;
459}
460"#;
461
462#[cfg(test)]
465mod tests {
466 use super::*;
467 use crate::glyph::atlas::FontAtlas;
468
469 #[test]
470 fn renderer_builds_text_instances() {
471 let mut renderer = UiLayerRenderer::new();
472 let mut ui = UiLayer::new(1280.0, 800.0);
473 ui.draw_text(10.0, 20.0, "Hi", 1.0, Vec4::ONE);
474
475 assert_eq!(renderer.glyph_count(), 0);
478 renderer.begin();
479 assert_eq!(renderer.glyph_count(), 0);
480 }
481
482 #[test]
483 fn rect_instance_size() {
484 assert_eq!(
485 std::mem::size_of::<RectInstance>(),
486 4 * 8, );
488 }
489}