Initial commit
This commit is contained in:
commit
bf8592b5fe
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/Cargo.lock
|
|
@ -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"
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "canary_egui_harness"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
canary = { path = "../.." }
|
||||||
|
eframe = "0.18"
|
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -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 {
|
||||||
|
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<RectButton>,
|
||||||
|
view: Offset<ScrollView<Inventory>>,
|
||||||
|
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) {}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "canary_script"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
canary_types = { path = "../types" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
glam = ["canary_types/glam"]
|
|
@ -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<Box<dyn PanelImpl>> = Vec::new();
|
||||||
|
|
||||||
|
pub fn bind_panel<T: BindPanel>(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<dyn PanelImpl>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
|
@ -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"
|
|
@ -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<glam::Vec2> for Vec2 {
|
||||||
|
fn from(other: glam::Vec2) -> Self {
|
||||||
|
Self { x: other.x, y: other.y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec2> for glam::Vec2 {
|
||||||
|
fn from(other: Vec2) -> Self {
|
||||||
|
Self::new(other.x, other.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<glam::Vec4> for Color {
|
||||||
|
fn from(other: glam::Vec4) -> Self {
|
||||||
|
Self::new(other.x, other.y, other.z, other.w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Color> for glam::Vec4 {
|
||||||
|
fn from(other: Color) -> Self {
|
||||||
|
let Color { r, g, b, a } = other;
|
||||||
|
Self::new(r, g, b, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<MeshVertex>,
|
||||||
|
indices: Vec<MeshIndex>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WasmtimeRuntime {
|
||||||
|
engine: wasmtime::Engine,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WasmtimeRuntime {
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
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<T: ScriptAbi>(
|
||||||
|
&self,
|
||||||
|
abi: T,
|
||||||
|
module: &[u8],
|
||||||
|
) -> anyhow::Result<WasmtimeScript<T>> {
|
||||||
|
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::<u32, u32, _>(&mut store, "bind_panel")?;
|
||||||
|
bind_panel.call(&mut store, 0u32)?;
|
||||||
|
|
||||||
|
Ok(WasmtimeScript {
|
||||||
|
store,
|
||||||
|
update,
|
||||||
|
draw,
|
||||||
|
on_cursor_event,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WasmtimeScript<T> {
|
||||||
|
store: wasmtime::Store<T>,
|
||||||
|
update: wasmtime::TypedFunc<f32, ()>,
|
||||||
|
draw: wasmtime::TypedFunc<(), ()>,
|
||||||
|
on_cursor_event: wasmtime::TypedFunc<(u32, f32, f32), ()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ScriptAbi> WasmtimeScript<T> {
|
||||||
|
pub fn link(linker: &mut wasmtime::Linker<T>) -> 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<D: bytemuck::Pod>(
|
||||||
|
caller: &mut wasmtime::Caller<'_, T>,
|
||||||
|
ptr: u32,
|
||||||
|
num: u32,
|
||||||
|
) -> &'static [D] {
|
||||||
|
let len = num as usize * std::mem::size_of::<D>();
|
||||||
|
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<T: ScriptAbi> ScriptInstance for WasmtimeScript<T> {
|
||||||
|
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<Vec<DrawCommand>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue