1use crate::{MathFunction, Glyph, RenderLayer};
4use crate::glyph::GlyphId;
5use crate::ProofEngine;
6use glam::{Vec2, Vec3, Vec4};
7
8pub struct UiLabel {
12 pub text: String,
13 pub position: Vec3,
14 pub color: Vec4,
15 pub char_scale: f32,
16 pub char_spacing: f32,
17 pub emission: f32,
18 pub glow_color: Vec3,
19 pub glow_radius: f32,
20 pub emission_fn: Option<MathFunction>,
22 pub color_fn: Option<MathFunction>,
24 pub visible: bool,
25}
26
27impl UiLabel {
28 pub fn new(text: impl Into<String>, position: Vec3, color: Vec4) -> Self {
29 Self {
30 text: text.into(),
31 position,
32 color,
33 char_scale: 0.6,
34 char_spacing: 0.45,
35 emission: 0.3,
36 glow_color: Vec3::new(color.x, color.y, color.z),
37 glow_radius: 0.0,
38 emission_fn: None,
39 color_fn: None,
40 visible: true,
41 }
42 }
43
44 pub fn with_glow(mut self, radius: f32) -> Self {
45 self.glow_radius = radius;
46 self.emission = 0.8;
47 self
48 }
49
50 pub fn with_pulse(mut self, rate: f32) -> Self {
51 self.emission_fn = Some(MathFunction::Sine {
52 amplitude: 0.4, frequency: rate, phase: 0.0,
53 }.offset(0.6));
54 self
55 }
56
57 pub fn with_color_cycle(mut self, speed: f32) -> Self {
58 self.color_fn = Some(MathFunction::Sine {
59 amplitude: 1.0, frequency: speed, phase: 0.0,
60 });
61 self
62 }
63
64 pub fn render(&self, engine: &mut ProofEngine, time: f32) -> Vec<GlyphId> {
66 if !self.visible { return Vec::new(); }
67 let mut ids = Vec::new();
68
69 let emission = if let Some(ref f) = self.emission_fn {
70 f.evaluate(time, 0.0).clamp(0.0, 2.0)
71 } else {
72 self.emission
73 };
74
75 let color = if let Some(ref f) = self.color_fn {
76 let v = f.evaluate(time, 0.0);
77 Vec4::new(
78 (self.color.x + v * 0.2).clamp(0.0, 1.0),
79 (self.color.y + v * 0.1).clamp(0.0, 1.0),
80 (self.color.z - v * 0.1).clamp(0.0, 1.0),
81 self.color.w,
82 )
83 } else {
84 self.color
85 };
86
87 for (i, ch) in self.text.chars().enumerate() {
88 let x = self.position.x + i as f32 * self.char_spacing;
89 let id = engine.scene.spawn_glyph(Glyph {
90 character: ch,
91 position: Vec3::new(x, self.position.y, self.position.z),
92 color,
93 scale: Vec2::splat(self.char_scale),
94 emission,
95 glow_color: self.glow_color,
96 glow_radius: self.glow_radius,
97 layer: RenderLayer::UI,
98 ..Default::default()
99 });
100 ids.push(id);
101 }
102 ids
103 }
104}
105
106pub struct UiProgressBar {
112 pub position: Vec3,
113 pub width_chars: usize,
114 pub value: f32, pub target_value: f32, pub smoothing: f32, pub full_color: Vec4,
118 pub empty_color: Vec4,
119 pub border_color: Vec4,
120 pub label: Option<String>,
121 pub label_color: Vec4,
122 pub show_value: bool,
123 pub flash_on_low: bool, flash_timer: f32,
125}
126
127const FILL_CHARS: &[char] = &[' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
128
129impl UiProgressBar {
130 pub fn new(position: Vec3, width_chars: usize, full_color: Vec4) -> Self {
131 Self {
132 position,
133 width_chars: width_chars.max(4),
134 value: 1.0,
135 target_value: 1.0,
136 smoothing: 8.0,
137 full_color,
138 empty_color: Vec4::new(0.15, 0.15, 0.15, 0.8),
139 border_color: Vec4::new(0.4, 0.4, 0.4, 0.7),
140 label: None,
141 label_color: Vec4::new(0.9, 0.9, 0.9, 1.0),
142 show_value: false,
143 flash_on_low: false,
144 flash_timer: 0.0,
145 }
146 }
147
148 pub fn with_label(mut self, label: impl Into<String>) -> Self {
149 self.label = Some(label.into());
150 self
151 }
152
153 pub fn with_value_display(mut self) -> Self {
154 self.show_value = true;
155 self
156 }
157
158 pub fn with_flash_on_low(mut self) -> Self {
159 self.flash_on_low = true;
160 self
161 }
162
163 pub fn set_value(&mut self, v: f32) {
165 self.target_value = v.clamp(0.0, 1.0);
166 }
167
168 pub fn snap_value(&mut self, v: f32) {
170 self.value = v.clamp(0.0, 1.0);
171 self.target_value = self.value;
172 }
173
174 pub fn tick(&mut self, dt: f32) {
176 let diff = self.target_value - self.value;
177 self.value += diff * (self.smoothing * dt).min(1.0);
178 if self.flash_on_low && self.value < 0.2 {
179 self.flash_timer += dt * 4.0;
180 } else {
181 self.flash_timer = 0.0;
182 }
183 }
184
185 pub fn render(&self, engine: &mut ProofEngine, time: f32) -> Vec<GlyphId> {
186 let mut ids = Vec::new();
187 let cols = self.width_chars;
188
189 let flash_alpha = if self.flash_on_low && self.value < 0.2 {
191 0.5 + 0.5 * self.flash_timer.sin()
192 } else {
193 1.0
194 };
195
196 let border_left = '[';
198 let border_right = ']';
199
200 let id = engine.scene.spawn_glyph(Glyph {
201 character: border_left,
202 position: self.position,
203 color: self.border_color * Vec4::new(1.0, 1.0, 1.0, flash_alpha),
204 scale: Vec2::splat(0.55),
205 layer: RenderLayer::UI,
206 ..Default::default()
207 });
208 ids.push(id);
209
210 let fill_amount = self.value * cols as f32;
212 for col in 0..cols {
213 let pos = self.position + Vec3::new((col + 1) as f32 * 0.48, 0.0, 0.0);
214 let cell_fill = (fill_amount - col as f32).clamp(0.0, 1.0);
215 let ch_idx = (cell_fill * (FILL_CHARS.len() - 1) as f32).round() as usize;
216 let ch = FILL_CHARS[ch_idx];
217
218 let blend = col as f32 / cols.max(1) as f32;
219 let color = if cell_fill > 0.0 {
220 Vec4::new(
221 self.full_color.x * (1.0 - blend * 0.3),
222 self.full_color.y,
223 self.full_color.z * (1.0 - blend * 0.2),
224 self.full_color.w * flash_alpha,
225 )
226 } else {
227 Vec4::new(
228 self.empty_color.x,
229 self.empty_color.y,
230 self.empty_color.z,
231 self.empty_color.w * flash_alpha,
232 )
233 };
234
235 let emission = if cell_fill > 0.8 { 0.5 } else { 0.1 };
236
237 let id = engine.scene.spawn_glyph(Glyph {
238 character: ch,
239 position: pos,
240 color,
241 scale: Vec2::splat(0.50),
242 emission,
243 glow_color: Vec3::new(self.full_color.x, self.full_color.y, self.full_color.z),
244 glow_radius: if cell_fill > 0.9 { 0.4 } else { 0.0 },
245 layer: RenderLayer::UI,
246 ..Default::default()
247 });
248 ids.push(id);
249 }
250
251 let rpos = self.position + Vec3::new((cols + 1) as f32 * 0.48, 0.0, 0.0);
253 let id = engine.scene.spawn_glyph(Glyph {
254 character: border_right,
255 position: rpos,
256 color: self.border_color * Vec4::new(1.0, 1.0, 1.0, flash_alpha),
257 scale: Vec2::splat(0.55),
258 layer: RenderLayer::UI,
259 ..Default::default()
260 });
261 ids.push(id);
262
263 if let Some(ref label) = self.label {
265 let label_x = self.position.x - label.len() as f32 * 0.35 - 0.3;
266 for (i, ch) in label.chars().enumerate() {
267 let pos = Vec3::new(label_x + i as f32 * 0.35, self.position.y, self.position.z);
268 let id = engine.scene.spawn_glyph(Glyph {
269 character: ch,
270 position: pos,
271 color: self.label_color,
272 scale: Vec2::splat(0.45),
273 layer: RenderLayer::UI,
274 ..Default::default()
275 });
276 ids.push(id);
277 }
278 }
279
280 if self.show_value {
282 let pct_str = format!("{:>3.0}%", self.value * 100.0);
283 let value_x = rpos.x + 0.5;
284 for (i, ch) in pct_str.chars().enumerate() {
285 let pos = Vec3::new(value_x + i as f32 * 0.35, self.position.y, self.position.z);
286 let id = engine.scene.spawn_glyph(Glyph {
287 character: ch,
288 position: pos,
289 color: Vec4::new(0.8, 0.8, 0.8, 0.9),
290 scale: Vec2::splat(0.42),
291 layer: RenderLayer::UI,
292 ..Default::default()
293 });
294 ids.push(id);
295 }
296 }
297
298 let _ = time; ids
300 }
301}
302
303pub struct UiButton {
310 pub label: String,
311 pub position: Vec3,
312 pub normal_color: Vec4,
313 pub hover_color: Vec4,
314 pub press_color: Vec4,
315 pub border_color: Vec4,
316 pub char_scale: f32,
317 pub padding: f32,
318 pub width: f32,
320 hovered: bool,
321 pressed: bool,
322 hover_anim: f32,
323 pub clicked: bool,
324 pub id: u32,
326}
327
328impl UiButton {
329 pub fn new(label: impl Into<String>, position: Vec3, id: u32) -> Self {
330 let label: String = label.into();
331 let width = label.len() as f32 * 0.5 + 0.8;
332 Self {
333 label,
334 position,
335 normal_color: Vec4::new(0.3, 0.3, 0.35, 0.9),
336 hover_color: Vec4::new(0.5, 0.5, 0.6, 1.0),
337 press_color: Vec4::new(0.7, 0.7, 0.8, 1.0),
338 border_color: Vec4::new(0.6, 0.6, 0.7, 0.8),
339 char_scale: 0.55,
340 padding: 0.3,
341 width,
342 hovered: false,
343 pressed: false,
344 hover_anim: 0.0,
345 clicked: false,
346 id,
347 }
348 }
349
350 pub fn tick(&mut self, mouse_ndc: glam::Vec2, mouse_clicked: bool, dt: f32) {
351 self.clicked = false;
352 self.hovered = false;
355 self.pressed = self.hovered && mouse_clicked;
356 let hover_anim_target = if self.hovered { 1.0 } else { 0.0 };
357 self.hover_anim += (hover_anim_target - self.hover_anim) * (10.0 * dt).min(1.0);
358 let _ = mouse_ndc;
359 }
360
361 pub fn render(&self, engine: &mut ProofEngine, time: f32) -> Vec<GlyphId> {
362 let mut ids = Vec::new();
363
364 let color = if self.pressed { self.press_color }
365 else if self.hovered { self.hover_color }
366 else { self.normal_color };
367
368 let emit = if self.hovered { 0.6 } else { 0.2 };
369 let pulse = if self.hovered {
370 0.0 + 0.1 * (time * 3.0).sin()
371 } else {
372 0.0
373 };
374
375 for (i, ch) in self.label.chars().enumerate() {
377 let x = self.position.x + i as f32 * 0.48 - self.label.len() as f32 * 0.24;
378 let id = engine.scene.spawn_glyph(Glyph {
379 character: ch,
380 position: Vec3::new(x, self.position.y + pulse, self.position.z),
381 color,
382 scale: Vec2::splat(self.char_scale),
383 emission: emit,
384 glow_color: Vec3::new(color.x, color.y, color.z),
385 glow_radius: if self.hovered { 0.6 } else { 0.2 },
386 layer: RenderLayer::UI,
387 ..Default::default()
388 });
389 ids.push(id);
390 }
391
392 ids
393 }
394}
395
396pub struct UiPanel {
400 pub position: Vec3,
401 pub width: usize,
402 pub height: usize,
403 pub border_color: Vec4,
404 pub fill_color: Vec4,
405 pub title: Option<String>,
406 pub title_color: Vec4,
407 pub char_scale: f32,
408}
409
410impl UiPanel {
411 pub fn new(position: Vec3, width: usize, height: usize) -> Self {
412 Self {
413 position,
414 width: width.max(3),
415 height: height.max(3),
416 border_color: Vec4::new(0.4, 0.5, 0.6, 0.8),
417 fill_color: Vec4::new(0.05, 0.05, 0.1, 0.5),
418 title: None,
419 title_color: Vec4::new(0.8, 0.9, 1.0, 1.0),
420 char_scale: 0.5,
421 }
422 }
423
424 pub fn with_title(mut self, title: impl Into<String>) -> Self {
425 self.title = Some(title.into());
426 self
427 }
428
429 pub fn render(&self, engine: &mut ProofEngine, _time: f32) -> Vec<GlyphId> {
430 let mut ids = Vec::new();
431 let cw = 0.5_f32;
432 let ch = 0.65_f32;
433
434 let top_chars = std::iter::once('┌')
439 .chain(std::iter::repeat('─').take(self.width))
440 .chain(std::iter::once('┐'));
441 let bot_chars = std::iter::once('└')
442 .chain(std::iter::repeat('─').take(self.width))
443 .chain(std::iter::once('┘'));
444
445 for (col, ch_) in top_chars.enumerate() {
446 let pos = self.position + Vec3::new(col as f32 * cw, 0.0, 0.0);
447 let id = engine.scene.spawn_glyph(Glyph {
448 character: ch_, position: pos, color: self.border_color,
449 scale: Vec2::splat(self.char_scale), layer: RenderLayer::UI,
450 ..Default::default()
451 });
452 ids.push(id);
453 }
454
455 for row in 1..=self.height {
456 let y = -(row as f32 * ch);
457 let left_pos = self.position + Vec3::new(0.0, y, 0.0);
458 let right_pos = self.position + Vec3::new((self.width + 1) as f32 * cw, y, 0.0);
459
460 let id = engine.scene.spawn_glyph(Glyph {
461 character: '│', position: left_pos, color: self.border_color,
462 scale: Vec2::splat(self.char_scale), layer: RenderLayer::UI,
463 ..Default::default()
464 });
465 ids.push(id);
466
467 for col in 1..=self.width {
469 let fill_pos = self.position + Vec3::new(col as f32 * cw, y, -0.1);
470 let id = engine.scene.spawn_glyph(Glyph {
471 character: ' ', position: fill_pos, color: self.fill_color,
472 scale: Vec2::splat(self.char_scale), layer: RenderLayer::UI,
473 ..Default::default()
474 });
475 ids.push(id);
476 }
477
478 let id = engine.scene.spawn_glyph(Glyph {
479 character: '│', position: right_pos, color: self.border_color,
480 scale: Vec2::splat(self.char_scale), layer: RenderLayer::UI,
481 ..Default::default()
482 });
483 ids.push(id);
484 }
485
486 let bot_y = -((self.height as f32 + 1.0) * ch);
488 for (col, ch_) in bot_chars.enumerate() {
489 let pos = self.position + Vec3::new(col as f32 * cw, bot_y, 0.0);
490 let id = engine.scene.spawn_glyph(Glyph {
491 character: ch_, position: pos, color: self.border_color,
492 scale: Vec2::splat(self.char_scale), layer: RenderLayer::UI,
493 ..Default::default()
494 });
495 ids.push(id);
496 }
497
498 if let Some(ref title) = self.title {
500 for (i, ch_) in title.chars().enumerate().take(self.width) {
501 let pos = self.position + Vec3::new((i + 1) as f32 * cw, 0.0, 0.1);
502 let id = engine.scene.spawn_glyph(Glyph {
503 character: ch_, position: pos, color: self.title_color,
504 scale: Vec2::splat(self.char_scale * 1.0),
505 emission: 0.4, glow_color: Vec3::new(0.8, 0.9, 1.0), glow_radius: 0.3,
506 layer: RenderLayer::UI,
507 ..Default::default()
508 });
509 ids.push(id);
510 }
511 }
512
513 ids
514 }
515}
516
517pub struct UiPulseRing {
521 pub center: Vec3,
522 pub radius: f32,
523 pub count: usize,
524 pub glyph: char,
525 pub color: Vec4,
526 pub speed: f32,
527 pub emission: f32,
528 phase: f32,
529}
530
531impl UiPulseRing {
532 pub fn new(center: Vec3, radius: f32, count: usize, color: Vec4) -> Self {
533 Self {
534 center, radius, count: count.max(3),
535 glyph: '◆', color, speed: 1.0, emission: 0.8, phase: 0.0,
536 }
537 }
538
539 pub fn with_glyph(mut self, ch: char) -> Self { self.glyph = ch; self }
540 pub fn with_speed(mut self, speed: f32) -> Self { self.speed = speed; self }
541
542 pub fn tick(&mut self, dt: f32) {
543 self.phase += dt * self.speed;
544 }
545
546 pub fn render(&self, engine: &mut ProofEngine, time: f32) -> Vec<GlyphId> {
547 let mut ids = Vec::new();
548 let n = self.count;
549
550 for i in 0..n {
551 let base_angle = (i as f32 / n as f32) * std::f32::consts::TAU;
552 let wobble = (time * self.speed * 2.0 + base_angle).sin() * 0.1;
553 let angle = base_angle + self.phase;
554 let r = self.radius + wobble;
555 let pos = self.center + Vec3::new(angle.cos() * r, angle.sin() * r, 0.0);
556
557 let pulse = 0.8 + 0.2 * ((time * self.speed * 3.0 + base_angle).sin());
559 let emit_pulse = self.emission * (0.5 + 0.5 * ((time * 2.0 + base_angle).sin()));
560
561 let id = engine.scene.spawn_glyph(Glyph {
562 character: self.glyph,
563 position: pos,
564 color: self.color,
565 scale: Vec2::splat(0.4 * pulse),
566 emission: emit_pulse,
567 glow_color: Vec3::new(self.color.x, self.color.y, self.color.z),
568 glow_radius: 0.6,
569 rotation: angle + self.phase,
570 layer: RenderLayer::UI,
571 ..Default::default()
572 });
573 ids.push(id);
574 }
575
576 ids
577 }
578}