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 rat_reloc::RelocatableState;
41use rat_text::HasScreenCursor;
42use ratatui::buffer::Buffer;
43use ratatui::layout::{Constraint, Flex, Layout, Rect};
44use ratatui::widgets::{StatefulWidget, Widget};
45use std::marker::PhantomData;
46use std::rc::Rc;
47
48/// How to split the area for the two widgets.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum PairSplit {
51    /// Both widgets have a preferred size.
52    Fix(u16, u16),
53    /// The first widget has a preferred size.
54    /// The second gets the rest.
55    Fix1(u16),
56    /// The second widget has a preferred size.
57    /// The first gets the rest.
58    Fix2(u16),
59    /// Always split the area in the given ratio.
60    Ratio(u16, u16),
61    /// Use the given Constraints
62    Constrain(Constraint, Constraint),
63}
64
65/// Renders 2 widgets side by side.
66#[derive(Debug)]
67pub struct Paired<'a, T, U> {
68    first: T,
69    second: U,
70    split: PairSplit,
71    spacing: u16,
72    flex: Flex,
73    phantom: PhantomData<&'a ()>,
74}
75
76#[derive(Debug)]
77pub struct PairedState<'a, TS, US> {
78    pub first: &'a mut TS,
79    pub second: &'a mut US,
80}
81
82impl<T, U> Paired<'_, T, U> {
83    pub fn new(first: T, second: U) -> Self {
84        Self {
85            first,
86            second,
87            split: PairSplit::Ratio(1, 1),
88            spacing: 1,
89            flex: Default::default(),
90            phantom: Default::default(),
91        }
92    }
93
94    pub fn split(mut self, split: PairSplit) -> Self {
95        self.split = split;
96        self
97    }
98
99    pub fn spacing(mut self, spacing: u16) -> Self {
100        self.spacing = spacing;
101        self
102    }
103
104    pub fn flex(mut self, flex: Flex) -> Self {
105        self.flex = flex;
106        self
107    }
108}
109
110impl<T, U> Paired<'_, T, U> {
111    fn layout(&self, area: Rect) -> Rc<[Rect]> {
112        match self.split {
113            PairSplit::Fix(a, b) => {
114                Layout::horizontal([Constraint::Length(a), Constraint::Length(b)])
115                    .spacing(self.spacing)
116                    .flex(self.flex)
117                    .split(area) //
118            }
119            PairSplit::Fix1(a) => {
120                Layout::horizontal([Constraint::Length(a), Constraint::Fill(1)])
121                    .spacing(self.spacing)
122                    .flex(self.flex)
123                    .split(area) //
124            }
125            PairSplit::Fix2(b) => {
126                Layout::horizontal([Constraint::Fill(1), Constraint::Length(b)])
127                    .spacing(self.spacing)
128                    .flex(self.flex)
129                    .split(area) //
130            }
131            PairSplit::Ratio(a, b) => {
132                Layout::horizontal([Constraint::Fill(a), Constraint::Fill(b)])
133                    .spacing(self.spacing)
134                    .flex(self.flex)
135                    .split(area) //
136            }
137            PairSplit::Constrain(a, b) => {
138                Layout::horizontal([a, b])
139                    .spacing(self.spacing)
140                    .flex(self.flex)
141                    .split(area) //
142            }
143        }
144    }
145}
146
147impl<'a, T, U, TS, US> StatefulWidget for Paired<'a, T, U>
148where
149    T: StatefulWidget<State = TS>,
150    U: StatefulWidget<State = US>,
151    TS: 'a,
152    US: 'a,
153{
154    type State = PairedState<'a, TS, US>;
155
156    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
157        let l = self.layout(area);
158        self.first.render(l[0], buf, state.first);
159        self.second.render(l[1], buf, state.second);
160    }
161}
162
163impl<T, U> Widget for Paired<'_, T, U>
164where
165    T: Widget,
166    U: Widget,
167{
168    fn render(self, area: Rect, buf: &mut Buffer)
169    where
170        Self: Sized,
171    {
172        let l = self.layout(area);
173        self.first.render(l[0], buf);
174        self.second.render(l[1], buf);
175    }
176}
177
178impl<TS, US> HasScreenCursor for PairedState<'_, TS, US>
179where
180    TS: HasScreenCursor,
181    US: HasScreenCursor,
182{
183    fn screen_cursor(&self) -> Option<(u16, u16)> {
184        self.first.screen_cursor().or(self.second.screen_cursor())
185    }
186}
187
188impl<TS, US> RelocatableState for PairedState<'_, TS, US>
189where
190    TS: RelocatableState,
191    US: RelocatableState,
192{
193    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
194        self.first.relocate(shift, clip);
195        self.second.relocate(shift, clip);
196    }
197}
198
199impl<'a, TS, US> PairedState<'a, TS, US> {
200    pub fn new(first: &'a mut TS, second: &'a mut US) -> Self {
201        Self { first, second }
202    }
203}
204
205/// If you want to pair up a StatefulWidget and a Widget you
206/// need this adapter for the widget.
207pub struct PairedWidget<'a, T> {
208    widget: T,
209    phantom: PhantomData<&'a ()>,
210}
211
212impl<'a, T> PairedWidget<'a, T> {
213    pub fn new(widget: T) -> Self {
214        Self {
215            widget,
216            phantom: Default::default(),
217        }
218    }
219}
220
221impl<'a, T> StatefulWidget for PairedWidget<'a, T>
222where
223    T: Widget,
224{
225    type State = ();
226
227    fn render(self, area: Rect, buf: &mut Buffer, _: &mut Self::State) {
228        self.widget.render(area, buf);
229    }
230}
231
232impl<'a, T> HasScreenCursor for PairedWidget<'a, T> {
233    fn screen_cursor(&self) -> Option<(u16, u16)> {
234        None
235    }
236}