diff --git a/Cargo.lock b/Cargo.lock index 8d96b38..b7f6144 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -887,7 +887,9 @@ dependencies = [ name = "galactica-util" version = "0.0.0" dependencies = [ + "anyhow", "nalgebra", + "rhai", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1d25249..0f96a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,6 @@ rhai = { version = "1.17.0", features = [ "f32_float", "only_i32", "metadata", - "sync", "no_custom_syntax", "no_closure", ] } diff --git a/TODO.md b/TODO.md index fc58fd6..f48f604 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/content/ui/flying.rhai b/content/ui/flying.rhai index 9b337df..14be702 100644 --- a/content/ui/flying.rhai +++ b/content/ui/flying.rhai @@ -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() ); diff --git a/content/ui/landed.rhai b/content/ui/landed.rhai index 786fab2..884c031 100644 --- a/content/ui/landed.rhai +++ b/content/ui/landed.rhai @@ -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; } diff --git a/content/ui/outfitter.rhai b/content/ui/outfitter.rhai index d2338be..4b63a56 100644 --- a/content/ui/outfitter.rhai +++ b/content/ui/outfitter.rhai @@ -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; } diff --git a/crates/content/src/part/config.rs b/crates/content/src/part/config.rs index e220073..a617443 100644 --- a/crates/content/src/part/config.rs +++ b/crates/content/src/part/config.rs @@ -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}`"))?, ); } diff --git a/crates/render/src/gpustate.rs b/crates/render/src/gpustate.rs index 950fa4a..f792588 100644 --- a/crates/render/src/gpustate.rs +++ b/crates/render/src/gpustate.rs @@ -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. diff --git a/crates/render/src/renderstate.rs b/crates/render/src/renderstate.rs index 1cced31..fa16909 100644 --- a/crates/render/src/renderstate.rs +++ b/crates/render/src/renderstate.rs @@ -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>, + pub text_renderer: TextRenderer, pub text_cache: SwashCache, pub text_atlas: TextAtlas, - pub text_renderer: TextRenderer, } impl RenderState { diff --git a/crates/render/src/ui/api/anchor.rs b/crates/render/src/ui/api/anchor.rs new file mode 100644 index 0000000..992e60a --- /dev/null +++ b/crates/render/src/ui/api/anchor.rs @@ -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; +} diff --git a/crates/render/src/ui/api/config.rs b/crates/render/src/ui/api/config.rs deleted file mode 100644 index 786c173..0000000 --- a/crates/render/src/ui/api/config.rs +++ /dev/null @@ -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) { - 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 - }); - } -} diff --git a/crates/render/src/ui/api/event.rs b/crates/render/src/ui/api/event.rs index f112cec..df2b6ed 100644 --- a/crates/render/src/ui/api/event.rs +++ b/crates/render/src/ui/api/event.rs @@ -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 { diff --git a/crates/render/src/ui/api/mod.rs b/crates/render/src/ui/api/mod.rs index 2029067..2951414 100644 --- a/crates/render/src/ui/api/mod.rs +++ b/crates/render/src/ui/api/mod.rs @@ -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::() .build_type::() - .build_type::() // State .build_type::() .build_type::() .build_type::() - // Builders - .build_type::() - .build_type::() - .build_type::() - // Elements - .build_type::() - .build_type::() // Events .build_type::() .build_type::() .build_type::() - // Larger modules - .register_type_with_name::("SpriteAnchor") - .register_static_module("SpriteAnchor", exported_module!(spriteanchor_mod).into()) - .register_type_with_name::("SceneAction") - .register_static_module("SceneAction", exported_module!(sceneaction_mod).into()); + // Bigger modules + .register_type_with_name::("Anchor") + .register_static_module("Anchor", exported_module!(anchor_mod).into()); } diff --git a/crates/render/src/ui/api/radialbuilder.rs b/crates/render/src/ui/api/radialbuilder.rs deleted file mode 100644 index 0df357c..0000000 --- a/crates/render/src/ui/api/radialbuilder.rs +++ /dev/null @@ -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) { - builder - .with_name("RadialBuilder") - .with_fn("RadialBuilder", Self::new) - .with_fn("set_progress", Self::set_progress); - } -} diff --git a/crates/render/src/ui/api/radialelement.rs b/crates/render/src/ui/api/radialelement.rs deleted file mode 100644 index 336e9a4..0000000 --- a/crates/render/src/ui/api/radialelement.rs +++ /dev/null @@ -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>, - pub ct: Arc, -} - -// TODO: remove this -unsafe impl Send for RadialElement {} -unsafe impl Sync for RadialElement {} - -impl RadialElement { - pub fn new(ct: Arc, bar: Rc>) -> Self { - Self { ct, bar } - } -} - -impl CustomType for RadialElement { - fn build(mut builder: TypeBuilder) { - builder - .with_name("RadialElement") - .with_fn("set_val", |s: &mut Self, val: f32| { - s.bar.borrow_mut().set_val(val) - }); - } -} diff --git a/crates/render/src/ui/api/rect.rs b/crates/render/src/ui/api/rect.rs index 9c4a413..ab8ab51 100644 --- a/crates/render/src/ui/api/rect.rs +++ b/crates/render/src/ui/api/rect.rs @@ -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, pub dim: Vector2, - 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 = state.window_size.to_logical(state.window.scale_factor()); + pub fn to_centered(&self, window: &Window, ui_scale: f32) -> CenteredRect { + let w: LogicalSize = 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; } }; diff --git a/crates/render/src/ui/api/sceneaction.rs b/crates/render/src/ui/api/sceneaction.rs deleted file mode 100644 index 46d4892..0000000 --- a/crates/render/src/ui/api/sceneaction.rs +++ /dev/null @@ -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) - } -} diff --git a/crates/render/src/ui/api/spritebuilder.rs b/crates/render/src/ui/api/spritebuilder.rs deleted file mode 100644 index 92ddf59..0000000 --- a/crates/render/src/ui/api/spritebuilder.rs +++ /dev/null @@ -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, -} - -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) { - builder - .with_name("SpriteBuilder") - .with_fn("SpriteBuilder", Self::new) - .with_fn("set_mask", Self::set_mask); - } -} diff --git a/crates/render/src/ui/api/spriteelement.rs b/crates/render/src/ui/api/spriteelement.rs deleted file mode 100644 index d0ef235..0000000 --- a/crates/render/src/ui/api/spriteelement.rs +++ /dev/null @@ -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>, - pub ct: Arc, -} - -// TODO: remove this -unsafe impl Send for SpriteElement {} -unsafe impl Sync for SpriteElement {} - -impl SpriteElement { - pub fn new(ct: Arc, sprite: Rc>) -> 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) { - builder - .with_name("SpriteElement") - .with_fn("has_name", Self::has_name) - .with_fn("take_edge", Self::take_edge); - } -} diff --git a/crates/render/src/ui/api/textboxbuilder.rs b/crates/render/src/ui/api/textboxbuilder.rs deleted file mode 100644 index f296173..0000000 --- a/crates/render/src/ui/api/textboxbuilder.rs +++ /dev/null @@ -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) { - 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); - } -} diff --git a/crates/render/src/ui/api/util.rs b/crates/render/src/ui/api/util.rs deleted file mode 100644 index 5f5b807..0000000 --- a/crates/render/src/ui/api/util.rs +++ /dev/null @@ -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; -} diff --git a/crates/render/src/ui/executor.rs b/crates/render/src/ui/executor.rs new file mode 100644 index 0000000..81c5baa --- /dev/null +++ b/crates/render/src/ui/executor.rs @@ -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>, + + last_player_state: u32, + last_scene: Option, +} + +impl UiScriptExecutor { + pub fn new(ct: Arc, 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) -> 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) -> 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, + font_src: Rc>, + s: Rc>, + 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}`") + } + } + }, + ); + } + } +} diff --git a/crates/render/src/ui/manager.rs b/crates/render/src/ui/manager.rs deleted file mode 100644 index 8c8cb8c..0000000 --- a/crates/render/src/ui/manager.rs +++ /dev/null @@ -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, - current_scene_config: SceneConfig, - engine: Engine, - scope: Scope<'static>, - ct: Arc, - - /// UI elements - elements: Vec, - /// 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, 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, - 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::() { - let s = v.cast::(); - 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::() { - let r = v.cast::(); - 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::() { - let t = v.cast::(); - 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, 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::() { - 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::() { - 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, - action: SceneAction, - ) -> Result { - 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> { - 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 - } -} diff --git a/crates/render/src/ui/mod.rs b/crates/render/src/ui/mod.rs index 193d98b..34d07ae 100644 --- a/crates/render/src/ui/mod.rs +++ b/crates/render/src/ui/mod.rs @@ -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::*; diff --git a/crates/render/src/ui/state.rs b/crates/render/src/ui/state.rs new file mode 100644 index 0000000..94d6ceb --- /dev/null +++ b/crates/render/src/ui/state.rs @@ -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, + pub elements: Vec, + + pub ct: Arc, + current_scene: Option, + + 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, 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 { + &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) { + 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> { + 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; + } +} diff --git a/crates/render/src/ui/uielement.rs b/crates/render/src/ui/uielement.rs deleted file mode 100644 index 9df8931..0000000 --- a/crates/render/src/ui/uielement.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use super::util::{RadialBar, Sprite, TextBox}; - -#[derive(Debug)] -pub enum UiElement { - Sprite(Rc>), - RadialBar(Rc>), - 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, - } - } -} diff --git a/crates/render/src/ui/util/fpsindicator.rs b/crates/render/src/ui/util/fpsindicator.rs index 59c40c0..fbef304 100644 --- a/crates/render/src/ui/util/fpsindicator.rs +++ b/crates/render/src/ui/util/fpsindicator.rs @@ -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, diff --git a/crates/render/src/ui/util/radialbar.rs b/crates/render/src/ui/util/radialbar.rs index 5cc8277..fe5c8ff 100644 --- a/crates/render/src/ui/util/radialbar.rs +++ b/crates/render/src/ui/util/radialbar.rs @@ -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], diff --git a/crates/render/src/ui/util/sprite.rs b/crates/render/src/ui/util/sprite.rs index 956beb9..493755e 100644 --- a/crates/render/src/ui/util/sprite.rs +++ b/crates/render/src/ui/util/sprite.rs @@ -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, + pub anim: SpriteAutomaton, + pub name: ImmutableString, rect: Rect, mask: Option, @@ -22,57 +19,30 @@ pub struct Sprite { } impl Sprite { - pub fn new( - ct: &Content, - name: String, - sprite: String, - mask: Option, - 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) { + 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); } } diff --git a/crates/render/src/ui/util/textbox.rs b/crates/render/src/ui/util/textbox.rs index a36372c..a3bcc69 100644 --- a/crates/render/src/ui/util/textbox.rs +++ b/crates/render/src/ui/util/textbox.rs @@ -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(); diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 89feb18..2f830f9 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -18,3 +18,5 @@ workspace = true [dependencies] nalgebra = { workspace = true } +anyhow = { workspace = true } +rhai = { workspace = true } diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 525ee53..afe7813 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -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, b: &Vector2) -> 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(r: Result) -> anyhow::Result +where + E: ToString, +{ + match r { + Ok(x) => Ok(x), + Err(e) => { + bail!("{}", e.to_string()) + } + } +}