Initial commit

This commit is contained in:
mars 2022-07-15 15:11:35 -06:00
commit bf8592b5fe
15 changed files with 2049 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/Cargo.lock

19
Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[workspace]
members = [
"crates/egui",
"crates/sao-ui-rs",
"crates/script",
"crates/types",
]
[package]
name = "canary"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
bytemuck = "1"
canary_types = { path = "crates/types" }
parking_lot = "0.12"
wasmtime = "0.38"

8
crates/egui/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "canary_egui_harness"
version = "0.1.0"
edition = "2021"
[dependencies]
canary = { path = "../.." }
eframe = "0.18"

118
crates/egui/src/main.rs Normal file
View File

@ -0,0 +1,118 @@
use canary::ScriptInstance;
use eframe::egui;
use std::time::Instant;
fn main() {
let args: Vec<String> = std::env::args().collect();
let module_path = args
.get(1)
.expect("Please pass a path to a Canary script!")
.to_owned();
let native_options = eframe::NativeOptions {
multisampling: 8,
..Default::default()
};
eframe::run_native(
"Canary egui Harness",
native_options,
Box::new(move |cc| {
cc.egui_ctx.set_visuals(egui::Visuals::dark());
Box::new(App::new(&module_path))
}),
);
}
struct App {
script: canary::WasmtimeScript<canary::SimpleScriptAbi>,
last_update: Instant,
}
impl App {
pub fn new(module_path: &str) -> Self {
let runtime = canary::WasmtimeRuntime::new().unwrap();
let abi = canary::SimpleScriptAbi::default();
let module = std::fs::read(module_path).unwrap();
let script = runtime.load_module(abi, &module).unwrap();
Self {
script,
last_update: Instant::now(),
}
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.request_repaint();
let dt = self.last_update.elapsed().as_secs_f32();
self.last_update = Instant::now();
self.script.update(dt);
egui::Window::new("Panel").show(ctx, |ui| {
let size = egui::vec2(800.0, 800.0);
let sense = egui::Sense {
click: true,
drag: true,
focusable: false,
};
let (rect, response) = ui.allocate_at_least(size, sense);
// TODO input events
/*let input = ui.input();
for event in input.events.iter() {
let event = match event {
egui::Event::PointerMoved(pos) => {
if input.pointer.primary_down() {
Some(())
}
}
_ => None,
};
if let Some((kind, x, y)) = event {
self.script.on_cursor_event(kind, canary::Vec2 { x, y });
}
}*/
let texture = egui::TextureId::Managed(0);
let uv = egui::pos2(0.0, 0.0);
let mut mesh = egui::Mesh::with_texture(texture);
self.script.draw(|commands| {
for command in commands.iter() {
let voff = mesh.vertices.len() as u32;
match command {
canary::DrawCommand::Mesh { vertices, indices } => {
for v in vertices.iter() {
use egui::epaint::Vertex;
let pos = egui::pos2(v.position.x, -v.position.y);
let pos = pos.to_vec2() / 2.0 + egui::vec2(0.5, 0.5);
let pos = rect.left_top() + pos * rect.size();
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
let v = Vertex { pos, uv, color };
mesh.vertices.push(v);
}
for i in indices.iter() {
mesh.indices.push(i + voff);
}
}
_ => unimplemented!(),
}
}
});
let painter = ui.painter_at(rect);
let shape = egui::Shape::mesh(mesh);
painter.add(shape);
response
});
}
}

View File

@ -0,0 +1,18 @@
[package]
name = "sao-ui-rs"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
bitflags = "^1"
glam = "^0.21"
keyframe = "1"
canary_script = { path = "../script", features = ["glam"] }
wee_alloc = "^0.4"
[profile.release]
opt-level = 3
lto = "fat"

View File

