Reworked renderer for Directives

Added OwnedTextArea & reworked textarea creation
Added ScrollBox
master
Mark 2024-02-07 15:58:14 -08:00
parent 55319d6872
commit b170f3f53f
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
26 changed files with 884 additions and 264 deletions

View File

@ -1,13 +1,13 @@
use anyhow::Result; use anyhow::Result;
use bytemuck; use bytemuck;
use galactica_content::Content; use galactica_content::Content;
use galactica_system::data::ShipState; use galactica_system::{data::ShipState, phys::PhysSimShipHandle, PlayerDirective};
use galactica_util::to_radians; use galactica_util::to_radians;
use glyphon::{FontSystem, Resolution, SwashCache, TextAtlas, TextRenderer}; 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 std::{cell::RefCell, iter, rc::Rc, sync::Arc};
use wgpu; use wgpu;
use winit; use winit::{self};
use crate::{ use crate::{
globaluniform::{GlobalDataContent, GlobalUniform, ObjectData}, globaluniform::{GlobalDataContent, GlobalUniform, ObjectData},
@ -17,7 +17,7 @@ use crate::{
texturearray::TextureArray, texturearray::TextureArray,
ui::UiScriptExecutor, ui::UiScriptExecutor,
vertexbuffer::{consts::SPRITE_INDICES, types::ObjectInstance}, 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. /// 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.config.height = new_size.height;
self.surface.configure(&self.device, &self.config); self.surface.configure(&self.device, &self.config);
} }
// TODO: this takes a long time. fix!
self.starfield.update_buffer(ct, &mut self.state); self.starfield.update_buffer(ct, &mut self.state);
} }
@ -274,18 +276,54 @@ impl GPUState {
self.starfield.update_buffer(ct, &mut self.state); self.starfield.update_buffer(ct, &mut self.state);
} }
/// Handle user input
pub fn process_input(
&mut self,
input: RenderInput,
event: InputEvent,
) -> Result<PlayerDirective> {
let input = Arc::new(input);
self.ui.process_input(&mut self.state, input, event)
}
/// Main render function. Draws sprites on a window. /// Main render function. Draws sprites on a window.
pub fn render(&mut self, input: RenderInput) -> Result<(), wgpu::SurfaceError> { pub fn render(&mut self, input: RenderInput) -> Result<(), wgpu::SurfaceError> {
let input = Arc::new(input); 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 // Update global values
self.state.queue.write_buffer( self.state.queue.write_buffer(
&self.state.global_uniform.data_buffer, &self.state.global_uniform.data_buffer,
0, 0,
bytemuck::cast_slice(&[GlobalDataContent { bytemuck::cast_slice(&[GlobalDataContent {
camera_position_x: input.camera_pos.x, camera_position_x: self.ui.state.borrow().camera.get_pos().x,
camera_position_y: input.camera_pos.y, camera_position_y: self.ui.state.borrow().camera.get_pos().y,
camera_zoom: input.camera_zoom, camera_zoom: self.ui.state.borrow().camera.get_zoom(),
camera_zoom_min: input.ct.config.zoom_min, camera_zoom_min: input.ct.config.zoom_min,
camera_zoom_max: input.ct.config.zoom_max, camera_zoom_max: input.ct.config.zoom_max,
window_size_w: self.state.window_size.width as f32, 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. // Game coordinates (relative to camera) of ne and sw corners of screen.
// Used to skip off-screen sprites. // Used to skip off-screen sprites.
let clip_ne = Point2::new(-self.state.window_aspect, 1.0) * input.camera_zoom; let clip_ne = Point2::new(-self.state.window_aspect, 1.0)
let clip_sw = Point2::new(self.state.window_aspect, -1.0) * input.camera_zoom; * 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. // Order matters, it determines what is drawn on top.
// The order inside ships and projectiles doesn't matter, // The order inside ships and projectiles doesn't matter,
@ -424,7 +464,9 @@ impl GPUState {
}, },
(*self.ui.state) (*self.ui.state)
.borrow_mut() .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, &mut self.state.text_cache,
) )
.unwrap(); .unwrap();
@ -483,8 +525,9 @@ impl GPUState {
// Position adjusted for parallax // Position adjusted for parallax
// TODO: adjust parallax for zoom? // TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for ships // 1.0 is z-coordinate, which is constant for ships
let pos: Point2<f32> = let pos: Point2<f32> = (Point2::new(ship_pos.x, ship_pos.y)
(Point2::new(ship_pos.x, ship_pos.y) - input.camera_pos) / ship_pos.z; - self.ui.state.borrow().camera.get_pos())
/ ship_pos.z;
// Game dimensions of this sprite post-scale. // Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger. // Post-scale width or height, whichever is larger.
@ -588,7 +631,7 @@ impl GPUState {
// Position adjusted for parallax // Position adjusted for parallax
// TODO: adjust parallax for zoom? // TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for projectiles // 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. // Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger. // Post-scale width or height, whichever is larger.
@ -649,7 +692,8 @@ impl GPUState {
for o in v { for o in v {
// Position adjusted for parallax // Position adjusted for parallax
let pos: Point2<f32> = (Point2::new(o.pos.x, o.pos.y) - input.camera_pos) / o.pos.z; let pos: Point2<f32> =
(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. // Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger. // Post-scale width or height, whichever is larger.
@ -711,7 +755,7 @@ impl GPUState {
// Position adjusted for parallax // Position adjusted for parallax
// TODO: adjust parallax for zoom? // TODO: adjust parallax for zoom?
// 1.0 is z-coordinate, which is constant for projectiles // 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. // Game dimensions of this sprite post-scale.
// Post-scale width or height, whichever is larger. // Post-scale width or height, whichever is larger.

View File

@ -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<f32>),
/// 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,
},
}

View File

@ -9,6 +9,7 @@
mod globaluniform; mod globaluniform;
mod gpustate; mod gpustate;
mod inputevent;
mod pipeline; mod pipeline;
mod renderinput; mod renderinput;
mod renderstate; mod renderstate;
@ -18,7 +19,8 @@ mod texturearray;
mod ui; mod ui;
mod vertexbuffer; mod vertexbuffer;
pub use gpustate::GPUState; pub use gpustate::*;
pub use inputevent::*;
pub use renderinput::RenderInput; pub use renderinput::RenderInput;
use renderstate::*; use renderstate::*;

View File

@ -4,23 +4,16 @@ use galactica_content::{Content, System};
use galactica_playeragent::PlayerAgent; use galactica_playeragent::PlayerAgent;
use galactica_system::phys::PhysImage; use galactica_system::phys::PhysImage;
use galactica_util::timing::Timing; use galactica_util::timing::Timing;
use nalgebra::Vector2;
/// Bundles parameters passed to a single call to GPUState::render /// Bundles parameters passed to a single call to GPUState::render
#[derive(Debug)] #[derive(Debug)]
pub struct RenderInput { pub struct RenderInput {
/// Camera position, in world units
pub camera_pos: Vector2<f32>,
/// Player ship data /// Player ship data
pub player: Arc<PlayerAgent>, pub player: Arc<PlayerAgent>,
/// The system we're currently in /// The system we're currently in
pub current_system: Arc<System>, pub current_system: Arc<System>,
/// Height of screen, in world units
pub camera_zoom: f32,
/// The world state to render /// The world state to render
pub phys_img: Arc<PhysImage>, pub phys_img: Arc<PhysImage>,
@ -28,9 +21,6 @@ pub struct RenderInput {
/// The current time, in seconds /// The current time, in seconds
pub current_time: f32, 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 /// Game content
pub ct: Arc<Content>, pub ct: Arc<Content>,

View File

@ -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)
}
}

View File

@ -38,3 +38,31 @@ impl CustomType for PlayerShipStateEvent {
builder.with_name("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<Self>) {
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<Self>) {
builder
.with_name("ScrollEvent")
.with_fn("val", |s: &mut Self| s.val);
}
}

View File

@ -1,11 +1,13 @@
mod conf; mod conf;
mod radialbar; mod radialbar;
mod scrollbox;
mod sprite; mod sprite;
mod textbox; mod textbox;
mod ui; mod ui;
pub use conf::build_conf_module; pub use conf::build_conf_module;
pub use radialbar::build_radialbar_module; pub use radialbar::build_radialbar_module;
pub use scrollbox::build_scrollbox_module;
pub use sprite::build_sprite_module; pub use sprite::build_sprite_module;
pub use textbox::build_textbox_module; pub use textbox::build_textbox_module;
pub use ui::build_ui_module; pub use ui::build_ui_module;

View File

@ -3,7 +3,7 @@ use rhai::{FnNamespace, FuncRegistration, ImmutableString, Module};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use super::super::{Color, Rect}; 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<RefCell<UiState>>) -> Module { pub fn build_radialbar_module(state_src: Rc<RefCell<UiState>>) -> Module {
let mut module = Module::new(); let mut module = Module::new();
@ -23,11 +23,13 @@ pub fn build_radialbar_module(state_src: Rc<RefCell<UiState>>) -> Module {
return; return;
} }
ui_state.names.push(name.clone()); ui_state.add_element(UiElement::RadialBar(UiRadialBar::new(
ui_state.elements.insert(
name.clone(), name.clone(),
UiElement::RadialBar(RadialBar::new(name.clone(), stroke, color, rect, 1.0)), stroke,
); color,
rect,
1.0,
)));
}, },
); );

View File

@ -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<RefCell<UiState>>) -> 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;
}

View File

@ -17,12 +17,12 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
.with_namespace(FnNamespace::Internal) .with_namespace(FnNamespace::Internal)
.set_into_module( .set_into_module(
&mut 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 mut ui_state = state.borrow_mut();
let sprite_handle = ct.sprites.get(&sprite.clone().into()); let sprite = ct.sprites.get(&sprite_name.clone().into());
if sprite_handle.is_none() { if sprite.is_none() {
error!("made a sprite using an invalid source `{sprite}`"); error!("made a sprite using an invalid source `{sprite_name}`");
return; return;
} }
@ -31,15 +31,11 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
return; return;
} }
ui_state.names.push(name.clone()); ui_state.add_element(UiElement::Sprite(UiSprite::new(
ui_state.elements.insert(
name.clone(), name.clone(),
UiElement::Sprite(UiSprite::new( sprite.unwrap().clone(),
name.clone(), rect,
sprite_handle.unwrap().clone(), )));
rect,
)),
);
}, },
); );
@ -60,8 +56,7 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
.set_into_module(&mut module, move |name: ImmutableString| { .set_into_module(&mut module, move |name: ImmutableString| {
let mut ui_state = state.borrow_mut(); let mut ui_state = state.borrow_mut();
if ui_state.elements.contains_key(&name) { if ui_state.elements.contains_key(&name) {
ui_state.elements.remove(&name).unwrap(); ui_state.remove_element(&name);
ui_state.names.retain(|x| *x != name);
} else { } else {
error!("called `sprite::remove` on an invalid name `{name}`") error!("called `sprite::remove` on an invalid name `{name}`")
} }
@ -94,7 +89,7 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
); );
let state = state_src.clone(); let state = state_src.clone();
let _ = FuncRegistration::new("take_edge") let _ = FuncRegistration::new("jump_to")
.with_namespace(FnNamespace::Internal) .with_namespace(FnNamespace::Internal)
.set_into_module( .set_into_module(
&mut module, &mut module,
@ -110,7 +105,7 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
let edge = match edge { let edge = match edge {
Err(_) => { Err(_) => {
error!( 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 edge_name, sprite.index
); );
return; return;
@ -121,7 +116,7 @@ pub fn build_sprite_module(ct_src: Arc<Content>, state_src: Rc<RefCell<UiState>>
x.anim.jump_to(&edge); 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}`")
} }
} }
}, },

View File

@ -4,7 +4,7 @@ use rhai::{FnNamespace, FuncRegistration, ImmutableString, Module};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use super::super::{Color, Rect}; use super::super::{Color, Rect};
use crate::ui::{elements::TextBox, UiElement, UiState}; use crate::ui::{elements::UiTextBox, UiElement, UiState};
pub fn build_textbox_module( pub fn build_textbox_module(
font_src: Rc<RefCell<FontSystem>>, font_src: Rc<RefCell<FontSystem>>,
@ -32,18 +32,14 @@ pub fn build_textbox_module(
return; return;
} }
ui_state.names.push(name.clone()); ui_state.add_element(UiElement::Text(UiTextBox::new(
ui_state.elements.insert( &mut font.borrow_mut(),
name.clone(), name.clone(),
UiElement::Text(TextBox::new( font_size,
&mut font.borrow_mut(), line_height,
name.clone(), rect,
font_size, color,
line_height, )));
rect,
color,
)),
);
}, },
); );

View File

@ -15,5 +15,17 @@ pub fn build_ui_module(state_src: Rc<RefCell<UiState>>) -> Module {
ui_state.set_scene(scene); 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; return module;
} }

View File

@ -1,9 +1,12 @@
use nalgebra::{Point2, Vector2}; use nalgebra::{Point2, Vector2};
use rhai::{CustomType, TypeBuilder}; use rhai::{CustomType, TypeBuilder};
use winit::{dpi::LogicalSize, window::Window}; use winit::{
dpi::{LogicalSize, PhysicalPosition},
window::Window,
};
use super::anchor::Anchor; use super::{anchor::Anchor, vector::UiVector};
use crate::{RenderInput, RenderState}; use crate::RenderState;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Rect { pub struct Rect {
@ -78,7 +81,11 @@ impl Rect {
impl CustomType for Rect { impl CustomType for Rect {
fn build(mut builder: TypeBuilder<Self>) { fn build(mut builder: TypeBuilder<Self>) {
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); 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<f32>) -> bool {
let fac = state.window.scale_factor() as f32; let fac = state.window.scale_factor() as f32;
let window_size = Vector2::new( let window_size = Vector2::new(
state.window_size.width as f32 / fac, state.window_size.width as f32 / fac,
state.window_size.height as f32 / fac, state.window_size.height as f32 / fac,
); );
let pos = input.player.input.get_mouse_pos();
let mouse_pos = Point2::new( let mouse_pos = Point2::new(
pos.x / fac - window_size.x / 2.0, mouse_pos.x / fac - window_size.x / 2.0,
window_size.y / 2.0 - pos.y / fac, window_size.y / 2.0 - mouse_pos.y / fac,
); );
return self.contains_point(mouse_pos); return self.contains_point(mouse_pos);

View File

@ -1,17 +1,19 @@
mod directive;
mod event; mod event;
mod functions; mod functions;
mod helpers; mod helpers;
mod state; mod state;
pub use directive::*;
pub use event::*; pub use event::*;
use glyphon::FontSystem;
pub use helpers::{anchor::*, color::*, rect::*, vector::*}; pub use helpers::{anchor::*, color::*, rect::*, vector::*};
use log::debug;
pub use state::*; pub use state::*;
use super::UiState; use super::UiState;
use galactica_content::Content; 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}; use std::{cell::RefCell, rc::Rc, sync::Arc};
pub fn register_into_engine( pub fn register_into_engine(
@ -29,21 +31,27 @@ pub fn register_into_engine(
.build_type::<State>() .build_type::<State>()
.build_type::<ShipState>() .build_type::<ShipState>()
.build_type::<SystemObjectState>() .build_type::<SystemObjectState>()
.build_type::<OutfitState>()
// Events // Events
.build_type::<MouseClickEvent>() .build_type::<MouseClickEvent>()
.build_type::<MouseHoverEvent>() .build_type::<MouseHoverEvent>()
.build_type::<PlayerShipStateEvent>() .build_type::<PlayerShipStateEvent>()
.build_type::<KeyboardEvent>()
.build_type::<ScrollEvent>()
// Bigger modules // Bigger modules
.register_type_with_name::<Anchor>("Anchor") .register_type_with_name::<Anchor>("Anchor")
.register_static_module("Anchor", exported_module!(anchor_mod).into()); .register_static_module("Anchor", exported_module!(anchor_mod).into())
.register_type_with_name::<PlayerDirective>("PlayerDirective")
.register_static_module(
"PlayerDirective",
exported_module!(player_directive_module).into(),
);
// Extra functions // Extra functions
engine.register_fn("print", move |d: Dynamic| { engine.register_fn("clamp", |x: f32, l: f32, h: f32| x.clamp(l, h));
debug!("{:?}", d);
});
engine.register_fn("clamp", move |x: f32, l: f32, h: f32| x.clamp(l, h));
// Modules // Modules
engine.register_static_module("ui", functions::build_ui_module(state_src.clone()).into());
engine.register_static_module( engine.register_static_module(
"sprite", "sprite",
functions::build_sprite_module(ct_src.clone(), state_src.clone()).into(), functions::build_sprite_module(ct_src.clone(), state_src.clone()).into(),
@ -56,7 +64,10 @@ pub fn register_into_engine(
"radialbar", "radialbar",
functions::build_radialbar_module(state_src.clone()).into(), 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( engine.register_static_module(
"conf", "conf",
functions::build_conf_module(state_src.clone()).into(), functions::build_conf_module(state_src.clone()).into(),

View File

@ -1,4 +1,4 @@
use galactica_content::{Ship, SystemObject}; use galactica_content::{Outfit, Ship, SystemObject};
use galactica_system::{ use galactica_system::{
data::{self}, data::{self},
phys::{objects::PhysShip, PhysSimShipHandle}, phys::{objects::PhysShip, PhysSimShipHandle},
@ -19,14 +19,15 @@ pub struct ShipState {
} }
impl ShipState { impl ShipState {
// All functions passed to rhai MUST be mut,
// even getters.
fn get_content(&mut self) -> &Ship { fn get_content(&mut self) -> &Ship {
let ship = self let ship = self
.input .input
.phys_img .phys_img
.get_ship(self.ship.as_ref().unwrap()) .get_ship(self.ship.as_ref().unwrap())
.unwrap(); .unwrap();
let handle = ship.ship.get_data().get_content(); ship.ship.get_data().get_content()
handle
} }
fn get_ship(&mut self) -> &PhysShip { fn get_ship(&mut self) -> &PhysShip {
@ -116,28 +117,64 @@ impl CustomType for ShipState {
} }
} }
#[derive(Debug, Clone)]
pub struct OutfitState {
outfit: Arc<Outfit>,
}
impl OutfitState {}
impl CustomType for OutfitState {
fn build(mut builder: TypeBuilder<Self>) {
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)] #[derive(Debug, Clone)]
pub struct SystemObjectState { pub struct SystemObjectState {
object: Option<Arc<SystemObject>>, object: Option<Arc<SystemObject>>,
} }
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 { impl CustomType for SystemObjectState {
fn build(mut builder: TypeBuilder<Self>) { fn build(mut builder: TypeBuilder<Self>) {
builder builder
.with_name("SystemObjectState") .with_name("SystemObjectState")
.with_fn("outfitter", Self::outfitter)
// //
// Get landable name // Get landable name
.with_fn("display_name", |s: &mut Self| { .with_fn("display_name", |s: &mut Self| {
s.object s.object
.as_ref() .as_ref()
.unwrap() .unwrap()
.display_name .landable
.as_ref() .as_ref()
.map(|x| x.to_string()) .map(|x| x.display_name.clone())
.unwrap_or_else(|| { .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() "".to_string()
}) })
}) })
@ -147,25 +184,32 @@ impl CustomType for SystemObjectState {
s.object s.object
.as_ref() .as_ref()
.unwrap() .unwrap()
.desc .landable
.as_ref() .as_ref()
.map(|x| x.to_string()) .map(|x| x.desc.clone())
.unwrap_or_else(|| { .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() "".to_string()
}) })
}) })
// //
// Get landable landscape image // Get landable landscape image
.with_fn("image", |s: &mut Self| { .with_fn("image", |s: &mut Self| {
if let Some(sprite) = &s.object.as_ref().unwrap().image { s.object
sprite.index.to_string() .as_ref()
} else { .unwrap()
error!("UI called `image()` on a system object which doesn't provide one"); .landable
"".to_string() .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_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) { .with_fn("==", |a: &mut Self, b: Self| match (&a.object, &b.object) {
(None, _) => false, (None, _) => false,
(_, None) => false, (_, None) => false,
@ -240,7 +284,6 @@ impl CustomType for State {
.with_fn("player_ship", Self::player_ship) .with_fn("player_ship", Self::player_ship)
.with_fn("ships", Self::ships) .with_fn("ships", Self::ships)
.with_fn("objects", Self::objects) .with_fn("objects", Self::objects)
.with_fn("window_aspect", |s: &mut Self| s.window_aspect) .with_fn("window_aspect", |s: &mut Self| s.window_aspect);
.with_fn("camera_zoom", |s: &mut Self| s.input.camera_zoom);
} }
} }

View File

@ -0,0 +1,35 @@
use nalgebra::Vector2;
#[derive(Debug, Clone)]
pub(crate) struct Camera {
/// The position of the camera, in game units
pos: Vector2<f32>,
/// 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<f32>) {
self.pos = pos
}
pub fn get_pos(&self) -> Vector2<f32> {
self.pos
}
pub fn set_zoom(&mut self, zoom: f32) {
self.zoom = zoom
}
pub fn get_zoom(&self) -> f32 {
self.zoom
}
}

View File

@ -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 winit::window::Window;
use crate::{RenderInput, RenderState}; use crate::{RenderInput, RenderState};
use super::OwnedTextArea;
pub(crate) struct FpsIndicator { pub(crate) struct FpsIndicator {
buffer: Buffer, buffer: Rc<Buffer>,
update_counter: u32, update_counter: u32,
} }
@ -21,7 +24,7 @@ impl FpsIndicator {
); );
Self { Self {
buffer, buffer: Rc::new(buffer),
update_counter: 0, update_counter: 0,
} }
} }
@ -29,26 +32,28 @@ impl FpsIndicator {
impl FpsIndicator { impl FpsIndicator {
pub fn step(&mut self, input: &RenderInput, font: &mut FontSystem) { pub fn step(&mut self, input: &RenderInput, font: &mut FontSystem) {
let buffer = Rc::get_mut(&mut self.buffer).unwrap();
if self.update_counter > 0 { if self.update_counter > 0 {
self.update_counter -= 1; self.update_counter -= 1;
return; return;
} }
self.update_counter = 100; self.update_counter = 100;
self.buffer.set_text( buffer.set_text(
font, font,
&input.timing.get_string(), &input.timing.get_string(),
Attrs::new().family(Family::Monospace), Attrs::new().family(Family::Monospace),
Shaping::Basic, Shaping::Basic,
); );
self.buffer.shape_until_scroll(font); buffer.shape_until_scroll(font);
} }
} }
impl<'a, 'b: 'a> FpsIndicator { impl<'a, 'b: 'a> FpsIndicator {
pub fn get_textarea(&'b self, input: &RenderInput, _window: &Window) -> TextArea<'a> { pub fn get_textarea(&'b self, input: &RenderInput, _window: &Window) -> OwnedTextArea {
TextArea { OwnedTextArea {
buffer: &self.buffer, buffer: self.buffer.clone(),
left: 10.0, left: 10.0,
top: 400.0, top: 400.0,
scale: input.ct.config.ui_scale, scale: input.ct.config.ui_scale,

View File

@ -1,9 +1,38 @@
mod fpsindicator; mod fpsindicator;
mod radialbar; mod radialbar;
mod scrollbox;
mod sprite; mod sprite;
mod textbox; mod textbox;
pub(super) use fpsindicator::*; pub(super) use fpsindicator::*;
pub(super) use radialbar::*; pub(super) use radialbar::*;
pub(super) use scrollbox::*;
pub(super) use sprite::*; pub(super) use sprite::*;
pub(super) use textbox::*; 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<Buffer>,
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,
}
}
}

View File

@ -6,7 +6,7 @@ use super::super::api::Rect;
use crate::{ui::api::Color, vertexbuffer::types::RadialBarInstance, RenderInput, RenderState}; use crate::{ui::api::Color, vertexbuffer::types::RadialBarInstance, RenderInput, RenderState};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RadialBar { pub struct UiRadialBar {
pub name: ImmutableString, pub name: ImmutableString,
rect: Rect, rect: Rect,
stroke: f32, stroke: f32,
@ -14,7 +14,7 @@ pub struct RadialBar {
progress: f32, progress: f32,
} }
impl RadialBar { impl UiRadialBar {
pub fn new( pub fn new(
name: ImmutableString, name: ImmutableString,
stroke: f32, stroke: f32,
@ -48,6 +48,4 @@ impl RadialBar {
angle: self.progress * TAU, angle: self.progress * TAU,
}); });
} }
pub fn step(&mut self, _input: &RenderInput, _state: &mut RenderState) {}
} }

View File

@ -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<f32>,
pub elements: HashMap<ImmutableString, Rc<RefCell<UiElement>>>,
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<RefCell<UiElement>>) {
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<Dynamic> {
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<OwnedTextArea> {
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;
}
}

View File

@ -2,13 +2,14 @@ use std::sync::Arc;
use super::super::api::Rect; use super::super::api::Rect;
use crate::{ use crate::{
ui::{api::Color, event::Event}, ui::api::{Color, MouseClickEvent, MouseHoverEvent},
vertexbuffer::types::UiInstance, vertexbuffer::types::UiInstance,
RenderInput, RenderState, InputEvent, RenderInput, RenderState,
}; };
use galactica_content::{Sprite, SpriteAutomaton}; use galactica_content::{Sprite, SpriteAutomaton};
use galactica_util::to_radians; use galactica_util::to_radians;
use rhai::ImmutableString; use nalgebra::Vector2;
use rhai::{Dynamic, ImmutableString};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UiSprite { pub struct UiSprite {
@ -26,8 +27,6 @@ pub struct UiSprite {
mask: Option<Arc<Sprite>>, mask: Option<Arc<Sprite>>,
color: Color, color: Color,
/// If true, ignore mouse events until click is released
waiting_for_release: bool,
has_mouse: bool, has_mouse: bool,
has_click: bool, has_click: bool,
} }
@ -43,7 +42,6 @@ impl UiSprite {
mask: None, mask: None,
has_mouse: false, has_mouse: false,
has_click: false, has_click: false,
waiting_for_release: false,
preserve_aspect: false, preserve_aspect: false,
} }
} }
@ -67,11 +65,23 @@ impl UiSprite {
pub fn set_preserve_aspect(&mut self, preserve_aspect: bool) { pub fn set_preserve_aspect(&mut self, preserve_aspect: bool) {
self.preserve_aspect = preserve_aspect; self.preserve_aspect = preserve_aspect;
} }
}
impl UiSprite {
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) { 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<f32>,
) {
let mut rect = self let mut rect = self
.rect .rect
.to_centered(&state.window, input.ct.config.ui_scale); .to_centered(&state.window, input.ct.config.ui_scale);
rect.pos += offset;
if self.preserve_aspect { if self.preserve_aspect {
let rect_aspect = rect.dim.x / rect.dim.y; 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<Dynamic> {
let r = self let r = self
.rect .rect
.to_centered(&state.window, input.ct.config.ui_scale); .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 // Release mouse when cursor leaves box
if self.has_click && !self.has_mouse { if self.has_click && !self.has_mouse {
self.has_click = false; self.has_click = false;
return Event::MouseRelease;
} }
if r.contains_mouse(input, state) && !self.has_mouse { match event {
if input.player.input.pressed_leftclick() { InputEvent::MouseMove(pos) => {
// If we're holding click when the cursor enters, if r.contains_mouse(state, pos) && !self.has_mouse {
// don't trigger the `Click` event. self.has_mouse = true;
self.waiting_for_release = 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 { return None;
self.waiting_for_release = false;
self.has_mouse = false;
return Event::MouseUnhover;
}
return Event::None;
} }
pub fn step(&mut self, input: &RenderInput, _state: &mut RenderState) { pub fn step(&mut self, t: f32) {
self.anim.step(input.time_since_last_run); self.anim.step(t);
} }
} }

View File

@ -1,27 +1,28 @@
use glyphon::{ use glyphon::{
cosmic_text::Align, Attrs, AttrsOwned, Buffer, Color, FamilyOwned, FontSystem, Metrics, cosmic_text::Align, Attrs, AttrsOwned, Buffer, Color, FamilyOwned, FontSystem, Metrics,
Shaping, Style, TextArea, TextBounds, Weight, Shaping, Style, TextBounds, Weight,
}; };
use nalgebra::Vector2; use nalgebra::Vector2;
use rhai::ImmutableString; use rhai::ImmutableString;
use std::rc::Rc;
use winit::window::Window; use winit::window::Window;
use super::super::api::Rect; use super::{super::api::Rect, OwnedTextArea};
use crate::{ui::api, RenderInput}; use crate::{ui::api, RenderInput};
#[derive(Debug)] #[derive(Debug)]
pub struct TextBox { pub struct UiTextBox {
pub name: ImmutableString, pub name: ImmutableString,
text: String, text: String,
justify: Align, justify: Align,
rect: Rect, rect: Rect,
buffer: Buffer, buffer: Rc<Buffer>,
color: api::Color, color: api::Color,
attrs: AttrsOwned, attrs: AttrsOwned,
} }
impl TextBox { impl UiTextBox {
pub fn new( pub fn new(
font: &mut FontSystem, font: &mut FontSystem,
name: ImmutableString, name: ImmutableString,
@ -38,7 +39,7 @@ impl TextBox {
Self { Self {
name, name,
rect, rect,
buffer, buffer: Rc::new(buffer),
color, color,
justify: Align::Left, justify: Align::Left,
attrs: AttrsOwned::new(Attrs::new()), attrs: AttrsOwned::new(Attrs::new()),
@ -47,14 +48,14 @@ impl TextBox {
} }
fn reflow(&mut self, font: &mut FontSystem) { fn reflow(&mut self, font: &mut FontSystem) {
self.buffer let buffer = Rc::get_mut(&mut self.buffer).unwrap();
.set_text(font, &self.text, self.attrs.as_attrs(), Shaping::Advanced); 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)); 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) { pub fn set_text(&mut self, font: &mut FontSystem, text: &str) {
@ -84,9 +85,19 @@ impl TextBox {
} }
} }
impl<'a, 'b: 'a> TextBox { impl<'a, 'b: 'a> UiTextBox {
pub fn get_textarea(&'b self, input: &RenderInput, window: &Window) -> TextArea<'a> { pub fn get_textarea(&'b self, input: &RenderInput, window: &Window) -> OwnedTextArea {
let rect = self.rect.to_centered(window, input.ct.config.ui_scale); 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<f32>,
) -> 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 // Glypon works with physical pixels, so we must do some conversion
let fac = window.scale_factor() as f32; 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 corner_sw = corner_ne + rect.dim * fac;
let c = self.color.as_array_u8(); let c = self.color.as_array_u8();
TextArea { OwnedTextArea {
buffer: &self.buffer, buffer: self.buffer.clone(),
top: corner_ne.y, top: corner_ne.y,
left: corner_ne.x, left: corner_ne.x,
scale: input.ct.config.ui_scale, scale: input.ct.config.ui_scale,

View File

@ -1,8 +0,0 @@
#[derive(Debug, Copy, Clone)]
pub enum Event {
None,
MouseClick,
MouseRelease,
MouseHover,
MouseUnhover,
}

View File

@ -1,20 +1,17 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use galactica_content::Content; use galactica_content::Content;
use galactica_system::phys::PhysSimShipHandle; use galactica_system::{phys::PhysSimShipHandle, PlayerDirective};
use galactica_util::rhai_error_to_anyhow; use galactica_util::rhai_error_to_anyhow;
use log::debug; use log::{debug, error};
use rhai::{ use rhai::{Dynamic, Engine, ImmutableString, Scope};
packages::{BasicArrayPackage, BasicStringPackage, LogicPackage, MoreStringPackage, Package},
Dynamic, Engine, ImmutableString, Scope,
};
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc}; use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc};
use winit::event::VirtualKeyCode;
use super::{ use super::{
api::{self, MouseClickEvent, MouseHoverEvent, PlayerShipStateEvent}, api::{self, KeyboardEvent, PlayerShipStateEvent, ScrollEvent},
event::Event,
UiConfig, UiElement, UiState, UiConfig, UiElement, UiState,
}; };
use crate::{ui::api::State, RenderInput, RenderState}; use crate::{ui::api::State, InputEvent, RenderInput, RenderState};
pub(crate) struct UiScriptExecutor { pub(crate) struct UiScriptExecutor {
engine: Engine, engine: Engine,
@ -31,14 +28,8 @@ impl UiScriptExecutor {
let scope = Scope::new(); let scope = Scope::new();
let elements = Rc::new(RefCell::new(UiState::new(ct.clone(), state))); let elements = Rc::new(RefCell::new(UiState::new(ct.clone(), state)));
let mut engine = Engine::new_raw(); // TODO: document all functions rhai provides
let mut engine = Engine::new();
// 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());
engine.set_max_expr_depths(0, 0); engine.set_max_expr_depths(0, 0);
// Enables custom operators // Enables custom operators
@ -64,6 +55,129 @@ impl UiScriptExecutor {
(*self.state).borrow().config.clone() (*self.state).borrow().config.clone()
} }
pub fn process_input(
&mut self,
state: &mut RenderState,
input: Arc<RenderInput>,
event: InputEvent,
) -> Result<PlayerDirective> {
let current_scene = (*self.state).borrow().get_scene().clone();
if current_scene.is_none() {
return Ok(PlayerDirective::None);
}
let mut arg: Option<Dynamic> = 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<RenderInput>,
arg: Dynamic,
) -> Result<PlayerDirective> {
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::<PlayerDirective>() {
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 /// Change the current scene
pub fn set_scene(&mut self, state: &RenderState, input: Arc<RenderInput>) -> Result<()> { pub fn set_scene(&mut self, state: &RenderState, input: Arc<RenderInput>) -> Result<()> {
let current_scene = (*self.state).borrow().get_scene().clone(); let current_scene = (*self.state).borrow().get_scene().clone();
@ -87,8 +201,8 @@ impl UiScriptExecutor {
let mut elm = self.state.borrow_mut(); let mut elm = self.state.borrow_mut();
elm.clear(); elm.clear();
drop(elm); drop(elm);
let ct = (*self.state).borrow().ct.clone();
let ct = (*self.state).borrow().ct.clone();
rhai_error_to_anyhow( rhai_error_to_anyhow(
self.engine.call_fn( self.engine.call_fn(
&mut self.scope, &mut self.scope,
@ -122,7 +236,6 @@ impl UiScriptExecutor {
(*self.state).borrow_mut().step(state, input.clone()); (*self.state).borrow_mut().step(state, input.clone());
// Run step() (if it is defined) // Run step() (if it is defined)
let ast = ct let ast = ct
.config .config
.ui_scenes .ui_scenes
@ -158,79 +271,25 @@ impl UiScriptExecutor {
true true
} }
} { } {
rhai_error_to_anyhow( self.run_event_callback(state, input.clone(), Dynamic::from(PlayerShipStateEvent {}))?;
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()))?;
} }
let len = (*self.state).borrow().len(); let len = (*self.state).borrow().len();
for i in 0..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) => { UiElement::Sprite(sprite) => {
// Draw and update sprites
sprite.step(&input, state);
sprite.push_to_buffer(&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) => { UiElement::RadialBar(x) => {
// Draw and update radialbar
x.step(&input, state);
x.push_to_buffer(&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 { UiElement::SubElement { .. } | UiElement::Text(..) => {}
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()))?;
} }
} }

View File

@ -1,9 +1,9 @@
mod api; mod api;
mod event; mod camera;
mod elements;
mod executor; mod executor;
mod state; mod state;
mod elements; pub(crate) use camera::*;
pub(crate) use executor::UiScriptExecutor; pub(crate) use executor::UiScriptExecutor;
pub(crate) use state::*; pub(crate) use state::*;

View File

@ -1,19 +1,39 @@
use galactica_content::Content; use galactica_content::Content;
use glyphon::TextArea;
use log::{debug, error}; use log::{debug, error};
use rhai::ImmutableString; use rhai::ImmutableString;
use std::collections::HashMap; use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc, time::Instant};
use std::sync::Arc;
use winit::window::Window; 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}; use crate::{RenderInput, RenderState};
#[derive(Debug)] #[derive(Debug)]
pub enum UiElement { pub enum UiElement {
Sprite(UiSprite), Sprite(UiSprite),
RadialBar(RadialBar), RadialBar(UiRadialBar),
Text(TextBox), Text(UiTextBox),
Scrollbox(UiScrollbox),
/// This is a sub-element managed by another element
SubElement {
parent: ImmutableString,
element: Rc<RefCell<UiElement>>,
},
}
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)] #[derive(Clone, Debug)]
@ -31,8 +51,13 @@ pub(crate) struct UiState {
show_timings: bool, show_timings: bool,
fps_indicator: FpsIndicator, fps_indicator: FpsIndicator,
last_step: Instant,
pub config: UiConfig, pub config: UiConfig,
/// The player's camera.
/// Only used when drawing physics.
pub camera: Camera,
} }
// TODO: remove this // TODO: remove this
unsafe impl Send for UiState {} unsafe impl Send for UiState {}
@ -52,6 +77,8 @@ impl UiState {
show_phys: false, show_phys: false,
show_starfield: 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<RenderInput>) { pub fn step(&mut self, state: &mut RenderState, input: Arc<RenderInput>) {
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 { if self.show_timings {
self.fps_indicator self.fps_indicator
.step(&input, &mut state.text_font_system.borrow_mut()); .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<UiElement> {
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 { impl<'a> UiState {
pub fn get_textareas(&'a mut self, input: &RenderInput, window: &Window) -> Vec<TextArea<'a>> { pub fn get_textareas(&'a self, input: &RenderInput, window: &Window) -> Vec<OwnedTextArea> {
let mut v = Vec::with_capacity(32); let mut v = Vec::with_capacity(32);
if self.current_scene.is_none() { if self.current_scene.is_none() {
@ -124,9 +193,10 @@ impl<'a> UiState {
v.push(self.fps_indicator.get_textarea(input, window)) v.push(self.fps_indicator.get_textarea(input, window))
} }
for t in self.elements.values() { for e in self.elements.values() {
match &t { match &e {
UiElement::Text(x) => v.push(x.get_textarea(input, window)), UiElement::Text(t) => v.push(t.get_textarea(input, window)),
UiElement::Scrollbox(b) => v.extend(b.get_textareas(input, window)),
_ => {} _ => {}
} }
} }