From b170f3f53f48dc490de835573eb917d4dd99169c Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 7 Feb 2024 15:58:14 -0800 Subject: [PATCH] Reworked renderer for Directives Added OwnedTextArea & reworked textarea creation Added ScrollBox --- crates/render/src/gpustate.rs | 74 ++++-- crates/render/src/inputevent.rs | 29 +++ crates/render/src/lib.rs | 4 +- crates/render/src/renderinput.rs | 10 - crates/render/src/ui/api/directive.rs | 28 +++ crates/render/src/ui/api/event.rs | 28 +++ crates/render/src/ui/api/functions/mod.rs | 2 + .../render/src/ui/api/functions/radialbar.rs | 12 +- .../render/src/ui/api/functions/scrollbox.rs | 77 +++++++ crates/render/src/ui/api/functions/sprite.rs | 29 +-- crates/render/src/ui/api/functions/textbox.rs | 20 +- crates/render/src/ui/api/functions/ui.rs | 12 + crates/render/src/ui/api/helpers/rect.rs | 22 +- crates/render/src/ui/api/mod.rs | 29 ++- crates/render/src/ui/api/state.rs | 79 +++++-- crates/render/src/ui/camera.rs | 35 +++ crates/render/src/ui/elements/fpsindicator.rs | 21 +- crates/render/src/ui/elements/mod.rs | 29 +++ crates/render/src/ui/elements/radialbar.rs | 6 +- crates/render/src/ui/elements/scrollbox.rs | 136 +++++++++++ crates/render/src/ui/elements/sprite.rs | 104 +++++---- crates/render/src/ui/elements/textbox.rs | 41 ++-- crates/render/src/ui/event.rs | 8 - crates/render/src/ui/executor.rs | 217 +++++++++++------- crates/render/src/ui/mod.rs | 6 +- crates/render/src/ui/state.rs | 90 +++++++- 26 files changed, 884 insertions(+), 264 deletions(-) create mode 100644 crates/render/src/inputevent.rs create mode 100644 crates/render/src/ui/api/directive.rs create mode 100644 crates/render/src/ui/api/functions/scrollbox.rs create mode 100644 crates/render/src/ui/camera.rs create mode 100644 crates/render/src/ui/elements/scrollbox.rs delete mode 100644 crates/render/src/ui/event.rs diff --git a/crates/render/src/gpustate.rs b/crates/render/src/gpustate.rs index 2e79d3f..e072b09 100644 --- a/crates/render/src/gpustate.rs +++ b/crates/render/src/gpustate.rs @@ -1,13 +1,13 @@ use anyhow::Result; use bytemuck; use galactica_content::Content; -use galactica_system::data::ShipState; +use galactica_system::{data::ShipState, phys::PhysSimShipHandle, PlayerDirective}; use galactica_util::to_radians; use glyphon::{FontSystem, Resolution, SwashCache, TextAtlas, TextRenderer}; -use nalgebra::{Point2, Point3}; +use nalgebra::{Point2, Point3, Vector2}; use std::{cell::RefCell, iter, rc::Rc, sync::Arc}; use wgpu; -use winit; +use winit::{self}; use crate::{ globaluniform::{GlobalDataContent, GlobalUniform, ObjectData}, @@ -17,7 +17,7 @@ use crate::{ texturearray::TextureArray, ui::UiScriptExecutor, vertexbuffer::{consts::SPRITE_INDICES, types::ObjectInstance}, - RenderInput, RenderState, VertexBuffers, + InputEvent, RenderInput, RenderState, VertexBuffers, }; /// A high-level GPU wrapper. Reads game state (via RenderInput), produces pretty pictures. @@ -259,6 +259,8 @@ impl GPUState { self.config.height = new_size.height; self.surface.configure(&self.device, &self.config); } + + // TODO: this takes a long time. fix! self.starfield.update_buffer(ct, &mut self.state); } @@ -274,18 +276,54 @@ impl GPUState { self.starfield.update_buffer(ct, &mut self.state); } + /// Handle user input + pub fn process_input( + &mut self, + input: RenderInput, + event: InputEvent, + ) -> Result { + let input = Arc::new(input); + self.ui.process_input(&mut self.state, input, event) + } + /// Main render function. Draws sprites on a window. pub fn render(&mut self, input: RenderInput) -> Result<(), wgpu::SurfaceError> { let input = Arc::new(input); + if let Some(ship) = input.player.ship { + let o = input.phys_img.get_ship(&PhysSimShipHandle(ship)); + if let Some(o) = o { + match o.ship.get_data().get_state() { + ShipState::Landing { .. } + | ShipState::UnLanding { .. } + | ShipState::Collapsing { .. } + | ShipState::Flying { .. } => self + .ui + .state + .borrow_mut() + .camera + .set_pos(*o.rigidbody.translation()), + + ShipState::Landed { target } => self + .ui + .state + .borrow_mut() + .camera + .set_pos(Vector2::new(target.pos.x, target.pos.y)), + + ShipState::Dead => {} + } + } + } + // Update global values self.state.queue.write_buffer( &self.state.global_uniform.data_buffer, 0, bytemuck::cast_slice(&[GlobalDataContent { - camera_position_x: input.camera_pos.x, - camera_position_y: input.camera_pos.y, - camera_zoom: input.camera_zoom, + camera_position_x: self.ui.state.borrow().camera.get_pos().x, + camera_position_y: self.ui.state.borrow().camera.get_pos().y, + camera_zoom: self.ui.state.borrow().camera.get_zoom(), camera_zoom_min: input.ct.config.zoom_min, camera_zoom_max: input.ct.config.zoom_max, window_size_w: self.state.window_size.width as f32, @@ -338,8 +376,10 @@ impl GPUState { // Game coordinates (relative to camera) of ne and sw corners of screen. // Used to skip off-screen sprites. - let clip_ne = Point2::new(-self.state.window_aspect, 1.0) * input.camera_zoom; - let clip_sw = Point2::new(self.state.window_aspect, -1.0) * input.camera_zoom; + let clip_ne = Point2::new(-self.state.window_aspect, 1.0) + * self.ui.state.borrow().camera.get_zoom(); + let clip_sw = Point2::new(self.state.window_aspect, -1.0) + * self.ui.state.borrow().camera.get_zoom(); // Order matters, it determines what is drawn on top. // The order inside ships and projectiles doesn't matter, @@ -424,7 +464,9 @@ impl GPUState { }, (*self.ui.state) .borrow_mut() - .get_textareas(&input, &self.state.window), + .get_textareas(&input, &self.state.window) + .iter() + .map(|x| x.get_textarea()), &mut self.state.text_cache, ) .unwrap(); @@ -483,8 +525,9 @@ impl GPUState { // Position adjusted for parallax // TODO: adjust parallax for zoom? // 1.0 is z-coordinate, which is constant for ships - let pos: Point2 = - (Point2::new(ship_pos.x, ship_pos.y) - input.camera_pos) / ship_pos.z; + let pos: Point2 = (Point2::new(ship_pos.x, ship_pos.y) + - self.ui.state.borrow().camera.get_pos()) + / ship_pos.z; // Game dimensions of this sprite post-scale. // Post-scale width or height, whichever is larger. @@ -588,7 +631,7 @@ impl GPUState { // Position adjusted for parallax // TODO: adjust parallax for zoom? // 1.0 is z-coordinate, which is constant for projectiles - let pos = (proj_pos - input.camera_pos) / 1.0; + let pos = (proj_pos - self.ui.state.borrow().camera.get_pos()) / 1.0; // Game dimensions of this sprite post-scale. // Post-scale width or height, whichever is larger. @@ -649,7 +692,8 @@ impl GPUState { for o in v { // Position adjusted for parallax - let pos: Point2 = (Point2::new(o.pos.x, o.pos.y) - input.camera_pos) / o.pos.z; + let pos: Point2 = + (Point2::new(o.pos.x, o.pos.y) - self.ui.state.borrow().camera.get_pos()) / o.pos.z; // Game dimensions of this sprite post-scale. // Post-scale width or height, whichever is larger. @@ -711,7 +755,7 @@ impl GPUState { // Position adjusted for parallax // TODO: adjust parallax for zoom? // 1.0 is z-coordinate, which is constant for projectiles - let adjusted_pos = (pos - input.camera_pos) / 1.0; + let adjusted_pos = (pos - self.ui.state.borrow().camera.get_pos()) / 1.0; // Game dimensions of this sprite post-scale. // Post-scale width or height, whichever is larger. diff --git a/crates/render/src/inputevent.rs b/crates/render/src/inputevent.rs new file mode 100644 index 0000000..430a3b6 --- /dev/null +++ b/crates/render/src/inputevent.rs @@ -0,0 +1,29 @@ +use winit::{dpi::PhysicalPosition, event::VirtualKeyCode}; + +/// Input received from the user +#[derive(Debug)] +pub enum InputEvent { + /// Mouse was moved + MouseMove(PhysicalPosition), + + /// Mouse left button was clicked. + /// True if pressed, false if released. + MouseLeftClick(bool), + + /// Mouse left button was clicked. + /// True if pressed, false if released. + MouseRightClick(bool), + + /// Mouse was scrolled. + /// Value is number of lines, positive or negative. + Scroll(f32), + + /// A key was pressed or released + Keyboard { + /// True if pressed, false if released + down: bool, + + /// The key that was pressed + key: VirtualKeyCode, + }, +} diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 426ea89..45a2013 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -9,6 +9,7 @@ mod globaluniform; mod gpustate; +mod inputevent; mod pipeline; mod renderinput; mod renderstate; @@ -18,7 +19,8 @@ mod texturearray; mod ui; mod vertexbuffer; -pub use gpustate::GPUState; +pub use gpustate::*; +pub use inputevent::*; pub use renderinput::RenderInput; use renderstate::*; diff --git a/crates/render/src/renderinput.rs b/crates/render/src/renderinput.rs index 71eecc8..a7ddc04 100644 --- a/crates/render/src/renderinput.rs +++ b/crates/render/src/renderinput.rs @@ -4,23 +4,16 @@ use galactica_content::{Content, System}; use galactica_playeragent::PlayerAgent; use galactica_system::phys::PhysImage; use galactica_util::timing::Timing; -use nalgebra::Vector2; /// Bundles parameters passed to a single call to GPUState::render #[derive(Debug)] pub struct RenderInput { - /// Camera position, in world units - pub camera_pos: Vector2, - /// Player ship data pub player: Arc, /// The system we're currently in pub current_system: Arc, - /// Height of screen, in world units - pub camera_zoom: f32, - /// The world state to render pub phys_img: Arc, @@ -28,9 +21,6 @@ pub struct RenderInput { /// The current time, in seconds pub current_time: f32, - /// The amount of time that has passed since the last frame was drawn - pub time_since_last_run: f32, - /// Game content pub ct: Arc, diff --git a/crates/render/src/ui/api/directive.rs b/crates/render/src/ui/api/directive.rs new file mode 100644 index 0000000..efabdd7 --- /dev/null +++ b/crates/render/src/ui/api/directive.rs @@ -0,0 +1,28 @@ +use rhai::{plugin::*, Dynamic, Module}; + +#[export_module] +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +pub mod player_directive_module { + use galactica_system::PlayerDirective; + + pub const None: PlayerDirective = PlayerDirective::None; + pub const Land: PlayerDirective = PlayerDirective::Land; + pub const UnLand: PlayerDirective = PlayerDirective::UnLand; + + pub fn Engine(state: bool) -> PlayerDirective { + PlayerDirective::Engine(state) + } + + pub fn TurnLeft(state: bool) -> PlayerDirective { + PlayerDirective::TurnLeft(state) + } + + pub fn TurnRight(state: bool) -> PlayerDirective { + PlayerDirective::TurnRight(state) + } + + pub fn Guns(state: bool) -> PlayerDirective { + PlayerDirective::Guns(state) + } +} diff --git a/crates/render/src/ui/api/event.rs b/crates/render/src/ui/api/event.rs index df2b6ed..ff65abb 100644 --- a/crates/render/src/ui/api/event.rs +++ b/crates/render/src/ui/api/event.rs @@ -38,3 +38,31 @@ impl CustomType for PlayerShipStateEvent { builder.with_name("PlayerShipStateEvent"); } } + +#[derive(Debug, Clone)] +pub struct KeyboardEvent { + pub down: bool, + pub key: ImmutableString, +} + +impl CustomType for KeyboardEvent { + fn build(mut builder: TypeBuilder) { + builder + .with_name("KeyboardEvent") + .with_fn("is_down", |s: &mut Self| s.down) + .with_fn("key", |s: &mut Self| s.key.clone()); + } +} + +#[derive(Debug, Clone)] +pub struct ScrollEvent { + pub val: f32, +} + +impl CustomType for ScrollEvent { + fn build(mut builder: TypeBuilder) { + builder + .with_name("ScrollEvent") + .with_fn("val", |s: &mut Self| s.val); + } +} diff --git a/crates/render/src/ui/api/functions/mod.rs b/crates/render/src/ui/api/functions/mod.rs index be7b3a5..058568c 100644 --- a/crates/render/src/ui/api/functions/mod.rs +++ b/crates/render/src/ui/api/functions/mod.rs @@ -1,11 +1,13 @@ mod conf; mod radialbar; +mod scrollbox; mod sprite; mod textbox; mod ui; pub use conf::build_conf_module; pub use radialbar::build_radialbar_module; +pub use scrollbox::build_scrollbox_module; pub use sprite::build_sprite_module; pub use textbox::build_textbox_module; pub use ui::build_ui_module; diff --git a/crates/render/src/ui/api/functions/radialbar.rs b/crates/render/src/ui/api/functions/radialbar.rs index f725ad5..a1d5dd3 100644 --- a/crates/render/src/ui/api/functions/radialbar.rs +++ b/crates/render/src/ui/api/functions/radialbar.rs @@ -3,7 +3,7 @@ use rhai::{FnNamespace, FuncRegistration, ImmutableString, Module}; use std::{cell::RefCell, rc::Rc}; use super::super::{Color, Rect}; -use crate::ui::{elements::RadialBar, UiElement, UiState}; +use crate::ui::{elements::UiRadialBar, UiElement, UiState}; pub fn build_radialbar_module(state_src: Rc>) -> Module { let mut module = Module::new(); @@ -23,11 +23,13 @@ pub fn build_radialbar_module(state_src: Rc>) -> Module { return; } - ui_state.names.push(name.clone()); - ui_state.elements.insert( + ui_state.add_element(UiElement::RadialBar(UiRadialBar::new( name.clone(), - UiElement::RadialBar(RadialBar::new(name.clone(), stroke, color, rect, 1.0)), - ); + stroke, + color, + rect, + 1.0, + ))); }, ); diff --git a/crates/render/src/ui/api/functions/scrollbox.rs b/crates/render/src/ui/api/functions/scrollbox.rs new file mode 100644 index 0000000..26207d7 --- /dev/null +++ b/crates/render/src/ui/api/functions/scrollbox.rs @@ -0,0 +1,77 @@ +use log::error; +use rhai::{FnNamespace, FuncRegistration, ImmutableString, Module}; +use std::{cell::RefCell, rc::Rc}; + +use super::super::Rect; +use crate::ui::{elements::UiScrollbox, UiElement, UiState}; + +pub fn build_scrollbox_module(state_src: Rc>) -> Module { + let mut module = Module::new(); + module.set_id("GalacticaScrollboxModule"); + + let state = state_src.clone(); + let _ = FuncRegistration::new("add") + .with_namespace(FnNamespace::Internal) + .set_into_module(&mut module, move |name: ImmutableString, rect: Rect| { + let mut ui_state = state.borrow_mut(); + + if ui_state.elements.contains_key(&name) { + error!("tried to make a scrollbox using an existing name `{name}`"); + return; + } + + ui_state.add_element(UiElement::Scrollbox(UiScrollbox::new(name.clone(), rect))); + }); + + let state = state_src.clone(); + let _ = FuncRegistration::new("add_element") + .with_namespace(FnNamespace::Internal) + .set_into_module( + &mut module, + move |name: ImmutableString, target: ImmutableString| { + let mut ui_state = state.borrow_mut(); + match ui_state.get_mut_by_name(&name) { + Some(UiElement::Scrollbox(_)) => { + match ui_state.get_mut_by_name(&target) { + Some(UiElement::Text(_)) | Some(UiElement::Sprite(_)) => { + let e = match ui_state.remove_element_incomplete(&target) { + Some(UiElement::Sprite(s)) => { + Rc::new(RefCell::new(UiElement::Sprite(s))) + } + Some(UiElement::Text(t)) => { + Rc::new(RefCell::new(UiElement::Text(t))) + } + _ => unreachable!(), + }; + + // Add a subelement pointing to this sprite + ui_state.add_element(UiElement::SubElement { + parent: name.clone(), + element: e.clone(), + }); + + // Add this sprite to a scrollbox + match ui_state.get_mut_by_name(&name) { + Some(UiElement::Scrollbox(s)) => { + s.add_element(e); + } + _ => unreachable!(), + }; + } + Some(_) => { + error!("cannot add `{name}` to scrollbox `{name}`, invalid type.") + } + None => { + error!("called `scrollbox::add_element` with a non-existing target `{target}`") + } + } + } + _ => { + error!("called `scrollbox::add_element` on an invalid name `{name}`") + } + } + }, + ); + + return module; +} diff --git a/crates/render/src/ui/api/functions/sprite.rs b/crates/render/src/ui/api/functions/sprite.rs index bd5c2a6..11d6ff6 100644 --- a/crates/render/src/ui/api/functions/sprite.rs +++ b/crates/render/src/ui/api/functions/sprite.rs @@ -17,12 +17,12 @@ pub fn build_sprite_module(ct_src: Arc, state_src: Rc> .with_namespace(FnNamespace::Internal) .set_into_module( &mut module, - move |name: ImmutableString, sprite: ImmutableString, rect: Rect| { + move |name: ImmutableString, sprite_name: ImmutableString, rect: Rect| { let mut ui_state = state.borrow_mut(); - let sprite_handle = ct.sprites.get(&sprite.clone().into()); - if sprite_handle.is_none() { - error!("made a sprite using an invalid source `{sprite}`"); + let sprite = ct.sprites.get(&sprite_name.clone().into()); + if sprite.is_none() { + error!("made a sprite using an invalid source `{sprite_name}`"); return; } @@ -31,15 +31,11 @@ pub fn build_sprite_module(ct_src: Arc, state_src: Rc> return; } - ui_state.names.push(name.clone()); - ui_state.elements.insert( + ui_state.add_element(UiElement::Sprite(UiSprite::new( name.clone(), - UiElement::Sprite(UiSprite::new( - name.clone(), - sprite_handle.unwrap().clone(), - rect, - )), - ); + sprite.unwrap().clone(), + rect, + ))); }, ); @@ -60,8 +56,7 @@ pub fn build_sprite_module(ct_src: Arc, state_src: Rc> .set_into_module(&mut module, move |name: ImmutableString| { let mut ui_state = state.borrow_mut(); if ui_state.elements.contains_key(&name) { - ui_state.elements.remove(&name).unwrap(); - ui_state.names.retain(|x| *x != name); + ui_state.remove_element(&name); } else { error!("called `sprite::remove` on an invalid name `{name}`") } @@ -94,7 +89,7 @@ pub fn build_sprite_module(ct_src: Arc, state_src: Rc> ); let state = state_src.clone(); - let _ = FuncRegistration::new("take_edge") + let _ = FuncRegistration::new("jump_to") .with_namespace(FnNamespace::Internal) .set_into_module( &mut module, @@ -110,7 +105,7 @@ pub fn build_sprite_module(ct_src: Arc, state_src: Rc> let edge = match edge { Err(_) => { error!( - "called `sprite::take_edge` on an invalid edge `{}` on sprite `{}`", + "called `sprite::jump_to` on an invalid edge `{}` on sprite `{}`", edge_name, sprite.index ); return; @@ -121,7 +116,7 @@ pub fn build_sprite_module(ct_src: Arc, state_src: Rc> x.anim.jump_to(&edge); } _ => { - error!("called `sprite::take_edge` on an invalid name `{name}`") + error!("called `sprite::jump_to` on an invalid name `{name}`") } } }, diff --git a/crates/render/src/ui/api/functions/textbox.rs b/crates/render/src/ui/api/functions/textbox.rs index a021aa3..d5e8a57 100644 --- a/crates/render/src/ui/api/functions/textbox.rs +++ b/crates/render/src/ui/api/functions/textbox.rs @@ -4,7 +4,7 @@ use rhai::{FnNamespace, FuncRegistration, ImmutableString, Module}; use std::{cell::RefCell, rc::Rc}; use super::super::{Color, Rect}; -use crate::ui::{elements::TextBox, UiElement, UiState}; +use crate::ui::{elements::UiTextBox, UiElement, UiState}; pub fn build_textbox_module( font_src: Rc>, @@ -32,18 +32,14 @@ pub fn build_textbox_module( return; } - ui_state.names.push(name.clone()); - ui_state.elements.insert( + ui_state.add_element(UiElement::Text(UiTextBox::new( + &mut font.borrow_mut(), name.clone(), - UiElement::Text(TextBox::new( - &mut font.borrow_mut(), - name.clone(), - font_size, - line_height, - rect, - color, - )), - ); + font_size, + line_height, + rect, + color, + ))); }, ); diff --git a/crates/render/src/ui/api/functions/ui.rs b/crates/render/src/ui/api/functions/ui.rs index 7b9c413..54f0ad9 100644 --- a/crates/render/src/ui/api/functions/ui.rs +++ b/crates/render/src/ui/api/functions/ui.rs @@ -15,5 +15,17 @@ pub fn build_ui_module(state_src: Rc>) -> Module { ui_state.set_scene(scene); }); + let state = state_src.clone(); + let _ = FuncRegistration::new("get_camera_zoom") + .with_namespace(FnNamespace::Internal) + .set_into_module(&mut module, move || state.borrow().camera.get_zoom()); + + let state = state_src.clone(); + let _ = FuncRegistration::new("set_camera_zoom") + .with_namespace(FnNamespace::Internal) + .set_into_module(&mut module, move |z: f32| { + state.borrow_mut().camera.set_zoom(z) + }); + return module; } diff --git a/crates/render/src/ui/api/helpers/rect.rs b/crates/render/src/ui/api/helpers/rect.rs index 1bd0bcc..bc5e145 100644 --- a/crates/render/src/ui/api/helpers/rect.rs +++ b/crates/render/src/ui/api/helpers/rect.rs @@ -1,9 +1,12 @@ use nalgebra::{Point2, Vector2}; use rhai::{CustomType, TypeBuilder}; -use winit::{dpi::LogicalSize, window::Window}; +use winit::{ + dpi::{LogicalSize, PhysicalPosition}, + window::Window, +}; -use super::anchor::Anchor; -use crate::{RenderInput, RenderState}; +use super::{anchor::Anchor, vector::UiVector}; +use crate::RenderState; #[derive(Debug, Clone)] pub struct Rect { @@ -78,7 +81,11 @@ impl Rect { impl CustomType for Rect { fn build(mut builder: TypeBuilder) { - builder.with_name("Rect").with_fn("Rect", Self::new); + builder + .with_name("Rect") + .with_fn("Rect", Self::new) + .with_fn("pos", |s: &mut Self| UiVector::new(s.pos.x, s.pos.y)) + .with_fn("dim", |s: &mut Self| UiVector::new(s.dim.x, s.dim.y)); } } @@ -100,17 +107,16 @@ impl CenteredRect { return (pt.y < ne.y && pt.y > sw.y) && (pt.x > ne.x && pt.x < sw.x); } - pub fn contains_mouse(&self, input: &RenderInput, state: &RenderState) -> bool { + pub fn contains_mouse(&self, state: &RenderState, mouse_pos: &PhysicalPosition) -> bool { let fac = state.window.scale_factor() as f32; let window_size = Vector2::new( state.window_size.width as f32 / fac, state.window_size.height as f32 / fac, ); - let pos = input.player.input.get_mouse_pos(); let mouse_pos = Point2::new( - pos.x / fac - window_size.x / 2.0, - window_size.y / 2.0 - pos.y / fac, + mouse_pos.x / fac - window_size.x / 2.0, + window_size.y / 2.0 - mouse_pos.y / fac, ); return self.contains_point(mouse_pos); diff --git a/crates/render/src/ui/api/mod.rs b/crates/render/src/ui/api/mod.rs index fe37744..7bfdf8e 100644 --- a/crates/render/src/ui/api/mod.rs +++ b/crates/render/src/ui/api/mod.rs @@ -1,17 +1,19 @@ +mod directive; mod event; mod functions; mod helpers; mod state; +pub use directive::*; pub use event::*; -use glyphon::FontSystem; pub use helpers::{anchor::*, color::*, rect::*, vector::*}; -use log::debug; pub use state::*; use super::UiState; use galactica_content::Content; -use rhai::{exported_module, Dynamic, Engine}; +use galactica_system::PlayerDirective; +use glyphon::FontSystem; +use rhai::{exported_module, Engine}; use std::{cell::RefCell, rc::Rc, sync::Arc}; pub fn register_into_engine( @@ -29,21 +31,27 @@ pub fn register_into_engine( .build_type::() .build_type::() .build_type::() + .build_type::() // Events .build_type::() .build_type::() .build_type::() + .build_type::() + .build_type::() // Bigger modules .register_type_with_name::("Anchor") - .register_static_module("Anchor", exported_module!(anchor_mod).into()); + .register_static_module("Anchor", exported_module!(anchor_mod).into()) + .register_type_with_name::("PlayerDirective") + .register_static_module( + "PlayerDirective", + exported_module!(player_directive_module).into(), + ); // Extra functions - engine.register_fn("print", move |d: Dynamic| { - debug!("{:?}", d); - }); - engine.register_fn("clamp", move |x: f32, l: f32, h: f32| x.clamp(l, h)); + engine.register_fn("clamp", |x: f32, l: f32, h: f32| x.clamp(l, h)); // Modules + engine.register_static_module("ui", functions::build_ui_module(state_src.clone()).into()); engine.register_static_module( "sprite", functions::build_sprite_module(ct_src.clone(), state_src.clone()).into(), @@ -56,7 +64,10 @@ pub fn register_into_engine( "radialbar", functions::build_radialbar_module(state_src.clone()).into(), ); - engine.register_static_module("ui", functions::build_ui_module(state_src.clone()).into()); + engine.register_static_module( + "scrollbox", + functions::build_scrollbox_module(state_src.clone()).into(), + ); engine.register_static_module( "conf", functions::build_conf_module(state_src.clone()).into(), diff --git a/crates/render/src/ui/api/state.rs b/crates/render/src/ui/api/state.rs index 4ea09f3..733a528 100644 --- a/crates/render/src/ui/api/state.rs +++ b/crates/render/src/ui/api/state.rs @@ -1,4 +1,4 @@ -use galactica_content::{Ship, SystemObject}; +use galactica_content::{Outfit, Ship, SystemObject}; use galactica_system::{ data::{self}, phys::{objects::PhysShip, PhysSimShipHandle}, @@ -19,14 +19,15 @@ pub struct ShipState { } impl ShipState { + // All functions passed to rhai MUST be mut, + // even getters. fn get_content(&mut self) -> &Ship { let ship = self .input .phys_img .get_ship(self.ship.as_ref().unwrap()) .unwrap(); - let handle = ship.ship.get_data().get_content(); - handle + ship.ship.get_data().get_content() } fn get_ship(&mut self) -> &PhysShip { @@ -116,28 +117,64 @@ impl CustomType for ShipState { } } +#[derive(Debug, Clone)] +pub struct OutfitState { + outfit: Arc, +} + +impl OutfitState {} + +impl CustomType for OutfitState { + fn build(mut builder: TypeBuilder) { + builder + .with_name("OutfitState") + .with_fn("display_name", |s: &mut Self| s.outfit.display_name.clone()) + .with_fn("index", |s: &mut Self| s.outfit.index.to_string()) + .with_fn("thumbnail", |s: &mut Self| { + s.outfit.thumbnail.index.to_string() + }); + } +} + #[derive(Debug, Clone)] pub struct SystemObjectState { object: Option>, } -impl SystemObjectState {} +impl SystemObjectState { + fn outfitter(&mut self) -> Array { + let mut a = Array::new(); + for o in &self + .object + .as_ref() + .unwrap() + .landable + .as_ref() + .unwrap() + .outfitter + { + a.push(Dynamic::from(OutfitState { outfit: o.clone() })); + } + return a; + } +} impl CustomType for SystemObjectState { fn build(mut builder: TypeBuilder) { builder .with_name("SystemObjectState") + .with_fn("outfitter", Self::outfitter) // // Get landable name .with_fn("display_name", |s: &mut Self| { s.object .as_ref() .unwrap() - .display_name + .landable .as_ref() - .map(|x| x.to_string()) + .map(|x| x.display_name.clone()) .unwrap_or_else(|| { - error!("UI called `name()` on a system object which doesn't provide one"); + error!("UI called `name()` on a system object which isn't landable"); "".to_string() }) }) @@ -147,25 +184,32 @@ impl CustomType for SystemObjectState { s.object .as_ref() .unwrap() - .desc + .landable .as_ref() - .map(|x| x.to_string()) + .map(|x| x.desc.clone()) .unwrap_or_else(|| { - error!("UI called `name()` on a system object which doesn't provide one"); + error!("UI called `desc()` on a system object which isn't landable"); "".to_string() }) }) // // Get landable landscape image .with_fn("image", |s: &mut Self| { - if let Some(sprite) = &s.object.as_ref().unwrap().image { - sprite.index.to_string() - } else { - error!("UI called `image()` on a system object which doesn't provide one"); - "".to_string() - } + s.object + .as_ref() + .unwrap() + .landable + .as_ref() + .map(|x| x.image.index.to_string()) + .unwrap_or_else(|| { + error!("UI called `image()` on a system object which isn't landable"); + "".to_string() + }) }) .with_fn("is_some", |s: &mut Self| s.object.is_some()) + .with_fn("is_landable", |s: &mut Self| { + s.object.as_ref().unwrap().landable.is_some() + }) .with_fn("==", |a: &mut Self, b: Self| match (&a.object, &b.object) { (None, _) => false, (_, None) => false, @@ -240,7 +284,6 @@ impl CustomType for State { .with_fn("player_ship", Self::player_ship) .with_fn("ships", Self::ships) .with_fn("objects", Self::objects) - .with_fn("window_aspect", |s: &mut Self| s.window_aspect) - .with_fn("camera_zoom", |s: &mut Self| s.input.camera_zoom); + .with_fn("window_aspect", |s: &mut Self| s.window_aspect); } } diff --git a/crates/render/src/ui/camera.rs b/crates/render/src/ui/camera.rs new file mode 100644 index 0000000..c988a05 --- /dev/null +++ b/crates/render/src/ui/camera.rs @@ -0,0 +1,35 @@ +use nalgebra::Vector2; + +#[derive(Debug, Clone)] +pub(crate) struct Camera { + /// The position of the camera, in game units + pos: Vector2, + + /// The height of the viewport, in game units. + zoom: f32, +} + +impl Camera { + pub fn new() -> Self { + Self { + pos: Vector2::new(0.0, 0.0), + zoom: 500.0, + } + } + + pub fn set_pos(&mut self, pos: Vector2) { + self.pos = pos + } + + pub fn get_pos(&self) -> Vector2 { + self.pos + } + + pub fn set_zoom(&mut self, zoom: f32) { + self.zoom = zoom + } + + pub fn get_zoom(&self) -> f32 { + self.zoom + } +} diff --git a/crates/render/src/ui/elements/fpsindicator.rs b/crates/render/src/ui/elements/fpsindicator.rs index 566c83f..f2e9141 100644 --- a/crates/render/src/ui/elements/fpsindicator.rs +++ b/crates/render/src/ui/elements/fpsindicator.rs @@ -1,10 +1,13 @@ -use glyphon::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, TextArea, TextBounds}; +use glyphon::{Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, TextBounds}; +use std::rc::Rc; use winit::window::Window; use crate::{RenderInput, RenderState}; +use super::OwnedTextArea; + pub(crate) struct FpsIndicator { - buffer: Buffer, + buffer: Rc, update_counter: u32, } @@ -21,7 +24,7 @@ impl FpsIndicator { ); Self { - buffer, + buffer: Rc::new(buffer), update_counter: 0, } } @@ -29,26 +32,28 @@ impl FpsIndicator { impl FpsIndicator { pub fn step(&mut self, input: &RenderInput, font: &mut FontSystem) { + let buffer = Rc::get_mut(&mut self.buffer).unwrap(); + if self.update_counter > 0 { self.update_counter -= 1; return; } self.update_counter = 100; - self.buffer.set_text( + buffer.set_text( font, &input.timing.get_string(), Attrs::new().family(Family::Monospace), Shaping::Basic, ); - self.buffer.shape_until_scroll(font); + buffer.shape_until_scroll(font); } } impl<'a, 'b: 'a> FpsIndicator { - pub fn get_textarea(&'b self, input: &RenderInput, _window: &Window) -> TextArea<'a> { - TextArea { - buffer: &self.buffer, + pub fn get_textarea(&'b self, input: &RenderInput, _window: &Window) -> OwnedTextArea { + OwnedTextArea { + buffer: self.buffer.clone(), left: 10.0, top: 400.0, scale: input.ct.config.ui_scale, diff --git a/crates/render/src/ui/elements/mod.rs b/crates/render/src/ui/elements/mod.rs index 4e72c6a..708cd60 100644 --- a/crates/render/src/ui/elements/mod.rs +++ b/crates/render/src/ui/elements/mod.rs @@ -1,9 +1,38 @@ mod fpsindicator; mod radialbar; +mod scrollbox; mod sprite; mod textbox; pub(super) use fpsindicator::*; pub(super) use radialbar::*; +pub(super) use scrollbox::*; pub(super) use sprite::*; pub(super) use textbox::*; + +use glyphon::{Buffer, Color, TextArea, TextBounds}; +use std::rc::Rc; + +/// A hack that lets us easily construct TextAreas +/// for [`UiTextBox`]es wrapped in Rcs. +pub struct OwnedTextArea { + pub buffer: Rc, + pub left: f32, + pub top: f32, + pub scale: f32, + pub bounds: TextBounds, + pub default_color: Color, +} + +impl OwnedTextArea { + pub fn get_textarea(&self) -> TextArea { + TextArea { + buffer: &self.buffer, + top: self.top, + left: self.left, + scale: self.scale, + bounds: self.bounds, + default_color: self.default_color, + } + } +} diff --git a/crates/render/src/ui/elements/radialbar.rs b/crates/render/src/ui/elements/radialbar.rs index 6b45d74..2609d84 100644 --- a/crates/render/src/ui/elements/radialbar.rs +++ b/crates/render/src/ui/elements/radialbar.rs @@ -6,7 +6,7 @@ use super::super::api::Rect; use crate::{ui::api::Color, vertexbuffer::types::RadialBarInstance, RenderInput, RenderState}; #[derive(Debug, Clone)] -pub struct RadialBar { +pub struct UiRadialBar { pub name: ImmutableString, rect: Rect, stroke: f32, @@ -14,7 +14,7 @@ pub struct RadialBar { progress: f32, } -impl RadialBar { +impl UiRadialBar { pub fn new( name: ImmutableString, stroke: f32, @@ -48,6 +48,4 @@ impl RadialBar { angle: self.progress * TAU, }); } - - pub fn step(&mut self, _input: &RenderInput, _state: &mut RenderState) {} } diff --git a/crates/render/src/ui/elements/scrollbox.rs b/crates/render/src/ui/elements/scrollbox.rs new file mode 100644 index 0000000..3e0aaa9 --- /dev/null +++ b/crates/render/src/ui/elements/scrollbox.rs @@ -0,0 +1,136 @@ +use nalgebra::Vector2; +use rhai::{Dynamic, ImmutableString}; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; +use winit::window::Window; + +use super::{super::api::Rect, OwnedTextArea}; +use crate::{ui::UiElement, InputEvent, RenderInput, RenderState}; + +#[derive(Debug)] +pub struct UiScrollbox { + pub name: ImmutableString, + pub rect: Rect, + pub offset: Vector2, + pub elements: HashMap>>, + + has_mouse: bool, +} + +impl UiScrollbox { + pub fn new(name: ImmutableString, rect: Rect) -> Self { + Self { + name, + rect, + elements: HashMap::new(), + offset: Vector2::new(0.0, 0.0), + has_mouse: false, + } + } + + pub fn add_element(&mut self, e: Rc>) { + let name = e.borrow().get_name().clone(); + self.elements.insert(name, e); + } + + pub fn remove_element(&mut self, sprite: &ImmutableString) { + self.elements.remove(sprite); + } + + pub fn step(&mut self, t: f32) { + for (_name, e) in &self.elements { + match &mut *e.clone().borrow_mut() { + UiElement::Sprite(sprite) => sprite.step(t), + UiElement::RadialBar(_) => {} + UiElement::Text(..) => {} + UiElement::Scrollbox(..) => {} + UiElement::SubElement { .. } => {} + } + } + } + + pub fn handle_event( + &mut self, + input: &RenderInput, + state: &mut RenderState, + event: &InputEvent, + ) -> Option { + let r = self + .rect + .to_centered(&state.window, input.ct.config.ui_scale); + + // TODO: handle only if used in event() + // i.e, scrollable sprites shouldn't break scrollboxes + // First, check if this event is captured by any sub-elements + for (_, e) in &mut self.elements { + let arg = match &mut *e.borrow_mut() { + UiElement::Sprite(sprite) => sprite.handle_event(&input, state, &event), + UiElement::Scrollbox(sbox) => sbox.handle_event(&input, state, &event), + + UiElement::RadialBar(_) | UiElement::Text(..) => None, + + // Subelements are intentionally skipped, + // they should be handled by their parent's `handle_event` method. + UiElement::SubElement { .. } => None, + }; + + if arg.is_some() { + return arg; + } + } + + // If no inner events were captured, handle self events. + match event { + InputEvent::MouseMove(pos) => { + if r.contains_mouse(state, pos) && !self.has_mouse { + self.has_mouse = true; + } + + if !r.contains_mouse(state, pos) && self.has_mouse { + self.has_mouse = false; + } + } + + InputEvent::Scroll(x) => { + if self.has_mouse { + self.offset.y -= x; + } + } + + _ => return None, + } + + return None; + } +} + +impl UiScrollbox { + pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) { + for (_name, e) in &self.elements { + match &*e.clone().borrow() { + UiElement::Sprite(sprite) => { + sprite.push_to_buffer_with_offset(input, state, self.offset) + } + UiElement::RadialBar(..) => {} + UiElement::Text(..) => {} + UiElement::Scrollbox(..) => {} + UiElement::SubElement { .. } => {} + } + } + } +} + +// TODO: don't allocate here +impl<'a> UiScrollbox { + pub fn get_textareas(&'a self, input: &RenderInput, window: &Window) -> Vec { + let mut v = Vec::with_capacity(32); + for e in self.elements.values() { + match &*e.clone().borrow() { + UiElement::Text(x) => { + v.push(x.get_textarea_with_offset(input, window, self.offset)) + } + _ => {} + } + } + return v; + } +} diff --git a/crates/render/src/ui/elements/sprite.rs b/crates/render/src/ui/elements/sprite.rs index 1f8a20b..2a577c7 100644 --- a/crates/render/src/ui/elements/sprite.rs +++ b/crates/render/src/ui/elements/sprite.rs @@ -2,13 +2,14 @@ use std::sync::Arc; use super::super::api::Rect; use crate::{ - ui::{api::Color, event::Event}, + ui::api::{Color, MouseClickEvent, MouseHoverEvent}, vertexbuffer::types::UiInstance, - RenderInput, RenderState, + InputEvent, RenderInput, RenderState, }; use galactica_content::{Sprite, SpriteAutomaton}; use galactica_util::to_radians; -use rhai::ImmutableString; +use nalgebra::Vector2; +use rhai::{Dynamic, ImmutableString}; #[derive(Debug, Clone)] pub struct UiSprite { @@ -26,8 +27,6 @@ pub struct UiSprite { mask: Option>, color: Color, - /// If true, ignore mouse events until click is released - waiting_for_release: bool, has_mouse: bool, has_click: bool, } @@ -43,7 +42,6 @@ impl UiSprite { mask: None, has_mouse: false, has_click: false, - waiting_for_release: false, preserve_aspect: false, } } @@ -67,11 +65,23 @@ impl UiSprite { pub fn set_preserve_aspect(&mut self, preserve_aspect: bool) { self.preserve_aspect = preserve_aspect; } +} +impl UiSprite { pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) { + self.push_to_buffer_with_offset(input, state, Vector2::new(0.0, 0.0)) + } + + pub fn push_to_buffer_with_offset( + &self, + input: &RenderInput, + state: &mut RenderState, + offset: Vector2, + ) { let mut rect = self .rect .to_centered(&state.window, input.ct.config.ui_scale); + rect.pos += offset; if self.preserve_aspect { let rect_aspect = rect.dim.x / rect.dim.y; @@ -108,55 +118,65 @@ impl UiSprite { }); } - pub fn check_events(&mut self, input: &RenderInput, state: &mut RenderState) -> Event { + pub fn handle_event( + &mut self, + input: &RenderInput, + state: &mut RenderState, + event: &InputEvent, + ) -> Option { let r = self .rect .to_centered(&state.window, input.ct.config.ui_scale); - if self.waiting_for_release && self.has_mouse && !input.player.input.pressed_leftclick() { - self.waiting_for_release = false; - } - - if !self.waiting_for_release - && self.has_mouse - && !self.has_click - && input.player.input.pressed_leftclick() - { - self.has_click = true; - return Event::MouseClick; - } - - if self.has_mouse && self.has_click && !input.player.input.pressed_leftclick() { - self.has_click = false; - return Event::MouseRelease; - } - // Release mouse when cursor leaves box if self.has_click && !self.has_mouse { self.has_click = false; - return Event::MouseRelease; } - if r.contains_mouse(input, state) && !self.has_mouse { - if input.player.input.pressed_leftclick() { - // If we're holding click when the cursor enters, - // don't trigger the `Click` event. - self.waiting_for_release = true; + match event { + InputEvent::MouseMove(pos) => { + if r.contains_mouse(state, pos) && !self.has_mouse { + self.has_mouse = true; + return Some(Dynamic::from(MouseHoverEvent { + enter: true, + element: self.name.clone(), + })); + } + + if !r.contains_mouse(state, pos) && self.has_mouse { + self.has_mouse = false; + return Some(Dynamic::from(MouseHoverEvent { + enter: false, + element: self.name.clone(), + })); + } } - self.has_mouse = true; - return Event::MouseHover; + + InputEvent::MouseLeftClick(pressed) => { + if self.has_mouse && !self.has_click && *pressed { + self.has_click = true; + return Some(Dynamic::from(MouseClickEvent { + down: true, + element: self.name.clone(), + })); + } + + if self.has_mouse && self.has_click && !*pressed { + self.has_click = false; + return Some(Dynamic::from(MouseClickEvent { + down: false, + element: self.name.clone(), + })); + } + } + + _ => return None, } - if !r.contains_mouse(input, state) && self.has_mouse { - self.waiting_for_release = false; - self.has_mouse = false; - return Event::MouseUnhover; - } - - return Event::None; + return None; } - pub fn step(&mut self, input: &RenderInput, _state: &mut RenderState) { - self.anim.step(input.time_since_last_run); + pub fn step(&mut self, t: f32) { + self.anim.step(t); } } diff --git a/crates/render/src/ui/elements/textbox.rs b/crates/render/src/ui/elements/textbox.rs index a6ee45c..6d89f57 100644 --- a/crates/render/src/ui/elements/textbox.rs +++ b/crates/render/src/ui/elements/textbox.rs @@ -1,27 +1,28 @@ use glyphon::{ cosmic_text::Align, Attrs, AttrsOwned, Buffer, Color, FamilyOwned, FontSystem, Metrics, - Shaping, Style, TextArea, TextBounds, Weight, + Shaping, Style, TextBounds, Weight, }; use nalgebra::Vector2; use rhai::ImmutableString; +use std::rc::Rc; use winit::window::Window; -use super::super::api::Rect; +use super::{super::api::Rect, OwnedTextArea}; use crate::{ui::api, RenderInput}; #[derive(Debug)] -pub struct TextBox { +pub struct UiTextBox { pub name: ImmutableString, text: String, justify: Align, rect: Rect, - buffer: Buffer, + buffer: Rc, color: api::Color, attrs: AttrsOwned, } -impl TextBox { +impl UiTextBox { pub fn new( font: &mut FontSystem, name: ImmutableString, @@ -38,7 +39,7 @@ impl TextBox { Self { name, rect, - buffer, + buffer: Rc::new(buffer), color, justify: Align::Left, attrs: AttrsOwned::new(Attrs::new()), @@ -47,14 +48,14 @@ impl TextBox { } fn reflow(&mut self, font: &mut FontSystem) { - self.buffer - .set_text(font, &self.text, self.attrs.as_attrs(), Shaping::Advanced); + let buffer = Rc::get_mut(&mut self.buffer).unwrap(); + buffer.set_text(font, &self.text, self.attrs.as_attrs(), Shaping::Advanced); - for l in &mut self.buffer.lines { + for l in &mut buffer.lines { l.set_align(Some(self.justify)); } - self.buffer.shape_until_scroll(font); + buffer.shape_until_scroll(font); } pub fn set_text(&mut self, font: &mut FontSystem, text: &str) { @@ -84,9 +85,19 @@ impl TextBox { } } -impl<'a, 'b: 'a> TextBox { - pub fn get_textarea(&'b self, input: &RenderInput, window: &Window) -> TextArea<'a> { - let rect = self.rect.to_centered(window, input.ct.config.ui_scale); +impl<'a, 'b: 'a> UiTextBox { + pub fn get_textarea(&'b self, input: &RenderInput, window: &Window) -> OwnedTextArea { + self.get_textarea_with_offset(input, window, Vector2::new(0.0, 0.0)) + } + + pub fn get_textarea_with_offset( + &'b self, + input: &RenderInput, + window: &Window, + offset: Vector2, + ) -> OwnedTextArea { + let mut rect = self.rect.to_centered(window, input.ct.config.ui_scale); + rect.pos += offset; // Glypon works with physical pixels, so we must do some conversion let fac = window.scale_factor() as f32; @@ -97,8 +108,8 @@ impl<'a, 'b: 'a> TextBox { let corner_sw = corner_ne + rect.dim * fac; let c = self.color.as_array_u8(); - TextArea { - buffer: &self.buffer, + OwnedTextArea { + buffer: self.buffer.clone(), top: corner_ne.y, left: corner_ne.x, scale: input.ct.config.ui_scale, diff --git a/crates/render/src/ui/event.rs b/crates/render/src/ui/event.rs deleted file mode 100644 index 66ca332..0000000 --- a/crates/render/src/ui/event.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[derive(Debug, Copy, Clone)] -pub enum Event { - None, - MouseClick, - MouseRelease, - MouseHover, - MouseUnhover, -} diff --git a/crates/render/src/ui/executor.rs b/crates/render/src/ui/executor.rs index c04668e..e72d778 100644 --- a/crates/render/src/ui/executor.rs +++ b/crates/render/src/ui/executor.rs @@ -1,20 +1,17 @@ use anyhow::{Context, Result}; use galactica_content::Content; -use galactica_system::phys::PhysSimShipHandle; +use galactica_system::{phys::PhysSimShipHandle, PlayerDirective}; use galactica_util::rhai_error_to_anyhow; -use log::debug; -use rhai::{ - packages::{BasicArrayPackage, BasicStringPackage, LogicPackage, MoreStringPackage, Package}, - Dynamic, Engine, ImmutableString, Scope, -}; +use log::{debug, error}; +use rhai::{Dynamic, Engine, ImmutableString, Scope}; use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc}; +use winit::event::VirtualKeyCode; use super::{ - api::{self, MouseClickEvent, MouseHoverEvent, PlayerShipStateEvent}, - event::Event, + api::{self, KeyboardEvent, PlayerShipStateEvent, ScrollEvent}, UiConfig, UiElement, UiState, }; -use crate::{ui::api::State, RenderInput, RenderState}; +use crate::{ui::api::State, InputEvent, RenderInput, RenderState}; pub(crate) struct UiScriptExecutor { engine: Engine, @@ -31,14 +28,8 @@ impl UiScriptExecutor { let scope = Scope::new(); let elements = Rc::new(RefCell::new(UiState::new(ct.clone(), state))); - let mut engine = Engine::new_raw(); - - // Required for array iteration - // We may need to add more packages here later. - engine.register_global_module(BasicArrayPackage::new().as_shared_module()); - engine.register_global_module(LogicPackage::new().as_shared_module()); - engine.register_global_module(BasicStringPackage::new().as_shared_module()); - engine.register_global_module(MoreStringPackage::new().as_shared_module()); + // TODO: document all functions rhai provides + let mut engine = Engine::new(); engine.set_max_expr_depths(0, 0); // Enables custom operators @@ -64,6 +55,129 @@ impl UiScriptExecutor { (*self.state).borrow().config.clone() } + pub fn process_input( + &mut self, + state: &mut RenderState, + input: Arc, + event: InputEvent, + ) -> Result { + let current_scene = (*self.state).borrow().get_scene().clone(); + if current_scene.is_none() { + return Ok(PlayerDirective::None); + } + let mut arg: Option = None; + + // First, check if this event is captured by any ui elements. + for (_, e) in &mut self.state.borrow_mut().elements { + arg = match e { + UiElement::Sprite(sprite) => sprite.handle_event(&input, state, &event), + UiElement::Scrollbox(sbox) => sbox.handle_event(&input, state, &event), + + UiElement::RadialBar(_) | UiElement::Text(..) => None, + + // Subelements are intentionally skipped, + // they should be handled by their parent's `handle_event` method. + UiElement::SubElement { .. } => None, + }; + + if arg.is_some() { + break; + } + } + + // If nothing was caught, check global events + if arg.is_none() { + arg = match event { + InputEvent::Scroll(val) => Some(Dynamic::from(ScrollEvent { val })), + InputEvent::Keyboard { down, key } => { + let str = match key { + VirtualKeyCode::A => Some("A"), + VirtualKeyCode::B => Some("B"), + VirtualKeyCode::C => Some("C"), + VirtualKeyCode::D => Some("D"), + VirtualKeyCode::E => Some("E"), + VirtualKeyCode::F => Some("F"), + VirtualKeyCode::G => Some("G"), + VirtualKeyCode::H => Some("H"), + VirtualKeyCode::I => Some("I"), + VirtualKeyCode::J => Some("J"), + VirtualKeyCode::K => Some("K"), + VirtualKeyCode::L => Some("L"), + VirtualKeyCode::M => Some("M"), + VirtualKeyCode::N => Some("N"), + VirtualKeyCode::O => Some("O"), + VirtualKeyCode::P => Some("P"), + VirtualKeyCode::Q => Some("Q"), + VirtualKeyCode::R => Some("R"), + VirtualKeyCode::S => Some("S"), + VirtualKeyCode::T => Some("T"), + VirtualKeyCode::U => Some("U"), + VirtualKeyCode::V => Some("V"), + VirtualKeyCode::W => Some("W"), + VirtualKeyCode::X => Some("X"), + VirtualKeyCode::Y => Some("Y"), + VirtualKeyCode::Z => Some("Z"), + VirtualKeyCode::Up => Some("up"), + VirtualKeyCode::Down => Some("down"), + VirtualKeyCode::Left => Some("left"), + VirtualKeyCode::Right => Some("right"), + VirtualKeyCode::Space => Some("space"), + _ => None, + }; + if let Some(str) = str { + Some(Dynamic::from(KeyboardEvent { + down, + key: ImmutableString::from(str), + })) + } else { + None + } + } + _ => None, + }; + } + + if let Some(arg) = arg { + self.run_event_callback(state, input, arg) + } else { + return Ok(PlayerDirective::None); + } + } + + fn run_event_callback( + &mut self, + state: &mut RenderState, + input: Arc, + arg: Dynamic, + ) -> Result { + let current_scene = (*self.state).borrow().get_scene().clone(); + if current_scene.is_none() { + return Ok(PlayerDirective::None); + } + let current_scene = current_scene.unwrap(); + let ct = (*self.state).borrow().ct.clone(); + + let d: Dynamic = rhai_error_to_anyhow(self.engine.call_fn( + &mut self.scope, + ct.config.ui_scenes.get(current_scene.as_str()).unwrap(), + "event", + (State::new(state, input.clone()), arg.clone()), + )) + .with_context(|| format!("while handling event `{:?}`", arg)) + .with_context(|| format!("in ui scene `{}`", current_scene))?; + + if d.is::() { + return Ok(d.cast()); + } else if !(d.is_unit()) { + error!( + "`event()` in UI scene `{current_scene}` returned invalid type `{}`", + d + ) + } + + return Ok(PlayerDirective::None); + } + /// Change the current scene pub fn set_scene(&mut self, state: &RenderState, input: Arc) -> Result<()> { let current_scene = (*self.state).borrow().get_scene().clone(); @@ -87,8 +201,8 @@ impl UiScriptExecutor { let mut elm = self.state.borrow_mut(); elm.clear(); drop(elm); - let ct = (*self.state).borrow().ct.clone(); + let ct = (*self.state).borrow().ct.clone(); rhai_error_to_anyhow( self.engine.call_fn( &mut self.scope, @@ -122,7 +236,6 @@ impl UiScriptExecutor { (*self.state).borrow_mut().step(state, input.clone()); // Run step() (if it is defined) - let ast = ct .config .ui_scenes @@ -158,79 +271,25 @@ impl UiScriptExecutor { true } } { - rhai_error_to_anyhow( - self.engine.call_fn( - &mut self.scope, - ct.config - .ui_scenes - .get(current_scene.as_ref().unwrap().as_str()) - .unwrap(), - "event", - (State::new(state, input.clone()), PlayerShipStateEvent {}), - ), - ) - .with_context(|| format!("while handling player state change event")) - .with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?; + self.run_event_callback(state, input.clone(), Dynamic::from(PlayerShipStateEvent {}))?; } 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() { + 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, - }; + UiElement::Scrollbox(x) => { + x.push_to_buffer(&input, state); + } - if let Some(event_arg) = event_arg { - rhai_error_to_anyhow( - self.engine.call_fn( - &mut self.scope, - ct.config - .ui_scenes - .get(current_scene.as_ref().unwrap().as_str()) - .unwrap(), - "event", - (State::new(state, input.clone()), event_arg.clone()), - ), - ) - .with_context(|| format!("while handling event `{:?}`", event_arg)) - .with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?; + UiElement::SubElement { .. } | UiElement::Text(..) => {} } } diff --git a/crates/render/src/ui/mod.rs b/crates/render/src/ui/mod.rs index 4f1d42f..7eff0e7 100644 --- a/crates/render/src/ui/mod.rs +++ b/crates/render/src/ui/mod.rs @@ -1,9 +1,9 @@ mod api; -mod event; +mod camera; +mod elements; mod executor; mod state; -mod elements; - +pub(crate) use camera::*; 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 index 0cbae65..6dace80 100644 --- a/crates/render/src/ui/state.rs +++ b/crates/render/src/ui/state.rs @@ -1,19 +1,39 @@ use galactica_content::Content; -use glyphon::TextArea; use log::{debug, error}; use rhai::ImmutableString; -use std::collections::HashMap; -use std::sync::Arc; +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc, time::Instant}; use winit::window::Window; -use super::elements::{FpsIndicator, RadialBar, TextBox, UiSprite}; +use super::{ + elements::{FpsIndicator, OwnedTextArea, UiRadialBar, UiScrollbox, UiSprite, UiTextBox}, + Camera, +}; use crate::{RenderInput, RenderState}; #[derive(Debug)] pub enum UiElement { Sprite(UiSprite), - RadialBar(RadialBar), - Text(TextBox), + RadialBar(UiRadialBar), + Text(UiTextBox), + Scrollbox(UiScrollbox), + + /// This is a sub-element managed by another element + SubElement { + parent: ImmutableString, + element: Rc>, + }, +} + +impl UiElement { + pub fn get_name(&self) -> ImmutableString { + match self { + Self::Sprite(x) => x.name.clone(), + Self::RadialBar(x) => x.name.clone(), + Self::Text(x) => x.name.clone(), + Self::Scrollbox(x) => x.name.clone(), + Self::SubElement { element, .. } => element.borrow().get_name(), + } + } } #[derive(Clone, Debug)] @@ -31,8 +51,13 @@ pub(crate) struct UiState { show_timings: bool, fps_indicator: FpsIndicator, + last_step: Instant, pub config: UiConfig, + + /// The player's camera. + /// Only used when drawing physics. + pub camera: Camera, } // TODO: remove this unsafe impl Send for UiState {} @@ -52,6 +77,8 @@ impl UiState { show_phys: false, show_starfield: false, }, + last_step: Instant::now(), + camera: Camera::new(), } } @@ -105,15 +132,57 @@ impl UiState { } pub fn step(&mut self, state: &mut RenderState, input: Arc) { + let t = self.last_step.elapsed().as_secs_f32(); + for (_, e) in &mut self.elements { + match e { + UiElement::Sprite(sprite) => sprite.step(t), + UiElement::Scrollbox(sbox) => sbox.step(t), + _ => {} + } + } + if self.show_timings { self.fps_indicator .step(&input, &mut state.text_font_system.borrow_mut()); } + self.last_step = Instant::now(); + } + + pub fn add_element(&mut self, e: UiElement) { + self.names.push(e.get_name().clone()); + self.elements.insert(e.get_name().clone(), e); + } + + // Remove an element from this sprite. + // This does NOT remove subelements from their parent sprites. + pub fn remove_element_incomplete(&mut self, name: &ImmutableString) -> Option { + let e = self.elements.remove(name); + self.names.retain(|x| *x != name); + return e; + } + + // Remove an element from this sprite and from all subsprites. + pub fn remove_element(&mut self, name: &ImmutableString) { + let e = self.elements.remove(name); + self.names.retain(|x| *x != name); + + match e { + Some(UiElement::SubElement { parent, element }) => { + let x = Rc::into_inner(element).unwrap().into_inner(); + let parent = self.elements.get_mut(&parent).unwrap(); + match parent { + UiElement::Scrollbox(s) => s.remove_element(&x.get_name()), + _ => unreachable!("invalid subelement parent"), + } + } + _ => {} + } } } +// TODO: don't allocate here, return an iterator impl<'a> UiState { - pub fn get_textareas(&'a mut self, input: &RenderInput, window: &Window) -> Vec> { + pub fn get_textareas(&'a self, input: &RenderInput, window: &Window) -> Vec { let mut v = Vec::with_capacity(32); if self.current_scene.is_none() { @@ -124,9 +193,10 @@ impl<'a> UiState { v.push(self.fps_indicator.get_textarea(input, window)) } - for t in self.elements.values() { - match &t { - UiElement::Text(x) => v.push(x.get_textarea(input, window)), + for e in self.elements.values() { + match &e { + UiElement::Text(t) => v.push(t.get_textarea(input, window)), + UiElement::Scrollbox(b) => v.extend(b.get_textareas(input, window)), _ => {} } }