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_core::buffer::Buffer;
10//! use ratatui_core::layout::Rect;
11//! use ratatui_core::text::Line;
12//! use ratatui_core::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_core::buffer::Buffer;
43use ratatui_core::layout::{Constraint, Flex, Layout, Rect};
44use ratatui_core::text::Span;
45use ratatui_core::widgets::{StatefulWidget, Widget};
46use std::marker::PhantomData;
47use std::rc::Rc;
48
49/// How to split the area for the two widgets.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum PairSplit {
52    /// Both widgets have a preferred size.
53    Fix(u16, u16),
54    /// The first widget has a preferred size.
55    /// The second gets the rest.
56    Fix1(u16),
57    /// The second widget has a preferred size.
58    /// The first gets the rest.
59    Fix2(u16),
60    /// Always split the area in the given ratio.
61    Ratio(u16, u16),
62    /// Use the given Constraints
63    Constrain(Constraint, Constraint),
64}
65
66/// Renders 2 widgets side by side.
67#[derive(Debug)]
68pub struct Paired<'a, T, U> {
69    first: T,
70    second: U,
71    split: PairSplit,
72    spacing: u16,
73    flex: Flex,
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
83/// New-type for Span without Widget trait.
84/// Used for [new_labeled].
85pub struct NSpan<'a> {
86    pub span: Span<'a>,
87}
88
89impl<'a, U> Paired<'a, NSpan<'a>, U> {
90    /// Create a pair of a label + a stateful widget.
91    pub fn new_labeled(label: impl Into<Span<'a>>, second: U) -> Self {
92        let label = label.into();
93        let width = label.width();
94        Self {
95            first: NSpan { span: label },
96            second,
97            split: PairSplit::Fix1(width as u16),
98            spacing: 1,
99            flex: Default::default(),
100            phantom: Default::default(),
101        }
102    }
103}
104
105impl<'a, T, U> Paired<'a, T, U> {
106    pub fn new(first: T, second: U) -> Self {
107        Self {
108            first,
109            second,
110            split: PairSplit::Ratio(1, 1),
111            spacing: 1,
112            flex: Default::default(),
113            phantom: Default::default(),
114        }
115    }
116
117    pub fn split(mut self, split: PairSplit) -> Self {
118        self.split = split;
119        self
120    }
121
122    pub fn spacing(mut self, spacing: u16) -> Self {
123        self.spacing = spacing;
124        self
125    }
126
127    pub fn flex(mut self, flex: Flex) -> Self {
128        self.flex = flex;
129        self
130    }
131}
132
133impl<T, U> Paired<'_, T, U> {
134    fn layout(&self, area: Rect) -> Rc<[Rect]> {
135        match self.split {
136            PairSplit::Fix(a, b) => {
137                Layout::horizontal([Constraint::Length(a), Constraint::Length(b)])
138                    .spacing(self.spacing)
139                    .flex(self.flex)
140                    .split(area) //
141            }
142            PairSplit::Fix1(a) => {
143                Layout::horizontal([Constraint::Length(a), Constraint::Fill(1)])
144                    .spacing(self.spacing)
145                    .flex(self.flex)
146                    .split(area) //
147            }
148            PairSplit::Fix2(b) => {
149                Layout::horizontal([Constraint::Fill(1), Constraint::Length(b)])
150                    .spacing(self.spacing)
151                    .flex(self.flex)
152                    .split(area) //
153            }
154            PairSplit::Ratio(a, b) => {
155                Layout::horizontal([Constraint::Fill(a), Constraint::Fill(b)])
156                    .spacing(self.spacing)
157                    .flex(self.flex)
158                    .split(area) //
159            }
160            PairSplit::Constrain(a, b) => {
161                Layout::horizontal([a, b])
162                    .spacing(self.spacing)
163                    .flex(self.flex)
164                    .split(area) //
165            }
166        }
167    }
168}
169
170impl<'a, U, US> StatefulWidget for Paired<'a, NSpan<'a>, U>
171where
172    U: StatefulWidget<State = US>,
173    US: 'a,
174{
175    type State = US;
176
177    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
178        let l = self.layout(area);
179        self.first.span.render(l[0], buf);
180        self.second.render(l[1], buf, state);
181    }
182}
183
184impl<'a, T, U, TS, US> StatefulWidget for Paired<'a, T, U>
185where
186    T: StatefulWidget<State = TS>,
187    U: StatefulWidget<State = US>,
188    TS: 'a,
189    US: 'a,
190{
191    type State = PairedState<'a, TS, US>;
192
193    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
194        let l = self.layout(area);
195        self.first.render(l[0], buf, state.first);
196        self.second.render(l[1], buf, state.second);
197    }
198}
199
200impl<'a, U> Widget for Paired<'a, NSpan<'a>, U>
201where
202    U: Widget,
203{
204    fn render(self, area: Rect, buf: &mut Buffer) {
205        let l = self.layout(area);
206        self.first.span.render(l[0], buf);
207        self.second.render(l[1], buf);
208    }
209}
210
211impl<T, U> Widget for Paired<'_, T, U>
212where
213    T: Widget,
214    U: Widget,
215{
216    fn render(self, area: Rect, buf: &mut Buffer)
217    where
218        Self: Sized,
219    {
220        let l = self.layout(area);
221        self.first.render(l[0], buf);
222        self.second.render(l[1], buf);
223    }
224}
225
226impl<TS, US> HasScreenCursor for PairedState<'_, TS, US>
227where
228    TS: HasScreenCursor,
229    US: HasScreenCursor,
230{
231    fn screen_cursor(&self) -> Option<(u16, u16)> {
232        self.first.screen_cursor().or(self.second.screen_cursor())
233    }
234}
235
236impl<TS, US> RelocatableState for PairedState<'_, TS, US>
237where
238    TS: RelocatableState,
239    US: RelocatableState,
240{
241    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
242        self.first.relocate(shift, clip);
243        self.second.relocate(shift, clip);
244    }
245}
246
247impl<'a, TS, US> PairedState<'a, TS, US> {
248    pub fn new(first: &'a mut TS, second: &'a mut US) -> Self {
249        Self { first, second }
250    }
251}
252
253/// If you want to pair up a StatefulWidget and a Widget you
254/// need this adapter for the widget.
255#[derive(Debug)]
256pub struct PairedWidget<'a, T> {
257    widget: T,
258    phantom: PhantomData<&'a ()>,
259}
260
261impl<'a, T> PairedWidget<'a, T> {
262    pub fn new(widget: T) -> Self {
263        Self {
264            widget,
265            phantom: Default::default(),
266        }
267    }
268}
269
270impl<'a, T> StatefulWidget for PairedWidget<'a, T>
271where
272    T: Widget,
273{
274    type State = ();
275
276    fn render(self, area: Rect, buf: &mut Buffer, _: &mut Self::State) {
277        self.widget.render(area, buf);
278    }
279}
280
281impl<'a, T> HasScreenCursor for PairedWidget<'a, T> {
282    fn screen_cursor(&self) -> Option<(u16, u16)> {
283        None
284    }
285}