commit bf8592b5fe1c4468af55e6003ad9cb496f476836 Author: mars Date: Fri Jul 15 15:11:35 2022 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..15ff80d --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml new file mode 100644 index 0000000..945b8d7 --- /dev/null +++ b/crates/egui/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "canary_egui_harness" +version = "0.1.0" +edition = "2021" + +[dependencies] +canary = { path = "../.." } +eframe = "0.18" diff --git a/crates/egui/src/main.rs b/crates/egui/src/main.rs new file mode 100644 index 0000000..8e71226 --- /dev/null +++ b/crates/egui/src/main.rs @@ -0,0 +1,118 @@ +use canary::ScriptInstance; +use eframe::egui; +use std::time::Instant; + +fn main() { + let args: Vec = 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, + 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 + }); + } +} diff --git a/crates/sao-ui-rs/Cargo.toml b/crates/sao-ui-rs/Cargo.toml new file mode 100644 index 0000000..99690bb --- /dev/null +++ b/crates/sao-ui-rs/Cargo.toml @@ -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" diff --git a/crates/sao-ui-rs/src/anim.rs b/crates/sao-ui-rs/src/anim.rs new file mode 100644 index 0000000..7ac9274 --- /dev/null +++ b/crates/sao-ui-rs/src/anim.rs @@ -0,0 +1,131 @@ +use keyframe::EasingFunction; +use crate::Color; + +pub trait AnimationLerp { + fn lerp(&self, x: f32) -> T; +} + +#[derive(Clone)] +pub struct Animation { + time: f32, + duration: f32, + in_delay: f32, + out_delay: f32, + from: T, + to: T, + function: F, + direction: bool, +} + +impl Animation { + 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 { + 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 AnimationLerp for Animation { + fn lerp(&self, x: f32) -> f32 { + (1.0 - x) * self.from + x * self.to + } +} + +impl AnimationLerp for Animation { + 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 Animation +where + F: EasingFunction, + T: Copy, + Animation: AnimationLerp, +{ + 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; + } +} diff --git a/crates/sao-ui-rs/src/draw.rs b/crates/sao-ui-rs/src/draw.rs new file mode 100644 index 0000000..d649c5a --- /dev/null +++ b/crates/sao-ui-rs/src/draw.rs @@ -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 { + 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, + clip_rect: Option, + opacity: Option, +} + +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 { + &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 { + 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 + } + } +} diff --git a/crates/sao-ui-rs/src/lib.rs b/crates/sao-ui-rs/src/lib.rs new file mode 100644 index 0000000..64b95d3 --- /dev/null +++ b/crates/sao-ui-rs/src/lib.rs @@ -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 { + 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()); + } +} diff --git a/crates/sao-ui-rs/src/panel.rs b/crates/sao-ui-rs/src/panel.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/sao-ui-rs/src/panel.rs @@ -0,0 +1 @@ + diff --git a/crates/sao-ui-rs/src/widgets.rs b/crates/sao-ui-rs/src/widgets.rs new file mode 100644 index 0000000..76614cd --- /dev/null +++ b/crates/sao-ui-rs/src/widgets.rs @@ -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, + 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) {} +} diff --git a/crates/script/Cargo.toml b/crates/script/Cargo.toml new file mode 100644 index 0000000..d2b9f0e --- /dev/null +++ b/crates/script/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "canary_script" +version = "0.1.0" +edition = "2021" + +[dependencies] +canary_types = { path = "../types" } + +[features] +glam = ["canary_types/glam"] diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs new file mode 100644 index 0000000..80618d9 --- /dev/null +++ b/crates/script/src/lib.rs @@ -0,0 +1,110 @@ +pub use canary_types::*; + +#[macro_export] +macro_rules! export_abi { + ($panel_impl: ident) => { + #[no_mangle] + pub extern "C" fn bind_panel(panel: u32) -> u32 { + ::canary_script::abi::bind_panel::<$panel_impl>(panel) + } + + #[no_mangle] + pub extern "C" fn update(dt: f32) { + ::canary_script::abi::update(dt) + } + + #[no_mangle] + pub extern "C" fn draw() { + ::canary_script::abi::draw() + } + + #[no_mangle] + pub extern "C" fn on_cursor_event(kind: u32, x: f32, y: f32) { + ::canary_script::abi::on_cursor_event(kind, x, y) + } + }; +} + +pub mod abi { + use super::*; + use num_traits::FromPrimitive; + + static mut PANEL_IMPLS: Vec> = Vec::new(); + + pub fn bind_panel(panel: u32) -> u32 { + unsafe { + let panel = Panel(panel); + let panel_impl = T::bind(panel); + let id = PANEL_IMPLS.len() as u32; + PANEL_IMPLS.push(panel_impl); + id + } + } + + pub fn update(dt: f32) { + unsafe { + for panel in PANEL_IMPLS.iter_mut() { + panel.update(dt); + } + } + } + + pub fn draw() { + unsafe { + for panel in PANEL_IMPLS.iter_mut() { + panel.draw(); + } + } + } + + pub fn on_cursor_event(kind: u32, x: f32, y: f32) { + let panel = unsafe { &mut PANEL_IMPLS[0] }; + let at = canary_types::Vec2 { x, y }; + let kind = CursorEventKind::from_u32(kind).unwrap(); + panel.on_cursor_event(kind, at); + } +} + +pub trait BindPanel { + fn bind(panel: Panel) -> Box; +} + +pub trait PanelImpl { + fn update(&mut self, dt: f32); + fn draw(&mut self); + fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2); +} + +#[repr(transparent)] +#[derive(Copy, Clone)] +pub struct Panel(u32); + +impl Panel { + pub unsafe fn bind(id: u32) -> Self { + Self(id) + } + + pub fn draw_triangle(&self, v1: Vec2, v2: Vec2, v3: Vec2, color: Color) { + unsafe { + UiPanel_drawTriangle( + self.0, v1.x, v1.y, v2.x, v2.y, v3.x, v3.y, color.r, color.g, color.b, color.a, + ) + } + } +} + +extern "C" { + fn UiPanel_drawTriangle( + panel: u32, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + x3: f32, + y3: f32, + r: f32, + g: f32, + b: f32, + a: f32, + ); +} diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml new file mode 100644 index 0000000..e1572ec --- /dev/null +++ b/crates/types/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "canary_types" +version = "0.1.0" +edition = "2021" + +[dependencies] +bytemuck = { version = "1", features = ["derive"] } +glam = { version = "^0.21", optional = true } +num-derive = "0.3" +num-traits = "0.2" diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs new file mode 100644 index 0000000..825093e --- /dev/null +++ b/crates/types/src/lib.rs @@ -0,0 +1,89 @@ +#[macro_use] +extern crate num_derive; + +pub use num_traits; +use bytemuck::{Pod, Zeroable}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct Vec2 { + pub x: f32, + pub y: f32, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct Color { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +impl Color { + pub const WHITE: Self = Self::new(1., 1., 1., 1.); + pub const BLACK: Self = Self::new(0., 0., 0., 1.); + pub const TRANSPARENT: Self = Self::new(0., 0., 0., 0.); + pub const RED: Self = Self::new(1., 0., 0., 1.); + pub const GREEN: Self = Self::new(1., 0., 0., 1.); + pub const BLUE: Self = Self::new(0., 1., 0., 1.); + pub const YELLOW: Self = Self::new(1., 1., 0., 1.); + pub const MAGENTA: Self = Self::new(1., 0., 1., 1.); + pub const CYAN: Self = Self::new(0., 1., 1., 1.); + + pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self { + Self { r, g, b, a } + } + + pub fn to_rgba_unmultiplied(&self) -> (u8, u8, u8, u8) { + let map = |c: f32| (c * 255.0).floor() as u8; + (map(self.r), map(self.g), map(self.b), map(self.a)) + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct MeshVertex { + pub position: Vec2, + pub color: Color, +} + +pub type MeshIndex = u32; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, FromPrimitive, ToPrimitive)] +pub enum CursorEventKind { + Hover = 0, + Select = 1, + Drag = 2, + Deselect = 3, +} + +#[cfg(feature = "glam")] +mod glam_interop { + use super::*; + + impl From for Vec2 { + fn from(other: glam::Vec2) -> Self { + Self { x: other.x, y: other.y } + } + } + + impl From for glam::Vec2 { + fn from(other: Vec2) -> Self { + Self::new(other.x, other.y) + } + } + + impl From for Color { + fn from(other: glam::Vec4) -> Self { + Self::new(other.x, other.y, other.z, other.w) + } + } + + impl From for glam::Vec4 { + fn from(other: Color) -> Self { + let Color { r, g, b, a } = other; + Self::new(r, g, b, a) + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b743a15 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,201 @@ +pub use canary_types::*; +use parking_lot::Mutex; + +/// Low-level script API callbacks. +pub trait ScriptAbi { + fn start_draw(&self); + fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]); + fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand])); +} + +pub trait ScriptInstance { + fn update(&mut self, dt: f32); + fn draw(&mut self, f: impl FnOnce(&[DrawCommand])); + fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2); +} + +#[non_exhaustive] +pub enum DrawCommand { + Mesh { + vertices: Vec, + indices: Vec, + }, +} + +pub struct WasmtimeRuntime { + engine: wasmtime::Engine, +} + +impl WasmtimeRuntime { + pub fn new() -> anyhow::Result { + let mut config = wasmtime::Config::new(); + config.wasm_simd(true); + config.wasm_bulk_memory(true); + config.cranelift_opt_level(wasmtime::OptLevel::Speed); + + let engine = wasmtime::Engine::new(&config)?; + + Ok(Self { engine }) + } + + pub fn load_module( + &self, + abi: T, + module: &[u8], + ) -> anyhow::Result> { + let module = wasmtime::Module::new(&self.engine, module)?; + let mut store = wasmtime::Store::new(&self.engine, abi); + let mut linker = wasmtime::Linker::new(&self.engine); + WasmtimeScript::link(&mut linker)?; + let instance = linker.instantiate(&mut store, &module)?; + let update = instance.get_typed_func(&mut store, "update")?; + let draw = instance.get_typed_func(&mut store, "draw")?; + let on_cursor_event = instance.get_typed_func(&mut store, "on_cursor_event")?; + + let bind_panel = instance.get_typed_func::(&mut store, "bind_panel")?; + bind_panel.call(&mut store, 0u32)?; + + Ok(WasmtimeScript { + store, + update, + draw, + on_cursor_event, + }) + } +} + +pub struct WasmtimeScript { + store: wasmtime::Store, + update: wasmtime::TypedFunc, + draw: wasmtime::TypedFunc<(), ()>, + on_cursor_event: wasmtime::TypedFunc<(u32, f32, f32), ()>, +} + +impl WasmtimeScript { + pub fn link(linker: &mut wasmtime::Linker) -> anyhow::Result<()> { + let module = "env"; + + linker.func_wrap( + module, + "draw_indexed", + |mut caller: wasmtime::Caller<'_, T>, + vertices_ptr: u32, + vertices_num: u32, + indices_ptr: u32, + indices_num: u32| { + let vertices = Self::get_memory_slice(&mut caller, vertices_ptr, vertices_num); + let indices = Self::get_memory_slice(&mut caller, indices_ptr, indices_num); + caller.data().draw_indexed(vertices, indices); + }, + )?; + + linker.func_wrap( + module, + "UiPanel_drawTriangle", + |caller: wasmtime::Caller<'_, T>, + _panel: u32, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + x3: f32, + y3: f32, + r: f32, + g: f32, + b: f32, + a: f32| { + let v1 = Vec2 { x: x1, y: y1 }; + let v2 = Vec2 { x: x2, y: y2 }; + let v3 = Vec2 { x: x3, y: y3 }; + let color = Color { r, g, b, a }; + + let vertices = [ + MeshVertex { + position: v1, + color, + }, + MeshVertex { + position: v2, + color, + }, + MeshVertex { + position: v3, + color, + }, + ]; + + let indices = [0, 1, 2]; + + caller.data().draw_indexed(&vertices, &indices); + }, + )?; + + Ok(()) + } + + fn get_memory_slice( + caller: &mut wasmtime::Caller<'_, T>, + ptr: u32, + num: u32, + ) -> &'static [D] { + let len = num as usize * std::mem::size_of::(); + let bytes = Self::get_memory_slice_bytes(caller, ptr as usize, len); + bytemuck::cast_slice(bytes) + } + + fn get_memory_slice_bytes( + caller: &mut wasmtime::Caller<'_, T>, + ptr: usize, + len: usize, + ) -> &'static [u8] { + let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); + if ptr + len > memory.data_size(&caller) { + panic!("Attempted wasm memory read is out-of-bounds!"); + } + + unsafe { + let ptr = memory.data_ptr(caller).add(ptr); + std::slice::from_raw_parts(ptr, len) + } + } +} + +impl ScriptInstance for WasmtimeScript { + fn update(&mut self, dt: f32) { + self.update.call(&mut self.store, dt).unwrap(); + } + + fn draw(&mut self, f: impl FnOnce(&[DrawCommand])) { + self.store.data().start_draw(); + self.draw.call(&mut self.store, ()).unwrap(); + self.store.data().with_draw_commands(f); + } + + fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) { + self.on_cursor_event + .call(&mut self.store, (kind as u32, at.x, at.y)) + .unwrap(); + } +} + +#[derive(Default)] +pub struct SimpleScriptAbi { + draw_cmds: Mutex>, +} + +impl ScriptAbi for SimpleScriptAbi { + fn start_draw(&self) { + self.draw_cmds.lock().clear(); + } + + fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) { + self.draw_cmds.lock().push(DrawCommand::Mesh { + vertices: vertices.to_vec(), + indices: indices.to_vec(), + }) + } + + fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand])) { + f(self.draw_cmds.lock().as_slice()); + } +}