sao-ui-rs/src/widgets.rs

571 lines
16 KiB
Rust

use crate::anim::Animation;
use crate::draw::{Corner, DrawContext};
use crate::{Color, CursorEventKind, Vec2};
use keyframe::functions::*;
pub trait Widget {
fn update(&mut self, dt: f32);
fn draw(&mut self, ctx: &DrawContext);
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2);
}
pub trait Button {
fn was_clicked(&self) -> bool;
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ButtonState {
Idle,
Clicking,
Clicked,
Releasing,
}
pub struct RoundButton {
pub pos: Vec2,
pub radius: f32,
pub spacing: f32,
pub thickness: f32,
pub shrink_anim: Animation<EaseOutQuint>,
pub was_clicked: bool,
pub state: ButtonState,
}
impl Default for RoundButton {
fn default() -> Self {
Self {
pos: Default::default(),
radius: 0.02,
spacing: 0.005,
thickness: 0.001,
shrink_anim: Animation::new(EaseOutQuint, 0.1, 1.0, 0.0),
was_clicked: false,
state: ButtonState::Idle,
}
}
}
impl Button for RoundButton {
fn was_clicked(&self) -> bool {
self.was_clicked
}
}
impl Widget for RoundButton {
fn update(&mut self, dt: f32) {
self.shrink_anim.update(dt);
self.was_clicked = false;
}
fn draw(&mut self, ctx: &DrawContext) {
let color = Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
let spacing = self.shrink_anim.get() * self.spacing;
ctx.draw_circle(self.pos, self.radius, color);
ctx.draw_ring(self.pos, self.radius + spacing, self.thickness, color);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
match kind {
CursorEventKind::Select => {
if self.pos.distance(at) < self.radius {
self.shrink_anim.ease_in();
self.state = ButtonState::Clicked;
}
}
CursorEventKind::Deselect => {
if self.state == ButtonState::Clicked {
self.shrink_anim.ease_out();
self.was_clicked = true;
self.state = ButtonState::Idle;
}
}
_ => {}
}
}
}
pub struct RectButton {
pub pos: Vec2,
pub size: Vec2,
pub was_clicked: bool,
pub color: Color,
}
impl Button for RectButton {
fn was_clicked(&self) -> bool {
self.was_clicked
}
}
impl RectButton {
pub fn new(pos: Vec2, size: Vec2) -> Self {
Self {
pos,
size,
was_clicked: true,
color: Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 0.2,
},
}
}
}
impl Widget for RectButton {
fn update(&mut self, dt: f32) {
self.was_clicked = false;
}
fn draw(&mut self, ctx: &DrawContext) {
ctx.draw_rect(self.pos, self.size, self.color);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
let local = at - self.pos;
if kind == CursorEventKind::Deselect
&& local.x > 0.0
&& local.y > 0.0
&& local.x < self.size.x
&& local.y < self.size.y
{
self.was_clicked = true;
}
}
}
#[derive(Eq, PartialEq)]
pub enum ScrollMenuState {
Opening,
Idle,
Scrolling,
Closing,
Closed,
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum ScrollMenuEvent {
None,
SubmenuOpen(usize),
SubmenuClose(usize),
}
pub struct ScrollMenuButton<T> {
pub widget: T,
pub slide_anim: Animation<EaseOut>,
pub opacity_anim: Animation<EaseOut>,
}
pub struct ScrollMenu<T> {
pub buttons: Vec<ScrollMenuButton<T>>,
pub spacing: f32,
pub scroll_anim: Animation<EaseInOutQuint>,
pub state: ScrollMenuState,
pub selected: usize,
pub was_clicked: Option<usize>,
pub event: ScrollMenuEvent,
}
impl<T> ScrollMenu<T> {
pub fn new(buttons: Vec<T>, spacing: f32) -> Self {
let inter_button_delay = 0.05;
let max_delay = buttons.len() as f32 * inter_button_delay;
let buttons: Vec<_> = buttons
.into_iter()
.enumerate()
.map(|(i, widget)| {
let duration = 0.25;
let out_delay = i as f32 * inter_button_delay;
let in_delay = max_delay - out_delay;
let mut slide_anim = Animation::new(EaseOut, duration, 0.25, 0.0);
slide_anim.set_in_delay(in_delay);
slide_anim.set_out_delay(out_delay);
slide_anim.ease_in();
let mut opacity_anim = Animation::new(EaseOut, duration, 0.0, 1.0);
opacity_anim.set_in_delay(in_delay);
opacity_anim.set_out_delay(out_delay);
opacity_anim.ease_in();
ScrollMenuButton {
widget,
slide_anim,
opacity_anim,
}
})
.collect();
Self {
buttons,
spacing,
scroll_anim: Animation::new(EaseInOutQuint, 0.25, 0.0, 0.0),
state: ScrollMenuState::Opening,
selected: 0,
was_clicked: None,
event: ScrollMenuEvent::None,
}
}
pub fn get_event(&self) -> ScrollMenuEvent {
self.event
}
pub fn get_was_clicked(&self) -> Option<usize> {
self.was_clicked
}
pub fn select(&mut self, button_id: usize) {
self.scroll_anim.ease_to(button_id as f32);
self.state = ScrollMenuState::Scrolling;
self.event = ScrollMenuEvent::SubmenuClose(self.selected);
self.selected = button_id;
}
pub fn close(&mut self) {
self.state = ScrollMenuState::Closing;
self.event = ScrollMenuEvent::SubmenuClose(self.selected);
for button in self.buttons.iter_mut() {
button.slide_anim.ease_out();
button.opacity_anim.ease_out();
}
}
pub fn for_buttons(&mut self, mut cb: impl FnMut(&mut ScrollMenuButton<T>, usize, f32)) {
for (i, button) in self.buttons.iter_mut().enumerate() {
let y = -(i as f32 - self.scroll_anim.get()) * self.spacing + button.slide_anim.get();
cb(button, i, y);
}
}
fn animate_buttons(&mut self, dt: f32) -> bool {
let mut all_slid = true;
self.for_buttons(|button, _i, _y| {
button.slide_anim.update(dt);
button.opacity_anim.update(dt);
if button.slide_anim.is_active() {
all_slid = false;
}
});
all_slid
}
}
impl<T: Widget + Button> Widget for ScrollMenu<T> {
fn update(&mut self, dt: f32) {
self.event = ScrollMenuEvent::None;
self.was_clicked = None;
match self.state {
ScrollMenuState::Opening => {
if self.animate_buttons(dt) {
self.state = ScrollMenuState::Idle;
self.event = ScrollMenuEvent::SubmenuOpen(self.selected);
}
}
ScrollMenuState::Idle => {}
ScrollMenuState::Scrolling => {
self.for_buttons(|button, _i, _y| {
button.widget.update(dt);
});
self.scroll_anim.update(dt);
if !self.scroll_anim.is_active() {
self.state = ScrollMenuState::Idle;
self.event = ScrollMenuEvent::SubmenuOpen(self.selected);
}
}
ScrollMenuState::Closing => {
if self.animate_buttons(dt) {
self.state = ScrollMenuState::Closed;
}
}
ScrollMenuState::Closed => {
return;
}
}
self.for_buttons(|button, _i, _y| {
button.widget.update(dt);
});
}
fn draw(&mut self, ctx: &DrawContext) {
if self.state == ScrollMenuState::Closed {
return;
}
self.for_buttons(|button, _i, y| {
let ctx = ctx.with_offset(Vec2::new(0.0, y));
let opacity = button.opacity_anim.get();
let ctx = if opacity != 1.0 {
ctx.with_opacity(opacity)
} else {
ctx
};
button.widget.draw(&ctx);
})
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
if self.state != ScrollMenuState::Idle {
return;
}
let mut was_clicked = None;
self.for_buttons(|button, i, y| {
let at = at - Vec2::new(0.0, y);
button.widget.on_cursor_event(kind, at);
if button.widget.was_clicked() {
was_clicked = Some(i);
}
});
if let Some(clicked) = was_clicked {
if clicked != self.selected {
self.was_clicked = Some(clicked);
}
}
}
}
pub struct Reveal<T> {
inner: T,
slide_anim: Animation<EaseIn>,
opacity_anim: Animation<EaseOut>,
state: bool,
}
impl<T: Widget> Reveal<T> {
pub fn new(inner: T, slide: f32, duration: f32) -> Self {
Self {
inner,
slide_anim: Animation::new(EaseIn, duration, slide, 0.0),
opacity_anim: Animation::new(EaseOut, duration, 0.0, 1.0),
state: false,
}
}
pub fn get_offset(&self) -> Vec2 {
Vec2::new(self.slide_anim.get(), 0.0)
}
pub fn show(&mut self) {
self.state = true;
self.slide_anim.ease_in();
self.opacity_anim.ease_in();
}
pub fn hide(&mut self) {
self.state = false;
self.slide_anim.ease_out();
self.opacity_anim.ease_out();
}
}
impl<T: Widget> Widget for Reveal<T> {
fn update(&mut self, dt: f32) {
self.slide_anim.update(dt);
self.opacity_anim.update(dt);
self.inner.update(dt);
}
fn draw(&mut self, ctx: &DrawContext) {
let ctx = ctx.with_offset(Vec2::new(self.slide_anim.get(), 0.0));
let ctx = if self.opacity_anim.is_active() {
ctx.with_opacity(self.opacity_anim.get())
} else if self.state {
ctx
} else {
return;
};
self.inner.draw(&ctx);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, mut at: Vec2) {
if !self.state || self.opacity_anim.is_active() {
return;
}
at -= Vec2::new(self.slide_anim.get(), 0.0);
self.inner.on_cursor_event(kind, at);
}
}
pub struct MainMenu {
pub menu: ScrollMenu<RoundButton>,
pub inventory: Reveal<Inventory>,
}
impl Default for MainMenu {
fn default() -> Self {
let mut buttons = Vec::new();
for _ in 0..5 {
let button = RoundButton::default();
buttons.push(button);
}
let inventory = Inventory::new();
let inventory = Reveal::new(inventory, -0.02, 0.1);
Self {
menu: ScrollMenu::new(buttons, 0.1),
inventory,
}
}
}
impl Widget for MainMenu {
fn update(&mut self, dt: f32) {
match self.menu.get_was_clicked() {
None => {}
Some(4) => self.menu.close(),
Some(button) => self.menu.select(button),
};
match self.menu.get_event() {
ScrollMenuEvent::SubmenuOpen(0) => self.inventory.show(),
ScrollMenuEvent::SubmenuClose(0) => self.inventory.hide(),
_ => {}
};
self.menu.update(dt);
self.inventory.update(dt);
}
fn draw(&mut self, ctx: &DrawContext) {
self.menu.draw(&ctx);
let inventory_ctx = ctx.with_offset(Vec2::new(0.05, 0.0));
self.inventory.draw(&inventory_ctx);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
self.menu.on_cursor_event(kind, at);
}
}
pub struct Inventory {
tabs: Vec<RectButton>,
}
impl Inventory {
const HEAD_RADIUS: f32 = 0.025;
const HEAD_HEIGHT: f32 = 0.03;
const TAB_WIDTH: f32 = 0.04;
const TAB_HEIGHT: f32 = 0.06;
const TAB_NUM: usize = 6;
const SEPARATOR_WIDTH: f32 = 0.015;
const BOX_SIZE: f32 = 0.025;
const BOX_MARGIN: f32 = 0.01;
const BOX_PADDING: f32 = 0.005;
const BOX_GRID_WIDTH: usize = 12;
pub fn new() -> Self {
let tab_size = Vec2::new(Self::TAB_WIDTH, Self::TAB_HEIGHT);
let mut tabs = Vec::new();
for i in 0..Self::TAB_NUM {
let y = (i + 1) as f32 * Self::TAB_HEIGHT;
let pos = Vec2::new(0.0, -y);
tabs.push(RectButton::new(pos, tab_size));
}
Self { tabs }
}
}
impl Widget for Inventory {
fn update(&mut self, dt: f32) {
for tab in self.tabs.iter_mut() {
tab.update(dt);
}
}
fn draw(&mut self, ctx: &DrawContext) {
for tab in self.tabs.iter_mut() {
tab.draw(ctx);
}
let head_color = Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
// alignment variables
let tab_list_height = Self::TAB_NUM as f32 * Self::TAB_HEIGHT;
let separator_xy = Vec2::new(Self::TAB_WIDTH, Self::SEPARATOR_WIDTH - tab_list_height);
let separator_size = Vec2::new(
Self::SEPARATOR_WIDTH,
tab_list_height - Self::SEPARATOR_WIDTH,
);
let body_width = Self::BOX_GRID_WIDTH as f32 * (Self::BOX_SIZE + Self::BOX_PADDING)
+ Self::BOX_MARGIN * 2.0;
let body_height = tab_list_height - Self::BOX_MARGIN * 2.0;
let head_width = Self::TAB_WIDTH + Self::SEPARATOR_WIDTH + body_width;
let head_inner_xy = Vec2::ZERO;
let head_inner_size = Vec2::new(head_width, Self::HEAD_HEIGHT - Self::HEAD_RADIUS);
let head_edge_xy = Vec2::new(Self::HEAD_RADIUS, Self::HEAD_HEIGHT - Self::HEAD_RADIUS);
let head_edge_size = Vec2::new(head_width - Self::HEAD_RADIUS * 2.0, Self::HEAD_RADIUS);
let head_tl_xy = head_edge_xy;
let head_tr_xy = head_tl_xy + Vec2::new(head_edge_size.x, 0.0);
// draw shapes
ctx.draw_rect(separator_xy, separator_size, head_color);
ctx.draw_quarter_circle(
Corner::BottomRight,
separator_xy,
Self::SEPARATOR_WIDTH,
head_color,
);
ctx.draw_rect(head_inner_xy, head_inner_size, head_color);
ctx.draw_rect(head_edge_xy, head_edge_size, head_color);
ctx.draw_quarter_circle(Corner::TopLeft, head_tl_xy, Self::HEAD_RADIUS, head_color);
ctx.draw_quarter_circle(Corner::TopRight, head_tr_xy, Self::HEAD_RADIUS, head_color);
// placeholder inventory item boxes
let box_grid_stride = Self::BOX_SIZE + Self::BOX_PADDING;
let box_grid_height = (body_height / box_grid_stride).floor() as usize;
let box_rect_size = Vec2::new(Self::BOX_SIZE, Self::BOX_SIZE);
let box_grid_xy = Vec2::new(
Self::TAB_WIDTH + Self::SEPARATOR_WIDTH + Self::BOX_MARGIN,
-Self::BOX_MARGIN - Self::BOX_SIZE,
);
let box_radius = 0.005;
for x in 0..Self::BOX_GRID_WIDTH {
for y in 0..box_grid_height {
let box_rect_xy = Vec2::new(x as f32, -(y as f32)) * box_grid_stride + box_grid_xy;
ctx.draw_rounded_rect(box_rect_xy, box_rect_size, box_radius, head_color);
}
}
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
for tab in self.tabs.iter_mut() {
tab.on_cursor_event(kind, at);
}
}
}