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}