Reworked ui scripting

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

2
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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