1use glam::{Vec2, Vec3, Vec4, Mat4};
17use std::collections::VecDeque;
18
19#[derive(Clone, Debug)]
23pub enum UiDrawCommand {
24 Text {
25 text: String,
26 x: f32,
27 y: f32,
28 scale: f32,
29 color: Vec4,
30 emission: f32,
31 alignment: TextAlign,
32 },
33 Rect {
34 x: f32,
35 y: f32,
36 w: f32,
37 h: f32,
38 color: Vec4,
39 filled: bool,
40 },
41 Panel {
42 x: f32,
43 y: f32,
44 w: f32,
45 h: f32,
46 border: BorderStyle,
47 fill_color: Vec4,
48 border_color: Vec4,
49 },
50 Bar {
51 x: f32,
52 y: f32,
53 w: f32,
54 h: f32,
55 fill_pct: f32,
56 fill_color: Vec4,
57 bg_color: Vec4,
58 ghost_pct: Option<f32>,
59 ghost_color: Vec4,
60 },
61 Sprite {
62 lines: Vec<String>,
63 x: f32,
64 y: f32,
65 color: Vec4,
66 },
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
71pub enum TextAlign {
72 #[default]
73 Left,
74 Center,
75 Right,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum BorderStyle {
81 Single,
83 Double,
85 Rounded,
87 Heavy,
89 Dashed,
91}
92
93impl BorderStyle {
94 pub fn chars(&self) -> [char; 8] {
96 match self {
97 BorderStyle::Single => ['┌', '─', '┐', '│', '│', '└', '─', '┘'],
98 BorderStyle::Double => ['╔', '═', '╗', '║', '║', '╚', '═', '╝'],
99 BorderStyle::Rounded => ['╭', '─', '╮', '│', '│', '╰', '─', '╯'],
100 BorderStyle::Heavy => ['┏', '━', '┓', '┃', '┃', '┗', '━', '┛'],
101 BorderStyle::Dashed => ['┌', '╌', '┐', '╎', '╎', '└', '╌', '┘'],
102 }
103 }
104}
105
106pub struct UiLayer {
111 pub screen_width: f32,
113 pub screen_height: f32,
114 pub char_width: f32,
116 pub char_height: f32,
117 draw_queue: Vec<UiDrawCommand>,
119 pub enabled: bool,
121}
122
123impl UiLayer {
124 pub fn new(screen_width: f32, screen_height: f32) -> Self {
125 Self {
126 screen_width,
127 screen_height,
128 char_width: 10.0,
129 char_height: 18.0,
130 draw_queue: Vec::with_capacity(256),
131 enabled: true,
132 }
133 }
134
135 pub fn resize(&mut self, width: f32, height: f32) {
137 self.screen_width = width;
138 self.screen_height = height;
139 }
140
141 pub fn set_char_size(&mut self, width: f32, height: f32) {
143 self.char_width = width;
144 self.char_height = height;
145 }
146
147 pub fn begin_frame(&mut self) {
149 self.draw_queue.clear();
150 }
151
152 pub fn projection(&self) -> Mat4 {
155 Mat4::orthographic_rh_gl(
156 0.0,
157 self.screen_width,
158 self.screen_height,
159 0.0,
160 -1.0,
161 1.0,
162 )
163 }
164
165 pub fn draw_queue(&self) -> &[UiDrawCommand] {
167 &self.draw_queue
168 }
169
170 pub fn command_count(&self) -> usize {
172 self.draw_queue.len()
173 }
174
175 pub fn draw_text(&mut self, x: f32, y: f32, text: &str, scale: f32, color: Vec4) {
179 self.draw_queue.push(UiDrawCommand::Text {
180 text: text.to_string(),
181 x, y, scale,
182 color,
183 emission: 0.0,
184 alignment: TextAlign::Left,
185 });
186 }
187
188 pub fn draw_text_glowing(&mut self, x: f32, y: f32, text: &str, scale: f32, color: Vec4, emission: f32) {
190 self.draw_queue.push(UiDrawCommand::Text {
191 text: text.to_string(),
192 x, y, scale,
193 color,
194 emission,
195 alignment: TextAlign::Left,
196 });
197 }
198
199 pub fn draw_text_aligned(&mut self, x: f32, y: f32, text: &str, scale: f32, color: Vec4, align: TextAlign) {
201 self.draw_queue.push(UiDrawCommand::Text {
202 text: text.to_string(),
203 x, y, scale,
204 color,
205 emission: 0.0,
206 alignment: align,
207 });
208 }
209
210 pub fn draw_centered_text(&mut self, y: f32, text: &str, scale: f32, color: Vec4) {
212 self.draw_text_aligned(self.screen_width / 2.0, y, text, scale, color, TextAlign::Center);
213 }
214
215 pub fn draw_wrapped_text(&mut self, x: f32, y: f32, max_width: f32, text: &str, scale: f32, color: Vec4) {
217 let char_w = self.char_width * scale;
218 let max_chars = (max_width / char_w.max(1.0)) as usize;
219 let lines = wrap_text_ui(text, max_chars);
220 let line_h = self.char_height * scale;
221 for (i, line) in lines.iter().enumerate() {
222 self.draw_text(x, y + i as f32 * line_h, line, scale, color);
223 }
224 }
225
226 pub fn measure_text(&self, text: &str, scale: f32) -> (f32, f32) {
228 let lines: Vec<&str> = text.lines().collect();
229 let max_cols = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
230 let width = max_cols as f32 * self.char_width * scale;
231 let height = lines.len() as f32 * self.char_height * scale;
232 (width, height)
233 }
234
235 pub fn draw_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Vec4, filled: bool) {
237 self.draw_queue.push(UiDrawCommand::Rect {
238 x, y, w, h, color, filled,
239 });
240 }
241
242 pub fn draw_panel(
244 &mut self,
245 x: f32,
246 y: f32,
247 w: f32,
248 h: f32,
249 border: BorderStyle,
250 fill_color: Vec4,
251 border_color: Vec4,
252 ) {
253 self.draw_queue.push(UiDrawCommand::Panel {
254 x, y, w, h, border, fill_color, border_color,
255 });
256 }
257
258 pub fn draw_bar(
260 &mut self,
261 x: f32,
262 y: f32,
263 w: f32,
264 h: f32,
265 fill_pct: f32,
266 fill_color: Vec4,
267 bg_color: Vec4,
268 ) {
269 self.draw_queue.push(UiDrawCommand::Bar {
270 x, y, w, h,
271 fill_pct: fill_pct.clamp(0.0, 1.0),
272 fill_color,
273 bg_color,
274 ghost_pct: None,
275 ghost_color: Vec4::ZERO,
276 });
277 }
278
279 pub fn draw_bar_with_ghost(
281 &mut self,
282 x: f32,
283 y: f32,
284 w: f32,
285 h: f32,
286 fill_pct: f32,
287 fill_color: Vec4,
288 bg_color: Vec4,
289 ghost_pct: f32,
290 ghost_color: Vec4,
291 ) {
292 self.draw_queue.push(UiDrawCommand::Bar {
293 x, y, w, h,
294 fill_pct: fill_pct.clamp(0.0, 1.0),
295 fill_color,
296 bg_color,
297 ghost_pct: Some(ghost_pct.clamp(0.0, 1.0)),
298 ghost_color,
299 });
300 }
301
302 pub fn draw_sprite(&mut self, x: f32, y: f32, lines: &[&str], color: Vec4) {
304 self.draw_queue.push(UiDrawCommand::Sprite {
305 lines: lines.iter().map(|s| s.to_string()).collect(),
306 x, y, color,
307 });
308 }
309}
310
311fn wrap_text_ui(text: &str, max_chars: usize) -> Vec<String> {
314 if max_chars == 0 {
315 return vec![text.to_string()];
316 }
317 let mut lines = Vec::new();
318 for paragraph in text.split('\n') {
319 if paragraph.is_empty() {
320 lines.push(String::new());
321 continue;
322 }
323 let words: Vec<&str> = paragraph.split_whitespace().collect();
324 let mut line = String::new();
325 for word in words {
326 if line.is_empty() {
327 if word.len() > max_chars {
328 let mut w = word;
329 while w.len() > max_chars {
330 lines.push(w[..max_chars].to_string());
331 w = &w[max_chars..];
332 }
333 line = w.to_string();
334 } else {
335 line = word.to_string();
336 }
337 } else if line.len() + 1 + word.len() <= max_chars {
338 line.push(' ');
339 line.push_str(word);
340 } else {
341 lines.push(std::mem::take(&mut line));
342 line = word.to_string();
343 }
344 }
345 if !line.is_empty() {
346 lines.push(line);
347 }
348 }
349 if lines.is_empty() {
350 lines.push(String::new());
351 }
352 lines
353}
354
355#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn ui_layer_projection_is_orthographic() {
363 let ui = UiLayer::new(1280.0, 800.0);
364 let proj = ui.projection();
365 let tl = proj * Vec4::new(0.0, 0.0, 0.0, 1.0);
367 assert!((tl.x / tl.w - (-1.0)).abs() < 0.01);
368 assert!((tl.y / tl.w - 1.0).abs() < 0.01);
369 }
370
371 #[test]
372 fn ui_layer_draw_and_clear() {
373 let mut ui = UiLayer::new(1280.0, 800.0);
374 ui.draw_text(0.0, 0.0, "Hello", 1.0, Vec4::ONE);
375 assert_eq!(ui.command_count(), 1);
376 ui.begin_frame();
377 assert_eq!(ui.command_count(), 0);
378 }
379
380 #[test]
381 fn measure_text_single_line() {
382 let ui = UiLayer::new(1280.0, 800.0);
383 let (w, h) = ui.measure_text("Hello", 1.0);
384 assert_eq!(w, 5.0 * ui.char_width);
385 assert_eq!(h, ui.char_height);
386 }
387
388 #[test]
389 fn measure_text_multi_line() {
390 let ui = UiLayer::new(1280.0, 800.0);
391 let (_, h) = ui.measure_text("Line1\nLine2\nLine3", 1.0);
392 assert_eq!(h, 3.0 * ui.char_height);
393 }
394
395 #[test]
396 fn border_style_chars() {
397 let chars = BorderStyle::Single.chars();
398 assert_eq!(chars[0], '┌');
399 assert_eq!(chars[7], '┘');
400 }
401
402 #[test]
403 fn wrap_text_ui_basic() {
404 let lines = wrap_text_ui("Hello world foo bar", 10);
405 for l in &lines {
406 assert!(l.len() <= 10, "Line too long: '{}'", l);
407 }
408 }
409
410 #[test]
411 fn bar_pct_clamped() {
412 let mut ui = UiLayer::new(1280.0, 800.0);
413 ui.draw_bar(0.0, 0.0, 100.0, 10.0, 1.5, Vec4::ONE, Vec4::ZERO);
414 if let UiDrawCommand::Bar { fill_pct, .. } = &ui.draw_queue()[0] {
415 assert_eq!(*fill_pct, 1.0);
416 }
417 }
418}