use hwpforge_foundation::{
ArcType, ArrowSize, ArrowType, BookmarkType, Color, CurveSegmentType, DropCapStyle, FieldType,
Flip, GradientType, HwpUnit, ImageFillMode, PatternType, RefContentType, RefType,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::caption::Caption;
use crate::chart::{
BarShape, ChartData, ChartGrouping, ChartType, LegendPosition, OfPieType, RadarStyle,
ScatterStyle, StockVariant,
};
use crate::error::{CoreError, CoreResult};
use crate::paragraph::Paragraph;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ShapePoint {
pub x: i32,
pub y: i32,
}
impl ShapePoint {
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub enum LineStyle {
#[default]
Solid,
Dash,
Dot,
DashDot,
DashDotDot,
None,
}
impl std::fmt::Display for LineStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Solid => f.write_str("SOLID"),
Self::Dash => f.write_str("DASH"),
Self::Dot => f.write_str("DOT"),
Self::DashDot => f.write_str("DASH_DOT"),
Self::DashDotDot => f.write_str("DASH_DOT_DOT"),
Self::None => f.write_str("NONE"),
}
}
}
impl std::str::FromStr for LineStyle {
type Err = CoreError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"SOLID" | "Solid" | "solid" => Ok(Self::Solid),
"DASH" | "Dash" | "dash" => Ok(Self::Dash),
"DOT" | "Dot" | "dot" => Ok(Self::Dot),
"DASH_DOT" | "DashDot" | "dash_dot" => Ok(Self::DashDot),
"DASH_DOT_DOT" | "DashDotDot" | "dash_dot_dot" => Ok(Self::DashDotDot),
"NONE" | "None" | "none" => Ok(Self::None),
_ => Err(CoreError::InvalidStructure {
context: "LineStyle".to_string(),
reason: format!(
"unknown line style '{s}', valid: SOLID, DASH, DOT, DASH_DOT, DASH_DOT_DOT, NONE"
),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ArrowStyle {
pub arrow_type: ArrowType,
pub size: ArrowSize,
pub filled: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub enum Fill {
Solid {
color: Color,
},
Gradient {
gradient_type: GradientType,
angle: i32,
colors: Vec<(Color, u32)>,
},
Pattern {
pattern_type: PatternType,
fg_color: Color,
bg_color: Color,
},
Image {
image_id: String,
mode: ImageFillMode,
},
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ShapeStyle {
pub line_color: Option<Color>,
pub fill_color: Option<Color>,
pub line_width: Option<u32>,
pub line_style: Option<LineStyle>,
pub rotation: Option<f32>,
pub flip: Option<Flip>,
pub head_arrow: Option<ArrowStyle>,
pub tail_arrow: Option<ArrowStyle>,
pub fill: Option<Fill>,
#[serde(default)]
pub drop_cap_style: DropCapStyle,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub enum Control {
TextBox {
paragraphs: Vec<Paragraph>,
width: HwpUnit,
height: HwpUnit,
horz_offset: i32,
vert_offset: i32,
caption: Option<Caption>,
style: Option<ShapeStyle>,
},
Hyperlink {
text: String,
url: String,
},
Footnote {
inst_id: Option<u32>,
paragraphs: Vec<Paragraph>,
},
Endnote {
inst_id: Option<u32>,
paragraphs: Vec<Paragraph>,
},
Line {
start: ShapePoint,
end: ShapePoint,
width: HwpUnit,
height: HwpUnit,
horz_offset: i32,
vert_offset: i32,
caption: Option<Caption>,
style: Option<ShapeStyle>,
},
Ellipse {
center: ShapePoint,
axis1: ShapePoint,
axis2: ShapePoint,
width: HwpUnit,
height: HwpUnit,
horz_offset: i32,
vert_offset: i32,
paragraphs: Vec<Paragraph>,
caption: Option<Caption>,
style: Option<ShapeStyle>,
},
Polygon {
vertices: Vec<ShapePoint>,
width: HwpUnit,
height: HwpUnit,
horz_offset: i32,
vert_offset: i32,
paragraphs: Vec<Paragraph>,
caption: Option<Caption>,
style: Option<ShapeStyle>,
},
Equation {
script: String,
width: HwpUnit,
height: HwpUnit,
base_line: u32,
text_color: Color,
font: String,
},
Chart {
chart_type: ChartType,
data: ChartData,
width: HwpUnit,
height: HwpUnit,
title: Option<String>,
legend: LegendPosition,
grouping: ChartGrouping,
bar_shape: Option<BarShape>,
explosion: Option<u32>,
of_pie_type: Option<OfPieType>,
radar_style: Option<RadarStyle>,
wireframe: Option<bool>,
bubble_3d: Option<bool>,
scatter_style: Option<ScatterStyle>,
show_markers: Option<bool>,
stock_variant: Option<StockVariant>,
},
Dutmal {
main_text: String,
sub_text: String,
position: DutmalPosition,
sz_ratio: u32,
align: DutmalAlign,
},
Compose {
compose_text: String,
circle_type: String,
char_sz: i32,
compose_type: String,
},
Arc {
arc_type: ArcType,
center: ShapePoint,
axis1: ShapePoint,
axis2: ShapePoint,
start1: ShapePoint,
end1: ShapePoint,
start2: ShapePoint,
end2: ShapePoint,
width: HwpUnit,
height: HwpUnit,
horz_offset: i32,
vert_offset: i32,
caption: Option<Caption>,
style: Option<ShapeStyle>,
},
Curve {
points: Vec<ShapePoint>,
segment_types: Vec<CurveSegmentType>,
width: HwpUnit,
height: HwpUnit,
horz_offset: i32,
vert_offset: i32,
caption: Option<Caption>,
style: Option<ShapeStyle>,
},
ConnectLine {
start: ShapePoint,
end: ShapePoint,
control_points: Vec<ShapePoint>,
connect_type: String,
width: HwpUnit,
height: HwpUnit,
horz_offset: i32,
vert_offset: i32,
caption: Option<Caption>,
style: Option<ShapeStyle>,
},
Bookmark {
name: String,
bookmark_type: BookmarkType,
},
CrossRef {
target_name: String,
ref_type: RefType,
content_type: RefContentType,
as_hyperlink: bool,
},
Field {
field_type: FieldType,
hint_text: Option<String>,
help_text: Option<String>,
},
Memo {
content: Vec<Paragraph>,
author: String,
date: String,
},
IndexMark {
primary: String,
secondary: Option<String>,
},
Unknown {
tag: String,
data: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub enum DutmalPosition {
#[default]
Top,
Bottom,
Right,
Left,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub enum DutmalAlign {
#[default]
Center,
Left,
Right,
}
impl Control {
pub fn is_text_box(&self) -> bool {
matches!(self, Self::TextBox { .. })
}
pub fn is_hyperlink(&self) -> bool {
matches!(self, Self::Hyperlink { .. })
}
pub fn is_footnote(&self) -> bool {
matches!(self, Self::Footnote { .. })
}
pub fn is_endnote(&self) -> bool {
matches!(self, Self::Endnote { .. })
}
pub fn is_line(&self) -> bool {
matches!(self, Self::Line { .. })
}
pub fn is_ellipse(&self) -> bool {
matches!(self, Self::Ellipse { .. })
}
pub fn is_polygon(&self) -> bool {
matches!(self, Self::Polygon { .. })
}
pub fn is_equation(&self) -> bool {
matches!(self, Self::Equation { .. })
}
pub fn is_chart(&self) -> bool {
matches!(self, Self::Chart { .. })
}
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown { .. })
}
pub fn is_dutmal(&self) -> bool {
matches!(self, Self::Dutmal { .. })
}
pub fn is_compose(&self) -> bool {
matches!(self, Self::Compose { .. })
}
pub fn is_arc(&self) -> bool {
matches!(self, Self::Arc { .. })
}
pub fn is_curve(&self) -> bool {
matches!(self, Self::Curve { .. })
}
pub fn is_connect_line(&self) -> bool {
matches!(self, Self::ConnectLine { .. })
}
pub fn is_bookmark(&self) -> bool {
matches!(self, Self::Bookmark { .. })
}
pub fn is_cross_ref(&self) -> bool {
matches!(self, Self::CrossRef { .. })
}
pub fn is_field(&self) -> bool {
matches!(self, Self::Field { .. })
}
pub fn is_memo(&self) -> bool {
matches!(self, Self::Memo { .. })
}
pub fn is_index_mark(&self) -> bool {
matches!(self, Self::IndexMark { .. })
}
pub fn bookmark(name: &str) -> Self {
Self::Bookmark { name: name.to_string(), bookmark_type: BookmarkType::Point }
}
pub fn field(hint: &str) -> Self {
Self::Field {
field_type: FieldType::ClickHere,
hint_text: Some(hint.to_string()),
help_text: None,
}
}
pub fn index_mark(primary: &str) -> Self {
Self::IndexMark { primary: primary.to_string(), secondary: None }
}
pub fn memo(content: Vec<Paragraph>, author: &str, date: &str) -> Self {
Self::Memo { content, author: author.to_string(), date: date.to_string() }
}
pub fn cross_ref(target: &str, ref_type: RefType, content_type: RefContentType) -> Self {
Self::CrossRef {
target_name: target.to_string(),
ref_type,
content_type,
as_hyperlink: false,
}
}
pub fn chart(chart_type: ChartType, data: ChartData) -> Self {
Self::Chart {
chart_type,
data,
width: HwpUnit::new(32250).expect("32250 is valid"),
height: HwpUnit::new(18750).expect("18750 is valid"),
title: None,
legend: LegendPosition::default(),
grouping: ChartGrouping::default(),
bar_shape: None,
explosion: None,
of_pie_type: None,
radar_style: None,
wireframe: None,
bubble_3d: None,
scatter_style: None,
show_markers: None,
stock_variant: None,
}
}
pub fn equation(script: &str) -> Self {
Self::Equation {
script: script.to_string(),
width: HwpUnit::new(8779).expect("8779 is valid"),
height: HwpUnit::new(2600).expect("2600 is valid"),
base_line: 71,
text_color: Color::BLACK,
font: "HancomEQN".to_string(),
}
}
pub fn text_box(paragraphs: Vec<Paragraph>, width: HwpUnit, height: HwpUnit) -> Self {
Self::TextBox {
paragraphs,
width,
height,
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
}
}
pub fn footnote(paragraphs: Vec<Paragraph>) -> Self {
Self::Footnote { inst_id: None, paragraphs }
}
pub fn endnote(paragraphs: Vec<Paragraph>) -> Self {
Self::Endnote { inst_id: None, paragraphs }
}
pub fn footnote_with_id(inst_id: u32, paragraphs: Vec<Paragraph>) -> Self {
Self::Footnote { inst_id: Some(inst_id), paragraphs }
}
pub fn endnote_with_id(inst_id: u32, paragraphs: Vec<Paragraph>) -> Self {
Self::Endnote { inst_id: Some(inst_id), paragraphs }
}
pub fn ellipse(width: HwpUnit, height: HwpUnit) -> Self {
let w = width.as_i32();
let h = height.as_i32();
Self::Ellipse {
center: ShapePoint::new(w / 2, h / 2),
axis1: ShapePoint::new(w, h / 2),
axis2: ShapePoint::new(w / 2, h),
width,
height,
horz_offset: 0,
vert_offset: 0,
paragraphs: vec![],
caption: None,
style: None,
}
}
pub fn ellipse_with_text(width: HwpUnit, height: HwpUnit, paragraphs: Vec<Paragraph>) -> Self {
let w = width.as_i32();
let h = height.as_i32();
Self::Ellipse {
center: ShapePoint::new(w / 2, h / 2),
axis1: ShapePoint::new(w, h / 2),
axis2: ShapePoint::new(w / 2, h),
width,
height,
horz_offset: 0,
vert_offset: 0,
paragraphs,
caption: None,
style: None,
}
}
pub fn polygon(vertices: Vec<ShapePoint>) -> CoreResult<Self> {
if vertices.len() < 3 {
return Err(CoreError::InvalidStructure {
context: "Control::polygon".to_string(),
reason: format!("polygon requires at least 3 vertices, got {}", vertices.len()),
});
}
let min_x = vertices.iter().map(|p| p.x as i64).min().unwrap_or(0);
let max_x = vertices.iter().map(|p| p.x as i64).max().unwrap_or(0);
let min_y = vertices.iter().map(|p| p.y as i64).min().unwrap_or(0);
let max_y = vertices.iter().map(|p| p.y as i64).max().unwrap_or(0);
let bbox_w = i32::try_from((max_x - min_x).max(0)).unwrap_or(i32::MAX);
let bbox_h = i32::try_from((max_y - min_y).max(0)).unwrap_or(i32::MAX);
let width = HwpUnit::new(bbox_w).map_err(|_| CoreError::InvalidStructure {
context: "Control::polygon".into(),
reason: format!("bounding box width {bbox_w} exceeds HwpUnit range"),
})?;
let height = HwpUnit::new(bbox_h).map_err(|_| CoreError::InvalidStructure {
context: "Control::polygon".into(),
reason: format!("bounding box height {bbox_h} exceeds HwpUnit range"),
})?;
Ok(Self::Polygon {
vertices,
width,
height,
horz_offset: 0,
vert_offset: 0,
paragraphs: vec![],
caption: None,
style: None,
})
}
pub fn line(start: ShapePoint, end: ShapePoint) -> CoreResult<Self> {
if start == end {
return Err(CoreError::InvalidStructure {
context: "Control::line".to_string(),
reason: "start and end points are identical (degenerate line)".to_string(),
});
}
let min_x = start.x.min(end.x);
let min_y = start.y.min(end.y);
let norm_start =
ShapePoint::new(start.x.saturating_sub(min_x), start.y.saturating_sub(min_y));
let norm_end = ShapePoint::new(end.x.saturating_sub(min_x), end.y.saturating_sub(min_y));
let raw_w =
i32::try_from(((end.x as i64) - (start.x as i64)).unsigned_abs()).unwrap_or(i32::MAX);
let raw_h =
i32::try_from(((end.y as i64) - (start.y as i64)).unsigned_abs()).unwrap_or(i32::MAX);
let raw_w = raw_w.max(100);
let raw_h = raw_h.max(100);
let width = HwpUnit::new(raw_w).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
let height = HwpUnit::new(raw_h).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
Ok(Self::Line {
start: norm_start,
end: norm_end,
width,
height,
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
})
}
pub fn horizontal_line(width: HwpUnit) -> Self {
let w = width.as_i32();
Self::Line {
start: ShapePoint::new(0, 0),
end: ShapePoint::new(w, 0),
width,
height: HwpUnit::new(100).expect("100 is valid"),
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
}
}
pub fn dutmal(main_text: impl Into<String>, sub_text: impl Into<String>) -> Self {
Self::Dutmal {
main_text: main_text.into(),
sub_text: sub_text.into(),
position: DutmalPosition::Top,
sz_ratio: 0,
align: DutmalAlign::Center,
}
}
pub fn compose(text: impl Into<String>) -> Self {
Self::Compose {
compose_text: text.into(),
circle_type: "SHAPE_REVERSAL_TIRANGLE".to_string(), char_sz: -3,
compose_type: "SPREAD".to_string(),
}
}
pub fn arc(arc_type: ArcType, width: HwpUnit, height: HwpUnit) -> Self {
let w = width.as_i32();
let h = height.as_i32();
Self::Arc {
arc_type,
center: ShapePoint::new(w / 2, h / 2),
axis1: ShapePoint::new(w, h / 2),
axis2: ShapePoint::new(w / 2, h),
start1: ShapePoint::new(w, h / 2),
end1: ShapePoint::new(w / 2, 0),
start2: ShapePoint::new(w, h / 2),
end2: ShapePoint::new(w / 2, 0),
width,
height,
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
}
}
pub fn curve(points: Vec<ShapePoint>) -> CoreResult<Self> {
if points.len() < 2 {
return Err(CoreError::InvalidStructure {
context: "Control::curve".to_string(),
reason: format!("curve requires at least 2 points, got {}", points.len()),
});
}
let min_x = points.iter().map(|p| p.x as i64).min().unwrap_or(0);
let max_x = points.iter().map(|p| p.x as i64).max().unwrap_or(0);
let min_y = points.iter().map(|p| p.y as i64).min().unwrap_or(0);
let max_y = points.iter().map(|p| p.y as i64).max().unwrap_or(0);
let bbox_w = i32::try_from((max_x - min_x).max(1)).unwrap_or(i32::MAX);
let bbox_h = i32::try_from((max_y - min_y).max(1)).unwrap_or(i32::MAX);
let width = HwpUnit::new(bbox_w).map_err(|_| CoreError::InvalidStructure {
context: "Control::curve".into(),
reason: format!("bounding box width {bbox_w} exceeds HwpUnit range"),
})?;
let height = HwpUnit::new(bbox_h).map_err(|_| CoreError::InvalidStructure {
context: "Control::curve".into(),
reason: format!("bounding box height {bbox_h} exceeds HwpUnit range"),
})?;
let seg_count = points.len().saturating_sub(1);
Ok(Self::Curve {
points,
segment_types: vec![CurveSegmentType::Curve; seg_count],
width,
height,
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
})
}
pub fn connect_line(start: ShapePoint, end: ShapePoint) -> CoreResult<Self> {
if start == end {
return Err(CoreError::InvalidStructure {
context: "Control::connect_line".to_string(),
reason: "start and end points are identical (degenerate line)".to_string(),
});
}
let min_x = start.x.min(end.x);
let min_y = start.y.min(end.y);
let norm_start =
ShapePoint::new(start.x.saturating_sub(min_x), start.y.saturating_sub(min_y));
let norm_end = ShapePoint::new(end.x.saturating_sub(min_x), end.y.saturating_sub(min_y));
let raw_w =
i32::try_from(((end.x as i64) - (start.x as i64)).unsigned_abs()).unwrap_or(i32::MAX);
let raw_h =
i32::try_from(((end.y as i64) - (start.y as i64)).unsigned_abs()).unwrap_or(i32::MAX);
let raw_w = raw_w.max(100);
let raw_h = raw_h.max(100);
let width = HwpUnit::new(raw_w).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
let height = HwpUnit::new(raw_h).unwrap_or_else(|_| HwpUnit::new(100).expect("valid"));
Ok(Self::ConnectLine {
start: norm_start,
end: norm_end,
control_points: Vec::new(),
connect_type: "STRAIGHT".to_string(),
width,
height,
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
})
}
pub fn hyperlink(text: &str, url: &str) -> Self {
Self::Hyperlink { text: text.to_string(), url: url.to_string() }
}
}
impl std::fmt::Display for Control {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TextBox { paragraphs, .. } => {
let n = paragraphs.len();
let word = if n == 1 { "paragraph" } else { "paragraphs" };
write!(f, "TextBox({n} {word})")
}
Self::Hyperlink { text, url } => {
let preview: String =
if text.len() > 30 { text.chars().take(30).collect() } else { text.clone() };
write!(f, "Hyperlink(\"{preview}\" -> {url})")
}
Self::Footnote { paragraphs, .. } => {
let n = paragraphs.len();
let word = if n == 1 { "paragraph" } else { "paragraphs" };
write!(f, "Footnote({n} {word})")
}
Self::Endnote { paragraphs, .. } => {
let n = paragraphs.len();
let word = if n == 1 { "paragraph" } else { "paragraphs" };
write!(f, "Endnote({n} {word})")
}
Self::Line { .. } => {
write!(f, "Line")
}
Self::Ellipse { paragraphs, .. } => {
let n = paragraphs.len();
let word = if n == 1 { "paragraph" } else { "paragraphs" };
write!(f, "Ellipse({n} {word})")
}
Self::Polygon { vertices, paragraphs, .. } => {
let nv = vertices.len();
let np = paragraphs.len();
let vw = if nv == 1 { "vertex" } else { "vertices" };
let pw = if np == 1 { "paragraph" } else { "paragraphs" };
write!(f, "Polygon({nv} {vw}, {np} {pw})")
}
Self::Chart { chart_type, data, .. } => {
let series_count = match data {
ChartData::Category { series, .. } => series.len(),
ChartData::Xy { series } => series.len(),
};
write!(f, "Chart({chart_type:?}, {series_count} series)")
}
Self::Equation { script, .. } => {
let preview: String = if script.len() > 30 {
script.chars().take(30).collect()
} else {
script.clone()
};
write!(f, "Equation(\"{preview}\")")
}
Self::Dutmal { main_text, sub_text, .. } => {
write!(f, "Dutmal(\"{main_text}\" / \"{sub_text}\")")
}
Self::Compose { compose_text, .. } => {
write!(f, "Compose(\"{compose_text}\")")
}
Self::Arc { arc_type, .. } => {
write!(f, "Arc({arc_type})")
}
Self::Curve { points, .. } => {
write!(f, "Curve({} points)", points.len())
}
Self::ConnectLine { .. } => {
write!(f, "ConnectLine")
}
Self::Bookmark { name, bookmark_type } => {
write!(f, "Bookmark(\"{name}\", {bookmark_type})")
}
Self::CrossRef { target_name, ref_type, .. } => {
write!(f, "CrossRef(\"{target_name}\", {ref_type})")
}
Self::Field { field_type, hint_text, .. } => {
let hint = hint_text.as_deref().unwrap_or("");
write!(f, "Field({field_type}, \"{hint}\")")
}
Self::Memo { content, author, .. } => {
let n = content.len();
let word = if n == 1 { "paragraph" } else { "paragraphs" };
write!(f, "Memo({n} {word}, by {author})")
}
Self::IndexMark { primary, secondary } => {
if let Some(sec) = secondary {
write!(f, "IndexMark(\"{primary}\" / \"{sec}\")")
} else {
write!(f, "IndexMark(\"{primary}\")")
}
}
Self::Unknown { tag, .. } => {
write!(f, "Unknown({tag})")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::run::Run;
use hwpforge_foundation::{CharShapeIndex, Color, ParaShapeIndex};
fn simple_paragraph() -> Paragraph {
Paragraph::with_runs(
vec![Run::text("footnote text", CharShapeIndex::new(0))],
ParaShapeIndex::new(0),
)
}
#[test]
fn shape_style_default_all_none() {
let s = ShapeStyle::default();
assert!(s.line_color.is_none());
assert!(s.fill_color.is_none());
assert!(s.line_width.is_none());
assert!(s.line_style.is_none());
}
#[test]
fn shape_style_with_typed_fields() {
let s = ShapeStyle {
line_color: Some(Color::from_rgb(255, 0, 0)),
fill_color: Some(Color::from_rgb(0, 255, 0)),
line_width: Some(100),
line_style: Some(LineStyle::Dash),
..Default::default()
};
assert_eq!(s.line_color.unwrap(), Color::from_rgb(255, 0, 0));
assert_eq!(s.fill_color.unwrap(), Color::from_rgb(0, 255, 0));
assert_eq!(s.line_width.unwrap(), 100);
assert_eq!(s.line_style.unwrap(), LineStyle::Dash);
}
#[test]
fn line_style_default() {
assert_eq!(LineStyle::default(), LineStyle::Solid);
}
#[test]
fn line_style_display() {
assert_eq!(LineStyle::Solid.to_string(), "SOLID");
assert_eq!(LineStyle::Dash.to_string(), "DASH");
assert_eq!(LineStyle::Dot.to_string(), "DOT");
assert_eq!(LineStyle::DashDot.to_string(), "DASH_DOT");
assert_eq!(LineStyle::DashDotDot.to_string(), "DASH_DOT_DOT");
assert_eq!(LineStyle::None.to_string(), "NONE");
}
#[test]
fn line_style_from_str() {
assert_eq!("SOLID".parse::<LineStyle>().unwrap(), LineStyle::Solid);
assert_eq!("Dash".parse::<LineStyle>().unwrap(), LineStyle::Dash);
assert_eq!("dot".parse::<LineStyle>().unwrap(), LineStyle::Dot);
assert_eq!("DASH_DOT".parse::<LineStyle>().unwrap(), LineStyle::DashDot);
assert_eq!("DashDotDot".parse::<LineStyle>().unwrap(), LineStyle::DashDotDot);
assert_eq!("NONE".parse::<LineStyle>().unwrap(), LineStyle::None);
assert!("INVALID".parse::<LineStyle>().is_err());
}
#[test]
fn line_style_serde_roundtrip() {
for style in [
LineStyle::Solid,
LineStyle::Dash,
LineStyle::Dot,
LineStyle::DashDot,
LineStyle::DashDotDot,
LineStyle::None,
] {
let json = serde_json::to_string(&style).unwrap();
let back: LineStyle = serde_json::from_str(&json).unwrap();
assert_eq!(style, back);
}
}
#[test]
fn text_box_construction() {
let ctrl = Control::TextBox {
paragraphs: vec![simple_paragraph()],
width: HwpUnit::from_mm(80.0).unwrap(),
height: HwpUnit::from_mm(40.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
};
assert!(ctrl.is_text_box());
assert!(!ctrl.is_hyperlink());
assert!(!ctrl.is_footnote());
assert!(!ctrl.is_endnote());
assert!(!ctrl.is_unknown());
}
#[test]
fn hyperlink_construction() {
let ctrl = Control::Hyperlink {
text: "Click".to_string(),
url: "https://example.com".to_string(),
};
assert!(ctrl.is_hyperlink());
assert!(!ctrl.is_text_box());
}
#[test]
fn footnote_construction() {
let ctrl = Control::Footnote { inst_id: None, paragraphs: vec![simple_paragraph()] };
assert!(ctrl.is_footnote());
assert!(!ctrl.is_text_box());
assert!(!ctrl.is_endnote());
}
#[test]
fn endnote_construction() {
let ctrl = Control::Endnote { inst_id: Some(123456), paragraphs: vec![simple_paragraph()] };
assert!(ctrl.is_endnote());
assert!(!ctrl.is_footnote());
assert!(!ctrl.is_text_box());
}
#[test]
fn unknown_construction() {
let ctrl = Control::Unknown {
tag: "custom:widget".to_string(),
data: Some("<data>value</data>".to_string()),
};
assert!(ctrl.is_unknown());
}
#[test]
fn unknown_without_data() {
let ctrl = Control::Unknown { tag: "header".to_string(), data: None };
assert!(ctrl.is_unknown());
}
#[test]
fn display_text_box() {
let ctrl = Control::TextBox {
paragraphs: vec![simple_paragraph(), simple_paragraph()],
width: HwpUnit::from_mm(80.0).unwrap(),
height: HwpUnit::from_mm(40.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
};
assert_eq!(ctrl.to_string(), "TextBox(2 paragraphs)");
}
#[test]
fn display_hyperlink() {
let ctrl =
Control::Hyperlink { text: "Short".to_string(), url: "https://x.com".to_string() };
let s = ctrl.to_string();
assert!(s.contains("Short"), "display: {s}");
assert!(s.contains("https://x.com"), "display: {s}");
}
#[test]
fn display_hyperlink_long_text_truncated() {
let ctrl =
Control::Hyperlink { text: "A".repeat(100), url: "https://example.com".to_string() };
let s = ctrl.to_string();
assert!(s.contains(&"A".repeat(30)), "display: {s}");
assert!(!s.contains(&"A".repeat(31)), "display: {s}");
}
#[test]
fn display_footnote() {
let ctrl = Control::Footnote { inst_id: None, paragraphs: vec![simple_paragraph()] };
assert_eq!(ctrl.to_string(), "Footnote(1 paragraph)");
}
#[test]
fn display_endnote() {
let ctrl = Control::Endnote { inst_id: Some(999), paragraphs: vec![simple_paragraph()] };
assert_eq!(ctrl.to_string(), "Endnote(1 paragraph)");
}
#[test]
fn display_unknown() {
let ctrl = Control::Unknown { tag: "bookmark".to_string(), data: None };
assert_eq!(ctrl.to_string(), "Unknown(bookmark)");
}
#[test]
fn equality() {
let a = Control::Hyperlink { text: "A".to_string(), url: "B".to_string() };
let b = Control::Hyperlink { text: "A".to_string(), url: "B".to_string() };
let c = Control::Hyperlink { text: "A".to_string(), url: "C".to_string() };
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn serde_roundtrip_text_box() {
let ctrl = Control::TextBox {
paragraphs: vec![simple_paragraph()],
width: HwpUnit::from_mm(80.0).unwrap(),
height: HwpUnit::from_mm(40.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
};
let json = serde_json::to_string(&ctrl).unwrap();
let back: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, back);
}
#[test]
fn serde_roundtrip_hyperlink() {
let ctrl = Control::Hyperlink {
text: "link text".to_string(),
url: "https://rust-lang.org".to_string(),
};
let json = serde_json::to_string(&ctrl).unwrap();
let back: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, back);
}
#[test]
fn serde_roundtrip_footnote() {
let ctrl = Control::Footnote { inst_id: Some(12345), paragraphs: vec![simple_paragraph()] };
let json = serde_json::to_string(&ctrl).unwrap();
let back: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, back);
}
#[test]
fn serde_roundtrip_endnote() {
let ctrl = Control::Endnote { inst_id: None, paragraphs: vec![simple_paragraph()] };
let json = serde_json::to_string(&ctrl).unwrap();
let back: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, back);
}
#[test]
fn serde_roundtrip_unknown() {
let ctrl = Control::Unknown { tag: "test".to_string(), data: Some("payload".to_string()) };
let json = serde_json::to_string(&ctrl).unwrap();
let back: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, back);
}
#[test]
fn line_construction() {
let ctrl = Control::Line {
start: ShapePoint { x: 0, y: 0 },
end: ShapePoint { x: 1000, y: 500 },
width: HwpUnit::from_mm(50.0).unwrap(),
height: HwpUnit::from_mm(25.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
};
assert!(ctrl.is_line());
assert!(!ctrl.is_text_box());
assert!(!ctrl.is_ellipse());
assert!(!ctrl.is_polygon());
}
#[test]
fn ellipse_construction() {
let ctrl = Control::Ellipse {
center: ShapePoint { x: 500, y: 500 },
axis1: ShapePoint { x: 1000, y: 500 },
axis2: ShapePoint { x: 500, y: 1000 },
width: HwpUnit::from_mm(40.0).unwrap(),
height: HwpUnit::from_mm(30.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
paragraphs: vec![],
caption: None,
style: None,
};
assert!(ctrl.is_ellipse());
assert!(!ctrl.is_line());
assert!(!ctrl.is_polygon());
}
#[test]
fn ellipse_with_paragraphs() {
let ctrl = Control::Ellipse {
center: ShapePoint { x: 500, y: 500 },
axis1: ShapePoint { x: 1000, y: 500 },
axis2: ShapePoint { x: 500, y: 1000 },
width: HwpUnit::from_mm(40.0).unwrap(),
height: HwpUnit::from_mm(30.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
paragraphs: vec![simple_paragraph()],
caption: None,
style: None,
};
assert!(ctrl.is_ellipse());
assert_eq!(ctrl.to_string(), "Ellipse(1 paragraph)");
}
#[test]
fn polygon_construction() {
let ctrl = Control::Polygon {
vertices: vec![
ShapePoint { x: 0, y: 0 },
ShapePoint { x: 1000, y: 0 },
ShapePoint { x: 500, y: 1000 },
],
width: HwpUnit::from_mm(50.0).unwrap(),
height: HwpUnit::from_mm(50.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
paragraphs: vec![],
caption: None,
style: None,
};
assert!(ctrl.is_polygon());
assert!(!ctrl.is_line());
assert!(!ctrl.is_ellipse());
assert_eq!(ctrl.to_string(), "Polygon(3 vertices, 0 paragraphs)");
}
#[test]
fn display_line() {
let ctrl = Control::Line {
start: ShapePoint { x: 0, y: 0 },
end: ShapePoint { x: 100, y: 200 },
width: HwpUnit::from_mm(10.0).unwrap(),
height: HwpUnit::from_mm(5.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
};
assert_eq!(ctrl.to_string(), "Line");
}
#[test]
fn serde_roundtrip_line() {
let ctrl = Control::Line {
start: ShapePoint { x: 100, y: 200 },
end: ShapePoint { x: 300, y: 400 },
width: HwpUnit::from_mm(20.0).unwrap(),
height: HwpUnit::from_mm(10.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
caption: None,
style: None,
};
let json = serde_json::to_string(&ctrl).unwrap();
let back: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, back);
}
#[test]
fn serde_roundtrip_ellipse() {
let ctrl = Control::Ellipse {
center: ShapePoint { x: 500, y: 500 },
axis1: ShapePoint { x: 1000, y: 500 },
axis2: ShapePoint { x: 500, y: 1000 },
width: HwpUnit::from_mm(40.0).unwrap(),
height: HwpUnit::from_mm(30.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
paragraphs: vec![simple_paragraph()],
caption: None,
style: None,
};
let json = serde_json::to_string(&ctrl).unwrap();
let back: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, back);
}
#[test]
fn serde_roundtrip_polygon() {
let ctrl = Control::Polygon {
vertices: vec![
ShapePoint { x: 0, y: 0 },
ShapePoint { x: 1000, y: 0 },
ShapePoint { x: 500, y: 1000 },
],
width: HwpUnit::from_mm(50.0).unwrap(),
height: HwpUnit::from_mm(50.0).unwrap(),
horz_offset: 0,
vert_offset: 0,
paragraphs: vec![],
caption: None,
style: None,
};
let json = serde_json::to_string(&ctrl).unwrap();
let back: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, back);
}
#[test]
fn shape_point_equality() {
let a = ShapePoint { x: 10, y: 20 };
let b = ShapePoint { x: 10, y: 20 };
let c = ShapePoint { x: 10, y: 30 };
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn shape_point_new() {
let pt = ShapePoint::new(100, 200);
assert_eq!(pt.x, 100);
assert_eq!(pt.y, 200);
}
#[test]
fn shape_point_serde_roundtrip() {
let pt = ShapePoint::new(500, 750);
let json = serde_json::to_string(&pt).unwrap();
let back: ShapePoint = serde_json::from_str(&json).unwrap();
assert_eq!(pt, back);
}
#[test]
fn equation_constructor_defaults() {
let ctrl = Control::equation("{a+b} over {c+d}");
assert!(ctrl.is_equation());
match ctrl {
Control::Equation { script, width, height, base_line, text_color, ref font } => {
assert_eq!(script, "{a+b} over {c+d}");
assert_eq!(width, HwpUnit::new(8779).unwrap());
assert_eq!(height, HwpUnit::new(2600).unwrap());
assert_eq!(base_line, 71);
assert_eq!(text_color, Color::BLACK);
assert_eq!(font, "HancomEQN");
}
_ => panic!("expected Equation"),
}
}
#[test]
fn equation_constructor_empty_script() {
let ctrl = Control::equation("");
assert!(ctrl.is_equation());
}
#[test]
fn text_box_constructor_defaults() {
let width = HwpUnit::from_mm(80.0).unwrap();
let height = HwpUnit::from_mm(40.0).unwrap();
let ctrl = Control::text_box(vec![simple_paragraph()], width, height);
assert!(ctrl.is_text_box());
match ctrl {
Control::TextBox { paragraphs, horz_offset, vert_offset, caption, style, .. } => {
assert_eq!(paragraphs.len(), 1);
assert_eq!(horz_offset, 0);
assert_eq!(vert_offset, 0);
assert!(caption.is_none());
assert!(style.is_none());
}
_ => panic!("expected TextBox"),
}
}
#[test]
fn footnote_constructor_defaults() {
let ctrl = Control::footnote(vec![simple_paragraph()]);
assert!(ctrl.is_footnote());
match ctrl {
Control::Footnote { inst_id, paragraphs } => {
assert!(inst_id.is_none());
assert_eq!(paragraphs.len(), 1);
}
_ => panic!("expected Footnote"),
}
}
#[test]
fn endnote_constructor_defaults() {
let ctrl = Control::endnote(vec![simple_paragraph()]);
assert!(ctrl.is_endnote());
match ctrl {
Control::Endnote { inst_id, paragraphs } => {
assert!(inst_id.is_none());
assert_eq!(paragraphs.len(), 1);
}
_ => panic!("expected Endnote"),
}
}
#[test]
fn ellipse_constructor_geometry() {
let width = HwpUnit::from_mm(40.0).unwrap();
let height = HwpUnit::from_mm(30.0).unwrap();
let ctrl = Control::ellipse(width, height);
assert!(ctrl.is_ellipse());
match &ctrl {
Control::Ellipse {
center,
axis1,
axis2,
horz_offset,
vert_offset,
paragraphs,
caption,
style,
..
} => {
let w = width.as_i32();
let h = height.as_i32();
assert_eq!(*center, ShapePoint::new(w / 2, h / 2));
assert_eq!(*axis1, ShapePoint::new(w, h / 2));
assert_eq!(*axis2, ShapePoint::new(w / 2, h));
assert_eq!(*horz_offset, 0);
assert_eq!(*vert_offset, 0);
assert!(paragraphs.is_empty());
assert!(caption.is_none());
assert!(style.is_none());
}
_ => panic!("expected Ellipse"),
}
}
#[test]
fn polygon_constructor_triangle() {
let vertices =
vec![ShapePoint::new(0, 1000), ShapePoint::new(500, 0), ShapePoint::new(1000, 1000)];
let ctrl = Control::polygon(vertices).unwrap();
assert!(ctrl.is_polygon());
match &ctrl {
Control::Polygon {
vertices,
width,
height,
horz_offset,
vert_offset,
paragraphs,
caption,
style,
} => {
assert_eq!(vertices.len(), 3);
assert_eq!(*width, HwpUnit::new(1000).unwrap());
assert_eq!(*height, HwpUnit::new(1000).unwrap());
assert_eq!(*horz_offset, 0);
assert_eq!(*vert_offset, 0);
assert!(paragraphs.is_empty());
assert!(caption.is_none());
assert!(style.is_none());
}
_ => panic!("expected Polygon"),
}
}
#[test]
fn polygon_constructor_fewer_than_3_vertices_errors() {
assert!(Control::polygon(vec![]).is_err());
assert!(Control::polygon(vec![ShapePoint::new(0, 0)]).is_err());
assert!(Control::polygon(vec![ShapePoint::new(0, 0), ShapePoint::new(1, 1)]).is_err());
}
#[test]
fn polygon_constructor_negative_coordinates() {
let vertices =
vec![ShapePoint::new(-500, -500), ShapePoint::new(500, -500), ShapePoint::new(0, 500)];
let ctrl = Control::polygon(vertices).unwrap();
assert!(ctrl.is_polygon());
match ctrl {
Control::Polygon { width, height, .. } => {
assert_eq!(width, HwpUnit::new(1000).unwrap());
assert_eq!(height, HwpUnit::new(1000).unwrap());
}
_ => panic!("expected Polygon"),
}
}
#[test]
fn polygon_constructor_degenerate_collinear() {
let vertices =
vec![ShapePoint::new(0, 0), ShapePoint::new(500, 0), ShapePoint::new(1000, 0)];
let ctrl = Control::polygon(vertices).unwrap();
assert!(ctrl.is_polygon());
match ctrl {
Control::Polygon { width, height, .. } => {
assert_eq!(width, HwpUnit::new(1000).unwrap());
assert_eq!(height, HwpUnit::new(0).unwrap());
}
_ => panic!("expected Polygon"),
}
}
#[test]
fn line_constructor_horizontal() {
let ctrl = Control::line(ShapePoint::new(0, 0), ShapePoint::new(5000, 0)).unwrap();
assert!(ctrl.is_line());
match ctrl {
Control::Line {
start,
end,
width,
height,
horz_offset,
vert_offset,
caption,
style,
} => {
assert_eq!(start, ShapePoint::new(0, 0));
assert_eq!(end, ShapePoint::new(5000, 0));
assert_eq!(width, HwpUnit::new(5000).unwrap());
assert_eq!(height, HwpUnit::new(100).unwrap()); assert_eq!(horz_offset, 0);
assert_eq!(vert_offset, 0);
assert!(caption.is_none());
assert!(style.is_none());
}
_ => panic!("expected Line"),
}
}
#[test]
fn line_constructor_vertical() {
let ctrl = Control::line(ShapePoint::new(0, 0), ShapePoint::new(0, 3000)).unwrap();
assert!(ctrl.is_line());
match ctrl {
Control::Line { width, height, .. } => {
assert_eq!(width, HwpUnit::new(100).unwrap()); assert_eq!(height, HwpUnit::new(3000).unwrap());
}
_ => panic!("expected Line"),
}
}
#[test]
fn line_constructor_diagonal_bounding_box() {
let ctrl = Control::line(ShapePoint::new(100, 200), ShapePoint::new(400, 500)).unwrap();
match ctrl {
Control::Line { width, height, .. } => {
assert_eq!(width, HwpUnit::new(300).unwrap());
assert_eq!(height, HwpUnit::new(300).unwrap());
}
_ => panic!("expected Line"),
}
}
#[test]
fn line_constructor_same_point_errors() {
let pt = ShapePoint::new(100, 200);
assert!(Control::line(pt, pt).is_err());
}
#[test]
fn horizontal_line_constructor() {
let width = HwpUnit::from_mm(100.0).unwrap();
let ctrl = Control::horizontal_line(width);
assert!(ctrl.is_line());
match ctrl {
Control::Line {
start,
end,
width: w,
height,
horz_offset,
vert_offset,
caption,
style,
} => {
assert_eq!(start, ShapePoint::new(0, 0));
assert_eq!(end.y, 0);
assert_eq!(end.x, width.as_i32());
assert_eq!(w, width);
assert_eq!(height, HwpUnit::new(100).unwrap()); assert_eq!(horz_offset, 0);
assert_eq!(vert_offset, 0);
assert!(caption.is_none());
assert!(style.is_none());
}
_ => panic!("expected Line"),
}
}
#[test]
fn hyperlink_constructor() {
let ctrl = Control::hyperlink("Visit Rust", "https://rust-lang.org");
assert!(ctrl.is_hyperlink());
match ctrl {
Control::Hyperlink { text, url } => {
assert_eq!(text, "Visit Rust");
assert_eq!(url, "https://rust-lang.org");
}
_ => panic!("expected Hyperlink"),
}
}
#[test]
fn footnote_with_id_sets_inst_id() {
let para = Paragraph::new(ParaShapeIndex::new(0));
let ctrl = Control::footnote_with_id(42, vec![para]);
assert!(ctrl.is_footnote());
match ctrl {
Control::Footnote { inst_id, paragraphs } => {
assert_eq!(inst_id, Some(42));
assert_eq!(paragraphs.len(), 1);
}
_ => panic!("expected Footnote"),
}
}
#[test]
fn endnote_with_id_sets_inst_id() {
let para = Paragraph::new(ParaShapeIndex::new(0));
let ctrl = Control::endnote_with_id(7, vec![para]);
assert!(ctrl.is_endnote());
match ctrl {
Control::Endnote { inst_id, paragraphs } => {
assert_eq!(inst_id, Some(7));
assert_eq!(paragraphs.len(), 1);
}
_ => panic!("expected Endnote"),
}
}
#[test]
fn footnote_with_id_differs_from_plain_footnote() {
let ctrl_plain = Control::footnote(vec![]);
let ctrl_id = Control::footnote_with_id(1, vec![]);
match ctrl_plain {
Control::Footnote { inst_id, .. } => assert_eq!(inst_id, None),
_ => panic!("expected Footnote"),
}
match ctrl_id {
Control::Footnote { inst_id, .. } => assert_eq!(inst_id, Some(1)),
_ => panic!("expected Footnote"),
}
}
#[test]
fn ellipse_with_text_has_correct_geometry_and_paragraphs() {
use hwpforge_foundation::HwpUnit;
let width = HwpUnit::from_mm(40.0).unwrap();
let height = HwpUnit::from_mm(30.0).unwrap();
let para = Paragraph::new(ParaShapeIndex::new(0));
let ctrl = Control::ellipse_with_text(width, height, vec![para]);
assert!(ctrl.is_ellipse());
match ctrl {
Control::Ellipse {
center,
axis1,
axis2,
width: w,
height: h,
horz_offset,
vert_offset,
paragraphs,
caption,
style,
} => {
let wv = w.as_i32();
let hv = h.as_i32();
assert_eq!(center, ShapePoint::new(wv / 2, hv / 2));
assert_eq!(axis1, ShapePoint::new(wv, hv / 2));
assert_eq!(axis2, ShapePoint::new(wv / 2, hv));
assert_eq!(horz_offset, 0);
assert_eq!(vert_offset, 0);
assert_eq!(paragraphs.len(), 1);
assert!(caption.is_none());
assert!(style.is_none());
}
_ => panic!("expected Ellipse"),
}
}
#[test]
fn serde_roundtrip_chart() {
use crate::chart::{ChartData, ChartGrouping, ChartType, LegendPosition};
let ctrl = Control::Chart {
chart_type: ChartType::Column,
data: ChartData::category(&["A", "B"], &[("S1", &[1.0, 2.0])]),
title: Some("Test Chart".to_string()),
legend: LegendPosition::Bottom,
grouping: ChartGrouping::Stacked,
width: HwpUnit::from_mm(100.0).unwrap(),
height: HwpUnit::from_mm(80.0).unwrap(),
stock_variant: None,
bar_shape: None,
scatter_style: None,
radar_style: None,
of_pie_type: None,
explosion: None,
wireframe: None,
bubble_3d: None,
show_markers: None,
};
let json = serde_json::to_string(&ctrl).unwrap();
let back: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, back);
}
#[test]
fn serde_roundtrip_equation() {
let ctrl = Control::Equation {
script: "{a+b} over {c+d}".to_string(),
width: HwpUnit::new(8779).unwrap(),
height: HwpUnit::new(2600).unwrap(),
base_line: 71,
text_color: Color::BLACK,
font: "HancomEQN".to_string(),
};
let json = serde_json::to_string(&ctrl).unwrap();
let back: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, back);
}
#[test]
fn ellipse_with_text_empty_paragraphs_matches_ellipse() {
use hwpforge_foundation::HwpUnit;
let width = HwpUnit::from_mm(20.0).unwrap();
let height = HwpUnit::from_mm(10.0).unwrap();
let plain = Control::ellipse(width, height);
let with_text = Control::ellipse_with_text(width, height, vec![]);
assert_eq!(plain, with_text);
}
#[test]
fn dutmal_constructor_defaults() {
let ctrl = Control::dutmal("본문", "주석");
assert!(ctrl.is_dutmal());
match ctrl {
Control::Dutmal { main_text, sub_text, position, sz_ratio, align } => {
assert_eq!(main_text, "본문");
assert_eq!(sub_text, "주석");
assert_eq!(position, DutmalPosition::Top);
assert_eq!(sz_ratio, 0);
assert_eq!(align, DutmalAlign::Center);
}
_ => panic!("expected Dutmal"),
}
}
#[test]
fn dutmal_is_dutmal_true() {
assert!(Control::dutmal("a", "b").is_dutmal());
}
#[test]
fn dutmal_is_compose_false() {
assert!(!Control::dutmal("a", "b").is_compose());
}
#[test]
fn dutmal_display() {
let ctrl = Control::dutmal("hello", "world");
assert_eq!(ctrl.to_string(), r#"Dutmal("hello" / "world")"#);
}
#[test]
fn dutmal_serde_roundtrip() {
let ctrl = Control::Dutmal {
main_text: "테스트".to_string(),
sub_text: "test".to_string(),
position: DutmalPosition::Bottom,
sz_ratio: 50,
align: DutmalAlign::Right,
};
let json = serde_json::to_string(&ctrl).unwrap();
let decoded: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, decoded);
}
#[test]
fn dutmal_position_default_is_top() {
assert_eq!(DutmalPosition::default(), DutmalPosition::Top);
}
#[test]
fn dutmal_align_default_is_center() {
assert_eq!(DutmalAlign::default(), DutmalAlign::Center);
}
#[test]
fn compose_constructor_defaults() {
let ctrl = Control::compose("가");
assert!(ctrl.is_compose());
match ctrl {
Control::Compose { compose_text, circle_type, char_sz, compose_type } => {
assert_eq!(compose_text, "가");
assert_eq!(circle_type, "SHAPE_REVERSAL_TIRANGLE");
assert_eq!(char_sz, -3);
assert_eq!(compose_type, "SPREAD");
}
_ => panic!("expected Compose"),
}
}
#[test]
fn compose_is_compose_true() {
assert!(Control::compose("나").is_compose());
}
#[test]
fn compose_is_dutmal_false() {
assert!(!Control::compose("나").is_dutmal());
}
#[test]
fn compose_display() {
let ctrl = Control::compose("가나");
assert_eq!(ctrl.to_string(), r#"Compose("가나")"#);
}
#[test]
fn compose_serde_roundtrip() {
let ctrl = Control::Compose {
compose_text: "①".to_string(),
circle_type: "SHAPE_REVERSAL_TIRANGLE".to_string(),
char_sz: -3,
compose_type: "SPREAD".to_string(),
};
let json = serde_json::to_string(&ctrl).unwrap();
let decoded: Control = serde_json::from_str(&json).unwrap();
assert_eq!(ctrl, decoded);
}
#[test]
fn compose_spec_typo_preserved() {
let ctrl = Control::compose("X");
match ctrl {
Control::Compose { circle_type, .. } => {
assert_eq!(circle_type, "SHAPE_REVERSAL_TIRANGLE");
assert!(!circle_type.contains("TRIANGLE")); }
_ => panic!("expected Compose"),
}
}
#[test]
fn line_extreme_coords_no_panic() {
let start = ShapePoint::new(i32::MIN, i32::MIN);
let end = ShapePoint::new(i32::MAX, i32::MAX);
let ctrl = Control::line(start, end).unwrap();
assert!(ctrl.is_line());
}
#[test]
fn connect_line_extreme_coords_no_panic() {
let start = ShapePoint::new(i32::MIN, 0);
let end = ShapePoint::new(i32::MAX, 0);
let ctrl = Control::connect_line(start, end).unwrap();
assert!(ctrl.is_connect_line());
}
#[test]
fn polygon_extreme_coords_no_panic() {
let vertices = vec![
ShapePoint::new(i32::MIN, 0),
ShapePoint::new(i32::MAX, 0),
ShapePoint::new(0, i32::MAX),
];
let _ = Control::polygon(vertices);
}
#[test]
fn curve_extreme_coords_no_panic() {
let points = vec![ShapePoint::new(i32::MIN, i32::MIN), ShapePoint::new(i32::MAX, i32::MAX)];
let _ = Control::curve(points);
}
}