1use glam::{Vec2, Vec4};
7
8use super::ui_layer::{UiLayer, TextAlign, BorderStyle};
9
10pub struct UiColors;
14
15impl UiColors {
16 pub const WHITE: Vec4 = Vec4::new(1.0, 1.0, 1.0, 1.0);
17 pub const GRAY: Vec4 = Vec4::new(0.6, 0.6, 0.6, 1.0);
18 pub const DARK_GRAY: Vec4 = Vec4::new(0.3, 0.3, 0.3, 1.0);
19 pub const RED: Vec4 = Vec4::new(1.0, 0.2, 0.2, 1.0);
20 pub const GREEN: Vec4 = Vec4::new(0.2, 1.0, 0.2, 1.0);
21 pub const BLUE: Vec4 = Vec4::new(0.3, 0.5, 1.0, 1.0);
22 pub const YELLOW: Vec4 = Vec4::new(1.0, 0.9, 0.2, 1.0);
23 pub const CYAN: Vec4 = Vec4::new(0.2, 0.9, 0.9, 1.0);
24 pub const MAGENTA: Vec4 = Vec4::new(0.9, 0.2, 0.9, 1.0);
25 pub const ORANGE: Vec4 = Vec4::new(1.0, 0.6, 0.1, 1.0);
26 pub const GOLD: Vec4 = Vec4::new(1.0, 0.84, 0.0, 1.0);
27 pub const PANEL_BG: Vec4 = Vec4::new(0.05, 0.05, 0.1, 0.85);
28 pub const PANEL_BORDER: Vec4 = Vec4::new(0.4, 0.4, 0.6, 1.0);
29 pub const HP_FILL: Vec4 = Vec4::new(0.8, 0.15, 0.15, 1.0);
30 pub const HP_BG: Vec4 = Vec4::new(0.3, 0.05, 0.05, 1.0);
31 pub const HP_GHOST: Vec4 = Vec4::new(1.0, 0.3, 0.3, 0.5);
32 pub const MP_FILL: Vec4 = Vec4::new(0.2, 0.3, 0.9, 1.0);
33 pub const MP_BG: Vec4 = Vec4::new(0.05, 0.05, 0.3, 1.0);
34 pub const XP_FILL: Vec4 = Vec4::new(0.9, 0.8, 0.1, 1.0);
35 pub const XP_BG: Vec4 = Vec4::new(0.3, 0.25, 0.05, 1.0);
36 pub const STAMINA_FILL: Vec4 = Vec4::new(0.1, 0.8, 0.3, 1.0);
37 pub const STAMINA_BG: Vec4 = Vec4::new(0.05, 0.25, 0.1, 1.0);
38}
39
40pub fn draw_titled_panel(
44 ui: &mut UiLayer,
45 x: f32,
46 y: f32,
47 w: f32,
48 h: f32,
49 title: &str,
50 border: BorderStyle,
51 fill_color: Vec4,
52 border_color: Vec4,
53 title_color: Vec4,
54) {
55 ui.draw_panel(x, y, w, h, border, fill_color, border_color);
56 let title_x = x + w * 0.5;
58 let title_y = y;
59 ui.draw_text_aligned(title_x, title_y, &format!(" {} ", title), 1.0, title_color, TextAlign::Center);
60}
61
62pub fn draw_hp_bar(
64 ui: &mut UiLayer,
65 x: f32,
66 y: f32,
67 w: f32,
68 current: f32,
69 max: f32,
70 ghost_pct: Option<f32>,
71) {
72 let pct = if max > 0.0 { current / max } else { 0.0 };
73 let label = format!("HP {}/{}", current as i32, max as i32);
74 ui.draw_text(x, y, &label, 1.0, UiColors::WHITE);
75 let bar_y = y + ui.char_height;
76 if let Some(ghost) = ghost_pct {
77 ui.draw_bar_with_ghost(
78 x, bar_y, w, ui.char_height,
79 pct, UiColors::HP_FILL, UiColors::HP_BG,
80 ghost, UiColors::HP_GHOST,
81 );
82 } else {
83 ui.draw_bar(x, bar_y, w, ui.char_height, pct, UiColors::HP_FILL, UiColors::HP_BG);
84 }
85}
86
87pub fn draw_mp_bar(
89 ui: &mut UiLayer,
90 x: f32,
91 y: f32,
92 w: f32,
93 current: f32,
94 max: f32,
95) {
96 let pct = if max > 0.0 { current / max } else { 0.0 };
97 let label = format!("MP {}/{}", current as i32, max as i32);
98 ui.draw_text(x, y, &label, 1.0, UiColors::BLUE);
99 let bar_y = y + ui.char_height;
100 ui.draw_bar(x, bar_y, w, ui.char_height, pct, UiColors::MP_FILL, UiColors::MP_BG);
101}
102
103pub fn draw_xp_bar(
105 ui: &mut UiLayer,
106 x: f32,
107 y: f32,
108 w: f32,
109 current: f32,
110 max: f32,
111 level: u32,
112) {
113 let pct = if max > 0.0 { current / max } else { 0.0 };
114 let label = format!("Lv.{} XP {}/{}", level, current as i32, max as i32);
115 ui.draw_text(x, y, &label, 1.0, UiColors::YELLOW);
116 let bar_y = y + ui.char_height;
117 ui.draw_bar(x, bar_y, w, ui.char_height, pct, UiColors::XP_FILL, UiColors::XP_BG);
118}
119
120pub fn draw_stamina_bar(
122 ui: &mut UiLayer,
123 x: f32,
124 y: f32,
125 w: f32,
126 current: f32,
127 max: f32,
128) {
129 let pct = if max > 0.0 { current / max } else { 0.0 };
130 let label = format!("STA {}/{}", current as i32, max as i32);
131 ui.draw_text(x, y, &label, 1.0, UiColors::GREEN);
132 let bar_y = y + ui.char_height;
133 ui.draw_bar(x, bar_y, w, ui.char_height, pct, UiColors::STAMINA_FILL, UiColors::STAMINA_BG);
134}
135
136pub fn draw_stat_line(
138 ui: &mut UiLayer,
139 x: f32,
140 y: f32,
141 label: &str,
142 value: &str,
143 label_color: Vec4,
144 value_color: Vec4,
145) {
146 ui.draw_text(x, y, label, 1.0, label_color);
147 let value_x = x + label.len() as f32 * ui.char_width;
148 ui.draw_text(value_x, y, value, 1.0, value_color);
149}
150
151pub fn draw_stat_line_justified(
153 ui: &mut UiLayer,
154 x: f32,
155 y: f32,
156 width: f32,
157 label: &str,
158 value: &str,
159 label_color: Vec4,
160 value_color: Vec4,
161) {
162 ui.draw_text(x, y, label, 1.0, label_color);
163 let value_w = value.len() as f32 * ui.char_width;
164 let value_x = x + width - value_w;
165 ui.draw_text(value_x, y, value, 1.0, value_color);
166}
167
168pub fn draw_tooltip(
170 ui: &mut UiLayer,
171 x: f32,
172 y: f32,
173 text: &str,
174) {
175 let (tw, th) = ui.measure_text(text, 1.0);
176 let padding = ui.char_width;
177 let panel_w = tw + padding * 2.0;
178 let panel_h = th + padding * 2.0;
179
180 let px = x + ui.char_width;
182 let py = y + ui.char_height;
183
184 let px = px.min(ui.screen_width - panel_w);
186 let py = py.min(ui.screen_height - panel_h);
187
188 ui.draw_panel(
189 px, py, panel_w, panel_h,
190 BorderStyle::Rounded,
191 UiColors::PANEL_BG,
192 UiColors::PANEL_BORDER,
193 );
194 ui.draw_text(px + padding, py + padding, text, 1.0, UiColors::WHITE);
195}
196
197pub fn draw_menu(
199 ui: &mut UiLayer,
200 x: f32,
201 y: f32,
202 options: &[&str],
203 selected: usize,
204 title: Option<&str>,
205) {
206 let max_len = options.iter().map(|o| o.len()).max().unwrap_or(10);
207 let title_len = title.map(|t| t.len()).unwrap_or(0);
208 let width = (max_len.max(title_len) + 6) as f32 * ui.char_width;
209 let height = (options.len() + 2 + if title.is_some() { 2 } else { 0 }) as f32 * ui.char_height;
210
211 if let Some(title) = title {
212 draw_titled_panel(
213 ui, x, y, width, height,
214 title,
215 BorderStyle::Double,
216 UiColors::PANEL_BG,
217 UiColors::PANEL_BORDER,
218 UiColors::GOLD,
219 );
220 } else {
221 ui.draw_panel(x, y, width, height, BorderStyle::Single, UiColors::PANEL_BG, UiColors::PANEL_BORDER);
222 }
223
224 let content_y = y + ui.char_height * (if title.is_some() { 2.0 } else { 1.0 });
225 let content_x = x + ui.char_width * 2.0;
226
227 for (i, option) in options.iter().enumerate() {
228 let oy = content_y + i as f32 * ui.char_height;
229 let (prefix, color) = if i == selected {
230 ("▶ ", UiColors::GOLD)
231 } else {
232 (" ", UiColors::GRAY)
233 };
234 ui.draw_text(content_x, oy, &format!("{}{}", prefix, option), 1.0, color);
235 }
236}
237
238pub fn draw_combat_log(
240 ui: &mut UiLayer,
241 x: f32,
242 y: f32,
243 w: f32,
244 h: f32,
245 lines: &[(&str, Vec4)],
246) {
247 ui.draw_panel(x, y, w, h, BorderStyle::Single, UiColors::PANEL_BG, UiColors::PANEL_BORDER);
248
249 let content_x = x + ui.char_width;
250 let content_y = y + ui.char_height;
251 let max_visible = ((h - ui.char_height * 2.0) / ui.char_height) as usize;
252 let start = if lines.len() > max_visible { lines.len() - max_visible } else { 0 };
253
254 for (i, (text, color)) in lines[start..].iter().enumerate() {
255 let ly = content_y + i as f32 * ui.char_height;
256 ui.draw_text(content_x, ly, text, 1.0, *color);
257 }
258}
259
260pub fn draw_fps_overlay(
262 ui: &mut UiLayer,
263 fps: f32,
264 glyph_count: usize,
265 particle_count: usize,
266 draw_calls: u32,
267) {
268 let x = ui.screen_width - ui.char_width * 25.0;
269 let y = ui.char_height * 0.5;
270 let color = if fps >= 55.0 {
271 UiColors::GREEN
272 } else if fps >= 30.0 {
273 UiColors::YELLOW
274 } else {
275 UiColors::RED
276 };
277
278 ui.draw_text(x, y, &format!("FPS: {:.0}", fps), 1.0, color);
279 ui.draw_text(x, y + ui.char_height, &format!("Glyphs: {}", glyph_count), 1.0, UiColors::GRAY);
280 ui.draw_text(x, y + ui.char_height * 2.0, &format!("Particles: {}", particle_count), 1.0, UiColors::GRAY);
281 ui.draw_text(x, y + ui.char_height * 3.0, &format!("Draws: {}", draw_calls), 1.0, UiColors::GRAY);
282}
283
284pub fn draw_floating_damage(
288 ui: &mut UiLayer,
289 x: f32,
290 y: f32,
291 damage: i32,
292 age: f32,
293 is_crit: bool,
294) {
295 let alpha = (1.0 - age).max(0.0);
296 let rise = age * ui.char_height * 3.0;
297
298 let (text, color) = if is_crit {
299 (format!("★{}★", damage), Vec4::new(1.0, 0.8, 0.0, alpha))
300 } else {
301 (format!("{}", damage), Vec4::new(1.0, 0.3, 0.3, alpha))
302 };
303
304 let scale = if is_crit { 1.5 } else { 1.0 };
305 ui.draw_text_aligned(x, y - rise, &text, scale, color, TextAlign::Center);
306}
307
308pub fn draw_separator(
310 ui: &mut UiLayer,
311 x: f32,
312 y: f32,
313 width: f32,
314 color: Vec4,
315) {
316 let chars = (width / ui.char_width) as usize;
317 let line: String = "─".repeat(chars);
318 ui.draw_text(x, y, &line, 1.0, color);
319}
320
321pub fn draw_notification(
323 ui: &mut UiLayer,
324 text: &str,
325 color: Vec4,
326 bg_alpha: f32,
327) {
328 let (tw, th) = ui.measure_text(text, 1.0);
329 let padding = ui.char_width * 2.0;
330 let banner_w = tw + padding * 2.0;
331 let banner_h = th + padding;
332 let bx = (ui.screen_width - banner_w) * 0.5;
333 let by = ui.char_height;
334
335 let bg = Vec4::new(0.0, 0.0, 0.0, bg_alpha);
336 ui.draw_rect(bx, by, banner_w, banner_h, bg, true);
337 ui.draw_centered_text(by + padding * 0.5, text, 1.0, color);
338}
339
340#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn draw_hp_bar_queues_commands() {
348 let mut ui = UiLayer::new(1280.0, 800.0);
349 draw_hp_bar(&mut ui, 10.0, 10.0, 200.0, 75.0, 100.0, None);
350 assert!(ui.command_count() >= 2); }
352
353 #[test]
354 fn draw_menu_queues_commands() {
355 let mut ui = UiLayer::new(1280.0, 800.0);
356 draw_menu(&mut ui, 100.0, 100.0, &["Option A", "Option B"], 0, Some("Menu"));
357 assert!(ui.command_count() > 0);
358 }
359
360 #[test]
361 fn draw_tooltip_clamps_to_screen() {
362 let mut ui = UiLayer::new(200.0, 200.0);
363 draw_tooltip(&mut ui, 190.0, 190.0, "Hello World");
364 assert!(ui.command_count() > 0);
366 }
367
368 #[test]
369 fn colors_are_valid() {
370 assert_eq!(UiColors::WHITE.w, 1.0);
371 assert!(UiColors::PANEL_BG.w > 0.0 && UiColors::PANEL_BG.w < 1.0);
372 }
373}