use anyhow::{Context, Result}; use nalgebra::{Point2, Vector2}; use serde::Deserialize; 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, pub outfitter: UiOutfitter, } #[derive(Debug, Deserialize)] pub struct UiLanded { pub frame: UiSprite, pub landscape: UiSprite, pub button: UiSprite, pub planet_name: UiText, pub planet_desc: UiText, } #[derive(Debug, Deserialize)] 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 anchor_self: super::UiPositionAnchor, pub anchor_parent: super::UiPositionAnchor, } #[derive(Debug, Deserialize)] pub struct EdgeSpec { pub edge: SectionEdge, 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, 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 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::UiSpriteConfig { sprite, mask, on_mouse_enter, on_mouse_leave, rect, }); } } } /// 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 { /// Landed interface frame pub landed_frame: UiSpriteConfig, /// Landed interface image pub landed_landscape: UiSpriteConfig, /// 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 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, /// This sprite's w and h, in logical pixels. pub dim: Vector2, /// The point on this sprite that pos is anchored to pub anchor_self: UiPositionAnchor, /// The point on the parent that pos is relative to pub anchor_parent: UiPositionAnchor, } /// 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: UiRect, /// 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, } /// 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; 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, true) .with_context(|| format!("in ui config (landed_frame)"))?, landed_landscape: ui .landed .landscape .build(build_context, ct, false) .with_context(|| format!("in ui config (landed_landscape)"))?, landed_button: ui .landed .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(()); } }