Reworked ui scripting

master
Mark 2024-02-04 11:45:49 -08:00
parent 7ce169bbea
commit 47a73224c6
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
32 changed files with 1048 additions and 1145 deletions

2
Cargo.lock generated
View File

@ -887,7 +887,9 @@ dependencies = [
name = "galactica-util"
version = "0.0.0"
dependencies = [
"anyhow",
"nalgebra",
"rhai",
]
[[package]]

View File

@ -78,7 +78,6 @@ rhai = { version = "1.17.0", features = [
"f32_float",
"only_i32",
"metadata",
"sync",
"no_custom_syntax",
"no_closure",
] }

View File

@ -1,18 +1,17 @@
# Specific projects
## Now:
- outfitter
- Clean up scripting & errors
- Clean up sprite content (and content in general)
- Flying UI
- Fix radar
- Mouse colliders
- UI captures input?
- No UI zoom scroll
- Preserve aspect for icons
- Check game version in config
- outfitter
## Small jobs
- Clean up sprite content (and content in general)
- Check game version in config
- Fix window resizing
- Debug: show mouse position
- 🌟 Better planet desc formatting

View File

@ -1,65 +1,55 @@
fn config() {
let config = SceneConfig();
config.show_starfield(true);
config.show_phys(true);
return config
}
fn init(state) {
let ring = SpriteBuilder(
conf_set_starfield(true);
conf_set_phys(true);
add_sprite(
"ring",
"ui::status",
Rect(
-5.0, -5.0, 100.0, 100.0,
SpriteAnchor::NorthEast,
SpriteAnchor::NorthEast
Anchor::NorthEast,
Anchor::NorthEast
)
);
let shield = RadialBuilder(
add_radialbar(
"shield", 2.5,
Color(0.3, 0.6, 0.8, 1.0),
Rect(
-9.5, -9.5, 91.0, 91.0,
SpriteAnchor::NorthEast,
SpriteAnchor::NorthEast
Anchor::NorthEast,
Anchor::NorthEast
)
);
shield.set_progress(1.0);
let hull = RadialBuilder(
add_radialbar(
"hull", 2.5,
Color(0.8, 0.7, 0.5, 1.0),
Rect(
-13.5, -13.5, 83.0, 83.0,
SpriteAnchor::NorthEast,
SpriteAnchor::NorthEast
Anchor::NorthEast,
Anchor::NorthEast
)
);
hull.set_progress(1.0);
return [
ring,
shield,
hull
];
}
fn event(state, event) {
if type_of(event) == "PlayerShipStateEvent" {
if state.player_ship().is_landed() {
return SceneAction::GoTo("landed");
go_to_scene("landed");
return;
}
return;
}
}
fn step(state, elements) {
elements["shield"].set_val(
fn step(state) {
radialbar_set_val("shield",
state.player_ship().get_shields()
/ state.player_ship().get_total_shields()
);
elements["hull"].set_val(
radialbar_set_val("hull",
state.player_ship().get_hull()
/ state.player_ship().get_total_hull()
);

View File

@ -1,24 +1,20 @@
fn config() {
let config = SceneConfig();
config.show_starfield(true);
config.show_phys(false);
return config
}
fn init(state) {
let player = state.player_ship();
let frame = SpriteBuilder(
"frame",
"ui::planet",
conf_set_starfield(true);
conf_set_phys(false);
add_sprite(
"button",
"ui::planet::button",
Rect(
0.0, 0.0, 400.0, 297.866,
SpriteAnchor::Center,
SpriteAnchor::Center
99.0, 128.0, 73.898, 18.708,
Anchor::NorthWest,
Anchor::Center
)
);
let landscape = SpriteBuilder(
add_sprite(
"landscape",
{
if player.is_landed() {
@ -29,73 +25,62 @@ fn init(state) {
},
Rect(
-180.0, 142.0, 274.0, 135.0,
SpriteAnchor::NorthWest,
SpriteAnchor::Center
Anchor::NorthWest,
Anchor::Center
)
);
landscape.set_mask("ui::landscapemask");
sprite_set_mask("landscape", "ui::landscapemask");
let button = SpriteBuilder(
"button",
"ui::planet::button",
add_sprite(
"frame",
"ui::planet",
Rect(
99.0, 128.0, 73.898, 18.708,
SpriteAnchor::NorthWest,
SpriteAnchor::Center
0.0, 0.0, 400.0, 297.866,
Anchor::Center,
Anchor::Center
)
);
let title = TextBoxBuilder(
add_textbox(
"title", 10.0, 10.0,
Rect(
-70.79, 138.0, 59.867, 10.0,
SpriteAnchor::NorthWest,
SpriteAnchor::Center
Anchor::NorthWest,
Anchor::Center
),
Color(1.0, 1.0, 1.0, 1.0)
);
textbox_align_center("title");
textbox_font_serif("title");
textbox_weight_bold("title");
if player.is_landed() {
title.set_text(player.landed_on().name());
} else {
title.set_text("");
textbox_set_text("title", player.landed_on().name());
}
title.align_center();
title.font_serif();
title.weight_bold();
let desc = TextBoxBuilder(
add_textbox(
"desc", 7.5, 8.0,
Rect(
-178.92, -20.3, 343.0, 81.467,
SpriteAnchor::NorthWest,
SpriteAnchor::Center
Anchor::NorthWest,
Anchor::Center
),
Color(1.0, 1.0, 1.0, 1.0)
);
textbox_font_sans("desc");
if player.is_landed() {
desc.set_text(player.landed_on().desc());
} else {
desc.set_text("");
textbox_set_text("desc", player.landed_on().desc());
}
desc.font_sansserif();
return [
button,
landscape,
frame,
title,
desc,
];
}
fn event(state, event) {
if type_of(event) == "MouseHoverEvent" {
let element = event.element();
if element.has_name("button") {
if element == "button" {
if event.is_enter() {
element.take_edge("on:top", 0.1);
sprite_take_edge("button", "on:top", 0.1);
} else {
element.take_edge("off:top", 0.1);
sprite_take_edge("button", "off:top", 0.1);
}
}
return;
@ -104,19 +89,21 @@ fn event(state, event) {
if type_of(event) == "MouseClickEvent" {
if !event.is_down() {
return SceneAction::None;
return;
}
let element = event.element();
if element.has_name("button") {
return SceneAction::GoTo("outfitter");
if element == "button" {
go_to_scene("outfitter");
return;
}
return;
}
if type_of(event) == "PlayerShipStateEvent" {
if !state.player_ship().is_landed() {
return SceneAction::GoTo("flying");
go_to_scene("flying");
return;
}
return;
}

View File

@ -1,196 +1,174 @@
fn config() {
let config = SceneConfig();
config.show_starfield(true);
config.show_phys(false);
return config
}
fn init(state) {
let se_box = SpriteBuilder(
conf_set_starfield(true);
conf_set_phys(false);
add_sprite(
"se_box",
"ui::outfitterbox",
Rect(
-1.0, -1.0, 202.345, 133.409,
SpriteAnchor::SouthWest,
SpriteAnchor::SouthWest
Anchor::SouthWest,
Anchor::SouthWest
)
);
let exit_text = TextBoxBuilder(
add_textbox(
"exit_text", 10.0, 10.0,
Rect(
122.71, 48.0, 51.0, 12.0,
SpriteAnchor::NorthWest,
SpriteAnchor::SouthWest
Anchor::NorthWest,
Anchor::SouthWest
),
Color(1.0, 1.0, 1.0, 1.0)
);
exit_text.set_text("Exit");
exit_text.font_serif();
exit_text.align_center();
textbox_font_serif("exit_text");
textbox_align_center("exit_text");
textbox_set_text("exit_text", "Exit");
let exit_button = SpriteBuilder(
add_sprite(
"exit_button",
"ui::button",
Rect(
113.35, 52.0, 69.8, 18.924,
SpriteAnchor::NorthWest,
SpriteAnchor::SouthWest
Anchor::NorthWest,
Anchor::SouthWest
)
);
let ship_bg = SpriteBuilder(
add_sprite(
"ship_bg",
"ui::outfitter-ship-bg",
Rect(
16.0, -16.0, 190.0, 353.0,
SpriteAnchor::NorthWest,
SpriteAnchor::NorthWest
Anchor::NorthWest,
Anchor::NorthWest
)
);
let ship_thumb = SpriteBuilder(
add_sprite(
"ship_thumb",
"icon::gypsum",
Rect(
111.0, -95.45, 90.0, 90.0,
SpriteAnchor::Center,
SpriteAnchor::NorthWest
Anchor::Center,
Anchor::NorthWest
)
);
let ship_name = TextBoxBuilder(
add_textbox(
"ship_name", 10.0, 10.0,
Rect(
111.0, -167.27, 145.0, 10.0,
SpriteAnchor::Center,
SpriteAnchor::NorthWest
Anchor::Center,
Anchor::NorthWest
),
Color(1.0, 1.0, 1.0, 1.0)
);
ship_name.set_text("Hyperion");
ship_name.font_serif();
ship_name.align_center();
textbox_font_serif("ship_name");
textbox_align_center("ship_name");
textbox_set_text("ship_name", "Hyperion");
let ship_type = TextBoxBuilder(
add_textbox(
"ship_type", 7.0, 8.5,
Rect(
111.0, -178.0, 145.0, 8.5,
SpriteAnchor::Center,
SpriteAnchor::NorthWest
Anchor::Center,
Anchor::NorthWest
),
Color(0.7, 0.7, 0.7, 1.0)
);
textbox_font_sans("ship_type");
textbox_align_center("ship_type");
if state.player_ship().is_some() {
ship_type.set_text(state.player_ship().name());
} else {
ship_type.set_text("ERR: SHIP IS NONE");
textbox_set_text("ship_type", state.player_ship().name());
}
ship_type.font_sansserif();
ship_type.align_center();
let ship_stats = TextBoxBuilder(
add_textbox(
"ship_stats", 7.0, 8.5,
Rect(
38.526, -192.332, 144.948, 154.5,
SpriteAnchor::NorthWest,
SpriteAnchor::NorthWest,
Anchor::NorthWest,
Anchor::NorthWest,
),
Color(1.0, 1.0, 1.0, 1.0)
);
ship_stats.set_text("Earth");
ship_stats.font_monospace();
textbox_font_mono("ship_stats");
textbox_set_text("ship_stats", "Earth");
let outfit_bg = SpriteBuilder(
add_sprite(
"outfit_bg",
"ui::outfitter-outfit-bg",
Rect(
-16.0, -16.0, 300.0, 480.0,
SpriteAnchor::NorthEast,
SpriteAnchor::NorthEast
Anchor::NorthEast,
Anchor::NorthEast
)
);
let outfit_thumb = SpriteBuilder(
add_sprite(
"outfit_thumb",
"icon::engine",
Rect(
-166.0, -109.0, 90.0, 90.0,
SpriteAnchor::Center,
SpriteAnchor::NorthEast
Anchor::Center,
Anchor::NorthEast
)
);
let outfit_name = TextBoxBuilder(
add_textbox(
"outfit_name", 16.0, 16.0,
Rect(
-312.0, -20.0, 200.0, 16.0,
SpriteAnchor::NorthWest,
SpriteAnchor::NorthEast,
Anchor::NorthWest,
Anchor::NorthEast,
),
Color(1.0, 1.0, 1.0, 1.0)
);
outfit_name.set_text("Earth");
outfit_name.font_serif();
outfit_name.weight_bold();
textbox_font_serif("outfit_name");
textbox_weight_bold("outfit_name");
textbox_set_text("outfit_name", "Earth");
let outfit_desc = TextBoxBuilder(
add_textbox(
"outfit_desc", 7.0, 8.5,
Rect(
-166.0, -219.0, 260.0, 78.0,
SpriteAnchor::Center,
SpriteAnchor::NorthEast,
Anchor::Center,
Anchor::NorthEast,
),
Color(1.0, 1.0, 1.0, 1.0)
);
outfit_desc.set_text("Earth");
outfit_desc.font_serif();
textbox_font_serif("outfit_desc");
textbox_set_text("outfit_desc", "Earth");
let outfit_stats = TextBoxBuilder(
add_textbox(
"outfit_stats", 7.0, 8.5,
Rect(
-295.0, -271.0, 164.0, 216.0,
SpriteAnchor::NorthWest,
SpriteAnchor::NorthEast,
Anchor::NorthWest,
Anchor::NorthEast,
),
Color(1.0, 1.0, 1.0, 1.0)
);
outfit_stats.set_text("Earth");
ship_stats.font_monospace();
textbox_font_mono("outfit_stats");
textbox_set_text("outfit_stats", "Earth");
return [
ship_bg,
ship_thumb,
ship_name,
ship_type,
ship_stats,
outfit_bg,
outfit_thumb,
outfit_name,
outfit_desc,
outfit_stats,
se_box,
exit_button,
exit_text
];
}
fn event(state, event) {
if type_of(event) == "MouseHoverEvent" {
let element = event.element();
if element.has_name("exit_button") {
if element == "exit_button" {
if event.is_enter() {
element.take_edge("on:top", 0.1);
sprite_take_edge("exit_button", "on:top", 0.1);
} else {
element.take_edge("off:top", 0.1);
sprite_take_edge("exit_button", "off:top", 0.1);
}
}
return;
@ -199,19 +177,21 @@ fn event(state, event) {
if type_of(event) == "MouseClickEvent" {
if !event.is_down() {
return SceneAction::None;
return;
}
let element = event.element();
if element.has_name("exit_button") {
return SceneAction::GoTo("landed");
if element == "exit_button" {
go_to_scene("landed");
return;
}
return;
}
if type_of(event) == "PlayerShipStateEvent" {
if !state.player_ship().is_landed() {
return SceneAction::GoTo("flying");
go_to_scene("flying");
return;
}
return;
}

View File

@ -5,6 +5,7 @@ use rhai::AST;
pub(crate) mod syntax {
use anyhow::{bail, Context, Result};
use galactica_packer::SpriteAtlas;
use galactica_util::rhai_error_to_anyhow;
use rhai::Engine;
use serde::Deserialize;
use std::{
@ -65,9 +66,8 @@ pub(crate) mod syntax {
for (n, p) in self.ui_scene {
ui_scenes.insert(
n.clone(),
engine
.compile_file(content_root.join(p))
.with_context(|| format!("while loading scene script `{}`", n))?,
rhai_error_to_anyhow(engine.compile_file(content_root.join(p)))
.with_context(|| format!("while loading scene script `{n}`"))?,
);
}

View File

@ -5,7 +5,7 @@ use galactica_system::data::ShipState;
use galactica_util::to_radians;
use glyphon::{FontSystem, Resolution, SwashCache, TextAtlas, TextRenderer};
use nalgebra::{Point2, Point3};
use std::{iter, sync::Arc};
use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
use wgpu;
use winit;
@ -15,7 +15,7 @@ use crate::{
shaderprocessor::preprocess_shader,
starfield::Starfield,
texturearray::TextureArray,
ui::UiManager,
ui::UiScriptExecutor,
vertexbuffer::{consts::SPRITE_INDICES, types::ObjectInstance},
RenderInput, RenderState, VertexBuffers,
};
@ -32,7 +32,7 @@ pub struct GPUState {
pub(crate) starfield: Starfield,
pub(crate) texture_array: TextureArray,
pub(crate) state: RenderState,
pub(crate) ui: UiManager,
pub(crate) ui: UiScriptExecutor,
}
impl GPUState {
@ -221,14 +221,14 @@ impl GPUState {
window_aspect,
global_uniform,
vertex_buffers,
text_renderer,
text_atlas,
text_cache,
text_font_system,
text_renderer,
text_font_system: Rc::new(RefCell::new(text_font_system)),
};
return Ok(Self {
ui: UiManager::new(ct, &mut state),
ui: UiScriptExecutor::new(ct, &mut state),
device,
config,
surface,
@ -332,7 +332,9 @@ impl GPUState {
timestamp_writes: None,
});
if self.ui.get_config().show_phys {
let config = self.ui.get_config();
if config.show_phys {
// Create sprite instances
// Game coordinates (relative to camera) of ne and sw corners of screen.
@ -349,14 +351,14 @@ impl GPUState {
self.push_effects(&input, (clip_ne, clip_sw));
}
self.ui.draw(input.clone(), &mut self.state).unwrap();
self.ui.draw(&mut self.state, input.clone()).unwrap();
// These should match the indices in each shader,
// and should each have a corresponding bind group layout.
render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]);
render_pass.set_bind_group(1, &self.state.global_uniform.bind_group, &[]);
if self.ui.get_config().show_starfield {
if config.show_starfield {
// Starfield pipeline
self.state
.vertex_buffers
@ -370,7 +372,7 @@ impl GPUState {
);
}
if self.ui.get_config().show_phys {
if config.show_phys {
// Sprite pipeline
self.state
.vertex_buffers
@ -409,27 +411,30 @@ impl GPUState {
0..self.state.get_radialbar_counter(),
);
let textareas = self.ui.get_textareas(&input, &self.state);
self.state
.text_renderer
.prepare(
&self.device,
&self.state.queue,
&mut self.state.text_font_system,
&mut self.state.text_atlas,
Resolution {
width: self.state.window_size.width,
height: self.state.window_size.height,
},
textareas,
&mut self.state.text_cache,
)
.unwrap();
{
self.state
.text_renderer
.prepare(
&self.device,
&self.state.queue,
&mut self.state.text_font_system.borrow_mut(),
&mut self.state.text_atlas,
Resolution {
width: self.state.window_size.width,
height: self.state.window_size.height,
},
(*self.ui.state)
.borrow_mut()
.get_textareas(&input, &self.state.window),
&mut self.state.text_cache,
)
.unwrap();
self.state
.text_renderer
.render(&self.state.text_atlas, &mut render_pass)
.unwrap();
self.state
.text_renderer
.render(&mut self.state.text_atlas, &mut render_pass)
.unwrap();
}
// begin_render_pass borrows encoder mutably,
// so we need to drop it before calling finish.

View File

@ -3,6 +3,7 @@ use galactica_util::constants::{
OBJECT_SPRITE_INSTANCE_LIMIT, RADIALBAR_SPRITE_INSTANCE_LIMIT, UI_SPRITE_INSTANCE_LIMIT,
};
use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer};
use std::{cell::RefCell, rc::Rc};
use wgpu::BufferAddress;
use winit::window::Window;
@ -101,10 +102,10 @@ pub(crate) struct RenderState {
pub global_uniform: GlobalUniform,
pub vertex_buffers: VertexBuffers,
pub text_font_system: FontSystem,
pub text_font_system: Rc<RefCell<FontSystem>>,
pub text_renderer: TextRenderer,
pub text_cache: SwashCache,
pub text_atlas: TextAtlas,
pub text_renderer: TextRenderer,
}
impl RenderState {

View File

@ -0,0 +1,28 @@
use rhai::plugin::*;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Anchor {
Center,
NorthWest,
SouthWest,
NorthEast,
SouthEast,
}
#[export_module]
pub mod anchor_mod {
#[allow(non_upper_case_globals)]
pub const Center: Anchor = Anchor::Center;
#[allow(non_upper_case_globals)]
pub const NorthWest: Anchor = Anchor::NorthWest;
#[allow(non_upper_case_globals)]
pub const NorthEast: Anchor = Anchor::NorthEast;
#[allow(non_upper_case_globals)]
pub const SouthWest: Anchor = Anchor::SouthWest;
#[allow(non_upper_case_globals)]
pub const SouthEast: Anchor = Anchor::SouthEast;
}

View File

@ -1,28 +0,0 @@
use rhai::{CustomType, TypeBuilder};
#[derive(Debug, Clone)]
pub struct SceneConfig {
pub show_phys: bool,
pub show_starfield: bool,
}
impl SceneConfig {
pub fn new() -> Self {
Self {
show_phys: false,
show_starfield: false,
}
}
}
impl CustomType for SceneConfig {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("SceneConfig")
.with_fn("SceneConfig", Self::new)
.with_fn("show_phys", |s: &mut Self, x: bool| s.show_phys = x)
.with_fn("show_starfield", |s: &mut Self, x: bool| {
s.show_starfield = x
});
}
}

View File

@ -1,11 +1,9 @@
use rhai::{CustomType, TypeBuilder};
use super::SpriteElement;
use rhai::{CustomType, ImmutableString, TypeBuilder};
#[derive(Debug, Clone)]
pub struct MouseClickEvent {
pub down: bool,
pub element: SpriteElement,
pub element: ImmutableString,
}
impl CustomType for MouseClickEvent {
@ -20,7 +18,7 @@ impl CustomType for MouseClickEvent {
#[derive(Debug, Clone)]
pub struct MouseHoverEvent {
pub enter: bool,
pub element: SpriteElement,
pub element: ImmutableString,
}
impl CustomType for MouseHoverEvent {

View File

@ -1,28 +1,14 @@
mod anchor;
mod color;
mod config;
mod event;
mod radialbuilder;
mod radialelement;
mod rect;
mod sceneaction;
mod spritebuilder;
mod spriteelement;
mod state;
mod textboxbuilder;
mod util;
pub use anchor::*;
pub use color::*;
pub use config::*;
pub use event::*;
pub use radialbuilder::*;
pub use radialelement::*;
pub use rect::*;
pub use sceneaction::*;
pub use spritebuilder::*;
pub use spriteelement::*;
pub use state::*;
pub use textboxbuilder::*;
pub use util::*;
use rhai::{exported_module, Engine};
@ -31,25 +17,15 @@ pub fn register_into_engine(engine: &mut Engine) {
// Helpers
.build_type::<Rect>()
.build_type::<Color>()
.build_type::<SceneConfig>()
// State
.build_type::<State>()
.build_type::<ShipState>()
.build_type::<SystemObjectState>()
// Builders
.build_type::<RadialBuilder>()
.build_type::<SpriteBuilder>()
.build_type::<TextBoxBuilder>()
// Elements
.build_type::<SpriteElement>()
.build_type::<RadialElement>()
// Events
.build_type::<MouseClickEvent>()
.build_type::<MouseHoverEvent>()
.build_type::<PlayerShipStateEvent>()
// Larger modules
.register_type_with_name::<SpriteAnchor>("SpriteAnchor")
.register_static_module("SpriteAnchor", exported_module!(spriteanchor_mod).into())
.register_type_with_name::<SceneAction>("SceneAction")
.register_static_module("SceneAction", exported_module!(sceneaction_mod).into());
// Bigger modules
.register_type_with_name::<Anchor>("Anchor")
.register_static_module("Anchor", exported_module!(anchor_mod).into());
}

View File

@ -1,39 +0,0 @@
use nalgebra::clamp;
use rhai::{CustomType, ImmutableString, TypeBuilder};
use super::{color::Color, Rect};
#[derive(Debug, Clone)]
pub struct RadialBuilder {
pub name: ImmutableString,
pub rect: Rect,
pub stroke: f32,
pub color: Color,
pub progress: f32,
}
impl RadialBuilder {
pub fn new(name: ImmutableString, stroke: f32, color: Color, rect: Rect) -> Self {
Self {
name,
rect,
stroke,
color,
progress: 0.0,
}
}
pub fn set_progress(&mut self, progress: f32) {
self.progress = clamp(progress, 0.0, 1.0);
}
}
impl CustomType for RadialBuilder {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("RadialBuilder")
.with_fn("RadialBuilder", Self::new)
.with_fn("set_progress", Self::set_progress);
}
}

View File

@ -1,31 +0,0 @@
use galactica_content::Content;
use rhai::{CustomType, TypeBuilder};
use std::{cell::RefCell, rc::Rc, sync::Arc};
use crate::ui::util::RadialBar;
#[derive(Debug, Clone)]
pub struct RadialElement {
pub bar: Rc<RefCell<RadialBar>>,
pub ct: Arc<Content>,
}
// TODO: remove this
unsafe impl Send for RadialElement {}
unsafe impl Sync for RadialElement {}
impl RadialElement {
pub fn new(ct: Arc<Content>, bar: Rc<RefCell<RadialBar>>) -> Self {
Self { ct, bar }
}
}
impl CustomType for RadialElement {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("RadialElement")
.with_fn("set_val", |s: &mut Self, val: f32| {
s.bar.borrow_mut().set_val(val)
});
}
}

View File

@ -1,27 +1,20 @@
use nalgebra::{Point2, Vector2};
use rhai::{CustomType, TypeBuilder};
use winit::dpi::LogicalSize;
use winit::{dpi::LogicalSize, window::Window};
use super::SpriteAnchor;
use super::Anchor;
use crate::{RenderInput, RenderState};
#[derive(Debug, Clone)]
pub struct Rect {
pub pos: Point2<f32>,
pub dim: Vector2<f32>,
pub anchor_self: SpriteAnchor,
pub anchor_parent: SpriteAnchor,
pub anchor_self: Anchor,
pub anchor_parent: Anchor,
}
impl Rect {
pub fn new(
x: f32,
y: f32,
w: f32,
h: f32,
anchor_self: SpriteAnchor,
anchor_parent: SpriteAnchor,
) -> Self {
pub fn new(x: f32, y: f32, w: f32, h: f32, anchor_self: Anchor, anchor_parent: Anchor) -> Self {
Self {
pos: Point2::new(x, y),
dim: Vector2::new(w, h),
@ -30,8 +23,8 @@ impl Rect {
}
}
pub fn to_centered(&self, state: &RenderState, ui_scale: f32) -> CenteredRect {
let w: LogicalSize<f32> = state.window_size.to_logical(state.window.scale_factor());
pub fn to_centered(&self, window: &Window, ui_scale: f32) -> CenteredRect {
let w: LogicalSize<f32> = window.inner_size().to_logical(window.scale_factor());
let w = Vector2::new(w.width, w.height);
let mut pos = self.pos * ui_scale;
@ -39,42 +32,42 @@ impl Rect {
// Origin
match self.anchor_parent {
SpriteAnchor::Center => {}
Anchor::Center => {}
SpriteAnchor::NorthWest => {
Anchor::NorthWest => {
pos += Vector2::new(-w.x, w.y) / 2.0;
}
SpriteAnchor::SouthWest => {
Anchor::SouthWest => {
pos += Vector2::new(-w.x, -w.y) / 2.0;
}
SpriteAnchor::NorthEast => {
Anchor::NorthEast => {
pos += Vector2::new(w.x, w.y) / 2.0;
}
SpriteAnchor::SouthEast => {
Anchor::SouthEast => {
pos += Vector2::new(w.x, -w.y) / 2.0;
}
}
// Offset for self dimensions
match self.anchor_self {
SpriteAnchor::Center => {}
Anchor::Center => {}
SpriteAnchor::NorthWest => {
Anchor::NorthWest => {
pos += Vector2::new(dim.x, -dim.y) / 2.0;
}
SpriteAnchor::NorthEast => {
Anchor::NorthEast => {
pos += Vector2::new(-dim.x, -dim.y) / 2.0;
}
SpriteAnchor::SouthWest => {
Anchor::SouthWest => {
pos += Vector2::new(dim.x, dim.y) / 2.0;
}
SpriteAnchor::SouthEast => {
Anchor::SouthEast => {
pos += Vector2::new(-dim.x, dim.y) / 2.0;
}
};

View File

@ -1,18 +0,0 @@
use rhai::plugin::*;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum SceneAction {
None,
GoTo(String),
}
#[export_module]
pub mod sceneaction_mod {
#[allow(non_upper_case_globals)]
pub const None: SceneAction = SceneAction::None;
#[allow(non_snake_case)]
pub fn GoTo(scene: String) -> SceneAction {
SceneAction::GoTo(scene)
}
}

View File

@ -1,35 +0,0 @@
use rhai::{CustomType, ImmutableString, TypeBuilder};
use super::Rect;
#[derive(Debug, Clone)]
pub struct SpriteBuilder {
pub name: ImmutableString,
pub rect: Rect,
pub sprite: ImmutableString,
pub mask: Option<ImmutableString>,
}
impl SpriteBuilder {
pub fn new(name: ImmutableString, sprite: ImmutableString, rect: Rect) -> Self {
Self {
name,
rect,
sprite,
mask: None,
}
}
pub fn set_mask(&mut self, mask: ImmutableString) {
self.mask = Some(mask);
}
}
impl CustomType for SpriteBuilder {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("SpriteBuilder")
.with_fn("SpriteBuilder", Self::new)
.with_fn("set_mask", Self::set_mask);
}
}

View File

@ -1,75 +0,0 @@
use galactica_content::{resolve_edge_as_edge, Content};
use log::error;
use rhai::{CustomType, TypeBuilder};
use std::{cell::RefCell, rc::Rc, sync::Arc};
use crate::ui::util::Sprite;
#[derive(Debug, Clone)]
pub struct SpriteElement {
pub sprite: Rc<RefCell<Sprite>>,
pub ct: Arc<Content>,
}
// TODO: remove this
unsafe impl Send for SpriteElement {}
unsafe impl Sync for SpriteElement {}
impl SpriteElement {
pub fn new(ct: Arc<Content>, sprite: Rc<RefCell<Sprite>>) -> Self {
Self { ct, sprite }
}
// This MUST be &mut, or rhai won't recognize it.
pub fn has_name(&mut self, s: String) -> bool {
self.sprite.as_ref().borrow().name == s
}
pub fn take_edge(&mut self, edge_name: &str, duration: f32) {
if self.sprite.as_ref().borrow().anim.is_none() {
return;
}
let sprite_handle = self
.sprite
.as_ref()
.borrow()
.anim
.as_ref()
.unwrap()
.get_sprite();
let sprite = self.ct.get_sprite(sprite_handle);
let edge = resolve_edge_as_edge(edge_name, duration, |x| {
sprite.get_section_handle_by_name(x)
});
let edge = match edge {
Err(x) => {
error!(
"UI script reference invalid section `{}` in sprite `{}`, skipping",
edge_name, sprite.name
);
error!("error: {:?}", x);
return;
}
Ok(s) => s,
};
self.sprite
.as_ref()
.borrow_mut()
.anim
.as_mut()
.unwrap()
.jump_to(&self.ct, edge);
}
}
impl CustomType for SpriteElement {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("SpriteElement")
.with_fn("has_name", Self::has_name)
.with_fn("take_edge", Self::take_edge);
}
}

View File

@ -1,70 +0,0 @@
use glyphon::{cosmic_text::Align, Attrs, AttrsOwned, FamilyOwned, Style, Weight};
use rhai::{CustomType, ImmutableString, TypeBuilder};
use super::{Color, Rect};
#[derive(Debug, Clone)]
pub struct TextBoxBuilder {
pub name: ImmutableString,
pub font_size: f32,
pub line_height: f32,
pub rect: Rect,
pub text: ImmutableString,
pub color: Color,
pub attrs: AttrsOwned,
pub justify: Align,
}
impl TextBoxBuilder {
pub fn new(
name: ImmutableString,
font_size: f32,
line_height: f32,
rect: Rect,
color: Color,
) -> Self {
Self {
color,
name,
font_size,
line_height,
rect,
text: ImmutableString::new(),
attrs: AttrsOwned::new(Attrs::new()),
justify: Align::Left,
}
}
pub fn set_text(&mut self, text: ImmutableString) {
self.text = text
}
}
impl CustomType for TextBoxBuilder {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("TextBoxBuilder")
.with_fn("TextBoxBuilder", Self::new)
.with_fn("set_text", Self::set_text)
.with_fn("align_left", |s: &mut Self| s.justify = Align::Left)
.with_fn("align_right", |s: &mut Self| s.justify = Align::Right)
.with_fn("align_justify", |s: &mut Self| s.justify = Align::Justified)
.with_fn("align_center", |s: &mut Self| s.justify = Align::Center)
.with_fn("weight_bold", |s: &mut Self| s.attrs.weight = Weight::BOLD)
.with_fn("weight_normal", |s: &mut Self| {
s.attrs.weight = Weight::NORMAL
})
.with_fn("font_serif", |s: &mut Self| {
s.attrs.family_owned = FamilyOwned::Serif
})
.with_fn("font_sansserif", |s: &mut Self| {
s.attrs.family_owned = FamilyOwned::SansSerif
})
.with_fn("font_monospace", |s: &mut Self| {
s.attrs.family_owned = FamilyOwned::Monospace
})
.with_fn("style_normal", |s: &mut Self| s.attrs.style = Style::Normal)
.with_fn("style_italic", |s: &mut Self| s.attrs.style = Style::Italic);
}
}

View File

@ -1,28 +0,0 @@
use rhai::plugin::*;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum SpriteAnchor {
Center,
NorthWest,
SouthWest,
NorthEast,
SouthEast,
}
#[export_module]
pub mod spriteanchor_mod {
#[allow(non_upper_case_globals)]
pub const Center: SpriteAnchor = SpriteAnchor::Center;
#[allow(non_upper_case_globals)]
pub const NorthWest: SpriteAnchor = SpriteAnchor::NorthWest;
#[allow(non_upper_case_globals)]
pub const NorthEast: SpriteAnchor = SpriteAnchor::NorthEast;
#[allow(non_upper_case_globals)]
pub const SouthWest: SpriteAnchor = SpriteAnchor::SouthWest;
#[allow(non_upper_case_globals)]
pub const SouthEast: SpriteAnchor = SpriteAnchor::SouthEast;
}

View File

@ -0,0 +1,567 @@
use anyhow::{Context, Result};
use galactica_content::{resolve_edge_as_edge, Content};
use galactica_system::phys::PhysSimShipHandle;
use galactica_util::rhai_error_to_anyhow;
use glyphon::{cosmic_text::Align, FamilyOwned, FontSystem, Style, Weight};
use log::{debug, error};
use rhai::{Dynamic, Engine, ImmutableString, Scope};
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc};
use super::{
api::{self, Color, MouseClickEvent, MouseHoverEvent, PlayerShipStateEvent, Rect},
event::Event,
util::{RadialBar, Sprite, TextBox},
UiConfig, UiElement, UiState,
};
use crate::{ui::api::State, RenderInput, RenderState};
pub(crate) struct UiScriptExecutor {
engine: Engine,
scope: Scope<'static>,
pub state: Rc<RefCell<UiState>>,
last_player_state: u32,
last_scene: Option<ImmutableString>,
}
impl UiScriptExecutor {
pub fn new(ct: Arc<Content>, state: &mut RenderState) -> Self {
let scope = Scope::new();
let elements = Rc::new(RefCell::new(UiState::new(ct.clone(), state)));
let mut engine = Engine::new_raw();
api::register_into_engine(&mut engine);
Self::register_api(
ct.clone(),
state.text_font_system.clone(),
elements.clone(),
&mut engine,
);
Self {
engine,
scope,
state: elements,
last_scene: None,
last_player_state: 0,
}
}
pub fn get_config(&self) -> UiConfig {
(*self.state).borrow().config.clone()
}
/// Change the current scene
pub fn set_scene(&mut self, input: Arc<RenderInput>) -> Result<()> {
let current_scene = (*self.state).borrow().get_scene().clone();
if self.last_scene == current_scene {
return Ok(());
}
self.last_scene = current_scene.clone();
if current_scene.is_none() {
return Ok(());
}
debug!(
"switched to {}, running `init()`",
current_scene.as_ref().unwrap()
);
self.scope.clear();
// Drop this right away, since all script calls borrow elm mutably.
let mut elm = self.state.borrow_mut();
elm.clear();
drop(elm);
let ct = (*self.state).borrow().ct.clone();
rhai_error_to_anyhow(
self.engine.call_fn(
&mut self.scope,
ct.get_config()
.ui_scenes
.get(current_scene.as_ref().unwrap().as_str())
.unwrap(),
"init",
(State::new(input.clone()),),
),
)
.with_context(|| format!("while running `init()`"))
.with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?;
return Ok(());
}
/// Draw all ui elements
pub fn draw(&mut self, state: &mut RenderState, input: Arc<RenderInput>) -> Result<()> {
let ct = (*self.state).borrow().ct.clone();
// Initialize start scene if we haven't yet
if (*self.state).borrow().get_scene().is_none() {
(*self.state)
.borrow_mut()
.set_scene(ImmutableString::from(&ct.get_config().start_ui_scene));
}
self.set_scene(input.clone())?;
let current_scene = (*self.state).borrow().get_scene().clone();
(*self.state).borrow_mut().step(state, input.clone());
// Run step() (if it is defined)
let ast = ct
.get_config()
.ui_scenes
.get(current_scene.as_ref().unwrap().as_str())
.unwrap();
if ast.iter_functions().any(|x| x.name == "step") {
rhai_error_to_anyhow(self.engine.call_fn(
&mut self.scope,
ast,
"step",
(State::new(input.clone()),),
))
.with_context(|| format!("while calling `step()`"))
.with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?;
}
// Send player state change events
if {
let player = input.player.ship;
if let Some(player) = player {
let ship = input.phys_img.get_ship(&PhysSimShipHandle(player)).unwrap();
if self.last_player_state == 0
|| NonZeroU32::new(self.last_player_state).unwrap()
!= ship.ship.get_data().get_state().as_int()
{
self.last_player_state = ship.ship.get_data().get_state().as_int().into();
true
} else {
false
}
} else {
self.last_player_state = 0;
true
}
} {
rhai_error_to_anyhow(
self.engine.call_fn(
&mut self.scope,
ct.get_config()
.ui_scenes
.get(current_scene.as_ref().unwrap().as_str())
.unwrap(),
"event",
(State::new(input.clone()), PlayerShipStateEvent {}),
),
)
.with_context(|| format!("while handling player state change event"))
.with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?;
}
let len = (*self.state).borrow().len();
for i in 0..len {
let event_arg = match (*self.state).borrow_mut().get_mut_by_idx(i).unwrap() {
UiElement::Sprite(sprite) => {
// Draw and update sprites
sprite.step(&input, state);
sprite.push_to_buffer(&input, state);
let event = sprite.check_events(&input, state);
match event {
Event::None => None,
Event::MouseClick => Some(Dynamic::from(MouseClickEvent {
down: true,
element: sprite.name.clone(),
})),
Event::MouseRelease => Some(Dynamic::from(MouseClickEvent {
down: false,
element: sprite.name.clone(),
})),
Event::MouseHover => Some(Dynamic::from(MouseHoverEvent {
enter: true,
element: sprite.name.clone(),
})),
Event::MouseUnhover => Some(Dynamic::from(MouseHoverEvent {
enter: false,
element: sprite.name.clone(),
})),
}
}
UiElement::RadialBar(x) => {
// Draw and update radialbar
x.step(&input, state);
x.push_to_buffer(&input, state);
None
}
UiElement::Text(..) => None,
};
if let Some(event_arg) = event_arg {
rhai_error_to_anyhow(
self.engine.call_fn(
&mut self.scope,
ct.get_config()
.ui_scenes
.get(current_scene.as_ref().unwrap().as_str())
.unwrap(),
"event",
(State::new(input.clone()), event_arg.clone()),
),
)
.with_context(|| format!("while handling event `{:?}`", event_arg))
.with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?;
}
}
return Ok(());
}
}
// API
impl UiScriptExecutor {
pub fn register_api(
ct_src: Arc<Content>,
font_src: Rc<RefCell<FontSystem>>,
s: Rc<RefCell<UiState>>,
engine: &mut Engine,
) {
// Utilities
{
let c = s.clone();
engine.register_fn("go_to_scene", move |scene: ImmutableString| {
let mut ui_state = c.borrow_mut();
ui_state.set_scene(scene);
});
let c = s.clone();
engine.register_fn("conf_set_phys", move |b: bool| {
let mut ui_state = c.borrow_mut();
ui_state.config.show_phys = b;
});
let c = s.clone();
engine.register_fn("conf_set_starfield", move |b: bool| {
let mut ui_state = c.borrow_mut();
ui_state.config.show_starfield = b;
});
}
// Sprites
{
let c = s.clone();
let ct = ct_src.clone();
engine.register_fn(
"add_sprite",
move |name: ImmutableString, sprite: ImmutableString, rect: Rect| {
let mut ui_state = c.borrow_mut();
let len = ui_state.len();
let sprite_handle = ct.get_sprite_handle(sprite.as_str());
if sprite_handle.is_none() {
error!("made a sprite using an invalid source `{sprite}`");
return;
}
ui_state.names.insert(name.clone(), len);
ui_state.elements.push(UiElement::Sprite(Sprite::new(
&ct,
name.clone(),
sprite_handle.unwrap(),
rect,
)));
},
);
let c = s.clone();
let ct = ct_src.clone();
engine.register_fn(
"sprite_set_mask",
move |name: ImmutableString, mask: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Sprite(x)) => {
let m = ct.get_sprite_handle(mask.as_str());
if m.is_none() {
error!("called `set_sprite_mask` with an invalid mask `{mask}`");
return;
}
x.set_mask(m)
}
_ => {
error!("called `set_sprite_mask` on an invalid name `{name}`")
}
}
},
);
let c = s.clone();
let ct = ct_src.clone();
engine.register_fn(
"sprite_take_edge",
move |name: ImmutableString, edge_name: ImmutableString, duration: f32| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Sprite(x)) => {
let sprite_handle = x.anim.get_sprite();
let sprite = &ct.get_sprite(sprite_handle);
let edge = resolve_edge_as_edge(edge_name.as_str(), duration, |x| {
sprite.get_section_handle_by_name(x)
});
let edge = match edge {
Err(_) => {
error!(
"called `sprite_take_edge` on an invalid edge `{}` on sprite `{}`",
edge_name, sprite.name
);
return;
}
Ok(s) => s,
};
x.anim.jump_to(&ct, edge);
}
_ => {
error!("called `sprite_take_edge` on an invalid name `{name}`")
}
}
},
);
}
// Textboxes
{
let c = s.clone();
let font = font_src.clone();
engine.register_fn(
"add_textbox",
// TODO: fix ugly spaces
move |name: ImmutableString,
font_size: f32,
line_height: f32,
rect: Rect,
color: Color| {
let mut ui_state = c.borrow_mut();
let len = ui_state.len();
ui_state.names.insert(name.clone(), len);
ui_state.elements.push(UiElement::Text(TextBox::new(
&mut font.borrow_mut(),
name.clone(),
font_size,
line_height,
rect,
color,
)));
},
);
let c = s.clone();
let font = font_src.clone();
engine.register_fn(
"textbox_set_text",
move |name: ImmutableString, text: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => {
x.set_text(&mut font.borrow_mut(), text.as_str())
}
_ => {
error!("called `textbox_set_text` on an invalid name `{name}`")
}
}
},
);
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_align_left", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => x.set_align(&mut font.borrow_mut(), Align::Left),
_ => {
error!("called `textbox_align_left` on an invalid name `{name}`")
}
}
});
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_align_right", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => x.set_align(&mut font.borrow_mut(), Align::Right),
_ => {
error!("called `textbox_align_right` on an invalid name `{name}`")
}
}
});
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_align_justify", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => {
x.set_align(&mut font.borrow_mut(), Align::Justified)
}
_ => {
error!("called `textbox_align_justify` on an invalid name `{name}`")
}
}
});
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_align_center", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => x.set_align(&mut font.borrow_mut(), Align::Center),
_ => {
error!("called `textbox_align_center` on an invalid name `{name}`")
}
}
});
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_weight_bold", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => x.set_weight(&mut font.borrow_mut(), Weight::BOLD),
_ => {
error!("called `textbox_weight_bold` on an invalid name `{name}`")
}
}
});
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_weight_normal", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => {
x.set_weight(&mut font.borrow_mut(), Weight::NORMAL)
}
_ => {
error!("called `textbox_weight_normal` on an invalid name `{name}`")
}
}
});
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_font_serif", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => {
x.set_font(&mut font.borrow_mut(), FamilyOwned::Serif)
}
_ => {
error!("called `textbox_font_serif` on an invalid name `{name}`")
}
}
});
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_font_sans", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => {
x.set_font(&mut font.borrow_mut(), FamilyOwned::SansSerif)
}
_ => {
error!("called `textbox_font_sans` on an invalid name `{name}`")
}
}
});
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_font_mono", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => {
x.set_font(&mut font.borrow_mut(), FamilyOwned::Monospace)
}
_ => {
error!("called `textbox_font_mono` on an invalid name `{name}`")
}
}
});
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_style_normal", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => x.set_style(&mut font.borrow_mut(), Style::Normal),
_ => {
error!("called `textbox_style_normal` on an invalid name `{name}`")
}
}
});
let c = s.clone();
let font = font_src.clone();
engine.register_fn("textbox_style_italic", move |name: ImmutableString| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::Text(x)) => x.set_style(&mut font.borrow_mut(), Style::Italic),
_ => {
error!("called `textbox_style_italic` on an invalid name `{name}`")
}
}
});
}
// Radialbars
{
let c = s.clone();
engine.register_fn(
"add_radialbar",
// TODO: fix ugly spaces
move |name: ImmutableString, stroke: f32, color: Color, rect: Rect| {
let mut ui_state = c.borrow_mut();
let len = ui_state.len();
ui_state.names.insert(name.clone(), len);
ui_state.elements.push(UiElement::RadialBar(RadialBar::new(
name.clone(),
stroke,
color,
rect,
1.0,
)));
},
);
let c = s.clone();
engine.register_fn(
"radialbar_set_val",
move |name: ImmutableString, val: f32| {
let mut ui_state = c.borrow_mut();
match ui_state.get_mut_by_name(&name) {
Some(UiElement::RadialBar(x)) => x.set_val(val),
_ => {
error!("called `radialbar_set_val` on an invalid name `{name}`")
}
}
},
);
}
}
}

View File

@ -1,418 +0,0 @@
use anyhow::{Context, Result};
use galactica_content::Content;
use galactica_system::phys::PhysSimShipHandle;
use glyphon::TextArea;
use log::{debug, error, trace};
use rhai::{Array, Dynamic, Engine, Map, Scope};
use std::{collections::HashSet, num::NonZeroU32, sync::Arc};
use super::{
api::{
self, MouseClickEvent, MouseHoverEvent, PlayerShipStateEvent, SceneAction, SceneConfig,
SpriteElement, TextBoxBuilder,
},
event::Event,
util::{FpsIndicator, RadialBar, TextBox},
UiElement,
};
use crate::{
ui::{
api::{RadialBuilder, RadialElement, SpriteBuilder, State},
util::Sprite,
},
RenderInput, RenderState,
};
pub(crate) struct UiManager {
current_scene: Option<String>,
current_scene_config: SceneConfig,
engine: Engine,
scope: Scope<'static>,
ct: Arc<Content>,
/// UI elements
elements: Vec<UiElement>,
/// Map of ui element name -> api handle.
/// Used for step() function.
element_index: Map,
last_player_state: u32,
show_timings: bool,
fps_indicator: FpsIndicator,
}
impl UiManager {
pub fn new(ct: Arc<Content>, state: &mut RenderState) -> Self {
let scope = Scope::new();
let mut engine = Engine::new_raw();
api::register_into_engine(&mut engine);
Self {
ct,
current_scene: None,
current_scene_config: SceneConfig::new(),
engine,
scope,
elements: Vec::new(),
element_index: Map::new(),
show_timings: true,
fps_indicator: FpsIndicator::new(state),
last_player_state: 0,
}
}
pub fn get_config(&self) -> &SceneConfig {
&self.current_scene_config
}
/// Change the current scene
pub fn set_scene(
&mut self,
state: &mut RenderState,
input: Arc<RenderInput>,
scene: String,
) -> Result<()> {
if !self.ct.get_config().ui_scenes.contains_key(&scene) {
error!("tried to switch to ui scene `{scene}`, which doesn't exist");
return Ok(());
}
debug!("switching to {:?}", scene);
self.current_scene = Some(scene);
self.current_scene_config = self
.engine
.call_fn(
&mut self.scope,
&self
.ct
.get_config()
.ui_scenes
.get(self.current_scene.as_ref().unwrap())
.unwrap(),
"config",
(),
)
.with_context(|| format!("while handling `config()`"))
.with_context(|| format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()))?;
self.scope.clear();
self.elements.clear();
let mut used_names = HashSet::new();
let builders: Array = self
.engine
.call_fn(
&mut self.scope,
&self
.ct
.get_config()
.ui_scenes
.get(self.current_scene.as_ref().unwrap())
.unwrap(),
"init",
(State::new(input.clone()),),
)
.with_context(|| format!("while running `init()`"))
.with_context(|| format!("in ui scene `{}`", self.current_scene.as_ref().unwrap()))?;
trace!("found {:?} builders", builders.len());
for v in builders {
if v.is::<SpriteBuilder>() {
let s = v.cast::<SpriteBuilder>();
if used_names.contains(s.name.as_str()) {
error!(
"UI scene `{}` re-uses element name `{}`",
self.current_scene.as_ref().unwrap(),
s.name
);
} else {
used_names.insert(s.name.to_string());
}
self.elements.push(UiElement::new_sprite(Sprite::new(
&self.ct,
s.name.to_string(),
s.sprite.to_string(),
s.mask.map(|x| x.to_string()),
s.rect,
)));
} else if v.is::<RadialBuilder>() {
let r = v.cast::<RadialBuilder>();
if used_names.contains(r.name.as_str()) {
error!(
"UI scene `{}` re-uses element name `{}`",
self.current_scene.as_ref().unwrap(),
r.name
);
} else {
used_names.insert(r.name.to_string());
}
self.elements.push(UiElement::new_radialbar(RadialBar::new(
&self.ct,
r.name.to_string(),
r.stroke,
r.color,
r.rect,
r.progress,
)));
} else if v.is::<TextBoxBuilder>() {
let t = v.cast::<TextBoxBuilder>();
if used_names.contains(t.name.as_str()) {
error!(
"UI scene `{}` re-uses element name `{}`",
self.current_scene.as_ref().unwrap(),
t.name
);
} else {
used_names.insert(t.name.to_string());
}
let mut b = TextBox::new(
state,
t.name.to_string(),
t.font_size,
t.line_height,
t.justify,
t.attrs,
t.rect,
t.color,
);
b.set_text(state, &t.text);
self.elements.push(UiElement::new_text(b));
} else {
// TODO: better message
error!("bad type in builder array")
}
self.element_index.clear();
for e in &self.elements {
match e {
UiElement::Text(_) => {}
UiElement::RadialBar(r) => {
self.element_index.insert(
(&r).borrow().name.clone().into(),
Dynamic::from(RadialElement::new(self.ct.clone(), r.clone())),
);
}
UiElement::Sprite(s) => {
self.element_index.insert(
(&s).borrow().name.clone().into(),
Dynamic::from(SpriteElement::new(self.ct.clone(), s.clone())),
);
}
}
}
}
return Ok(());
}
/// Draw all ui elements
pub fn draw(&mut self, input: Arc<RenderInput>, state: &mut RenderState) -> Result<()> {
// Initialize start scene if we haven't yet
if self.current_scene.is_none() {
self.set_scene(
state,
input.clone(),
self.ct.get_config().start_ui_scene.clone(),
)?;
}
// Run step() (if it is defined)
let ast = &self
.ct
.get_config()
.ui_scenes
.get(self.current_scene.as_ref().unwrap())
.unwrap();
if ast.iter_functions().any(|x| x.name == "step") {
self.engine
.call_fn(
&mut self.scope,
ast,
"step",
(State::new(input.clone()), self.element_index.clone()),
)
.with_context(|| format!("while handling player state change event"))
.with_context(|| {
format!("in ui scene `{}`", self.current_scene.as_ref().unwrap())
})?;
}
// Update timings if they're being displayed
if self.show_timings {
self.fps_indicator.step(&input, state);
}
// Send player state change events
if {
let player = input.player.ship;
if let Some(player) = player {
let ship = input.phys_img.get_ship(&PhysSimShipHandle(player)).unwrap();
if self.last_player_state == 0
|| NonZeroU32::new(self.last_player_state).unwrap()
!= ship.ship.get_data().get_state().as_int()
{
self.last_player_state = ship.ship.get_data().get_state().as_int().into();
true
} else {
false
}
} else {
self.last_player_state = 0;
true
}
} {
let action: Dynamic = self
.engine
.call_fn(
&mut self.scope,
&self
.ct
.get_config()
.ui_scenes
.get(self.current_scene.as_ref().unwrap())
.unwrap(),
"event",
(State::new(input.clone()), PlayerShipStateEvent {}),
)
.with_context(|| format!("while handling player state change event"))
.with_context(|| {
format!("in ui scene `{}`", self.current_scene.as_ref().unwrap())
})?;
if let Some(action) = action.try_cast::<SceneAction>() {
if self.handle_action(state, input.clone(), action)? {
return Ok(());
}
}
}
for i in 0..self.elements.len() {
match &self.elements[i] {
UiElement::Sprite(e) => {
// Draw and update sprites
let mut sprite = (*e).borrow_mut();
sprite.step(&input, state);
sprite.push_to_buffer(&input, state);
let event = sprite.check_events(&input, state);
// we MUST drop here, since script calls mutate the sprite RefCell
drop(sprite);
match event {
Event::None => continue,
_ => {}
}
let event_arg = match event {
Event::None => unreachable!("this shouldn't happen"),
Event::MouseClick => Dynamic::from(MouseClickEvent {
down: true,
element: SpriteElement::new(self.ct.clone(), e.clone()),
}),
Event::MouseRelease => Dynamic::from(MouseClickEvent {
down: false,
element: SpriteElement::new(self.ct.clone(), e.clone()),
}),
Event::MouseHover => Dynamic::from(MouseHoverEvent {
enter: true,
element: SpriteElement::new(self.ct.clone(), e.clone()),
}),
Event::MouseUnhover => Dynamic::from(MouseHoverEvent {
enter: false,
element: SpriteElement::new(self.ct.clone(), e.clone()),
}),
};
let action: Dynamic = self
.engine
.call_fn(
&mut self.scope,
&self
.ct
.get_config()
.ui_scenes
.get(self.current_scene.as_ref().unwrap())
.unwrap(),
"event",
(State::new(input.clone()), event_arg),
)
.with_context(|| format!("while handling event `{:?}`", event))
.with_context(|| {
format!("in ui scene `{}`", self.current_scene.as_ref().unwrap())
})?;
if let Some(action) = action.try_cast::<SceneAction>() {
if self.handle_action(state, input.clone(), action)? {
return Ok(());
}
}
}
UiElement::RadialBar(e) => {
// Draw and update radialbar
let mut x = (*e).borrow_mut();
x.step(&input, state);
x.push_to_buffer(&input, state);
}
UiElement::Text(..) => {}
}
}
return Ok(());
}
/// Do a SceneAction.
/// If this returns true, all evaluation stops immediately.
fn handle_action(
&mut self,
state: &mut RenderState,
input: Arc<RenderInput>,
action: SceneAction,
) -> Result<bool> {
Ok(match action {
SceneAction::None => false,
SceneAction::GoTo(s) => {
self.set_scene(state, input.clone(), s.clone())?;
true
}
})
}
}
impl<'a> UiManager {
/// Get textareas
pub fn get_textareas(
&'a mut self,
input: &RenderInput,
state: &RenderState,
) -> Vec<TextArea<'a>> {
let mut v = Vec::with_capacity(32);
if self.current_scene.is_none() {
return v;
}
if self.show_timings {
v.push(self.fps_indicator.get_textarea(state, input))
}
for t in self
.elements
.iter()
.map(|x| x.text())
.filter(|x| x.is_some())
.map(|x| x.unwrap())
.map(|x| x.get_textarea(state, input))
{
v.push(t)
}
v
}
}

View File

@ -1,8 +1,9 @@
mod api;
mod event;
mod manager;
mod uielement;
mod executor;
mod state;
mod util;
pub(crate) use manager::UiManager;
pub(crate) use uielement::UiElement;
pub(crate) use executor::UiScriptExecutor;
pub(crate) use state::*;

View File

@ -0,0 +1,136 @@
use galactica_content::Content;
use glyphon::TextArea;
use log::{debug, error};
use rhai::ImmutableString;
use std::collections::HashMap;
use std::sync::Arc;
use winit::window::Window;
use super::util::{FpsIndicator, RadialBar, Sprite, TextBox};
use crate::{RenderInput, RenderState};
#[derive(Debug)]
pub enum UiElement {
Sprite(Sprite),
RadialBar(RadialBar),
Text(TextBox),
}
#[derive(Clone, Debug)]
pub(crate) struct UiConfig {
pub show_phys: bool,
pub show_starfield: bool,
}
pub(crate) struct UiState {
pub names: HashMap<ImmutableString, usize>,
pub elements: Vec<UiElement>,
pub ct: Arc<Content>,
current_scene: Option<ImmutableString>,
show_timings: bool,
fps_indicator: FpsIndicator,
pub config: UiConfig,
}
// TODO: remove this
unsafe impl Send for UiState {}
unsafe impl Sync for UiState {}
impl UiState {
pub fn new(ct: Arc<Content>, state: &mut RenderState) -> Self {
Self {
ct,
names: HashMap::new(),
elements: Vec::new(),
current_scene: None,
show_timings: true,
fps_indicator: FpsIndicator::new(state),
config: UiConfig {
show_phys: false,
show_starfield: false,
},
}
}
pub fn clear(&mut self) {
self.elements.clear();
self.names.clear();
}
pub fn len(&self) -> usize {
self.elements.len()
}
/*
pub fn get_by_idx(&self, idx: usize) -> Option<&UiElement> {
self.elements.get(idx)
}
pub fn get_by_name(&self, name: &ImmutableString) -> Option<&UiElement> {
let idx = self.names.get(name);
if idx.is_none() {
return None;
}
self.get_by_idx(*idx.unwrap())
}
*/
pub fn get_mut_by_idx(&mut self, idx: usize) -> Option<&mut UiElement> {
self.elements.get_mut(idx)
}
pub fn get_mut_by_name(&mut self, name: &ImmutableString) -> Option<&mut UiElement> {
let idx = self.names.get(name);
if idx.is_none() {
return None;
}
self.get_mut_by_idx(*idx.unwrap())
}
pub fn get_scene(&self) -> &Option<ImmutableString> {
&self.current_scene
}
pub fn set_scene(&mut self, scene: ImmutableString) {
if !self.ct.get_config().ui_scenes.contains_key(scene.as_str()) {
error!("tried to switch to ui scene `{scene}`, which doesn't exist");
return;
}
debug!("switching to {:?}", scene);
self.current_scene = Some(scene);
}
pub fn step(&mut self, state: &mut RenderState, input: Arc<RenderInput>) {
if self.show_timings {
self.fps_indicator
.step(&input, &mut state.text_font_system.borrow_mut());
}
}
}
impl<'a> UiState {
pub fn get_textareas(&'a mut self, input: &RenderInput, window: &Window) -> Vec<TextArea<'a>> {
let mut v = Vec::with_capacity(32);
if self.current_scene.is_none() {
return v;
}
if self.show_timings {
v.push(self.fps_indicator.get_textarea(input, window))
}
for t in self.elements.iter() {
match &t {
UiElement::Text(x) => v.push(x.get_textarea(input, window)),
_ => {}
}
}
return v;
}
}

View File

@ -1,31 +0,0 @@
use std::{cell::RefCell, rc::Rc};
use super::util::{RadialBar, Sprite, TextBox};
#[derive(Debug)]
pub enum UiElement {
Sprite(Rc<RefCell<Sprite>>),
RadialBar(Rc<RefCell<RadialBar>>),
Text(TextBox),
}
impl UiElement {
pub fn new_sprite(sprite: Sprite) -> Self {
Self::Sprite(Rc::new(RefCell::new(sprite)))
}
pub fn new_radialbar(bar: RadialBar) -> Self {
Self::RadialBar(Rc::new(RefCell::new(bar)))
}
pub fn new_text(text: TextBox) -> Self {
Self::Text(text)
}
pub fn text(&self) -> Option<&TextBox> {
match self {
Self::Text(t) => Some(t),
_ => None,
}
}
}

View File

@ -1,4 +1,5 @@
use glyphon::{Attrs, Buffer, Color, Family, Metrics, Shaping, TextArea, TextBounds};
use glyphon::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, TextArea, TextBounds};
use winit::window::Window;
use crate::{RenderInput, RenderState};
@ -9,9 +10,12 @@ pub(crate) struct FpsIndicator {
impl FpsIndicator {
pub fn new(state: &mut RenderState) -> Self {
let mut buffer = Buffer::new(&mut state.text_font_system, Metrics::new(7.0, 8.0));
let mut buffer = Buffer::new(
&mut state.text_font_system.borrow_mut(),
Metrics::new(7.0, 8.0),
);
buffer.set_size(
&mut state.text_font_system,
&mut state.text_font_system.borrow_mut(),
state.window_size.width as f32,
state.window_size.height as f32,
);
@ -24,7 +28,7 @@ impl FpsIndicator {
}
impl FpsIndicator {
pub fn step(&mut self, input: &RenderInput, state: &mut RenderState) {
pub fn step(&mut self, input: &RenderInput, font: &mut FontSystem) {
if self.update_counter > 0 {
self.update_counter -= 1;
return;
@ -32,17 +36,17 @@ impl FpsIndicator {
self.update_counter = 100;
self.buffer.set_text(
&mut state.text_font_system,
font,
&input.timing.get_string(),
Attrs::new().family(Family::Monospace),
Shaping::Basic,
);
self.buffer.shape_until_scroll(&mut state.text_font_system);
self.buffer.shape_until_scroll(font);
}
}
impl<'a, 'b: 'a> FpsIndicator {
pub fn get_textarea(&'b self, _state: &RenderState, input: &RenderInput) -> TextArea<'a> {
pub fn get_textarea(&'b self, input: &RenderInput, _window: &Window) -> TextArea<'a> {
TextArea {
buffer: &self.buffer,
left: 10.0,

View File

@ -1,12 +1,13 @@
use galactica_content::Content;
use std::f32::consts::TAU;
use rhai::ImmutableString;
use super::super::api::Rect;
use crate::{ui::api::Color, vertexbuffer::types::RadialBarInstance, RenderInput, RenderState};
#[derive(Debug, Clone)]
pub struct RadialBar {
pub name: String,
pub name: ImmutableString,
rect: Rect,
stroke: f32,
color: Color,
@ -15,8 +16,7 @@ pub struct RadialBar {
impl RadialBar {
pub fn new(
_ct: &Content,
name: String,
name: ImmutableString,
stroke: f32,
color: Color,
rect: Rect,
@ -36,7 +36,9 @@ impl RadialBar {
}
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
let rect = self.rect.to_centered(state, input.ct.get_config().ui_scale);
let rect = self
.rect
.to_centered(&state.window, input.ct.get_config().ui_scale);
state.push_radialbar_buffer(RadialBarInstance {
position: [rect.pos.x, rect.pos.y],

View File

@ -1,16 +1,13 @@
use galactica_content::{Content, SpriteAutomaton, SpriteHandle};
use galactica_util::to_radians;
use log::error;
use super::super::api::Rect;
use crate::{ui::event::Event, vertexbuffer::types::UiInstance, RenderInput, RenderState};
use galactica_content::{Content, SpriteAutomaton, SpriteHandle};
use galactica_util::to_radians;
use rhai::ImmutableString;
#[derive(Debug, Clone)]
pub struct Sprite {
pub name: String,
/// If this is none, this was constructed with an invalid sprite
pub anim: Option<SpriteAutomaton>,
pub anim: SpriteAutomaton,
pub name: ImmutableString,
rect: Rect,
mask: Option<SpriteHandle>,
@ -22,57 +19,30 @@ pub struct Sprite {
}
impl Sprite {
pub fn new(
ct: &Content,
name: String,
sprite: String,
mask: Option<String>,
rect: Rect,
) -> Self {
let sprite_handle = ct.get_sprite_handle(&sprite);
if sprite_handle.is_none() {
error!("UI constructed a sprite named `{name}` using an invalid source `{sprite}`")
}
let mask = {
if mask.is_some() {
let m = ct.get_sprite_handle(mask.as_ref().unwrap());
if m.is_none() {
error!(
"UI constructed a sprite named `{name}` using an invalid mask` {}`",
mask.unwrap()
);
None
} else {
m
}
} else {
None
}
};
pub fn new(ct: &Content, name: ImmutableString, sprite: SpriteHandle, rect: Rect) -> Self {
Self {
name,
anim: sprite_handle.map(|x| SpriteAutomaton::new(&ct, x)),
anim: SpriteAutomaton::new(&ct, sprite),
rect,
mask,
mask: None,
has_mouse: false,
has_click: false,
waiting_for_release: false,
}
}
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
if self.anim.is_none() {
return;
}
pub fn set_mask(&mut self, mask: Option<SpriteHandle>) {
self.mask = mask;
}
let rect = self.rect.to_centered(state, input.ct.get_config().ui_scale);
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
let rect = self
.rect
.to_centered(&state.window, input.ct.get_config().ui_scale);
// TODO: use both dimensions,
// not just height
let anim_state = self.anim.as_ref().unwrap().get_texture_idx();
let anim_state = self.anim.get_texture_idx();
state.push_ui_buffer(UiInstance {
position: rect.pos.into(),
@ -93,7 +63,9 @@ impl Sprite {
}
pub fn check_events(&mut self, input: &RenderInput, state: &mut RenderState) -> Event {
let r = self.rect.to_centered(state, input.ct.get_config().ui_scale);
let r = self
.rect
.to_centered(&state.window, input.ct.get_config().ui_scale);
if self.waiting_for_release && self.has_mouse && !input.player.input.pressed_leftclick() {
self.waiting_for_release = false;
@ -139,13 +111,6 @@ impl Sprite {
}
pub fn step(&mut self, input: &RenderInput, _state: &mut RenderState) {
if self.anim.is_none() {
return;
}
self.anim
.as_mut()
.unwrap()
.step(&input.ct, input.time_since_last_run);
self.anim.step(&input.ct, input.time_since_last_run);
}
}

View File

@ -1,14 +1,19 @@
use glyphon::{
cosmic_text::Align, AttrsOwned, Buffer, Color, Metrics, Shaping, TextArea, TextBounds,
cosmic_text::Align, Attrs, AttrsOwned, Buffer, Color, FamilyOwned, FontSystem, Metrics,
Shaping, Style, TextArea, TextBounds, Weight,
};
use nalgebra::Vector2;
use rhai::ImmutableString;
use winit::window::Window;
use super::super::api::Rect;
use crate::{ui::api, RenderInput, RenderState};
use crate::{ui::api, RenderInput};
#[derive(Debug)]
pub struct TextBox {
pub name: String,
pub name: ImmutableString,
text: String,
justify: Align,
rect: Rect,
buffer: Buffer,
@ -18,58 +23,78 @@ pub struct TextBox {
impl TextBox {
pub fn new(
state: &mut RenderState,
name: String,
font: &mut FontSystem,
name: ImmutableString,
font_size: f32,
line_height: f32,
justify: Align,
attrs: AttrsOwned,
rect: Rect,
color: api::Color,
) -> Self {
let mut buffer = Buffer::new(
&mut state.text_font_system,
Metrics::new(font_size, line_height),
);
let mut buffer = Buffer::new(font, Metrics::new(font_size, line_height));
// Do NOT apply UI scale here, that's only done when we make a TextArea
buffer.set_size(&mut state.text_font_system, rect.dim.x, rect.dim.y);
buffer.set_size(font, rect.dim.x, rect.dim.y);
Self {
name,
justify,
rect,
buffer,
color,
attrs,
justify: Align::Left,
attrs: AttrsOwned::new(Attrs::new()),
text: String::new(),
}
}
pub fn set_text(&mut self, state: &mut RenderState, text: &str) {
self.buffer.set_text(
&mut state.text_font_system,
text,
self.attrs.as_attrs(),
Shaping::Advanced,
);
fn reflow(&mut self, font: &mut FontSystem) {
self.buffer
.set_text(font, &self.text, self.attrs.as_attrs(), Shaping::Advanced);
for l in &mut self.buffer.lines {
l.set_align(Some(self.justify));
}
self.buffer.shape_until_scroll(&mut state.text_font_system);
self.buffer.shape_until_scroll(font);
}
pub fn set_text(&mut self, font: &mut FontSystem, text: &str) {
self.text.clear();
self.text.push_str(text);
self.reflow(font);
}
pub fn set_align(&mut self, font: &mut FontSystem, align: Align) {
self.justify = align;
self.reflow(font);
}
pub fn set_weight(&mut self, font: &mut FontSystem, weight: Weight) {
self.attrs.weight = weight;
self.reflow(font);
}
pub fn set_font(&mut self, font: &mut FontSystem, family: FamilyOwned) {
self.attrs.family_owned = family;
self.reflow(font);
}
pub fn set_style(&mut self, font: &mut FontSystem, style: Style) {
self.attrs.style = style;
self.reflow(font);
}
}
impl<'a, 'b: 'a> TextBox {
pub fn get_textarea(&'b self, state: &RenderState, input: &RenderInput) -> TextArea<'a> {
let rect = self.rect.to_centered(state, input.ct.get_config().ui_scale);
pub fn get_textarea(&'b self, input: &RenderInput, window: &Window) -> TextArea<'a> {
let rect = self
.rect
.to_centered(window, input.ct.get_config().ui_scale);
// Glypon works with physical pixels, so we must do some conversion
let fac = state.window.scale_factor() as f32;
let fac = window.scale_factor() as f32;
let corner_ne = Vector2::new(
(rect.pos.x - rect.dim.x / 2.0) * fac + state.window_size.width as f32 / 2.0,
state.window_size.height as f32 / 2.0 - (rect.pos.y * fac + rect.dim.y / 2.0),
(rect.pos.x - rect.dim.x / 2.0) * fac + window.inner_size().width as f32 / 2.0,
window.inner_size().height as f32 / 2.0 - (rect.pos.y * fac + rect.dim.y / 2.0),
);
let corner_sw = corner_ne + rect.dim * fac;
let c = self.color.as_array_u8();

View File

@ -18,3 +18,5 @@ workspace = true
[dependencies]
nalgebra = { workspace = true }
anyhow = { workspace = true }
rhai = { workspace = true }

View File

@ -2,6 +2,7 @@
//! Various utilities
use anyhow::bail;
use nalgebra::Vector2;
pub mod constants;
@ -17,3 +18,18 @@ pub fn to_radians(degrees: f32) -> f32 {
pub fn clockwise_angle(a: &Vector2<f32>, b: &Vector2<f32>) -> f32 {
(a.x * b.y - b.x * a.y).atan2(a.dot(&b))
}
/// Convert a rhai error to an anyhow error.
/// We can't do this directly, since anyhow requires send + sync,
/// and we don't need send+sync on rhai.
pub fn rhai_error_to_anyhow<T, E>(r: Result<T, E>) -> anyhow::Result<T>
where
E: ToString,
{
match r {
Ok(x) => Ok(x),
Err(e) => {
bail!("{}", e.to_string())
}
}
}