rat_widget/pager/
form.rs

1//! Widget that helps with rendering a layout using GenericForm.
2//!
3//!
4//! ```
5//! # use ratatui::buffer::Buffer;
6//! # use ratatui::layout::{Flex, Rect};
7//! # use ratatui::text::Span;
8//! # use ratatui::widgets::{Padding, Widget, StatefulWidget};
9//! # use rat_focus::{FocusFlag, HasFocus};
10//! # use rat_text::text_input::{TextInput, TextInputState};
11//! # use rat_widget::layout::{FormLabel, FormWidget, GenericLayout, LayoutForm};
12//! use rat_widget::pager::{Form, FormState};
13//! #
14//! # struct State {
15//! #     form: FormState<FocusFlag>,
16//! #     text1: TextInputState,
17//! #     text2: TextInputState,
18//! #     text3: TextInputState,
19//! # }
20//! #
21//! # let mut state = State {form: Default::default(),text1: Default::default(),text2: Default::default(),text3: Default::default()};
22//! # let area = Rect::default();
23//! # let mut buf = Buffer::empty(Rect::default());
24//! # let buf = &mut buf;
25//!
26//! if !state.form.valid_layout(area.as_size()) {
27//!     let mut form_layout = LayoutForm::new()
28//!             .spacing(1)
29//!             .flex(Flex::Legacy)
30//!             .line_spacing(1)
31//!             .min_label(10);
32//!
33//!     form_layout.widget(
34//!         state.text1.focus(),
35//!         FormLabel::Str("Text 1"),
36//!         // single row
37//!         FormWidget::Width(22)
38//!     );
39//!     form_layout.widget(
40//!         state.text2.focus(),
41//!         FormLabel::Str("Text 2"),
42//!         // stretch to the form-width, preferred with 15, 1 row high.
43//!         FormWidget::StretchX(15, 1)
44//!     );
45//!     form_layout.widget(
46//!         state.text3.focus(),
47//!         FormLabel::Str("Text 3"),
48//!         // stretch to the form-width and fill vertically.
49//!         // preferred width is 15 3 rows high.
50//!         FormWidget::StretchXY(15, 3)
51//!     );
52//!
53//!     // calculate the layout and set it.
54//!     state.form.set_layout(form_layout.paged(area.as_size(), Padding::default()));
55//!  }
56//!
57//!  let mut form = Form::new()
58//!     .into_buffer(area, buf, &mut state.form);
59//!
60//!  form.render(state.text1.focus(),
61//!     || TextInput::new(),
62//!     &mut state.text1
63//!  );
64//!  form.render(state.text2.focus(),
65//!     || TextInput::new(),
66//!     &mut state.text2
67//!  );
68//!  form.render(state.text3.focus(),
69//!     || TextInput::new(),
70//!     &mut state.text3
71//!  );
72//!
73//! ```
74
75use crate::_private::NonExhaustive;
76use crate::layout::GenericLayout;
77use crate::pager::{Pager, PagerBuffer, PagerStyle};
78use rat_reloc::RelocatableState;
79use ratatui::buffer::Buffer;
80use ratatui::layout::{Alignment, Rect, Size};
81use ratatui::prelude::BlockExt;
82use ratatui::style::Style;
83use ratatui::widgets::{Block, StatefulWidget, Widget};
84use std::borrow::Cow;
85use std::cell::{Ref, RefCell, RefMut};
86use std::hash::Hash;
87use std::marker::PhantomData;
88use std::rc::Rc;
89
90/// A widget that helps with rendering a Form.
91/// Uses a GenericLayout for its layout information.
92/// Does no scrolling/paging whatsoever and any widgets
93/// out of view are simply not rendered.
94#[derive(Debug, Clone)]
95pub struct Form<'a, W>
96where
97    W: Eq + Hash + Clone,
98{
99    style: Style,
100    block: Option<Block<'a>>,
101    layout: Option<GenericLayout<W>>,
102    pager: Pager<W>,
103    phantom: PhantomData<&'a ()>,
104}
105
106/// Renders directly to the frame buffer.
107///
108/// * It maps your widget area from layout coordinates
109///   to screen coordinates before rendering.
110/// * It helps with cleanup of the widget state if your
111///   widget is currently invisible.
112#[derive(Debug)]
113pub struct FormBuffer<'a, W>
114where
115    W: Eq + Hash + Clone,
116{
117    pager: PagerBuffer<'a, W>,
118}
119
120/// Widget state.
121#[derive(Debug, Clone)]
122pub struct FormState<W>
123where
124    W: Eq + Hash + Clone,
125{
126    /// Page layout
127    /// __read+write__ renewed with each render.
128    pub layout: Rc<RefCell<GenericLayout<W>>>,
129
130    /// Only construct with `..Default::default()`.
131    pub non_exhaustive: NonExhaustive,
132}
133
134impl<W> Default for Form<'_, W>
135where
136    W: Eq + Hash + Clone,
137{
138    fn default() -> Self {
139        Self {
140            style: Default::default(),
141            block: Default::default(),
142            layout: Default::default(),
143            pager: Default::default(),
144            phantom: Default::default(),
145        }
146    }
147}
148
149impl<'a, W> Form<'a, W>
150where
151    W: Eq + Hash + Clone,
152{
153    /// New SinglePage.
154    pub fn new() -> Self {
155        Self::default()
156    }
157
158    /// Set the layout. If no layout is set here the layout is
159    /// taken from the state.
160    pub fn layout(mut self, layout: GenericLayout<W>) -> Self {
161        self.layout = Some(layout);
162        self
163    }
164
165    /// Base style.
166    pub fn style(mut self, style: Style) -> Self {
167        self.style = style;
168        self.block = self.block.map(|v| v.style(style));
169        self.pager = self.pager.style(style);
170        self
171    }
172
173    /// Style for text labels.
174    pub fn label_style(mut self, style: Style) -> Self {
175        self.pager = self.pager.label_style(style);
176        self
177    }
178
179    /// Alignment for text labels.
180    pub fn label_alignment(mut self, alignment: Alignment) -> Self {
181        self.pager = self.pager.label_alignment(alignment);
182        self
183    }
184
185    /// Set all styles.
186    pub fn styles(mut self, styles: PagerStyle) -> Self {
187        self.style = styles.style;
188        self.block = self.block.map(|v| v.style(styles.style));
189        self.pager = self.pager.styles(styles.clone());
190        self
191    }
192
193    /// Block
194    pub fn block(mut self, block: Block<'a>) -> Self {
195        self.block = Some(block.style(self.style));
196        self
197    }
198
199    /// Calculate the layout page size.
200    pub fn layout_size(&self, area: Rect) -> Size {
201        self.block.inner_if_some(area).as_size()
202    }
203
204    // Calculate the view area for all columns.
205    pub fn inner(&self, area: Rect) -> Rect {
206        self.block.inner_if_some(area)
207    }
208
209    /// Render the page navigation and create the SinglePagerBuffer
210    /// that will do the actual widget rendering.
211    pub fn into_buffer(
212        self,
213        area: Rect,
214        buf: &'a mut Buffer,
215        state: &'a mut FormState<W>,
216    ) -> FormBuffer<'a, W> {
217        // render border
218        self.block.render(area, buf);
219        // set layout
220        if let Some(layout) = self.layout {
221            state.layout = Rc::new(RefCell::new(layout));
222        }
223
224        FormBuffer {
225            pager: self
226                .pager //
227                .layout(state.layout.clone())
228                .page(0)
229                .into_buffer(area, Rc::new(RefCell::new(buf))),
230        }
231    }
232}
233
234impl<'a, W> FormBuffer<'a, W>
235where
236    W: Eq + Hash + Clone,
237{
238    /// Is the given area visible?
239    pub fn is_visible(&self, widget: W) -> bool {
240        if let Some(idx) = self.pager.widget_idx(widget) {
241            self.pager.is_visible(idx)
242        } else {
243            false
244        }
245    }
246
247    /// Render all blocks for the current page.
248    pub fn render_block(&mut self) {
249        self.pager.render_block()
250    }
251
252    /// Render a manual label.
253    #[inline(always)]
254    pub fn render_label<FN, WW>(&mut self, widget: W, render_fn: FN) -> bool
255    where
256        FN: FnOnce(&Option<Cow<'static, str>>) -> WW,
257        WW: Widget,
258    {
259        let Some(idx) = self.pager.widget_idx(widget) else {
260            return false;
261        };
262        self.pager.render_label(idx, render_fn)
263    }
264
265    /// Render a stateless widget and its label, if any.
266    #[inline(always)]
267    pub fn render_widget<FN, WW>(&mut self, widget: W, render_fn: FN) -> bool
268    where
269        FN: FnOnce() -> WW,
270        WW: Widget,
271    {
272        let Some(idx) = self.pager.widget_idx(widget) else {
273            return false;
274        };
275        self.pager.render_auto_label(idx);
276        self.pager.render_widget(idx, render_fn)
277    }
278
279    /// Render an optional stateful widget and its label, if any.
280    #[inline(always)]
281    pub fn render_opt<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
282    where
283        FN: FnOnce() -> Option<WW>,
284        WW: StatefulWidget<State = SS>,
285        SS: RelocatableState,
286    {
287        let Some(idx) = self.pager.widget_idx(widget) else {
288            return false;
289        };
290        self.pager.render_auto_label(idx);
291        if !self.pager.render_opt(idx, render_fn, state) {
292            self.hidden(state);
293            false
294        } else {
295            true
296        }
297    }
298
299    /// Render a stateful widget and its label, if any.
300    #[inline(always)]
301    pub fn render<FN, WW, SS>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> bool
302    where
303        FN: FnOnce() -> WW,
304        WW: StatefulWidget<State = SS>,
305        SS: RelocatableState,
306    {
307        let Some(idx) = self.pager.widget_idx(widget) else {
308            return false;
309        };
310        self.pager.render_auto_label(idx);
311        if !self.pager.render(idx, render_fn, state) {
312            self.hidden(state);
313            false
314        } else {
315            true
316        }
317    }
318
319    /// Render a stateful widget and its label, if any.
320    /// The closure can return a second value, which will be forwarded
321    /// if the widget is visible.
322    #[inline(always)]
323    #[allow(clippy::question_mark)]
324    pub fn render2<FN, WW, SS, R>(&mut self, widget: W, render_fn: FN, state: &mut SS) -> Option<R>
325    where
326        FN: FnOnce() -> (WW, R),
327        WW: StatefulWidget<State = SS>,
328        SS: RelocatableState,
329    {
330        let Some(idx) = self.pager.widget_idx(widget) else {
331            return None;
332        };
333        self.pager.render_auto_label(idx);
334        if let Some(remainder) = self.pager.render2(idx, render_fn, state) {
335            Some(remainder)
336        } else {
337            self.hidden(state);
338            None
339        }
340    }
341
342    /// Calculate the necessary shift from view to screen.
343    /// This does nothing as pager always places the widgets
344    /// in screen coordinates.
345    ///
346    /// Just to keep the api in sync with [Clipper](crate::clipper::Clipper).
347    pub fn shift(&self) -> (i16, i16) {
348        (0, 0)
349    }
350
351    /// Relocate the widget area to screen coordinates.
352    /// Returns None if the widget is not visible.
353    /// This clips the area to page_area.
354    #[allow(clippy::question_mark)]
355    pub fn locate_widget(&self, widget: W) -> Option<Rect> {
356        let Some(idx) = self.pager.widget_idx(widget) else {
357            return None;
358        };
359        self.pager.locate_widget(idx)
360    }
361
362    /// Relocate the label area to screen coordinates.
363    /// Returns None if the widget is not visible.
364    /// This clips the area to page_area.
365    #[allow(clippy::question_mark)]
366    pub fn locate_label(&self, widget: W) -> Option<Rect> {
367        let Some(idx) = self.pager.widget_idx(widget) else {
368            return None;
369        };
370        self.pager.locate_label(idx)
371    }
372
373    /// Relocate an area from layout coordinates to screen coordinates.
374    /// A result None indicates that the area is invisible.
375    ///
376    /// This will clip the area to the page_area.
377    pub fn locate_area(&self, area: Rect) -> Option<Rect> {
378        self.pager.locate_area(area)
379    }
380
381    /// Does nothing for pager.
382    /// Just to keep the api in sync with [Clipper](crate::clipper::Clipper).
383    pub fn relocate<S>(&self, _state: &mut S)
384    where
385        S: RelocatableState,
386    {
387    }
388
389    /// Clear the areas in the widget-state.
390    /// This is called by render_xx whenever a widget is invisible.
391    pub fn hidden<S>(&self, state: &mut S)
392    where
393        S: RelocatableState,
394    {
395        state.relocate((0, 0), Rect::default())
396    }
397
398    /// Get access to the buffer during rendering a page.
399    pub fn buffer<'b>(&'b mut self) -> RefMut<'b, &'a mut Buffer> {
400        self.pager.buffer()
401    }
402}
403
404impl<W> Default for FormState<W>
405where
406    W: Eq + Hash + Clone,
407{
408    fn default() -> Self {
409        Self {
410            layout: Default::default(),
411            non_exhaustive: NonExhaustive,
412        }
413    }
414}
415
416impl<W> FormState<W>
417where
418    W: Eq + Hash + Clone,
419{
420    pub fn new() -> Self {
421        Self::default()
422    }
423
424    /// Layout needs to change?
425    pub fn valid_layout(&self, size: Size) -> bool {
426        let layout = self.layout.borrow();
427        !layout.size_changed(size) && !layout.is_empty()
428    }
429
430    /// Set the layout.
431    pub fn set_layout(&mut self, layout: GenericLayout<W>) {
432        self.layout = Rc::new(RefCell::new(layout));
433    }
434
435    /// Layout.
436    pub fn layout(&self) -> Ref<'_, GenericLayout<W>> {
437        self.layout.borrow()
438    }
439
440    /// Clear the layout data and reset the page/page-count.
441    pub fn clear(&mut self) {
442        self.layout.borrow_mut().clear();
443    }
444}