Reworked UI manager

master
Mark 2024-02-02 22:32:03 -08:00
parent 7d681268b9
commit 348fc40c33
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
20 changed files with 441 additions and 1256 deletions

View File

@ -183,7 +183,7 @@ fn try_main() -> Result<()> {
| ShipState::Flying { .. } => {
if was_landed {
was_landed = false;
gpu.set_scene(&content, RenderScenes::System);
gpu.set_scene(RenderScenes::System);
}
Some(*o.rigidbody.translation())
@ -192,7 +192,7 @@ fn try_main() -> Result<()> {
ShipState::Landed { target } => {
if !was_landed {
was_landed = true;
gpu.set_scene(&content, RenderScenes::Landed);
gpu.set_scene(RenderScenes::Landed);
}
let b = content.get_system_object(*target);

View File

@ -1,3 +1,5 @@
use std::rc::Rc;
use anyhow::Result;
use bytemuck;
use galactica_content::Content;
@ -13,10 +15,7 @@ use crate::{
shaderprocessor::preprocess_shader,
starfield::Starfield,
texturearray::TextureArray,
ui::{
scenes::{UiFlyingScene, UiLandedScene},
UiManager, UiScenes,
},
ui::{UiManager, UiScene},
RenderInput, RenderScenes, RenderState, VertexBuffers,
};
@ -40,7 +39,7 @@ impl GPUState {
/// Make a new GPUState that draws on `window`
pub async fn new(
window: winit::window::Window,
ct: &Content,
ct: Rc<Content>,
scene: RenderScenes,
) -> Result<Self> {
let window_size = window.inner_size();
@ -106,11 +105,11 @@ impl GPUState {
surface.configure(&device, &config);
}
let vertex_buffers = VertexBuffers::new(&device, ct);
let vertex_buffers = VertexBuffers::new(&device, &ct);
// Load uniforms
let global_uniform = GlobalUniform::new(&device);
let texture_array = TextureArray::new(&device, &queue, ct)?;
let texture_array = TextureArray::new(&device, &queue, &ct)?;
// Make sure these match the indices in each shader
let bind_group_layouts = &[
@ -217,23 +216,10 @@ impl GPUState {
.build();
let mut starfield = Starfield::new();
starfield.regenerate(ct);
let mut state = RenderState {
queue,
window,
window_size,
window_aspect,
global_uniform,
vertex_buffers,
text_atlas,
text_cache,
text_font_system,
text_renderer,
};
starfield.regenerate(&ct);
return Ok(Self {
ui: UiManager::new(ct, &mut state),
ui: UiManager::new(ct),
device,
config,
surface,
@ -244,7 +230,19 @@ impl GPUState {
ui_pipeline,
radialbar_pipeline,
scene,
state,
state: RenderState {
queue,
window,
window_size,
window_aspect,
global_uniform,
vertex_buffers,
text_atlas,
text_cache,
text_font_system,
text_renderer,
},
});
}
}
@ -255,18 +253,14 @@ impl GPUState {
&self.state.window
}
/// Change the current scene
pub fn set_scene(&mut self, ct: &Content, scene: RenderScenes) {
/// Change the current scenection
pub fn set_scene(&mut self, scene: RenderScenes) {
debug!("switching to {:?}", scene);
match scene {
RenderScenes::Landed => self
.ui
.set_scene(UiScenes::Landed(UiLandedScene::new(ct, &mut self.state))),
RenderScenes::System => self
.ui
.set_scene(UiScenes::Flying(UiFlyingScene::new(ct, &mut self.state))),
}
RenderScenes::Landed => self.ui.set_scene(&mut self.state, UiScene::Landed).unwrap(),
RenderScenes::System => self.ui.set_scene(&mut self.state, UiScene::Flying).unwrap(),
};
self.scene = scene;
}

View File

@ -1,3 +1,5 @@
use std::rc::Rc;
use galactica_content::{Content, SystemHandle};
use galactica_playeragent::PlayerAgent;
use galactica_system::phys::PhysImage;
@ -29,7 +31,7 @@ pub struct RenderInput<'a> {
pub time_since_last_run: f32,
/// Game content
pub ct: &'a Content,
pub ct: Rc<Content>,
/// Time we spent in each part of the game loop
pub timing: Timing,

View File

@ -40,7 +40,7 @@ impl RenderScene for LandedScene {
});
// Create sprite instances
g.ui.draw(&input, &mut g.state);
g.ui.draw(&input, &mut g.state)?;
// These should match the indices in each shader,
// and should each have a corresponding bind group layout.

View File

@ -54,7 +54,7 @@ impl RenderScene for SystemScene {
Self::push_ships(g, &input, (clip_ne, clip_sw));
Self::push_projectiles(g, &input, (clip_ne, clip_sw));
Self::push_effects(g, &input, (clip_ne, clip_sw));
g.ui.draw(&input, &mut g.state);
g.ui.draw(&input, &mut g.state)?;
// These should match the indices in each shader,
// and should each have a corresponding bind group layout.

View File

@ -1,7 +1,5 @@
use galactica_content::Content;
use galactica_util::constants::{
OBJECT_SPRITE_INSTANCE_LIMIT, RADIALBAR_SPRITE_INSTANCE_LIMIT, UI_SPRITE_INSTANCE_LIMIT,
};
use galactica_util::constants::{OBJECT_SPRITE_INSTANCE_LIMIT, UI_SPRITE_INSTANCE_LIMIT};
use glyphon::{FontSystem, SwashCache, TextAtlas, TextRenderer};
use std::rc::Rc;
use wgpu::BufferAddress;
@ -154,6 +152,7 @@ impl RenderState {
self.vertex_buffers.object_counter as u32
}
/*
pub fn push_radialbar_buffer(&mut self, instance: RadialBarInstance) {
// Enforce buffer limit
if self.vertex_buffers.radialbar_counter as u64 > RADIALBAR_SPRITE_INSTANCE_LIMIT {
@ -168,6 +167,7 @@ impl RenderState {
);
self.vertex_buffers.radialbar_counter += 1;
}
*/
pub fn get_radialbar_counter(&self) -> u32 {
self.vertex_buffers.radialbar_counter as u32

View File

@ -1,146 +1,265 @@
use std::fmt::Debug;
use anyhow::Result;
use galactica_content::Content;
use glyphon::TextArea;
use log::debug;
use log::{debug, error, trace};
use rhai::{Array, Dynamic, Engine, Scope, AST};
use std::{cell::RefCell, collections::HashSet, fmt::Debug, rc::Rc};
use super::scenes::{UiFlyingScene, UiLandedScene, UiOutfitterScene};
use crate::{RenderInput, RenderState};
use super::{
api::{self, SceneAction, SpriteElement, TextBoxBuilder},
util::{Sprite, TextBox},
};
use crate::{
ui::api::{SpriteBuilder, State},
RenderInput, RenderState,
};
/// Output from a ui scene step
pub struct UiSceneStepResult {
/// If Some, switch to this scene
pub new_scene: Option<UiScenes>,
#[derive(Debug, Copy, Clone)]
pub enum MouseEvent {
Click,
Release,
Enter,
Leave,
None,
}
pub trait UiScene<'this>
where
Self: 'this,
{
/// Draw this scene
fn draw(&mut self, input: &RenderInput, state: &mut RenderState);
/// Update this scene's state for this frame.
/// Handles clicks, keys, etc.
fn step(&mut self, input: &RenderInput, state: &mut RenderState) -> UiSceneStepResult;
/// Add all textareas in this scene to `v`
fn get_textareas(
&'this self,
v: &mut Vec<TextArea<'this>>,
input: &RenderInput,
state: &RenderState,
);
}
pub(crate) enum UiScenes {
Landed(UiLandedScene),
Flying(UiFlyingScene),
Outfitter(UiOutfitterScene),
}
impl Debug for UiScenes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl MouseEvent {
pub fn is_enter(&self) -> bool {
match self {
Self::Flying(_) => write!(f, "UiScenes::Flying"),
Self::Landed(_) => write!(f, "UiScenes::Landed"),
Self::Outfitter(_) => write!(f, "UiScenes::Outfitter"),
}
}
}
/*
impl UiScenes {
fn is_flying(&self) -> bool {
match self {
Self::Flying(_) => true,
Self::Enter => true,
_ => false,
}
}
fn is_landed(&self) -> bool {
pub fn is_click(&self) -> bool {
match self {
Self::Landed(_) => true,
_ => false,
}
}
fn is_outfitter(&self) -> bool {
match self {
Self::Outfitter(_) => true,
Self::Click => true,
_ => false,
}
}
}
*/
impl<'a> UiScene<'a> for UiScenes {
fn draw(&mut self, input: &RenderInput, state: &mut RenderState) {
match self {
Self::Flying(s) => s.draw(input, state),
Self::Landed(s) => s.draw(input, state),
Self::Outfitter(s) => s.draw(input, state),
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum UiScene {
Landed,
Flying,
Outfitter,
}
fn step(&mut self, input: &RenderInput, state: &mut RenderState) -> UiSceneStepResult {
impl ToString for UiScene {
fn to_string(&self) -> String {
match self {
Self::Flying(s) => s.step(input, state),
Self::Landed(s) => s.step(input, state),
Self::Outfitter(s) => s.step(input, state),
}
}
fn get_textareas(
&'a self,
v: &mut Vec<TextArea<'a>>,
input: &RenderInput,
state: &RenderState,
) {
match self {
Self::Flying(s) => s.get_textareas(v, input, state),
Self::Landed(s) => s.get_textareas(v, input, state),
Self::Outfitter(s) => s.get_textareas(v, input, state),
Self::Flying => "flying".to_string(),
Self::Landed => "landed".to_string(),
Self::Outfitter => "outfitter".to_string(),
}
}
}
pub struct UiManager {
current_scene: UiScenes,
enum UiElement {
Sprite(Rc<RefCell<Sprite>>),
Text(TextBox),
}
impl UiElement {
pub fn new_sprite(sprite: Sprite) -> Self {
Self::Sprite(Rc::new(RefCell::new(sprite)))
}
pub fn new_text(text: TextBox) -> Self {
Self::Text(text)
}
pub fn sprite(&self) -> Option<Rc<RefCell<Sprite>>> {
match self {
Self::Sprite(s) => Some(s.clone()),
_ => None,
}
}
pub fn text(&self) -> Option<&TextBox> {
match self {
Self::Text(t) => Some(t),
_ => None,
}
}
}
pub(crate) struct UiManager {
current_scene: UiScene,
engine: Engine,
scope: Scope<'static>,
elements: Vec<UiElement>,
ct: Rc<Content>,
}
impl UiManager {
pub fn new(ct: &Content, state: &mut RenderState) -> Self {
pub fn new(ct: Rc<Content>) -> Self {
let scope = Scope::new();
let mut engine = Engine::new();
api::register_into_engine(&mut engine);
Self {
current_scene: UiScenes::Flying(UiFlyingScene::new(ct, state)),
ct,
current_scene: UiScene::Flying,
engine,
scope,
elements: Vec::new(),
}
}
pub fn get_scene_ast(ct: &Content, scene: UiScene) -> &AST {
match scene {
UiScene::Landed => &ct.get_config().ui_landed_scene,
UiScene::Flying => &ct.get_config().ui_flying_scene,
UiScene::Outfitter => &ct.get_config().ui_outfitter_scene,
}
}
/// Change the current scene
pub fn set_scene(&mut self, scene: UiScenes) {
pub fn set_scene(&mut self, state: &mut RenderState, scene: UiScene) -> Result<()> {
debug!("switching to {:?}", scene);
self.current_scene = scene;
self.scope.clear();
self.elements.clear();
let mut used_names = HashSet::new();
trace!("running init for `{}`", self.current_scene.to_string());
let builders: Array = self.engine.call_fn(
&mut self.scope,
Self::get_scene_ast(&self.ct, self.current_scene),
"init",
(State {
planet_landscape: "ui::landscape::test".to_string(),
planet_name: "Earth".to_string(),
},),
)?;
trace!("found {:?} builders", builders.len());
for v in builders {
if v.is::<SpriteBuilder>() {
let s = v.cast::<SpriteBuilder>();
if used_names.contains(&s.name) {
error!(
"UI scene `{}` re-uses element name `{}`",
self.current_scene.to_string(),
s.name
);
} else {
used_names.insert(s.name.clone());
}
self.elements.push(UiElement::new_sprite(Sprite::new(
&self.ct, s.name, s.sprite, s.mask, s.rect,
)));
} else if v.is::<TextBoxBuilder>() {
let t = v.cast::<TextBoxBuilder>();
if used_names.contains(&t.name) {
error!(
"UI scene `{}` re-uses element name `{}`",
self.current_scene.to_string(),
t.name
);
} else {
used_names.insert(t.name.clone());
}
let mut b = TextBox::new(
state,
t.name,
t.font_size,
t.line_height,
t.font,
t.justify,
t.rect,
);
b.set_text(state, &t.text);
self.elements.push(UiElement::new_text(b));
} else {
// TODO: better message
error!("bad type in builder array")
}
}
return Ok(());
}
/// Draw all ui elements
pub fn draw(&mut self, input: &RenderInput, state: &mut RenderState) {
loop {
let r = self.current_scene.step(input, state);
if let Some(new_scene) = r.new_scene {
debug!("{:?} changed scene", self.current_scene);
self.set_scene(new_scene)
} else {
break;
pub fn draw(&mut self, input: &RenderInput, state: &mut RenderState) -> Result<()> {
let mut iter = self
.elements
.iter()
.map(|x| x.sprite())
.filter(|x| x.is_some())
.map(|x| x.unwrap());
let action: SceneAction = loop {
let e = match iter.next() {
Some(e) => e,
None => break SceneAction::None,
};
let mut x = (*e).borrow_mut();
let m = x.check_mouse(input, state);
x.step(input, state);
x.push_to_buffer(input, state);
drop(x);
// we MUST drop here, since script calls mutate the sprite RefCell
let action: Dynamic = match m {
MouseEvent::None => Dynamic::from(SceneAction::None),
MouseEvent::Release | MouseEvent::Click => self.engine.call_fn(
&mut self.scope,
Self::get_scene_ast(&self.ct, self.current_scene),
"click",
(SpriteElement::new(self.ct.clone(), e.clone()), m.is_click()),
)?,
MouseEvent::Leave | MouseEvent::Enter => self.engine.call_fn(
&mut self.scope,
Self::get_scene_ast(&self.ct, self.current_scene),
"hover",
(SpriteElement::new(self.ct.clone(), e.clone()), m.is_enter()),
)?,
};
if let Some(action) = action.try_cast::<SceneAction>() {
match action {
SceneAction::None => {}
_ => {
break action;
}
}
}
};
drop(iter);
match action {
SceneAction::None => {}
SceneAction::SceneOutfitter => self.set_scene(state, UiScene::Outfitter)?,
SceneAction::SceneLanded => self.set_scene(state, UiScene::Landed)?,
}
self.current_scene.draw(input, state);
}
/// Textareas to show while player is flying
pub fn get_textareas(&self, input: &RenderInput, state: &RenderState) -> Vec<TextArea> {
let mut v = Vec::with_capacity(5);
self.current_scene.get_textareas(&mut v, input, state);
return v;
return Ok(());
}
}
impl<'a> UiManager {
/// Get textareas
pub fn get_textareas(
&'a mut self,
input: &RenderInput,
state: &RenderState,
) -> Vec<TextArea<'a>> {
self.elements
.iter()
.map(|x| x.text())
.filter(|x| x.is_some())
.map(|x| x.unwrap())
.map(|x| x.get_textarea(state, input))
.collect()
}
}

View File

@ -1,6 +1,6 @@
mod api;
mod manager;
pub(crate) mod scenes;
mod util;
pub use manager::UiManager;
pub(crate) use manager::UiScenes;
pub(crate) use manager::UiManager;
pub(crate) use manager::UiScene;

View File

@ -1,59 +0,0 @@
use glyphon::{Attrs, Buffer, Color, Family, Metrics, Shaping, TextArea, TextBounds};
use crate::{RenderInput, RenderState};
pub(super) struct FpsIndicator {
buffer: Buffer,
update_counter: u32,
}
impl FpsIndicator {
pub fn new(state: &mut RenderState) -> Self {
let mut buffer = Buffer::new(&mut state.text_font_system, Metrics::new(15.0, 20.0));
buffer.set_size(
&mut state.text_font_system,
state.window_size.width as f32,
state.window_size.height as f32,
);
buffer.shape_until_scroll(&mut state.text_font_system);
Self {
buffer,
update_counter: 0,
}
}
}
impl FpsIndicator {
pub fn update(&mut self, input: &RenderInput, state: &mut RenderState) {
// Update once every n frames
if self.update_counter > 0 {
self.update_counter -= 1;
return;
}
self.update_counter = 100;
self.buffer.set_text(
&mut state.text_font_system,
&input.timing.get_string(),
Attrs::new().family(Family::Monospace),
Shaping::Basic,
);
}
pub fn get_textarea(&self) -> TextArea {
TextArea {
buffer: &self.buffer,
left: 10.0,
top: 400.0,
scale: 1.0,
bounds: TextBounds {
left: 10,
top: 400,
right: 300,
bottom: 800,
},
default_color: Color::rgb(255, 255, 255),
}
}
}

View File

@ -1,6 +0,0 @@
mod fpsindicator;
mod radar;
mod scene;
mod status;
pub use scene::UiFlyingScene;

View File

@ -1,265 +0,0 @@
use galactica_system::data::ShipState;
use galactica_util::{clockwise_angle, to_radians};
use nalgebra::{Point2, Rotation2, Vector2};
use crate::{vertexbuffer::types::UiInstance, PositionAnchor, RenderInput, RenderState};
pub(super) struct Radar {
last_player_position: Point2<f32>,
}
impl Radar {
pub fn new() -> Self {
Self {
last_player_position: Point2::new(0.0, 0.0),
}
}
}
impl Radar {
pub fn draw(&mut self, input: &RenderInput, state: &mut RenderState) {
let radar_range = 4000.0;
let radar_size = 300.0;
let hide_range = 0.85;
let shrink_distance = 20.0;
let system_object_scale = 1.0 / 600.0;
let ship_scale = 1.0 / 10.0;
// TODO: maybe a cleaner solution for last posititon?
// This is necessary because the player may be dead or landed
let player_ship = input
.phys_img
.get_ship(&galactica_system::phys::PhysSimShipHandle(
input.player.ship.unwrap(),
))
.unwrap();
match player_ship.ship.get_data().get_state() {
ShipState::Dead => {}
ShipState::Landed { target } => {
let landed_body = input.ct.get_system_object(*target);
self.last_player_position = Point2::new(landed_body.pos.x, landed_body.pos.y);
}
ShipState::UnLanding { .. }
| ShipState::Landing { .. }
| ShipState::Flying { .. }
| ShipState::Collapsing { .. } => {
self.last_player_position = (*player_ship.rigidbody.translation()).into();
}
};
// TODO: don't hard-code these, add config options
let planet_sprite = input.ct.get_sprite_handle("ui::planetblip");
let ship_sprite = input.ct.get_sprite_handle("ui::shipblip");
let arrow_sprite = input.ct.get_sprite_handle("ui::centerarrow");
let sprite = input.ct.get_sprite(input.ct.get_sprite_handle("ui::radar"));
let texture_a = sprite.get_first_frame(); // ANIMATE
// Push this object's instance
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwNw.to_int(),
position: [10.0, -10.0],
angle: 0.0,
dim: [sprite.aspect * radar_size, radar_size],
color: [1.0, 1.0, 1.0, 1.0],
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
mask_index: [0, 0],
});
// Draw system objects
let system = input.ct.get_system(input.current_system);
for o in &system.objects {
let size = (o.size / o.pos.z) / (radar_range * system_object_scale);
let p = Point2::new(o.pos.x, o.pos.y);
let d = (p - self.last_player_position) / radar_range;
// Add half the blip sprite's height to distance
let m = d.magnitude() + (size / (2.0 * radar_size));
if m < hide_range {
// Shrink blips as they get closeto the edge
let size = size.min((hide_range - m) * size * shrink_distance);
if size <= 2.0 {
// Don't draw super tiny sprites, they flicker
continue;
}
let sprite = input.ct.get_sprite(planet_sprite);
let texture_a = sprite.get_first_frame(); // ANIMATE
// Push this object's instance
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwC.to_int(),
position: (Point2::new(radar_size / 2.0 + 10.0, radar_size / -2.0 - 10.0)
+ (d * (radar_size / 2.0)))
.into(),
angle: o.angle,
dim: [sprite.aspect * size, size],
color: [0.5, 0.5, 0.5, 1.0],
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
mask_index: [0, 0],
})
};
}
// Draw ships
for s in input.phys_img.iter_ships() {
let ship = input.ct.get_ship(s.ship.get_data().get_content());
let (color, z_scale) = match s.ship.get_data().get_state() {
ShipState::Dead | ShipState::Landed { .. } => {
continue;
}
// TODO: different color for landing?
// TODO: scale blip for ship z-position
ShipState::Landing { .. } => ([0.2, 0.2, 0.2, 1.0], 1.0),
ShipState::UnLanding { .. } => ([0.2, 0.2, 0.2, 1.0], 1.0),
ShipState::Collapsing { .. } => {
// TODO: configurable
([0.2, 0.2, 0.2, 1.0], 1.0)
}
ShipState::Flying { .. } => {
let c = input.ct.get_faction(s.ship.get_data().get_faction()).color;
([c[0], c[1], c[2], 1.0], 1.0)
}
};
let size = (ship.size * input.ct.get_sprite(ship.sprite).aspect) * ship_scale * z_scale;
let p: Point2<f32> = {
if s.ship.collider == input.player.ship.unwrap() {
self.last_player_position
} else {
(*s.rigidbody.translation()).into()
}
};
let d = (p - self.last_player_position) / radar_range;
let m = d.magnitude() + (size / (2.0 * radar_size));
if m < hide_range {
let size = size.min((hide_range - m) * size * shrink_distance);
if size < 2.0 {
continue;
}
let position = Point2::new(radar_size / 2.0 + 10.0, radar_size / -2.0 - 10.0)
+ (d * (radar_size / 2.0));
let sprite = input.ct.get_sprite(ship_sprite);
let texture_a = sprite.get_first_frame(); // ANIMATE
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwC.to_int(),
position: position.into(),
angle: player_ship.rigidbody.rotation().angle(),
dim: [sprite.aspect * size, size],
color,
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
mask_index: [0, 0],
});
}
}
// Draw viewport frame
let d = Vector2::new(
(input.camera_zoom / 2.0) * state.window_aspect,
input.camera_zoom / 2.0,
) / radar_range;
let m = d.magnitude();
let d = d * (radar_size / 2.0);
let color = [0.3, 0.3, 0.3, 1.0];
if m < 0.8 {
let sprite = input.ct.get_sprite_handle("ui::radarframe");
let size = 7.0f32.min((0.8 - m) * 70.0);
let sprite = input.ct.get_sprite(sprite);
let texture_a = sprite.get_first_frame(); // ANIMATE
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwNw.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) - d.x,
(radar_size / -2.0 - 10.0) + d.y,
)
.into(),
angle: to_radians(90.0),
dim: [sprite.aspect * size, size],
color,
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
mask_index: [0, 0],
});
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwSw.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) - d.x,
(radar_size / -2.0 - 10.0) - d.y,
)
.into(),
angle: to_radians(180.0),
dim: [sprite.aspect * size, size],
color,
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
mask_index: [0, 0],
});
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwSe.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) + d.x,
(radar_size / -2.0 - 10.0) - d.y,
)
.into(),
angle: to_radians(270.0),
dim: [sprite.aspect * size, size],
color,
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
mask_index: [0, 0],
});
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwNe.to_int(),
position: Point2::new(
(radar_size / 2.0 + 10.0) + d.x,
(radar_size / -2.0 - 10.0) + d.y,
)
.into(),
angle: to_radians(0.0),
dim: [sprite.aspect * size, size],
color,
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
mask_index: [0, 0],
});
}
// Arrow to center of system
let q = Point2::new(0.0, 0.0) - self.last_player_position;
let m = q.magnitude();
if m > 200.0 {
let angle = clockwise_angle(&Vector2::new(1.0, 0.0), &q);
let position = Point2::new(10.0 + (radar_size / 2.0), -10.0 - (radar_size / 2.0))
+ Rotation2::new(angle) * Vector2::new(0.915 * (radar_size / 2.0), 0.0);
let sprite = input.ct.get_sprite(arrow_sprite);
let texture_a = sprite.get_first_frame(); // ANIMATE
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NwC.to_int(),
position: position.into(),
angle,
dim: [10.0 * sprite.aspect, 10.0],
color: [1.0, 1.0, 1.0, 1f32.min((m - 200.0) / 400.0)],
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
mask_index: [0, 0],
});
}
}
}

