From 92a19cb6040dfced50ba384891ab0063a22b445d Mon Sep 17 00:00:00 2001 From: Tayfun Bocek Date: Wed, 5 Mar 2025 01:26:59 +0300 Subject: [PATCH] feat(list)!: highlight symbol styling (#1595) Allow styling for `List`'s highlight symbol This change makes it so anything that implements `Into` can be used as a highlight symbol. BREAKING CHANGE: `List::highlight_symbol` can no longer be used in const context BREAKING CHANGE: `List::highlight_symbol` accepted `&str`. Conversion methods that rely on type inference will need to be rewritten as the compiler cannot infer the type. closes: https://github.com/ratatui/ratatui/issues/1443 --------- Co-authored-by: Josh McKinney --- BREAKING-CHANGES.md | 8 ++++ ratatui-widgets/examples/list.rs | 2 +- ratatui-widgets/src/list.rs | 11 +++-- ratatui-widgets/src/list/rendering.rs | 64 ++++++++++++++++----------- 4 files changed, 55 insertions(+), 30 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 9c42d5fc..33ec0af8 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -13,6 +13,7 @@ This is a quick summary of the sections below: - [Unreleased](#unreleased) - The `From` impls for backend types are now replaced with more specific traits - `FrameExt` trait for `unstable-widget-ref` feature + - `List::highlight_symbol` now accepts `Into` instead of `&str` - [v0.29.0](#v0290) - `Sparkline::data` takes `IntoIterator` instead of `&[u64]` and is no longer const - Removed public fields from `Rect` iterators @@ -77,6 +78,13 @@ This is a quick summary of the sections below: ## Unreleased (0.30.0) +### `List::highlight_symbol` accepts `Into` ([#1595]) + +[#1595]: https://github.com/ratatui/ratatui/pull/1595 + +Previously `List::highlight_symbol` accepted `&str`. Any code that uses conversion methods will need +to be rewritten. Since `Into::into` is not const, this function cannot be called in const context. + ### `FrameExt` trait for `unstable-widget-ref` feature ([#1530]) [#1530]: https://github.com/ratatui/ratatui/pull/1530 diff --git a/ratatui-widgets/examples/list.rs b/ratatui-widgets/examples/list.rs index 8b7aea90..3d98decc 100644 --- a/ratatui-widgets/examples/list.rs +++ b/ratatui-widgets/examples/list.rs @@ -91,7 +91,7 @@ pub fn render_bottom_list(frame: &mut Frame, area: Rect) { let list = List::new(items) .style(Color::White) .highlight_style(Style::new().yellow().italic()) - .highlight_symbol("> ") + .highlight_symbol("> ".red()) .scroll_padding(1) .direction(ListDirection::BottomToTop) .repeat_highlight_symbol(true); diff --git a/ratatui-widgets/src/list.rs b/ratatui-widgets/src/list.rs index db9c75ac..6a154828 100644 --- a/ratatui-widgets/src/list.rs +++ b/ratatui-widgets/src/list.rs @@ -1,6 +1,9 @@ //! The [`List`] widget is used to display a list of items and allows selecting one or multiple //! items. -use ratatui_core::style::{Style, Styled}; +use ratatui_core::{ + style::{Style, Styled}, + text::Line, +}; use strum::{Display, EnumString}; pub use self::{item::ListItem, state::ListState}; @@ -116,7 +119,7 @@ pub struct List<'a> { /// Style used to render selected item pub(crate) highlight_style: Style, /// Symbol in front of the selected item (Shift all items to the right) - pub(crate) highlight_symbol: Option<&'a str>, + pub(crate) highlight_symbol: Option>, /// Whether to repeat the highlight symbol for each line of the selected item pub(crate) repeat_highlight_symbol: bool, /// Decides when to allocate spacing for the selection symbol @@ -298,8 +301,8 @@ impl<'a> List<'a> { /// let list = List::new(items).highlight_symbol(">>"); /// ``` #[must_use = "method moves the value of self and returns the modified value"] - pub const fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self { - self.highlight_symbol = Some(highlight_symbol); + pub fn highlight_symbol>>(mut self, highlight_symbol: L) -> Self { + self.highlight_symbol = Some(highlight_symbol.into()); self } diff --git a/ratatui-widgets/src/list/rendering.rs b/ratatui-widgets/src/list/rendering.rs index 34c86d7b..2c98fa3b 100644 --- a/ratatui-widgets/src/list/rendering.rs +++ b/ratatui-widgets/src/list/rendering.rs @@ -1,9 +1,9 @@ use ratatui_core::{ buffer::Buffer, layout::Rect, + text::{Line, ToLine}, widgets::{StatefulWidget, Widget}, }; -use unicode_width::UnicodeWidthStr; use crate::{ block::BlockExt, @@ -62,8 +62,14 @@ impl StatefulWidget for &List<'_> { state.offset = first_visible_index; // Get our set highlighted symbol (if one was set) - let highlight_symbol = self.highlight_symbol.unwrap_or(""); - let blank_symbol = " ".repeat(highlight_symbol.width()); + let default_highlight_symbol = Line::default(); + let highlight_symbol = self + .highlight_symbol + .as_ref() + .unwrap_or(&default_highlight_symbol); + let highlight_symbol_width = highlight_symbol.width() as u16; + let empty_symbol = " ".repeat(highlight_symbol_width as usize); + let empty_symbol = empty_symbol.to_line(); let mut current_height = 0; let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some()); @@ -83,12 +89,7 @@ impl StatefulWidget for &List<'_> { pos }; - let row_area = Rect { - x, - y, - width: list_area.width, - height: item.height() as u16, - }; + let row_area = Rect::new(x, y, list_area.width, item.height() as u16); let item_style = self.style.patch(item.style); buf.set_style(row_area, item_style); @@ -96,7 +97,6 @@ impl StatefulWidget for &List<'_> { let is_selected = state.selected == Some(i); let item_area = if selection_spacing { - let highlight_symbol_width = self.highlight_symbol.unwrap_or("").width() as u16; Rect { x: row_area.x + highlight_symbol_width, width: row_area.width.saturating_sub(highlight_symbol_width), @@ -107,29 +107,23 @@ impl StatefulWidget for &List<'_> { }; Widget::render(&item.content, item_area, buf); + if is_selected { + buf.set_style(row_area, self.highlight_style); + } if selection_spacing { for j in 0..item.content.height() { // if the item is selected, we need to display the highlight symbol: // - either for the first line of the item only, // - or for each line of the item if the appropriate option is set - let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) { + let line = if is_selected && (j == 0 || self.repeat_highlight_symbol) { highlight_symbol } else { - &blank_symbol + &empty_symbol }; - buf.set_stringn( - x, - y + j as u16, - symbol, - list_area.width as usize, - item_style, - ); + let highlight_area = Rect::new(x, y + j as u16, highlight_symbol_width, 1); + line.render(highlight_area, buf); } } - - if is_selected { - buf.set_style(row_area, self.highlight_style); - } } } } @@ -675,6 +669,25 @@ mod tests { assert_eq!(buffer, expected); } + #[test] + fn highlight_symbol_style_and_style() { + let list = List::new(["Item 0", "Item 1", "Item 2"]) + .highlight_symbol(Line::from(">>").red().bold()) + .highlight_style(Style::default().fg(Color::Yellow)); + let mut state = ListState::default(); + state.select(Some(1)); + let buffer = stateful_widget(list, &mut state, 10, 5); + let mut expected = Buffer::with_lines([ + " Item 0 ".into(), + ">>Item 1 ".yellow(), + " Item 2 ".into(), + " ".into(), + " ".into(), + ]); + expected.set_style(Rect::new(0, 1, 2, 1), Style::new().red().bold()); + assert_eq!(buffer, expected); + } + #[test] fn highlight_spacing_default_when_selected() { // when not selected @@ -788,19 +801,20 @@ mod tests { #[test] fn repeat_highlight_symbol() { let list = List::new(["Item 0\nLine 2", "Item 1", "Item 2"]) - .highlight_symbol(">>") + .highlight_symbol(Line::from(">>").red().bold()) .highlight_style(Style::default().fg(Color::Yellow)) .repeat_highlight_symbol(true); let mut state = ListState::default(); state.select(Some(0)); let buffer = stateful_widget(list, &mut state, 10, 5); - let expected = Buffer::with_lines([ + let mut expected = Buffer::with_lines([ ">>Item 0 ".yellow(), ">>Line 2 ".yellow(), " Item 1 ".into(), " Item 2 ".into(), " ".into(), ]); + expected.set_style(Rect::new(0, 0, 2, 2), Style::new().red().bold()); assert_eq!(buffer, expected); }