1use crate::math::{Vec2, Rect};
12use crate::color::Color;
13use crate::input::{InputState, KeyCode, MouseButton};
14
15#[derive(Debug, Clone)]
21pub struct UiTheme {
22 pub primary: Color,
24 pub secondary: Color,
26 pub text: Color,
28 pub text_hovered: Color,
30 pub input_bg: Color,
32 pub border: Color,
34 pub border_focused: Color,
36 pub font_size: f32,
38 pub corner_radius: f32,
40 pub padding: Vec2,
42 pub spacing: f32,
44 pub animation_speed: f32,
46 pub show_focus: bool,
48}
49
50impl Default for UiTheme {
51 fn default() -> Self {
52 Self {
53 primary: Color::from_hex("#4A90D9").unwrap(),
54 secondary: Color::from_hex("#2C2C3E").unwrap(),
55 text: Color::WHITE,
56 text_hovered: Color::new(1.0, 1.0, 0.8, 1.0),
57 input_bg: Color::from_hex("#1E1E2E").unwrap(),
58 border: Color::from_hex("#444466").unwrap(),
59 border_focused: Color::from_hex("#6CA0DC").unwrap(),
60 font_size: 16.0,
61 corner_radius: 6.0,
62 padding: Vec2::new(12.0, 8.0),
63 spacing: 8.0,
64 animation_speed: 8.0,
65 show_focus: true,
66 }
67 }
68}
69
70impl UiTheme {
71 pub fn dark() -> Self {
73 Self::default()
74 }
75
76 pub fn light() -> Self {
78 Self {
79 primary: Color::from_hex("#3B82F6").unwrap(),
80 secondary: Color::from_hex("#F3F4F6").unwrap(),
81 text: Color::from_hex("#111827").unwrap(),
82 text_hovered: Color::from_hex("#1D4ED8").unwrap(),
83 input_bg: Color::WHITE,
84 border: Color::from_hex("#D1D5DB").unwrap(),
85 border_focused: Color::from_hex("#3B82F6").unwrap(),
86 ..Self::default()
87 }
88 }
89}
90
91#[derive(Debug, Default)]
97pub struct UiState {
98 hovered_id: Option<u64>,
100 active_id: Option<u64>,
102 focused_id: Option<u64>,
104 hot_id: Option<u64>,
106 hover_animations: std::collections::HashMap<u64, f32>,
108 z_index: u32,
110}
111
112impl UiState {
113 pub fn id_from_label(label: &str) -> u64 {
115 use std::hash::{Hash, Hasher};
116 let mut hasher = std::collections::hash_map::DefaultHasher::new();
117 label.hash(&mut hasher);
118 hasher.finish()
119 }
120
121 pub fn is_hovered(&self, id: u64) -> bool {
123 self.hovered_id == Some(id)
124 }
125
126 pub fn is_active(&self, id: u64) -> bool {
128 self.active_id == Some(id)
129 }
130
131 pub fn is_focused(&self, id: u64) -> bool {
133 self.focused_id == Some(id)
134 }
135
136 pub fn hover_t(&self, id: u64) -> f32 {
138 self.hover_animations.get(&id).copied().unwrap_or(0.0)
139 }
140
141 pub fn update_animations(&mut self, dt: f32, speed: f32) {
143 let ids: Vec<u64> = self.hover_animations.keys().copied().collect();
146 let mut to_remove: Vec<u64> = Vec::new();
147 for id in ids {
148 let t = *self.hover_animations.get(&id).unwrap_or(&0.0);
149 if self.hovered_id == Some(id) {
150 let new_t = (t + dt * speed).min(1.0);
151 self.hover_animations.insert(id, new_t);
152 } else if t > 0.0 {
153 let new_t = (t - dt * speed).max(0.0);
154 if new_t <= 0.0 {
155 to_remove.push(id);
156 } else {
157 self.hover_animations.insert(id, new_t);
158 }
159 } else {
160 to_remove.push(id);
161 }
162 }
163
164 for id in to_remove {
165 self.hover_animations.remove(&id);
166 }
167 }
168
169 pub fn begin_frame(&mut self) {
171 self.hovered_id = None;
172 self.hot_id = None;
173 self.z_index = 0;
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq)]
183pub struct UiInteraction {
184 pub clicked: bool,
186 pub pressed: bool,
188 pub released: bool,
190 pub hovered: bool,
192 pub focused: bool,
194}
195
196pub fn button(
209 input: &InputState,
210 ui: &mut UiState,
211 theme: &UiTheme,
212 label: &str,
213 rect: Rect,
214) -> UiInteraction {
215 let id = UiState::id_from_label(label);
216 let _mouse_in_rect = input.mouse.is_down(MouseButton::Left) && rect.contains(input.mouse.position);
217 let mouse_hovering = rect.contains(input.mouse.position);
218
219 if mouse_hovering {
221 ui.hovered_id = Some(id);
222 if !ui.hover_animations.contains_key(&id) {
223 ui.hover_animations.insert(id, 0.0);
224 }
225 }
226
227 let was_active = ui.active_id == Some(id);
229 if mouse_hovering && input.mouse.is_pressed(MouseButton::Left) {
230 ui.active_id = Some(id);
231 ui.hot_id = Some(id);
232 }
233
234 let clicked = was_active && input.mouse.is_released(MouseButton::Left) && mouse_hovering;
235 let released = was_active && input.mouse.is_released(MouseButton::Left);
236 let pressed = ui.active_id == Some(id);
237
238 if released {
239 ui.active_id = None;
240 }
241
242 let hover_t = ui.hover_animations.get(&id).copied().unwrap_or(0.0);
244 let color = if pressed {
245 theme.primary.darkened(0.3)
246 } else {
247 theme.primary.lerp(theme.primary.lightened(0.15), hover_t)
248 };
249
250 let _ = (color, label); UiInteraction {
253 clicked,
254 pressed,
255 released,
256 hovered: mouse_hovering,
257 focused: false,
258 }
259}
260
261pub fn slider(
272 input: &InputState,
273 ui: &mut UiState,
274 _theme: &UiTheme,
275 label: &str,
276 rect: Rect,
277 current_value: f32,
278 min: f32,
279 max: f32,
280) -> (f32, UiInteraction) {
281 let id = UiState::id_from_label(label);
282 let hovering = rect.contains(input.mouse.position);
283
284 if hovering {
285 ui.hovered_id = Some(id);
286 }
287
288 let mut value = current_value;
289 let interaction = UiInteraction {
290 clicked: false,
291 pressed: ui.active_id == Some(id),
292 released: false,
293 hovered: hovering,
294 focused: false,
295 };
296
297 if hovering && input.mouse.is_pressed(MouseButton::Left) {
298 ui.active_id = Some(id);
299 }
300
301 if ui.active_id == Some(id) {
302 if input.mouse.is_released(MouseButton::Left) {
303 ui.active_id = None;
304 } else {
305 let t = ((input.mouse.position.x - rect.x) / rect.w).clamp(0.0, 1.0);
307 value = min + (max - min) * t;
308 }
309 }
310
311 (value, interaction)
312}
313
314#[derive(Debug, Clone)]
320pub struct TextInputState {
321 pub text: String,
323 pub cursor: usize,
325 pub selection_start: Option<usize>,
327 pub cursor_visible: bool,
329 blink_timer: f32,
331 pub scroll_offset: f32,
333}
334
335impl Default for TextInputState {
336 fn default() -> Self {
337 Self {
338 text: String::new(),
339 cursor: 0,
340 selection_start: None,
341 cursor_visible: true,
342 blink_timer: 0.0,
343 scroll_offset: 0.0,
344 }
345 }
346}
347
348impl TextInputState {
349 pub fn new(text: &str) -> Self {
351 let len = text.len();
352 Self {
353 text: text.to_string(),
354 cursor: len,
355 ..Self::default()
356 }
357 }
358
359 pub fn handle_text_input(&mut self, input_text: &str) {
361 if self.selection_start.is_some() {
362 self.delete_selection();
363 }
364 self.text.insert_str(self.cursor, input_text);
365 self.cursor += input_text.len();
366 }
367
368 pub fn handle_key(&mut self, key: KeyCode, modifiers: KeyModifiers) {
370 match key {
371 KeyCode::Backspace => {
372 if self.selection_start.is_some() {
373 self.delete_selection();
374 } else if self.cursor > 0 {
375 let prev = self.text[..self.cursor]
377 .char_indices()
378 .next_back()
379 .map(|(i, _)| i)
380 .unwrap_or(0);
381 self.text.drain(prev..self.cursor);
382 self.cursor = prev;
383 }
384 }
385 KeyCode::Delete => {
386 if self.selection_start.is_some() {
387 self.delete_selection();
388 } else if self.cursor < self.text.len() {
389 let next = self.text[self.cursor..]
390 .char_indices()
391 .nth(1)
392 .map(|(i, _)| self.cursor + i)
393 .unwrap_or(self.text.len());
394 self.text.drain(self.cursor..next);
395 }
396 }
397 KeyCode::Left => {
398 if modifiers.shift {
399 self.selection_start = Some(self.selection_start.unwrap_or(self.cursor));
400 } else {
401 self.selection_start = None;
402 }
403 if self.cursor > 0 {
404 self.cursor = self.text[..self.cursor]
405 .char_indices()
406 .next_back()
407 .map(|(i, _)| i)
408 .unwrap_or(0);
409 }
410 }
411 KeyCode::Right => {
412 if modifiers.shift {
413 self.selection_start = Some(self.selection_start.unwrap_or(self.cursor));
414 } else {
415 self.selection_start = None;
416 }
417 if self.cursor < self.text.len() {
418 self.cursor = self.text[self.cursor..]
419 .char_indices()
420 .nth(1)
421 .map(|(i, _)| self.cursor + i)
422 .unwrap_or(self.text.len());
423 }
424 }
425 KeyCode::Home => {
426 self.cursor = 0;
427 self.selection_start = None;
428 }
429 KeyCode::End => {
430 self.cursor = self.text.len();
431 self.selection_start = None;
432 }
433 KeyCode::A if modifiers.ctrl => {
434 self.selection_start = Some(0);
435 self.cursor = self.text.len();
436 }
437 KeyCode::C if modifiers.ctrl => {
438 }
440 KeyCode::V if modifiers.ctrl => {
441 }
443 KeyCode::X if modifiers.ctrl => {
444 }
446 KeyCode::Enter => {
447 }
449 _ => {}
450 }
451 }
452
453 fn delete_selection(&mut self) {
454 if let Some(start) = self.selection_start {
455 let (lo, hi) = if start < self.cursor {
456 (start, self.cursor)
457 } else {
458 (self.cursor, start)
459 };
460 self.text.drain(lo..hi);
461 self.cursor = lo;
462 self.selection_start = None;
463 }
464 }
465
466 pub fn update(&mut self, dt: f32) {
468 self.blink_timer += dt;
469 if self.blink_timer >= 0.5 {
470 self.blink_timer = 0.0;
471 self.cursor_visible = !self.cursor_visible;
472 }
473 }
474}
475
476#[derive(Debug, Clone, Copy, Default)]
478pub struct KeyModifiers {
479 pub shift: bool,
480 pub ctrl: bool,
481 pub alt: bool,
482 pub super_key: bool,
483}
484
485pub fn text_input(
487 input: &InputState,
488 ui: &mut UiState,
489 theme: &UiTheme,
490 label: &str,
491 rect: Rect,
492 state: &mut TextInputState,
493) -> UiInteraction {
494 let id = UiState::id_from_label(label);
495 let hovering = rect.contains(input.mouse.position);
496
497 if hovering {
498 ui.hovered_id = Some(id);
499 }
500
501 if hovering && input.mouse.is_pressed(MouseButton::Left) {
503 ui.focused_id = Some(id);
504 let rel_x = input.mouse.position.x - rect.x;
506 let char_width = theme.font_size * 0.6; state.cursor = (rel_x / char_width).max(0.0) as usize;
508 state.cursor = state.cursor.min(state.text.len());
509 state.blink_timer = 0.0;
510 state.cursor_visible = true;
511 }
512
513 if ui.focused_id == Some(id) {
515 let text = input.keyboard.text();
516 if !text.is_empty() {
517 state.handle_text_input(text);
518 }
519 }
520
521 let focused = ui.focused_id == Some(id);
522 let border_color = if focused { theme.border_focused } else { theme.border };
523
524 let _ = (border_color, state, label, rect); UiInteraction {
527 clicked: false,
528 pressed: false,
529 released: false,
530 hovered: hovering,
531 focused,
532 }
533}
534
535pub fn label(
541 theme: &UiTheme,
542 text: &str,
543 position: Vec2,
544 font_size: Option<f32>,
545 color: Option<Color>,
546) -> Rect {
547 let size = font_size.unwrap_or(theme.font_size);
548 let width = text.len() as f32 * size * 0.6;
549 let height = size * 1.2;
550 let _ = color;
551 Rect::new(position.x, position.y, width, height)
552}
553
554#[derive(Debug, Clone)]
560pub struct VerticalLayout {
561 pub origin: Vec2,
563 cursor_y: f32,
565 pub width: f32,
567 pub spacing: f32,
569}
570
571impl VerticalLayout {
572 pub fn new(origin: Vec2, width: f32, spacing: f32) -> Self {
574 Self {
575 origin,
576 cursor_y: origin.y,
577 width,
578 spacing,
579 }
580 }
581
582 pub fn next(&mut self, height: f32) -> Vec2 {
584 let pos = Vec2::new(self.origin.x, self.cursor_y);
585 self.cursor_y += height + self.spacing;
586 pos
587 }
588
589 pub fn next_rect(&mut self, height: f32) -> Rect {
591 let pos = self.next(height);
592 Rect::new(pos.x, pos.y, self.width, height)
593 }
594
595 pub fn reset(&mut self) {
597 self.cursor_y = self.origin.y;
598 }
599}
600
601#[derive(Debug, Clone)]
603pub struct HorizontalLayout {
604 pub origin: Vec2,
606 cursor_x: f32,
608 pub height: f32,
610 pub spacing: f32,
612}
613
614impl HorizontalLayout {
615 pub fn new(origin: Vec2, height: f32, spacing: f32) -> Self {
617 Self {
618 origin,
619 cursor_x: origin.x,
620 height,
621 spacing,
622 }
623 }
624
625 pub fn next(&mut self, width: f32) -> Vec2 {
627 let pos = Vec2::new(self.cursor_x, self.origin.y);
628 self.cursor_x += width + self.spacing;
629 pos
630 }
631
632 pub fn next_rect(&mut self, width: f32) -> Rect {
634 let pos = self.next(width);
635 Rect::new(pos.x, pos.y, width, self.height)
636 }
637
638 pub fn reset(&mut self) {
640 self.cursor_x = self.origin.x;
641 }
642}
643
644pub fn checkbox(
650 input: &InputState,
651 ui: &mut UiState,
652 theme: &UiTheme,
653 label: &str,
654 position: Vec2,
655 checked: &mut bool,
656) -> UiInteraction {
657 let size = theme.font_size;
658 let rect = Rect::new(position.x, position.y, size, size);
659
660 let interaction = button(input, ui, theme, label, rect);
661 if interaction.clicked {
662 *checked = !*checked;
663 }
664
665 interaction
666}
667
668pub fn progress_bar(
674 _theme: &UiTheme,
675 _rect: Rect,
676 progress: f32,
677 fill_color: Option<Color>,
678 bg_color: Option<Color>,
679) {
680 let _fill = fill_color.unwrap_or(Color::from_hex("#4CAF50").unwrap());
681 let _bg = bg_color.unwrap_or(Color::from_hex("#333333").unwrap());
682 let _clamped_progress = progress.clamp(0.0, 1.0);
683 }