// 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 { pub widget: T, pub slide_anim: Animation, pub opacity_anim: Animation, } pub struct SlotMenu { pub buttons: Vec>, pub spacing: f32, pub scroll_anim: Animation, pub state: SlotMenuState, pub selected: usize, pub was_clicked: Option, pub event: SlotMenuEvent, } impl SlotMenu { pub fn new(buttons: Vec, 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 { 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, 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 Widget for SlotMenu { 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, search: Offset, tabs: Vec, view: Offset>, 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, ); } }