ratatui/widgets/
scrollbar.rs

1#![warn(clippy::pedantic)]
2#![allow(
3    clippy::cast_possible_truncation,
4    clippy::cast_precision_loss,
5    clippy::cast_sign_loss,
6    clippy::module_name_repetitions
7)]
8
9use std::iter;
10
11use strum::{Display, EnumString};
12use unicode_width::UnicodeWidthStr;
13
14use crate::{
15    buffer::Buffer,
16    layout::Rect,
17    style::Style,
18    symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
19    widgets::StatefulWidget,
20};
21
22/// A widget to display a scrollbar
23///
24/// The following components of the scrollbar are customizable in symbol and style. Note the
25/// scrollbar is represented horizontally but it can also be set vertically (which is actually the
26/// default).
27///
28/// ```text
29/// <--▮------->
30/// ^  ^   ^   ^
31/// │  │   │   └ end
32/// │  │   └──── track
33/// │  └──────── thumb
34/// └─────────── begin
35/// ```
36///
37/// # Important
38///
39/// You must specify the [`ScrollbarState::content_length`] before rendering the `Scrollbar`, or
40/// else the `Scrollbar` will render blank.
41///
42/// # Examples
43///
44/// ```rust
45/// use ratatui::{
46///     layout::{Margin, Rect},
47///     text::Line,
48///     widgets::{
49///         Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
50///         StatefulWidget,
51///     },
52///     Frame,
53/// };
54///
55/// # fn render_paragraph_with_scrollbar(frame: &mut Frame, area: Rect) {
56/// let vertical_scroll = 0; // from app state
57///
58/// let items = vec![
59///     Line::from("Item 1"),
60///     Line::from("Item 2"),
61///     Line::from("Item 3"),
62/// ];
63/// let paragraph = Paragraph::new(items.clone())
64///     .scroll((vertical_scroll as u16, 0))
65///     .block(Block::new().borders(Borders::RIGHT)); // to show a background for the scrollbar
66///
67/// let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
68///     .begin_symbol(Some("↑"))
69///     .end_symbol(Some("↓"));
70///
71/// let mut scrollbar_state = ScrollbarState::new(items.len()).position(vertical_scroll);
72///
73/// let area = frame.area();
74/// // Note we render the paragraph
75/// frame.render_widget(paragraph, area);
76/// // and the scrollbar, those are separate widgets
77/// frame.render_stateful_widget(
78///     scrollbar,
79///     area.inner(Margin {
80///         // using an inner vertical margin of 1 unit makes the scrollbar inside the block
81///         vertical: 1,
82///         horizontal: 0,
83///     }),
84///     &mut scrollbar_state,
85/// );
86/// # }
87/// ```
88#[derive(Debug, Clone, Eq, PartialEq, Hash)]
89pub struct Scrollbar<'a> {
90    orientation: ScrollbarOrientation,
91    thumb_style: Style,
92    thumb_symbol: &'a str,
93    track_style: Style,
94    track_symbol: Option<&'a str>,
95    begin_symbol: Option<&'a str>,
96    begin_style: Style,
97    end_symbol: Option<&'a str>,
98    end_style: Style,
99}
100
101/// This is the position of the scrollbar around a given area.
102///
103/// ```plain
104///           HorizontalTop
105///             ┌───────┐
106/// VerticalLeft│       │VerticalRight
107///             └───────┘
108///          HorizontalBottom
109/// ```
110#[derive(Debug, Default, Display, EnumString, Clone, Eq, PartialEq, Hash)]
111pub enum ScrollbarOrientation {
112    /// Positions the scrollbar on the right, scrolling vertically
113    #[default]
114    VerticalRight,
115    /// Positions the scrollbar on the left, scrolling vertically
116    VerticalLeft,
117    /// Positions the scrollbar on the bottom, scrolling horizontally
118    HorizontalBottom,
119    /// Positions the scrollbar on the top, scrolling horizontally
120    HorizontalTop,
121}
122
123/// A struct representing the state of a Scrollbar widget.
124///
125/// # Important
126///
127/// It's essential to set the `content_length` field when using this struct. This field
128/// represents the total length of the scrollable content. The default value is zero
129/// which will result in the Scrollbar not rendering.
130///
131/// For example, in the following list, assume there are 4 bullet points:
132///
133/// - the `content_length` is 4
134/// - the `position` is 0
135/// - the `viewport_content_length` is 2
136///
137/// ```text
138/// ┌───────────────┐
139/// │1. this is a   █
140/// │   single item █
141/// │2. this is a   ║
142/// │   second item ║
143/// └───────────────┘
144/// ```
145///
146/// If you don't have multi-line content, you can leave the `viewport_content_length` set to the
147/// default and it'll use the track size as a `viewport_content_length`.
148#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
149#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
150pub struct ScrollbarState {
151    /// The total length of the scrollable content.
152    content_length: usize,
153    /// The current position within the scrollable content.
154    position: usize,
155    /// The length of content in current viewport.
156    ///
157    /// FIXME: this should be `Option<usize>`, but it will break serialization to change it.
158    viewport_content_length: usize,
159}
160
161/// An enum representing a scrolling direction.
162///
163/// This is used with [`ScrollbarState::scroll`].
164///
165/// It is useful for example when you want to store in which direction to scroll.
166#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
167pub enum ScrollDirection {
168    /// Forward scroll direction, usually corresponds to scrolling downwards or rightwards.
169    #[default]
170    Forward,
171    /// Backward scroll direction, usually corresponds to scrolling upwards or leftwards.
172    Backward,
173}
174
175impl<'a> Default for Scrollbar<'a> {
176    fn default() -> Self {
177        Self::new(ScrollbarOrientation::default())
178    }
179}
180
181impl<'a> Scrollbar<'a> {
182    /// Creates a new scrollbar with the given orientation.
183    ///
184    /// Most of the time you'll want [`ScrollbarOrientation::VerticalRight`] or
185    /// [`ScrollbarOrientation::HorizontalBottom`]. See [`ScrollbarOrientation`] for more options.
186    #[must_use = "creates the Scrollbar"]
187    pub const fn new(orientation: ScrollbarOrientation) -> Self {
188        let symbols = if orientation.is_vertical() {
189            DOUBLE_VERTICAL
190        } else {
191            DOUBLE_HORIZONTAL
192        };
193        Self::new_with_symbols(orientation, &symbols)
194    }
195
196    /// Creates a new scrollbar with the given orientation and symbol set.
197    #[must_use = "creates the Scrollbar"]
198    const fn new_with_symbols(orientation: ScrollbarOrientation, symbols: &Set) -> Self {
199        Self {
200            orientation,
201            thumb_symbol: symbols.thumb,
202            thumb_style: Style::new(),
203            track_symbol: Some(symbols.track),
204            track_style: Style::new(),
205            begin_symbol: Some(symbols.begin),
206            begin_style: Style::new(),
207            end_symbol: Some(symbols.end),
208            end_style: Style::new(),
209        }
210    }
211
212    /// Sets the position of the scrollbar.
213    ///
214    /// The orientation of the scrollbar is the position it will take around a [`Rect`]. See
215    /// [`ScrollbarOrientation`] for more details.
216    ///
217    /// Resets the symbols to [`DOUBLE_VERTICAL`] or [`DOUBLE_HORIZONTAL`] based on orientation.
218    ///
219    /// This is a fluent setter method which must be chained or used as it consumes self
220    #[must_use = "method moves the value of self and returns the modified value"]
221    pub const fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
222        self.orientation = orientation;
223        let symbols = if self.orientation.is_vertical() {
224            DOUBLE_VERTICAL
225        } else {
226            DOUBLE_HORIZONTAL
227        };
228        self.symbols(symbols)
229    }
230
231    /// Sets the orientation and symbols for the scrollbar from a [`Set`].
232    ///
233    /// This has the same effect as calling [`Scrollbar::orientation`] and then
234    /// [`Scrollbar::symbols`]. See those for more details.
235    ///
236    /// This is a fluent setter method which must be chained or used as it consumes self
237    #[must_use = "method moves the value of self and returns the modified value"]
238    pub const fn orientation_and_symbol(
239        mut self,
240        orientation: ScrollbarOrientation,
241        symbols: Set,
242    ) -> Self {
243        self.orientation = orientation;
244        self.symbols(symbols)
245    }
246
247    /// Sets the symbol that represents the thumb of the scrollbar.
248    ///
249    /// The thumb is the handle representing the progression on the scrollbar. See [`Scrollbar`]
250    /// for a visual example of what this represents.
251    ///
252    /// This is a fluent setter method which must be chained or used as it consumes self
253    #[must_use = "method moves the value of self and returns the modified value"]
254    pub const fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
255        self.thumb_symbol = thumb_symbol;
256        self
257    }
258
259    /// Sets the style on the scrollbar thumb.
260    ///
261    /// The thumb is the handle representing the progression on the scrollbar. See [`Scrollbar`]
262    /// for a visual example of what this represents.
263    ///
264    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
265    /// your own type that implements [`Into<Style>`]).
266    ///
267    /// This is a fluent setter method which must be chained or used as it consumes self
268    ///
269    /// [`Color`]: crate::style::Color
270    #[must_use = "method moves the value of self and returns the modified value"]
271    pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
272        self.thumb_style = thumb_style.into();
273        self
274    }
275
276    /// Sets the symbol that represents the track of the scrollbar.
277    ///
278    /// See [`Scrollbar`] for a visual example of what this represents.
279    ///
280    /// This is a fluent setter method which must be chained or used as it consumes self
281    #[must_use = "method moves the value of self and returns the modified value"]
282    pub const fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
283        self.track_symbol = track_symbol;
284        self
285    }
286
287    /// Sets the style that is used for the track of the scrollbar.
288    ///
289    /// See [`Scrollbar`] for a visual example of what this represents.
290    ///
291    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
292    /// your own type that implements [`Into<Style>`]).
293    ///
294    /// This is a fluent setter method which must be chained or used as it consumes self
295    ///
296    /// [`Color`]: crate::style::Color
297    #[must_use = "method moves the value of self and returns the modified value"]
298    pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
299        self.track_style = track_style.into();
300        self
301    }
302
303    /// Sets the symbol that represents the beginning of the scrollbar.
304    ///
305    /// See [`Scrollbar`] for a visual example of what this represents.
306    ///
307    /// This is a fluent setter method which must be chained or used as it consumes self
308    #[must_use = "method moves the value of self and returns the modified value"]
309    pub const fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
310        self.begin_symbol = begin_symbol;
311        self
312    }
313
314    /// Sets the style that is used for the beginning of the scrollbar.
315    ///
316    /// See [`Scrollbar`] for a visual example of what this represents.
317    ///
318    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
319    /// your own type that implements [`Into<Style>`]).
320    ///
321    /// This is a fluent setter method which must be chained or used as it consumes self
322    ///
323    /// [`Color`]: crate::style::Color
324    #[must_use = "method moves the value of self and returns the modified value"]
325    pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
326        self.begin_style = begin_style.into();
327        self
328    }
329
330    /// Sets the symbol that represents the end of the scrollbar.
331    ///
332    /// See [`Scrollbar`] for a visual example of what this represents.
333    ///
334    /// This is a fluent setter method which must be chained or used as it consumes self
335    #[must_use = "method moves the value of self and returns the modified value"]
336    pub const fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
337        self.end_symbol = end_symbol;
338        self
339    }
340
341    /// Sets the style that is used for the end of the scrollbar.
342    ///
343    /// See [`Scrollbar`] for a visual example of what this represents.
344    ///
345    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
346    /// your own type that implements [`Into<Style>`]).
347    ///
348    /// This is a fluent setter method which must be chained or used as it consumes self
349    ///
350    /// [`Color`]: crate::style::Color
351    #[must_use = "method moves the value of self and returns the modified value"]
352    pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
353        self.end_style = end_style.into();
354        self
355    }
356
357    /// Sets the symbols used for the various parts of the scrollbar from a [`Set`].
358    ///
359    /// ```text
360    /// <--▮------->
361    /// ^  ^   ^   ^
362    /// │  │   │   └ end
363    /// │  │   └──── track
364    /// │  └──────── thumb
365    /// └─────────── begin
366    /// ```
367    ///
368    /// Only sets `begin_symbol`, `end_symbol` and `track_symbol` if they already contain a value.
369    /// If they were set to `None` explicitly, this function will respect that choice. Use their
370    /// respective setters to change their value.
371    ///
372    /// This is a fluent setter method which must be chained or used as it consumes self
373    #[allow(clippy::needless_pass_by_value)] // Breaking change
374    #[must_use = "method moves the value of self and returns the modified value"]
375    pub const fn symbols(mut self, symbols: Set) -> Self {
376        self.thumb_symbol = symbols.thumb;
377        if self.track_symbol.is_some() {
378            self.track_symbol = Some(symbols.track);
379        }
380        if self.begin_symbol.is_some() {
381            self.begin_symbol = Some(symbols.begin);
382        }
383        if self.end_symbol.is_some() {
384            self.end_symbol = Some(symbols.end);
385        }
386        self
387    }
388
389    /// Sets the style used for the various parts of the scrollbar from a [`Style`].
390    ///
391    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
392    /// your own type that implements [`Into<Style>`]).
393    ///
394    /// ```text
395    /// <--▮------->
396    /// ^  ^   ^   ^
397    /// │  │   │   └ end
398    /// │  │   └──── track
399    /// │  └──────── thumb
400    /// └─────────── begin
401    /// ```
402    ///
403    /// This is a fluent setter method which must be chained or used as it consumes self
404    ///
405    /// [`Color`]: crate::style::Color
406    #[must_use = "method moves the value of self and returns the modified value"]
407    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
408        let style = style.into();
409        self.track_style = style;
410        self.thumb_style = style;
411        self.begin_style = style;
412        self.end_style = style;
413        self
414    }
415}
416
417impl ScrollbarState {
418    /// Constructs a new [`ScrollbarState`] with the specified content length.
419    ///
420    /// `content_length` is the total number of element, that can be scrolled. See
421    /// [`ScrollbarState`] for more details.
422    #[must_use = "creates the ScrollbarState"]
423    pub const fn new(content_length: usize) -> Self {
424        Self {
425            content_length,
426            position: 0,
427            viewport_content_length: 0,
428        }
429    }
430
431    /// Sets the scroll position of the scrollbar.
432    ///
433    /// This represents the number of scrolled items.
434    ///
435    /// This is a fluent setter method which must be chained or used as it consumes self
436    #[must_use = "method moves the value of self and returns the modified value"]
437    pub const fn position(mut self, position: usize) -> Self {
438        self.position = position;
439        self
440    }
441
442    /// Sets the length of the scrollable content.
443    ///
444    /// This is the number of scrollable items. If items have a length of one, then this is the
445    /// same as the number of scrollable cells.
446    ///
447    /// This is a fluent setter method which must be chained or used as it consumes self
448    #[must_use = "method moves the value of self and returns the modified value"]
449    pub const fn content_length(mut self, content_length: usize) -> Self {
450        self.content_length = content_length;
451        self
452    }
453
454    /// Sets the items' size.
455    ///
456    /// This is a fluent setter method which must be chained or used as it consumes self
457    #[must_use = "method moves the value of self and returns the modified value"]
458    pub const fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
459        self.viewport_content_length = viewport_content_length;
460        self
461    }
462
463    /// Decrements the scroll position by one, ensuring it doesn't go below zero.
464    pub fn prev(&mut self) {
465        self.position = self.position.saturating_sub(1);
466    }
467
468    /// Increments the scroll position by one, ensuring it doesn't exceed the length of the content.
469    pub fn next(&mut self) {
470        self.position = self
471            .position
472            .saturating_add(1)
473            .min(self.content_length.saturating_sub(1));
474    }
475
476    /// Sets the scroll position to the start of the scrollable content.
477    pub fn first(&mut self) {
478        self.position = 0;
479    }
480
481    /// Sets the scroll position to the end of the scrollable content.
482    pub fn last(&mut self) {
483        self.position = self.content_length.saturating_sub(1);
484    }
485
486    /// Changes the scroll position based on the provided [`ScrollDirection`].
487    pub fn scroll(&mut self, direction: ScrollDirection) {
488        match direction {
489            ScrollDirection::Forward => {
490                self.next();
491            }
492            ScrollDirection::Backward => {
493                self.prev();
494            }
495        }
496    }
497}
498
499impl<'a> StatefulWidget for Scrollbar<'a> {
500    type State = ScrollbarState;
501
502    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
503        if state.content_length == 0 || self.track_length_excluding_arrow_heads(area) == 0 {
504            return;
505        }
506
507        let mut bar = self.bar_symbols(area, state);
508        let area = self.scollbar_area(area);
509        for x in area.left()..area.right() {
510            for y in area.top()..area.bottom() {
511                if let Some(Some((symbol, style))) = bar.next() {
512                    buf.set_string(x, y, symbol, style);
513                }
514            }
515        }
516    }
517}
518
519impl Scrollbar<'_> {
520    /// Returns an iterator over the symbols and styles of the scrollbar.
521    fn bar_symbols(
522        &self,
523        area: Rect,
524        state: &ScrollbarState,
525    ) -> impl Iterator<Item = Option<(&str, Style)>> {
526        let (track_start_len, thumb_len, track_end_len) = self.part_lengths(area, state);
527
528        let begin = self.begin_symbol.map(|s| Some((s, self.begin_style)));
529        let track = Some(self.track_symbol.map(|s| (s, self.track_style)));
530        let thumb = Some(Some((self.thumb_symbol, self.thumb_style)));
531        let end = self.end_symbol.map(|s| Some((s, self.end_style)));
532
533        // `<`
534        iter::once(begin)
535            // `<═══`
536            .chain(iter::repeat(track).take(track_start_len))
537            // `<═══█████`
538            .chain(iter::repeat(thumb).take(thumb_len))
539            // `<═══█████═══════`
540            .chain(iter::repeat(track).take(track_end_len))
541            // `<═══█████═══════>`
542            .chain(iter::once(end))
543            .flatten()
544    }
545
546    /// Returns the lengths of the parts of a scrollbar
547    ///
548    /// The scrollbar has 3 parts of note:
549    /// - `<═══█████═══════>`: full scrollbar
550    /// - ` ═══             `: track start
551    /// - `    █████        `: thumb
552    /// - `         ═══════ `: track end
553    ///
554    /// This method returns the length of the start, thumb, and end as a tuple.
555    fn part_lengths(&self, area: Rect, state: &ScrollbarState) -> (usize, usize, usize) {
556        let track_length = f64::from(self.track_length_excluding_arrow_heads(area));
557        let viewport_length = self.viewport_length(state, area) as f64;
558
559        // Ensure that the position of the thumb is within the bounds of the content taking into
560        // account the content and viewport length. When the last line of the content is at the top
561        // of the viewport, the thumb should be at the bottom of the track.
562        let max_position = state.content_length.saturating_sub(1) as f64;
563        let start_position = (state.position as f64).clamp(0.0, max_position);
564        let max_viewport_position = max_position + viewport_length;
565        let end_position = start_position + viewport_length;
566
567        // Calculate the start and end positions of the thumb. The size will be proportional to the
568        // viewport length compared to the total amount of possible visible rows.
569        let thumb_start = start_position * track_length / max_viewport_position;
570        let thumb_end = end_position * track_length / max_viewport_position;
571
572        // Make sure that the thumb is at least 1 cell long by ensuring that the start of the thumb
573        // is less than the track_len. We use the positions instead of the sizes and use nearest
574        // integer instead of floor / ceil to avoid problems caused by rounding errors.
575        let thumb_start = thumb_start.round().clamp(0.0, track_length - 1.0) as usize;
576        let thumb_end = thumb_end.round().clamp(0.0, track_length) as usize;
577
578        let thumb_length = thumb_end.saturating_sub(thumb_start).max(1);
579        let track_end_length = (track_length as usize).saturating_sub(thumb_start + thumb_length);
580
581        (thumb_start, thumb_length, track_end_length)
582    }
583
584    fn scollbar_area(&self, area: Rect) -> Rect {
585        match self.orientation {
586            ScrollbarOrientation::VerticalLeft => area.columns().next(),
587            ScrollbarOrientation::VerticalRight => area.columns().last(),
588            ScrollbarOrientation::HorizontalTop => area.rows().next(),
589            ScrollbarOrientation::HorizontalBottom => area.rows().last(),
590        }
591        .expect("Scrollbar area is empty") // this should never happen as we check for empty area
592    }
593
594    /// Calculates length of the track excluding the arrow heads
595    ///
596    /// ```plain
597    ///        ┌────────── track_length
598    ///  vvvvvvvvvvvvvvv
599    /// <═══█████═══════>
600    /// ```
601    fn track_length_excluding_arrow_heads(&self, area: Rect) -> u16 {
602        let start_len = self.begin_symbol.map_or(0, |s| s.width() as u16);
603        let end_len = self.end_symbol.map_or(0, |s| s.width() as u16);
604        let arrows_len = start_len.saturating_add(end_len);
605        if self.orientation.is_vertical() {
606            area.height.saturating_sub(arrows_len)
607        } else {
608            area.width.saturating_sub(arrows_len)
609        }
610    }
611
612    const fn viewport_length(&self, state: &ScrollbarState, area: Rect) -> usize {
613        if state.viewport_content_length != 0 {
614            state.viewport_content_length
615        } else if self.orientation.is_vertical() {
616            area.height as usize
617        } else {
618            area.width as usize
619        }
620    }
621}
622
623impl ScrollbarOrientation {
624    /// Returns `true` if the scrollbar is vertical.
625    #[must_use = "returns the requested kind of the scrollbar"]
626    pub const fn is_vertical(&self) -> bool {
627        matches!(self, Self::VerticalRight | Self::VerticalLeft)
628    }
629
630    /// Returns `true` if the scrollbar is horizontal.
631    #[must_use = "returns the requested kind of the scrollbar"]
632    pub const fn is_horizontal(&self) -> bool {
633        matches!(self, Self::HorizontalBottom | Self::HorizontalTop)
634    }
635}
636
637#[cfg(test)]
638mod tests {
639    use std::str::FromStr;
640
641    use rstest::{fixture, rstest};
642    use strum::ParseError;
643
644    use super::*;
645    use crate::{text::Text, widgets::Widget};
646
647    #[test]
648    fn scroll_direction_to_string() {
649        assert_eq!(ScrollDirection::Forward.to_string(), "Forward");
650        assert_eq!(ScrollDirection::Backward.to_string(), "Backward");
651    }
652
653    #[test]
654    fn scroll_direction_from_str() {
655        assert_eq!("Forward".parse(), Ok(ScrollDirection::Forward));
656        assert_eq!("Backward".parse(), Ok(ScrollDirection::Backward));
657        assert_eq!(
658            ScrollDirection::from_str(""),
659            Err(ParseError::VariantNotFound)
660        );
661    }
662
663    #[test]
664    fn scrollbar_orientation_to_string() {
665        use ScrollbarOrientation::*;
666        assert_eq!(VerticalRight.to_string(), "VerticalRight");
667        assert_eq!(VerticalLeft.to_string(), "VerticalLeft");
668        assert_eq!(HorizontalBottom.to_string(), "HorizontalBottom");
669        assert_eq!(HorizontalTop.to_string(), "HorizontalTop");
670    }
671
672    #[test]
673    fn scrollbar_orientation_from_str() {
674        use ScrollbarOrientation::*;
675        assert_eq!("VerticalRight".parse(), Ok(VerticalRight));
676        assert_eq!("VerticalLeft".parse(), Ok(VerticalLeft));
677        assert_eq!("HorizontalBottom".parse(), Ok(HorizontalBottom));
678        assert_eq!("HorizontalTop".parse(), Ok(HorizontalTop));
679        assert_eq!(
680            ScrollbarOrientation::from_str(""),
681            Err(ParseError::VariantNotFound)
682        );
683    }
684
685    #[fixture]
686    fn scrollbar_no_arrows() -> Scrollbar<'static> {
687        Scrollbar::new(ScrollbarOrientation::HorizontalTop)
688            .begin_symbol(None)
689            .end_symbol(None)
690            .track_symbol(Some("-"))
691            .thumb_symbol("#")
692    }
693
694    #[rstest]
695    #[case::area_2_position_0("#-", 0, 2)]
696    #[case::area_2_position_1("-#", 1, 2)]
697    fn render_scrollbar_simplest(
698        #[case] expected: &str,
699        #[case] position: usize,
700        #[case] content_length: usize,
701        scrollbar_no_arrows: Scrollbar,
702    ) {
703        let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
704        let mut state = ScrollbarState::new(content_length).position(position);
705        scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
706        assert_eq!(buffer, Buffer::with_lines([expected]));
707    }
708
709    #[rstest]
710    #[case::position_0("#####-----", 0, 10)]
711    #[case::position_1("-#####----", 1, 10)]
712    #[case::position_2("-#####----", 2, 10)]
713    #[case::position_3("--#####---", 3, 10)]
714    #[case::position_4("--#####---", 4, 10)]
715    #[case::position_5("---#####--", 5, 10)]
716    #[case::position_6("---#####--", 6, 10)]
717    #[case::position_7("----#####-", 7, 10)]
718    #[case::position_8("----#####-", 8, 10)]
719    #[case::position_9("-----#####", 9, 10)]
720    fn render_scrollbar_simple(
721        #[case] expected: &str,
722        #[case] position: usize,
723        #[case] content_length: usize,
724        scrollbar_no_arrows: Scrollbar,
725    ) {
726        let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
727        let mut state = ScrollbarState::new(content_length).position(position);
728        scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
729        assert_eq!(buffer, Buffer::with_lines([expected]));
730    }
731
732    #[rstest]
733    #[case::position_0("          ", 0, 0)]
734    fn render_scrollbar_nobar(
735        #[case] expected: &str,
736        #[case] position: usize,
737        #[case] content_length: usize,
738        scrollbar_no_arrows: Scrollbar,
739    ) {
740        let size = expected.width();
741        let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
742        let mut state = ScrollbarState::new(content_length).position(position);
743        scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
744        assert_eq!(buffer, Buffer::with_lines([expected]));
745    }
746
747    #[rstest]
748    #[case::fullbar_position_0("##########", 0, 1)]
749    #[case::almost_fullbar_position_0("#########-", 0, 2)]
750    #[case::almost_fullbar_position_1("-#########", 1, 2)]
751    fn render_scrollbar_fullbar(
752        #[case] expected: &str,
753        #[case] position: usize,
754        #[case] content_length: usize,
755        scrollbar_no_arrows: Scrollbar,
756    ) {
757        let size = expected.width();
758        let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
759        let mut state = ScrollbarState::new(content_length).position(position);
760        scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
761        assert_eq!(buffer, Buffer::with_lines([expected]));
762    }
763
764    #[rstest]
765    #[case::position_0("#########-", 0, 2)]
766    #[case::position_1("-#########", 1, 2)]
767    fn render_scrollbar_almost_fullbar(
768        #[case] expected: &str,
769        #[case] position: usize,
770        #[case] content_length: usize,
771        scrollbar_no_arrows: Scrollbar,
772    ) {
773        let size = expected.width();
774        let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
775        let mut state = ScrollbarState::new(content_length).position(position);
776        scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
777        assert_eq!(buffer, Buffer::with_lines([expected]));
778    }
779
780    #[rstest]
781    #[case::position_0("█████═════", 0, 10)]
782    #[case::position_1("═█████════", 1, 10)]
783    #[case::position_2("═█████════", 2, 10)]
784    #[case::position_3("══█████═══", 3, 10)]
785    #[case::position_4("══█████═══", 4, 10)]
786    #[case::position_5("═══█████══", 5, 10)]
787    #[case::position_6("═══█████══", 6, 10)]
788    #[case::position_7("════█████═", 7, 10)]
789    #[case::position_8("════█████═", 8, 10)]
790    #[case::position_9("═════█████", 9, 10)]
791    #[case::position_out_of_bounds("═════█████", 100, 10)]
792    fn render_scrollbar_without_symbols(
793        #[case] expected: &str,
794        #[case] position: usize,
795        #[case] content_length: usize,
796    ) {
797        let size = expected.width() as u16;
798        let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
799        let mut state = ScrollbarState::new(content_length).position(position);
800        Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
801            .begin_symbol(None)
802            .end_symbol(None)
803            .render(buffer.area, &mut buffer, &mut state);
804        assert_eq!(buffer, Buffer::with_lines([expected]));
805    }
806
807    #[rstest]
808    #[case::position_0("█████     ", 0, 10)]
809    #[case::position_1(" █████    ", 1, 10)]
810    #[case::position_2(" █████    ", 2, 10)]
811    #[case::position_3("  █████   ", 3, 10)]
812    #[case::position_4("  █████   ", 4, 10)]
813    #[case::position_5("   █████  ", 5, 10)]
814    #[case::position_6("   █████  ", 6, 10)]
815    #[case::position_7("    █████ ", 7, 10)]
816    #[case::position_8("    █████ ", 8, 10)]
817    #[case::position_9("     █████", 9, 10)]
818    #[case::position_out_of_bounds("     █████", 100, 10)]
819    fn render_scrollbar_without_track_symbols(
820        #[case] expected: &str,
821        #[case] position: usize,
822        #[case] content_length: usize,
823    ) {
824        let size = expected.width() as u16;
825        let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
826        let mut state = ScrollbarState::new(content_length).position(position);
827        Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
828            .track_symbol(None)
829            .begin_symbol(None)
830            .end_symbol(None)
831            .render(buffer.area, &mut buffer, &mut state);
832        assert_eq!(buffer, Buffer::with_lines([expected]));
833    }
834
835    #[rstest]
836    #[case::position_0("█████-----", 0, 10)]
837    #[case::position_1("-█████----", 1, 10)]
838    #[case::position_2("-█████----", 2, 10)]
839    #[case::position_3("--█████---", 3, 10)]
840    #[case::position_4("--█████---", 4, 10)]
841    #[case::position_5("---█████--", 5, 10)]
842    #[case::position_6("---█████--", 6, 10)]
843    #[case::position_7("----█████-", 7, 10)]
844    #[case::position_8("----█████-", 8, 10)]
845    #[case::position_9("-----█████", 9, 10)]
846    #[case::position_out_of_bounds("-----█████", 100, 10)]
847    fn render_scrollbar_without_track_symbols_over_content(
848        #[case] expected: &str,
849        #[case] position: usize,
850        #[case] content_length: usize,
851    ) {
852        let size = expected.width() as u16;
853        let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
854        let width = buffer.area.width as usize;
855        let s = "";
856        Text::from(format!("{s:-^width$}")).render(buffer.area, &mut buffer);
857        let mut state = ScrollbarState::new(content_length).position(position);
858        Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
859            .track_symbol(None)
860            .begin_symbol(None)
861            .end_symbol(None)
862            .render(buffer.area, &mut buffer, &mut state);
863        assert_eq!(buffer, Buffer::with_lines([expected]));
864    }
865
866    #[rstest]
867    #[case::position_0("<####---->", 0, 10)]
868    #[case::position_1("<#####--->", 1, 10)]
869    #[case::position_2("<-####--->", 2, 10)]
870    #[case::position_3("<-####--->", 3, 10)]
871    #[case::position_4("<--####-->", 4, 10)]
872    #[case::position_5("<--####-->", 5, 10)]
873    #[case::position_6("<---####->", 6, 10)]
874    #[case::position_7("<---####->", 7, 10)]
875    #[case::position_8("<---#####>", 8, 10)]
876    #[case::position_9("<----####>", 9, 10)]
877    #[case::position_one_out_of_bounds("<----####>", 10, 10)]
878    #[case::position_few_out_of_bounds("<----####>", 15, 10)]
879    #[case::position_very_many_out_of_bounds("<----####>", 500, 10)]
880    fn render_scrollbar_with_symbols(
881        #[case] expected: &str,
882        #[case] position: usize,
883        #[case] content_length: usize,
884    ) {
885        let size = expected.width() as u16;
886        let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
887        let mut state = ScrollbarState::new(content_length).position(position);
888        Scrollbar::new(ScrollbarOrientation::HorizontalTop)
889            .begin_symbol(Some("<"))
890            .end_symbol(Some(">"))
891            .track_symbol(Some("-"))
892            .thumb_symbol("#")
893            .render(buffer.area, &mut buffer, &mut state);
894        assert_eq!(buffer, Buffer::with_lines([expected]));
895    }
896
897    #[rstest]
898    #[case::position_0("█████═════", 0, 10)]
899    #[case::position_1("═█████════", 1, 10)]
900    #[case::position_2("═█████════", 2, 10)]
901    #[case::position_3("══█████═══", 3, 10)]
902    #[case::position_4("══█████═══", 4, 10)]
903    #[case::position_5("═══█████══", 5, 10)]
904    #[case::position_6("═══█████══", 6, 10)]
905    #[case::position_7("════█████═", 7, 10)]
906    #[case::position_8("════█████═", 8, 10)]
907    #[case::position_9("═════█████", 9, 10)]
908    #[case::position_out_of_bounds("═════█████", 100, 10)]
909    fn render_scrollbar_horizontal_bottom(
910        #[case] expected: &str,
911        #[case] position: usize,
912        #[case] content_length: usize,
913    ) {
914        let size = expected.width() as u16;
915        let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
916        let mut state = ScrollbarState::new(content_length).position(position);
917        Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
918            .begin_symbol(None)
919            .end_symbol(None)
920            .render(buffer.area, &mut buffer, &mut state);
921        let empty_string = " ".repeat(size as usize);
922        assert_eq!(buffer, Buffer::with_lines([&empty_string, expected]));
923    }
924
925    #[rstest]
926    #[case::position_0("█████═════", 0, 10)]
927    #[case::position_1("═█████════", 1, 10)]
928    #[case::position_2("═█████════", 2, 10)]
929    #[case::position_3("══█████═══", 3, 10)]
930    #[case::position_4("══█████═══", 4, 10)]
931    #[case::position_5("═══█████══", 5, 10)]
932    #[case::position_6("═══█████══", 6, 10)]
933    #[case::position_7("════█████═", 7, 10)]
934    #[case::position_8("════█████═", 8, 10)]
935    #[case::position_9("═════█████", 9, 10)]
936    #[case::position_out_of_bounds("═════█████", 100, 10)]
937    fn render_scrollbar_horizontal_top(
938        #[case] expected: &str,
939        #[case] position: usize,
940        #[case] content_length: usize,
941    ) {
942        let size = expected.width() as u16;
943        let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
944        let mut state = ScrollbarState::new(content_length).position(position);
945        Scrollbar::new(ScrollbarOrientation::HorizontalTop)
946            .begin_symbol(None)
947            .end_symbol(None)
948            .render(buffer.area, &mut buffer, &mut state);
949        let empty_string = " ".repeat(size as usize);
950        assert_eq!(buffer, Buffer::with_lines([expected, &empty_string]));
951    }
952
953    #[rstest]
954    #[case::position_0("<####---->", 0, 10)]
955    #[case::position_1("<#####--->", 1, 10)]
956    #[case::position_2("<-####--->", 2, 10)]
957    #[case::position_3("<-####--->", 3, 10)]
958    #[case::position_4("<--####-->", 4, 10)]
959    #[case::position_5("<--####-->", 5, 10)]
960    #[case::position_6("<---####->", 6, 10)]
961    #[case::position_7("<---####->", 7, 10)]
962    #[case::position_8("<---#####>", 8, 10)]
963    #[case::position_9("<----####>", 9, 10)]
964    #[case::position_one_out_of_bounds("<----####>", 10, 10)]
965    fn render_scrollbar_vertical_left(
966        #[case] expected: &str,
967        #[case] position: usize,
968        #[case] content_length: usize,
969    ) {
970        let size = expected.width() as u16;
971        let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
972        let mut state = ScrollbarState::new(content_length).position(position);
973        Scrollbar::new(ScrollbarOrientation::VerticalLeft)
974            .begin_symbol(Some("<"))
975            .end_symbol(Some(">"))
976            .track_symbol(Some("-"))
977            .thumb_symbol("#")
978            .render(buffer.area, &mut buffer, &mut state);
979        let bar = expected.chars().map(|c| format!("{c}    "));
980        assert_eq!(buffer, Buffer::with_lines(bar));
981    }
982
983    #[rstest]
984    #[case::position_0("<####---->", 0, 10)]
985    #[case::position_1("<#####--->", 1, 10)]
986    #[case::position_2("<-####--->", 2, 10)]
987    #[case::position_3("<-####--->", 3, 10)]
988    #[case::position_4("<--####-->", 4, 10)]
989    #[case::position_5("<--####-->", 5, 10)]
990    #[case::position_6("<---####->", 6, 10)]
991    #[case::position_7("<---####->", 7, 10)]
992    #[case::position_8("<---#####>", 8, 10)]
993    #[case::position_9("<----####>", 9, 10)]
994    #[case::position_one_out_of_bounds("<----####>", 10, 10)]
995    fn render_scrollbar_vertical_rightl(
996        #[case] expected: &str,
997        #[case] position: usize,
998        #[case] content_length: usize,
999    ) {
1000        let size = expected.width() as u16;
1001        let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
1002        let mut state = ScrollbarState::new(content_length).position(position);
1003        Scrollbar::new(ScrollbarOrientation::VerticalRight)
1004            .begin_symbol(Some("<"))
1005            .end_symbol(Some(">"))
1006            .track_symbol(Some("-"))
1007            .thumb_symbol("#")
1008            .render(buffer.area, &mut buffer, &mut state);
1009        let bar = expected.chars().map(|c| format!("    {c}"));
1010        assert_eq!(buffer, Buffer::with_lines(bar));
1011    }
1012
1013    #[rstest]
1014    #[case::position_0("##--------", 0, 10)]
1015    #[case::position_1("-##-------", 1, 10)]
1016    #[case::position_2("--##------", 2, 10)]
1017    #[case::position_3("---##-----", 3, 10)]
1018    #[case::position_4("----#-----", 4, 10)]
1019    #[case::position_5("-----#----", 5, 10)]
1020    #[case::position_6("-----##---", 6, 10)]
1021    #[case::position_7("------##--", 7, 10)]
1022    #[case::position_8("-------##-", 8, 10)]
1023    #[case::position_9("--------##", 9, 10)]
1024    #[case::position_one_out_of_bounds("--------##", 10, 10)]
1025    fn custom_viewport_length(
1026        #[case] expected: &str,
1027        #[case] position: usize,
1028        #[case] content_length: usize,
1029        scrollbar_no_arrows: Scrollbar,
1030    ) {
1031        let size = expected.width() as u16;
1032        let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
1033        let mut state = ScrollbarState::new(content_length)
1034            .position(position)
1035            .viewport_content_length(2);
1036        scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
1037        assert_eq!(buffer, Buffer::with_lines([expected]));
1038    }
1039
1040    /// Fixes <https://github.com/ratatui/ratatui/pull/959> which was a bug that would not
1041    /// render a thumb when the viewport was very small in comparison to the content length.
1042    #[rstest]
1043    #[case::position_0("#----", 0, 100)]
1044    #[case::position_10("#----", 10, 100)]
1045    #[case::position_20("-#---", 20, 100)]
1046    #[case::position_30("-#---", 30, 100)]
1047    #[case::position_40("--#--", 40, 100)]
1048    #[case::position_50("--#--", 50, 100)]
1049    #[case::position_60("---#-", 60, 100)]
1050    #[case::position_70("---#-", 70, 100)]
1051    #[case::position_80("----#", 80, 100)]
1052    #[case::position_90("----#", 90, 100)]
1053    #[case::position_one_out_of_bounds("----#", 100, 100)]
1054    fn thumb_visible_on_very_small_track(
1055        #[case] expected: &str,
1056        #[case] position: usize,
1057        #[case] content_length: usize,
1058        scrollbar_no_arrows: Scrollbar,
1059    ) {
1060        let size = expected.width() as u16;
1061        let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
1062        let mut state = ScrollbarState::new(content_length)
1063            .position(position)
1064            .viewport_content_length(2);
1065        scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
1066        assert_eq!(buffer, Buffer::with_lines([expected]));
1067    }
1068}