View File

@ -1,45 +0,0 @@
use galactica_content::Content;
use glyphon::TextArea;
use super::{fpsindicator::FpsIndicator, radar::Radar, status::Status};
use crate::{
ui::manager::{UiScene, UiSceneStepResult},
RenderInput, RenderState,
};
pub struct UiFlyingScene {
radar: Radar,
status: Status,
fps: FpsIndicator,
}
impl UiFlyingScene {
pub fn new(_ct: &Content, state: &mut RenderState) -> Self {
Self {
radar: Radar::new(),
status: Status::new(),
fps: FpsIndicator::new(state),
}
}
}
impl<'this> UiScene<'this> for UiFlyingScene {
fn step(&mut self, input: &RenderInput, state: &mut RenderState) -> UiSceneStepResult {
self.fps.update(input, state);
return UiSceneStepResult { new_scene: None };
}
fn draw(&mut self, input: &RenderInput, state: &mut RenderState) {
self.radar.draw(input, state);
self.status.draw(input, state);
}
fn get_textareas(
&'this self,
v: &mut Vec<TextArea<'this>>,
_input: &RenderInput,
_state: &RenderState,
) {
v.push(self.fps.get_textarea());
}
}

View File

@ -1,96 +0,0 @@
use galactica_system::data::ShipState;
use std::f32::consts::TAU;
use crate::{
vertexbuffer::types::{RadialBarInstance, UiInstance},
PositionAnchor, RenderInput, RenderState,
};
pub(super) struct Status {}
impl Status {
pub fn new() -> Self {
Self {}
}
}
impl Status {
pub fn draw(&mut self, input: &RenderInput, state: &mut RenderState) {
let max_shields;
let current_shields;
let current_hull;
let max_hull;
let player_ship = input
.phys_img
.get_ship(&galactica_system::phys::PhysSimShipHandle(
input.player.ship.unwrap(),
))
.unwrap();
match player_ship.ship.get_data().get_state() {
ShipState::Dead => {
current_shields = 0.0;
current_hull = 0.0;
max_shields = player_ship
.ship
.get_data()
.get_outfits()
.get_shield_strength();
max_hull = input
.ct
.get_ship(player_ship.ship.get_data().get_content())
.hull;
}
ShipState::UnLanding { .. }
| ShipState::Landing { .. }
| ShipState::Landed { .. }
| ShipState::Collapsing { .. }
| ShipState::Flying { .. } => {
current_shields = player_ship.ship.get_data().get_shields();
current_hull = player_ship.ship.get_data().get_hull();
max_shields = player_ship
.ship
.get_data()
.get_outfits()
.get_shield_strength();
max_hull = input
.ct
.get_ship(player_ship.ship.get_data().get_content())
.hull;
}
}
let sprite = input
.ct
.get_sprite(input.ct.get_sprite_handle("ui::status"));
let texture_a = sprite.get_first_frame(); // ANIMATE
state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::NeNe.to_int(),
position: [-10.0, -10.0],
angle: 0.0,
dim: [sprite.aspect * 200.0, 200.0],
color: [1.0, 1.0, 1.0, 1.0],
texture_index: [texture_a, texture_a],
texture_fade: 1.0,
mask_index: [0, 0],
});
state.push_radialbar_buffer(RadialBarInstance {
position: [-19.0, -19.0],
anchor: PositionAnchor::NeNe.to_int(),
diameter: 182.0,
stroke: 5.0,
color: [0.3, 0.6, 0.8, 1.0],
angle: (current_shields / max_shields) * TAU,
});
state.push_radialbar_buffer(RadialBarInstance {
position: [-27.0, -27.0],
anchor: PositionAnchor::NeNe.to_int(),
diameter: 166.0,
stroke: 5.0,
color: [0.8, 0.7, 0.5, 1.0],
angle: (current_hull / max_hull) * TAU,
});
}
}

View File

@ -1,135 +0,0 @@
use galactica_content::{Content, SystemObject, SystemObjectHandle};
use galactica_system::{data::ShipState, phys::PhysSimShipHandle};
use glyphon::{Attrs, TextArea, Weight};
use crate::{
ui::{
manager::{UiScene, UiSceneStepResult, UiScenes},
util::{UiSprite, UiTextArea},
},
RenderInput, RenderState,
};
use super::UiOutfitterScene;
pub struct UiLandedScene {
// UI elements
description: UiTextArea,
title: UiTextArea,
frame: UiSprite,
landscape: UiSprite,
button: UiSprite,
/// What object we're displaying currently.
/// Whenever this changes, we need to reflow text.
current_object: Option<SystemObjectHandle>,
/// True if we've caught a left click event.
/// Used for edge detection.
leftclick_down: bool,
}
impl UiLandedScene {
pub fn new(ct: &Content, state: &mut RenderState) -> Self {
let frame = UiSprite::from(ct, &ct.get_ui().landed_frame);
let button = UiSprite::from(ct, &ct.get_ui().landed_button);
let landscape = UiSprite::from_with_sprite(
ct,
&ct.get_ui().landed_landscape,
ct.get_sprite_handle("ui::landscape::test"),
);
let s = Self {
// height of element in logical pixels
current_object: None,
leftclick_down: false,
description: UiTextArea::from(ct, state, &ct.get_ui().landed_planet_desc),
title: UiTextArea::from(ct, state, &ct.get_ui().landed_planet_name),
frame,
landscape,
button,
};
return s;
}
fn reflow(&mut self, planet: &SystemObject, state: &mut RenderState) {
self.description.set_text(
state,
&planet.desc,
Attrs::new()
.weight(Weight::NORMAL)
.family(glyphon::Family::SansSerif),
);
self.title.set_text(
state,
&planet.name,
Attrs::new()
.weight(Weight::BOLD)
.family(glyphon::Family::Serif),
);
self.current_object = Some(planet.handle);
}
}
impl<'this> UiScene<'this> for UiLandedScene {
fn step(&mut self, input: &RenderInput, state: &mut RenderState) -> UiSceneStepResult {
self.button.step(input, state);
self.landscape.step(input, state);
self.frame.step(input, state);
let mut new_scene = None;
if input.player.input.pressed_leftclick() && !self.leftclick_down {
self.leftclick_down = true;
if self.button.contains_mouse(input, state) {
new_scene = Some(UiScenes::Outfitter(UiOutfitterScene::new(input.ct, state)));
}
} else if !input.player.input.pressed_leftclick() {
self.leftclick_down = false;
}
return UiSceneStepResult { new_scene };
}
fn draw(&mut self, input: &RenderInput, state: &mut RenderState) {
// Get required data
let ship_handle = input.player.ship.unwrap();
let ship_data = &input
.phys_img
.get_ship(&PhysSimShipHandle(ship_handle))
.unwrap()
.ship;
let planet_handle = match ship_data.get_data().get_state() {
ShipState::Landed { target } => *target,
_ => unreachable!("tried to draw planet interface while not landed!"),
};
let planet = input.ct.get_system_object(planet_handle);
// Reconfigure for new planet if necessary
if self
.current_object
.map(|x| x != planet_handle)
.unwrap_or(true)
{
self.reflow(planet, state);
}
// Draw elements
self.button.push_to_buffer(input, state);
self.landscape.push_to_buffer(input, state);
self.frame.push_to_buffer(input, state);
}
fn get_textareas(
&'this self,
v: &mut Vec<TextArea<'this>>,
_input: &RenderInput,
state: &RenderState,
) {
v.push(self.description.get_textarea(state));
v.push(self.title.get_textarea(state));
}
}

View File

@ -1,7 +0,0 @@
mod flying;
mod landed;
mod outfitter;
pub use flying::UiFlyingScene;
pub use landed::UiLandedScene;
pub use outfitter::UiOutfitterScene;

View File

@ -1,129 +0,0 @@
use galactica_content::{Content, SystemObject, SystemObjectHandle, UiPositionAnchor};
use galactica_system::{data::ShipState, phys::PhysSimShipHandle};
use glyphon::{cosmic_text::Align, Attrs, Color, Metrics, TextArea, Weight};
use nalgebra::{Point2, Vector2};
use crate::{
ui::{
manager::{UiScene, UiSceneStepResult, UiScenes},
util::{SpriteRect, UiSprite, UiTextArea},
},
RenderInput, RenderState,
};
use super::UiLandedScene;
pub struct UiOutfitterScene {
// UI elements
se_box: UiSprite,
exit_button: UiSprite,
exit_text: UiTextArea,
/// What object we're displaying currently.
/// Whenever this changes, we need to reflow text.
current_object: Option<SystemObjectHandle>,
/// True if we've caught a left click event.
/// Used for edge detection.
leftclick_down: bool,
}
impl UiOutfitterScene {
pub fn new(ct: &Content, state: &mut RenderState) -> Self {
let exit_button = UiSprite::from(ct, &ct.get_ui().outfitter_exit_button);
let se_box = UiSprite::from(ct, &ct.get_ui().outfitter_se_box);
let s = Self {
// height of element in logical pixels
current_object: None,
leftclick_down: false,
exit_text: UiTextArea::new(
ct,
state,
SpriteRect {
pos: Point2::new(0.0, 0.0),
dim: Vector2::new(200.0, 200.0), // TODO: do this better
anchor_self: UiPositionAnchor::Center,
anchor_parent: UiPositionAnchor::Center,
},
Metrics::new(16.0, 18.0),
Color::rgb(255, 255, 255),
Align::Center,
),
se_box,
exit_button,
};
return s;
}
fn reflow(&mut self, planet: &SystemObject, state: &mut RenderState) {
self.exit_text.set_text(
state,
"Exit",
Attrs::new()
.weight(Weight::NORMAL)
.family(glyphon::Family::SansSerif),
);
self.current_object = Some(planet.handle);
}
}
impl<'this> UiScene<'this> for UiOutfitterScene {
fn step(&mut self, input: &RenderInput, state: &mut RenderState) -> UiSceneStepResult {
self.se_box.step(input, state);
self.exit_button.step(input, state);
let mut new_scene = None;
if input.player.input.pressed_leftclick() && !self.leftclick_down {
self.leftclick_down = true;
if self.exit_button.contains_mouse(input, state) {
new_scene = Some(UiScenes::Landed(UiLandedScene::new(input.ct, state)));
}
} else if !input.player.input.pressed_leftclick() {
self.leftclick_down = false;
}
return UiSceneStepResult { new_scene };
}
fn draw(&mut self, input: &RenderInput, state: &mut RenderState) {
// Get required data
let ship_handle = input.player.ship.unwrap();
let ship_data = &input
.phys_img
.get_ship(&PhysSimShipHandle(ship_handle))
.unwrap()
.ship;
let planet_handle = match ship_data.get_data().get_state() {
ShipState::Landed { target } => *target,
_ => unreachable!("tried to draw planet interface while not landed!"),
};
let planet = input.ct.get_system_object(planet_handle);
// Reconfigure for new planet if necessary
if self
.current_object
.map(|x| x != planet_handle)
.unwrap_or(true)
{
self.reflow(planet, state);
}
// Draw elements
self.se_box.push_to_buffer(input, state);
self.exit_button.push_to_buffer(input, state);
}
fn get_textareas(
&'this self,
v: &mut Vec<TextArea<'this>>,
_input: &RenderInput,
state: &RenderState,
) {
v.push(self.exit_text.get_textarea(state));
}
}

View File

@ -1,171 +1,5 @@
mod sprite;
mod textarea;
mod textbox;
use galactica_content::UiPositionAnchor;
pub(super) use sprite::UiSprite;
pub(super) use textarea::UiTextArea;
use nalgebra::{Point2, Vector2};
use winit::dpi::LogicalSize;
use crate::{RenderInput, RenderState};
/// Represents a rectangular region
#[derive(Debug, Clone, Copy)]
pub(crate) struct SpriteRect {
/// The position of the top-left corner of this rectangle, in fractional units.
/// (0.0 is left edge of sprite, 1.0 is right edge)
pub pos: Point2<f32>,
/// The width and height of this rectangle, in fractional units.
/// 1.0 will be as tall as the sprite, 0.5 will be half as tall
pub dim: Vector2<f32>,
/// How to compute this rectangle's coordinates relative to its parent
pub anchor_self: UiPositionAnchor,
/// How to compute this rectangle's coordinates relative to its parent
pub anchor_parent: UiPositionAnchor,
}
impl SpriteRect {
/*
pub fn to_centered_relative(&self, parent: SpriteRectCentered) -> SpriteRectCentered {
let dim = Vector2::new(self.dim.x * parent.dim.x, self.dim.y * parent.dim.y);
let pos = Vector2::new(self.pos.x * parent.dim.x, self.pos.y * parent.dim.y);
let mut zero = match self.anchor_parent {
PositionAnchor::Center => parent.pos,
PositionAnchor::NorthWest => Point2::new(
parent.pos.x - (parent.dim.x / 2.0),
parent.pos.y + (parent.dim.y / 2.0),
),
PositionAnchor::SouthWest => Point2::new(
parent.pos.x - (parent.dim.x / 2.0),
parent.pos.y - (parent.dim.y / 2.0),
),
PositionAnchor::NorthEast => Point2::new(
parent.pos.x + (parent.dim.x / 2.0),
parent.pos.y + (parent.dim.y / 2.0),
),
PositionAnchor::SouthEast => Point2::new(
parent.pos.x + (parent.dim.x / 2.0),
parent.pos.y - (parent.dim.y / 2.0),
),
};
match self.anchor_self {
PositionAnchor::Center => {}
PositionAnchor::NorthWest => {
zero += Vector2::new(dim.x, -dim.y) / 2.0;
}
PositionAnchor::NorthEast => {
zero += Vector2::new(-dim.x, -dim.y) / 2.0;
}
PositionAnchor::SouthWest => {
zero += Vector2::new(dim.x, dim.y) / 2.0;
}
PositionAnchor::SouthEast => {
zero += Vector2::new(-dim.x, dim.y) / 2.0;
}
};
return SpriteRectCentered {
dim,
pos: zero + pos,
};
}
*/
/// Convert this rectangle to a centered rectangle.
pub fn to_centered(&self, state: &RenderState) -> CenteredSpriteRect {
let w: LogicalSize<f32> = state.window_size.to_logical(state.window.scale_factor());
let w = Vector2::new(w.width, w.height);
let mut pos = self.pos;
let dim = self.dim;
// Origin
match self.anchor_parent {
UiPositionAnchor::Center => {}
UiPositionAnchor::NorthWest => {
pos += Vector2::new(-w.x, w.y) / 2.0;
}
UiPositionAnchor::SouthWest => {
pos += Vector2::new(-w.x, -w.y) / 2.0;
}
UiPositionAnchor::NorthEast => {
pos += Vector2::new(w.x, w.y) / 2.0;
}
UiPositionAnchor::SouthEast => {
pos += Vector2::new(w.x, -w.y) / 2.0;
}
}
// Offset for self dimensions
match self.anchor_self {
UiPositionAnchor::Center => {}
UiPositionAnchor::NorthWest => {
pos += Vector2::new(dim.x, -dim.y) / 2.0;
}
UiPositionAnchor::NorthEast => {
pos += Vector2::new(-dim.x, -dim.y) / 2.0;
}
UiPositionAnchor::SouthWest => {
pos += Vector2::new(dim.x, dim.y) / 2.0;
}
UiPositionAnchor::SouthEast => {
pos += Vector2::new(dim.x, dim.y) / 2.0;
}
};
return CenteredSpriteRect { pos, dim };
}
}
/// Represents a rectangular region, in absolute coordinates relative to the screen center.
#[derive(Debug, Clone, Copy)]
pub(crate) struct CenteredSpriteRect {
/// The position of the top-left corner of this rectangle, in fractional units.
/// (0.0 is left edge of sprite, 1.0 is right edge)
pub pos: Point2<f32>,
/// The width and height of this rectangle, in fractional units.
/// 1.0 will be as tall as the sprite, 0.5 will be half as tall
pub dim: Vector2<f32>,
}
impl CenteredSpriteRect {
pub fn contains_point(&self, pt: Point2<f32>) -> bool {
let ne = self.pos + Vector2::new(-self.dim.x, self.dim.y) / 2.0;
let sw = self.pos + Vector2::new(self.dim.x, -self.dim.y) / 2.0;
return (pt.y < ne.y && pt.y > sw.y) && (pt.x > ne.x && pt.x < sw.x);
}
}
pub(super) trait UiElement {
fn push_to_buffer_child(
&self,
input: &RenderInput,
state: &mut RenderState,
parent_pos: Point2<f32>,
parent_size: Vector2<f32>,
);
}
pub use sprite::*;
pub use textbox::*;

View File

@ -1,118 +1,57 @@
use galactica_content::{Content, SectionEdge, SpriteAutomaton, SpriteHandle, UiSpriteConfig};
use galactica_content::{Content, SpriteAutomaton, SpriteHandle};
use galactica_util::to_radians;
use nalgebra::{Point2, Vector2};
use super::{CenteredSpriteRect, SpriteRect};
use crate::{vertexbuffer::types::UiInstance, RenderInput, RenderState};
use super::super::api::Rect;
use crate::{ui::manager::MouseEvent, vertexbuffer::types::UiInstance, RenderInput, RenderState};
pub struct UiSprite {
#[derive(Debug, Clone)]
pub struct Sprite {
pub anim: SpriteAutomaton,
mask: Option<SpriteHandle>,
pub rect: Rect,
pub mask: Option<SpriteHandle>,
pub name: String,
rect: SpriteRect,
has_mouse: bool,
on_mouse_enter: Option<SectionEdge>,
on_mouse_leave: Option<SectionEdge>,
has_click: bool,
/// If true, ignore mouse events until click is released
waiting_for_release: bool,
}
impl UiSprite {
impl Sprite {
pub fn new(
ct: &Content,
sprite: SpriteHandle,
mask: Option<SpriteHandle>,
rect: SpriteRect,
on_mouse_enter: Option<SectionEdge>,
on_mouse_leave: Option<SectionEdge>,
name: String,
sprite: String,
mask: Option<String>,
rect: Rect,
) -> Self {
return Self {
anim: SpriteAutomaton::new(ct, sprite),
mask,
rect,
has_mouse: false,
on_mouse_enter,
on_mouse_leave,
let sprite_handle = ct.get_sprite_handle(&sprite).unwrap(); // TODO: errors
let mask = {
if mask.is_some() {
Some(ct.get_sprite_handle(&mask.unwrap()).unwrap())
} else {
None
}
};
}
pub fn from(ct: &Content, ui: &UiSpriteConfig) -> Self {
if ui.sprite.is_none() {
unreachable!("called `UiSprite.from()` on a UiSprite with a None sprite!")
Self {
name,
anim: SpriteAutomaton::new(&ct, sprite_handle),
rect,
mask,
has_mouse: false,
has_click: false,
waiting_for_release: false,
}
Self::from_with_sprite(ct, ui, ui.sprite.unwrap())
}
pub fn from_with_sprite(ct: &Content, ui: &UiSpriteConfig, sprite: SpriteHandle) -> Self {
Self::new(
ct,
sprite,
ui.mask,
SpriteRect {
pos: ui.rect.pos,
dim: ui.rect.dim,
anchor_self: ui.rect.anchor_self,
anchor_parent: ui.rect.anchor_parent,
},
ui.on_mouse_enter,
ui.on_mouse_leave,
)
}
pub fn step(&mut self, input: &RenderInput, state: &RenderState) {
if self.contains_mouse(input, state) && !self.has_mouse && self.on_mouse_enter.is_some() {
self.has_mouse = true;
self.anim.jump_to(input.ct, self.on_mouse_enter.unwrap())
} else if !self.contains_mouse(input, state)
&& self.has_mouse
&& self.on_mouse_leave.is_some()
{
self.has_mouse = false;
self.anim.jump_to(input.ct, self.on_mouse_leave.unwrap())
}
self.anim.step(input.ct, input.time_since_last_run);
}
pub fn contains_mouse(&self, input: &RenderInput, state: &RenderState) -> bool {
let rect = self.get_rect(state);
let fac = state.window.scale_factor() as f32;
let window_size = Vector2::new(
state.window_size.width as f32 / fac,
state.window_size.height as f32 / fac,
);
let pos = input.player.input.get_mouse_pos();
let mouse_pos = Point2::new(
pos.x / fac - window_size.x / 2.0,
window_size.y / 2.0 - pos.y / fac,
);
return rect.contains_point(mouse_pos);
}
pub fn get_rect(
&self,
state: &RenderState,
//parent: Option<SpriteRectCentered>,
) -> CenteredSpriteRect {
/*
if let Some(parent) = parent {
return self.rect.to_centered_relative(parent);
} else {
return self.rect.to_centered(state);
}*/
return self.rect.to_centered(state);
}
/// Add this image to the gpu sprite buffer
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) {
let rect = self.get_rect(state);
let rect = self.rect.to_centered(state, input.ct.get_config().ui_scale);
// TODO: use both dimensions,
// not just height
let anim_state = self.anim.get_texture_idx();
state.push_ui_buffer(UiInstance {
anchor: crate::PositionAnchor::CC.to_int(),
position: rect.pos.into(),
@ -125,10 +64,60 @@ impl UiSprite {
.mask
.map(|x| {
let sprite = input.ct.get_sprite(x);
let texture_b = sprite.get_first_frame(); // ANIMATE
let texture_b = sprite.get_first_frame(); // TODO: animate?
[1, texture_b]
})
.unwrap_or([0, 0]),
});
}
pub fn check_mouse(&mut self, input: &RenderInput, state: &mut RenderState) -> MouseEvent {
let r = self.rect.to_centered(state, input.ct.get_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 MouseEvent::Click;
}
if self.has_mouse && self.has_click && !input.player.input.pressed_leftclick() {
self.has_click = false;
return MouseEvent::Release;
}
// Release mouse when cursor leaves box
if self.has_click && !self.has_mouse {
self.has_click = false;
return MouseEvent::Release;
}
if r.contains_mouse(input, state) && !self.has_mouse {
if input.player.input.pressed_leftclick() {
// If we're holding click when the cursor enters,
// don't trigger the `Click` event.
self.waiting_for_release = true;
}
self.has_mouse = true;
return MouseEvent::Enter;
}
if !r.contains_mouse(input, state) && self.has_mouse {
self.waiting_for_release = false;
self.has_mouse = false;
return MouseEvent::Leave;
}
return MouseEvent::None;
}
pub fn step(&mut self, input: &RenderInput, _state: &mut RenderState) {
self.anim.step(&input.ct, input.time_since_last_run);
}
}

View File

@ -1,102 +0,0 @@
use galactica_content::{Content, UiTextAlign, UiTextConfig};
use glyphon::{cosmic_text::Align, Attrs, Buffer, Color, Metrics, Shaping, TextArea, TextBounds};
use nalgebra::Vector2;
use super::SpriteRect;
use crate::RenderState;
/// Represents a text area inside a sprite.
pub(crate) struct UiTextArea {
/// Bounds of text area
rect: SpriteRect,
/// Text buffer
buffer: Buffer,
/// Text color
color: Color,
/// Text alignment
align: Align,
}
impl UiTextArea {
pub fn new(
_ct: &Content,
state: &mut RenderState,
rect: SpriteRect,
text_metrics: Metrics,
color: Color,
align: Align,
) -> Self {
let mut s = Self {
buffer: Buffer::new(&mut state.text_font_system, text_metrics),
rect,
align,
color,
};
s.buffer.set_size(
&mut state.text_font_system,
s.rect.dim.x * state.window.scale_factor() as f32,
s.rect.dim.y * state.window.scale_factor() as f32,
);
return s;
}
pub fn from(ct: &Content, state: &mut RenderState, ui: &UiTextConfig) -> Self {
Self::new(
ct,
state,
SpriteRect {
pos: ui.rect.pos,
dim: ui.rect.dim,
anchor_self: ui.rect.anchor_self,
anchor_parent: ui.rect.anchor_parent,
},
Metrics::new(ui.font_size, ui.line_height),
Color::rgb(255, 255, 255),
match ui.align {
UiTextAlign::Center => Align::Center,
UiTextAlign::Left => Align::Left,
},
)
}
pub fn set_text(&mut self, state: &mut RenderState, text: &str, attrs: Attrs) {
self.buffer
.set_text(&mut state.text_font_system, text, attrs, Shaping::Advanced);
for l in &mut self.buffer.lines {
l.set_align(Some(self.align));
}
self.buffer.shape_until_scroll(&mut state.text_font_system);
}
pub fn get_textarea(&self, state: &RenderState) -> TextArea {
let rect = self.rect.to_centered(state);
// Glypon works with physical pixels, so we must do some conversion
let fac = state.window.scale_factor() as f32;
let corner_ne = Vector2::new(
(rect.pos.x - rect.dim.x / 2.0) * fac + state.window_size.width as f32 / 2.0,
state.window_size.height as f32 / 2.0 - (rect.pos.y * fac + rect.dim.y / 2.0),
);
let corner_sw = corner_ne + rect.dim * fac;
TextArea {
buffer: &self.buffer,
top: corner_ne.y,
left: corner_ne.x,
scale: 1.0,
bounds: TextBounds {
top: (corner_ne.y) as i32,
bottom: (corner_sw.y) as i32,
left: (corner_ne.x) as i32,
right: (corner_sw.x) as i32,
},
default_color: self.color,
}
}
}

View File

@ -0,0 +1,91 @@
use glyphon::{cosmic_text::Align, Attrs, Buffer, Color, Metrics, Shaping, TextArea, TextBounds};
use nalgebra::Vector2;
use super::super::api::{Rect, TextBoxFont, TextBoxJustify};
use crate::{RenderInput, RenderState};
#[derive(Debug)]
pub struct TextBox {
pub name: String,
pub font: TextBoxFont,
pub justify: TextBoxJustify,
pub rect: Rect,
pub buffer: Buffer,
}
impl TextBox {
pub fn new(
state: &mut RenderState,
name: String,
font_size: f32,
line_height: f32,
font: TextBoxFont,
justify: TextBoxJustify,
rect: Rect,
) -> Self {
let mut buffer = Buffer::new(
&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
buffer.set_size(&mut state.text_font_system, rect.dim.x, rect.dim.y);
Self {
name,
font,
justify,
rect,
buffer,
}
}
pub fn set_text(&mut self, state: &mut RenderState, text: &str) {
let mut attrs = Attrs::new();
attrs = match self.font {
TextBoxFont::Monospace => attrs.family(glyphon::Family::Monospace),
TextBoxFont::SansSerif => attrs.family(glyphon::Family::SansSerif),
TextBoxFont::Serif => attrs.family(glyphon::Family::Serif),
};
self.buffer
.set_text(&mut state.text_font_system, text, attrs, Shaping::Advanced);
for l in &mut self.buffer.lines {
l.set_align(Some(match self.justify {
TextBoxJustify::Center => Align::Center,
TextBoxJustify::Left => Align::Left,
}));
}
self.buffer.shape_until_scroll(&mut state.text_font_system);
}
}
impl<'a, 'b: 'a> TextBox {
pub fn get_textarea(&'b self, state: &RenderState, input: &RenderInput) -> TextArea<'a> {
let rect = self.rect.to_centered(state, input.ct.get_config().ui_scale);
// Glypon works with physical pixels, so we must do some conversion
let fac = state.window.scale_factor() as f32;
let corner_ne = Vector2::new(
(rect.pos.x - rect.dim.x / 2.0) * fac + state.window_size.width as f32 / 2.0,
state.window_size.height as f32 / 2.0 - (rect.pos.y * fac + rect.dim.y / 2.0),
);
let corner_sw = corner_ne + rect.dim * fac;
TextArea {
buffer: &self.buffer,
top: corner_ne.y,
left: corner_ne.x,
scale: input.ct.get_config().ui_scale,
bounds: TextBounds {
top: (corner_ne.y) as i32,
bottom: (corner_sw.y) as i32,
left: (corner_ne.x) as i32,
right: (corner_sw.x) as i32,
},
default_color: Color::rgb(255, 255, 255),
}
}
}