diff --git a/content/ui.toml b/content/ui.toml new file mode 100644 index 0000000..2f448fe --- /dev/null +++ b/content/ui.toml @@ -0,0 +1,18 @@ +[ui.landed] +frame.sprite = "ui::planet" +frame.pos = [0.0, 0.0] +frame.dim = [800.0, 800.0] + +landscape.mask = "ui::landscapemask" +landscape.pos = [32.0, 75.0] +landscape.dim = [344.0, 173.0] +landscape.loc_div = 512 + +button.sprite = "ui::planet::button" +button.pos = [356, 90] +button.dim = [113.569, 20] +button.loc_div = 512 +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 diff --git a/crates/content/src/lib.rs b/crates/content/src/lib.rs index 7e29ea6..e639d24 100644 --- a/crates/content/src/lib.rs +++ b/crates/content/src/lib.rs @@ -33,6 +33,7 @@ mod syntax { use crate::{ config, part::{effect, faction, outfit, ship, sprite, system}, + ui, }; #[derive(Debug, Deserialize)] @@ -44,6 +45,7 @@ mod syntax { pub faction: Option>, pub effect: Option>, pub config: Option, + pub ui: Option, } fn merge_hashmap( @@ -81,6 +83,7 @@ mod syntax { faction: None, effect: None, config: None, + ui: None, } } @@ -96,6 +99,7 @@ mod syntax { .with_context(|| "while merging factions")?; merge_hashmap(&mut self.effect, other.effect) .with_context(|| "while merging effects")?; + if self.config.is_some() { if other.config.is_some() { bail!("invalid content dir, multiple config tables") @@ -103,6 +107,15 @@ mod syntax { } else { self.config = other.config; } + + if self.ui.is_some() { + if other.ui.is_some() { + bail!("invalid content dir, multiple ui tables") + } + } else { + self.ui = other.ui; + } + return Ok(()); } } @@ -159,6 +172,7 @@ pub struct Content { factions: Vec, effects: Vec, config: Config, + ui: Option, } // Loading methods @@ -227,6 +241,7 @@ impl Content { factions: Vec::new(), effects: Vec::new(), sprite_index: HashMap::new(), + ui: None, }; // TODO: enforce sprite and image limits @@ -239,6 +254,7 @@ impl Content { &mut content, )?; } + if root.effect.is_some() { part::effect::Effect::build( root.effect.take().unwrap(), @@ -247,6 +263,10 @@ impl Content { )?; } + if root.ui.is_some() { + part::ui::Ui::build(root.ui.take().unwrap(), &mut build_context, &mut content)?; + } + // Order below this line does not matter if root.ship.is_some() { part::ship::Ship::build(root.ship.take().unwrap(), &mut build_context, &mut content)?; @@ -353,4 +373,9 @@ impl Content { pub fn get_config(&self) -> &Config { return &self.config; } + + /// Get ui configuration + pub fn get_ui(&self) -> &Ui { + return self.ui.as_ref().unwrap(); + } } diff --git a/crates/content/src/part/mod.rs b/crates/content/src/part/mod.rs index b7f27e9..ec5a8d8 100644 --- a/crates/content/src/part/mod.rs +++ b/crates/content/src/part/mod.rs @@ -8,6 +8,7 @@ pub(crate) mod outfitspace; pub(crate) mod ship; pub(crate) mod sprite; pub(crate) mod system; +pub(crate) mod ui; pub use config::Config; pub use effect::*; @@ -20,3 +21,4 @@ pub use ship::{ }; pub use sprite::*; pub use system::{System, SystemObject}; +pub use ui::*; diff --git a/crates/content/src/part/ui.rs b/crates/content/src/part/ui.rs new file mode 100644 index 0000000..ec970e0 --- /dev/null +++ b/crates/content/src/part/ui.rs @@ -0,0 +1,250 @@ +use anyhow::{Context, Result}; +use nalgebra::{Point2, Vector2}; + +use crate::{handle::SpriteHandle, Content, ContentBuildContext, SectionEdge}; + +pub(crate) mod syntax { + use crate::{sprite::syntax::SectionEdge, Content, ContentBuildContext}; + use anyhow::{bail, Context, Result}; + use nalgebra::{Point2, Vector2}; + use serde::Deserialize; + // Raw serde syntax structs. + // These are never seen by code outside this crate. + + #[derive(Debug, Deserialize)] + pub struct Ui { + pub landed: UiLanded, + } + + #[derive(Debug, Deserialize)] + pub struct UiLanded { + pub frame: UiSprite, + pub landscape: UiSprite, + pub button: UiSprite, + } + + #[derive(Debug, Deserialize)] + pub struct UiSprite { + pub sprite: Option, + 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, + } + + #[derive(Debug, Deserialize)] + pub struct EdgeSpec { + pub edge: SectionEdge, + pub duration: f32, + } + + 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 + should_have_sprite: bool, + ) -> Result { + let sprite = { + if should_have_sprite { + if self.sprite.is_none() { + bail!("no sprite given, but expected a value") + } + match ct.sprite_index.get(self.sprite.as_ref().unwrap()) { + None => bail!("ui sprite `{}` doesn't exist", self.sprite.unwrap()), + Some(t) => Some(*t), + } + } else { + if self.sprite.is_some() { + bail!("got a sprite, but didn't expect one") + } + None + } + }; + + let mask = if let Some(mask) = self.mask { + Some(match ct.sprite_index.get(&mask) { + None => bail!("mask `{}` doesn't exist", mask), + Some(t) => *t, + }) + } else { + None + }; + + let on_mouse_enter = { + if let Some(x) = self.on_mouse_enter { + if sprite.is_none() { + bail!("got `on_mouse_enter` on a ui element with no fixed sprite") + } + + Some( + x.edge + .resolve_as_edge(sprite.unwrap(), build_context, x.duration) + .with_context(|| format!("failed to resolve mouse enter edge"))?, + ) + } else { + None + } + }; + + let on_mouse_leave = { + if let Some(x) = self.on_mouse_leave { + if sprite.is_none() { + bail!("got `on_mouse_leave` on a ui element with no fixed sprite") + } + + Some( + x.edge + .resolve_as_edge(sprite.unwrap(), build_context, x.duration) + .with_context(|| format!("failed to resolve mouse leave edge"))?, + ) + } else { + None + } + }; + + 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, + } + } + }; + + return Ok(super::UiSpriteConfig { + sprite, + mask, + on_mouse_enter, + on_mouse_leave, + rect, + }); + } + } +} + +/// UI Configuration +#[derive(Debug, Clone)] +pub struct Ui { + /// Landed interface frame + pub landed_frame: UiSpriteConfig, + + /// Landed interface image + pub landed_landscape: UiSpriteConfig, + + /// Test button + pub landed_button: 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. + + // 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, as a fraction of the parent's width and height. + dim: Vector2, + }, + + /// 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, + } + } +} + +/// A single UI sprite instance +#[derive(Debug, Clone)] +pub struct UiSpriteConfig { + /// The sprite to show + pub sprite: Option, + + /// The mask to use + pub mask: Option, + + /// This sprite's position and size + pub rect: UiSpriteRect, + + /// Animation edge to take when mouse enters this sprite + pub on_mouse_enter: Option, + + /// Animation edge to take when mouse leaves this sprite + pub on_mouse_leave: Option, +} + +impl crate::Build for Ui { + type InputSyntaxType = syntax::Ui; + + fn build( + ui: Self::InputSyntaxType, + build_context: &mut ContentBuildContext, + ct: &mut Content, + ) -> Result<()> { + ct.ui = Some(Ui { + landed_frame: ui + .landed + .frame + .build(build_context, ct, false, true) + .with_context(|| format!("in ui config (frame)"))?, + landed_landscape: ui + .landed + .landscape + .build(build_context, ct, true, false) + .with_context(|| format!("in ui config (image)"))?, + landed_button: ui + .landed + .button + .build(build_context, ct, true, true) + .with_context(|| format!("in ui config (button)"))?, + }); + + return Ok(()); + } +} diff --git a/crates/render/src/ui/planet.rs b/crates/render/src/ui/planet.rs index 8f9916f..0423d13 100644 --- a/crates/render/src/ui/planet.rs +++ b/crates/render/src/ui/planet.rs @@ -10,7 +10,9 @@ pub(super) struct Planet { // UI elements planet_desc: UiTextArea, planet_name: UiTextArea, - sprite: UiSprite, + frame: UiSprite, + landscape: UiSprite, + button: UiSprite, /// What object we're displaying currently. /// Whenever this changes, we need to reflow text. @@ -21,32 +23,13 @@ pub(super) struct Planet { impl Planet { pub fn new(ct: &Content, state: &mut RenderState) -> Self { - let mut sprite = UiSprite::new( - ct.get_sprite_handle("ui::planet"), - None, - SpriteRect { - pos: Point2::new(0.0, 0.0), - dim: Vector2::new(800.0, 800.0), - }, - ); - - sprite.add_child_under(Box::new(UiSprite::new( + 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"), - Some(ct.get_sprite_handle("ui::landscapemask")), - SpriteRect { - pos: Point2::new(32.0, 75.0) / 512.0, - dim: Vector2::new(344.0, 173.0) / 512.0, - }, - ))); - - sprite.add_child_under(Box::new(UiSprite::new( - ct.get_sprite_handle("ui::planet::button"), - None, - SpriteRect { - pos: Point2::new(375.0, 90.0) / 512.0, - dim: Vector2::new(113.569, 20.0) / 512.0, - }, - ))); + ); let s = Self { // height of element in logical pixels @@ -55,9 +38,9 @@ impl Planet { planet_desc: UiTextArea::new( ct, state, - ct.get_sprite_handle("ui::planet"), + frame.get_sprite(), Point2::new(0.0, 0.0), - 800.0, + frame.get_height(), SpriteRect { pos: Point2::new(25.831, 284.883) / 512.0, dim: Vector2::new(433.140, 97.220) / 512.0, @@ -70,9 +53,9 @@ impl Planet { planet_name: UiTextArea::new( ct, state, - ct.get_sprite_handle("ui::planet"), + frame.get_sprite(), Point2::new(0.0, 0.0), - 800.0, + frame.get_height(), SpriteRect { pos: Point2::new(165.506, 82.0) / 512.0, dim: Vector2::new(74.883, 17.0) / 512.0, @@ -82,9 +65,9 @@ impl Planet { Align::Center, ), - // TODO: use both dimensions, - // not just height - sprite, + frame, + landscape, + button, }; return s; @@ -134,8 +117,16 @@ impl Planet { self.reflow(planet, state); } + self.button.step(input, state); + self.landscape.step(input, state); + self.frame.step(input, state); + // Draw elements - self.sprite.push_to_buffer(input, state); + self.button + .push_to_buffer(input, state, Some(self.frame.get_rect(input))); + self.landscape + .push_to_buffer(input, state, Some(self.frame.get_rect(input))); + self.frame.push_to_buffer(input, state, None); } pub fn get_textarea(&self, input: &RenderInput, state: &RenderState) -> [TextArea; 2] { diff --git a/crates/render/src/ui/util/sprite.rs b/crates/render/src/ui/util/sprite.rs index cc2784e..5aca730 100644 --- a/crates/render/src/ui/util/sprite.rs +++ b/crates/render/src/ui/util/sprite.rs @@ -1,119 +1,145 @@ -use galactica_content::SpriteHandle; +use galactica_content::{Content, SectionEdge, SpriteAutomaton, SpriteHandle, UiSpriteConfig}; use galactica_util::to_radians; use nalgebra::{Point2, Vector2}; -use super::{SpriteRect, UiElement}; +use super::SpriteRect; use crate::{vertexbuffer::types::UiInstance, PositionAnchor, RenderInput, RenderState}; pub struct UiSprite { - sprite: SpriteHandle, + pub anim: SpriteAutomaton, mask: Option, rect: SpriteRect, - children_under: Vec>, - children_above: Vec>, + has_mouse: bool, + + on_mouse_enter: Option, + on_mouse_leave: Option, } impl UiSprite { - pub fn new(sprite: SpriteHandle, mask: Option, rect: SpriteRect) -> Self { + pub fn from(ct: &Content, ui: &UiSpriteConfig) -> Self { + Self::from_with_sprite(ct, ui, ui.sprite.unwrap()) + } + + pub fn from_with_sprite(ct: &Content, ui: &UiSpriteConfig, sprite: SpriteHandle) -> Self { + if ui.sprite.is_none() { + unreachable!("called `UiSprite.from()` on a UiSprite with a None sprite!") + } + return Self { - sprite, - mask, - rect, - children_under: Vec::new(), - children_above: Vec::new(), + anim: SpriteAutomaton::new(ct, sprite), + mask: ui.mask, + rect: SpriteRect { + pos: *ui.rect.get_pos(), + dim: *ui.rect.get_dim(), + }, + has_mouse: false, + on_mouse_enter: ui.on_mouse_enter, + on_mouse_leave: ui.on_mouse_leave, }; } - /// Add a child under this sprite - pub fn add_child_under(&mut self, child: Box) { - self.children_under.push(child); + pub fn step(&mut self, input: &RenderInput, state: &RenderState) { + if self.contains_mouse(input, state, Some(self.get_rect(input))) + && !self.has_mouse + && self.on_mouse_enter.is_some() + { + self.has_mouse = true; + self.anim.jump_to(input.ct, self.on_mouse_enter.unwrap()) + } + + if !self.contains_mouse(input, state, Some(self.get_rect(input))) + && 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); } - /// Add a child above this sprite - //pub fn add_child_above(&mut self, child: Box) { - // self.children_above.push(child); - //} + pub fn contains_mouse( + &self, + input: &RenderInput, + state: &RenderState, + parent: Option, + ) -> bool { + let rect = self.get_relative_rect(input, parent); - /// Add this image to the gpu sprite buffer - pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) { + 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 = Vector2::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); + } + + 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.sprite).aspect, + self.rect.dim.y * input.ct.get_sprite(self.anim.get_sprite()).aspect, self.rect.dim.y, ); - for c in &self.children_under { - c.push_to_buffer_child(input, state, pos, dim); - } + return SpriteRect { dim, pos }; + } - let sprite = input.ct.get_sprite(self.sprite); - let texture_a = sprite.get_first_frame(); // ANIMATE + pub fn get_relative_rect(&self, input: &RenderInput, parent: Option) -> SpriteRect { + 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), + ); - state.push_ui_buffer(UiInstance { - anchor: PositionAnchor::CC.to_int(), - position: pos.into(), - angle: to_radians(90.0), - size: dim.y, - color: [1.0, 1.0, 1.0, 1.0], - texture_index: [texture_a, texture_a], - texture_fade: 1.0, - mask_index: self - .mask - .map(|x| { - let sprite = input.ct.get_sprite(x); - let texture_b = sprite.get_first_frame(); // ANIMATE - [1, texture_b] - }) - .unwrap_or([0, 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, + ); - for c in &self.children_above { - c.push_to_buffer_child(input, state, pos, dim); + pos += Vector2::new(dim.x, -dim.y) / 2.0; + + return SpriteRect { dim, pos }; + } else { + return self.get_rect(input); } } -} -impl UiElement for UiSprite { - /// Add this image to the gpu sprite buffer, - /// as a child of another sprite - fn push_to_buffer_child( + /// Add this image to the gpu sprite buffer + pub fn push_to_buffer( &self, input: &RenderInput, state: &mut RenderState, - parent_pos: Point2, - parent_size: Vector2, + parent: Option, ) { - let zero = Point2::new( - parent_pos.x - (parent_size.x / 2.0), - parent_pos.y + (parent_size.y / 2.0), - ); - - let pos = zero - + Vector2::new( - self.rect.pos.x * parent_size.x, - -self.rect.pos.y * parent_size.y, - ); - let dim = Vector2::new( - self.rect.dim.x * parent_size.x, - self.rect.dim.y * parent_size.y, - ); - - for c in &self.children_under { - c.push_to_buffer_child(input, state, pos, dim); - } - - let sprite = input.ct.get_sprite(self.sprite); - let texture_a = sprite.get_first_frame(); // ANIMATE + let rect = self.get_relative_rect(input, parent.clone()); + // TODO: use both dimensions, + // not just height + let anim_state = self.anim.get_texture_idx(); state.push_ui_buffer(UiInstance { - anchor: PositionAnchor::CNw.to_int(), - position: pos.into(), + anchor: PositionAnchor::CC.to_int(), + position: rect.pos.into(), angle: to_radians(90.0), - size: dim.y, + size: rect.dim.y, color: [1.0, 1.0, 1.0, 1.0], - texture_index: [texture_a, texture_a], - texture_fade: 1.0, + texture_index: anim_state.texture_index(), + texture_fade: anim_state.fade, mask_index: self .mask .map(|x| { @@ -123,9 +149,13 @@ impl UiElement for UiSprite { }) .unwrap_or([0, 0]), }); + } - for c in &self.children_above { - c.push_to_buffer_child(input, state, pos, dim); - } + pub fn get_sprite(&self) -> SpriteHandle { + self.anim.get_sprite() + } + + pub fn get_height(&self) -> f32 { + self.rect.dim.y } }