use serde::Deserialize;
use serde::de::Deserializer;
use super::animatable::Animatable;
use super::decode_warn;
use super::keywords::de_bg_image_mode;
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum ImageMode {
Keyword(String),
Spec(ImageModeSpec),
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ImageModeSpec {
Sliced(SliceSpec),
Tiled(TiledSpec),
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SliceSpec {
#[serde(default)]
pub border: SliceBorder,
#[serde(default)]
pub center_scale_mode: Option<SliceScale>,
#[serde(default)]
pub sides_scale_mode: Option<SliceScale>,
#[serde(default)]
pub max_corner_scale: Option<f32>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(untagged)]
pub enum SliceBorder {
#[default]
Zero,
Uniform(f32),
Sides {
#[serde(default)]
top: f32,
#[serde(default)]
right: f32,
#[serde(default)]
bottom: f32,
#[serde(default)]
left: f32,
},
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum SliceScale {
Keyword(String),
Tile { tile: f32 },
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TiledSpec {
#[serde(default)]
pub tile_x: bool,
#[serde(default)]
pub tile_y: bool,
#[serde(default)]
pub stretch_value: Option<f32>,
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SourceRect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AtlasSpec {
pub tile_width: u32,
pub tile_height: u32,
pub columns: u32,
pub rows: u32,
#[serde(default)]
pub padding: Option<[u32; 2]>,
#[serde(default)]
pub offset: Option<[u32; 2]>,
#[serde(default)]
pub index: usize,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum BackgroundImageSource {
Path(String),
Texture { texture: String },
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BackgroundImageSpec {
pub src: BackgroundImageSource,
#[serde(default)]
pub tint: Option<Animatable<String>>,
#[serde(default, deserialize_with = "de_bg_image_mode")]
pub mode: Option<BackgroundImageMode>,
#[serde(default)]
pub scale: Option<Animatable<f32>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BackgroundImageMode {
#[default]
Stretch,
Repeat,
RepeatX,
RepeatY,
}
impl BackgroundImageMode {
pub fn tiles(self) -> bool {
self != Self::Stretch
}
pub fn tile_axes(self) -> (bool, bool) {
match self {
Self::Stretch => (false, false),
Self::Repeat => (true, true),
Self::RepeatX => (true, false),
Self::RepeatY => (false, true),
}
}
}
pub(crate) fn de_background_image<'de, D: Deserializer<'de>>(
d: D,
) -> Result<Option<BackgroundImageSpec>, D::Error> {
let v = serde_json::Value::deserialize(d)?;
if v.is_null() {
return Ok(None);
}
match BackgroundImageSpec::deserialize(&v) {
Ok(spec) => {
if spec.scale.is_some() && !spec.mode.unwrap_or_default().tiles() {
decode_warn(
"backgroundImage",
&v.to_string(),
"backgroundImage `scale` only applies to the repeat modes; ignored under \"stretch\"",
);
}
Ok(Some(spec))
}
Err(err) => {
decode_warn(
"backgroundImage",
&v.to_string(),
&format!("invalid backgroundImage (object with `src` required): {err}"),
);
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::animatable::AnimatableField;
use crate::protocol::style::Style;
#[test]
fn background_image_decodes() {
let s: Style = serde_json::from_value(serde_json::json!({
"backgroundImage": {
"src": "images/bg.png",
"mode": "repeatX",
"scale": 2.0,
"tint": "#ff0000",
}
}))
.unwrap();
let spec = s.background_image.expect("spec decodes");
assert!(matches!(&spec.src, BackgroundImageSource::Path(p) if p == "images/bg.png"));
assert_eq!(spec.mode, Some(BackgroundImageMode::RepeatX));
assert_eq!(spec.scale.static_val(), Some(2.0));
assert_eq!(spec.tint.static_ref().map(String::as_str), Some("#ff0000"));
let s: Style = serde_json::from_value(serde_json::json!({
"backgroundImage": {
"src": "bg.png",
"tint": { "animated": { "id": 7 } },
}
}))
.unwrap();
let spec = s.background_image.expect("animated tint decodes");
assert!(spec.tint.static_ref().is_none());
assert!(spec.tint.binding().is_some());
let s: Style = serde_json::from_value(serde_json::json!({
"backgroundImage": { "src": { "texture": "minimap" } }
}))
.unwrap();
let spec = s.background_image.expect("texture source decodes");
assert!(
matches!(&spec.src, BackgroundImageSource::Texture { texture } if texture == "minimap")
);
assert_eq!(spec.mode, None);
let s: Style = serde_json::from_value(serde_json::json!({
"backgroundImage": { "src": "bg.png", "mode": "auto" }
}))
.unwrap();
assert_eq!(
s.background_image.unwrap().mode,
Some(BackgroundImageMode::Stretch)
);
for bad in [
serde_json::json!("bg.png"),
serde_json::json!({ "tint": "red" }),
] {
let s: Style = serde_json::from_value(serde_json::json!({
"backgroundImage": bad, "width": 10,
}))
.unwrap();
assert!(s.background_image.is_none());
assert!(s.width.is_some(), "sibling fields survive the bad value");
}
}
#[test]
fn background_image_mode_axes() {
use BackgroundImageMode::*;
assert_eq!(Stretch.tile_axes(), (false, false));
assert_eq!(Repeat.tile_axes(), (true, true));
assert_eq!(RepeatX.tile_axes(), (true, false));
assert_eq!(RepeatY.tile_axes(), (false, true));
assert!(!Stretch.tiles() && Repeat.tiles());
}
}