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(crate) fn walk_paragraphs_mut(&mut self, f: &mut dyn FnMut(&mut Paragraph)) {
106 for p in &mut self.paragraphs {
107 p.walk_paragraphs_mut(f);
108 }
109 }
110
111 pub fn new(paragraphs: Vec<Paragraph>, side: CaptionSide) -> Self {
113 Self {
114 side,
115 width: None,
116 gap: HwpUnit::new(DEFAULT_CAPTION_GAP).expect("DEFAULT_CAPTION_GAP is valid"),
117 paragraphs,
118 }
119 }
120}
121
122#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
137pub enum CaptionSide {
138 Left,
140 Right,
142 Top,
144 #[default]
146 Bottom,
147}
148
149impl std::fmt::Display for CaptionSide {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 match self {
152 Self::Left => write!(f, "Left"),
153 Self::Right => write!(f, "Right"),
154 Self::Top => write!(f, "Top"),
155 Self::Bottom => write!(f, "Bottom"),
156 }
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::run::Run;
164 use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
165
166 fn simple_paragraph() -> Paragraph {
167 Paragraph::with_runs(
168 vec![Run::text("Figure 1: Example", CharShapeIndex::new(0))],
169 ParaShapeIndex::new(0),
170 )
171 }
172
173 #[test]
174 fn caption_new_bottom() {
175 let cap = Caption::new(vec![simple_paragraph()], CaptionSide::Bottom);
176 assert_eq!(cap.side, CaptionSide::Bottom);
177 assert_eq!(cap.gap.as_i32(), 850);
178 assert!(cap.width.is_none());
179 assert_eq!(cap.paragraphs.len(), 1);
180 }
181
182 #[test]
183 fn caption_new_top() {
184 let cap = Caption::new(vec![simple_paragraph(), simple_paragraph()], CaptionSide::Top);
185 assert_eq!(cap.side, CaptionSide::Top);
186 assert_eq!(cap.paragraphs.len(), 2);
187 }
188
189 #[test]
190 fn caption_new_empty_paragraphs() {
191 let cap = Caption::new(vec![], CaptionSide::Left);
192 assert!(cap.paragraphs.is_empty());
193 assert_eq!(cap.side, CaptionSide::Left);
194 }
195
196 #[test]
197 fn caption_default_values() {
198 let cap = Caption::default();
199 assert_eq!(cap.side, CaptionSide::Bottom);
200 assert!(cap.width.is_none());
201 assert_eq!(cap.gap.as_i32(), 850);
202 assert!(cap.paragraphs.is_empty());
203 }
204
205 #[test]
206 fn caption_side_default_is_bottom() {
207 assert_eq!(CaptionSide::default(), CaptionSide::Bottom);
208 }
209
210 #[test]
211 fn caption_side_all_variants() {
212 let sides = [CaptionSide::Left, CaptionSide::Right, CaptionSide::Top, CaptionSide::Bottom];
213 assert_eq!(sides.len(), 4);
214
215 assert_eq!(CaptionSide::Left.to_string(), "Left");
217 assert_eq!(CaptionSide::Right.to_string(), "Right");
218 assert_eq!(CaptionSide::Top.to_string(), "Top");
219 assert_eq!(CaptionSide::Bottom.to_string(), "Bottom");
220 }
221
222 #[test]
223 fn caption_serde_roundtrip() {
224 let cap = Caption {
225 side: CaptionSide::Top,
226 width: Some(HwpUnit::from_mm(80.0).unwrap()),
227 gap: HwpUnit::new(1000).unwrap(),
228 paragraphs: vec![simple_paragraph()],
229 };
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_serde_roundtrip_default() {
237 let cap = Caption::default();
238 let json = serde_json::to_string(&cap).unwrap();
239 let back: Caption = serde_json::from_str(&json).unwrap();
240 assert_eq!(cap, back);
241 }
242
243 #[test]
244 fn caption_side_serde_roundtrip() {
245 for side in [CaptionSide::Left, CaptionSide::Right, CaptionSide::Top, CaptionSide::Bottom] {
246 let json = serde_json::to_string(&side).unwrap();
247 let back: CaptionSide = serde_json::from_str(&json).unwrap();
248 assert_eq!(side, back);
249 }
250 }
251
252 #[test]
253 fn caption_with_paragraphs() {
254 let cap = Caption {
255 side: CaptionSide::Bottom,
256 width: None,
257 gap: HwpUnit::new(850).unwrap(),
258 paragraphs: vec![simple_paragraph(), simple_paragraph()],
259 };
260 assert_eq!(cap.paragraphs.len(), 2);
261 }
262
263 #[test]
264 fn caption_empty_paragraphs() {
265 let cap = Caption { paragraphs: vec![], ..Caption::default() };
267 assert!(cap.paragraphs.is_empty());
268 let json = serde_json::to_string(&cap).unwrap();
270 let back: Caption = serde_json::from_str(&json).unwrap();
271 assert_eq!(cap, back);
272 }
273
274 #[test]
275 fn caption_clone_independence() {
276 let cap = Caption {
277 side: CaptionSide::Left,
278 width: Some(HwpUnit::from_mm(50.0).unwrap()),
279 gap: HwpUnit::new(500).unwrap(),
280 paragraphs: vec![simple_paragraph()],
281 };
282 let mut cloned = cap.clone();
283 cloned.side = CaptionSide::Right;
284 assert_eq!(cap.side, CaptionSide::Left);
285 }
286
287 #[test]
288 fn caption_equality() {
289 let a = Caption::default();
290 let b = Caption::default();
291 assert_eq!(a, b);
292
293 let c = Caption { side: CaptionSide::Top, ..Caption::default() };
294 assert_ne!(a, c);
295 }
296
297 #[test]
298 fn caption_side_hash() {
299 use std::collections::HashSet;
300 let mut set = HashSet::new();
301 set.insert(CaptionSide::Left);
302 set.insert(CaptionSide::Right);
303 set.insert(CaptionSide::Left);
304 assert_eq!(set.len(), 2);
305 }
306
307 #[test]
308 fn caption_side_copy() {
309 let side = CaptionSide::Top;
310 let copied = side;
311 assert_eq!(side, copied);
312 }
313
314 #[test]
315 fn caption_custom_gap() {
316 let cap = Caption { gap: HwpUnit::from_mm(5.0).unwrap(), ..Caption::default() };
317 assert!(cap.gap.as_i32() > 850);
318 }
319
320 #[test]
321 fn caption_new_empty_bottom_equals_default() {
322 let from_new = Caption::new(vec![], CaptionSide::Bottom);
323 let from_default = Caption::default();
324 assert_eq!(from_new, from_default);
325 }
326
327 #[test]
328 fn default_caption_gap_constant() {
329 assert_eq!(super::DEFAULT_CAPTION_GAP, 850);
330 let cap = Caption::default();
331 assert_eq!(cap.gap.as_i32(), super::DEFAULT_CAPTION_GAP);
332 }
333}