use super::vertex2d::Vertex2D;
use crate::impl_shared_attributes;
use crate::vpx::gameitem::font::FontJson;
use crate::vpx::gameitem::select::{TimerData, WriteSharedAttributes};
use crate::vpx::{
biff::{self, BiffRead, BiffReader, BiffWrite},
color::Color,
gameitem::font::Font,
};
use log::warn;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, PartialEq, Clone, Default)]
#[cfg_attr(test, derive(fake::Dummy))]
pub enum TextAlignment {
#[default]
Left = 0,
Center = 1,
Right = 2,
}
impl From<u32> for TextAlignment {
fn from(value: u32) -> Self {
match value {
0 => TextAlignment::Left,
1 => TextAlignment::Center,
2 => TextAlignment::Right,
_ => panic!("Invalid value for TextAlignment: {value}"),
}
}
}
impl From<&TextAlignment> for u32 {
fn from(value: &TextAlignment) -> Self {
match value {
TextAlignment::Left => 0,
TextAlignment::Center => 1,
TextAlignment::Right => 2,
}
}
}
impl Serialize for TextAlignment {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
TextAlignment::Left => serializer.serialize_str("left"),
TextAlignment::Center => serializer.serialize_str("center"),
TextAlignment::Right => serializer.serialize_str("right"),
}
}
}
impl<'de> Deserialize<'de> for TextAlignment {
fn deserialize<D>(deserializer: D) -> Result<TextAlignment, D::Error>
where
D: serde::Deserializer<'de>,
{
struct TextAlignmentVisitor;
impl serde::de::Visitor<'_> for TextAlignmentVisitor {
type Value = TextAlignment;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or number representing a TargetType")
}
fn visit_u64<E>(self, value: u64) -> Result<TextAlignment, E>
where
E: serde::de::Error,
{
match value {
0 => Ok(TextAlignment::Left),
1 => Ok(TextAlignment::Center),
2 => Ok(TextAlignment::Right),
_ => Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Unsigned(value),
&"0, 1, or 2",
)),
}
}
fn visit_str<E>(self, value: &str) -> Result<TextAlignment, E>
where
E: serde::de::Error,
{
match value {
"left" => Ok(TextAlignment::Left),
"center" => Ok(TextAlignment::Center),
"right" => Ok(TextAlignment::Right),
_ => Err(serde::de::Error::unknown_variant(
value,
&["left", "center", "right"],
)),
}
}
}
deserializer.deserialize_any(TextAlignmentVisitor)
}
}
#[derive(Debug, PartialEq)]
#[cfg_attr(test, derive(fake::Dummy))]
pub struct TextBox {
pub ver1: Vertex2D, pub ver2: Vertex2D, pub back_color: Color, pub font_color: Color, pub intensity_scale: f32, pub text: String, pub name: String, pub align: TextAlignment, pub is_transparent: bool, pub is_dmd: Option<bool>, pub font: Font,
pub timer: TimerData,
pub is_locked: bool,
pub editor_layer: Option<u32>,
pub editor_layer_name: Option<String>,
pub editor_layer_visibility: Option<bool>, pub part_group_name: Option<String>,
}
impl_shared_attributes!(TextBox);
#[derive(Serialize, Deserialize)]
struct TextBoxJson {
ver1: Vertex2D,
ver2: Vertex2D,
back_color: Color,
font_color: Color,
intensity_scale: f32,
text: String,
#[serde(flatten)]
pub timer: TimerData,
name: String,
align: TextAlignment,
is_transparent: bool,
is_dmd: Option<bool>,
font: FontJson,
#[serde(skip_serializing_if = "Option::is_none")]
part_group_name: Option<String>,
}
impl TextBoxJson {
fn from_textbox(textbox: &TextBox) -> Self {
Self {
ver1: textbox.ver1,
ver2: textbox.ver2,
back_color: textbox.back_color,
font_color: textbox.font_color,
intensity_scale: textbox.intensity_scale,
text: textbox.text.clone(),
timer: textbox.timer.clone(),
name: textbox.name.clone(),
align: textbox.align.clone(),
is_transparent: textbox.is_transparent,
is_dmd: textbox.is_dmd,
font: FontJson::from_font(&textbox.font),
part_group_name: textbox.part_group_name.clone(),
}
}
fn into_textbox(self) -> TextBox {
TextBox {
ver1: self.ver1,
ver2: self.ver2,
back_color: self.back_color,
font_color: self.font_color,
intensity_scale: self.intensity_scale,
text: self.text,
timer: self.timer.clone(),
name: self.name,
align: self.align,
is_transparent: self.is_transparent,
is_dmd: self.is_dmd,
font: self.font.to_font(),
is_locked: false,
editor_layer: None,
editor_layer_name: None,
editor_layer_visibility: None,
part_group_name: self.part_group_name,
}
}
}
impl Serialize for TextBox {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
TextBoxJson::from_textbox(self).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for TextBox {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let textbox_json = TextBoxJson::deserialize(deserializer)?;
Ok(textbox_json.into_textbox())
}
}
impl Default for TextBox {
fn default() -> Self {
Self {
ver1: Vertex2D::default(),
ver2: Vertex2D::default(),
back_color: Color::BLACK,
font_color: Color::WHITE,
intensity_scale: 1.0,
text: Default::default(),
timer: TimerData::default(),
name: Default::default(),
align: Default::default(),
is_transparent: false,
is_dmd: None,
font: Font::default(),
is_locked: false,
editor_layer: Default::default(),
editor_layer_name: None,
editor_layer_visibility: None,
part_group_name: None,
}
}
}
impl BiffRead for TextBox {
fn biff_read(reader: &mut BiffReader<'_>) -> Self {
let mut textbox = TextBox::default();
loop {
reader.next(biff::WARN);
if reader.is_eof() {
break;
}
let tag = reader.tag();
let tag_str = tag.as_str();
match tag_str {
"VER1" => {
textbox.ver1 = Vertex2D::biff_read(reader);
}
"VER2" => {
textbox.ver2 = Vertex2D::biff_read(reader);
}
"CLRB" => {
textbox.back_color = Color::biff_read(reader);
}
"CLRF" => {
textbox.font_color = Color::biff_read(reader);
}
"INSC" => {
textbox.intensity_scale = reader.get_f32();
}
"TEXT" => {
textbox.text = reader.get_string();
}
"NAME" => {
textbox.name = reader.get_wide_string();
}
"ALGN" => {
textbox.align = reader.get_u32().into();
}
"TRNS" => {
textbox.is_transparent = reader.get_bool();
}
"IDMD" => {
textbox.is_dmd = Some(reader.get_bool());
}
"FONT" => {
textbox.font = Font::biff_read(reader);
}
_ => {
if !textbox.timer.biff_read_tag(tag_str, reader)
&& !textbox.read_shared_attribute(tag_str, reader)
{
warn!(
"Unknown tag {} for {}",
tag_str,
std::any::type_name::<Self>()
);
reader.skip_tag();
}
}
}
}
textbox
}
}
impl BiffWrite for TextBox {
fn biff_write(&self, writer: &mut biff::BiffWriter) {
writer.write_tagged("VER1", &self.ver1);
writer.write_tagged("VER2", &self.ver2);
writer.write_tagged_with("CLRB", &self.back_color, Color::biff_write);
writer.write_tagged_with("CLRF", &self.font_color, Color::biff_write);
writer.write_tagged_f32("INSC", self.intensity_scale);
writer.write_tagged_string("TEXT", &self.text);
self.timer.biff_write(writer);
writer.write_tagged_wide_string("NAME", &self.name);
writer.write_tagged_u32("ALGN", (&self.align).into());
writer.write_tagged_bool("TRNS", self.is_transparent);
if let Some(is_dmd) = self.is_dmd {
writer.write_tagged_bool("IDMD", is_dmd);
}
self.write_shared_attributes(writer);
writer.write_tagged_without_size("FONT", &self.font);
writer.close(true);
}
}
#[cfg(test)]
mod tests {
use crate::vpx::biff::BiffWriter;
use fake::{Fake, Faker};
use std::collections::HashSet;
use super::*;
use crate::vpx::gameitem::font::{CHARSET_ANSI, FontStyle};
use pretty_assertions::assert_eq;
#[test]
fn test_write_read() {
let textbox = TextBox {
ver1: Vertex2D::new(1.0, 2.0),
ver2: Vertex2D::new(3.0, 4.0),
back_color: Faker.fake(),
font_color: Faker.fake(),
intensity_scale: 1.0,
text: "test text".to_string(),
timer: TimerData {
is_enabled: true,
interval: 3,
},
name: "test timer".to_string(),
align: Faker.fake(),
is_transparent: false,
is_dmd: Some(false),
font: Font::new(
CHARSET_ANSI,
HashSet::from([FontStyle::Bold, FontStyle::Underline]),
123,
456,
"test font".to_string(),
),
is_locked: false,
editor_layer: Some(1),
editor_layer_name: Some("test layer".to_string()),
editor_layer_visibility: Some(true),
part_group_name: Some("test group".to_string()),
};
let mut writer = BiffWriter::new();
TextBox::biff_write(&textbox, &mut writer);
let textbox_read = TextBox::biff_read(&mut BiffReader::new(writer.get_data()));
assert_eq!(textbox, textbox_read);
}
#[test]
fn test_text_alignment_json() {
let sizing_type = TextAlignment::Center;
let json = serde_json::to_string(&sizing_type).unwrap();
assert_eq!(json, "\"center\"");
let sizing_type_read: TextAlignment = serde_json::from_str(&json).unwrap();
assert_eq!(sizing_type, sizing_type_read);
let json = serde_json::Value::from(2);
let sizing_type_read: TextAlignment = serde_json::from_value(json).unwrap();
assert_eq!(TextAlignment::Right, sizing_type_read);
}
#[test]
#[should_panic = "Error(\"unknown variant `foo`, expected one of `left`, `center`, `right`\", line: 0, column: 0)"]
fn test_text_alignment_json_fail_string() {
let json = serde_json::Value::from("foo");
let _: TextAlignment = serde_json::from_value(json).unwrap();
}
}