rat_widget/
paired.rs

1//!
2//! Render two widgets in one area.
3//!
4//! This is nice when you have your layout figured out and
5//! then there is the special case where you have to fit
6//! two widgets in one layout-area.
7//!
8//! ```
9//! use ratatui::buffer::Buffer;
10//! use ratatui::layout::Rect;
11//! use ratatui::text::Line;
12//! use ratatui::widgets::StatefulWidget;
13//! use rat_widget::paired::{PairSplit, Paired, PairedState, PairedWidget};
14//! use rat_widget::slider::{Slider, SliderState};
15//!
16//! let value = "2024";
17//! # let area = Rect::new(10, 10, 30, 1);
18//! # let mut buf = Buffer::empty(area);
19//! # let buf = &mut buf;
20//! # let mut slider_state = SliderState::new_range((2015u32, 2024u32), 3u32);
21//!
22//! Paired::new(
23//!     Slider::new()
24//!         .range((2015u32, 2024u32))
25//!         .step(3u32),
26//!     PairedWidget::new(Line::from(value)),
27//! )
28//! .split(PairSplit::Fix1(18))
29//! .render(area, buf, &mut PairedState::new(
30//!     &mut slider_state,
31//!     &mut ()
32//! ));
33//!
34//! ```
35//!
36//! This example also uses `PairedWidget` to convert a Widget to
37//! a StatefulWidget. Otherwise, you can only combine two Widgets
38//! or two StatefulWidgets.
39//!
40use crate::_private::NonExhaustive;
41use map_range_int::MapRange;
42use rat_reloc::RelocatableState;
43use rat_text::HasScreenCursor;
44use ratatui::buffer::Buffer;
45use ratatui::layout::Rect;
46use ratatui::widgets::{StatefulWidget, Widget};
47use std::cmp::min;
48use std::marker::PhantomData;
49
50/// How to split the area for the two widgets.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum PairSplit {
53    /// Both widgets have a preferred size.
54    /// Both start at their preferred size and get
55    /// resized with half of the remainder.  
56    Fix(u16, u16),
57    /// The first widget has a preferred size.
58    /// The second gets the rest.
59    Fix1(u16),
60    /// The second widget has a preferred size.
61    /// The first gets the rest.
62    Fix2(u16),
63    /// Always split the area in the given ratio.
64    Ratio(u16, u16),
65}
66
67/// Renders 2 widgets side by side.
68#[derive(Debug)]
69pub struct Paired<'a, T, U> {
70    first: T,
71    second: U,
72    split: PairSplit,
73    spacing: u16,
74    phantom: PhantomData<&'a ()>,
75}
76
77#[derive(Debug)]
78pub struct PairedState<'a, TS, US> {
79    pub first: &'a mut TS,
80    pub second: &'a mut US,
81
82    pub non_exhaustive: NonExhaustive,
83}
84
85impl<T, U> Paired<'_, T, U> {
86    pub fn new(first: T, second: U) -> Self {
87        Self {
88            first,
89            second,
90            split: PairSplit::Ratio(1, 1),
91            spacing: 1,
92            phantom: Default::default(),
93        }
94    }
95
96    pub fn split(mut self, split: PairSplit) -> Self {
97        self.split = split;
98        self
99    }
100
101    pub fn spacing(mut self, spacing: u16) -> Self {
102        self.spacing = spacing;
103        self
104    }
105}
106
107impl<T, U> Paired<'_, T, U> {
108    fn layout(&self, area: Rect) -> (u16, u16, u16) {
109        let mut sp = self.spacing;
110
111        match self.split {
112            PairSplit::Fix(a, b) => {
113                if a + sp + b > area.width {
114                    let rest = area.width - (a + sp + b);
115                    (a - rest / 2, sp, b - (rest - rest / 2))
116                } else {
117                    let rest = (a + sp + b) - area.width;
118                    (a + rest / 2, sp, b + (rest - rest / 2))
119                }
120            }
121            PairSplit::Fix1(a) => {
122                if a > area.width {
123                    sp = 0;
124                    (area.width, sp, 0)
125                } else {
126                    (a, sp, area.width.saturating_sub(a + sp))
127                }
128            }
129            PairSplit::Fix2(b) => {
130                if b > area.width {
131                    sp = 0;
132                    (area.width, sp, 0)
133                } else {
134                    (area.width.saturating_sub(b + sp), sp, b)
135                }
136            }
137            PairSplit::Ratio(a, b) => {
138                sp = min(sp, area.width);
139                (
140                    a.map_range_unchecked((0, a + b), (0, area.width - sp)),
141                    sp,
142                    b.map_range_unchecked((0, a + b), (0, area.width - sp)),
143                )
144            }
145        }
146    }
147}
148
149impl<'a, T, U, TS, US> StatefulWidget for Paired<'a, T, U>
150where
151    T: StatefulWidget<State = TS>,
152    U: StatefulWidget<State = US>,
153    TS: 'a,
154    US: 'a,
155{
156    type State = PairedState<'a, TS, US>;
157
158    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
159        let (a, sp, b) = self.layout(area);
160
161        let area_a = Rect::new(area.x, area.y, a, area.height);
162        let area_b = Rect::new(area.x + a + sp, area.y, b, area.height);
163
164        self.first.render(area_a, buf, state.first);
165        self.second.render(area_b, buf, state.second);
166    }
167}
168
169impl<T, U> Widget for Paired<'_, T, U>
170where
171    T: Widget,
172    U: Widget,
173{
174    fn render(self, area: Rect, buf: &mut Buffer)
175    where
176        Self: Sized,
177    {
178        let (a, sp, b) = self.layout(area);
179
180        let area_a = Rect::new(area.x, area.y, a, area.height);
181        let area_b = Rect::new(area.x + a + sp, area.y, b, area.height);
182
183        self.first.render(area_a, buf);
184        self.second.render(area_b, buf);
185    }
186}
187
188impl<TS, US> HasScreenCursor for PairedState<'_, TS, US>
189where
190    TS: HasScreenCursor,
191    US: HasScreenCursor,
192{
193    fn screen_cursor(&self) -> Option<(u16, u16)> {
194        self.first.screen_cursor().or(self.second.screen_cursor())
195    }
196}
197
198impl<TS, US> RelocatableState for PairedState<'_, TS, US>
199where
200    TS: RelocatableState,
201    US: RelocatableState,
202{
203    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
204        self.first.relocate(shift, clip);
205        self.second.relocate(shift, clip);
206    }
207}
208
209impl<'a, TS, US> PairedState<'a, TS, US> {
210    pub fn new(first: &'a mut TS, second: &'a mut US) -> Self {
211        Self {
212            first,
213            second,
214            non_exhaustive: NonExhaustive,
215        }
216    }
217}
218
219/// If you want to pair up a StatefulWidget and a Widget you
220/// need this adapter for the widget.
221pub struct PairedWidget<'a, T> {
222    widget: T,
223    phantom: PhantomData<&'a ()>,
224}
225
226impl<'a, T> PairedWidget<'a, T> {
227    pub fn new(widget: T) -> Self {
228        Self {
229            widget,
230            phantom: Default::default(),
231        }
232    }
233}
234
235impl<'a, T> StatefulWidget for PairedWidget<'a, T>
236where
237    T: Widget,
238{
239    type State = ();
240
241    fn render(self, area: Rect, buf: &mut Buffer, _: &mut Self::State) {
242        self.widget.render(area, buf);
243    }
244}
245
246impl<'a, T> HasScreenCursor for PairedWidget<'a, T> {
247    fn screen_cursor(&self) -> Option<(u16, u16)> {
248        None
249    }
250}