1use crate::area::{Area, AreaContent, AreaId, AreaTree, AreaType};
6use fop_types::{Length, Point, Rect, Result, Size};
7
8pub struct ListLayout {
10 available_width: Length,
12
13 label_width: Length,
16
17 label_separation: Length,
19
20 body_start_offset: Length,
23}
24
25#[derive(Debug, Clone)]
27pub struct ListItemLayout {
28 pub y_position: Length,
30
31 pub height: Length,
33
34 pub label_id: Option<AreaId>,
36
37 pub body_id: Option<AreaId>,
39}
40
41impl ListLayout {
42 pub fn new(available_width: Length) -> Self {
44 Self {
45 available_width,
46 label_width: Length::from_pt(18.0), label_separation: Length::from_pt(6.0), body_start_offset: Length::from_pt(24.0), }
50 }
51
52 pub fn with_label_width(mut self, width: Length) -> Self {
55 self.label_width = width;
56 self
57 }
58
59 pub fn with_label_separation(mut self, separation: Length) -> Self {
61 self.label_separation = separation;
62 self
63 }
64
65 pub fn with_body_start(mut self, body_start: Length) -> Self {
68 self.body_start_offset = body_start;
69 self
70 }
71
72 pub fn body_width(&self) -> Length {
74 (self.available_width - self.body_start_offset).max(Length::from_pt(10.0))
75 }
76
77 pub fn body_start(&self) -> Length {
79 self.body_start_offset
80 }
81
82 pub fn label_end(&self) -> Length {
84 self.label_width
85 }
86
87 pub fn body_start_x(&self) -> Length {
89 self.body_start_offset
90 }
91
92 pub fn layout_item(
94 &self,
95 area_tree: &mut AreaTree,
96 y_position: Length,
97 label_content: Option<&str>,
98 body_height: Length,
99 ) -> Result<ListItemLayout> {
100 let item_height = body_height.max(Length::from_pt(12.0)); let label_id = if let Some(label_text) = label_content {
104 let label_rect = Rect::from_point_size(
105 Point::new(Length::ZERO, y_position),
106 Size::new(self.label_width, item_height),
107 );
108 let mut label_area = Area::new(AreaType::Block, label_rect);
109 label_area.content = Some(AreaContent::Text(label_text.to_string()));
110
111 Some(area_tree.add_area(label_area))
112 } else {
113 None
114 };
115
116 let body_rect = Rect::from_point_size(
118 Point::new(self.body_start_x(), y_position),
119 Size::new(self.body_width(), item_height),
120 );
121 let body_area = Area::new(AreaType::Block, body_rect);
122 let body_id = Some(area_tree.add_area(body_area));
123
124 Ok(ListItemLayout {
125 y_position,
126 height: item_height,
127 label_id,
128 body_id,
129 })
130 }
131
132 pub fn layout_list(
134 &self,
135 area_tree: &mut AreaTree,
136 items: &[(Option<String>, Length)], start_y: Length,
138 ) -> Result<Vec<ListItemLayout>> {
139 let mut layouts = Vec::new();
140 let mut current_y = start_y;
141
142 for (label, body_height) in items {
143 let layout = self.layout_item(area_tree, current_y, label.as_deref(), *body_height)?;
144
145 current_y += layout.height;
146 layouts.push(layout);
147 }
148
149 Ok(layouts)
150 }
151
152 pub fn generate_marker(&self, index: usize, style: ListMarkerStyle) -> String {
154 match style {
155 ListMarkerStyle::Disc => "•".to_string(),
156 ListMarkerStyle::Circle => "○".to_string(),
157 ListMarkerStyle::Square => "■".to_string(),
158 ListMarkerStyle::Decimal => index.to_string(),
159 ListMarkerStyle::LowerAlpha => Self::to_alpha(index, false),
160 ListMarkerStyle::UpperAlpha => Self::to_alpha(index, true),
161 ListMarkerStyle::LowerRoman => Self::to_roman(index, false),
162 ListMarkerStyle::UpperRoman => Self::to_roman(index, true),
163 ListMarkerStyle::None => String::new(),
164 }
165 }
166
167 fn to_alpha(mut n: usize, uppercase: bool) -> String {
169 if n == 0 {
170 return String::new();
171 }
172
173 let mut result = String::new();
174 while n > 0 {
175 n -= 1;
176 let ch = if uppercase {
177 (b'A' + (n % 26) as u8) as char
178 } else {
179 (b'a' + (n % 26) as u8) as char
180 };
181 result.insert(0, ch);
182 n /= 26;
183 }
184 result
185 }
186
187 fn to_roman(n: usize, uppercase: bool) -> String {
189 if n == 0 || n > 3999 {
190 return n.to_string();
191 }
192
193 let values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
194 let symbols_lower = [
195 "m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i",
196 ];
197 let symbols_upper = [
198 "M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I",
199 ];
200
201 let symbols = if uppercase {
202 symbols_upper
203 } else {
204 symbols_lower
205 };
206
207 let mut result = String::new();
208 let mut num = n;
209
210 for (i, &value) in values.iter().enumerate() {
211 while num >= value {
212 result.push_str(symbols[i]);
213 num -= value;
214 }
215 }
216
217 result
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum ListMarkerStyle {
224 Disc,
226
227 Circle,
229
230 Square,
232
233 Decimal,
235
236 LowerAlpha,
238
239 UpperAlpha,
241
242 LowerRoman,
244
245 UpperRoman,
247
248 None,
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_list_layout_creation() {
258 let layout = ListLayout::new(Length::from_pt(400.0));
259 assert_eq!(layout.available_width, Length::from_pt(400.0));
260 assert_eq!(layout.label_end(), Length::from_pt(18.0));
262 assert_eq!(layout.body_start(), Length::from_pt(24.0));
264 }
265
266 #[test]
267 fn test_body_width_calculation() {
268 let layout = ListLayout::new(Length::from_pt(400.0))
272 .with_label_width(Length::from_pt(50.0))
273 .with_label_separation(Length::from_pt(10.0))
274 .with_body_start(Length::from_pt(60.0));
275
276 assert_eq!(layout.body_width(), Length::from_pt(340.0));
277 }
278
279 #[test]
280 fn test_body_start_position() {
281 let layout = ListLayout::new(Length::from_pt(400.0))
282 .with_label_width(Length::from_pt(50.0))
283 .with_label_separation(Length::from_pt(10.0))
284 .with_body_start(Length::from_pt(60.0));
285
286 assert_eq!(layout.body_start(), Length::from_pt(60.0));
288 assert_eq!(layout.body_start_x(), Length::from_pt(60.0));
289 }
290
291 #[test]
292 fn test_layout_single_item() {
293 let layout = ListLayout::new(Length::from_pt(400.0));
294 let mut area_tree = AreaTree::new();
295
296 let item_layout = layout
297 .layout_item(
298 &mut area_tree,
299 Length::ZERO,
300 Some("1."),
301 Length::from_pt(20.0),
302 )
303 .expect("test: should succeed");
304
305 assert_eq!(item_layout.y_position, Length::ZERO);
306 assert_eq!(item_layout.height, Length::from_pt(20.0));
307 assert!(item_layout.label_id.is_some());
308 assert!(item_layout.body_id.is_some());
309 }
310
311 #[test]
312 fn test_layout_item_without_label() {
313 let layout = ListLayout::new(Length::from_pt(400.0));
314 let mut area_tree = AreaTree::new();
315
316 let item_layout = layout
317 .layout_item(&mut area_tree, Length::ZERO, None, Length::from_pt(20.0))
318 .expect("test: should succeed");
319
320 assert!(item_layout.label_id.is_none());
321 assert!(item_layout.body_id.is_some());
322 }
323
324 #[test]
325 fn test_layout_complete_list() {
326 let layout = ListLayout::new(Length::from_pt(400.0));
327 let mut area_tree = AreaTree::new();
328
329 let items = vec![
330 (Some("1.".to_string()), Length::from_pt(20.0)),
331 (Some("2.".to_string()), Length::from_pt(30.0)),
332 (Some("3.".to_string()), Length::from_pt(20.0)),
333 ];
334
335 let layouts = layout
336 .layout_list(&mut area_tree, &items, Length::ZERO)
337 .expect("test: should succeed");
338
339 assert_eq!(layouts.len(), 3);
340 assert_eq!(layouts[0].y_position, Length::ZERO);
341 assert_eq!(layouts[1].y_position, Length::from_pt(20.0));
342 assert_eq!(layouts[2].y_position, Length::from_pt(50.0));
343 }
344
345 #[test]
346 fn test_marker_disc() {
347 let layout = ListLayout::new(Length::from_pt(400.0));
348 assert_eq!(layout.generate_marker(1, ListMarkerStyle::Disc), "•");
349 }
350
351 #[test]
352 fn test_marker_decimal() {
353 let layout = ListLayout::new(Length::from_pt(400.0));
354 assert_eq!(layout.generate_marker(5, ListMarkerStyle::Decimal), "5");
355 }
356
357 #[test]
358 fn test_marker_lower_alpha() {
359 let layout = ListLayout::new(Length::from_pt(400.0));
360 assert_eq!(layout.generate_marker(1, ListMarkerStyle::LowerAlpha), "a");
361 assert_eq!(layout.generate_marker(26, ListMarkerStyle::LowerAlpha), "z");
362 assert_eq!(
363 layout.generate_marker(27, ListMarkerStyle::LowerAlpha),
364 "aa"
365 );
366 }
367
368 #[test]
369 fn test_marker_lower_roman() {
370 let layout = ListLayout::new(Length::from_pt(400.0));
371 assert_eq!(layout.generate_marker(1, ListMarkerStyle::LowerRoman), "i");
372 assert_eq!(layout.generate_marker(4, ListMarkerStyle::LowerRoman), "iv");
373 assert_eq!(
374 layout.generate_marker(1994, ListMarkerStyle::LowerRoman),
375 "mcmxciv"
376 );
377 }
378
379 #[test]
380 fn test_marker_upper_roman() {
381 let layout = ListLayout::new(Length::from_pt(400.0));
382 assert_eq!(layout.generate_marker(1, ListMarkerStyle::UpperRoman), "I");
383 assert_eq!(
384 layout.generate_marker(2023, ListMarkerStyle::UpperRoman),
385 "MMXXIII"
386 );
387 }
388
389 #[test]
390 fn test_marker_none() {
391 let layout = ListLayout::new(Length::from_pt(400.0));
392 assert_eq!(layout.generate_marker(1, ListMarkerStyle::None), "");
393 }
394 #[test]
395 fn test_roman_zero_returns_zero_string() {
396 let layout = ListLayout::new(Length::from_pt(400.0));
398 let result = layout.generate_marker(0, ListMarkerStyle::LowerRoman);
399 assert_eq!(
400 result, "0",
401 "Zero should return '0' since it has no Roman form"
402 );
403 }
404
405 #[test]
406 fn test_roman_large_over_3999_returns_decimal() {
407 let layout = ListLayout::new(Length::from_pt(400.0));
409 let result = layout.generate_marker(4000, ListMarkerStyle::LowerRoman);
410 assert_eq!(result, "4000", "Numbers > 3999 should fall back to decimal");
411 }
412
413 #[test]
414 fn test_roman_3999_max_valid() {
415 let layout = ListLayout::new(Length::from_pt(400.0));
416 let result = layout.generate_marker(3999, ListMarkerStyle::UpperRoman);
417 assert_eq!(result, "MMMCMXCIX");
418 }
419
420 #[test]
421 fn test_roman_subtractive_notation() {
422 let layout = ListLayout::new(Length::from_pt(400.0));
423 assert_eq!(layout.generate_marker(4, ListMarkerStyle::LowerRoman), "iv");
424 assert_eq!(layout.generate_marker(9, ListMarkerStyle::LowerRoman), "ix");
425 assert_eq!(
426 layout.generate_marker(40, ListMarkerStyle::LowerRoman),
427 "xl"
428 );
429 assert_eq!(
430 layout.generate_marker(90, ListMarkerStyle::LowerRoman),
431 "xc"
432 );
433 assert_eq!(
434 layout.generate_marker(400, ListMarkerStyle::LowerRoman),
435 "cd"
436 );
437 assert_eq!(
438 layout.generate_marker(900, ListMarkerStyle::LowerRoman),
439 "cm"
440 );
441 }
442
443 #[test]
444 fn test_alpha_zero_returns_empty() {
445 let layout = ListLayout::new(Length::from_pt(400.0));
447 let result = layout.generate_marker(0, ListMarkerStyle::LowerAlpha);
448 assert_eq!(result, "");
449 }
450
451 #[test]
452 fn test_alpha_overflow_27_is_aa() {
453 let layout = ListLayout::new(Length::from_pt(400.0));
454 assert_eq!(
455 layout.generate_marker(27, ListMarkerStyle::LowerAlpha),
456 "aa"
457 );
458 }
459
460 #[test]
461 fn test_alpha_overflow_52_is_az() {
462 let layout = ListLayout::new(Length::from_pt(400.0));
464 assert_eq!(
465 layout.generate_marker(52, ListMarkerStyle::LowerAlpha),
466 "az"
467 );
468 }
469
470 #[test]
471 fn test_alpha_overflow_53_is_ba() {
472 let layout = ListLayout::new(Length::from_pt(400.0));
474 assert_eq!(
475 layout.generate_marker(53, ListMarkerStyle::LowerAlpha),
476 "ba"
477 );
478 }
479
480 #[test]
481 fn test_alpha_upper_case() {
482 let layout = ListLayout::new(Length::from_pt(400.0));
483 assert_eq!(layout.generate_marker(1, ListMarkerStyle::UpperAlpha), "A");
484 assert_eq!(layout.generate_marker(26, ListMarkerStyle::UpperAlpha), "Z");
485 assert_eq!(
486 layout.generate_marker(27, ListMarkerStyle::UpperAlpha),
487 "AA"
488 );
489 }
490
491 #[test]
492 fn test_marker_circle_and_square() {
493 let layout = ListLayout::new(Length::from_pt(400.0));
494 assert_eq!(layout.generate_marker(1, ListMarkerStyle::Circle), "○");
495 assert_eq!(layout.generate_marker(1, ListMarkerStyle::Square), "■");
496 }
497}