1use glam::{Vec2, Vec3, Vec4, Mat4};
11
12use super::ui_layer::{UiLayer, UiDrawCommand, UiParticle, UiPass, TextAlign, BorderStyle};
13use crate::glyph::batch::GlyphInstance;
14use crate::glyph::atlas::FontAtlas;
15
16pub struct UiLayerRenderer {
23 instances: Vec<GlyphInstance>,
26 hud: Vec<GlyphInstance>,
28 world: Vec<GlyphInstance>,
31 rect_instances: Vec<RectInstance>,
33}
34
35#[repr(C)]
37#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
38pub struct RectInstance {
39 pub position: [f32; 2],
40 pub size: [f32; 2],
41 pub color: [f32; 4],
42}
43
44impl UiLayerRenderer {
45 pub fn new() -> Self {
46 Self {
47 instances: Vec::with_capacity(2048),
48 hud: Vec::with_capacity(2048),
49 world: Vec::with_capacity(1 << 16),
50 rect_instances: Vec::with_capacity(256),
51 }
52 }
53
54 pub fn begin(&mut self) {
56 self.instances.clear();
57 self.hud.clear();
58 self.world.clear();
59 self.rect_instances.clear();
60 }
61
62 pub fn build_instances(&mut self, ui: &UiLayer, atlas: &FontAtlas) {
64 self.begin();
65
66 if !ui.enabled {
67 return;
68 }
69
70 for (i, cmd) in ui.draw_queue().iter().enumerate() {
71 match cmd {
72 UiDrawCommand::Text { text, x, y, scale, color, emission, alignment } => {
73 self.build_text_instances(
74 text, *x, *y, *scale, *color, *emission, *alignment, ui, atlas,
75 );
76 }
77 UiDrawCommand::Rect { x, y, w, h, color, filled } => {
78 if *filled {
79 self.push_filled_rect(*x, *y, *w, *h, *color, atlas);
80 } else {
81 self.build_rect_outline(*x, *y, *w, *h, *color, ui, atlas);
82 }
83 }
84 UiDrawCommand::Panel { x, y, w, h, border, fill_color, border_color } => {
85 let split = !ui.pass_forced(i);
90 let fill = if split {
91 Vec4::new(fill_color.x, fill_color.y, fill_color.z, 0.0)
92 } else {
93 *fill_color
94 };
95 if split && fill_color.w > 0.0 {
96 self.build_panel(*x, *y, *w, *h, *border, *fill_color, Vec4::ZERO, ui, atlas);
97 self.world.extend(self.instances.drain(..));
98 }
99 self.build_panel(*x, *y, *w, *h, *border, fill, *border_color, ui, atlas);
100 }
101 UiDrawCommand::Bar { x, y, w, h, fill_pct, fill_color, bg_color, ghost_pct, ghost_color } => {
102 self.build_bar(*x, *y, *w, *h, *fill_pct, *fill_color, *bg_color, *ghost_pct, *ghost_color, ui, atlas);
103 }
104 UiDrawCommand::Sprite { lines, x, y, color } => {
105 self.build_sprite(lines, *x, *y, *color, ui, atlas);
106 }
107 UiDrawCommand::Particles(particles) => {
108 self.build_particle_instances(particles, 0.0, 0.0, atlas);
109 }
110 UiDrawCommand::SharedParticles { particles, dx, dy } => {
111 self.build_particle_instances(particles, *dx, *dy, atlas);
112 }
113 }
114 match ui.pass_of(i) {
116 UiPass::World => self.world.extend(self.instances.drain(..)),
117 UiPass::Hud => self.hud.extend(self.instances.drain(..)),
118 }
119 }
120 }
121
122 pub fn glyph_instances(&self) -> &[GlyphInstance] {
124 &self.hud
125 }
126
127 pub fn glyph_bytes(&self) -> &[u8] {
129 bytemuck::cast_slice(&self.hud)
130 }
131
132 pub fn world_instances(&self) -> &[GlyphInstance] {
134 &self.world
135 }
136
137 pub fn world_bytes(&self) -> &[u8] {
139 bytemuck::cast_slice(&self.world)
140 }
141
142 pub fn world_count(&self) -> usize {
144 self.world.len()
145 }
146
147 pub fn rect_instances(&self) -> &[RectInstance] {
149 &self.rect_instances
150 }
151
152 pub fn rect_bytes(&self) -> &[u8] {
154 bytemuck::cast_slice(&self.rect_instances)
155 }
156
157 pub fn glyph_count(&self) -> usize {
159 self.hud.len()
160 }
161
162 pub fn rect_count(&self) -> usize {
164 self.rect_instances.len()
165 }
166
167 fn build_text_instances(
170 &mut self,
171 text: &str,
172 x: f32,
173 y: f32,
174 scale: f32,
175 color: Vec4,
176 emission: f32,
177 alignment: TextAlign,
178 ui: &UiLayer,
179 atlas: &FontAtlas,
180 ) {
181 let char_w = ui.char_width * scale;
182 let char_h = ui.char_height * scale;
183 let text_width = text.chars().count() as f32 * char_w;
184
185 let start_x = match alignment {
186 TextAlign::Left => x,
187 TextAlign::Center => x - text_width * 0.5,
188 TextAlign::Right => x - text_width,
189 };
190
191 for (i, ch) in text.chars().enumerate() {
192 if ch == ' ' {
193 continue;
194 }
195 let uv = atlas.uv_for(ch);
196 let px = start_x + i as f32 * char_w + char_w * 0.5;
197 let py = y + char_h * 0.5;
198
199 self.instances.push(GlyphInstance {
200 position: [px, py, 0.0],
201 scale: [char_w, char_h],
202 rotation: 0.0,
203 color: color.to_array(),
204 emission,
205 glow_color: [color.x, color.y, color.z],
206 glow_radius: 0.0,
207 uv_offset: uv.offset(),
208 uv_size: uv.size(),
209 _pad: [0.0; 2],
210 });
211 }
212 }
213
214
215 fn build_particle_instances(
220 &mut self,
221 particles: &[UiParticle],
222 dx: f32,
223 dy: f32,
224 atlas: &FontAtlas,
225 ) {
226 self.instances.reserve(particles.len());
227 for p in particles {
228 if p.color.w <= 0.0 || p.w <= 0.0 || p.h <= 0.0 {
229 continue;
230 }
231 if !(p.x.is_finite() && p.y.is_finite() && p.rotation.is_finite()) {
232 continue;
233 }
234 let uv = atlas.uv_for(p.ch);
235 self.instances.push(GlyphInstance {
236 position: [p.x + dx, p.y + dy, 0.0],
237 scale: [p.w, p.h],
238 rotation: p.rotation,
239 color: p.color.to_array(),
240 emission: p.emission,
241 glow_color: [p.color.x, p.color.y, p.color.z],
242 glow_radius: p.glow,
243 uv_offset: uv.offset(),
244 uv_size: uv.size(),
245 _pad: [0.0; 2],
246 });
247 }
248 }
249
250 fn push_filled_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Vec4, atlas: &FontAtlas) {
257 if w <= 0.0 || h <= 0.0 || color.w <= 0.0 {
258 return;
259 }
260 let uv = atlas.uv_for('\u{2588}'); let cu = (uv.u0 + uv.u1) * 0.5;
267 let cv = (uv.v0 + uv.v1) * 0.5;
268 self.instances.push(GlyphInstance {
269 position: [x + w * 0.5, y + h * 0.5, 0.0],
270 scale: [w, h],
271 rotation: 0.0,
272 color: color.to_array(),
273 emission: 0.0,
274 glow_color: [color.x, color.y, color.z],
275 glow_radius: 0.0,
276 uv_offset: [cu, cv],
277 uv_size: [0.0, 0.0],
278 _pad: [1.0, 0.0],
281 });
282 }
283
284 fn build_rect_outline(
285 &mut self,
286 x: f32,
287 y: f32,
288 w: f32,
289 h: f32,
290 color: Vec4,
291 ui: &UiLayer,
292 atlas: &FontAtlas,
293 ) {
294 let char_w = ui.char_width;
296 let char_h = ui.char_height;
297 let cols = (w / char_w).ceil() as usize;
298 let rows = (h / char_h).ceil() as usize;
299
300 if cols < 2 || rows < 2 {
301 return;
302 }
303
304 let border = BorderStyle::Single;
305 let chars = border.chars();
306
307 self.push_char(x, y, chars[0], color, atlas, ui);
309 for c in 1..cols - 1 {
310 self.push_char(x + c as f32 * char_w, y, chars[1], color, atlas, ui);
311 }
312 self.push_char(x + (cols - 1) as f32 * char_w, y, chars[2], color, atlas, ui);
313
314 for r in 1..rows - 1 {
316 let ry = y + r as f32 * char_h;
317 self.push_char(x, ry, chars[3], color, atlas, ui);
318 self.push_char(x + (cols - 1) as f32 * char_w, ry, chars[4], color, atlas, ui);
319 }
320
321 let by = y + (rows - 1) as f32 * char_h;
323 self.push_char(x, by, chars[5], color, atlas, ui);
324 for c in 1..cols - 1 {
325 self.push_char(x + c as f32 * char_w, by, chars[6], color, atlas, ui);
326 }
327 self.push_char(x + (cols - 1) as f32 * char_w, by, chars[7], color, atlas, ui);
328 }
329
330 fn build_panel(
331 &mut self,
332 x: f32,
333 y: f32,
334 w: f32,
335 h: f32,
336 border: BorderStyle,
337 fill_color: Vec4,
338 border_color: Vec4,
339 ui: &UiLayer,
340 atlas: &FontAtlas,
341 ) {
342 let char_w = ui.char_width;
343 let char_h = ui.char_height;
344 let cols = (w / char_w).ceil() as usize;
345 let rows = (h / char_h).ceil() as usize;
346
347 if cols < 2 || rows < 2 {
348 return;
349 }
350
351 if fill_color.w > 0.0 {
353 self.push_filled_rect(
354 x + char_w,
355 y + char_h,
356 w - char_w * 2.0,
357 h - char_h * 2.0,
358 fill_color,
359 atlas,
360 );
361 }
362
363 let chars = border.chars();
364
365 self.push_char(x, y, chars[0], border_color, atlas, ui);
367 for c in 1..cols - 1 {
368 self.push_char(x + c as f32 * char_w, y, chars[1], border_color, atlas, ui);
369 }
370 self.push_char(x + (cols - 1) as f32 * char_w, y, chars[2], border_color, atlas, ui);
371
372 for r in 1..rows - 1 {
374 let ry = y + r as f32 * char_h;
375 self.push_char(x, ry, chars[3], border_color, atlas, ui);
376 self.push_char(x + (cols - 1) as f32 * char_w, ry, chars[4], border_color, atlas, ui);
377 }
378
379 let by = y + (rows - 1) as f32 * char_h;
381 self.push_char(x, by, chars[5], border_color, atlas, ui);
382 for c in 1..cols - 1 {
383 self.push_char(x + c as f32 * char_w, by, chars[6], border_color, atlas, ui);
384 }
385 self.push_char(x + (cols - 1) as f32 * char_w, by, chars[7], border_color, atlas, ui);
386 }
387
388 fn build_bar(
389 &mut self,
390 x: f32,
391 y: f32,
392 w: f32,
393 h: f32,
394 fill_pct: f32,
395 fill_color: Vec4,
396 bg_color: Vec4,
397 ghost_pct: Option<f32>,
398 ghost_color: Vec4,
399 ui: &UiLayer,
400 atlas: &FontAtlas,
401 ) {
402 let char_w = ui.char_width;
403 let total_chars = (w / char_w).floor() as usize;
404 if total_chars == 0 {
405 return;
406 }
407
408 let filled_chars = (fill_pct * total_chars as f32).round() as usize;
409 let ghost_chars = ghost_pct
410 .map(|g| (g * total_chars as f32).round() as usize)
411 .unwrap_or(0);
412
413 for i in 0..total_chars {
414 let cx = x + i as f32 * char_w;
415 let (ch, color) = if i < filled_chars {
416 ('█', fill_color)
417 } else if i < ghost_chars {
418 ('█', ghost_color)
419 } else {
420 ('░', bg_color)
421 };
422 self.push_char(cx, y, ch, color, atlas, ui);
423 }
424 }
425
426 fn build_sprite(
427 &mut self,
428 lines: &[String],
429 x: f32,
430 y: f32,
431 color: Vec4,
432 ui: &UiLayer,
433 atlas: &FontAtlas,
434 ) {
435 let char_w = ui.char_width;
436 let char_h = ui.char_height;
437
438 for (row, line) in lines.iter().enumerate() {
439 let ly = y + row as f32 * char_h;
440 for (col, ch) in line.chars().enumerate() {
441 if ch == ' ' {
442 continue;
443 }
444 let cx = x + col as f32 * char_w;
445 self.push_char(cx, ly, ch, color, atlas, ui);
446 }
447 }
448 }
449
450 fn push_char(
452 &mut self,
453 x: f32,
454 y: f32,
455 ch: char,
456 color: Vec4,
457 atlas: &FontAtlas,
458 ui: &UiLayer,
459 ) {
460 let uv = atlas.uv_for(ch);
461 let char_w = ui.char_width;
462 let char_h = ui.char_height;
463
464 self.instances.push(GlyphInstance {
465 position: [x + char_w * 0.5, y + char_h * 0.5, 0.0],
466 scale: [char_w, char_h],
467 rotation: 0.0,
468 color: color.to_array(),
469 emission: 0.0,
470 glow_color: [0.0, 0.0, 0.0],
471 glow_radius: 0.0,
472 uv_offset: uv.offset(),
473 uv_size: uv.size(),
474 _pad: [0.0; 2],
475 });
476 }
477}
478
479impl Default for UiLayerRenderer {
480 fn default() -> Self {
481 Self::new()
482 }
483}
484
485pub const UI_VERT_SRC: &str = r#"
490#version 330 core
491
492layout(location = 0) in vec2 v_pos;
493layout(location = 1) in vec2 v_uv;
494
495layout(location = 2) in vec3 i_position;
496layout(location = 3) in vec2 i_scale;
497layout(location = 4) in float i_rotation;
498layout(location = 5) in vec4 i_color;
499layout(location = 6) in float i_emission;
500layout(location = 7) in vec3 i_glow_color;
501layout(location = 8) in float i_glow_radius;
502layout(location = 9) in vec2 i_uv_offset;
503layout(location = 10) in vec2 i_uv_size;
504
505uniform mat4 u_view_proj;
506
507out vec2 f_uv;
508out vec4 f_color;
509out float f_emission;
510
511void main() {
512 float c = cos(i_rotation);
513 float s = sin(i_rotation);
514 vec2 rotated = vec2(
515 v_pos.x * c - v_pos.y * s,
516 v_pos.x * s + v_pos.y * c
517 ) * i_scale;
518
519 gl_Position = u_view_proj * vec4(i_position + vec3(rotated, 0.0), 1.0);
520
521 f_uv = i_uv_offset + v_uv * i_uv_size;
522 f_color = i_color;
523 f_emission = i_emission;
524}
525"#;
526
527pub const UI_FRAG_SRC: &str = r#"
530#version 330 core
531
532in vec2 f_uv;
533in vec4 f_color;
534in float f_emission;
535
536uniform sampler2D u_atlas;
537
538layout(location = 0) out vec4 o_color;
539
540void main() {
541 float alpha = texture(u_atlas, f_uv).r;
542 if (alpha < 0.05) discard;
543 o_color = vec4(f_color.rgb, alpha * f_color.a);
544}
545"#;
546
547pub const RECT_VERT_SRC: &str = r#"
549#version 330 core
550
551layout(location = 0) in vec2 v_pos; // [0,1] unit quad
552
553layout(location = 1) in vec2 i_position; // top-left corner
554layout(location = 2) in vec2 i_size; // width, height
555layout(location = 3) in vec4 i_color;
556
557uniform mat4 u_projection;
558
559out vec4 f_color;
560
561void main() {
562 vec2 world = i_position + v_pos * i_size;
563 gl_Position = u_projection * vec4(world, 0.0, 1.0);
564 f_color = i_color;
565}
566"#;
567
568pub const RECT_FRAG_SRC: &str = r#"
570#version 330 core
571
572in vec4 f_color;
573
574layout(location = 0) out vec4 o_color;
575
576void main() {
577 o_color = f_color;
578}
579"#;
580
581#[cfg(test)]
584mod tests {
585 use super::*;
586 use crate::glyph::atlas::FontAtlas;
587
588 #[test]
589 fn renderer_builds_text_instances() {
590 let mut renderer = UiLayerRenderer::new();
591 let mut ui = UiLayer::new(1280.0, 800.0);
592 ui.draw_text(10.0, 20.0, "Hi", 1.0, Vec4::ONE);
593
594 assert_eq!(renderer.glyph_count(), 0);
597 renderer.begin();
598 assert_eq!(renderer.glyph_count(), 0);
599 }
600
601 #[test]
602 fn rect_instance_size() {
603 assert_eq!(
604 std::mem::size_of::<RectInstance>(),
605 4 * 8, );
607 }
608}