use crate::anim::Animation; use crate::draw::{CornerFlags, DrawContext, Rect}; 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; } pub trait FixedWidth { fn get_width(&self) -> f32; } #[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, 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 rect: Rect, pub rounded_corners: CornerFlags, pub radius: f32, pub was_clicked: bool, pub is_selected: bool, pub is_hovering: bool, pub color_anim: Animation, } impl Button for RectButton { fn was_clicked(&self) -> bool { self.was_clicked } } impl RectButton { pub const INACTIVE_COLOR: Color = Color::new(1., 1., 1., 0.2); pub const HOVER_COLOR: Color = Color::new(1., 1., 1., 0.8); pub const SELECTED_COLOR: Color = Color::new(1., 1., 0., 1.); pub fn new(rect: Rect, rounded_corners: CornerFlags, radius: f32) -> Self { Self { rect, rounded_corners, radius, was_clicked: false, is_selected: false, is_hovering: false, color_anim: Animation::new( EaseInQuad, 0.05, Self::INACTIVE_COLOR, Self::INACTIVE_COLOR, ), } } } impl Widget for RectButton { fn update(&mut self, dt: f32) { self.was_clicked = false; self.color_anim.update(dt); } fn draw(&mut self, ctx: &DrawContext) { ctx.draw_partially_rounded_rect( self.rounded_corners, self.rect, self.radius, self.color_anim.get(), ); } fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { let is_on = self.rect.contains_point(at); match kind { CursorEventKind::Hover | CursorEventKind::Drag => { if is_on { if !self.is_hovering && !self.is_selected { self.color_anim.ease_to(Self::HOVER_COLOR); } self.is_hovering = true; } else { if self.is_hovering && !self.is_selected { self.color_anim.ease_to(Self::INACTIVE_COLOR); } self.is_hovering = false; } } CursorEventKind::Select => { if is_on { self.is_selected = true; self.color_anim.ease_to(Self::SELECTED_COLOR); } } CursorEventKind::Deselect => { if self.is_selected { self.was_clicked = true; self.is_selected = false; if self.is_hovering { self.color_anim.ease_to(Self::HOVER_COLOR); } else { self.color_anim.ease_to(Self::INACTIVE_COLOR); } } } } } } pub struct ScrollBarStyle { pub margin: Vec2, pub body_radius: f32, pub body_width: f32, pub body_idle_color: Color, pub body_hover_color: Color, pub body_selected_color: Color, pub rail_width: f32, pub rail_color: Color, } impl Default for ScrollBarStyle { fn default() -> Self { Self { margin: Vec2::new(0.01, 0.01), body_radius: 0.005, body_width: 0.015, body_idle_color: Color::new(0.5, 0.5, 0.5, 1.0), body_hover_color: Color::new(0.8, 0.8, 0.8, 1.0), body_selected_color: Color::new(1.0, 1.0, 0.0, 1.0), rail_width: 0.005, rail_color: Color::new(0.7, 0.7, 0.7, 0.5), } } } pub struct ScrollBar { height: f32, style: ScrollBarStyle, is_dirty: bool, scroll: f32, content_height: f32, rail_rect: Rect, body_color_anim: Animation, is_hovering: bool, is_selected: bool, grab_coord: f32, grab_scroll: f32, } impl ScrollBar { pub fn new(height: f32, content_height: f32, style: ScrollBarStyle) -> Self { let center_x = style.body_width / 2.0 + style.margin.x; let rail_rect = Rect { bl: Vec2::new(center_x - style.rail_width / 2.0, style.margin.y), tr: Vec2::new(center_x + style.rail_width / 2.0, height - style.margin.y), }; let body_color_anim = Animation::new( EaseInQuad, 0.05, style.body_idle_color, style.body_idle_color, ); Self { height, style, is_dirty: true, scroll: 0.0, content_height, rail_rect, body_color_anim, is_hovering: false, is_selected: false, grab_coord: 0.0, grab_scroll: 0.0, } } pub fn get_body_rect(&self) -> Rect { let style = &self.style; let rail_height = self.rail_rect.height(); let body_height = (self.height / self.content_height) * rail_height; let body_y = rail_height - (self.scroll / self.content_height) * rail_height - body_height; let body_xy = Vec2::new(style.margin.x, body_y + style.margin.y); let body_size = Vec2::new(style.body_width, body_height); Rect::from_xy_size(body_xy, body_size) } pub fn is_dirty(&self) -> bool { self.is_dirty } pub fn get_scroll(&self) -> f32 { self.scroll } } impl Widget for ScrollBar { fn update(&mut self, dt: f32) { self.is_dirty = false; self.body_color_anim.update(dt); } fn draw(&mut self, ctx: &DrawContext) { let style = &self.style; let body_rect = self.get_body_rect(); ctx.draw_rect(self.rail_rect, style.rail_color); ctx.draw_rounded_rect(body_rect, style.body_radius, self.body_color_anim.get()); } fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { let is_on = self.get_body_rect().contains_point(at); match kind { CursorEventKind::Hover | CursorEventKind::Drag => { if is_on { if !self.is_hovering && !self.is_selected { self.body_color_anim.ease_to(self.style.body_hover_color); } self.is_hovering = true; } else { if self.is_hovering && !self.is_selected { self.body_color_anim.ease_to(self.style.body_idle_color); } self.is_hovering = false; } if kind == CursorEventKind::Drag && self.is_selected { self.scroll = ((self.grab_coord - at.y) / self.rail_rect.height()) * self.content_height + self.grab_scroll; let scroll_cap = self.content_height - self.height; if self.scroll > scroll_cap { self.scroll = scroll_cap; } else if self.scroll < 0.0 { self.scroll = 0.0; } self.is_dirty = true; } } CursorEventKind::Select => { if is_on { self.is_selected = true; self.body_color_anim.ease_to(self.style.body_selected_color); self.grab_coord = at.y; self.grab_scroll = self.scroll; } } CursorEventKind::Deselect => { if self.is_selected { self.is_selected = false; if self.is_hovering { self.body_color_anim.ease_to(self.style.body_hover_color); } else { self.body_color_anim.ease_to(self.style.body_idle_color); } } } } } } pub struct ScrollView { inner: Offset, scroll_bar: Offset, clip_rect: Rect, height: f32, content_height: f32, } impl ScrollView { pub fn new( bar_style: ScrollBarStyle, width: f32, height: f32, inner_cb: impl FnOnce(f32) -> (T, f32), ) -> Self { let content_width = width - bar_style.body_width - bar_style.margin.x * 2.0; let (inner, content_height) = inner_cb(content_width); let inner = Offset::new(inner, Vec2::ZERO); let scroll_bar_offset = Vec2::new(content_width, 0.0); let scroll_bar = ScrollBar::new(height, content_height, bar_style); let scroll_bar = Offset::new(scroll_bar, scroll_bar_offset); let clip_rect = Rect::from_xy_size(Vec2::ZERO, Vec2::new(content_width, height)); Self { inner, scroll_bar, clip_rect, content_height, height, } } } impl Widget for ScrollView { fn update(&mut self, dt: f32) { if self.scroll_bar.inner.is_dirty() { let yoff = self.scroll_bar.inner.get_scroll() - self.content_height + self.height; self.inner.set_offset(Vec2::new(0.0, yoff)); } self.inner.update(dt); self.scroll_bar.update(dt); } fn draw(&mut self, ctx: &DrawContext) { self.scroll_bar.draw(ctx); if let Some(ctx) = ctx.with_clip_rect(self.clip_rect) { self.inner.draw(&ctx); } } fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { self.inner.on_cursor_event(kind, at); self.scroll_bar.on_cursor_event(kind, at); } } #[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 { pub widget: T, pub slide_anim: Animation, pub opacity_anim: Animation, } pub struct ScrollMenu { pub buttons: Vec>, pub spacing: f32, pub scroll_anim: Animation, pub state: ScrollMenuState, pub selected: usize, pub was_clicked: Option, pub event: ScrollMenuEvent, } impl ScrollMenu { 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, 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 { 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, 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 ScrollMenu { 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 Offset { inner: T, offset: Vec2, } impl Offset { pub fn new(inner: T, offset: Vec2) -> Self { Self { inner, offset } } pub fn set_offset(&mut self, offset: Vec2) { self.offset = offset; } } impl Widget for Offset { fn update(&mut self, dt: f32) { self.inner.update(dt); } fn draw(&mut self, ctx: &DrawContext) { let ctx = ctx.with_offset(self.offset); self.inner.draw(&ctx); } fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { let at = at - self.offset; self.inner.on_cursor_event(kind, at); } } pub struct Reveal { inner: T, slide_anim: Animation, opacity_anim: Animation, state: bool, } impl Reveal { 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(Linear, 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 Widget for Reveal { 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, pub inventory: Reveal>, } impl MainMenu { pub const SUBMENU_SPACING: f32 = 0.05; } 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 = TabMenu::new(); let inventory = Offset::new(inventory, Vec2::new(Self::SUBMENU_SPACING, 0.0)); 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); self.inventory.draw(&ctx); } fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { self.menu.on_cursor_event(kind, at); self.inventory.on_cursor_event(kind, at); } } pub struct TabMenu { tabs: Vec, view: Offset>, head_rect: Rect, separator_rect: Rect, } impl TabMenu { 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 INNER_RADIUS: f32 = 0.005; const CONTENT_WIDTH: f32 = 0.4; 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 radius = Self::HEAD_RADIUS; let corners = if i == Self::TAB_NUM - 1 { CornerFlags::BOTTOM_LEFT } else { CornerFlags::empty() }; let rect = Rect::from_xy_size(pos, tab_size); tabs.push(RectButton::new(rect, corners, radius)); } 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.inner.style.body_width + scroll_bar.inner.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 { tabs, view, separator_rect, head_rect, } } } impl Widget for TabMenu { fn update(&mut self, dt: f32) { for tab in self.tabs.iter_mut() { tab.update(dt); } self.view.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, }; // draw shapes 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, ); self.view.draw(ctx); } fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { for tab in self.tabs.iter_mut() { tab.on_cursor_event(kind, at); } self.view.on_cursor_event(kind, at); } } pub struct Inventory { width: f32, height: f32, } impl Inventory { pub fn new(available_width: f32) -> (Self, f32) { let height = 1.5; ( Self { width: available_width, height, }, height, ) } } impl Widget for Inventory { fn update(&mut self, dt: f32) {} fn draw(&mut self, ctx: &DrawContext) { let box_size = 0.04; let box_margin = 0.01; let box_stride = box_size + box_margin; let grid_width = (self.width / box_stride).floor() as usize; let grid_height = (self.height / box_stride).floor() as usize; for x in 0..grid_width { for y in 0..grid_height { let off = Vec2::new(x as f32, y as f32) * box_stride; let rect = Rect::from_xy_size(off, Vec2::new(box_size, box_size)); let color = Color::MAGENTA; ctx.draw_rect(rect, color); } } } fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {} }