From 847542a8cb78177ad925552a3084e3b728ae4367 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 8 Feb 2024 20:40:12 -0800 Subject: [PATCH] Reworked subelements Cleaned up argument passing Added persistent UI script variables --- crates/galactica/src/main.rs | 89 +++---- crates/render/src/gpustate.rs | 9 +- crates/render/src/renderinput.rs | 4 +- crates/render/src/ui/api/mod.rs | 2 +- crates/render/src/ui/api/state.rs | 4 +- crates/render/src/ui/elements/scrollbox.rs | 97 ++------ crates/render/src/ui/elements/sprite.rs | 31 ++- crates/render/src/ui/elements/textbox.rs | 2 +- crates/render/src/ui/executor.rs | 267 +++++++++++++-------- crates/render/src/ui/state.rs | 88 +++---- 10 files changed, 287 insertions(+), 306 deletions(-) diff --git a/crates/galactica/src/main.rs b/crates/galactica/src/main.rs index 9860d65..f4ff5c9 100644 --- a/crates/galactica/src/main.rs +++ b/crates/galactica/src/main.rs @@ -123,21 +123,26 @@ fn try_main() -> Result<()> { let mut game = game::Game::new(content.clone()); let p = game.make_player(); - let player = Arc::new(PlayerAgent::new(&content, p.0)); - let mut phys_img = Arc::new(PhysImage::new()); + let mut input = Arc::new(RenderInput { + current_time: game.get_current_time(), + ct: content.clone(), + phys_img: PhysImage::new(), + player: PlayerAgent::new(&content, p.0), + // TODO: this is a hack for testing. + current_system: content.systems.values().next().unwrap().clone(), + timing: game.get_timing().clone(), + }); event_loop.run(move |event, _, control_flow| { + { + let i = Arc::get_mut(&mut input).unwrap(); + i.current_time = game.get_current_time(); + i.timing = game.get_timing().clone(); + } + match event { Event::RedrawRequested(window_id) if window_id == gpu.window().id() => { - match gpu.render(RenderInput { - current_time: game.get_current_time(), - ct: content.clone(), - phys_img: phys_img.clone(), - player: player.clone(), - // TODO: this is a hack for testing. - current_system: content.systems.values().next().unwrap().clone(), - timing: game.get_timing().clone(), - }) { + match gpu.render(&input) { Ok(_) => {} Err(wgpu::SurfaceError::Lost) => gpu.resize(&content), Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, @@ -147,8 +152,9 @@ fn try_main() -> Result<()> { } Event::MainEventsCleared => { - game.step(&phys_img); - game.update_image(Arc::get_mut(&mut phys_img).unwrap()); + let i = Arc::get_mut(&mut input).unwrap(); + game.step(&i.phys_img); + game.update_image(&mut i.phys_img); gpu.window().request_redraw(); } @@ -171,37 +177,20 @@ fn try_main() -> Result<()> { } => { let directive = gpu .process_input( - RenderInput { - current_time: game.get_current_time(), - ct: content.clone(), - phys_img: phys_img.clone(), - player: player.clone(), - current_system: content.systems.values().next().unwrap().clone(), - timing: game.get_timing().clone(), - }, + &input, InputEvent::Keyboard { down: state == &ElementState::Pressed, key: *key, }, ) .unwrap(); - game.apply_directive(directive, &player); + game.apply_directive(directive, &input.player); } WindowEvent::CursorMoved { position, .. } => { let directive = gpu - .process_input( - RenderInput { - current_time: game.get_current_time(), - ct: content.clone(), - phys_img: phys_img.clone(), - player: player.clone(), - current_system: content.systems.values().next().unwrap().clone(), - timing: game.get_timing().clone(), - }, - InputEvent::MouseMove(position.cast()), - ) + .process_input(&input, InputEvent::MouseMove(position.cast())) .unwrap(); - game.apply_directive(directive, &player); + game.apply_directive(directive, &input.player); } WindowEvent::MouseInput { state, button, .. } => { let down = state == &ElementState::Pressed; @@ -211,45 +200,21 @@ fn try_main() -> Result<()> { _ => None, }; if let Some(event) = event { - let directive = gpu - .process_input( - RenderInput { - current_time: game.get_current_time(), - ct: content.clone(), - phys_img: phys_img.clone(), - player: player.clone(), - current_system: content - .systems - .values() - .next() - .unwrap() - .clone(), - timing: game.get_timing().clone(), - }, - event, - ) - .unwrap(); - game.apply_directive(directive, &player); + let directive = gpu.process_input(&input, event).unwrap(); + game.apply_directive(directive, &input.player); } } WindowEvent::MouseWheel { delta, .. } => { let directive = gpu .process_input( - RenderInput { - current_time: game.get_current_time(), - ct: content.clone(), - phys_img: phys_img.clone(), - player: player.clone(), - current_system: content.systems.values().next().unwrap().clone(), - timing: game.get_timing().clone(), - }, + &input, InputEvent::Scroll(match delta { MouseScrollDelta::LineDelta(_h, v) => *v, MouseScrollDelta::PixelDelta(v) => v.x as f32, }), ) .unwrap(); - game.apply_directive(directive, &player); + game.apply_directive(directive, &input.player); } WindowEvent::Resized(_) => { gpu.resize(&content); diff --git a/crates/render/src/gpustate.rs b/crates/render/src/gpustate.rs index e072b09..f7cbc90 100644 --- a/crates/render/src/gpustate.rs +++ b/crates/render/src/gpustate.rs @@ -279,17 +279,14 @@ impl GPUState { /// Handle user input pub fn process_input( &mut self, - input: RenderInput, + input: &Arc, 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); - + pub fn render(&mut self, input: &Arc) -> Result<(), wgpu::SurfaceError> { if let Some(ship) = input.player.ship { let o = input.phys_img.get_ship(&PhysSimShipHandle(ship)); if let Some(o) = o { @@ -390,7 +387,7 @@ impl GPUState { self.push_effects(&input, (clip_ne, clip_sw)); } - self.ui.draw(&mut self.state, input.clone()).unwrap(); + self.ui.draw(&mut self.state, input).unwrap(); // These should match the indices in each shader, // and should each have a corresponding bind group layout. diff --git a/crates/render/src/renderinput.rs b/crates/render/src/renderinput.rs index a7ddc04..6456f5b 100644 --- a/crates/render/src/renderinput.rs +++ b/crates/render/src/renderinput.rs @@ -9,13 +9,13 @@ use galactica_util::timing::Timing; #[derive(Debug)] pub struct RenderInput { /// Player ship data - pub player: Arc, + pub player: PlayerAgent, /// The system we're currently in pub current_system: Arc, /// The world state to render - pub phys_img: Arc, + pub phys_img: PhysImage, // TODO: handle overflow. is it a problem? /// The current time, in seconds diff --git a/crates/render/src/ui/api/mod.rs b/crates/render/src/ui/api/mod.rs index 7bfdf8e..6996ab4 100644 --- a/crates/render/src/ui/api/mod.rs +++ b/crates/render/src/ui/api/mod.rs @@ -38,7 +38,7 @@ pub fn register_into_engine( .build_type::() .build_type::() .build_type::() - // Bigger modules + // Enums .register_type_with_name::("Anchor") .register_static_module("Anchor", exported_module!(anchor_mod).into()) .register_type_with_name::("PlayerDirective") diff --git a/crates/render/src/ui/api/state.rs b/crates/render/src/ui/api/state.rs index 733a528..829a8bf 100644 --- a/crates/render/src/ui/api/state.rs +++ b/crates/render/src/ui/api/state.rs @@ -241,9 +241,9 @@ unsafe impl Send for State {} unsafe impl Sync for State {} impl State { - pub fn new(state: &RenderState, input: Arc) -> Self { + pub fn new(state: &RenderState, input: &Arc) -> Self { Self { - input, + input: input.clone(), window_aspect: state.window_aspect, } } diff --git a/crates/render/src/ui/elements/scrollbox.rs b/crates/render/src/ui/elements/scrollbox.rs index 3e0aaa9..f2f4a10 100644 --- a/crates/render/src/ui/elements/scrollbox.rs +++ b/crates/render/src/ui/elements/scrollbox.rs @@ -1,17 +1,14 @@ 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}; +use super::super::api::Rect; +use crate::{InputEvent, RenderInput, RenderState}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct UiScrollbox { pub name: ImmutableString, pub rect: Rect, pub offset: Vector2, - pub elements: HashMap>>, has_mouse: bool, } @@ -21,31 +18,13 @@ impl UiScrollbox { 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 step(&mut self, _t: f32) { + // TODO: inertia } pub fn handle_event( @@ -54,31 +33,21 @@ impl UiScrollbox { state: &mut RenderState, event: &InputEvent, ) -> Option { - let r = self + self.handle_event_with_offset(input, state, event, Vector2::new(0.0, 0.0)) + } + + pub fn handle_event_with_offset( + &mut self, + input: &RenderInput, + state: &mut RenderState, + event: &InputEvent, + offset: Vector2, + ) -> Option { + let mut r = self .rect .to_centered(&state.window, input.ct.config.ui_scale); + r.pos += offset; - // 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 { @@ -102,35 +71,3 @@ impl UiScrollbox { 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 2a577c7..35c39de 100644 --- a/crates/render/src/ui/elements/sprite.rs +++ b/crates/render/src/ui/elements/sprite.rs @@ -19,6 +19,9 @@ pub struct UiSprite { /// Sprite angle, in degrees angle: f32, + /// If true, this sprite ignores all events + disable_events: bool, + /// If true, this sprite will be scaled to fit in its box without affecting aspect ratio. /// If false, this sprite will be stretched to fit in its box preserve_aspect: bool, @@ -43,6 +46,7 @@ impl UiSprite { has_mouse: false, has_click: false, preserve_aspect: false, + disable_events: false, } } @@ -62,6 +66,10 @@ impl UiSprite { self.color = color; } + pub fn set_disable_events(&mut self, disable_events: bool) { + self.disable_events = disable_events; + } + pub fn set_preserve_aspect(&mut self, preserve_aspect: bool) { self.preserve_aspect = preserve_aspect; } @@ -124,14 +132,24 @@ impl UiSprite { state: &mut RenderState, event: &InputEvent, ) -> Option { - let r = self + self.handle_event_with_offset(input, state, event, Vector2::new(0.0, 0.0)) + } + + pub fn handle_event_with_offset( + &mut self, + input: &RenderInput, + state: &mut RenderState, + event: &InputEvent, + offset: Vector2, + ) -> Option { + if self.disable_events { + return None; + } + + let mut r = self .rect .to_centered(&state.window, input.ct.config.ui_scale); - - // Release mouse when cursor leaves box - if self.has_click && !self.has_mouse { - self.has_click = false; - } + r.pos += offset; match event { InputEvent::MouseMove(pos) => { @@ -145,6 +163,7 @@ impl UiSprite { if !r.contains_mouse(state, pos) && self.has_mouse { self.has_mouse = false; + self.has_click = false; return Some(Dynamic::from(MouseHoverEvent { enter: false, element: self.name.clone(), diff --git a/crates/render/src/ui/elements/textbox.rs b/crates/render/src/ui/elements/textbox.rs index 6d89f57..6b272dc 100644 --- a/crates/render/src/ui/elements/textbox.rs +++ b/crates/render/src/ui/elements/textbox.rs @@ -10,7 +10,7 @@ use winit::window::Window; use super::{super::api::Rect, OwnedTextArea}; use crate::{ui::api, RenderInput}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct UiTextBox { pub name: ImmutableString, diff --git a/crates/render/src/ui/executor.rs b/crates/render/src/ui/executor.rs index e72d778..74a5ebd 100644 --- a/crates/render/src/ui/executor.rs +++ b/crates/render/src/ui/executor.rs @@ -3,7 +3,7 @@ use galactica_content::Content; use galactica_system::{phys::PhysSimShipHandle, PlayerDirective}; use galactica_util::rhai_error_to_anyhow; use log::{debug, error}; -use rhai::{Dynamic, Engine, ImmutableString, Scope}; +use rhai::{CallFnOptions, Dynamic, Engine, ImmutableString, Scope}; use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc}; use winit::event::VirtualKeyCode; @@ -58,87 +58,119 @@ impl UiScriptExecutor { pub fn process_input( &mut self, state: &mut RenderState, - input: Arc, + 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 { + let len = (*self.state).borrow().len(); + // Iterate front to back + // (this also ensures that sub-elements are handled BEFORE their container.) + for i in (0..len).rev() { + let mut ui_state = self.state.borrow_mut(); + let arg = match ui_state.get_mut_by_idx(i).unwrap() { 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, + UiElement::SubElement { parent, element } => { + // Be very careful here, to avoid + // borrowing mutably twice... + let e_name = element.get_name(); + let p_name = parent.clone(); + let parent = ui_state.get_by_name(&p_name).unwrap(); + match parent { + UiElement::Scrollbox(sbox) => { + let offset = sbox.offset; + let element = ui_state.get_mut_by_name(&e_name).unwrap(); + match element { + UiElement::SubElement { element, .. } => match &mut **element { + UiElement::Sprite(sprite) => sprite + .handle_event_with_offset(&input, state, &event, offset), + UiElement::Scrollbox(sbox) => { + sbox.handle_event_with_offset(&input, state, &event, offset) + } + UiElement::RadialBar(_) | UiElement::Text(..) => None, + UiElement::SubElement { .. } => unreachable!(), + }, + _ => unreachable!(), + } + } + _ => unreachable!(), + } + } }; + drop(ui_state); - if arg.is_some() { - break; + // Return if event hook returns any PlayerDirective (including None), + // continue to the next element if the hook returns (). + if let Some(arg) = arg { + let result = self.run_event_callback(state, &input, arg)?; + + if let Some(result) = result { + return Ok(result); + } } } // 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 - } + let 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, - }; - } + } + _ => None, + }; if let Some(arg) = arg { - self.run_event_callback(state, input, arg) + Ok(self + .run_event_callback(state, &input, arg)? + .unwrap_or(PlayerDirective::None)) } else { return Ok(PlayerDirective::None); } @@ -147,39 +179,42 @@ impl UiScriptExecutor { fn run_event_callback( &mut self, state: &mut RenderState, - input: Arc, + input: &Arc, arg: Dynamic, - ) -> Result { + ) -> Result> { let current_scene = (*self.state).borrow().get_scene().clone(); if current_scene.is_none() { - return Ok(PlayerDirective::None); + return Ok(None); } let current_scene = current_scene.unwrap(); let ct = (*self.state).borrow().ct.clone(); + let ast = ct.config.ui_scenes.get(current_scene.as_str()).unwrap(); - let d: Dynamic = rhai_error_to_anyhow(self.engine.call_fn( + let d: Dynamic = rhai_error_to_anyhow::(self.engine.call_fn_with_options( + CallFnOptions::new().rewind_scope(true), &mut self.scope, - ct.config.ui_scenes.get(current_scene.as_str()).unwrap(), + ast, "event", - (State::new(state, input.clone()), arg.clone()), + (State::new(state, input), arg.clone()), )) - .with_context(|| format!("while handling event `{:?}`", arg)) - .with_context(|| format!("in ui scene `{}`", current_scene))?; + .with_context(|| format!("while calling `event()`")) + .with_context(|| format!("in UI scene `{}`", current_scene.as_str()))?; if d.is::() { - return Ok(d.cast()); + return Ok(Some(d.cast::())); } else if !(d.is_unit()) { error!( - "`event()` in UI scene `{current_scene}` returned invalid type `{}`", - d + "`event()` in UI scene `{}` returned invalid type `{}`", + d, + current_scene.as_str() ) } - return Ok(PlayerDirective::None); + return Ok(None); } /// Change the current scene - pub fn set_scene(&mut self, state: &RenderState, input: Arc) -> Result<()> { + pub fn set_scene(&mut self, state: &RenderState, input: &Arc) -> Result<()> { let current_scene = (*self.state).borrow().get_scene().clone(); if self.last_scene == current_scene { return Ok(()); @@ -195,33 +230,44 @@ impl UiScriptExecutor { 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.config - .ui_scenes - .get(current_scene.as_ref().unwrap().as_str()) - .unwrap(), - "init", - (State::new(state, input.clone()),), - ), - ) + let ast = ct + .config + .ui_scenes + .get(current_scene.as_ref().unwrap().as_str()) + .unwrap(); + + // Clear previous state + self.scope.clear(); + self.state.borrow_mut().clear(); + + rhai_error_to_anyhow(self.engine.call_fn_with_options( + CallFnOptions::new().rewind_scope(false), + &mut self.scope, + ast, + "init", + (State::new(state, input),), + )) .with_context(|| format!("while running `init()`")) .with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?; + // Make sure input isn't borrowed (which would happen if the UI script adds a state class to the scope) + // At this point, the only reference to input should be the one in main.rs. + // Note how we always pass input as &Arc, not Arc. + if Arc::strong_count(&input) != 1 { + error!( + "State has been captured in the scope of UI scene `{}`", + current_scene.as_ref().unwrap() + ); + error!("Clearing scope to prevent a panic, this may break the UI script."); + self.scope.clear(); + }; + return Ok(()); } /// Draw all ui elements - pub fn draw(&mut self, state: &mut RenderState, input: Arc) -> Result<()> { + 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 @@ -230,10 +276,10 @@ impl UiScriptExecutor { .borrow_mut() .set_scene(ImmutableString::from(&ct.config.start_ui_scene)); } - self.set_scene(state, input.clone())?; + self.set_scene(state, input)?; let current_scene = (*self.state).borrow().get_scene().clone(); - (*self.state).borrow_mut().step(state, input.clone()); + (*self.state).borrow_mut().step(state, input); // Run step() (if it is defined) let ast = ct @@ -242,11 +288,12 @@ impl UiScriptExecutor { .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( + rhai_error_to_anyhow(self.engine.call_fn_with_options( + CallFnOptions::new().rewind_scope(true), &mut self.scope, ast, "step", - (State::new(state, input.clone()),), + (State::new(state, input),), )) .with_context(|| format!("while calling `step()`")) .with_context(|| format!("in ui scene `{}`", current_scene.as_ref().unwrap()))?; @@ -271,12 +318,12 @@ impl UiScriptExecutor { true } } { - self.run_event_callback(state, input.clone(), Dynamic::from(PlayerShipStateEvent {}))?; + self.run_event_callback(state, &input, Dynamic::from(PlayerShipStateEvent {}))?; } let len = (*self.state).borrow().len(); for i in 0..len { - match (*self.state).borrow_mut().get_mut_by_idx(i).unwrap() { + match self.state.borrow().get_by_idx(i).unwrap() { UiElement::Sprite(sprite) => { sprite.push_to_buffer(&input, state); } @@ -285,15 +332,27 @@ impl UiScriptExecutor { x.push_to_buffer(&input, state); } - UiElement::Scrollbox(x) => { - x.push_to_buffer(&input, state); - } + UiElement::Scrollbox(_) => {} + UiElement::Text(..) => {} - UiElement::SubElement { .. } | UiElement::Text(..) => {} + UiElement::SubElement { element, parent } => { + match self.state.borrow().get_by_name(parent).unwrap() { + UiElement::Scrollbox(sbox) => match &**element { + UiElement::Sprite(sprite) => { + sprite.push_to_buffer_with_offset(input, state, sbox.offset) + } + UiElement::RadialBar(..) => {} + UiElement::Text(..) => {} + UiElement::Scrollbox(..) => {} + UiElement::SubElement { .. } => {} + }, + _ => {} + } + } } } - self.scope.rewind(0); + //self.scope.rewind(0); return Ok(()); } } diff --git a/crates/render/src/ui/state.rs b/crates/render/src/ui/state.rs index 6dace80..2e8e501 100644 --- a/crates/render/src/ui/state.rs +++ b/crates/render/src/ui/state.rs @@ -1,7 +1,7 @@ use galactica_content::Content; use log::{debug, error}; use rhai::ImmutableString; -use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc, time::Instant}; +use std::{collections::HashMap, sync::Arc, time::Instant}; use winit::window::Window; use super::{ @@ -10,7 +10,7 @@ use super::{ }; use crate::{RenderInput, RenderState}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum UiElement { Sprite(UiSprite), RadialBar(UiRadialBar), @@ -20,7 +20,7 @@ pub enum UiElement { /// This is a sub-element managed by another element SubElement { parent: ImmutableString, - element: Rc>, + element: Box, }, } @@ -31,7 +31,7 @@ impl UiElement { 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(), + Self::SubElement { element, .. } => element.get_name(), } } } @@ -43,8 +43,11 @@ pub(crate) struct UiConfig { } pub(crate) struct UiState { - pub elements: HashMap, - pub names: Vec, + /// The Ui elements on the screen right now + elements: HashMap, + + /// Keeps track of element order + elements_ordered: Vec, pub ct: Arc, current_scene: Option, @@ -68,7 +71,7 @@ impl UiState { Self { ct, elements: HashMap::new(), - names: Vec::new(), + elements_ordered: Vec::new(), current_scene: None, show_timings: true, @@ -84,29 +87,32 @@ impl UiState { pub fn clear(&mut self) { self.elements.clear(); - self.names.clear(); + self.elements_ordered.clear(); } pub fn len(&self) -> usize { - self.elements.len() + assert!(self.elements.len() == self.elements_ordered.len()); + return self.elements.len(); } - /* - pub fn get_by_idx(&self, idx: usize) -> Option<&UiElement> { - self.elements.get(idx) + pub fn contains_name(&self, name: &ImmutableString) -> bool { + self.elements.contains_key(name) } pub fn get_by_name(&self, name: &ImmutableString) -> Option<&UiElement> { - let idx = self.names.get(name); - if idx.is_none() { + self.elements.get(name) + } + + pub fn get_by_idx(&self, idx: usize) -> Option<&UiElement> { + let name = self.elements_ordered.get(idx); + if name.is_none() { return None; } - self.get_by_idx(*idx.unwrap()) + self.elements.get(name.unwrap()) } - */ pub fn get_mut_by_idx(&mut self, idx: usize) -> Option<&mut UiElement> { - let name = self.names.get(idx); + let name = self.elements_ordered.get(idx); if name.is_none() { return None; } @@ -131,12 +137,19 @@ impl UiState { self.current_scene = Some(scene); } - pub fn step(&mut self, state: &mut RenderState, input: Arc) { + 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), + UiElement::SubElement { element, .. } => match &mut **element { + UiElement::Sprite(sprite) => sprite.step(t), + UiElement::RadialBar(_) => {} + UiElement::Text(..) => {} + UiElement::Scrollbox(..) => {} + UiElement::SubElement { .. } => {} + }, _ => {} } } @@ -149,34 +162,14 @@ impl UiState { } pub fn add_element(&mut self, e: UiElement) { - self.names.push(e.get_name().clone()); + self.elements_ordered.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"), - } - } - _ => {} - } + let _ = self.elements.remove(name); + self.elements_ordered.retain(|x| *x != name); } } @@ -196,7 +189,18 @@ impl<'a> UiState { 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)), + UiElement::SubElement { parent, element } => { + let parent = self.get_by_name(parent).unwrap(); + match parent { + UiElement::Scrollbox(sbox) => match &**element { + UiElement::Text(x) => { + v.push(x.get_textarea_with_offset(input, window, sbox.offset)) + } + _ => {} + }, + _ => unreachable!(), + } + } _ => {} } }