feat(list)!: highlight symbol styling (#1595)

Allow styling for `List`'s highlight symbol

This change makes it so anything that implements `Into<Line>` 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 <joshka@users.noreply.github.com>
This commit is contained in:
Tayfun Bocek 2025-03-05 01:26:59 +03:00 committed by GitHub
parent 5710b7a8d9
commit 92a19cb604
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 55 additions and 30 deletions

View File

@ -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<Line>` instead of `&str`
- [v0.29.0](#v0290)
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` 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<Line>` ([#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

View File

@ -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);

View File

@ -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<Line<'a>>,
/// 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<L: Into<Line<'a>>>(mut self, highlight_symbol: L) -> Self {
self.highlight_symbol = Some(highlight_symbol.into());
self
}

View File

@ -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);
}