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