@ -0,0 +1,131 @@
use keyframe::EasingFunction;
use crate::Color;
pub trait AnimationLerp<T> {
fn lerp(&self, x: f32) -> T;
}
#[derive(Clone)]
pub struct Animation<F, T = f32> {
time: f32,
duration: f32,
in_delay: f32,
out_delay: f32,
from: T,
to: T,
function: F,
direction: bool,
}
impl<F: EasingFunction, T: Copy> Animation<F, T> {
pub fn new(function: F, duration: f32, from: T, to: T) -> Self {
Self {
time: duration,
duration,
from,
to,
in_delay: 0.0,
out_delay: 0.0,
function,
direction: false,
}
}
pub fn update(&mut self, dt: f32) {
self.time += dt;
}
pub fn is_active(&self) -> bool {
self.time < self.duration
}
pub fn ease_in(&mut self) {
if !self.direction {
self.ease_toggle();
}
}
pub fn ease_out(&mut self) {
if self.direction {
self.ease_toggle();
}
}
pub fn ease_toggle(&mut self) {
if self.is_active() {
self.time = self.duration - self.time;
} else if self.direction {
self.time = -self.out_delay;
} else {
self.time = -self.in_delay;
}
self.direction = !self.direction;
}
pub fn set_in_delay(&mut self, delay: f32) {
self.in_delay = delay;
}
pub fn set_out_delay(&mut self, delay: f32) {
self.out_delay = delay;
}
pub fn clamped(&self) -> Option<T> {
if self.is_active() {
if self.time <= 0.0 {
if self.direction {
Some(self.from)
} else {
Some(self.to)
}
} else {
None
}
} else if self.direction {
Some(self.to)
} else {
Some(self.from)
}
}
}
impl<F: EasingFunction> AnimationLerp<f32> for Animation<F, f32> {
fn lerp(&self, x: f32) -> f32 {
(1.0 - x) * self.from + x * self.to
}
}
impl<F: EasingFunction> AnimationLerp<Color> for Animation<F, Color> {
fn lerp(&self, x: f32) -> Color {
let from: glam::Vec4 = self.from.into();
let to: glam::Vec4 = self.to.into();
let lerp = (1.0 - x) * from + x * to;
lerp.into()
}
}
impl<F, T> Animation<F, T>
where
F: EasingFunction,
T: Copy,
Animation<F, T>: AnimationLerp<T>,
{
pub fn get(&self) -> T {
if let Some(clamped) = self.clamped() {
clamped
} else {
let x = self.time / self.duration;
let x = if self.direction { x } else { 1.0 - x };
let lerp = self.function.y(x as f64) as f32;
self.lerp(lerp)
}
}
pub fn ease_to(&mut self, to: T) {
self.from = self.get();
self.to = to;
self.time = 0.0;
self.direction = true;
}
}

View File

