Refactor SAO UI widgets
This commit is contained in:
parent
2e36ef9851
commit
ca1841af10
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,199 @@
|
|||
use super::*;
|
||||
|
||||
pub trait Button {
|
||||
fn was_clicked(&self) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ButtonState {
|
||||
Idle,
|
||||
Clicking,
|
||||
Clicked,
|
||||
Releasing,
|
||||
}
|
||||
|
||||
pub struct RoundButton {
|
||||
pos: Vec2,
|
||||
radius: f32,
|
||||
spacing: f32,
|
||||
thickness: f32,
|
||||
shrink_anim: Animation<EaseOutQuint>,
|
||||
was_clicked: bool,
|
||||
state: ButtonState,
|
||||
label: Option<Label>,
|
||||
}
|
||||
|
||||
impl RoundButton {
|
||||
pub fn new(label: Option<LabelText>) -> Self {
|
||||
let radius = 0.05;
|
||||
|
||||
let label = label.map(|text| {
|
||||
let scale = radius * 1.5;
|
||||
let alignment = HorizontalAlignment::Center;
|
||||
let left = -radius;
|
||||
let right = radius;
|
||||
let baseline = 0.0;
|
||||
let color = Color::BLACK;
|
||||
let center_y = true;
|
||||
|
||||
Label::new(
|
||||
text, alignment, scale, color, left, right, baseline, center_y,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
pos: Default::default(),
|
||||
radius,
|
||||
spacing: 0.01,
|
||||
thickness: 0.002,
|
||||
shrink_anim: Animation::new(EaseOutQuint, 0.1, 1.0, 0.0),
|
||||
was_clicked: false,
|
||||
state: ButtonState::Idle,
|
||||
label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if let Some(label) = self.label.as_mut() {
|
||||
label.draw(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
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<EaseInQuad, Color>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,335 @@
|
|||
use super::*;
|
||||
|
||||
#[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 {
|
||||
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;
|
||||
|
||||
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.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 {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
use crate::anim::Animation;
|
||||
use crate::draw::{CornerFlags, DrawContext, Rect};
|
||||
use crate::{Color, CursorEventKind, Vec2};
|
||||
use canary_script::{Font, GlyphPosition, TextLayout};
|
||||
use keyframe::functions::*;
|
||||
|
||||
pub mod button;
|
||||
pub mod menu;
|
||||
pub mod scroll;
|
||||
pub mod shell;
|
||||
pub mod text;
|
||||
|
||||
use button::{Button, RectButton, RoundButton};
|
||||
use scroll::{ScrollBar, ScrollView};
|
||||
use shell::{Offset, Reveal};
|
||||
use text::{HorizontalAlignment, Label, LabelText};
|
||||
use menu::{SlotMenu, SlotMenuEvent, TabMenu};
|
||||
|
||||
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 FixedWidth {
|
||||
fn get_width(&self) -> f32;
|
||||
}
|
||||
|
||||
pub struct MainMenu {
|
||||
pub menu: SlotMenu<RoundButton>,
|
||||
pub inventory: Reveal<Offset<TabMenu>>,
|
||||
}
|
||||
|
||||
impl MainMenu {
|
||||
pub const SUBMENU_SPACING: f32 = 0.1;
|
||||
}
|
||||
|
||||
impl Default for MainMenu {
|
||||
fn default() -> Self {
|
||||
let icon_font = Font::new("Iosevka Nerd Font");
|
||||
let icons = ["", "", "", "", ""];
|
||||
|
||||
let mut buttons = Vec::new();
|
||||
for icon in icons {
|
||||
let text = LabelText {
|
||||
font: icon_font,
|
||||
text: icon.to_string(),
|
||||
};
|
||||
|
||||
let button = RoundButton::new(Some(text));
|
||||
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: SlotMenu::new(buttons, 0.2),
|
||||
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() {
|
||||
SlotMenuEvent::SubmenuOpen(0) => self.inventory.show(),
|
||||
SlotMenuEvent::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 Inventory {
|
||||
width: f32,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
impl Inventory {
|
||||
pub fn new(available_width: f32) -> (Self, f32) {
|
||||
let height = 1.28;
|
||||
|
||||
(
|
||||
Self {
|
||||
width: available_width,
|
||||
height,
|
||||
},
|
||||
height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Inventory {
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
let box_size = 0.06;
|
||||
let box_margin = 0.02;
|
||||
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;
|
||||
let grid_offset = Vec2::new(box_margin, box_margin) / 2.0;
|
||||
|
||||
for x in 0..grid_width {
|
||||
for y in 0..grid_height {
|
||||
let off = Vec2::new(x as f32, y as f32) * box_stride + grid_offset;
|
||||
let rect = Rect::from_xy_size(off, Vec2::new(box_size, box_size));
|
||||
let color = Color::MAGENTA;
|
||||
ctx.draw_rect(rect, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
use super::*;
|
||||
|
||||
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 {
|
||||
// TODO: accessor method/trait for styled widgets?
|
||||
pub style: ScrollBarStyle,
|
||||
|
||||
height: f32,
|
||||
is_dirty: bool,
|
||||
scroll: f32,
|
||||
content_height: f32,
|
||||
rail_rect: Rect,
|
||||
body_color_anim: Animation<EaseInQuad, Color>,
|
||||
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<T> {
|
||||
inner: Offset<T>,
|
||||
scroll_bar: Offset<ScrollBar>,
|
||||
clip_rect: Rect,
|
||||
height: f32,
|
||||
content_height: f32,
|
||||
}
|
||||
|
||||
impl<T: Widget> ScrollView<T> {
|
||||
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<T: Widget> Widget for ScrollView<T> {
|
||||
fn update(&mut self, dt: f32) {
|
||||
if self.scroll_bar.is_dirty() {
|
||||
let yoff = self.scroll_bar.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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
use super::Widget;
|
||||
use crate::anim::Animation;
|
||||
use crate::draw::DrawContext;
|
||||
use crate::{CursorEventKind, Vec2};
|
||||
use keyframe::functions::*;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
macro_rules! impl_shell_inner {
|
||||
($shell:ident) => {
|
||||
impl<T> Deref for $shell<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for $shell<T> {
|
||||
fn deref_mut(&mut self) -> &mut T {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct Reveal<T> {
|
||||
inner: T,
|
||||
slide_anim: Animation<EaseIn>,
|
||||
opacity_anim: Animation<Linear>,
|
||||
state: bool,
|
||||
}
|
||||
|
||||
impl_shell_inner!(Reveal);
|
||||
|
||||
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(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<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 Offset<T> {
|
||||
inner: T,
|
||||
offset: Vec2,
|
||||
}
|
||||
|
||||
impl_shell_inner!(Offset);
|
||||
|
||||
impl<T: Widget> Offset<T> {
|
||||
pub fn new(inner: T, offset: Vec2) -> Self {
|
||||
Self { inner, offset }
|
||||
}
|
||||
|
||||
pub fn set_offset(&mut self, offset: Vec2) {
|
||||
self.offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Widget> Widget for Offset<T> {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
use super::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum HorizontalAlignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub struct LabelText {
|
||||
pub font: Font,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
pub struct Label {
|
||||
text: LabelText,
|
||||
alignment: HorizontalAlignment,
|
||||
scale: f32,
|
||||
color: Color,
|
||||
left: f32,
|
||||
right: f32,
|
||||
baseline: f32,
|
||||
center_y: bool,
|
||||
dirty: bool,
|
||||
glyphs: Vec<GlyphPosition>,
|
||||
bounds: Rect,
|
||||
offset: Vec2,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(
|
||||
text: LabelText,
|
||||
alignment: HorizontalAlignment,
|
||||
scale: f32,
|
||||
color: Color,
|
||||
left: f32,
|
||||
right: f32,
|
||||
baseline: f32,
|
||||
center_y: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
text,
|
||||
alignment,
|
||||
scale,
|
||||
color,
|
||||
left,
|
||||
right,
|
||||
baseline,
|
||||
center_y,
|
||||
dirty: true,
|
||||
glyphs: Vec::new(),
|
||||
bounds: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||
offset: Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Label {
|
||||
fn draw(&mut self, ctx: &DrawContext) {
|
||||
if self.dirty {
|
||||
let layout = TextLayout::new(&self.text.font, &self.text.text);
|
||||
let bounds = Rect::from(layout.get_bounds()).scale(self.scale);
|
||||
self.bounds = bounds;
|
||||
self.glyphs = layout.get_glyphs();
|
||||
let xoff = match self.alignment {
|
||||
HorizontalAlignment::Left => self.left - bounds.bl.x,
|
||||
HorizontalAlignment::Right => self.right - bounds.tr.x,
|
||||
HorizontalAlignment::Center => {
|
||||
let available = self.right - self.left;
|
||||
let halfway = available / 2.0 + self.left;
|
||||
let width = bounds.tr.x - bounds.bl.x;
|
||||
let left = halfway - width / 2.0;
|
||||
left - bounds.bl.x
|
||||
}
|
||||
};
|
||||
|
||||
let yoff = if self.center_y {
|
||||
(bounds.bl.y - bounds.tr.y) / 2.0 - bounds.bl.y
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
self.offset = Vec2::new(xoff, yoff + self.baseline);
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
ctx.draw_text(&self.glyphs, self.offset, self.scale, self.color);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue