diff --git a/assets b/assets index df96cae..e589879 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit df96cae3dec9e5dce1ee0f1d59b1dbb90add8db8 +Subproject commit e5898796144c7590c3cf18e14867a5ec47637fd4 diff --git a/content/sprite.toml b/content/sprite.toml index f0ac321..e7dff2d 100644 --- a/content/sprite.toml +++ b/content/sprite.toml @@ -94,6 +94,9 @@ file = "ui/landscape-mask.png" [sprite."ui::outfitbg"] file = "ui/outfit-bg.png" +[sprite."ui::outfitterbox"] +file = "ui/outfitter-box.png" + [sprite."ui::shopsepbar"] file = "ui/shop-sep-bar.png" diff --git a/content/ui.toml b/content/ui.toml index 9fc14c8..1367384 100644 --- a/content/ui.toml +++ b/content/ui.toml @@ -1,12 +1,10 @@ [ui.status] # TODO: unified color value -# TODO: anchor # TODO: bar type: linear/radial # TODO: bar as ui util struct # TODO: mouse collider # TODO: modular UI (how?) -# TODO: textbox configuration # shield_bar.pos = [-19, -19] # shield_bar.diameter = 182 @@ -24,19 +22,59 @@ [ui.landed] frame.sprite = "ui::planet" -frame.pos = [0.0, 0.0] -frame.dim = [800.0, 800.0] +frame.rect.pos = [0.0, 0.0] +frame.rect.dim = [800.0, 800.0] +frame.rect.anchor_self = "center" +frame.rect.anchor_parent = "center" landscape.mask = "ui::landscapemask" -landscape.pos = [32.0, 75.0] -landscape.dim = [344.0, 173.0] -landscape.loc_div = 512 +landscape.rect.pos = [-350.0, 282.8] +landscape.rect.dim = [537.5, 270.31] +landscape.rect.anchor_self = "northwest" +landscape.rect.anchor_parent = "center" + button.sprite = "ui::planet::button" -button.pos = [356, 90] -button.dim = [113.569, 20] -button.loc_div = 512 +button.rect.pos = [178.12, 254.6] +button.rect.dim = [175.16, 38.437] +button.rect.anchor_self = "northwest" +button.rect.anchor_parent = "center" button.on_mouse_enter.edge = "on:top" button.on_mouse_enter.duration = 0.1 button.on_mouse_leave.edge = "off:top" button.on_mouse_leave.duration = 0.1 + +planet_name.rect.pos = [-143.89, 273] +planet_name.rect.dim = [121.98, 18.094] +planet_name.rect.anchor_self = "northwest" +planet_name.rect.anchor_parent = "center" +planet_name.font_size = 19 +planet_name.line_height = 19 +planet_name.align = "center" + +planet_desc.rect.pos = [-358.43, -32] +planet_desc.rect.dim = [673.91, 153.75] +planet_desc.rect.anchor_self = "northwest" +planet_desc.rect.anchor_parent = "center" +planet_desc.font_size = 16 +planet_desc.line_height = 18 +planet_desc.align = "left" + + +[ui.outfitter] + +se_box.sprite = "ui::outfitterbox" +se_box.rect.pos = [-10.0, -10.0] +se_box.rect.dim = [512.0, 337.0] # todo: auto aspect +se_box.rect.anchor_self = "southwest" +se_box.rect.anchor_parent = "southwest" + +exit_button.sprite = "ui::button" +exit_button.rect.pos = [279.07, 135.38] +exit_button.rect.dim = [173.44, 45.01] +exit_button.rect.anchor_self = "northwest" +exit_button.rect.anchor_parent = "southwest" +exit_button.on_mouse_enter.edge = "on:top" +exit_button.on_mouse_enter.duration = 0.1 +exit_button.on_mouse_leave.edge = "off:top" +exit_button.on_mouse_leave.duration = 0.1 diff --git a/crates/content/src/part/ui.rs b/crates/content/src/part/ui.rs index ec970e0..1e2991b 100644 --- a/crates/content/src/part/ui.rs +++ b/crates/content/src/part/ui.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use nalgebra::{Point2, Vector2}; +use serde::Deserialize; use crate::{handle::SpriteHandle, Content, ContentBuildContext, SectionEdge}; @@ -14,6 +15,7 @@ pub(crate) mod syntax { #[derive(Debug, Deserialize)] pub struct Ui { pub landed: UiLanded, + pub outfitter: UiOutfitter, } #[derive(Debug, Deserialize)] @@ -21,17 +23,22 @@ pub(crate) mod syntax { pub frame: UiSprite, pub landscape: UiSprite, pub button: UiSprite, + pub planet_name: UiText, + pub planet_desc: UiText, } #[derive(Debug, Deserialize)] - pub struct UiSprite { - pub sprite: Option, + pub struct UiOutfitter { + pub se_box: UiSprite, + pub exit_button: UiSprite, + } + + #[derive(Debug, Deserialize)] + pub struct UiRect { pub pos: [f32; 2], pub dim: [f32; 2], - pub loc_div: Option, - pub mask: Option, - pub on_mouse_enter: Option, - pub on_mouse_leave: Option, + pub anchor_self: super::UiPositionAnchor, + pub anchor_parent: super::UiPositionAnchor, } #[derive(Debug, Deserialize)] @@ -40,15 +47,53 @@ pub(crate) mod syntax { pub duration: f32, } + #[derive(Debug, Deserialize)] + pub struct UiText { + pub rect: UiRect, + pub font_size: f32, + pub line_height: f32, + pub align: super::UiTextAlign, + } + + impl UiText { + pub fn build( + self, + _build_context: &ContentBuildContext, + _ct: &Content, + ) -> Result { + let rect = { + super::UiRect { + pos: Point2::new(self.rect.pos[0], self.rect.pos[1]), + dim: Vector2::new(self.rect.dim[0], self.rect.dim[1]), + anchor_self: self.rect.anchor_self, + anchor_parent: self.rect.anchor_parent, + } + }; + + return Ok(super::UiTextConfig { + rect, + font_size: self.font_size, + line_height: self.line_height, + align: self.align, + }); + } + } + + #[derive(Debug, Deserialize)] + pub struct UiSprite { + pub sprite: Option, + pub rect: UiRect, + pub mask: Option, + pub on_mouse_enter: Option, + pub on_mouse_leave: Option, + } + impl UiSprite { pub fn build( self, build_context: &ContentBuildContext, ct: &Content, - // If true, this ui sprite will be positioned relative to another - is_child: bool, - // If true, fail if self.sprite is missing. // If false, fail if self.sprite exists. // This is false for sprites that may change---for example, planet landscapes @@ -112,19 +157,12 @@ pub(crate) mod syntax { } }; - let d = self.loc_div.unwrap_or(1.0); - let rect = { - if is_child { - super::UiSpriteRect::Relative { - pos: Point2::new(self.pos[0], self.pos[1]) / d, - dim: Vector2::new(self.dim[0], self.dim[1]) / d, - } - } else { - super::UiSpriteRect::Absolute { - pos: Point2::new(self.pos[0], self.pos[1]) / d, - dim: Vector2::new(self.dim[0], self.dim[1]) / d, - } + super::UiRect { + pos: Point2::new(self.rect.pos[0], self.rect.pos[1]), + dim: Vector2::new(self.rect.dim[0], self.rect.dim[1]), + anchor_self: self.rect.anchor_self, + anchor_parent: self.rect.anchor_parent, } }; @@ -139,6 +177,42 @@ pub(crate) mod syntax { } } +/// How to align text in a text box +#[derive(Debug, Deserialize, Clone, Copy)] +pub enum UiTextAlign { + /// Center-align + #[serde(rename = "center")] + Center, + + /// Left-align + #[serde(rename = "left")] + Left, +} + +/// How to position a UI sprite +#[derive(Debug, Deserialize, Clone, Copy)] +pub enum UiPositionAnchor { + /// Anchored at center + #[serde(rename = "center")] + Center, + + /// Anchored at top-left + #[serde(rename = "northwest")] + NorthWest, + + /// Anchored at top-right + #[serde(rename = "northeast")] + NorthEast, + + /// Anchored at bottom-left + #[serde(rename = "southwest")] + SouthWest, + + /// Anchored at bottom-right + #[serde(rename = "southeast")] + SouthEast, +} + /// UI Configuration #[derive(Debug, Clone)] pub struct Ui { @@ -150,54 +224,35 @@ pub struct Ui { /// Test button pub landed_button: UiSpriteConfig, + + /// Landed planet name + pub landed_planet_name: UiTextConfig, + + /// Landed planet description + pub landed_planet_desc: UiTextConfig, + + /// Outfitter exit button + pub outfitter_exit_button: UiSpriteConfig, + + /// Outfitter south-east box + pub outfitter_se_box: UiSpriteConfig, } /// A UI sprite's position #[derive(Debug, Clone)] -pub enum UiSpriteRect { - /// Positioning relative to a parent sprite - Relative { - // Note that both pos and dim include transparent pixels, - // of this sprite AND its parent. +pub struct UiRect { + /// The position of the center of this sprite, in logical pixels, + /// with 0, 0 at the center of the screen + pub pos: Point2, - // We use the top left corner here because that's how inkscape - // positions its objects. This makes it very easy to compute position. - // TODO: maybe add anchors here too? - /// The position of this sprite's northeast corner, relative to its parent's NE corner. - /// Positive X is right, positive Y is down. - pos: Point2, + /// This sprite's w and h, in logical pixels. + pub dim: Vector2, - /// This sprite's w and h, as a fraction of the parent's width and height. - dim: Vector2, - }, + /// The point on this sprite that pos is anchored to + pub anchor_self: UiPositionAnchor, - /// Absolute positioning - Absolute { - /// The position of the center of this sprite, in logical pixels, - /// with 0, 0 at the center of the screen - pos: Point2, - - /// This sprite's w and h, in logical pixels. - dim: Vector2, - }, -} - -impl UiSpriteRect { - /// Get this rectangle's position - pub fn get_pos(&self) -> &Point2 { - match self { - Self::Relative { pos, .. } => pos, - Self::Absolute { pos, .. } => pos, - } - } - - /// Get this rectangle's dimensions - pub fn get_dim(&self) -> &Vector2 { - match self { - Self::Relative { dim, .. } => dim, - Self::Absolute { dim, .. } => dim, - } - } + /// The point on the parent that pos is relative to + pub anchor_parent: UiPositionAnchor, } /// A single UI sprite instance @@ -210,7 +265,7 @@ pub struct UiSpriteConfig { pub mask: Option, /// This sprite's position and size - pub rect: UiSpriteRect, + pub rect: UiRect, /// Animation edge to take when mouse enters this sprite pub on_mouse_enter: Option, @@ -219,6 +274,22 @@ pub struct UiSpriteConfig { pub on_mouse_leave: Option, } +/// A UI text box +#[derive(Debug, Clone)] +pub struct UiTextConfig { + /// Text box location and dimensions + pub rect: UiRect, + + /// Text box font size + pub font_size: f32, + + /// Text box line height + pub line_height: f32, + + /// Text box alignment + pub align: UiTextAlign, +} + impl crate::Build for Ui { type InputSyntaxType = syntax::Ui; @@ -231,18 +302,39 @@ impl crate::Build for Ui { landed_frame: ui .landed .frame - .build(build_context, ct, false, true) - .with_context(|| format!("in ui config (frame)"))?, + .build(build_context, ct, true) + .with_context(|| format!("in ui config (landed_frame)"))?, landed_landscape: ui .landed .landscape - .build(build_context, ct, true, false) - .with_context(|| format!("in ui config (image)"))?, + .build(build_context, ct, false) + .with_context(|| format!("in ui config (landed_landscape)"))?, landed_button: ui .landed .button - .build(build_context, ct, true, true) - .with_context(|| format!("in ui config (button)"))?, + .build(build_context, ct, true) + .with_context(|| format!("in ui config (landed_button)"))?, + landed_planet_name: ui + .landed + .planet_name + .build(build_context, ct) + .with_context(|| format!("in ui config (landed_planet_name)"))?, + landed_planet_desc: ui + .landed + .planet_desc + .build(build_context, ct) + .with_context(|| format!("in ui config (landed_planet_desc)"))?, + + outfitter_exit_button: ui + .outfitter + .exit_button + .build(build_context, ct, true) + .with_context(|| format!("in ui config (outfitter_exit_button)"))?, + outfitter_se_box: ui + .outfitter + .se_box + .build(build_context, ct, true) + .with_context(|| format!("in ui config (outfitter_se_box)"))?, }); return Ok(()); diff --git a/crates/render/shaders/ui.wgsl b/crates/render/shaders/ui.wgsl index 34721cf..40847a6 100644 --- a/crates/render/shaders/ui.wgsl +++ b/crates/render/shaders/ui.wgsl @@ -4,7 +4,7 @@ struct InstanceInput { @location(2) anchor: u32, @location(3) position: vec2, @location(4) angle: f32, - @location(5) size: f32, + @location(5) dim: vec2, @location(6) color_transform: vec4, @location(7) texture_index: vec2, @location(8) texture_fade: f32, @@ -44,30 +44,26 @@ fn transform_vertex( texture_index: u32, ) -> vec4 { + // Window size in logical pixels let window_dim = ( vec2(global_data.window_size_w, global_data.window_size_h) / global_data.window_scale ); - let scale = instance.size / window_dim.y; - let aspect = ( - global_atlas[instance.texture_index.x].width / - global_atlas[instance.texture_index.x].height - ); + let scale = instance.dim.y / window_dim.y; - // Apply scale and sprite aspect - // Note that our mesh starts centered at (0, 0). This is important! var pos: vec2 = vec2( - vertex_position.x * scale * aspect, + vertex_position.x * scale * (instance.dim.x / instance.dim.y), vertex_position.y * scale ); - // Apply rotation (and adjust sprite angle, since sprites point north) pos = mat2x2( vec2(cos(instance.angle - 1.5708), sin(instance.angle - 1.5708)), vec2(-sin(instance.angle - 1.5708), cos(instance.angle - 1.5708)) ) * pos; + // Apply rotation (and adjust sprite angle, since sprites point north) + // Correct for screen aspect, preserving height pos = vec2( pos.x / global_data.window_aspect, @@ -77,7 +73,7 @@ fn transform_vertex( pos = pos + anchor( instance.anchor, instance.position, - vec2(instance.size * aspect, instance.size) + instance.dim ); return vec4(pos, 0.0, 1.0); @@ -90,9 +86,6 @@ fn vertex_main( ) -> VertexOutput { var out: VertexOutput; - // TODO: this will break if we try to use texture 0. - // implement animations for ui sprites & fix that here. - // Pick texture size by the size of the visible texture // (texture index 0 is special, it's the "hidden" texture) if instance.texture_index.x == 0u && instance.texture_index.y == 0u { diff --git a/crates/render/src/ui/scenes/flying/radar.rs b/crates/render/src/ui/scenes/flying/radar.rs index faa227e..ec45148 100644 --- a/crates/render/src/ui/scenes/flying/radar.rs +++ b/crates/render/src/ui/scenes/flying/radar.rs @@ -63,7 +63,7 @@ impl Radar { anchor: PositionAnchor::NwNw.to_int(), position: [10.0, -10.0], angle: 0.0, - size: radar_size, + 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, @@ -96,7 +96,7 @@ impl Radar { + (d * (radar_size / 2.0))) .into(), angle: o.angle, - size, + dim: [sprite.aspect * size, size], color: [0.5, 0.5, 0.5, 1.0], texture_index: [texture_a, texture_a], texture_fade: 1.0, @@ -154,7 +154,7 @@ impl Radar { anchor: PositionAnchor::NwC.to_int(), position: position.into(), angle: player_ship.rigidbody.rotation().angle(), - size, + dim: [sprite.aspect * size, size], color, texture_index: [texture_a, texture_a], texture_fade: 1.0, @@ -186,7 +186,7 @@ impl Radar { ) .into(), angle: to_radians(90.0), - size, + dim: [sprite.aspect * size, size], color, texture_index: [texture_a, texture_a], texture_fade: 1.0, @@ -201,7 +201,7 @@ impl Radar { ) .into(), angle: to_radians(180.0), - size, + dim: [sprite.aspect * size, size], color, texture_index: [texture_a, texture_a], texture_fade: 1.0, @@ -216,7 +216,7 @@ impl Radar { ) .into(), angle: to_radians(270.0), - size, + dim: [sprite.aspect * size, size], color, texture_index: [texture_a, texture_a], texture_fade: 1.0, @@ -231,7 +231,7 @@ impl Radar { ) .into(), angle: to_radians(0.0), - size, + dim: [sprite.aspect * size, size], color, texture_index: [texture_a, texture_a], texture_fade: 1.0, @@ -254,7 +254,7 @@ impl Radar { anchor: PositionAnchor::NwC.to_int(), position: position.into(), angle, - size: 10.0, + 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, diff --git a/crates/render/src/ui/scenes/flying/status.rs b/crates/render/src/ui/scenes/flying/status.rs index 9845445..b55282b 100644 --- a/crates/render/src/ui/scenes/flying/status.rs +++ b/crates/render/src/ui/scenes/flying/status.rs @@ -68,7 +68,7 @@ impl Status { anchor: PositionAnchor::NeNe.to_int(), position: [-10.0, -10.0], angle: 0.0, - size: 200.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, diff --git a/crates/render/src/ui/scenes/landed.rs b/crates/render/src/ui/scenes/landed.rs index c672a4c..af0780a 100644 --- a/crates/render/src/ui/scenes/landed.rs +++ b/crates/render/src/ui/scenes/landed.rs @@ -1,12 +1,11 @@ use galactica_content::{Content, SystemObject, SystemObjectHandle}; use galactica_system::{data::ShipState, phys::PhysSimShipHandle}; -use glyphon::{cosmic_text::Align, Attrs, Color, Metrics, TextArea, Weight}; -use nalgebra::{Point2, Vector2}; +use glyphon::{Attrs, TextArea, Weight}; use crate::{ ui::{ manager::{UiScene, UiSceneStepResult, UiScenes}, - util::{SpriteRect, UiSprite, UiTextArea}, + util::{UiSprite, UiTextArea}, }, RenderInput, RenderState, }; @@ -45,35 +44,8 @@ impl UiLandedScene { current_object: None, leftclick_down: false, - description: UiTextArea::new( - ct, - state, - frame.get_sprite(), - Point2::new(0.0, 0.0), - frame.get_height(), - SpriteRect { - pos: Point2::new(25.831, 284.883) / 512.0, - dim: Vector2::new(433.140, 97.220) / 512.0, - }, - Metrics::new(16.0, 18.0), - Color::rgb(255, 255, 255), - Align::Left, - ), - - title: UiTextArea::new( - ct, - state, - frame.get_sprite(), - Point2::new(0.0, 0.0), - frame.get_height(), - SpriteRect { - pos: Point2::new(165.506, 82.0) / 512.0, - dim: Vector2::new(74.883, 17.0) / 512.0, - }, - Metrics::new(19.0, 19.0), - Color::rgb(255, 255, 255), - Align::Center, - ), + description: UiTextArea::from(ct, state, &ct.get_ui().landed_planet_desc), + title: UiTextArea::from(ct, state, &ct.get_ui().landed_planet_name), frame, landscape, @@ -105,15 +77,14 @@ impl UiLandedScene { impl<'this> UiScene<'this> for UiLandedScene { fn step(&mut self, input: &RenderInput, state: &mut RenderState) -> UiSceneStepResult { - let frame_rect = Some(self.frame.get_rect(input)); - self.button.step(input, state, frame_rect); - self.landscape.step(input, state, frame_rect); - self.frame.step(input, state, None); + 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, frame_rect) { + if self.button.contains_mouse(input, state) { new_scene = Some(UiScenes::Outfitter(UiOutfitterScene::new(input.ct, state))); } } else if !input.player.input.pressed_leftclick() { @@ -147,19 +118,18 @@ impl<'this> UiScene<'this> for UiLandedScene { } // Draw elements - let frame_rect = Some(self.frame.get_rect(input)); - self.button.push_to_buffer(input, state, frame_rect); - self.landscape.push_to_buffer(input, state, frame_rect); - self.frame.push_to_buffer(input, state, None); + 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>, - input: &RenderInput, + _input: &RenderInput, state: &RenderState, ) { - v.push(self.description.get_textarea(input, state)); - v.push(self.title.get_textarea(input, state)); + v.push(self.description.get_textarea(state)); + v.push(self.title.get_textarea(state)); } } diff --git a/crates/render/src/ui/scenes/outfitter.rs b/crates/render/src/ui/scenes/outfitter.rs index ffc3898..cce9aa0 100644 --- a/crates/render/src/ui/scenes/outfitter.rs +++ b/crates/render/src/ui/scenes/outfitter.rs @@ -1,4 +1,4 @@ -use galactica_content::{Content, SystemObject, SystemObjectHandle}; +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}; @@ -15,9 +15,9 @@ use super::UiLandedScene; pub struct UiOutfitterScene { // UI elements - description: UiTextArea, - landscape: UiSprite, - button: UiSprite, + se_box: UiSprite, + exit_button: UiSprite, + exit_text: UiTextArea, /// What object we're displaying currently. /// Whenever this changes, we need to reflow text. @@ -30,55 +30,39 @@ pub struct UiOutfitterScene { impl UiOutfitterScene { pub fn new(ct: &Content, state: &mut RenderState) -> Self { - let button = UiSprite::new( - ct, - ct.get_sprite_handle("ui::button"), - None, - SpriteRect { - pos: Point2::new(0.0, 0.0), - dim: Vector2::new(200.0, 200.0), - }, - None, - None, - ); - - let landscape = UiSprite::from_with_sprite( - ct, - &ct.get_ui().landed_landscape, - ct.get_sprite_handle("ui::landscape::test"), - ); + 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, - description: UiTextArea::new( + exit_text: UiTextArea::new( ct, state, - button.get_sprite(), - Point2::new(0.0, 0.0), - 800.0, SpriteRect { - pos: Point2::new(25.831, 284.883) / 512.0, - dim: Vector2::new(433.140, 97.220) / 512.0, + 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::Left, + Align::Center, ), - landscape, - button, + se_box, + exit_button, }; return s; } fn reflow(&mut self, planet: &SystemObject, state: &mut RenderState) { - self.description.set_text( + self.exit_text.set_text( state, - &planet.desc, + "Exit", Attrs::new() .weight(Weight::NORMAL) .family(glyphon::Family::SansSerif), @@ -90,13 +74,13 @@ impl UiOutfitterScene { impl<'this> UiScene<'this> for UiOutfitterScene { fn step(&mut self, input: &RenderInput, state: &mut RenderState) -> UiSceneStepResult { - self.button.step(input, state, None); - self.landscape.step(input, state, None); + 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.button.contains_mouse(input, state, None) { + 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() { @@ -130,16 +114,16 @@ impl<'this> UiScene<'this> for UiOutfitterScene { } // Draw elements - self.button.push_to_buffer(input, state, None); - self.landscape.push_to_buffer(input, state, None); + self.se_box.push_to_buffer(input, state); + self.exit_button.push_to_buffer(input, state); } fn get_textareas( &'this self, v: &mut Vec>, - input: &RenderInput, + _input: &RenderInput, state: &RenderState, ) { - v.push(self.description.get_textarea(input, state)); + v.push(self.exit_text.get_textarea(state)); } } diff --git a/crates/render/src/ui/util/mod.rs b/crates/render/src/ui/util/mod.rs index 1d5bcf4..49f78ac 100644 --- a/crates/render/src/ui/util/mod.rs +++ b/crates/render/src/ui/util/mod.rs @@ -1,14 +1,16 @@ mod sprite; mod textarea; +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 inside a sprite. +/// 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. @@ -18,17 +20,143 @@ pub(crate) struct SpriteRect { /// 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, + + /// 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 { - /// Northeast corner of this rect - pub fn ne_corner(&self) -> Point2 { - self.pos + Vector2::new(-self.dim.x, self.dim.y) / 2.0 - } + /* + 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); - /// Southwest corner of this rect - pub fn sw_corner(&self) -> Point2 { - self.pos + Vector2::new(self.dim.x, -self.dim.y) / 2.0 + 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 = 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, + + /// 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, +} + +impl CenteredSpriteRect { + pub fn contains_point(&self, pt: Point2) -> 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); } } diff --git a/crates/render/src/ui/util/sprite.rs b/crates/render/src/ui/util/sprite.rs index 088b771..8cad915 100644 --- a/crates/render/src/ui/util/sprite.rs +++ b/crates/render/src/ui/util/sprite.rs @@ -2,8 +2,8 @@ use galactica_content::{Content, SectionEdge, SpriteAutomaton, SpriteHandle, UiS use galactica_util::to_radians; use nalgebra::{Point2, Vector2}; -use super::SpriteRect; -use crate::{vertexbuffer::types::UiInstance, PositionAnchor, RenderInput, RenderState}; +use super::{CenteredSpriteRect, SpriteRect}; +use crate::{vertexbuffer::types::UiInstance, RenderInput, RenderState}; pub struct UiSprite { pub anim: SpriteAutomaton, @@ -49,22 +49,21 @@ impl UiSprite { sprite, ui.mask, SpriteRect { - pos: *ui.rect.get_pos(), - dim: *ui.rect.get_dim(), + 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, parent: Option) { - if self.contains_mouse(input, state, parent) - && !self.has_mouse - && self.on_mouse_enter.is_some() - { + 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, parent) + } else if !self.contains_mouse(input, state) && self.has_mouse && self.on_mouse_leave.is_some() { @@ -75,13 +74,8 @@ impl UiSprite { self.anim.step(input.ct, input.time_since_last_run); } - pub fn contains_mouse( - &self, - input: &RenderInput, - state: &RenderState, - parent: Option, - ) -> bool { - let rect = self.get_relative_rect(input, parent); + 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( @@ -90,69 +84,40 @@ impl UiSprite { ); let pos = input.player.input.get_mouse_pos(); - let mouse_pos = Vector2::new( + let mouse_pos = Point2::new( pos.x / fac - window_size.x / 2.0, window_size.y / 2.0 - pos.y / fac, ); - let ne = rect.ne_corner(); - let sw = rect.sw_corner(); - return (mouse_pos.y < ne.y && mouse_pos.y > sw.y) - && (mouse_pos.x > ne.x && mouse_pos.x < sw.x); + return rect.contains_point(mouse_pos); } - pub fn get_rect(&self, input: &RenderInput) -> SpriteRect { - let pos = Point2::new(self.rect.pos.x, self.rect.pos.y); - let dim = Vector2::new( - self.rect.dim.y * input.ct.get_sprite(self.anim.get_sprite()).aspect, - self.rect.dim.y, - ); - - return SpriteRect { dim, pos }; - } - - pub fn get_relative_rect(&self, input: &RenderInput, parent: Option) -> SpriteRect { + pub fn get_rect( + &self, + state: &RenderState, + //parent: Option, + ) -> CenteredSpriteRect { + /* if let Some(parent) = parent { - let zero = Point2::new( - parent.pos.x - (parent.dim.x / 2.0), - parent.pos.y + (parent.dim.y / 2.0), - ); - - let mut pos = zero - + Vector2::new( - self.rect.pos.x * parent.dim.x, - -self.rect.pos.y * parent.dim.y, - ); - let dim = Vector2::new( - self.rect.dim.x * parent.dim.x, - self.rect.dim.y * parent.dim.y, - ); - - pos += Vector2::new(dim.x, -dim.y) / 2.0; - - return SpriteRect { dim, pos }; + return self.rect.to_centered_relative(parent); } else { - return self.get_rect(input); - } + 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, - parent: Option, - ) { - let rect = self.get_relative_rect(input, parent.clone()); + pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) { + let rect = self.get_rect(state); // TODO: use both dimensions, // not just height let anim_state = self.anim.get_texture_idx(); state.push_ui_buffer(UiInstance { - anchor: PositionAnchor::CC.to_int(), + anchor: crate::PositionAnchor::CC.to_int(), position: rect.pos.into(), angle: to_radians(90.0), - size: rect.dim.y, + dim: rect.dim.into(), color: [1.0, 1.0, 1.0, 1.0], texture_index: anim_state.texture_index(), texture_fade: anim_state.fade, @@ -166,12 +131,4 @@ impl UiSprite { .unwrap_or([0, 0]), }); } - - pub fn get_sprite(&self) -> SpriteHandle { - self.anim.get_sprite() - } - - pub fn get_height(&self) -> f32 { - self.rect.dim.y - } } diff --git a/crates/render/src/ui/util/textarea.rs b/crates/render/src/ui/util/textarea.rs index c55b97d..9a4b385 100644 --- a/crates/render/src/ui/util/textarea.rs +++ b/crates/render/src/ui/util/textarea.rs @@ -1,22 +1,12 @@ -use galactica_content::{Content, SpriteHandle}; +use galactica_content::{Content, UiTextAlign, UiTextConfig}; use glyphon::{cosmic_text::Align, Attrs, Buffer, Color, Metrics, Shaping, TextArea, TextBounds}; -use nalgebra::{Point2, Vector2}; +use nalgebra::Vector2; use super::SpriteRect; -use crate::{RenderInput, RenderState}; +use crate::RenderState; /// Represents a text area inside a sprite. pub(crate) struct UiTextArea { - /// Parent sprite - sprite: SpriteHandle, - - /// Position of parent sprite's center, in logical pixels, - /// with 0, 0 at the center of the screen - sprite_position: Point2, - - /// Height of parent sprite, in logical pixels - sprite_size: f32, - /// Bounds of text area rect: SpriteRect, @@ -32,11 +22,8 @@ pub(crate) struct UiTextArea { impl UiTextArea { pub fn new( - ct: &Content, + _ct: &Content, state: &mut RenderState, - sprite: SpriteHandle, - sprite_position: Point2, - sprite_size: f32, rect: SpriteRect, text_metrics: Metrics, color: Color, @@ -44,25 +31,37 @@ impl UiTextArea { ) -> Self { let mut s = Self { buffer: Buffer::new(&mut state.text_font_system, text_metrics), - sprite_size: f32::NAN, - sprite, - sprite_position, rect, align, color, }; - s.set_size(ct, state, sprite_size); + + 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 set_size(&mut self, ct: &Content, state: &mut RenderState, sprite_size: f32) { - self.sprite_size = sprite_size; - self.buffer.set_size( - &mut state.text_font_system, - (self.rect.dim.x * self.sprite_size) * state.window.scale_factor() as f32, - (self.rect.dim.y * self.sprite_size * ct.get_sprite(self.sprite).aspect) - * state.window.scale_factor() as f32, - ); + 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) { @@ -75,31 +74,27 @@ impl UiTextArea { self.buffer.shape_until_scroll(&mut state.text_font_system); } - pub fn get_textarea(&self, input: &RenderInput, state: &RenderState) -> TextArea { - let h = self.sprite_size; - let w = input.ct.get_sprite(self.sprite).aspect * h; + pub fn get_textarea(&self, state: &RenderState) -> TextArea { + let rect = self.rect.to_centered(state); - // Glypon works with physical pixels, so we must convert + // Glypon works with physical pixels, so we must do some conversion let fac = state.window.scale_factor() as f32; - - // All the units below are in logical pixels - let zero = Vector2::new( - (state.window_size.width as f32 / (2.0 * fac)) - (w / 2.0) + self.sprite_position.x, - (state.window_size.height as f32 / (2.0 * fac)) - (h / 2.0) - self.sprite_position.y, + 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_ne = zero + Vector2::new(self.rect.pos.x * w, self.rect.pos.y * h); - let corner_sw = corner_ne + Vector2::new(self.rect.dim.x * w, self.rect.dim.y * h); + let corner_sw = corner_ne + rect.dim * fac; TextArea { buffer: &self.buffer, - top: corner_ne.y * fac, - left: corner_ne.x * fac, + top: corner_ne.y, + left: corner_ne.x, scale: 1.0, bounds: TextBounds { - top: (corner_ne.y * fac) as i32, - bottom: (corner_sw.y * fac) as i32, - left: (corner_ne.x * fac) as i32, - right: (corner_sw.x * fac) as i32, + 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, } diff --git a/crates/render/src/vertexbuffer/types.rs b/crates/render/src/vertexbuffer/types.rs index a4f10d5..afc20d0 100644 --- a/crates/render/src/vertexbuffer/types.rs +++ b/crates/render/src/vertexbuffer/types.rs @@ -153,8 +153,9 @@ pub struct UiInstance { /// The angle of this sprite, in radians pub angle: f32, - /// The height of this sprite, in logical pixels - pub size: f32, + /// The width and height of this sprite, in logical pixels. + /// Aspect ratio is not automatically preserved. + pub dim: [f32; 2], /// This lets us color ui sprites dynamically. /// Each fragment's color is multiplied by this value. @@ -200,33 +201,33 @@ impl BufferObject for UiInstance { shader_location: 4, format: wgpu::VertexFormat::Float32, }, - // Size + // Dim wgpu::VertexAttribute { offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, shader_location: 5, - format: wgpu::VertexFormat::Float32, + format: wgpu::VertexFormat::Float32x2, }, // Color wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress, + offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress, shader_location: 6, format: wgpu::VertexFormat::Float32x4, }, // Texture wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 9]>() as wgpu::BufferAddress, + offset: mem::size_of::<[f32; 10]>() as wgpu::BufferAddress, shader_location: 7, format: wgpu::VertexFormat::Uint32x2, }, // Texture fade wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress, + offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress, shader_location: 8, format: wgpu::VertexFormat::Float32, }, // Mask wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress, + offset: mem::size_of::<[f32; 13]>() as wgpu::BufferAddress, shader_location: 9, format: wgpu::VertexFormat::Uint32x2, },