2024-01-30 17:06:14 -08:00

343 lines
7.7 KiB
Rust

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<super::UiTextConfig> {
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<String>,
pub rect: UiRect,
pub mask: Option<String>,
pub on_mouse_enter: Option<EdgeSpec>,
pub on_mouse_leave: Option<EdgeSpec>,
}
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<super::UiSpriteConfig> {
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<f32>,
/// This sprite's w and h, in logical pixels.
pub dim: Vector2<f32>,
/// 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<SpriteHandle>,
/// The mask to use
pub mask: Option<SpriteHandle>,
/// This sprite's position and size
pub rect: UiRect,
/// Animation edge to take when mouse enters this sprite
pub on_mouse_enter: Option<SectionEdge>,
/// Animation edge to take when mouse leaves this sprite
pub on_mouse_leave: Option<SectionEdge>,
}
/// 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(());
}
}