tui_realm_stdlib/components/
textarea.rs1extern crate unicode_width;
8
9use tuirealm::command::{Cmd, CmdResult, Direction, Position};
10use tuirealm::props::{
11 Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
12 TextModifiers, TextSpan,
13};
14use tuirealm::ratatui::{
15 layout::Rect,
16 widgets::{List, ListItem, ListState},
17};
18use tuirealm::{Frame, MockComponent, State};
19use unicode_width::UnicodeWidthStr;
20
21#[derive(Default)]
24pub struct TextareaStates {
25 pub list_index: usize, pub list_len: usize, }
28
29impl TextareaStates {
30 pub fn set_list_len(&mut self, len: usize) {
34 self.list_len = len;
35 self.fix_list_index();
36 }
37
38 pub fn incr_list_index(&mut self) {
42 if self.list_index + 1 < self.list_len {
44 self.list_index += 1;
45 }
46 }
47
48 pub fn decr_list_index(&mut self) {
52 if self.list_index > 0 {
54 self.list_index -= 1;
55 }
56 }
57
58 pub fn fix_list_index(&mut self) {
62 if self.list_index >= self.list_len && self.list_len > 0 {
63 self.list_index = self.list_len - 1;
64 } else if self.list_len == 0 {
65 self.list_index = 0;
66 }
67 }
68
69 pub fn list_index_at_first(&mut self) {
73 self.list_index = 0;
74 }
75
76 pub fn list_index_at_last(&mut self) {
80 if self.list_len > 0 {
81 self.list_index = self.list_len - 1;
82 } else {
83 self.list_index = 0;
84 }
85 }
86
87 fn calc_max_step_ahead(&self, max: usize) -> usize {
91 let remaining: usize = match self.list_len {
92 0 => 0,
93 len => len - 1 - self.list_index,
94 };
95 if remaining > max {
96 max
97 } else {
98 remaining
99 }
100 }
101
102 fn calc_max_step_behind(&self, max: usize) -> usize {
106 if self.list_index > max {
107 max
108 } else {
109 self.list_index
110 }
111 }
112}
113
114#[derive(Default)]
120pub struct Textarea {
121 props: Props,
122 pub states: TextareaStates,
123 hg_str: Option<String>, }
125
126impl Textarea {
127 pub fn foreground(mut self, fg: Color) -> Self {
128 self.attr(Attribute::Foreground, AttrValue::Color(fg));
129 self
130 }
131
132 pub fn background(mut self, bg: Color) -> Self {
133 self.attr(Attribute::Background, AttrValue::Color(bg));
134 self
135 }
136
137 pub fn inactive(mut self, s: Style) -> Self {
138 self.attr(Attribute::FocusStyle, AttrValue::Style(s));
139 self
140 }
141
142 pub fn modifiers(mut self, m: TextModifiers) -> Self {
143 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
144 self
145 }
146
147 pub fn borders(mut self, b: Borders) -> Self {
148 self.attr(Attribute::Borders, AttrValue::Borders(b));
149 self
150 }
151
152 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
153 self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
154 self
155 }
156
157 pub fn step(mut self, step: usize) -> Self {
158 self.attr(Attribute::ScrollStep, AttrValue::Length(step));
159 self
160 }
161
162 pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
163 self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
164 self
165 }
166
167 pub fn text_rows(mut self, rows: &[TextSpan]) -> Self {
168 self.states.set_list_len(rows.len());
169 self.attr(
170 Attribute::Text,
171 AttrValue::Payload(PropPayload::Vec(
172 rows.iter().cloned().map(PropValue::TextSpan).collect(),
173 )),
174 );
175 self
176 }
177}
178
179impl MockComponent for Textarea {
180 fn view(&mut self, render: &mut Frame, area: Rect) {
181 if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
183 self.hg_str = self
186 .props
187 .get(Attribute::HighlightedStr)
188 .map(|x| x.unwrap_string());
189 let wrap_width =
191 (area.width as usize) - self.hg_str.as_ref().map(|x| x.width()).unwrap_or(0) - 2;
192 let lines: Vec<ListItem> =
193 match self.props.get(Attribute::Text).map(|x| x.unwrap_payload()) {
194 Some(PropPayload::Vec(spans)) => spans
195 .iter()
196 .cloned()
197 .map(|x| x.unwrap_text_span())
198 .map(|x| {
199 crate::utils::wrap_spans(vec![x].as_slice(), wrap_width, &self.props)
200 })
201 .map(ListItem::new)
202 .collect(),
203 _ => Vec::new(),
204 };
205 let foreground = self
206 .props
207 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
208 .unwrap_color();
209 let background = self
210 .props
211 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
212 .unwrap_color();
213 let modifiers = self
214 .props
215 .get_or(
216 Attribute::TextProps,
217 AttrValue::TextModifiers(TextModifiers::empty()),
218 )
219 .unwrap_text_modifiers();
220 let title = self
221 .props
222 .get_or(
223 Attribute::Title,
224 AttrValue::Title((String::default(), Alignment::Center)),
225 )
226 .unwrap_title();
227 let borders = self
228 .props
229 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
230 .unwrap_borders();
231 let focus = self
232 .props
233 .get_or(Attribute::Focus, AttrValue::Flag(false))
234 .unwrap_flag();
235 let inactive_style = self
236 .props
237 .get(Attribute::FocusStyle)
238 .map(|x| x.unwrap_style());
239 let mut state: ListState = ListState::default();
240 state.select(Some(self.states.list_index));
241 let mut list = List::new(lines)
244 .block(crate::utils::get_block(
245 borders,
246 Some(title),
247 focus,
248 inactive_style,
249 ))
250 .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
251 .style(
252 Style::default()
253 .fg(foreground)
254 .bg(background)
255 .add_modifier(modifiers),
256 );
257
258 if let Some(hg_str) = &self.hg_str {
259 list = list.highlight_symbol(hg_str);
260 }
261 render.render_stateful_widget(list, area, &mut state);
262 }
263 }
264
265 fn query(&self, attr: Attribute) -> Option<AttrValue> {
266 self.props.get(attr)
267 }
268
269 fn attr(&mut self, attr: Attribute, value: AttrValue) {
270 self.props.set(attr, value);
271 self.states.set_list_len(
273 match self.props.get(Attribute::Text).map(|x| x.unwrap_payload()) {
274 Some(PropPayload::Vec(spans)) => spans.len(),
275 _ => 0,
276 },
277 );
278 self.states.fix_list_index();
279 }
280
281 fn state(&self) -> State {
282 State::None
283 }
284
285 fn perform(&mut self, cmd: Cmd) -> CmdResult {
286 match cmd {
287 Cmd::Move(Direction::Down) => {
288 self.states.incr_list_index();
289 }
290 Cmd::Move(Direction::Up) => {
291 self.states.decr_list_index();
292 }
293 Cmd::Scroll(Direction::Down) => {
294 let step = self
295 .props
296 .get_or(Attribute::ScrollStep, AttrValue::Length(8))
297 .unwrap_length();
298 let step = self.states.calc_max_step_ahead(step);
299 (0..step).for_each(|_| self.states.incr_list_index());
300 }
301 Cmd::Scroll(Direction::Up) => {
302 let step = self
303 .props
304 .get_or(Attribute::ScrollStep, AttrValue::Length(8))
305 .unwrap_length();
306 let step = self.states.calc_max_step_behind(step);
307 (0..step).for_each(|_| self.states.decr_list_index());
308 }
309 Cmd::GoTo(Position::Begin) => {
310 self.states.list_index_at_first();
311 }
312 Cmd::GoTo(Position::End) => {
313 self.states.list_index_at_last();
314 }
315 _ => {}
316 }
317 CmdResult::None
318 }
319}
320
321#[cfg(test)]
322mod tests {
323
324 use super::*;
325
326 use pretty_assertions::assert_eq;
327
328 #[test]
329 fn test_components_textarea() {
330 let mut component = Textarea::default()
332 .foreground(Color::Red)
333 .background(Color::Blue)
334 .modifiers(TextModifiers::BOLD)
335 .borders(Borders::default())
336 .highlighted_str("🚀")
337 .step(4)
338 .title("textarea", Alignment::Center)
339 .text_rows(&[TextSpan::from("welcome to "), TextSpan::from("tui-realm")]);
340 component.states.list_index += 1;
342 assert_eq!(component.states.list_index, 1);
343 component.attr(
345 Attribute::Text,
346 AttrValue::Payload(PropPayload::Vec(vec![
347 PropValue::TextSpan(TextSpan::from("welcome")),
348 PropValue::TextSpan(TextSpan::from("to")),
349 PropValue::TextSpan(TextSpan::from("tui-realm")),
350 ])),
351 );
352 assert_eq!(component.states.list_index, 1); assert_eq!(component.states.list_len, 3);
355 assert_eq!(component.state(), State::None);
357 assert_eq!(component.states.list_index, 1);
359 assert_eq!(
361 component.perform(Cmd::Move(Direction::Down)),
362 CmdResult::None
363 );
364 assert_eq!(component.states.list_index, 2);
366 assert_eq!(component.perform(Cmd::Move(Direction::Up)), CmdResult::None);
368 assert_eq!(component.states.list_index, 1);
370 assert_eq!(
372 component.perform(Cmd::Scroll(Direction::Down)),
373 CmdResult::None
374 );
375 assert_eq!(component.states.list_index, 2);
377 assert_eq!(
379 component.perform(Cmd::Scroll(Direction::Up)),
380 CmdResult::None
381 );
382 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
384 assert_eq!(component.states.list_index, 2);
385 assert_eq!(
387 component.perform(Cmd::GoTo(Position::Begin)),
388 CmdResult::None
389 );
390 assert_eq!(component.states.list_index, 0);
392 assert_eq!(component.perform(Cmd::Delete), CmdResult::None);
394 }
395}