diff options
Diffstat (limited to 'src/components/textinput.rs')
-rw-r--r-- | src/components/textinput.rs | 1022 |
1 files changed, 511 insertions, 511 deletions
diff --git a/src/components/textinput.rs b/src/components/textinput.rs index 0eb71031..63c94527 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -1,545 +1,545 @@ use crate::ui::Size; use crate::{ - components::{ - popup_paragraph, visibility_blocking, CommandBlocking, - CommandInfo, Component, DrawableComponent, EventState, - }, - keys::SharedKeyConfig, - strings, - ui::{self, style::SharedTheme}, + components::{ + popup_paragraph, visibility_blocking, CommandBlocking, + CommandInfo, Component, DrawableComponent, EventState, + }, + keys::SharedKeyConfig, + strings, + ui::{self, style::SharedTheme}, }; use anyhow::Result; use crossterm::event::{Event, KeyCode, KeyModifiers}; use itertools::Itertools; use std::{cell::Cell, collections::HashMap, ops::Range}; use tui::{ - backend::Backend, - layout::{Alignment, Rect}, - style::Modifier, - text::{Spans, Text}, - widgets::{Clear, Paragraph}, - Frame, + backend::Backend, + layout::{Alignment, Rect}, + style::Modifier, + text::{Spans, Text}, + widgets::{Clear, Paragraph}, + Frame, }; #[derive(PartialEq)] pub enum InputType { - Singleline, - Multiline, - Password, + Singleline, + Multiline, + Password, } /// primarily a subcomponet for user input of text (used in `CommitComponent`) pub struct TextInputComponent { - title: String, - default_msg: String, - msg: String, - visible: bool, - show_char_count: bool, - theme: SharedTheme, - key_config: SharedKeyConfig, - cursor_position: usize, - input_type: InputType, - current_area: Cell<Rect>, + title: String, + default_msg: String, + msg: String, + visible: bool, + show_char_count: bool, + theme: SharedTheme, + key_config: SharedKeyConfig, + cursor_position: usize, + input_type: InputType, + current_area: Cell<Rect>, } impl TextInputComponent { - /// - pub fn new( - theme: SharedTheme, - key_config: SharedKeyConfig, - title: &str, - default_msg: &str, - show_char_count: bool, - ) -> Self { - Self { - msg: String::new(), - visible: false, - theme, - key_config, - show_char_count, - title: title.to_string(), - default_msg: default_msg.to_string(), - cursor_position: 0, - input_type: InputType::Multiline, - current_area: Cell::new(Rect::default()), - } - } - - pub const fn with_input_type( - mut self, - input_type: InputType, - ) -> Self { - self.input_type = input_type; - self - } - - /// Clear the `msg`. - pub fn clear(&mut self) { - self.msg.clear(); - self.cursor_position = 0; - } - - /// Get the `msg`. - pub const fn get_text(&self) -> &String { - &self.msg - } - - /// screen area (last time we got drawn) - pub fn get_area(&self) -> Rect { - self.current_area.get() - } - - /// Move the cursor right one char. - fn incr_cursor(&mut self) { - if let Some(pos) = self.next_char_position() { - self.cursor_position = pos; - } - } - - /// Move the cursor left one char. - fn decr_cursor(&mut self) { - let mut index = self.cursor_position.saturating_sub(1); - while index > 0 && !self.msg.is_char_boundary(index) { - index -= 1; - } - self.cursor_position = index; - } - - /// Get the position of the next char, or, if the cursor points - /// to the last char, the `msg.len()`. - /// Returns None when the cursor is already at `msg.len()`. - fn next_char_position(&self) -> Option<usize> { - if self.cursor_position >= self.msg.len() { - return None; - } - let mut index = self.cursor_position.saturating_add(1); - while index < self.msg.len() - && !self.msg.is_char_boundary(index) - { - index += 1; - } - Some(index) - } - - fn backspace(&mut self) { - if self.cursor_position > 0 { - self.decr_cursor(); - self.msg.remove(self.cursor_position); - } - } - - /// Set the `msg`. - pub fn set_text(&mut self, msg: String) { - self.msg = msg; - self.cursor_position = 0; - } - - /// Set the `title`. - pub fn set_title(&mut self, t: String) { - self.title = t; - } - - fn get_draw_text(&self) -> Text { - let style = self.theme.text(true, false); - - let mut txt = Text::default(); - // The portion of the text before the cursor is added - // if the cursor is not at the first character. - if self.cursor_position > 0 { - let text_before_cursor = - self.get_msg(0..self.cursor_position); - let ends_in_nl = text_before_cursor.ends_with('\n'); - txt = text_append( - txt, - Text::styled(text_before_cursor, style), - ); - if ends_in_nl { - txt.lines.push(Spans::default()); - } - } - - let cursor_str = self - .next_char_position() - // if the cursor is at the end of the msg - // a whitespace is used to underline - .map_or(" ".to_owned(), |pos| { - self.get_msg(self.cursor_position..pos) - }); - - let cursor_highlighting = { - let mut h = HashMap::with_capacity(2); - h.insert("\n", "\u{21b5}\n\r"); - h.insert(" ", "\u{00B7}"); - h - }; - - if let Some(substitute) = - cursor_highlighting.get(cursor_str.as_str()) - { - txt = text_append( - txt, - Text::styled( - substitute.to_owned(), - self.theme - .text(false, false) - .add_modifier(Modifier::UNDERLINED), - ), - ); - } else { - txt = text_append( - txt, - Text::styled( - cursor_str, - style.add_modifier(Modifier::UNDERLINED), - ), - ); - } - - // The final portion of the text is added if there are - // still remaining characters. - if let Some(pos) = self.next_char_position() { - if pos < self.msg.len() { - txt = text_append( - txt, - Text::styled( - self.get_msg(pos..self.msg.len()), - style, - ), - ); - } - } - - txt - } - - fn get_msg(&self, range: Range<usize>) -> String { - match self.input_type { - InputType::Password => range.map(|_| "*").join(""), - _ => self.msg[range].to_owned(), - } - } - - fn draw_char_count<B: Backend>(&self, f: &mut Frame<B>, r: Rect) { - let count = self.msg.len(); - if count > 0 { - let w = Paragraph::new(format!("[{} chars]", count)) - .alignment(Alignment::Right); - - let mut rect = { - let mut rect = r; - rect.y += rect.height.saturating_sub(1); - rect - }; - - rect.x += 1; - rect.width = rect.width.saturating_sub(2); - rect.height = rect - .height - .saturating_sub(rect.height.saturating_sub(1)); - - f.render_widget(w, rect); - } - } + /// + pub fn new( + theme: SharedTheme, + key_config: SharedKeyConfig, + title: &str, + default_msg: &str, + show_char_count: bool, + ) -> Self { + Self { + msg: String::new(), + visible: false, + theme, + key_config, + show_char_count, + title: title.to_string(), + default_msg: default_msg.to_string(), + cursor_position: 0, + input_type: InputType::Multiline, + current_area: Cell::new(Rect::default()), + } + } + + pub const fn with_input_type( + mut self, + input_type: InputType, + ) -> Self { + self.input_type = input_type; + self + } + + /// Clear the `msg`. + pub fn clear(&mut self) { + self.msg.clear(); + self.cursor_position = 0; + } + + /// Get the `msg`. + pub const fn get_text(&self) -> &String { + &self.msg + } + + /// screen area (last time we got drawn) + pub fn get_area(&self) -> Rect { + self.current_area.get() + } + + /// Move the cursor right one char. + fn incr_cursor(&mut self) { + if let Some(pos) = self.next_char_position() { + self.cursor_position = pos; + } + } + + /// Move the cursor left one char. + fn decr_cursor(&mut self) { + let mut index = self.cursor_position.saturating_sub(1); + while index > 0 && !self.msg.is_char_boundary(index) { + index -= 1; + } + self.cursor_position = index; + } + + /// Get the position of the next char, or, if the cursor points + /// to the last char, the `msg.len()`. + /// Returns None when the cursor is already at `msg.len()`. + fn next_char_position(&self) -> Option<usize> { + if self.cursor_position >= self.msg.len() { + return None; + } + let mut index = self.cursor_position.saturating_add(1); + while index < self.msg.len() + && !self.msg.is_char_boundary(index) + { + index += 1; + } + Some(index) + } + + fn backspace(&mut self) { + if self.cursor_position > 0 { + self.decr_cursor(); + self.msg.remove(self.cursor_position); + } + } + + /// Set the `msg`. + pub fn set_text(&mut self, msg: String) { + self.msg = msg; + self.cursor_position = 0; + } + + /// Set the `title`. + pub fn set_title(&mut self, t: String) { + self.title = t; + } + + fn get_draw_text(&self) -> Text { + let style = self.theme.text(true, false); + + let mut txt = Text::default(); + // The portion of the text before the cursor is added + // if the cursor is not at the first character. + if self.cursor_position > 0 { + let text_before_cursor = + self.get_msg(0..self.cursor_position); + let ends_in_nl = text_before_cursor.ends_with('\n'); + txt = text_append( + txt, + Text::styled(text_before_cursor, style), + ); + if ends_in_nl { + txt.lines.push(Spans::default()); + } + } + + let cursor_str = self + .next_char_position() + // if the cursor is at the end of the msg + // a whitespace is used to underline + .map_or(" ".to_owned(), |pos| { + self.get_msg(self.cursor_position..pos) + }); + + let cursor_highlighting = { + let mut h = HashMap::with_capacity(2); + h.insert("\n", "\u{21b5}\n\r"); + h.insert(" ", "\u{00B7}"); + h + }; + + if let Some(substitute) = + cursor_highlighting.get(cursor_str.as_str()) + { + txt = text_append( + txt, + Text::styled( + substitute.to_owned(), + self.theme + .text(false, false) + .add_modifier(Modifier::UNDERLINED), + ), + ); + } else { + txt = text_append( + txt, + Text::styled( + cursor_str, + style.add_modifier(Modifier::UNDERLINED), + ), + ); + } + + // The final portion of the text is added if there are + // still remaining characters. + if let Some(pos) = self.next_char_position() { + if pos < self.msg.len() { + txt = text_append( + txt, + Text::styled( + self.get_msg(pos..self.msg.len()), + style, + ), + ); + } + } + + txt + } + + fn get_msg(&self, range: Range<usize>) -> String { + match self.input_type { + InputType::Password => range.map(|_| "*").join(""), + _ => self.msg[range].to_owned(), + } + } + + fn draw_char_count<B: Backend>(&self, f: &mut Frame<B>, r: Rect) { + let count = self.msg.len(); + if count > 0 { + let w = Paragraph::new(format!("[{} chars]", count)) + .alignment(Alignment::Right); + + let mut rect = { + let mut rect = r; + rect.y += rect.height.saturating_sub(1); + rect + }; + + rect.x += 1; + rect.width = rect.width.saturating_sub(2); + rect.height = rect + .height + .saturating_sub(rect.height.saturating_sub(1)); + + f.render_widget(w, rect); + } + } } // merges last line of `txt` with first of `append` so we do not generate unneeded newlines fn text_append<'a>(txt: Text<'a>, append: Text<'a>) -> Text<'a> { - let mut txt = txt; - if let Some(last_line) = txt.lines.last_mut() { - if let Some(first_line) = append.lines.first() { - last_line.0.extend(first_line.0.clone()); - } - - if append.lines.len() > 1 { - for line in 1..append.lines.len() { - let spans = append.lines[line].clone(); - txt.lines.push(spans); - } - } - } else { - txt = append; - } - txt + let mut txt = txt; + if let Some(last_line) = txt.lines.last_mut() { + if let Some(first_line) = append.lines.first() { + last_line.0.extend(first_line.0.clone()); + } + + if append.lines.len() > 1 { + for line in 1..append.lines.len() { + let spans = append.lines[line].clone(); + txt.lines.push(spans); + } + } + } else { + txt = append; + } + txt } impl DrawableComponent for TextInputComponent { - fn draw<B: Backend>( - &self, - f: &mut Frame<B>, - _rect: Rect, - ) -> Result<()> { - if self.visible { - let txt = if self.msg.is_empty() { - Text::styled( - self.default_msg.as_str(), - self.theme.text(false, false), - ) - } else { - self.get_draw_text() - }; - - let area = match self.input_type { - InputType::Multiline => { - let area = ui::centered_rect(60, 20, f.size()); - ui::rect_inside( - Size::new(10, 3), - f.size().into(), - area, - ) - } - _ => ui::centered_rect_absolute(32, 3, f.size()), - }; - - f.render_widget(Clear, area); - f.render_widget( - popup_paragraph( - self.title.as_str(), - txt, - &self.theme, - true, - ), - area, - ); - - if self.show_char_count { - self.draw_char_count(f, area); - } - - self.current_area.set(area); - } - - Ok(()) - } + fn draw<B: Backend>( + &self, + f: &mut Frame<B>, + _rect: Rect, + ) -> Result<()> { + if self.visible { + let txt = if self.msg.is_empty() { + Text::styled( + self.default_msg.as_str(), + self.theme.text(false, false), + ) + } else { + self.get_draw_text() + }; + + let area = match self.input_type { + InputType::Multiline => { + let area = ui::centered_rect(60, 20, f.size()); + ui::rect_inside( + Size::new(10, 3), + f.size().into(), + area, + ) + } + _ => ui::centered_rect_absolute(32, 3, f.size()), + }; + + f.render_widget(Clear, area); + f.render_widget( + popup_paragraph( + self.title.as_str(), + txt, + &self.theme, + true, + ), + area, + ); + + if self.show_char_count { + self.draw_char_count(f, area); + } + + self.current_area.set(area); + } + + Ok(()) + } } impl Component for TextInputComponent { - fn commands( - &self, - out: &mut Vec<CommandInfo>, - _force_all: bool, - ) -> CommandBlocking { - out.push( - CommandInfo::new( - strings::commands::close_popup(&self.key_config), - true, - self.visible, - ) - .order(1), - ); - visibility_blocking(self) - } - - fn event(&mut self, ev: Event) -> Result<EventState> { - if self.visible { - if let Event::Key(e) = ev { - if e == self.key_config.exit_popup { - self.hide(); - return Ok(EventState::Consumed); - } - - let is_ctrl = - e.modifiers.contains(KeyModifiers::CONTROL); - - match e.code { - KeyCode::Char(c) if !is_ctrl => { - self.msg.insert(self.cursor_position, c); - self.incr_cursor(); - return Ok(EventState::Consumed); - } - KeyCode::Delete => { - if self.cursor_position < self.msg.len() { - self.msg.remove(self.cursor_position); - } - return Ok(EventState::Consumed); - } - KeyCode::Backspace => { - self.backspace(); - return Ok(EventState::Consumed); - } - KeyCode::Left => { - self.decr_cursor(); - return Ok(EventState::Consumed); - } - KeyCode::Right => { - self.incr_cursor(); - return Ok(EventState::Consumed); - } - KeyCode::Home => { - self.cursor_position = 0; - return Ok(EventState::Consumed); - } - KeyCode::End => { - self.cursor_position = self.msg.len(); - return Ok(EventState::Consumed); - } - _ => (), - }; - } - } - Ok(EventState::NotConsumed) - } - - fn is_visible(&self) -> bool { - self.visible - } - - fn hide(&mut self) { - self.visible = false; - } - - fn show(&mut self) -> Result<()> { - self.visible = true; - - Ok(()) - } + fn commands( + &self, + out: &mut Vec<CommandInfo>, + _force_all: bool, + ) -> CommandBlocking { + out.push( + CommandInfo::new( + strings::commands::close_popup(&self.key_config), + true, + self.visible, + ) + .order(1), + ); + visibility_blocking(self) + } + + fn event(&mut self, ev: Event) -> Result<EventState> { + if self.visible { + if let Event::Key(e) = ev { + if e == self.key_config.exit_popup { + self.hide(); + return Ok(EventState::Consumed); + } + + let is_ctrl = + e.modifiers.contains(KeyModifiers::CONTROL); + + match e.code { + KeyCode::Char(c) if !is_ctrl => { + self.msg.insert(self.cursor_position, c); + self.incr_cursor(); + return Ok(EventState::Consumed); + } + KeyCode::Delete => { + if self.cursor_position < self.msg.len() { + self.msg.remove(self.cursor_position); + } + return Ok(EventState::Consumed); + } + KeyCode::Backspace => { + self.backspace(); + return Ok(EventState::Consumed); + } + KeyCode::Left => { + self.decr_cursor(); + return Ok(EventState::Consumed); + } + KeyCode::Right => { + self.incr_cursor(); + return Ok(EventState::Consumed); + } + KeyCode::Home => { + self.cursor_position = 0; + return Ok(EventState::Consumed); + } + KeyCode::End => { + self.cursor_position = self.msg.len(); + return Ok(EventState::Consumed); + } + _ => (), + }; + } + } + Ok(EventState::NotConsumed) + } + + fn is_visible(&self) -> bool { + self.visible + } + + fn hide(&mut self) { + self.visible = false; + } + + fn show(&mut self) -> Result<()> { + self.visible = true; + + Ok(()) + } } #[cfg(test)] mod tests { - use super::*; - use tui::{style::Style, text::Span}; - - #[test] - fn test_smoke() { - let mut comp = TextInputComponent::new( - SharedTheme::default(), - SharedKeyConfig::default(), - "", - "", - false, - ); - - comp.set_text(String::from("a\nb")); - - assert_eq!(comp.cursor_position, 0); - - comp.incr_cursor(); - assert_eq!(comp.cursor_position, 1); - - comp.decr_cursor(); - assert_eq!(comp.cursor_position, 0); - } - - #[test] - fn text_cursor_initial_position() { - let mut comp = TextInputComponent::new( - SharedTheme::default(), - SharedKeyConfig::default(), - "", - "", - false, - ); - let theme = SharedTheme::default(); - let underlined = theme - .text(true, false) - .add_modifier(Modifier::UNDERLINED); - - comp.set_text(String::from("a")); - - let txt = comp.get_draw_text(); - - assert_eq!(txt.lines[0].0.len(), 1); - assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); - assert_eq!(get_style(&txt.lines[0].0[0]), Some(&underlined)); - } - - #[test] - fn test_cursor_second_position() { - let mut comp = TextInputComponent::new( - SharedTheme::default(), - SharedKeyConfig::default(), - "", - "", - false, - ); - let theme = SharedTheme::default(); - let underlined_whitespace = theme - .text(false, false) - .add_modifier(Modifier::UNDERLINED); - - let not_underlined = Style::default(); - - comp.set_text(String::from("a")); - comp.incr_cursor(); - - let txt = comp.get_draw_text(); - - assert_eq!(txt.lines[0].0.len(), 2); - assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); - assert_eq!( - get_style(&txt.lines[0].0[0]), - Some(¬_underlined) - ); - assert_eq!(get_text(&txt.lines[0].0[1]), Some("\u{00B7}")); - assert_eq!( - get_style(&txt.lines[0].0[1]), - Some(&underlined_whitespace) - ); - } - - #[test] - fn test_visualize_newline() { - let mut comp = TextInputComponent::new( - SharedTheme::default(), - SharedKeyConfig::default(), - "", - "", - false, - ); - - let theme = SharedTheme::default(); - let underlined = theme - .text(false, false) - .add_modifier(Modifier::UNDERLINED); - - comp.set_text(String::from("a\nb")); - comp.incr_cursor(); - - let txt = comp.get_draw_text(); - - assert_eq!(txt.lines.len(), 2); - assert_eq!(txt.lines[0].0.len(), 2); - assert_eq!(txt.lines[1].0.len(), 2); - assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); - assert_eq!(get_text(&txt.lines[0].0[1]), Some("\u{21b5}")); - assert_eq!(get_style(&txt.lines[0].0[1]), Some(&underlined)); - assert_eq!(get_text(&txt.lines[1].0[0]), Some("")); - assert_eq!(get_text(&txt.lines[1].0[1]), Some("b")); - } - - #[test] - fn test_invisible_newline() { - let mut comp = TextInputComponent::new( - SharedTheme::default(), - SharedKeyConfig::default(), - "", - "", - false, - ); - - let theme = SharedTheme::default(); - let underlined = theme - .text(true, false) - .add_modifier(Modifier::UNDERLINED); - - comp.set_text(String::from("a\nb")); - - let txt = comp.get_draw_text(); - - assert_eq!(txt.lines.len(), 2); - assert_eq!(txt.lines[0].0.len(), 2); - assert_eq!(txt.lines[1].0.len(), 1); - assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); - assert_eq!(get_text(&txt.lines[0].0[1]), Some("")); - assert_eq!(get_style(&txt.lines[0].0[0]), Some(&underlined)); - assert_eq!(get_text(&txt.lines[1].0[0]), Some("b")); - } - - fn get_text<'a>(t: &'a Span) -> Option<&'a str> { - Some(&t.content) - } - - fn get_style<'a>(t: &'a Span) -> Option<&'a Style> { - Some(&t.style) - } + use super::*; + use tui::{style::Style, text::Span}; + + #[test] + fn test_smoke() { + let mut comp = TextInputComponent::new( + SharedTheme::default(), + SharedKeyConfig::default(), + "", + "", + false, + ); + + comp.set_text(String::from("a\nb")); + + assert_eq!(comp.cursor_position, 0); + + comp.incr_cursor(); + assert_eq!(comp.cursor_position, 1); + + comp.decr_cursor(); + assert_eq!(comp.cursor_position, 0); + } + + #[test] + fn text_cursor_initial_position() { + let mut comp = TextInputComponent::new( + SharedTheme::default(), + SharedKeyConfig::default(), + "", + "", + false, + ); + let theme = SharedTheme::default(); + let underlined = theme + .text(true, false) + .add_modifier(Modifier::UNDERLINED); + + comp.set_text(String::from("a")); + + let txt = comp.get_draw_text(); + + assert_eq!(txt.lines[0].0.len(), 1); + assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); + assert_eq!(get_style(&txt.lines[0].0[0]), Some(&underlined)); + } + + #[test] + fn test_cursor_second_position() { + let mut comp = TextInputComponent::new( + SharedTheme::default(), + SharedKeyConfig::default(), + "", + "", + false, + ); + let theme = SharedTheme::default(); + let underlined_whitespace = theme + .text(false, false) + .add_modifier(Modifier::UNDERLINED); + + let not_underlined = Style::default(); + + comp.set_text(String::from("a")); + comp.incr_cursor(); + + let txt = comp.get_draw_text(); + + assert_eq!(txt.lines[0].0.len(), 2); + assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); + assert_eq!( + get_style(&txt.lines[0].0[0]), + Some(¬_underlined) + ); + assert_eq!(get_text(&txt.lines[0].0[1]), Some("\u{00B7}")); + assert_eq!( + get_style(&txt.lines[0].0[1]), + Some(&underlined_whitespace) + ); + } + + #[test] + fn test_visualize_newline() { + let mut comp = TextInputComponent::new( + SharedTheme::default(), + SharedKeyConfig::default(), + "", + "", + false, + ); + + let theme = SharedTheme::default(); + let underlined = theme + .text(false, false) + .add_modifier(Modifier::UNDERLINED); + + comp.set_text(String::from("a\nb")); + comp.incr_cursor(); + + let txt = comp.get_draw_text(); + + assert_eq!(txt.lines.len(), 2); + assert_eq!(txt.lines[0].0.len(), 2); + assert_eq!(txt.lines[1].0.len(), 2); + assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); + assert_eq!(get_text(&txt.lines[0].0[1]), Some("\u{21b5}")); + assert_eq!(get_style(&txt.lines[0].0[1]), Some(&underlined)); + assert_eq!(get_text(&txt.lines[1].0[0]), Some("")); + assert_eq!(get_text(&txt.lines[1].0[1]), Some("b")); + } + + #[test] + fn test_invisible_newline() { + let mut comp = TextInputComponent::new( + SharedTheme::default(), + SharedKeyConfig::default(), + "", + "", + false, + ); + + let theme = SharedTheme::default(); + let underlined = theme + .text(true, false) + .add_modifier(Modifier::UNDERLINED); + + comp.set_text(String::from("a\nb")); + + let txt = comp.get_draw_text(); + + assert_eq!(txt.lines.len(), 2); + assert_eq!(txt.lines[0].0.len(), 2); + assert_eq!(txt.lines[1].0.len(), 1); + assert_eq!(get_text(&txt.lines[0].0[0]), Some("a")); + assert_eq!(get_text(&txt.lines[0].0[1]), Some("")); + assert_eq!(get_style(&txt.lines[0].0[0]), Some(&underlined)); + assert_eq!(get_text(&txt.lines[1].0[0]), Some("b")); + } + + fn get_text<'a>(t: &'a Span) -> Option<&'a str> { + Some(&t.content) + } + + fn get_style<'a>(t: &'a Span) -> Option<&'a Style> { + Some(&t.style) + } } |