1use std::collections::HashMap;
33
34use teksilo_canvas::Rect;
35
36use super::PaneBoundaries;
37use super::column::{ColumnSpec, ColumnWidth, HasColumnSpec, PinnedSide};
38
39pub(crate) struct ColumnSolver;
43
44impl ColumnSolver {
45 #[cfg(test)]
47 pub(crate) fn resolve<C: HasColumnSpec>(
48 columns: &[C],
49 available_width: f32,
50 min_width_default: f32,
51 overrides: &HashMap<String, f32>,
52 ) -> Vec<f32> {
53 let order: Vec<usize> = (0..columns.len()).collect();
54 Self::resolve_in_order(
55 columns,
56 &order,
57 available_width,
58 min_width_default,
59 overrides,
60 false,
61 )
62 }
63
64 #[cfg(test)]
66 pub(crate) fn resolve_stretch_last<C: HasColumnSpec>(
67 columns: &[C],
68 available_width: f32,
69 min_width_default: f32,
70 overrides: &HashMap<String, f32>,
71 ) -> Vec<f32> {
72 let order: Vec<usize> = (0..columns.len()).collect();
73 Self::resolve_in_order(
74 columns,
75 &order,
76 available_width,
77 min_width_default,
78 overrides,
79 true,
80 )
81 }
82
83 pub(crate) fn resolve_in_order<C: HasColumnSpec>(
91 columns: &[C],
92 display_order: &[usize],
93 available_width: f32,
94 min_width_default: f32,
95 overrides: &HashMap<String, f32>,
96 stretch_last: bool,
97 ) -> Vec<f32> {
98 if display_order.is_empty() {
99 return Vec::new();
100 }
101
102 let mut widths = vec![0.0_f32; display_order.len()];
103 let mut flex_total: f32 = 0.0;
104 let mut consumed: f32 = 0.0;
105 let last_slot = display_order.len() - 1;
106 let override_of = |slot: usize, col: &ColumnSpec| -> Option<f32> {
110 if stretch_last && slot == last_slot {
111 None
112 } else {
113 overrides.get(&col.id).copied()
114 }
115 };
116
117 for (slot, &col_idx) in display_order.iter().enumerate() {
120 let col = columns[col_idx].column_spec();
121 let floor = col.min_width.unwrap_or(min_width_default);
122 if let Some(override_w) = override_of(slot, col) {
123 let clamped = clamp(override_w, floor, col.max_width);
124 widths[slot] = clamped;
125 consumed += clamped;
126 continue;
127 }
128 match col.width {
129 ColumnWidth::Fixed(px) => {
130 let clamped = clamp(px, floor, col.max_width);
131 widths[slot] = clamped;
132 consumed += clamped;
133 }
134 ColumnWidth::Auto => {
135 let clamped = clamp(floor, floor, col.max_width);
136 widths[slot] = clamped;
137 consumed += clamped;
138 }
139 ColumnWidth::Flex(factor) => {
140 flex_total += factor.max(0.0);
141 }
142 }
143 }
144
145 let leftover = (available_width - consumed).max(0.0);
153 if flex_total > 0.0 {
154 struct FlexSlot {
155 slot: usize,
156 factor: f32,
157 floor: f32,
158 max: Option<f32>,
159 }
160 let mut pool: Vec<FlexSlot> = display_order
161 .iter()
162 .enumerate()
163 .filter_map(|(slot, &col_idx)| {
164 let col = columns[col_idx].column_spec();
165 if override_of(slot, col).is_some() {
166 return None;
167 }
168 match col.width {
169 ColumnWidth::Flex(factor) => Some(FlexSlot {
170 slot,
171 factor: factor.max(0.0),
172 floor: col.min_width.unwrap_or(min_width_default),
173 max: col.max_width,
174 }),
175 _ => None,
176 }
177 })
178 .collect();
179
180 let mut pool_leftover = leftover;
181 let mut pool_flex_total: f32 = pool.iter().map(|s| s.factor).sum();
182
183 while !pool.is_empty() {
184 if pool_flex_total <= 0.0 {
185 for slot in &pool {
189 widths[slot.slot] = slot.floor;
190 }
191 break;
192 }
193 let round_leftover = pool_leftover;
198 let round_flex_total = pool_flex_total;
199 let mut next_pool = Vec::with_capacity(pool.len());
200 let mut any_clamped = false;
201 for slot in pool {
202 let share = round_leftover * (slot.factor / round_flex_total);
203 let violates = share < slot.floor || slot.max.is_some_and(|m| share > m);
204 if violates {
205 let clamped = clamp(share, slot.floor, slot.max);
206 widths[slot.slot] = clamped;
207 pool_leftover -= clamped;
208 pool_flex_total -= slot.factor;
209 any_clamped = true;
210 } else {
211 next_pool.push(slot);
212 }
213 }
214 if !any_clamped {
215 for slot in &next_pool {
218 let share = pool_leftover * (slot.factor / pool_flex_total);
219 widths[slot.slot] = share;
220 }
221 break;
222 }
223 pool_leftover = pool_leftover.max(0.0);
224 pool = next_pool;
225 }
226 }
227
228 if stretch_last {
232 let gap = available_width - widths.iter().sum::<f32>();
233 if gap > 0.0 {
234 let col = columns[display_order[last_slot]].column_spec();
235 let floor = col.min_width.unwrap_or(min_width_default);
236 widths[last_slot] = clamp(widths[last_slot] + gap, floor, col.max_width);
237 }
238 }
239
240 widths
241 }
242
243 #[allow(dead_code)]
247 pub(crate) fn total_width(widths: &[f32]) -> f32 {
248 widths.iter().sum()
249 }
250
251 #[allow(dead_code)]
256 pub(crate) fn x_offset(widths: &[f32], i: usize) -> f32 {
257 widths.iter().take(i).sum()
258 }
259}
260
261fn clamp(value: f32, min: f32, max: Option<f32>) -> f32 {
262 let m = max.unwrap_or(f32::INFINITY);
263 value.max(min).min(m)
264}
265
266pub(crate) fn display_order<C: HasColumnSpec>(
278 columns: &[C],
279 order: &[String],
280 pinning: &HashMap<String, PinnedSide>,
281) -> (Vec<usize>, PaneBoundaries) {
282 let position: HashMap<&str, usize> = order
283 .iter()
284 .enumerate()
285 .map(|(i, id)| (id.as_str(), i))
286 .collect();
287 let mut leading: Vec<usize> = Vec::new();
288 let mut middle: Vec<usize> = Vec::new();
289 let mut trailing: Vec<usize> = Vec::new();
290 for (i, col) in columns.iter().enumerate() {
291 let spec = col.column_spec();
292 match pinning.get(&spec.id).copied().unwrap_or(spec.pinned) {
293 PinnedSide::Leading => leading.push(i),
294 PinnedSide::None => middle.push(i),
295 PinnedSide::Trailing => trailing.push(i),
296 }
297 }
298 const FALLBACK_BASE: usize = usize::MAX / 2;
301 let key = |i: usize| {
302 position
303 .get(columns[i].column_spec().id.as_str())
304 .copied()
305 .unwrap_or(FALLBACK_BASE + i)
306 };
307 leading.sort_by_key(|&i| key(i));
308 middle.sort_by_key(|&i| key(i));
309 trailing.sort_by_key(|&i| key(i));
310 let mut out = Vec::with_capacity(columns.len());
311 out.extend(leading);
312 let leading_count = out.len();
313 out.extend(middle);
314 let middle_end = out.len();
315 out.extend(trailing);
316 (out, PaneBoundaries::new(leading_count, middle_end))
317}
318
319fn sum_range(widths: &[f32], range: std::ops::Range<usize>) -> f32 {
338 let start = range.start.min(widths.len());
339 let end = range.end.min(widths.len()).max(start);
340 widths[start..end].iter().sum()
341}
342
343pub(crate) fn pane_widths(widths: &[f32], boundaries: PaneBoundaries) -> (f32, f32, f32) {
349 let leading = sum_range(widths, 0..boundaries.leading_count);
350 let middle = sum_range(widths, boundaries.leading_count..boundaries.middle_end);
351 let trailing = sum_range(widths, boundaries.middle_end..widths.len());
352 (leading, middle, trailing)
353}
354
355pub(crate) fn middle_viewport_width(
360 band_width: f32,
361 widths: &[f32],
362 boundaries: PaneBoundaries,
363) -> f32 {
364 let (leading, _, trailing) = pane_widths(widths, boundaries);
365 (band_width - leading - trailing).max(0.0)
366}
367
368pub(crate) fn max_scroll_x(band_width: f32, widths: &[f32], boundaries: PaneBoundaries) -> f32 {
371 let (_, middle_content, _) = pane_widths(widths, boundaries);
372 let viewport = middle_viewport_width(band_width, widths, boundaries);
373 (middle_content - viewport).max(0.0)
374}
375
376pub(crate) fn band_rects(
387 bounds: Rect,
388 widths: &[f32],
389 boundaries: PaneBoundaries,
390 rtl: bool,
391) -> (Rect, Rect, Rect) {
392 let (leading_w, _, trailing_w) = pane_widths(widths, boundaries);
393 let middle_w = middle_viewport_width(bounds.width, widths, boundaries);
394 if rtl {
395 let leading = Rect::new(
396 bounds.right() - leading_w,
397 bounds.y,
398 leading_w,
399 bounds.height,
400 );
401 let trailing = Rect::new(bounds.x, bounds.y, trailing_w, bounds.height);
402 let middle = Rect::new(bounds.x + trailing_w, bounds.y, middle_w, bounds.height);
403 (leading, middle, trailing)
404 } else {
405 let leading = Rect::new(bounds.x, bounds.y, leading_w, bounds.height);
406 let middle = Rect::new(bounds.x + leading_w, bounds.y, middle_w, bounds.height);
407 let trailing = Rect::new(
408 bounds.x + bounds.width - trailing_w,
409 bounds.y,
410 trailing_w,
411 bounds.height,
412 );
413 (leading, middle, trailing)
414 }
415}
416
417pub(crate) fn column_logical_x(
423 widths: &[f32],
424 boundaries: PaneBoundaries,
425 scroll_x: f32,
426 band_width: f32,
427 slot: usize,
428) -> Option<f32> {
429 if slot >= widths.len() {
430 return None;
431 }
432 if slot < boundaries.leading_count {
433 return Some(sum_range(widths, 0..slot));
434 }
435 let (leading_w, _, trailing_w) = pane_widths(widths, boundaries);
436 if slot < boundaries.middle_end {
437 let within = sum_range(widths, boundaries.leading_count..slot);
438 return Some(leading_w - scroll_x + within);
439 }
440 let within = sum_range(widths, boundaries.middle_end..slot);
441 Some(band_width - trailing_w + within)
442}
443
444pub(crate) fn insertion_slot_at_x(
451 widths: &[f32],
452 boundaries: PaneBoundaries,
453 scroll_x: f32,
454 band_width: f32,
455 x: f32,
456) -> usize {
457 let leading_end = boundaries.leading_count.min(widths.len());
458 let mut cursor = 0.0;
459 for i in 0..leading_end {
460 let w = widths[i];
461 if x < cursor + w * 0.5 {
462 return i;
463 }
464 cursor += w;
465 }
466 let (leading_w, _, trailing_w) = pane_widths(widths, boundaries);
467 let middle_end = boundaries.middle_end.min(widths.len()).max(leading_end);
468 let mut cursor = leading_w - scroll_x;
469 for i in leading_end..middle_end {
470 let w = widths[i];
471 if x < cursor + w * 0.5 {
472 return i;
473 }
474 cursor += w;
475 }
476 let mut cursor = band_width - trailing_w;
477 for i in middle_end..widths.len() {
478 let w = widths[i];
479 if x < cursor + w * 0.5 {
480 return i;
481 }
482 cursor += w;
483 }
484 widths.len()
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use crate::primitives::TextWidget;
491 use crate::table_view::column::{CellContext, Column};
492 use teksilo_i18n::lit;
493
494 fn col(id: &str, w: ColumnWidth) -> Column<&'static str> {
495 Column::<&str>::new(id, lit!("h"), |_, _: &CellContext| {
496 Box::new(TextWidget::new(lit!("x")))
497 })
498 .width(w)
499 }
500
501 #[test]
502 fn fixed_widths_pass_through() {
503 let cols = vec![
504 col("a", ColumnWidth::Fixed(80.0)),
505 col("b", ColumnWidth::Fixed(120.0)),
506 ];
507 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
508 assert_eq!(widths, vec![80.0, 120.0]);
509 }
510
511 #[test]
512 fn flex_columns_split_leftover() {
513 let cols = vec![
514 col("a", ColumnWidth::Fixed(100.0)),
515 col("b", ColumnWidth::Flex(1.0)),
516 col("c", ColumnWidth::Flex(2.0)),
517 ];
518 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
520 assert_eq!(widths[0], 100.0);
521 assert!((widths[1] - 100.0).abs() < 0.01);
522 assert!((widths[2] - 200.0).abs() < 0.01);
523 }
524
525 #[test]
526 fn flex_clamps_to_min_width() {
527 let cols = vec![
528 col("a", ColumnWidth::Fixed(380.0)),
529 col("b", ColumnWidth::Flex(1.0)).min_width(60.0),
530 ];
531 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
533 assert_eq!(widths[1], 60.0);
534 }
535
536 #[test]
537 fn flex_clamps_to_max_width() {
538 let cols = vec![col("a", ColumnWidth::Flex(1.0)).max_width(120.0)];
539 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
540 assert_eq!(widths[0], 120.0);
541 }
542
543 #[test]
544 fn fixed_clamps_to_min_when_below() {
545 let cols = vec![col("a", ColumnWidth::Fixed(10.0)).min_width(60.0)];
546 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
547 assert_eq!(widths[0], 60.0);
548 }
549
550 #[test]
551 fn fixed_clamps_to_max_when_above() {
552 let cols = vec![col("a", ColumnWidth::Fixed(500.0)).max_width(180.0)];
553 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
554 assert_eq!(widths[0], 180.0);
555 }
556
557 #[test]
558 fn auto_falls_back_to_min_default() {
559 let cols = vec![col("a", ColumnWidth::Auto)];
560 let widths = ColumnSolver::resolve(&cols, 400.0, 48.0, &HashMap::new());
561 assert_eq!(widths[0], 48.0);
562 }
563
564 #[test]
565 fn auto_with_min_uses_min() {
566 let cols = vec![col("a", ColumnWidth::Auto).min_width(100.0)];
567 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
568 assert_eq!(widths[0], 100.0);
569 }
570
571 #[test]
572 fn no_flex_no_overflow() {
573 let cols = vec![
577 col("a", ColumnWidth::Fixed(80.0)),
578 col("b", ColumnWidth::Fixed(120.0)),
579 ];
580 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
581 assert_eq!(ColumnSolver::total_width(&widths), 200.0);
582 }
583
584 #[test]
585 fn x_offset_walks_widths() {
586 let widths = vec![80.0, 120.0, 60.0];
587 assert_eq!(ColumnSolver::x_offset(&widths, 0), 0.0);
588 assert_eq!(ColumnSolver::x_offset(&widths, 1), 80.0);
589 assert_eq!(ColumnSolver::x_offset(&widths, 2), 200.0);
590 assert_eq!(ColumnSolver::x_offset(&widths, 3), 260.0);
591 }
592
593 #[test]
594 fn empty_columns_returns_empty() {
595 let cols: Vec<Column<&'static str>> = vec![];
596 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
597 assert!(widths.is_empty());
598 }
599
600 #[test]
601 fn override_pins_column_regardless_of_width_policy() {
602 let cols = vec![
603 col("a", ColumnWidth::Flex(1.0)),
604 col("b", ColumnWidth::Flex(1.0)),
605 ];
606 let mut over = HashMap::new();
607 over.insert("a".to_string(), 250.0);
608 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &over);
609 assert_eq!(widths[0], 250.0);
611 assert!((widths[1] - 150.0).abs() < 0.01, "got {}", widths[1]);
612 }
613
614 #[test]
615 fn override_clamps_to_min_max() {
616 let cols = vec![
617 col("a", ColumnWidth::Flex(1.0))
618 .min_width(80.0)
619 .max_width(200.0),
620 ];
621 let mut over = HashMap::new();
622 over.insert("a".to_string(), 5.0); let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &over);
624 assert_eq!(widths[0], 80.0);
625
626 let mut over = HashMap::new();
627 over.insert("a".to_string(), 999.0); let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &over);
629 assert_eq!(widths[0], 200.0);
630 }
631
632 #[test]
633 fn negative_leftover_keeps_min() {
634 let cols = vec![
635 col("a", ColumnWidth::Fixed(500.0)),
636 col("b", ColumnWidth::Flex(1.0)).min_width(50.0),
637 ];
638 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
640 assert_eq!(widths[1], 50.0);
641 }
642
643 #[test]
644 fn zero_flex_factor_treated_as_zero_share() {
645 let cols = vec![
646 col("a", ColumnWidth::Flex(0.0)).min_width(40.0),
647 col("b", ColumnWidth::Flex(1.0)),
648 ];
649 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
650 assert_eq!(widths[0], 40.0);
657 assert_eq!(widths[1], 360.0);
658 }
659
660 #[test]
661 fn flex_min_width_redistributes_to_siblings() {
662 let cols = vec![
663 col("fixed", ColumnWidth::Fixed(100.0)),
664 col("a", ColumnWidth::Flex(1.0)),
665 col("b", ColumnWidth::Flex(1.0)).min_width(200.0),
666 ];
667 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
676 assert_eq!(widths[0], 100.0);
677 assert_eq!(widths[1], 100.0);
678 assert_eq!(widths[2], 200.0);
679 assert_eq!(ColumnSolver::total_width(&widths), 400.0);
680 }
681
682 #[test]
683 fn flex_min_widths_that_oversubscribe_the_pane_still_overflow() {
684 let cols = vec![
688 col("a", ColumnWidth::Flex(1.0)).min_width(300.0),
689 col("b", ColumnWidth::Flex(1.0)).min_width(300.0),
690 ];
691 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
692 assert_eq!(widths[0], 300.0);
693 assert_eq!(widths[1], 300.0);
694 }
695
696 #[test]
699 fn stretch_last_hands_the_gap_to_the_last_column() {
700 let cols = vec![
701 col("a", ColumnWidth::Fixed(100.0)),
702 col("b", ColumnWidth::Fixed(100.0)),
703 ];
704 let widths = ColumnSolver::resolve_stretch_last(&cols, 400.0, 32.0, &HashMap::new());
705 assert_eq!(widths, vec![100.0, 300.0]);
706 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
708 assert_eq!(widths, vec![100.0, 100.0]);
709 }
710
711 #[test]
712 fn stretch_last_is_a_no_op_when_a_flex_column_already_fills_the_pane() {
713 let cols = vec![
714 col("a", ColumnWidth::Flex(1.0)),
715 col("b", ColumnWidth::Fixed(100.0)),
716 ];
717 let stretched = ColumnSolver::resolve_stretch_last(&cols, 400.0, 32.0, &HashMap::new());
718 let plain = ColumnSolver::resolve(&cols, 400.0, 32.0, &HashMap::new());
719 assert_eq!(stretched, plain);
720 assert_eq!(stretched, vec![300.0, 100.0]);
721 }
722
723 #[test]
724 fn stretch_last_ignores_the_last_columns_own_override() {
725 let cols = vec![
726 col("a", ColumnWidth::Fixed(100.0)),
727 col("b", ColumnWidth::Fixed(100.0)),
728 ];
729 let mut over = HashMap::new();
730 over.insert("b".to_string(), 50.0);
731 let widths = ColumnSolver::resolve_stretch_last(&cols, 400.0, 32.0, &over);
732 assert_eq!(widths, vec![100.0, 300.0]);
733 let widths = ColumnSolver::resolve(&cols, 400.0, 32.0, &over);
735 assert_eq!(widths, vec![100.0, 50.0]);
736 over.insert("a".to_string(), 150.0);
738 let widths = ColumnSolver::resolve_stretch_last(&cols, 400.0, 32.0, &over);
739 assert_eq!(widths, vec![150.0, 250.0]);
740 }
741
742 #[test]
743 fn stretch_last_stops_at_the_last_columns_max_width() {
744 let cols = vec![
745 col("a", ColumnWidth::Fixed(100.0)),
746 col("b", ColumnWidth::Fixed(100.0)).max_width(150.0),
747 ];
748 let widths = ColumnSolver::resolve_stretch_last(&cols, 400.0, 32.0, &HashMap::new());
749 assert_eq!(widths, vec![100.0, 150.0]);
750 }
751
752 #[test]
753 fn stretch_last_has_nothing_to_give_once_the_others_overflow() {
754 let cols = vec![
755 col("a", ColumnWidth::Fixed(300.0)),
756 col("b", ColumnWidth::Fixed(200.0)),
757 ];
758 let widths = ColumnSolver::resolve_stretch_last(&cols, 400.0, 32.0, &HashMap::new());
759 assert_eq!(widths, vec![300.0, 200.0]);
760 }
761
762 #[test]
763 fn pane_widths_splits_leading_middle_trailing() {
764 let widths = [50.0, 60.0, 70.0, 80.0, 90.0];
765 let b = PaneBoundaries::new(1, 4);
767 assert_eq!(pane_widths(&widths, b), (50.0, 210.0, 90.0));
768 }
769
770 #[test]
771 fn pane_widths_all_middle_when_unpinned() {
772 let widths = [50.0, 60.0, 70.0];
773 let b = PaneBoundaries::new(0, 3);
774 assert_eq!(pane_widths(&widths, b), (0.0, 180.0, 0.0));
775 }
776
777 #[test]
778 fn middle_viewport_width_is_band_minus_pinned_panes() {
779 let widths = [60.0, 100.0, 100.0, 100.0, 60.0];
780 let b = PaneBoundaries::new(1, 4);
781 assert_eq!(middle_viewport_width(400.0, &widths, b), 280.0);
783 }
784
785 #[test]
786 fn middle_viewport_width_floors_at_zero_when_pinned_panes_overflow() {
787 let widths = [300.0, 100.0, 300.0];
788 let b = PaneBoundaries::new(1, 2);
789 assert_eq!(middle_viewport_width(400.0, &widths, b), 0.0);
791 }
792
793 #[test]
794 fn max_scroll_x_is_zero_when_content_fits() {
795 let widths = [60.0, 100.0, 60.0];
796 let b = PaneBoundaries::new(1, 2);
797 assert_eq!(max_scroll_x(400.0, &widths, b), 0.0);
799 }
800
801 #[test]
802 fn max_scroll_x_clamps_after_a_pane_shrink() {
803 let widths = [60.0, 500.0, 60.0];
805 let b = PaneBoundaries::new(1, 2);
806 assert_eq!(max_scroll_x(400.0, &widths, b), 500.0 - 280.0);
807 let narrower = max_scroll_x(300.0, &widths, b);
812 assert_eq!(narrower, 500.0 - (300.0 - 120.0));
813 assert!(narrower > 0.0);
814 assert_eq!(max_scroll_x(50.0, &widths, b), 500.0);
818 }
819
820 #[test]
821 fn band_rects_ltr_places_leading_left_middle_center_trailing_right() {
822 let widths = [60.0, 200.0, 60.0];
823 let b = PaneBoundaries::new(1, 2);
824 let bounds = Rect::new(10.0, 20.0, 400.0, 30.0);
825 let (leading, middle, trailing) = band_rects(bounds, &widths, b, false);
826 assert_eq!(leading, Rect::new(10.0, 20.0, 60.0, 30.0));
827 assert_eq!(middle, Rect::new(70.0, 20.0, 280.0, 30.0));
828 assert_eq!(trailing, Rect::new(350.0, 20.0, 60.0, 30.0));
829 }
830
831 #[test]
832 fn band_rects_rtl_mirrors_leading_to_the_physical_right() {
833 let widths = [60.0, 200.0, 60.0];
834 let b = PaneBoundaries::new(1, 2);
835 let bounds = Rect::new(10.0, 20.0, 400.0, 30.0);
836 let (leading, middle, trailing) = band_rects(bounds, &widths, b, true);
837 assert_eq!(leading, Rect::new(350.0, 20.0, 60.0, 30.0));
839 assert_eq!(trailing, Rect::new(10.0, 20.0, 60.0, 30.0));
841 assert_eq!(middle, Rect::new(70.0, 20.0, 280.0, 30.0));
842 }
843
844 #[test]
845 fn column_logical_x_pinned_columns_ignore_scroll() {
846 let widths = [60.0, 80.0, 200.0, 60.0];
847 let b = PaneBoundaries::new(1, 3);
848 for scroll in [0.0, 40.0, 999.0] {
849 assert_eq!(
850 column_logical_x(&widths, b, scroll, 400.0, 0),
851 Some(0.0),
852 "leading column never moves"
853 );
854 assert_eq!(
855 column_logical_x(&widths, b, scroll, 400.0, 3),
856 Some(400.0 - 60.0),
857 "trailing column never moves"
858 );
859 }
860 }
861
862 #[test]
863 fn column_logical_x_middle_column_shifts_left_by_scroll() {
864 let widths = [60.0, 80.0, 200.0, 60.0];
865 let b = PaneBoundaries::new(1, 3);
866 assert_eq!(column_logical_x(&widths, b, 0.0, 400.0, 1), Some(60.0));
868 assert_eq!(column_logical_x(&widths, b, 25.0, 400.0, 1), Some(35.0));
869 assert_eq!(
870 column_logical_x(&widths, b, 25.0, 400.0, 2),
871 Some(60.0 - 25.0 + 80.0)
872 );
873 }
874
875 #[test]
876 fn column_logical_x_out_of_range_is_none() {
877 let widths = [60.0, 80.0];
878 let b = PaneBoundaries::new(0, 2);
879 assert_eq!(column_logical_x(&widths, b, 0.0, 400.0, 2), None);
880 }
881
882 #[test]
883 fn insertion_slot_at_x_finds_pinned_and_scrolled_columns() {
884 let widths = [60.0, 80.0, 200.0, 60.0];
885 let b = PaneBoundaries::new(1, 3);
886 assert_eq!(insertion_slot_at_x(&widths, b, 0.0, 400.0, 0.0), 0);
888 assert_eq!(insertion_slot_at_x(&widths, b, 0.0, 400.0, 10_000.0), 4);
890 assert_eq!(insertion_slot_at_x(&widths, b, 0.0, 400.0, 65.0), 1);
893 assert_eq!(insertion_slot_at_x(&widths, b, 70.0, 400.0, 65.0), 2);
898 }
899}