rat_widget/
paired.rs

1//!
2//! Render two widgets in one area.
3//!
4use 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/// How to split the area for the two widgets.
15#[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/// Renders 2 widgets side by side.
24#[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}