use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::Style;
use ratatui_core::widgets::Widget;
use super::{ArrowLayout, ScrollBar, ScrollBarOrientation};
use crate::metrics::{CellFill, ScrollMetrics};
use crate::ScrollLengths;
impl Widget for &ScrollBar {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_inner(area, buf);
}
}
impl ScrollBar {
fn render_inner(&self, area: Rect, buf: &mut Buffer) {
if area.width == 0 || area.height == 0 {
return;
}
let layout = self.arrow_layout(area);
self.render_arrows(&layout, buf);
if layout.track_area.width == 0 || layout.track_area.height == 0 {
return;
}
match self.orientation {
ScrollBarOrientation::Vertical => {
self.render_vertical_track(layout.track_area, buf);
}
ScrollBarOrientation::Horizontal => {
self.render_horizontal_track(layout.track_area, buf);
}
}
}
fn render_arrows(&self, layout: &ArrowLayout, buf: &mut Buffer) {
let arrow_style = self.arrow_style.unwrap_or(self.track_style);
if let Some((x, y)) = layout.start {
let glyph = match self.orientation {
ScrollBarOrientation::Vertical => self.glyph_set.arrow_vertical_start,
ScrollBarOrientation::Horizontal => self.glyph_set.arrow_horizontal_start,
};
let cell = &mut buf[(x, y)];
cell.set_char(glyph);
cell.set_style(arrow_style);
}
if let Some((x, y)) = layout.end {
let glyph = match self.orientation {
ScrollBarOrientation::Vertical => self.glyph_set.arrow_vertical_end,
ScrollBarOrientation::Horizontal => self.glyph_set.arrow_horizontal_end,
};
let cell = &mut buf[(x, y)];
cell.set_char(glyph);
cell.set_style(arrow_style);
}
}
fn render_vertical_track(&self, area: Rect, buf: &mut Buffer) {
let metrics = ScrollMetrics::new(
ScrollLengths {
content_len: self.content_len,
viewport_len: self.viewport_len,
},
self.offset,
area.height,
);
let x = area.x;
for (idx, y) in (area.y..area.y.saturating_add(area.height)).enumerate() {
let (glyph, style) = self.glyph_for_vertical(metrics.cell_fill(idx));
let cell = &mut buf[(x, y)];
cell.set_char(glyph);
cell.set_style(style);
}
}
fn render_horizontal_track(&self, area: Rect, buf: &mut Buffer) {
let metrics = ScrollMetrics::new(
ScrollLengths {
content_len: self.content_len,
viewport_len: self.viewport_len,
},
self.offset,
area.width,
);
let y = area.y;
for (idx, x) in (area.x..area.x.saturating_add(area.width)).enumerate() {
let (glyph, style) = self.glyph_for_horizontal(metrics.cell_fill(idx));
let cell = &mut buf[(x, y)];
cell.set_char(glyph);
cell.set_style(style);
}
}
fn glyph_for_vertical(&self, fill: CellFill) -> (char, Style) {
match fill {
CellFill::Empty => (self.glyph_set.track_vertical, self.track_style),
CellFill::Full => (self.glyph_set.thumb_vertical_lower[7], self.thumb_style),
CellFill::Partial { start, len } => {
let index = len.saturating_sub(1) as usize;
let glyph = if start == 0 {
self.glyph_set.thumb_vertical_upper[index]
} else {
self.glyph_set.thumb_vertical_lower[index]
};
(glyph, self.thumb_style)
}
}
}
fn glyph_for_horizontal(&self, fill: CellFill) -> (char, Style) {
match fill {
CellFill::Empty => (self.glyph_set.track_horizontal, self.track_style),
CellFill::Full => (self.glyph_set.thumb_horizontal_left[7], self.thumb_style),
CellFill::Partial { start, len } => {
let index = len.saturating_sub(1) as usize;
let glyph = if start == 0 {
self.glyph_set.thumb_horizontal_left[index]
} else {
self.glyph_set.thumb_horizontal_right[index]
};
(glyph, self.thumb_style)
}
}
}
}
#[cfg(test)]
mod tests {
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use super::*;
use crate::{GlyphSet, ScrollBarArrows, ScrollLengths};
fn assert_horizontal_thumb_walk(
glyph_set: GlyphSet,
track_char: char,
expected_lines: [&str; 9],
) {
let lengths = ScrollLengths {
content_len: 8 * crate::SUBCELL,
viewport_len: 2 * crate::SUBCELL,
};
for (offset, expected_line) in expected_lines.into_iter().enumerate() {
let scrollbar = ScrollBar::horizontal(lengths)
.arrows(ScrollBarArrows::None)
.glyph_set(glyph_set.clone())
.offset(offset);
let mut buf = Buffer::empty(Rect::new(0, 0, 8, 1));
(&scrollbar).render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec![expected_line]);
expected.set_style(expected.area, scrollbar.track_style);
for (x, symbol) in expected_line.chars().enumerate() {
if symbol != track_char {
expected[(x as u16, 0)].set_style(scrollbar.thumb_style);
}
}
assert_eq!(buf, expected);
}
}
#[test]
fn render_vertical_fractional_thumb() {
let scrollbar = ScrollBar::vertical(ScrollLengths {
content_len: 10,
viewport_len: 3,
})
.arrows(ScrollBarArrows::None)
.offset(1);
let mut buf = Buffer::empty(Rect::new(0, 0, 1, 4));
(&scrollbar).render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec!["▅", "▀", " ", " "]);
expected.set_style(expected.area, scrollbar.track_style);
expected[(0, 0)].set_style(scrollbar.thumb_style);
expected[(0, 1)].set_style(scrollbar.thumb_style);
assert_eq!(buf, expected);
}
#[test]
fn render_horizontal_fractional_thumb() {
let scrollbar = ScrollBar::horizontal(ScrollLengths {
content_len: 10,
viewport_len: 3,
})
.arrows(ScrollBarArrows::None)
.offset(1);
let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
(&scrollbar).render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec!["🮉▌ "]);
expected.set_style(expected.area, scrollbar.track_style);
expected[(0, 0)].set_style(scrollbar.thumb_style);
expected[(1, 0)].set_style(scrollbar.thumb_style);
assert_eq!(buf, expected);
}
#[test]
fn render_horizontal_fractional_thumb_box_drawing_track() {
let scrollbar = ScrollBar::horizontal(ScrollLengths {
content_len: 10,
viewport_len: 3,
})
.arrows(ScrollBarArrows::None)
.offset(1)
.glyph_set(GlyphSet::box_drawing());
let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
(&scrollbar).render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec!["🮉▌──"]);
expected.set_style(expected.area, scrollbar.track_style);
expected[(0, 0)].set_style(scrollbar.thumb_style);
expected[(1, 0)].set_style(scrollbar.thumb_style);
assert_eq!(buf, expected);
}
#[test]
fn render_horizontal_fractional_thumb_unicode_glyphs() {
let scrollbar = ScrollBar::horizontal(ScrollLengths {
content_len: 10,
viewport_len: 3,
})
.arrows(ScrollBarArrows::None)
.offset(1)
.glyph_set(GlyphSet::unicode());
let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
(&scrollbar).render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec!["▐▌──"]);
expected.set_style(expected.area, scrollbar.track_style);
expected[(0, 0)].set_style(scrollbar.thumb_style);
expected[(1, 0)].set_style(scrollbar.thumb_style);
assert_eq!(buf, expected);
}
#[test]
fn render_horizontal_thumb_walk_minimal_glyphs() {
assert_horizontal_thumb_walk(
GlyphSet::minimal(),
' ',
[
"██ ",
"🮋█▏ ",
"🮊█▎ ",
"🮉█▍ ",
"▐█▌ ",
"🮈█▋ ",
"🮇█▊ ",
"▕█▉ ",
" ██ ",
],
);
}
#[test]
fn render_horizontal_thumb_walk_legacy_glyphs() {
assert_horizontal_thumb_walk(
GlyphSet::symbols_for_legacy_computing(),
'─',
[
"██──────",
"🮋█▏─────",
"🮊█▎─────",
"🮉█▍─────",
"▐█▌─────",
"🮈█▋─────",
"🮇█▊─────",
"▕█▉─────",
"─██─────",
],
);
}
#[test]
fn render_horizontal_thumb_walk_unicode_glyphs() {
assert_horizontal_thumb_walk(
GlyphSet::unicode(),
'─',
[
"██──────",
"██▏─────",
"▐█▎─────",
"▐█▍─────",
"▐█▌─────",
"▐█▋─────",
"▕█▊─────",
"▕█▉─────",
"─██─────",
],
);
}
#[test]
fn render_full_thumb_when_no_scroll() {
let scrollbar = ScrollBar::vertical(ScrollLengths {
content_len: 5,
viewport_len: 10,
})
.arrows(ScrollBarArrows::None);
let mut buf = Buffer::empty(Rect::new(0, 0, 1, 3));
(&scrollbar).render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec!["█", "█", "█"]);
expected.set_style(expected.area, scrollbar.thumb_style);
assert_eq!(buf, expected);
}
#[test]
fn render_vertical_arrows() {
let scrollbar = ScrollBar::vertical(ScrollLengths {
content_len: 5,
viewport_len: 2,
})
.arrows(ScrollBarArrows::Both);
let mut buf = Buffer::empty(Rect::new(0, 0, 1, 3));
(&scrollbar).render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec!["▲", "█", "▼"]);
expected[(0, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
expected[(0, 1)].set_style(scrollbar.thumb_style);
expected[(0, 2)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
assert_eq!(buf, expected);
}
#[test]
fn render_horizontal_arrows() {
let scrollbar = ScrollBar::horizontal(ScrollLengths {
content_len: 5,
viewport_len: 2,
})
.arrows(ScrollBarArrows::Both);
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
(&scrollbar).render(buf.area, &mut buf);
let mut expected = Buffer::with_lines(vec!["◀█▶"]);
expected[(0, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
expected[(1, 0)].set_style(scrollbar.thumb_style);
expected[(2, 0)].set_style(scrollbar.arrow_style.unwrap_or(scrollbar.track_style));
assert_eq!(buf, expected);
}
}