canary-rs/scripts/sao-ui/src/widgets/menu.rs

367 lines
10 KiB
Rust

// Copyright (c) 2022 Marceline Cramer
// SPDX-License-Identifier: AGPL-3.0-or-later
use super::prelude::*;
use crate::main_menu::Inventory;
use button::{RectButton, RectButtonStyle, RoundButton, RoundButtonStyle};
use scroll::{ScrollBar, ScrollView};
use shell::Offset;
#[derive(Eq, PartialEq)]
pub enum SlotMenuState {
Opening,
Idle,
Scrolling,
Closing,
Closed,
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum SlotMenuEvent {
None,
SubmenuOpen(usize),
SubmenuClose(usize),
}
pub struct SlotMenuButton<T> {
pub widget: T,
pub slide_anim: Animation<EaseOut>,
pub opacity_anim: Animation<EaseOut, u8>,
}
pub struct SlotMenu<T> {
pub buttons: Vec<SlotMenuButton<T>>,
pub spacing: f32,
pub scroll_anim: Animation<EaseInOutQuint>,
pub state: SlotMenuState,
pub selected: usize,
pub was_clicked: Option<usize>,
pub event: SlotMenuEvent,
}
impl<T> SlotMenu<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, -50.0, 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, 0xff);
opacity_anim.set_in_delay(in_delay);
opacity_anim.set_out_delay(out_delay);
opacity_anim.ease_in();
SlotMenuButton {
widget,
slide_anim,
opacity_anim,
}
})
.collect();
Self {
buttons,
spacing,
scroll_anim: Animation::new(EaseInOutQuint, 0.25, 0.0, 0.0),
state: SlotMenuState::Opening,
selected: 0,
was_clicked: None,
event: SlotMenuEvent::None,
}
}
pub fn get_event(&self) -> SlotMenuEvent {
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 = SlotMenuState::Scrolling;
self.event = SlotMenuEvent::SubmenuClose(self.selected);
self.selected = button_id;
}
pub fn close(&mut self) {
self.state = SlotMenuState::Closing;
self.event = SlotMenuEvent::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 SlotMenuButton<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 SlotMenu<T> {
fn update(&mut self, dt: f32) {
self.event = SlotMenuEvent::None;
self.was_clicked = None;
match self.state {
SlotMenuState::Opening => {
if self.animate_buttons(dt) {
self.state = SlotMenuState::Idle;
self.event = SlotMenuEvent::SubmenuOpen(self.selected);
}
}
SlotMenuState::Idle => {}
SlotMenuState::Scrolling => {
self.for_buttons(|button, _i, _y| {
button.widget.update(dt);
});
self.scroll_anim.update(dt);
if !self.scroll_anim.is_active() {
self.state = SlotMenuState::Idle;
self.event = SlotMenuEvent::SubmenuOpen(self.selected);
}
}
SlotMenuState::Closing => {
if self.animate_buttons(dt) {
self.state = SlotMenuState::Closed;
}
}
SlotMenuState::Closed => {
return;
}
}
self.for_buttons(|button, _i, _y| {
button.widget.update(dt);
});
}
fn draw(&mut self, ctx: &DrawContext) {
if self.state == SlotMenuState::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 != u8::MAX {
ctx.with_opacity(opacity)
} else {
ctx
};
button.widget.draw(&ctx);
})
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
if self.state != SlotMenuState::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 TabMenu {
pop_out: Offset<RoundButton>,
search: Offset<RoundButton>,
tabs: Vec<RectButton>,
view: Offset<ScrollView<Inventory>>,
head_rect: Rect,
separator_rect: Rect,
}
impl TabMenu {
const HEAD_RADIUS: f32 = 5.0;
const HEAD_HEIGHT: f32 = 15.0;
const HEAD_COLOR: Color = THEME.palette.surface;
const TAB_WIDTH: f32 = 15.0;
const TAB_HEIGHT: f32 = 25.0;
const TAB_NUM: usize = 6;
const SEPARATOR_WIDTH: f32 = 5.0;
const INNER_RADIUS: f32 = 5.0;
const CONTENT_WIDTH: f32 = 100.0;
const HEAD_BUTTON_STYLE: RoundButtonStyle = RoundButtonStyle {
radius: Self::HEAD_HEIGHT * 0.25,
spacing: Self::HEAD_HEIGHT * 0.1,
thickness: Self::HEAD_HEIGHT * 0.05,
body_color: Self::HEAD_COLOR,
ring_color: THEME.palette.black,
icon_color: THEME.palette.black,
};
const HEAD_BUTTON_MARGIN: f32 = Self::HEAD_HEIGHT / 2.0;
const HEAD_BUTTON_SPACING: f32 = Self::HEAD_HEIGHT;
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 as f32 * Self::TAB_HEIGHT;
let pos = Vec2::new(0.0, y);
let mut style = RectButtonStyle::default();
style.radius = Self::HEAD_RADIUS;
if i == Self::TAB_NUM - 1 {
style.rounded_corners = CornerFlags::BOTTOM_LEFT;
}
let rect = Rect::from_xy_size(pos, tab_size);
tabs.push(RectButton::new(style, rect, None, None));
}
let icon_font = Font::new(crate::ICON_FONT);
let pop_out = RoundButton::new(
Self::HEAD_BUTTON_STYLE,
Some(text::LabelText {
font: icon_font,
text: "".to_string(),
}),
);
let search = RoundButton::new(
Self::HEAD_BUTTON_STYLE,
Some(text::LabelText {
font: icon_font,
text: "".to_string(),
}),
);
let head_button_y = -Self::HEAD_HEIGHT / 2.0;
let pop_out_x = Self::HEAD_BUTTON_MARGIN;
let pop_out = Offset::new(pop_out, Vec2::new(pop_out_x, head_button_y));
let search_x = pop_out_x + Self::HEAD_BUTTON_SPACING;
let search = Offset::new(search, Vec2::new(search_x, head_button_y));
let tab_list_height = Self::TAB_NUM as f32 * Self::TAB_HEIGHT;
let scroll_bar = ScrollBar::new(tab_list_height, tab_list_height * 3.0, Default::default());
let scroll_x = Self::TAB_WIDTH + Self::SEPARATOR_WIDTH + Self::CONTENT_WIDTH;
let scroll_bar = Offset::new(scroll_bar, Vec2::new(scroll_x, -tab_list_height));
let separator_rect = Rect {
tl: Vec2::new(Self::TAB_WIDTH, 0.0),
br: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, tab_list_height),
};
let head_width = Self::TAB_WIDTH
+ Self::SEPARATOR_WIDTH
+ Self::CONTENT_WIDTH
+ scroll_bar.style.body_width
+ scroll_bar.style.margin.x * 2.0;
let head_rect = Rect {
tl: Vec2::new(0.0, -Self::HEAD_HEIGHT),
br: Vec2::new(head_width, 0.0),
};
let view_rect = Rect {
tl: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, 0.0),
br: Vec2::new(head_rect.br.x, tab_list_height),
};
let view = ScrollView::new(
Default::default(),
view_rect.width(),
view_rect.height(),
|available_width: f32| Inventory::new(available_width),
);
let view = Offset::new(view, view_rect.tl);
Self {
pop_out,
search,
tabs,
view,
separator_rect,
head_rect,
}
}
}
impl Container for TabMenu {
fn with_children(&mut self, mut f: impl FnMut(&mut dyn Widget)) {
f(&mut self.pop_out);
f(&mut self.search);
for tab in self.tabs.iter_mut() {
f(tab);
}
f(&mut self.view);
}
fn draw(&mut self, ctx: &DrawContext) {
ctx.draw_partially_rounded_rect(
CornerFlags::BOTTOM_RIGHT,
self.separator_rect,
Self::INNER_RADIUS,
Self::HEAD_COLOR,
);
ctx.draw_partially_rounded_rect(
CornerFlags::TOP,
self.head_rect,
Self::HEAD_RADIUS,
Self::HEAD_COLOR,
);
}
}