1use crate::_private::NonExhaustive;
5use map_range_int::MapRange;
6use rat_reloc::RelocatableState;
7use rat_text::HasScreenCursor;
8use ratatui::buffer::Buffer;
9use ratatui::layout::Rect;
10use ratatui::widgets::{StatefulWidget, Widget};
11use std::cmp::min;
12use std::marker::PhantomData;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum PairSplit {
17 Fix(u16, u16),
18 Fix1(u16),
19 Fix2(u16),
20 Ratio(u16, u16),
21}
22
23#[derive(Debug)]
25pub struct Paired<'a, T, U> {
26 first: T,
27 second: U,
28 split: PairSplit,
29 spacing: u16,
30 phantom: PhantomData<&'a ()>,
31}
32
33#[derive(Debug)]
34pub struct PairedState<'a, TS, US> {
35 pub first: &'a mut TS,
36 pub second: &'a mut US,
37
38 pub non_exhaustive: NonExhaustive,
39}
40
41impl<T, U> Paired<'_, T, U> {
42 pub fn new(first: T, second: U) -> Self {
43 Self {
44 first,
45 second,
46 split: PairSplit::Ratio(1, 1),
47 spacing: 1,
48 phantom: Default::default(),
49 }
50 }
51
52 pub fn split(mut self, split: PairSplit) -> Self {
53 self.split = split;
54 self
55 }
56
57 pub fn spacing(mut self, spacing: u16) -> Self {
58 self.spacing = spacing;
59 self
60 }
61}
62
63impl<T, U> Paired<'_, T, U> {
64 fn layout(&self, area: Rect) -> (u16, u16, u16) {
65 let mut sp = self.spacing;
66
67 match self.split {
68 PairSplit::Fix(a, b) => {
69 if a + sp + b > area.width {
70 let rest = area.width - (a + sp + b);
71 (a - rest / 2, sp, b - (rest - rest / 2))
72 } else {
73 let rest = (a + sp + b) - area.width;
74 (a + rest / 2, sp, b + (rest - rest / 2))
75 }
76 }
77 PairSplit::Fix1(a) => {
78 if a > area.width {
79 sp = 0;
80 (area.width, sp, 0)
81 } else {
82 (a, sp, area.width.saturating_sub(a + sp))
83 }
84 }
85 PairSplit::Fix2(b) => {
86 if b > area.width {
87 sp = 0;
88 (area.width, sp, 0)
89 } else {
90 (area.width.saturating_sub(b + sp), sp, b)
91 }
92 }
93 PairSplit::Ratio(a, b) => {
94 sp = min(sp, area.width);
95 (
96 a.map_range_unchecked((0, a + b), (0, area.width - sp)),
97 sp,
98 b.map_range_unchecked((0, a + b), (0, area.width - sp)),
99 )
100 }
101 }
102 }
103}
104
105impl<'a, T, U, TS, US> StatefulWidget for Paired<'a, T, U>
106where
107 T: StatefulWidget<State = TS>,
108 U: StatefulWidget<State = US>,
109 TS: 'a,
110 US: 'a,
111{
112 type State = PairedState<'a, TS, US>;
113
114 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
115 let (a, sp, b) = self.layout(area);
116
117 let area_a = Rect::new(area.x, area.y, a, area.height);
118 let area_b = Rect::new(area.x + a + sp, area.y, b, area.height);
119
120 self.first.render(area_a, buf, state.first);
121 self.second.render(area_b, buf, state.second);
122 }
123}
124
125impl<T, U> Widget for Paired<'_, T, U>
126where
127 T: Widget,
128 U: Widget,
129{
130 fn render(self, area: Rect, buf: &mut Buffer)
131 where
132 Self: Sized,
133 {
134 let (a, sp, b) = self.layout(area);
135
136 let area_a = Rect::new(area.x, area.y, a, area.height);
137 let area_b = Rect::new(area.x + a + sp, area.y, b, area.height);
138
139 self.first.render(area_a, buf);
140 self.second.render(area_b, buf);
141 }
142}
143
144impl<TS, US> HasScreenCursor for PairedState<'_, TS, US>
145where
146 TS: HasScreenCursor,
147 US: HasScreenCursor,
148{
149 fn screen_cursor(&self) -> Option<(u16, u16)> {
150 self.first.screen_cursor().or(self.second.screen_cursor())
151 }
152}
153
154impl<TS, US> RelocatableState for PairedState<'_, TS, US>
155where
156 TS: RelocatableState,
157 US: RelocatableState,
158{
159 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
160 self.first.relocate(shift, clip);
161 self.second.relocate(shift, clip);
162 }
163}
164
165impl<'a, TS, US> PairedState<'a, TS, US> {
166 pub fn new(first: &'a mut TS, second: &'a mut US) -> Self {
167 Self {
168 first,
169 second,
170 non_exhaustive: NonExhaustive,
171 }
172 }
173}