use crate::math::{Vec2, Rect};
use crate::color::Color;
use crate::input::{InputState, KeyCode, MouseButton};
#[derive(Debug, Clone)]
pub struct UiTheme {
pub primary: Color,
pub secondary: Color,
pub text: Color,
pub text_hovered: Color,
pub input_bg: Color,
pub border: Color,
pub border_focused: Color,
pub font_size: f32,
pub corner_radius: f32,
pub padding: Vec2,
pub spacing: f32,
pub animation_speed: f32,
pub show_focus: bool,
}
impl Default for UiTheme {
fn default() -> Self {
Self {
primary: Color::from_hex("#4A90D9").unwrap(),
secondary: Color::from_hex("#2C2C3E").unwrap(),
text: Color::WHITE,
text_hovered: Color::new(1.0, 1.0, 0.8, 1.0),
input_bg: Color::from_hex("#1E1E2E").unwrap(),
border: Color::from_hex("#444466").unwrap(),
border_focused: Color::from_hex("#6CA0DC").unwrap(),
font_size: 16.0,
corner_radius: 6.0,
padding: Vec2::new(12.0, 8.0),
spacing: 8.0,
animation_speed: 8.0,
show_focus: true,
}
}
}
impl UiTheme {
pub fn dark() -> Self {
Self::default()
}
pub fn light() -> Self {
Self {
primary: Color::from_hex("#3B82F6").unwrap(),
secondary: Color::from_hex("#F3F4F6").unwrap(),
text: Color::from_hex("#111827").unwrap(),
text_hovered: Color::from_hex("#1D4ED8").unwrap(),
input_bg: Color::WHITE,
border: Color::from_hex("#D1D5DB").unwrap(),
border_focused: Color::from_hex("#3B82F6").unwrap(),
..Self::default()
}
}
}
#[derive(Debug, Default)]
pub struct UiState {
hovered_id: Option<u64>,
active_id: Option<u64>,
focused_id: Option<u64>,
hot_id: Option<u64>,
hover_animations: std::collections::HashMap<u64, f32>,
z_index: u32,
}
impl UiState {
pub fn id_from_label(label: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
label.hash(&mut hasher);
hasher.finish()
}
pub fn is_hovered(&self, id: u64) -> bool {
self.hovered_id == Some(id)
}
pub fn is_active(&self, id: u64) -> bool {
self.active_id == Some(id)
}
pub fn is_focused(&self, id: u64) -> bool {
self.focused_id == Some(id)
}
pub fn hover_t(&self, id: u64) -> f32 {
self.hover_animations.get(&id).copied().unwrap_or(0.0)
}
pub fn update_animations(&mut self, dt: f32, speed: f32) {
let ids: Vec<u64> = self.hover_animations.keys().copied().collect();
let mut to_remove: Vec<u64> = Vec::new();
for id in ids {
let t = *self.hover_animations.get(&id).unwrap_or(&0.0);
if self.hovered_id == Some(id) {
let new_t = (t + dt * speed).min(1.0);
self.hover_animations.insert(id, new_t);
} else if t > 0.0 {
let new_t = (t - dt * speed).max(0.0);
if new_t <= 0.0 {
to_remove.push(id);
} else {
self.hover_animations.insert(id, new_t);
}
} else {
to_remove.push(id);
}
}
for id in to_remove {
self.hover_animations.remove(&id);
}
}
pub fn begin_frame(&mut self) {
self.hovered_id = None;
self.hot_id = None;
self.z_index = 0;
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct UiInteraction {
pub clicked: bool,
pub pressed: bool,
pub released: bool,
pub hovered: bool,
pub focused: bool,
}
pub fn button(
input: &InputState,
ui: &mut UiState,
theme: &UiTheme,
label: &str,
rect: Rect,
) -> UiInteraction {
let id = UiState::id_from_label(label);
let _mouse_in_rect = input.mouse.is_down(MouseButton::Left) && rect.contains(input.mouse.position);
let mouse_hovering = rect.contains(input.mouse.position);
if mouse_hovering {
ui.hovered_id = Some(id);
if !ui.hover_animations.contains_key(&id) {
ui.hover_animations.insert(id, 0.0);
}
}
let was_active = ui.active_id == Some(id);
if mouse_hovering && input.mouse.is_pressed(MouseButton::Left) {
ui.active_id = Some(id);
ui.hot_id = Some(id);
}
let clicked = was_active && input.mouse.is_released(MouseButton::Left) && mouse_hovering;
let released = was_active && input.mouse.is_released(MouseButton::Left);
let pressed = ui.active_id == Some(id);
if released {
ui.active_id = None;
}
let hover_t = ui.hover_animations.get(&id).copied().unwrap_or(0.0);
let color = if pressed {
theme.primary.darkened(0.3)
} else {
theme.primary.lerp(theme.primary.lightened(0.15), hover_t)
};
let _ = (color, label);
UiInteraction {
clicked,
pressed,
released,
hovered: mouse_hovering,
focused: false,
}
}
pub fn slider(
input: &InputState,
ui: &mut UiState,
_theme: &UiTheme,
label: &str,
rect: Rect,
current_value: f32,
min: f32,
max: f32,
) -> (f32, UiInteraction) {
let id = UiState::id_from_label(label);
let hovering = rect.contains(input.mouse.position);
if hovering {
ui.hovered_id = Some(id);
}
let mut value = current_value;
let interaction = UiInteraction {
clicked: false,
pressed: ui.active_id == Some(id),
released: false,
hovered: hovering,
focused: false,
};
if hovering && input.mouse.is_pressed(MouseButton::Left) {
ui.active_id = Some(id);
}
if ui.active_id == Some(id) {
if input.mouse.is_released(MouseButton::Left) {
ui.active_id = None;
} else {
let t = ((input.mouse.position.x - rect.x) / rect.w).clamp(0.0, 1.0);
value = min + (max - min) * t;
}
}
(value, interaction)
}
#[derive(Debug, Clone)]
pub struct TextInputState {
pub text: String,
pub cursor: usize,
pub selection_start: Option<usize>,
pub cursor_visible: bool,
blink_timer: f32,
pub scroll_offset: f32,
}
impl Default for TextInputState {
fn default() -> Self {
Self {
text: String::new(),
cursor: 0,
selection_start: None,
cursor_visible: true,
blink_timer: 0.0,
scroll_offset: 0.0,
}
}
}
impl TextInputState {
pub fn new(text: &str) -> Self {
let len = text.len();
Self {
text: text.to_string(),
cursor: len,
..Self::default()
}
}
pub fn handle_text_input(&mut self, input_text: &str) {
if self.selection_start.is_some() {
self.delete_selection();
}
self.text.insert_str(self.cursor, input_text);
self.cursor += input_text.len();
}
pub fn handle_key(&mut self, key: KeyCode, modifiers: KeyModifiers) {
match key {
KeyCode::Backspace => {
if self.selection_start.is_some() {
self.delete_selection();
} else if self.cursor > 0 {
let prev = self.text[..self.cursor]
.char_indices()
.next_back()
.map(|(i, _)| i)
.unwrap_or(0);
self.text.drain(prev..self.cursor);
self.cursor = prev;
}
}
KeyCode::Delete => {
if self.selection_start.is_some() {
self.delete_selection();
} else if self.cursor < self.text.len() {
let next = self.text[self.cursor..]
.char_indices()
.nth(1)
.map(|(i, _)| self.cursor + i)
.unwrap_or(self.text.len());
self.text.drain(self.cursor..next);
}
}
KeyCode::Left => {
if modifiers.shift {
self.selection_start = Some(self.selection_start.unwrap_or(self.cursor));
} else {
self.selection_start = None;
}
if self.cursor > 0 {
self.cursor = self.text[..self.cursor]
.char_indices()
.next_back()
.map(|(i, _)| i)
.unwrap_or(0);
}
}
KeyCode::Right => {
if modifiers.shift {
self.selection_start = Some(self.selection_start.unwrap_or(self.cursor));
} else {
self.selection_start = None;
}
if self.cursor < self.text.len() {
self.cursor = self.text[self.cursor..]
.char_indices()
.nth(1)
.map(|(i, _)| self.cursor + i)
.unwrap_or(self.text.len());
}
}
KeyCode::Home => {
self.cursor = 0;
self.selection_start = None;
}
KeyCode::End => {
self.cursor = self.text.len();
self.selection_start = None;
}
KeyCode::A if modifiers.ctrl => {
self.selection_start = Some(0);
self.cursor = self.text.len();
}
KeyCode::C if modifiers.ctrl => {
}
KeyCode::V if modifiers.ctrl => {
}
KeyCode::X if modifiers.ctrl => {
}
KeyCode::Enter => {
}
_ => {}
}
}
fn delete_selection(&mut self) {
if let Some(start) = self.selection_start {
let (lo, hi) = if start < self.cursor {
(start, self.cursor)
} else {
(self.cursor, start)
};
self.text.drain(lo..hi);
self.cursor = lo;
self.selection_start = None;
}
}
pub fn update(&mut self, dt: f32) {
self.blink_timer += dt;
if self.blink_timer >= 0.5 {
self.blink_timer = 0.0;
self.cursor_visible = !self.cursor_visible;
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct KeyModifiers {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub super_key: bool,
}
pub fn text_input(
input: &InputState,
ui: &mut UiState,
theme: &UiTheme,
label: &str,
rect: Rect,
state: &mut TextInputState,
) -> UiInteraction {
let id = UiState::id_from_label(label);
let hovering = rect.contains(input.mouse.position);
if hovering {
ui.hovered_id = Some(id);
}
if hovering && input.mouse.is_pressed(MouseButton::Left) {
ui.focused_id = Some(id);
let rel_x = input.mouse.position.x - rect.x;
let char_width = theme.font_size * 0.6; state.cursor = (rel_x / char_width).max(0.0) as usize;
state.cursor = state.cursor.min(state.text.len());
state.blink_timer = 0.0;
state.cursor_visible = true;
}
if ui.focused_id == Some(id) {
let text = input.keyboard.text();
if !text.is_empty() {
state.handle_text_input(text);
}
}
let focused = ui.focused_id == Some(id);
let border_color = if focused { theme.border_focused } else { theme.border };
let _ = (border_color, state, label, rect);
UiInteraction {
clicked: false,
pressed: false,
released: false,
hovered: hovering,
focused,
}
}
pub fn label(
theme: &UiTheme,
text: &str,
position: Vec2,
font_size: Option<f32>,
color: Option<Color>,
) -> Rect {
let size = font_size.unwrap_or(theme.font_size);
let width = text.len() as f32 * size * 0.6;
let height = size * 1.2;
let _ = color;
Rect::new(position.x, position.y, width, height)
}
#[derive(Debug, Clone)]
pub struct VerticalLayout {
pub origin: Vec2,
cursor_y: f32,
pub width: f32,
pub spacing: f32,
}
impl VerticalLayout {
pub fn new(origin: Vec2, width: f32, spacing: f32) -> Self {
Self {
origin,
cursor_y: origin.y,
width,
spacing,
}
}
pub fn next(&mut self, height: f32) -> Vec2 {
let pos = Vec2::new(self.origin.x, self.cursor_y);
self.cursor_y += height + self.spacing;
pos
}
pub fn next_rect(&mut self, height: f32) -> Rect {
let pos = self.next(height);
Rect::new(pos.x, pos.y, self.width, height)
}
pub fn reset(&mut self) {
self.cursor_y = self.origin.y;
}
}
#[derive(Debug, Clone)]
pub struct HorizontalLayout {
pub origin: Vec2,
cursor_x: f32,
pub height: f32,
pub spacing: f32,
}
impl HorizontalLayout {
pub fn new(origin: Vec2, height: f32, spacing: f32) -> Self {
Self {
origin,
cursor_x: origin.x,
height,
spacing,
}
}
pub fn next(&mut self, width: f32) -> Vec2 {
let pos = Vec2::new(self.cursor_x, self.origin.y);
self.cursor_x += width + self.spacing;
pos
}
pub fn next_rect(&mut self, width: f32) -> Rect {
let pos = self.next(width);
Rect::new(pos.x, pos.y, width, self.height)
}
pub fn reset(&mut self) {
self.cursor_x = self.origin.x;
}
}
pub fn checkbox(
input: &InputState,
ui: &mut UiState,
theme: &UiTheme,
label: &str,
position: Vec2,
checked: &mut bool,
) -> UiInteraction {
let size = theme.font_size;
let rect = Rect::new(position.x, position.y, size, size);
let interaction = button(input, ui, theme, label, rect);
if interaction.clicked {
*checked = !*checked;
}
interaction
}
pub fn progress_bar(
_theme: &UiTheme,
_rect: Rect,
progress: f32,
fill_color: Option<Color>,
bg_color: Option<Color>,
) {
let _fill = fill_color.unwrap_or(Color::from_hex("#4CAF50").unwrap());
let _bg = bg_color.unwrap_or(Color::from_hex("#333333").unwrap());
let _clamped_progress = progress.clamp(0.0, 1.0);
}