mirror of
https://github.com/ratatui/ratatui.git
synced 2025-09-28 13:31:14 +00:00
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:
parent
5710b7a8d9
commit
92a19cb604
@ -13,6 +13,7 @@ This is a quick summary of the sections below:
|
|||||||
- [Unreleased](#unreleased)
|
- [Unreleased](#unreleased)
|
||||||
- The `From` impls for backend types are now replaced with more specific traits
|
- The `From` impls for backend types are now replaced with more specific traits
|
||||||
- `FrameExt` trait for `unstable-widget-ref` feature
|
- `FrameExt` trait for `unstable-widget-ref` feature
|
||||||
|
- `List::highlight_symbol` now accepts `Into<Line>` instead of `&str`
|
||||||
- [v0.29.0](#v0290)
|
- [v0.29.0](#v0290)
|
||||||
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
|
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
|
||||||
- Removed public fields from `Rect` iterators
|
- Removed public fields from `Rect` iterators
|
||||||
@ -77,6 +78,13 @@ This is a quick summary of the sections below:
|
|||||||
|
|
||||||
## Unreleased (0.30.0)
|
## 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])
|
### `FrameExt` trait for `unstable-widget-ref` feature ([#1530])
|
||||||
|
|
||||||
[#1530]: https://github.com/ratatui/ratatui/pull/1530
|
[#1530]: https://github.com/ratatui/ratatui/pull/1530
|
||||||
|
@ -91,7 +91,7 @@ pub fn render_bottom_list(frame: &mut Frame, area: Rect) {
|
|||||||
let list = List::new(items)
|
let list = List::new(items)
|
||||||
.style(Color::White)
|
.style(Color::White)
|
||||||
.highlight_style(Style::new().yellow().italic())
|
.highlight_style(Style::new().yellow().italic())
|
||||||
.highlight_symbol("> ")
|
.highlight_symbol("> ".red())
|
||||||
.scroll_padding(1)
|
.scroll_padding(1)
|
||||||
.direction(ListDirection::BottomToTop)
|
.direction(ListDirection::BottomToTop)
|
||||||
.repeat_highlight_symbol(true);
|
.repeat_highlight_symbol(true);
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
//! The [`List`] widget is used to display a list of items and allows selecting one or multiple
|
//! The [`List`] widget is used to display a list of items and allows selecting one or multiple
|
||||||
//! items.
|
//! items.
|
||||||
use ratatui_core::style::{Style, Styled};
|
use ratatui_core::{
|
||||||
|
style::{Style, Styled},
|
||||||
|
text::Line,
|
||||||
|
};
|
||||||
use strum::{Display, EnumString};
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
pub use self::{item::ListItem, state::ListState};
|
pub use self::{item::ListItem, state::ListState};
|
||||||
@ -116,7 +119,7 @@ pub struct List<'a> {
|
|||||||
/// Style used to render selected item
|
/// Style used to render selected item
|
||||||
pub(crate) highlight_style: Style,
|
pub(crate) highlight_style: Style,
|
||||||
/// Symbol in front of the selected item (Shift all items to the right)
|
/// 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
|
/// Whether to repeat the highlight symbol for each line of the selected item
|
||||||
pub(crate) repeat_highlight_symbol: bool,
|
pub(crate) repeat_highlight_symbol: bool,
|
||||||
/// Decides when to allocate spacing for the selection symbol
|
/// Decides when to allocate spacing for the selection symbol
|
||||||
@ -298,8 +301,8 @@ impl<'a> List<'a> {
|
|||||||
/// let list = List::new(items).highlight_symbol(">>");
|
/// let list = List::new(items).highlight_symbol(">>");
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[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 {
|
pub fn highlight_symbol<L: Into<Line<'a>>>(mut self, highlight_symbol: L) -> Self {
|
||||||
self.highlight_symbol = Some(highlight_symbol);
|
self.highlight_symbol = Some(highlight_symbol.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use ratatui_core::{
|
use ratatui_core::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
|
text::{Line, ToLine},
|
||||||
widgets::{StatefulWidget, Widget},
|
widgets::{StatefulWidget, Widget},
|
||||||
};
|
};
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
block::BlockExt,
|
block::BlockExt,
|
||||||
@ -62,8 +62,14 @@ impl StatefulWidget for &List<'_> {
|
|||||||
state.offset = first_visible_index;
|
state.offset = first_visible_index;
|
||||||
|
|
||||||
// Get our set highlighted symbol (if one was set)
|
// Get our set highlighted symbol (if one was set)
|
||||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
let default_highlight_symbol = Line::default();
|
||||||
let blank_symbol = " ".repeat(highlight_symbol.width());
|
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 mut current_height = 0;
|
||||||
let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some());
|
let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some());
|
||||||
@ -83,12 +89,7 @@ impl StatefulWidget for &List<'_> {
|
|||||||
pos
|
pos
|
||||||
};
|
};
|
||||||
|
|
||||||
let row_area = Rect {
|
let row_area = Rect::new(x, y, list_area.width, item.height() as u16);
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width: list_area.width,
|
|
||||||
height: item.height() as u16,
|
|
||||||
};
|
|
||||||
|
|
||||||
let item_style = self.style.patch(item.style);
|
let item_style = self.style.patch(item.style);
|
||||||
buf.set_style(row_area, 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 is_selected = state.selected == Some(i);
|
||||||
|
|
||||||
let item_area = if selection_spacing {
|
let item_area = if selection_spacing {
|
||||||
let highlight_symbol_width = self.highlight_symbol.unwrap_or("").width() as u16;
|
|
||||||
Rect {
|
Rect {
|
||||||
x: row_area.x + highlight_symbol_width,
|
x: row_area.x + highlight_symbol_width,
|
||||||
width: row_area.width.saturating_sub(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);
|
Widget::render(&item.content, item_area, buf);
|
||||||
|
|
||||||
|
if is_selected {
|
||||||
|
buf.set_style(row_area, self.highlight_style);
|
||||||
|
}
|
||||||
if selection_spacing {
|
if selection_spacing {
|
||||||
for j in 0..item.content.height() {
|
for j in 0..item.content.height() {
|
||||||
// if the item is selected, we need to display the highlight symbol:
|
// if the item is selected, we need to display the highlight symbol:
|
||||||
// - either for the first line of the item only,
|
// - either for the first line of the item only,
|
||||||
// - or for each line of the item if the appropriate option is set
|
// - 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
|
highlight_symbol
|
||||||
} else {
|
} else {
|
||||||
&blank_symbol
|
&empty_symbol
|
||||||
};
|
};
|
||||||
buf.set_stringn(
|
let highlight_area = Rect::new(x, y + j as u16, highlight_symbol_width, 1);
|
||||||
x,
|
line.render(highlight_area, buf);
|
||||||
y + j as u16,
|
|
||||||
symbol,
|
|
||||||
list_area.width as usize,
|
|
||||||
item_style,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_selected {
|
|
||||||
buf.set_style(row_area, self.highlight_style);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -675,6 +669,25 @@ mod tests {
|
|||||||
assert_eq!(buffer, expected);
|
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]
|
#[test]
|
||||||
fn highlight_spacing_default_when_selected() {
|
fn highlight_spacing_default_when_selected() {
|
||||||
// when not selected
|
// when not selected
|
||||||
@ -788,19 +801,20 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn repeat_highlight_symbol() {
|
fn repeat_highlight_symbol() {
|
||||||
let list = List::new(["Item 0\nLine 2", "Item 1", "Item 2"])
|
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))
|
.highlight_style(Style::default().fg(Color::Yellow))
|
||||||
.repeat_highlight_symbol(true);
|
.repeat_highlight_symbol(true);
|
||||||
let mut state = ListState::default();
|
let mut state = ListState::default();
|
||||||
state.select(Some(0));
|
state.select(Some(0));
|
||||||
let buffer = stateful_widget(list, &mut state, 10, 5);
|
let buffer = stateful_widget(list, &mut state, 10, 5);
|
||||||
let expected = Buffer::with_lines([
|
let mut expected = Buffer::with_lines([
|
||||||
">>Item 0 ".yellow(),
|
">>Item 0 ".yellow(),
|
||||||
">>Line 2 ".yellow(),
|
">>Line 2 ".yellow(),
|
||||||
" Item 1 ".into(),
|
" Item 1 ".into(),
|
||||||
" Item 2 ".into(),
|
" Item 2 ".into(),
|
||||||
" ".into(),
|
" ".into(),
|
||||||
]);
|
]);
|
||||||
|
expected.set_style(Rect::new(0, 0, 2, 2), Style::new().red().bold());
|
||||||
assert_eq!(buffer, expected);
|
assert_eq!(buffer, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user