1extern crate alloc;
24use alloc::vec::Vec;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct MultiColSpec {
29 pub count: Option<i32>,
31 pub width: Option<i32>,
33 pub gap: i32,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct Column {
40 pub x: i32,
42 pub width: i32,
44}
45
46pub fn resolve_column_count(spec: &MultiColSpec, avail_w: i32) -> i32 {
56 if avail_w <= 0 {
57 return 1;
58 }
59 let from_width = spec.width.and_then(|w| {
61 if w <= 0 {
62 return None;
63 }
64 let denom = w.saturating_add(spec.gap.max(0));
67 if denom <= 0 {
68 return None;
69 }
70 let n = (avail_w.saturating_add(spec.gap.max(0))) / denom;
71 Some(n.max(1))
72 });
73 let from_count = spec.count.map(|c| c.max(1));
74
75 match (from_count, from_width) {
76 (None, None) => 1,
77 (Some(c), None) => c,
78 (None, Some(w)) => w,
79 (Some(c), Some(w)) => c.min(w),
81 }
82}
83
84pub fn layout_columns(spec: &MultiColSpec, avail_w: i32) -> Vec<Column> {
90 let n = resolve_column_count(spec, avail_w);
91 let gap = spec.gap.max(0);
92 if n <= 1 {
93 return alloc::vec![Column {
94 x: 0,
95 width: avail_w.max(0),
96 }];
97 }
98 let total_gap = gap.saturating_mul(n - 1);
99 let usable = avail_w.saturating_sub(total_gap);
100 if usable <= 0 {
101 return (0..n)
103 .map(|i| Column {
104 x: (gap.saturating_add(0)).saturating_mul(i),
105 width: 0,
106 })
107 .collect();
108 }
109 let base = usable / n;
110 let mut cols = Vec::with_capacity(n as usize);
111 let mut x = 0;
112 for i in 0..n {
113 let w = if i == n - 1 { usable - base * (n - 1) } else { base };
115 cols.push(Column { x, width: w });
116 x = x.saturating_add(w).saturating_add(gap);
117 }
118 cols
119}
120
121pub fn parse_columns_shorthand(s: &str) -> (Option<i32>, Option<i32>) {
131 let mut width = None;
132 let mut count = None;
133 for tok in s.split_whitespace() {
134 let t = tok.trim();
135 if t.is_empty() || t.eq_ignore_ascii_case("auto") {
136 continue;
137 }
138 if let Some(px) = t.strip_suffix("px") {
139 if let Ok(v) = px.trim().parse::<f32>() {
140 if v.is_finite() && v > 0.0 {
141 width = Some(v as i32);
142 }
143 }
144 continue;
145 }
146 if let Ok(v) = t.parse::<i32>() {
148 if v > 0 {
149 count = Some(v);
150 }
151 continue;
152 }
153 }
155 (width, count)
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum ColumnFill {
161 Balance,
163 Auto,
165}
166
167pub fn parse_column_fill(s: &str) -> ColumnFill {
169 if s.trim().eq_ignore_ascii_case("auto") {
170 ColumnFill::Auto
171 } else {
172 ColumnFill::Balance
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct Placement {
179 pub col: usize,
181 pub y: i32,
183}
184
185pub fn assign_to_columns_balanced(
188 heights: &[i32],
189 n_cols: usize,
190 fill: ColumnFill,
191 max_h: Option<i32>,
192) -> Vec<Placement> {
193 let n = n_cols.max(1);
194 if heights.is_empty() {
195 return Vec::new();
196 }
197 if n == 1 {
198 let mut y = 0i32;
199 let mut out = Vec::with_capacity(heights.len());
200 for &h in heights {
201 out.push(Placement { col: 0, y });
202 y = y.saturating_add(h.max(0));
203 }
204 return out;
205 }
206
207 match fill {
208 ColumnFill::Auto => {
209 let col_limit = max_h.unwrap_or(i32::MAX);
210 let mut out = Vec::with_capacity(heights.len());
211 let mut current_col = 0usize;
212 let mut current_y = 0i32;
213
214 for &h in heights {
215 let item_h = h.max(0);
216 if current_y > 0 && current_y.saturating_add(item_h) > col_limit && current_col + 1 < n {
217 current_col += 1;
218 current_y = 0;
219 }
220 out.push(Placement {
221 col: current_col,
222 y: current_y,
223 });
224 current_y = current_y.saturating_add(item_h);
225 }
226 out
227 }
228 ColumnFill::Balance => {
229 let mut low = heights.iter().map(|&h| h.max(0)).max().unwrap_or(0);
230 let mut high: i32 = heights
231 .iter()
232 .map(|&h| h.max(0))
233 .fold(0i32, |acc, x| acc.saturating_add(x));
234 if high <= 0 {
235 return (0..heights.len()).map(|_| Placement { col: 0, y: 0 }).collect();
236 }
237
238 let can_partition = |cap: i32| -> bool {
239 let mut cols_used = 1usize;
240 let mut current_sum = 0i32;
241 for &h in heights {
242 let item_h = h.max(0);
243 if current_sum > 0 && current_sum.saturating_add(item_h) > cap {
244 cols_used += 1;
245 current_sum = item_h;
246 } else {
247 current_sum = current_sum.saturating_add(item_h);
248 }
249 }
250 cols_used <= n
251 };
252
253 let mut best_cap = high;
254 while low <= high {
255 let mid = low + (high - low) / 2;
256 if can_partition(mid) {
257 best_cap = mid;
258 high = mid - 1;
259 } else {
260 low = mid + 1;
261 }
262 }
263
264 let mut out = Vec::with_capacity(heights.len());
265 let mut current_col = 0usize;
266 let mut current_y = 0i32;
267 let total_items = heights.len();
268
269 for (i, &h) in heights.iter().enumerate() {
270 let item_h = h.max(0);
271 let remaining_items = total_items - i;
272 let remaining_cols = n - current_col;
273
274 if current_col + 1 < n {
275 let exceeds_cap = current_y > 0 && current_y.saturating_add(item_h) > best_cap;
276 let must_split_for_remaining = remaining_items <= remaining_cols && current_y > 0;
277 if exceeds_cap || must_split_for_remaining {
278 current_col += 1;
279 current_y = 0;
280 }
281 }
282
283 out.push(Placement {
284 col: current_col,
285 y: current_y,
286 });
287 current_y = current_y.saturating_add(item_h);
288 }
289 out
290 }
291 }
292}
293
294pub fn assign_to_columns(heights: &[i32], n_cols: usize) -> Vec<Placement> {
304 let n = n_cols.max(1);
305 let mut bottoms = alloc::vec![0i32; n];
306 let mut out = Vec::with_capacity(heights.len());
307 for &h in heights {
308 let mut min_i = 0usize;
310 for i in 1..n {
311 if bottoms[i] < bottoms[min_i] {
312 min_i = i;
313 }
314 }
315 out.push(Placement {
316 col: min_i,
317 y: bottoms[min_i],
318 });
319 bottoms[min_i] = bottoms[min_i].saturating_add(h.max(0));
320 }
321 out
322}
323
324pub fn column_heights_balanced(
326 heights: &[i32],
327 n_cols: usize,
328 fill: ColumnFill,
329 max_h: Option<i32>,
330) -> Vec<i32> {
331 let n = n_cols.max(1);
332 let places = assign_to_columns_balanced(heights, n, fill, max_h);
333 let mut cols_h = alloc::vec![0i32; n];
334 for (idx, p) in places.into_iter().enumerate() {
335 if p.col < n {
336 let item_h = heights.get(idx).copied().unwrap_or(0).max(0);
337 cols_h[p.col] = cols_h[p.col].max(p.y.saturating_add(item_h));
338 }
339 }
340 cols_h
341}
342
343pub fn column_heights(heights: &[i32], n_cols: usize) -> Vec<i32> {
347 let n = n_cols.max(1);
348 let mut bottoms = alloc::vec![0i32; n];
349 for &h in heights {
350 let mut min_i = 0usize;
351 for i in 1..n {
352 if bottoms[i] < bottoms[min_i] {
353 min_i = i;
354 }
355 }
356 bottoms[min_i] = bottoms[min_i].saturating_add(h.max(0));
357 }
358 bottoms
359}
360
361pub fn resolve_column_gap(gap_str: Option<&str>, avail_w: i32, font_px: f32) -> i32 {
364 let s = match gap_str {
365 Some(v) => v.trim(),
366 None => return (if font_px > 0.0 { font_px } else { 16.0 }) as i32,
367 };
368 if s.is_empty() || s.eq_ignore_ascii_case("normal") {
369 return (if font_px > 0.0 { font_px } else { 16.0 }) as i32;
370 }
371 let font = if font_px > 0.0 { font_px } else { 16.0 };
372 if let Some(pct) = s.strip_suffix('%') {
373 let v = pct.trim().parse::<f32>().unwrap_or(0.0);
374 (v * avail_w as f32 / 100.0).max(0.0) as i32
375 } else if let Some(rem) = s.strip_suffix("rem") {
376 (rem.trim().parse::<f32>().unwrap_or(0.0) * 16.0).max(0.0) as i32
377 } else if let Some(em) = s.strip_suffix("em") {
378 (em.trim().parse::<f32>().unwrap_or(0.0) * font).max(0.0) as i32
379 } else if let Some(pt) = s.strip_suffix("pt") {
380 (pt.trim().parse::<f32>().unwrap_or(0.0) * 4.0 / 3.0).max(0.0) as i32
381 } else if let Some(px) = s.strip_suffix("px") {
382 px.trim().parse::<f32>().unwrap_or(0.0).max(0.0) as i32
383 } else if let Ok(v) = s.parse::<f32>() {
384 v.max(0.0) as i32
385 } else {
386 font as i32
387 }
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392pub enum ChildKind {
393 ColumnItem { height: i32 },
394 Spanner { height: i32 },
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub struct ChildPlacement {
400 pub col: usize,
402 pub is_span: bool,
404 pub y: i32,
406}
407
408pub fn assign_children_with_spans_ex(
413 items: &[ChildKind],
414 n_cols: usize,
415 fill: ColumnFill,
416 max_h: Option<i32>,
417) -> (Vec<ChildPlacement>, i32) {
418 let mut out = Vec::with_capacity(items.len());
419 let mut y_base = 0i32;
420
421 let mut i = 0;
422 while i < items.len() {
423 match items[i] {
424 ChildKind::Spanner { height } => {
425 out.push(ChildPlacement {
426 col: 0,
427 is_span: true,
428 y: y_base,
429 });
430 y_base = y_base.saturating_add(height.max(0));
431 i += 1;
432 }
433 ChildKind::ColumnItem { .. } => {
434 let seg_start = i;
436 while i < items.len() {
437 if let ChildKind::Spanner { .. } = items[i] {
438 break;
439 }
440 i += 1;
441 }
442 let seg_heights: Vec<i32> = items[seg_start..i]
443 .iter()
444 .map(|it| match it {
445 ChildKind::ColumnItem { height } => *height,
446 _ => unreachable!(),
447 })
448 .collect();
449
450 let seg_places = assign_to_columns_balanced(&seg_heights, n_cols, fill, max_h);
451 let seg_col_heights = column_heights_balanced(&seg_heights, n_cols, fill, max_h);
452 let seg_max_h = seg_col_heights.into_iter().max().unwrap_or(0);
453
454 for p in seg_places {
455 out.push(ChildPlacement {
456 col: p.col,
457 is_span: false,
458 y: y_base.saturating_add(p.y),
459 });
460 }
461 y_base = y_base.saturating_add(seg_max_h);
462 }
463 }
464 }
465
466 (out, y_base)
467}
468
469pub fn assign_children_with_spans(
474 items: &[ChildKind],
475 n_cols: usize,
476) -> (Vec<ChildPlacement>, i32) {
477 let mut out = Vec::with_capacity(items.len());
478 let mut y_base = 0i32;
479
480 let mut i = 0;
481 while i < items.len() {
482 match items[i] {
483 ChildKind::Spanner { height } => {
484 out.push(ChildPlacement {
485 col: 0,
486 is_span: true,
487 y: y_base,
488 });
489 y_base = y_base.saturating_add(height.max(0));
490 i += 1;
491 }
492 ChildKind::ColumnItem { .. } => {
493 let seg_start = i;
495 while i < items.len() {
496 if let ChildKind::Spanner { .. } = items[i] {
497 break;
498 }
499 i += 1;
500 }
501 let seg_heights: Vec<i32> = items[seg_start..i]
502 .iter()
503 .map(|it| match it {
504 ChildKind::ColumnItem { height } => *height,
505 _ => unreachable!(),
506 })
507 .collect();
508
509 let seg_places = assign_to_columns(&seg_heights, n_cols);
510 let seg_col_heights = column_heights(&seg_heights, n_cols);
511 let seg_max_h = seg_col_heights.into_iter().max().unwrap_or(0);
512
513 for p in seg_places {
514 out.push(ChildPlacement {
515 col: p.col,
516 is_span: false,
517 y: y_base.saturating_add(p.y),
518 });
519 }
520 y_base = y_base.saturating_add(seg_max_h);
521 }
522 }
523 }
524
525 (out, y_base)
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
530pub enum RuleStyle {
531 None,
532 Solid,
533 Dashed,
534 Dotted,
535 Double,
536 Hidden,
537}
538
539impl RuleStyle {
540 pub fn parse(s: &str) -> Self {
541 match s.trim().to_ascii_lowercase().as_str() {
542 "solid" => RuleStyle::Solid,
543 "dashed" => RuleStyle::Dashed,
544 "dotted" => RuleStyle::Dotted,
545 "double" => RuleStyle::Double,
546 "hidden" => RuleStyle::Hidden,
547 _ => RuleStyle::None,
548 }
549 }
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq)]
554pub struct ColumnRuleStyle {
555 pub width: i32,
556 pub style: RuleStyle,
557 pub color: u32,
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub struct ColumnRuleRect {
563 pub x: i32,
564 pub y: i32,
565 pub width: i32,
566 pub height: i32,
567 pub color: u32,
568}
569
570fn parse_rule_width(s: &str, font_px: f32) -> Option<i32> {
572 match s.trim().to_ascii_lowercase().as_str() {
573 "thin" => Some(1),
574 "medium" => Some(3),
575 "thick" => Some(5),
576 _ => {
577 let font = if font_px > 0.0 { font_px } else { 16.0 };
578 let s = s.trim();
579 if let Some(px) = s.strip_suffix("px") {
580 px.trim().parse::<f32>().ok().filter(|f| f.is_finite() && *f >= 0.0).map(|f| f as i32)
581 } else if let Some(em) = s.strip_suffix("em") {
582 em.trim().parse::<f32>().ok().filter(|f| f.is_finite() && *f >= 0.0).map(|f| (f * font) as i32)
583 } else if let Some(rem) = s.strip_suffix("rem") {
584 rem.trim().parse::<f32>().ok().filter(|f| f.is_finite() && *f >= 0.0).map(|f| (f * 16.0) as i32)
585 } else if let Some(pt) = s.strip_suffix("pt") {
586 pt.trim().parse::<f32>().ok().filter(|f| f.is_finite() && *f >= 0.0).map(|f| (f * 4.0 / 3.0) as i32)
587 } else if let Ok(v) = s.parse::<f32>() {
588 if v.is_finite() && v >= 0.0 {
589 Some(v as i32)
590 } else {
591 None
592 }
593 } else {
594 None
595 }
596 }
597 }
598}
599
600pub fn parse_column_rule(
602 rule_shorthand: Option<&str>,
603 width_val: Option<&str>,
604 style_val: Option<&str>,
605 color_val: Option<&str>,
606 font_px: f32,
607) -> Option<ColumnRuleStyle> {
608 let mut width = None;
609 let mut style = None;
610 let mut color = None;
611
612 if let Some(sh) = rule_shorthand {
614 for tok in sh.split_whitespace() {
615 let t = tok.trim();
616 if t.is_empty() {
617 continue;
618 }
619 let st = RuleStyle::parse(t);
620 if st != RuleStyle::None {
621 style = Some(st);
622 continue;
623 }
624 if t.eq_ignore_ascii_case("none") {
625 style = Some(RuleStyle::None);
626 continue;
627 }
628 if let Some(w) = parse_rule_width(t, font_px) {
629 width = Some(w);
630 continue;
631 }
632 if let Some(c) = crate::os_lib::css::parse_color(t) {
633 color = Some(c);
634 continue;
635 }
636 }
637 }
638
639 if let Some(w_str) = width_val {
641 if let Some(w) = parse_rule_width(w_str.trim(), font_px) {
642 width = Some(w);
643 }
644 }
645 if let Some(s_str) = style_val {
646 style = Some(RuleStyle::parse(s_str.trim()));
647 }
648 if let Some(c_str) = color_val {
649 if let Some(c) = crate::os_lib::css::parse_color(c_str.trim()) {
650 color = Some(c);
651 }
652 }
653
654 let final_style = style.unwrap_or(RuleStyle::None);
655 if final_style == RuleStyle::None || final_style == RuleStyle::Hidden {
656 return None;
657 }
658 let final_width = width.unwrap_or(3); if final_width <= 0 {
660 return None;
661 }
662 let final_color = color.unwrap_or(0xFF000000); Some(ColumnRuleStyle {
665 width: final_width,
666 style: final_style,
667 color: final_color,
668 })
669}
670
671pub fn compute_column_rules(
673 cols: &[Column],
674 gap: i32,
675 y: i32,
676 height: i32,
677 rule: &ColumnRuleStyle,
678) -> Vec<ColumnRuleRect> {
679 if cols.len() <= 1 || rule.width <= 0 || height <= 0 {
680 return Vec::new();
681 }
682 if rule.style == RuleStyle::None || rule.style == RuleStyle::Hidden {
683 return Vec::new();
684 }
685 let mut out = Vec::with_capacity(cols.len() - 1);
686 for i in 0..cols.len() - 1 {
687 let left_edge = cols[i].x + cols[i].width;
688 let right_edge = cols[i + 1].x;
689 let center_x = if right_edge > left_edge {
690 (left_edge + right_edge) / 2
691 } else {
692 left_edge + gap / 2
693 };
694 let x = center_x - rule.width / 2;
695 out.push(ColumnRuleRect {
696 x,
697 y,
698 width: rule.width,
699 height,
700 color: rule.color,
701 });
702 }
703 out
704}
705
706