use uuid::Uuid;
use vek::Vec4;
use crate::ui::{
drawable::Drawable,
event::{UiAction, UiEvent, UiEventKind, UiEventOutcome},
layouts::Layoutable,
workspace::{NodeId, UiView, ViewContext},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonKind {
Momentary,
Toggle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PopupAlignment {
Right,
Left,
Top,
Bottom,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Debug, Clone)]
pub struct ButtonStyle {
pub rect: [f32; 4], pub fill: Vec4<f32>,
pub border: Vec4<f32>,
pub pressed_fill: Vec4<f32>,
pub pressed_border: Vec4<f32>,
pub radius_px: f32, pub border_px: f32, pub layer: i32,
pub text_color: Vec4<f32>, pub icon_tint: Vec4<f32>, }
impl ButtonStyle {
pub fn with_id(self, id: impl Into<String>) -> Button {
Button::new(self).with_id(id)
}
}
impl Default for ButtonStyle {
fn default() -> Self {
Self {
rect: [10.0, 10.0, 120.0, 44.0],
fill: Vec4::new(0.15, 0.15, 0.18, 1.0),
border: Vec4::new(0.0, 0.0, 0.0, 0.0),
pressed_fill: Vec4::new(0.12, 0.12, 0.15, 1.0),
pressed_border: Vec4::new(0.0, 0.0, 0.0, 0.0),
radius_px: 4.0,
border_px: 0.0,
layer: 10,
text_color: Vec4::new(0.9, 0.9, 0.95, 1.0),
icon_tint: Vec4::new(1.0, 1.0, 1.0, 1.0), }
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ButtonState {
Idle,
Hover,
Pressed,
On,
}
#[derive(Debug, Clone)]
pub struct Button {
pub id: String,
render_id: Uuid, pub style: ButtonStyle,
pub kind: ButtonKind,
pub tile_id: Option<Uuid>, pub pressed_tile_id: Option<Uuid>, pub tile_offset: f32, pub tile_tint: Vec4<f32>, toggled: bool,
state: ButtonState,
active_pointer: Option<u32>,
pub popup_content: Option<NodeId>, pub popup_alignment: PopupAlignment, pub popup_visible: bool, }
impl Button {
pub fn new(style: ButtonStyle) -> Self {
let icon_tint = style.icon_tint;
Self {
id: String::new(),
render_id: Uuid::new_v4(),
style,
kind: ButtonKind::Momentary,
tile_id: None,
pressed_tile_id: None,
tile_offset: 0.0,
tile_tint: icon_tint, toggled: false,
state: ButtonState::Idle,
active_pointer: None,
popup_content: None,
popup_alignment: PopupAlignment::Bottom,
popup_visible: false,
}
}
pub fn with_popup(mut self, content: NodeId, alignment: PopupAlignment) -> Self {
self.popup_content = Some(content);
self.popup_alignment = alignment;
self
}
pub fn toggle_popup(&mut self) {
self.popup_visible = !self.popup_visible;
}
pub fn show_popup(&mut self) {
self.popup_visible = true;
}
pub fn hide_popup(&mut self) {
self.popup_visible = false;
}
pub fn is_popup_visible(&self) -> bool {
self.popup_visible
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = id.into();
self
}
pub fn with_kind(mut self, kind: ButtonKind) -> Self {
self.kind = kind;
self
}
pub fn with_tile(mut self, tile_id: Uuid) -> Self {
self.tile_id = Some(tile_id);
self
}
pub fn with_pressed_tile(mut self, tile_id: Uuid) -> Self {
self.pressed_tile_id = Some(tile_id);
self
}
pub fn with_tile_offset(mut self, offset: f32) -> Self {
self.tile_offset = offset;
self
}
pub fn with_tile_tint(mut self, tint: Vec4<f32>) -> Self {
self.tile_tint = tint;
self
}
pub fn set_tile_tint(&mut self, tint: Vec4<f32>) {
self.tile_tint = tint;
}
pub fn set_toggled(&mut self, toggled: bool) {
if self.kind == ButtonKind::Toggle {
self.toggled = toggled;
self.state = if toggled {
ButtonState::On
} else {
ButtonState::Idle
};
}
}
pub fn is_toggled(&self) -> bool {
self.toggled
}
pub fn calculate_popup_position(
&self,
popup_size: [f32; 2],
screen_size: [f32; 2],
) -> [f32; 2] {
let [btn_x, btn_y, btn_w, btn_h] = self.style.rect;
let [popup_w, popup_h] = popup_size;
let [screen_w, screen_h] = screen_size;
let gap = 4.0;
let (mut x, mut y) = match self.popup_alignment {
PopupAlignment::Right => (btn_x + btn_w + gap, btn_y),
PopupAlignment::Left => (btn_x - popup_w - gap, btn_y),
PopupAlignment::Bottom => (btn_x, btn_y + btn_h + gap),
PopupAlignment::Top => (btn_x, btn_y - popup_h - gap),
PopupAlignment::TopLeft => (btn_x + btn_w - popup_w, btn_y - popup_h - gap),
PopupAlignment::TopRight => (btn_x, btn_y - popup_h - gap), PopupAlignment::BottomLeft => (btn_x + btn_w - popup_w, btn_y + btn_h + gap),
PopupAlignment::BottomRight => (btn_x + btn_w + gap, btn_y + btn_h + gap),
};
x = x.max(0.0).min(screen_w - popup_w);
y = y.max(0.0).min(screen_h - popup_h);
[x, y]
}
fn hit(&self, pos: [f32; 2]) -> bool {
let [x, y, w, h] = self.style.rect;
pos[0] >= x && pos[0] <= x + w && pos[1] >= y && pos[1] <= y + h
}
fn set_state(&mut self, next: ButtonState) -> UiEventOutcome {
if self.state != next {
self.state = next;
UiEventOutcome::dirty()
} else {
UiEventOutcome::none()
}
}
}
impl UiView for Button {
fn build(&mut self, ctx: &mut ViewContext) {
let (fill, border) = match self.state {
ButtonState::Pressed | ButtonState::On => {
(self.style.pressed_fill, self.style.pressed_border)
}
_ => (self.style.fill, self.style.border),
};
ctx.push(Drawable::Rect {
id: self.render_id,
rect: self.style.rect,
fill,
border,
radius_px: self.style.radius_px,
border_px: self.style.border_px,
layer: self.style.layer,
});
let tile_to_render = match self.state {
ButtonState::Pressed | ButtonState::On => self.pressed_tile_id.or(self.tile_id),
_ => self.tile_id,
};
if let Some(tile_id) = tile_to_render {
let [x, y, w, h] = self.style.rect;
let offset = self.tile_offset;
let tile_rect = [x + offset, y + offset, w - offset * 2.0, h - offset * 2.0];
ctx.push(Drawable::Quad {
id: Uuid::new_v4(),
tile_id,
rect: tile_rect,
uv: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
layer: self.style.layer + 1,
tint: self.tile_tint,
});
}
}
fn handle_event(&mut self, evt: &UiEvent) -> UiEventOutcome {
let inside = self.hit(evt.pos);
match evt.kind {
UiEventKind::PointerDown => {
if inside {
self.active_pointer = Some(evt.pointer_id);
return self.set_state(ButtonState::Pressed);
}
}
UiEventKind::PointerUp => {
let was_active = self.active_pointer == Some(evt.pointer_id);
self.active_pointer = None;
if was_active {
match self.kind {
ButtonKind::Momentary => {
let mut outcome = self.set_state(ButtonState::Idle);
if inside {
if self.popup_content.is_some() {
self.toggle_popup();
}
outcome.merge(UiEventOutcome::with_action(
UiAction::ButtonPressed(self.id.clone()),
));
}
return outcome;
}
ButtonKind::Toggle => {
if inside {
self.toggled = !self.toggled;
let mut outcome = self.set_state(if self.toggled {
ButtonState::On
} else {
ButtonState::Idle
});
outcome.merge(UiEventOutcome::with_action(
UiAction::ButtonToggled(self.id.clone(), self.toggled),
));
return outcome;
} else {
return self.set_state(if self.toggled {
ButtonState::On
} else {
ButtonState::Idle
});
}
}
}
}
}
UiEventKind::PointerMove => {
if let Some(pid) = self.active_pointer {
if pid == evt.pointer_id {
return self.set_state(if inside {
ButtonState::Pressed
} else if self.kind == ButtonKind::Toggle && self.toggled {
ButtonState::On
} else {
ButtonState::Idle
});
}
}
}
_ => {}
}
UiEventOutcome::none()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn view_id(&self) -> &str {
&self.id
}
}
impl Layoutable for Button {
fn set_layout_rect(&mut self, rect: [f32; 4]) {
self.style.rect = rect;
}
fn get_desired_size(&self) -> Option<[f32; 2]> {
let [_x, _y, w, h] = self.style.rect;
Some([w, h])
}
}