1use hwpforge_foundation::HwpUnit;
30use schemars::JsonSchema;
31use serde::{Deserialize, Serialize};
32
33use crate::paragraph::Paragraph;
34
35pub const DEFAULT_CAPTION_GAP: i32 = 850;
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
61pub struct Caption {
62 pub side: CaptionSide,
64 pub width: Option<HwpUnit>,
66 pub gap: HwpUnit,
68 pub paragraphs: Vec<Paragraph>,
70}
71
72impl Default for Caption {
73 fn default() -> Self {
74 Self {
75 side: CaptionSide::default(),
76 width: None,
77 gap: HwpUnit::new(DEFAULT_CAPTION_GAP).unwrap(),
78 paragraphs: Vec::new(),
79 }
80 }
81}
82
83impl Caption {
84 pub fn new(paragraphs: Vec<Paragraph>, side: CaptionSide) -> Self {
105 Self {
106 side,
107 width: None,
108 gap: HwpUnit::new(DEFAULT_CAPTION_GAP).expect("DEFAULT_CAPTION_GAP is valid"),
109 paragraphs,
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
129pub enum CaptionSide {
130 Left,
132 Right,
134 Top,
136 #[default]
138 Bottom,
139}
140
141impl std::fmt::Display for CaptionSide {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 match self {
144 Self::Left => write!(f, "Left"),
145 Self::Right => write!(f, "Right"),
146 Self::Top => write!(f, "Top"),
147 Self::Bottom => write!(f, "Bottom"),
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::run::Run;
156 use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
157
158 fn simple_paragraph() -> Paragraph {
159 Paragraph::with_runs(
160 vec![Run::text("Figure 1: Example", CharShapeIndex::new(0))],
161 ParaShapeIndex::new(0),
162 )
163 }
164
165 #[test]
166 fn caption_new_bottom() {
167 let cap = Caption::new(vec![simple_paragraph()], CaptionSide::Bottom);
168 assert_eq!(cap.side, CaptionSide::Bottom);
169 assert_eq!(cap.gap.as_i32(), 850);
170 assert!(cap.width.is_none());
171 assert_eq!(cap.paragraphs.len(), 1);
172 }
173
174 #[test]
175 fn caption_new_top() {
176 let cap = Caption::new(vec![simple_paragraph(), simple_paragraph()], CaptionSide::Top);
177 assert_eq!(cap.side, CaptionSide::Top);
178 assert_eq!(cap.paragraphs.len(), 2);
179 }
180
181 #[test]
182 fn caption_new_empty_paragraphs() {
183 let cap = Caption::new(vec![], CaptionSide::Left);
184 assert!(cap.paragraphs.is_empty());
185 assert_eq!(cap.side, CaptionSide::Left);
186 }
187
188 #[test]
189 fn caption_default_values() {
190 let cap = Caption::default();
191 assert_eq!(cap.side, CaptionSide::Bottom);
192 assert!(cap.width.is_none());
193 assert_eq!(cap.gap.as_i32(), 850);
194 assert!(cap.paragraphs.is_empty());
195 }
196
197 #[test]
198 fn caption_side_default_is_bottom() {
199 assert_eq!(CaptionSide::default(), CaptionSide::Bottom);
200 }
201
202 #[test]
203 fn caption_side_all_variants() {
204 let sides = [CaptionSide::Left, CaptionSide::Right, CaptionSide::Top, CaptionSide::Bottom];
205 assert_eq!(sides.len(), 4);
206
207 assert_eq!(CaptionSide::Left.to_string(), "Left");
209 assert_eq!(CaptionSide::Right.to_string(), "Right");
210 assert_eq!(CaptionSide::Top.to_string(), "Top");
211 assert_eq!(CaptionSide::Bottom.to_string(), "Bottom");
212 }
213
214 #[test]
215 fn caption_serde_roundtrip() {
216 let cap = Caption {
217 side: CaptionSide::Top,
218 width: Some(HwpUnit::from_mm(80.0).unwrap()),
219 gap: HwpUnit::new(1000).unwrap(),
220 paragraphs: vec![simple_paragraph()],
221 };
222 let json = serde_json::to_string(&cap).unwrap();
223 let back: Caption = serde_json::from_str(&json).unwrap();
224 assert_eq!(cap, back);
225 }
226
227 #[test]
228 fn caption_serde_roundtrip_default() {
229 let cap = Caption::default();
230 let json = serde_json::to_string(&cap).unwrap();
231 let back: Caption = serde_json::from_str(&json).unwrap();
232 assert_eq!(cap, back);
233 }
234
235 #[test]
236 fn caption_side_serde_roundtrip() {
237 for side in [CaptionSide::Left, CaptionSide::Right, CaptionSide::Top, CaptionSide::Bottom] {
238 let json = serde_json::to_string(&side).unwrap();
239 let back: CaptionSide = serde_json::from_str(&json).unwrap();
240 assert_eq!(side, back);
241 }
242 }
243
244 #[test]
245 fn caption_with_paragraphs() {
246 let cap = Caption {
247 side: CaptionSide::Bottom,
248 width: None,
249 gap: HwpUnit::new(850).unwrap(),
250 paragraphs: vec![simple_paragraph(), simple_paragraph()],
251 };
252 assert_eq!(cap.paragraphs.len(), 2);
253 }
254
255 #[test]
256 fn caption_empty_paragraphs() {
257 let cap = Caption { paragraphs: vec![], ..Caption::default() };
259 assert!(cap.paragraphs.is_empty());
260 let json = serde_json::to_string(&cap).unwrap();
262 let back: Caption = serde_json::from_str(&json).unwrap();
263 assert_eq!(cap, back);
264 }
265
266 #[test]
267 fn caption_clone_independence() {
268 let cap = Caption {
269 side: CaptionSide::Left,
270 width: Some(HwpUnit::from_mm(50.0).unwrap()),
271 gap: HwpUnit::new(500).unwrap(),
272 paragraphs: vec![simple_paragraph()],
273 };
274 let mut cloned = cap.clone();
275 cloned.side = CaptionSide::Right;
276 assert_eq!(cap.side, CaptionSide::Left);
277 }
278
279 #[test]
280 fn caption_equality() {
281 let a = Caption::default();
282 let b = Caption::default();
283 assert_eq!(a, b);
284
285 let c = Caption { side: CaptionSide::Top, ..Caption::default() };
286 assert_ne!(a, c);
287 }
288
289 #[test]
290 fn caption_side_hash() {
291 use std::collections::HashSet;
292 let mut set = HashSet::new();
293 set.insert(CaptionSide::Left);
294 set.insert(CaptionSide::Right);
295 set.insert(CaptionSide::Left);
296 assert_eq!(set.len(), 2);
297 }
298
299 #[test]
300 fn caption_side_copy() {
301 let side = CaptionSide::Top;
302 let copied = side;
303 assert_eq!(side, copied);
304 }
305
306 #[test]
307 fn caption_custom_gap() {
308 let cap = Caption { gap: HwpUnit::from_mm(5.0).unwrap(), ..Caption::default() };
309 assert!(cap.gap.as_i32() > 850);
310 }
311
312 #[test]
313 fn caption_new_empty_bottom_equals_default() {
314 let from_new = Caption::new(vec![], CaptionSide::Bottom);
315 let from_default = Caption::default();
316 assert_eq!(from_new, from_default);
317 }
318
319 #[test]
320 fn default_caption_gap_constant() {
321 assert_eq!(super::DEFAULT_CAPTION_GAP, 850);
322 let cap = Caption::default();
323 assert_eq!(cap.gap.as_i32(), super::DEFAULT_CAPTION_GAP);
324 }
325}