@ -0,0 +1,352 @@
use canary_script::Panel;
use crate::{Color, Vec2};
use bitflags::bitflags;
pub enum Corner {
TopRight,
BottomRight,
BottomLeft,
TopLeft,
}
bitflags! {
pub struct CornerFlags: u8 {
const TOP_RIGHT = 0x01;
const BOTTOM_RIGHT = 0x02;
const BOTTOM_LEFT = 0x04;
const TOP_LEFT = 0x08;
const TOP = 0x09;
const RIGHT = 0x03;
const BOTTOM = 0x06;
const LEFT = 0x0C;
const ALL = 0x0F;
}
}
#[derive(Copy, Clone)]
pub struct ColoredTriangle {
pub v1: Vec2,
pub v2: Vec2,
pub v3: Vec2,
pub color: Color,
}
#[derive(Copy, Clone)]
pub struct Rect {
pub bl: Vec2,
pub tr: Vec2,
}
impl Rect {
pub fn from_xy_size(xy: Vec2, size: Vec2) -> Self {
Self {
bl: xy,
tr: xy + size,
}
}
pub fn from_circle_bounds(center: Vec2, radius: f32) -> Self {
Self {
bl: center - radius,
tr: center + radius,
}
}
pub fn from_triangle_bounds(tri: &ColoredTriangle) -> Self {
Self {
bl: tri.v1.min(tri.v2).min(tri.v3),
tr: tri.v1.max(tri.v2).max(tri.v3),
}
}
pub fn inset(&self, d: f32) -> Self {
Self {
bl: self.bl + d,
tr: self.tr - d,
}
}
pub fn tl(&self) -> Vec2 {
Vec2::new(self.bl.x, self.tr.y)
}
pub fn br(&self) -> Vec2 {
Vec2::new(self.tr.x, self.bl.y)
}
pub fn offset(&self, offset: Vec2) -> Self {
Self {
bl: self.bl + offset,
tr: self.tr + offset,
}
}
pub fn is_valid(&self) -> bool {
self.bl.cmplt(self.tr).all()
}
pub fn intersects_rect(&self, other: &Self) -> bool {
self.bl.cmple(other.tr).all() && self.tr.cmpge(other.bl).all()
}
pub fn intersection(&self, other: &Self) -> Option<Self> {
let clipped = Self {
bl: self.bl.max(other.bl),
tr: self.tr.min(other.tr),
};
if clipped.is_valid() {
Some(clipped)
} else {
None
}
}
pub fn contains_rect(&self, other: &Self) -> bool {
self.bl.x < other.bl.x
&& self.bl.y < other.bl.y
&& self.tr.x > other.tr.x
&& self.tr.y > other.tr.y
}
pub fn contains_point(&self, xy: Vec2) -> bool {
self.bl.x < xy.x && self.bl.y < xy.y && self.tr.x > xy.x && self.tr.y > xy.y
}
pub fn width(&self) -> f32 {
self.tr.x - self.bl.x
}
pub fn height(&self) -> f32 {
self.tr.y - self.bl.y
}
}
pub struct DrawContext {
panel: Panel,
offset: Option<Vec2>,
clip_rect: Option<Rect>,
opacity: Option<f32>,
}
impl DrawContext {
pub fn new(panel: Panel) -> Self {
Self {
panel,
offset: None,
clip_rect: None,
opacity: None,
}
}
pub fn draw_triangle(&self, v1: Vec2, v2: Vec2, v3: Vec2, color: Color) {
if let Some(clip_rect) = self.clip_rect.as_ref() {
let tri = ColoredTriangle { v1, v2, v3, color };
let bb = Rect::from_triangle_bounds(&tri);
if clip_rect.contains_rect(&bb) {
self.draw_triangle_noclip(tri.v1, tri.v2, tri.v3, tri.color);
}
} else {
self.draw_triangle_noclip(v1, v2, v3, color);
}
}
fn draw_triangle_noclip(&self, mut v1: Vec2, mut v2: Vec2, mut v3: Vec2, mut color: Color) {
if let Some(offset) = self.offset {
v1 += offset;
v2 += offset;
v3 += offset;
}
if let Some(opacity) = self.opacity.as_ref() {
color.a *= opacity;
}
self.panel.draw_triangle(v1.into(), v2.into(), v3.into(), color);
}
pub fn draw_circle(&self, center: Vec2, radius: f32, color: Color) {
use std::f32::consts::PI;
let delta = PI / 16.0;
let limit = PI * 2.0 + delta;
let mut last_spoke = Vec2::new(radius + center.x, center.y);
let mut theta = delta;
while theta < limit {
let new_spoke = Vec2::from_angle(theta) * radius + center;
self.draw_triangle(center, last_spoke, new_spoke, color);
last_spoke = new_spoke;
theta += delta;
}
}
pub fn draw_quarter_circle(&self, corner: Corner, center: Vec2, radius: f32, color: Color) {
use std::f32::consts::{FRAC_PI_2, PI};
let spoke_num = 16.0;
let delta = PI / 4.0 / spoke_num;
let (mut theta, limit) = match corner {
Corner::TopRight => (0.0, FRAC_PI_2),
Corner::BottomRight => (FRAC_PI_2 * 3.0, PI * 2.0),
Corner::BottomLeft => (PI, FRAC_PI_2 * 3.0),
Corner::TopLeft => (FRAC_PI_2, PI),
};
let mut last_spoke = Vec2::from_angle(theta) * radius + center;
while theta < limit {
theta += delta;
let new_spoke = Vec2::from_angle(theta) * radius + center;
self.draw_triangle(center, last_spoke, new_spoke, color);
last_spoke = new_spoke;
}
}
pub fn draw_ring(&self, center: Vec2, radius: f32, thickness: f32, color: Color) {
use std::f32::consts::PI;
let delta = PI / 64.0;
let limit = PI * 2.0 + delta;
let mut last_spoke = glam::Vec2::new(radius + center.x, center.y);
let mut last_theta = 0.0;
let mut theta = delta;
while theta < limit {
let angle = Vec2::from_angle(theta);
let new_spoke = angle * radius + center;
let new_spoke2 = angle * (radius + thickness) + center;
let last_spoke2 = Vec2::from_angle(last_theta) * (radius + thickness) + center;
self.draw_triangle(new_spoke2, last_spoke, new_spoke, color);
self.draw_triangle(new_spoke2, last_spoke2, last_spoke, color);
last_spoke = new_spoke;
last_theta = theta;
theta += delta;
}
}
pub fn draw_rect(&self, rect: Rect, color: Color) {
let rect = if let Some(clip_rect) = self.clip_rect.as_ref() {
if let Some(clipped) = clip_rect.intersection(&rect) {
clipped
} else {
return;
}
} else {
rect
};
let v1 = rect.bl;
let v2 = Vec2::new(rect.bl.x, rect.tr.y);
let v3 = Vec2::new(rect.tr.x, rect.bl.y);
let v4 = rect.tr;
self.draw_triangle_noclip(v1, v2, v3, color);
self.draw_triangle_noclip(v2, v3, v4, color);
}
pub fn draw_rounded_rect(&self, rect: Rect, radius: f32, color: Color) {
self.draw_partially_rounded_rect(CornerFlags::ALL, rect, radius, color);
}
pub fn draw_partially_rounded_rect(
&self,
corners: CornerFlags,
rect: Rect,
radius: f32,
color: Color,
) {
if corners.is_empty() {
self.draw_rect(rect, color);
return;
}
let mut inner_rect = rect;
let inset = rect.inset(radius);
if corners.intersects(CornerFlags::BOTTOM) {
inner_rect.bl.y += radius;
let mut bottom_edge = Rect {
bl: rect.bl,
tr: Vec2::new(rect.tr.x, rect.bl.y + radius),
};
if corners.contains(CornerFlags::BOTTOM_LEFT) {
bottom_edge.bl.x += radius;
self.draw_quarter_circle(Corner::BottomLeft, inset.bl, radius, color);
}
if corners.contains(CornerFlags::BOTTOM_RIGHT) {
bottom_edge.tr.x -= radius;
self.draw_quarter_circle(Corner::BottomRight, inset.br(), radius, color);
}
self.draw_rect(bottom_edge, color);
}
if corners.intersects(CornerFlags::TOP) {
inner_rect.tr.y -= radius;
let mut top_edge = Rect {
bl: Vec2::new(rect.bl.x, rect.tr.y - radius),
tr: rect.tr,
};
if corners.contains(CornerFlags::TOP_LEFT) {
top_edge.bl.x += radius;
self.draw_quarter_circle(Corner::TopLeft, inset.tl(), radius, color);
}
if corners.contains(CornerFlags::TOP_RIGHT) {
top_edge.tr.x -= radius;
self.draw_quarter_circle(Corner::TopRight, inset.tr, radius, color);
}
self.draw_rect(top_edge, color);
}
self.draw_rect(inner_rect, color);
}
pub fn get_clip_rect(&self) -> &Option<Rect> {
&self.clip_rect
}
pub fn with_offset(&self, offset: Vec2) -> Self {
Self {
offset: self.offset.map(|old| old + offset).or(Some(offset)),
clip_rect: self.clip_rect.map(|r| r.offset(-offset)),
..*self
}
}
pub fn with_clip_rect(&self, mut clip_rect: Rect) -> Option<Self> {
if let Some(old) = self.clip_rect {
if let Some(clipped) = old.intersection(&clip_rect) {
clip_rect = clipped;
} else {
return None;
}
}
Some(Self {
clip_rect: Some(clip_rect),
..*self
})
}
pub fn with_opacity(&self, mut opacity: f32) -> Self {
if let Some(old) = self.opacity {
opacity *= old;
}
Self {
opacity: Some(opacity),
..*self
}
}
}

