373 lines
10 KiB
Rust
373 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>,
|
|
}
|
|
|
|
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, 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();
|
|
|
|
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 != 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 != 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 = 0.05;
|
|
const HEAD_HEIGHT: f32 = 0.1;
|
|
const TAB_WIDTH: f32 = 0.1;
|
|
const TAB_HEIGHT: f32 = 0.15;
|
|
const TAB_NUM: usize = 6;
|
|
const SEPARATOR_WIDTH: f32 = 0.02;
|
|
const INNER_RADIUS: f32 = 0.01;
|
|
const CONTENT_WIDTH: f32 = 0.64;
|
|
|
|
const HEAD_BUTTON_STYLE: RoundButtonStyle = RoundButtonStyle {
|
|
radius: Self::HEAD_RADIUS * 0.5,
|
|
spacing: Self::HEAD_RADIUS * 0.2,
|
|
thickness: Self::HEAD_RADIUS * 0.1,
|
|
body_color: Color::WHITE,
|
|
ring_color: Color::BLACK,
|
|
icon_color: Color::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 + 1) 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 {
|
|
bl: Vec2::new(Self::TAB_WIDTH, -tab_list_height),
|
|
tr: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, 0.0),
|
|
};
|
|
|
|
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 {
|
|
bl: Vec2::ZERO,
|
|
tr: Vec2::new(head_width, Self::HEAD_HEIGHT),
|
|
};
|
|
|
|
let view_rect = Rect {
|
|
bl: Vec2::new(Self::TAB_WIDTH + Self::SEPARATOR_WIDTH, -tab_list_height),
|
|
tr: Vec2::new(head_rect.tr.x, 0.0),
|
|
};
|
|
|
|
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.bl);
|
|
|
|
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) {
|
|
let head_color = Color {
|
|
r: 1.0,
|
|
g: 1.0,
|
|
b: 1.0,
|
|
a: 1.0,
|
|
};
|
|
|
|
ctx.draw_partially_rounded_rect(
|
|
CornerFlags::BOTTOM_RIGHT,
|
|
self.separator_rect,
|
|
Self::INNER_RADIUS,
|
|
head_color,
|
|
);
|
|
|
|
ctx.draw_partially_rounded_rect(
|
|
CornerFlags::TOP_LEFT | CornerFlags::TOP_RIGHT,
|
|
self.head_rect,
|
|
Self::HEAD_RADIUS,
|
|
head_color,
|
|
);
|
|
}
|
|
}
|