requestty_ui/
widgets.rs

1//! A module containing the in-built widgets and types required by them
2
3use std::io;
4
5use textwrap::{core::Fragment, WordSeparator};
6
7use crate::{backend::Backend, events::KeyEvent, layout::Layout};
8
9pub use crate::char_input::CharInput;
10pub use crate::prompt::{Delimiter, Prompt};
11pub use crate::select::{List, Select};
12pub use crate::string_input::StringInput;
13pub use crate::text::Text;
14
15/// The default type for `filter_map` in [`StringInput`] and [`CharInput`]
16pub type FilterMapChar = fn(char) -> Option<char>;
17
18/// Character filter that lets every character through
19pub(crate) fn no_filter(c: char) -> Option<char> {
20    Some(c)
21}
22
23/// A trait to represent renderable objects.
24///
25/// There are 2 purposes of a widget.
26/// 1. Rendering to the screen.
27/// 2. Handling input events.
28///
29/// # Render Cycle
30///
31/// Rendering happens in a 3 step process.
32/// 1. First, the height is calculated with the [`height`] function.
33/// 2. Then, the [`render`] function is called which is where the actual drawing happens. The
34///    cursor should end at the position reflected by the layout.
35/// 3. Finally, the cursor position which the user needs should see is calculated with the
36///    [`cursor_pos`] function.
37///
38/// While it is not a guarantee that the terminal will be in raw mode, it is highly recommended that
39/// those implementing the render cycle call render while in raw mode.
40///
41/// [`height`]: Widget::height
42/// [`render`]: Widget::render
43/// [`cursor_pos`]: Widget::cursor_pos
44pub trait Widget {
45    /// Render to a given backend.
46    ///
47    /// The widget is responsible for updating the layout to reflect the space that it has used.
48    fn render<B: Backend>(&mut self, layout: &mut Layout, backend: &mut B) -> io::Result<()>;
49
50    /// The number of rows of the terminal the widget will take when rendered.
51    ///
52    /// The widget is responsible for updating the layout to reflect the space that it will use.
53    fn height(&mut self, layout: &mut Layout) -> u16;
54
55    /// The position of the cursor to be placed at after render. The returned value should be in the
56    /// form of (x, y), with (0, 0) being the top left of the screen.
57    ///
58    /// For example, if you want the cursor to be at the first character that could be printed,
59    /// `cursor_pos` would be `(layout.offset_x + layout.line_offset, layout.offset_y)`. Also see
60    /// [`Layout::offset_cursor`].
61    fn cursor_pos(&mut self, layout: Layout) -> (u16, u16);
62
63    /// Handle a key input. It should return whether key was handled.
64    fn handle_key(&mut self, key: KeyEvent) -> bool;
65}
66
67impl<T: std::ops::Deref<Target = str> + ?Sized> Widget for T {
68    /// Does not allow multi-line strings. If the string requires more than a single line, it adds
69    /// cuts it short and adds '...' to the end.
70    ///
71    /// If a multi-line string is required, use the [`Text`](crate::widgets::Text) widget.
72    fn render<B: Backend>(&mut self, layout: &mut Layout, backend: &mut B) -> io::Result<()> {
73        let max_width = layout.line_width() as usize;
74
75        layout.offset_y += 1;
76        layout.line_offset = 0;
77
78        if max_width <= 3 {
79            for _ in 0..max_width {
80                backend.write_all(b".")?;
81            }
82        } else if textwrap::core::display_width(self) > max_width {
83            let mut width = 0;
84            let mut prev_whitespace_len = 0;
85            let max_width = max_width - 3; // leave space for the '...'
86
87            for word in WordSeparator::UnicodeBreakProperties.find_words(self) {
88                width += word.width() as usize + prev_whitespace_len;
89                if width > max_width {
90                    break;
91                }
92
93                // Write out the whitespace only if the next word can also fit
94                for _ in 0..prev_whitespace_len {
95                    backend.write_all(b" ")?;
96                }
97                backend.write_all(word.as_bytes())?;
98
99                prev_whitespace_len = word.whitespace_width() as usize;
100            }
101
102            backend.write_all(b"...")?;
103        } else {
104            backend.write_all(self.as_bytes())?;
105        }
106
107        backend
108            .move_cursor_to(layout.offset_x, layout.offset_y)
109            .map_err(Into::into)
110    }
111
112    /// Does not allow multi-line strings.
113    ///
114    /// If a multi-line string is required, use the [`Text`](crate::widgets::Text) widget.
115    fn height(&mut self, layout: &mut Layout) -> u16 {
116        layout.offset_y += 1;
117        layout.line_offset = 0;
118        1
119    }
120
121    /// Returns the location of the first character
122    fn cursor_pos(&mut self, layout: Layout) -> (u16, u16) {
123        layout.offset_cursor((layout.line_offset, 0))
124    }
125
126    /// This widget does not handle any events
127    fn handle_key(&mut self, _: crate::events::KeyEvent) -> bool {
128        false
129    }
130}