View File

@ -0,0 +1,42 @@
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
pub mod anim;
pub mod draw;
pub mod panel;
pub mod widgets;
use canary_script::*;
use glam::Vec2;
use widgets::Widget;
export_abi!(DummyPanel);
pub struct DummyPanel {
panel: Panel,
menu: widgets::MainMenu,
}
impl BindPanel for DummyPanel {
fn bind(panel: Panel) -> Box<dyn PanelImpl> {
Box::new(Self {
panel,
menu: widgets::MainMenu::default(),
})
}
}
impl PanelImpl for DummyPanel {
fn update(&mut self, dt: f32) {
self.menu.update(dt);
}
fn draw(&mut self) {
let ctx = draw::DrawContext::new(self.panel);
self.menu.draw(&ctx);
}
fn on_cursor_event(&mut self, kind: CursorEventKind, at: canary_script::Vec2) {
self.menu.on_cursor_event(kind, at.into());
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,938 @@
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<EaseOutQuint>,
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<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);
}
}
}
}
}
}
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<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.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<T> {
pub widget: T,
pub slide_anim: Animation<EaseOut>,
pub opacity_anim: Animation<EaseOut>,
}
pub struct ScrollMenu<T> {
pub buttons: Vec<ScrollMenuButton<T>>,
pub spacing: f32,
pub scroll_anim: Animation<EaseInOutQuint>,
pub state: ScrollMenuState,
pub selected: usize,
pub was_clicked: Option<usize>,
pub event: ScrollMenuEvent,
}
impl<T> ScrollMenu<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();
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<usize> {
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<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 ScrollMenu<T> {
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<T> {
inner: T,
offset: Vec2,
}
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);
}
}
pub struct Reveal<T> {
inner: T,
slide_anim: Animation<EaseIn>,
opacity_anim: Animation<Linear>,
state: bool,
}
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 MainMenu {
pub menu: ScrollMenu<RoundButton>,
pub inventory: Reveal<Offset<TabMenu>>,
}
impl MainMenu {
pub const SUBMENU_SPACING: f32 = 0.05;
}
impl Default for MainMenu {