Skip to main content

altui_core/widgets/
scrollbar.rs

1// Copyright (c) 2023 The Ratatui Developers
2
3use crate::{
4    buffer::Buffer,
5    layout::Rect,
6    style::Style,
7    symbols::{block::FULL, line},
8    widgets::Widget,
9};
10
11/// Scrollbar Set
12/// ```text
13/// <--▮------->
14/// ^  ^   ^   ^
15/// │  │   │   └ end
16/// │  │   └──── track
17/// │  └──────── thumb
18/// └─────────── begin
19/// ```
20#[derive(Debug, Clone)]
21pub struct Set {
22    pub track: &'static str,
23    pub thumb: &'static str,
24    pub begin: &'static str,
25    pub end: &'static str,
26}
27
28pub const DOUBLE_VERTICAL: Set = Set {
29    track: line::DOUBLE_VERTICAL,
30    thumb: FULL,
31    begin: "▲",
32    end: "▼",
33};
34
35pub const DOUBLE_HORIZONTAL: Set = Set {
36    track: line::DOUBLE_HORIZONTAL,
37    thumb: FULL,
38    begin: "◄",
39    end: "►",
40};
41
42pub const VERTICAL: Set = Set {
43    track: line::VERTICAL,
44    thumb: FULL,
45    begin: "↑",
46    end: "↓",
47};
48
49pub const HORIZONTAL: Set = Set {
50    track: line::HORIZONTAL,
51    thumb: FULL,
52    begin: "←",
53    end: "→",
54};
55
56/// An enum representing the direction of scrolling in a Scrollbar widget.
57#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
58pub enum ScrollDirection {
59    /// Forward scroll direction, usually corresponds to scrolling downwards or rightwards.
60    #[default]
61    Forward,
62    /// Backward scroll direction, usually corresponds to scrolling upwards or leftwards.
63    Backward,
64}
65
66/// Scrollbar Orientation
67#[derive(Default, Debug, Clone, Copy)]
68pub enum ScrollbarOrientation {
69    #[default]
70    VerticalRight,
71    VerticalLeft,
72    HorizontalBottom,
73    HorizontalTop,
74}
75
76/// Scrollbar widget for altui-core library.
77///
78/// This widget can be used to display a scrollbar in a terminal user interface.
79/// The following components of the scrollbar are customizable in symbol and style.
80///
81/// ```text
82/// <--▮------->
83/// ^  ^   ^   ^
84/// │  │   │   └ end
85/// │  │   └──── track
86/// │  └──────── thumb
87/// └─────────── begin
88/// ```
89#[derive(Debug, Clone)]
90pub struct Scrollbar<'a> {
91    orientation: ScrollbarOrientation,
92    thumb_style: Style,
93    thumb_symbol: &'a str,
94    track_style: Style,
95    track_symbol: &'a str,
96    begin_symbol: Option<&'a str>,
97    begin_style: Style,
98    end_symbol: Option<&'a str>,
99    end_style: Style,
100    // The current position within the scrollable content.
101    position: u16,
102    // The current offset added to the Rect size.
103    offset: u16,
104    // The total length of the scrollable content.
105    content_length: u16,
106    // The length of content in current viewport.
107    viewport_content_length: u16,
108}
109
110impl<'a> Default for Scrollbar<'a> {
111    fn default() -> Self {
112        Self {
113            orientation: ScrollbarOrientation::default(),
114            thumb_symbol: DOUBLE_VERTICAL.thumb,
115            thumb_style: Style::default(),
116            track_symbol: DOUBLE_VERTICAL.track,
117            track_style: Style::default(),
118            begin_symbol: Some(DOUBLE_VERTICAL.begin),
119            begin_style: Style::default(),
120            end_symbol: Some(DOUBLE_VERTICAL.end),
121            end_style: Style::default(),
122            position: Default::default(),
123            offset: Default::default(),
124            content_length: Default::default(),
125            viewport_content_length: Default::default(),
126        }
127    }
128}
129
130impl<'a> Scrollbar<'a> {
131    pub fn new(orientation: ScrollbarOrientation) -> Self {
132        Self::default().orientation(orientation)
133    }
134
135    /// Sets the orientation of the scrollbar.
136    /// Resets the symbols to [`DOUBLE_VERTICAL`] or [`DOUBLE_HORIZONTAL`] based on orientation
137    pub fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
138        self.orientation = orientation;
139        let set = if self.is_vertical() {
140            DOUBLE_VERTICAL
141        } else {
142            DOUBLE_HORIZONTAL
143        };
144        self.symbols(set)
145    }
146
147    /// Shows scrollbar orientation
148    pub fn show_orientation(&self) -> ScrollbarOrientation {
149        self.orientation
150    }
151
152    /// Sets the orientation and symbols for the scrollbar from a [`Set`].
153    pub fn orientation_and_symbol(mut self, orientation: ScrollbarOrientation, set: Set) -> Self {
154        self.orientation = orientation;
155        self.symbols(set)
156    }
157
158    /// Sets the symbol that represents the thumb of the scrollbar.
159    pub fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
160        self.thumb_symbol = thumb_symbol;
161        self
162    }
163
164    /// Sets the style that represents the thumb of the scrollbar.
165    pub fn thumb_style(mut self, thumb_style: Style) -> Self {
166        self.thumb_style = thumb_style;
167        self
168    }
169
170    /// Sets the symbol that represents the track of the scrollbar.
171    pub fn track_symbol(mut self, track_symbol: &'a str) -> Self {
172        self.track_symbol = track_symbol;
173        self
174    }
175
176    /// Sets the style that is used for the track of the scrollbar.
177    pub fn track_style(mut self, track_style: Style) -> Self {
178        self.track_style = track_style;
179        self
180    }
181
182    /// Sets the symbol that represents the beginning of the scrollbar.
183    pub fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
184        self.begin_symbol = begin_symbol;
185        self
186    }
187
188    /// Sets the style that is used for the beginning of the scrollbar.
189    pub fn begin_style(mut self, begin_style: Style) -> Self {
190        self.begin_style = begin_style;
191        self
192    }
193
194    /// Sets the symbol that represents the end of the scrollbar.
195    pub fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
196        self.end_symbol = end_symbol;
197        self
198    }
199
200    /// Sets the style that is used for the end of the scrollbar.
201    pub fn end_style(mut self, end_style: Style) -> Self {
202        self.end_style = end_style;
203        self
204    }
205
206    /// Sets the symbols used for the various parts of the scrollbar from a [`Set`].
207    ///
208    /// ```text
209    /// <--▮------->
210    /// ^  ^   ^   ^
211    /// │  │   │   └ end
212    /// │  │   └──── track
213    /// │  └──────── thumb
214    /// └─────────── begin
215    /// ```
216    ///
217    /// Only sets begin_symbol and end_symbol if they already contain a value.
218    /// If begin_symbol and/or end_symbol were set to `None` explicitly, this function will respect
219    /// that choice.
220    pub fn symbols(mut self, symbol: Set) -> Self {
221        self.track_symbol = symbol.track;
222        self.thumb_symbol = symbol.thumb;
223        if self.begin_symbol.is_some() {
224            self.begin_symbol = Some(symbol.begin);
225        }
226        if self.end_symbol.is_some() {
227            self.end_symbol = Some(symbol.end);
228        }
229        self
230    }
231
232    /// Sets the style used for the various parts of the scrollbar from a [`Style`].
233    /// ```text
234    /// <--▮------->
235    /// ^  ^   ^   ^
236    /// │  │   │   └ end
237    /// │  │   └──── track
238    /// │  └──────── thumb
239    /// └─────────── begin
240    /// ```
241    pub fn style(mut self, style: Style) -> Self {
242        self.track_style = style;
243        self.thumb_style = style;
244        self.begin_style = style;
245        self.end_style = style;
246        self
247    }
248
249    fn is_vertical(&self) -> bool {
250        match self.orientation {
251            ScrollbarOrientation::VerticalRight | ScrollbarOrientation::VerticalLeft => true,
252            ScrollbarOrientation::HorizontalBottom | ScrollbarOrientation::HorizontalTop => false,
253        }
254    }
255
256    fn get_track_area(&self, area: Rect) -> Rect {
257        // Decrease track area if a begin arrow is present
258        let area = if self.begin_symbol.is_some() {
259            if self.is_vertical() {
260                // For vertical scrollbar, reduce the height by one
261                Rect::new(
262                    area.x,
263                    area.y + 1,
264                    area.width,
265                    area.height.saturating_sub(1),
266                )
267            } else {
268                // For horizontal scrollbar, reduce the width by one
269                Rect::new(
270                    area.x + 1,
271                    area.y,
272                    area.width.saturating_sub(1),
273                    area.height,
274                )
275            }
276        } else {
277            area
278        };
279        // Further decrease scrollbar area if an end arrow is present
280        if self.end_symbol.is_some() {
281            if self.is_vertical() {
282                // For vertical scrollbar, reduce the height by one
283                Rect::new(area.x, area.y, area.width, area.height.saturating_sub(1))
284            } else {
285                // For horizontal scrollbar, reduce the width by one
286                Rect::new(area.x, area.y, area.width.saturating_sub(1), area.height)
287            }
288        } else {
289            area
290        }
291    }
292
293    fn should_not_render(&self, track_start: u16, track_end: u16, content_length: u16) -> bool {
294        if track_end - track_start == 0 || content_length == 0 {
295            return true;
296        }
297        false
298    }
299
300    fn get_track_start_end(&self, area: Rect) -> (u16, u16, u16) {
301        match self.orientation {
302            ScrollbarOrientation::VerticalRight => {
303                (area.top(), area.bottom(), area.right().saturating_sub(1))
304            }
305            ScrollbarOrientation::VerticalLeft => (area.top(), area.bottom(), area.left()),
306            ScrollbarOrientation::HorizontalBottom => {
307                (area.left(), area.right(), area.bottom().saturating_sub(1))
308            }
309            ScrollbarOrientation::HorizontalTop => (area.left(), area.right(), area.top()),
310        }
311    }
312
313    /// Calculate the starting and ending position of a scrollbar thumb.
314    ///
315    /// The scrollbar thumb's position and size are determined based on the current state of the
316    /// scrollbar, and the dimensions of the scrollbar track.
317    ///
318    /// This function returns a tuple `(thumb_start, thumb_end)` where `thumb_start` is the position
319    /// at which the scrollbar thumb begins, and `thumb_end` is the position at which the
320    /// scrollbar thumb ends.
321    ///
322    /// The size of the thumb (i.e., `thumb_end - thumb_start`) is proportional to the ratio of the
323    /// viewport content length to the total content length.
324    ///
325    /// The position of the thumb (i.e., `thumb_start`) is proportional to the ratio of the current
326    /// scroll position to the total content length.
327    fn get_thumb_start_end(&self, track_start_end: (u16, u16)) -> (u16, u16) {
328        // let (track_start, track_end) = track_start_end;
329        // let track_size = track_end - track_start;
330        // let thumb_size =
331        //     ((state.viewport_content_length / state.content_length) * track_size).max(1);
332        // let thumb_start = (state.position / state.content_length) *
333        //                    state.viewport_content_length;
334        // let thumb_end = thumb_size + thumb_start;
335        // (thumb_start, thumb_end)
336
337        let (track_start, track_end) = track_start_end;
338
339        let viewport_content_length = if self.viewport_content_length == 0 {
340            track_end - track_start
341        } else {
342            self.viewport_content_length
343        };
344
345        let scroll_position_ratio = if self.offset == 0 {
346            self.position as f64 / self.content_length as f64
347        } else {
348            let gap = f64::from(self.content_length.saturating_sub(viewport_content_length));
349            if gap != 0.0 {
350                f64::from(self.offset) / gap
351            } else {
352                0.0
353            }
354        }
355        .min(1.0);
356
357        let thumb_size = (((viewport_content_length as f64 / self.content_length as f64)
358            * (track_end - track_start) as f64)
359            .round() as u16)
360            .max(1);
361
362        let track_size = (track_end - track_start).saturating_sub(thumb_size);
363
364        let thumb_start = track_start + (scroll_position_ratio * track_size as f64).round() as u16;
365
366        let thumb_end = thumb_start + thumb_size;
367
368        (thumb_start, thumb_end)
369    }
370
371    /// Sets the scroll position of the scrollbar and returns the modified ScrollbarState.
372    pub fn offset(&mut self, offset: u16) {
373        self.offset = offset;
374    }
375
376    /// Sets the scroll position of the scrollbar and returns the modified ScrollbarState.
377    pub fn position(&mut self, position: u16) {
378        self.position = position;
379    }
380
381    /// Sets the length of the scrollable content and returns the modified ScrollbarState.
382    pub fn content_length(&mut self, content_length: u16) {
383        self.content_length = content_length;
384    }
385
386    /// Sets the length of the viewport content and returns the modified ScrollbarState.
387    pub fn viewport_content_length(&mut self, viewport_content_length: u16) {
388        self.viewport_content_length = viewport_content_length;
389    }
390
391    /// Decrements the scroll position by one, ensuring it doesn't go below zero.
392    pub fn prev(&mut self) {
393        self.position = self.position.saturating_sub(1);
394    }
395
396    /// Increments the scroll position by one, ensuring it doesn't exceed the length of the content.
397    pub fn next(&mut self) {
398        self.position = self
399            .position
400            .saturating_add(1)
401            .clamp(0, self.content_length.saturating_sub(1))
402    }
403
404    /// Sets the scroll position to the start of the scrollable content.
405    pub fn first(&mut self) {
406        self.position = 0;
407    }
408
409    /// Sets the scroll position to the end of the scrollable content.
410    pub fn last(&mut self) {
411        self.position = self.content_length.saturating_sub(1)
412    }
413
414    /// Changes the scroll position based on the provided ScrollDirection.
415    pub fn scroll(&mut self, direction: ScrollDirection) {
416        match direction {
417            ScrollDirection::Forward => {
418                self.next();
419            }
420            ScrollDirection::Backward => {
421                self.prev();
422            }
423        }
424    }
425}
426
427impl<'a> Widget for Scrollbar<'a> {
428    fn render(&mut self, area: Rect, buf: &mut Buffer) {
429        //
430        // For ScrollbarOrientation::VerticalRight
431        //
432        //                   ┌───────── track_axis  (x)
433        //                   v
434        //   ┌───────────────┐
435        //   │               ║<──────── track_start (y1)
436        //   │               █
437        //   │               █
438        //   │               ║
439        //   │               ║<──────── track_end   (y2)
440        //   └───────────────┘
441        //
442        // For ScrollbarOrientation::HorizontalBottom
443        //
444        //   ┌───────────────┐
445        //   │               │
446        //   │               │
447        //   │               │
448        //   └═══███═════════┘<──────── track_axis  (y)
449        //    ^             ^
450        //    │             └────────── track_end   (x2)
451        //    │
452        //    └──────────────────────── track_start (x1)
453        //
454
455        // Find track_start, track_end, and track_axis
456        let area = self.get_track_area(area);
457        let (track_start, track_end, track_axis) = self.get_track_start_end(area);
458
459        if self.should_not_render(track_start, track_end, self.content_length) {
460            return;
461        }
462
463        let (thumb_start, thumb_end) = self.get_thumb_start_end((track_start, track_end));
464
465        for i in track_start..track_end {
466            let (style, symbol) = if i >= thumb_start && i < thumb_end {
467                (self.thumb_style, self.thumb_symbol)
468            } else {
469                (self.track_style, self.track_symbol)
470            };
471
472            if self.is_vertical() {
473                buf.set_string(track_axis, i, symbol, style);
474            } else {
475                buf.set_string(i, track_axis, symbol, style);
476            }
477        }
478
479        if let Some(s) = self.begin_symbol {
480            if self.is_vertical() {
481                buf.set_string(track_axis, track_start - 1, s, self.begin_style);
482            } else {
483                buf.set_string(track_start - 1, track_axis, s, self.begin_style);
484            }
485        };
486        if let Some(s) = self.end_symbol {
487            if self.is_vertical() {
488                buf.set_string(track_axis, track_end, s, self.end_style);
489            } else {
490                buf.set_string(track_end, track_axis, s, self.end_style);
491            }
492        }
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499    use crate::assert_buffer_eq;
500
501    #[test]
502    fn test_no_render_when_area_zero() {
503        let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0));
504        let mut scrollbar = Scrollbar::default();
505        scrollbar.position(0);
506        scrollbar.content_length(1);
507        scrollbar.render(buffer.area, &mut buffer);
508        assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
509    }
510
511    #[test]
512    fn test_no_render_when_height_zero_with_without_arrows() {
513        let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 0));
514        let mut scrollbar = Scrollbar::default();
515        scrollbar.position(0);
516        scrollbar.content_length(1);
517        scrollbar.render(buffer.area, &mut buffer);
518        assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
519
520        let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 0));
521        let mut scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
522        scrollbar.position(0);
523        scrollbar.content_length(1);
524        scrollbar.render(buffer.area, &mut buffer);
525        assert_buffer_eq!(buffer, Buffer::empty(buffer.area));
526    }
527
528    #[test]
529    fn test_no_render_when_height_too_small_for_arrows() {
530        let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
531        let mut scrollbar = Scrollbar::default();
532        scrollbar.position(0);
533        scrollbar.content_length(1);
534        scrollbar.render(buffer.area, &mut buffer);
535        assert_buffer_eq!(buffer, Buffer::with_lines(vec!["    ", "    "]));
536    }
537
538    #[test]
539    fn test_renders_all_thumbs_at_minimum_height_without_arrows() {
540        let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
541        let mut scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
542        scrollbar.position(0);
543        scrollbar.content_length(1);
544        scrollbar.render(buffer.area, &mut buffer);
545        assert_buffer_eq!(buffer, Buffer::with_lines(vec!["   █", "   █"]));
546    }
547
548    #[test]
549    fn test_renders_all_thumbs_at_minimum_height_and_minimum_width_without_arrows() {
550        let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 2));
551        let mut scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
552        scrollbar.position(0);
553        scrollbar.content_length(1);
554        scrollbar.render(buffer.area, &mut buffer);
555        assert_buffer_eq!(buffer, Buffer::with_lines(vec!["█", "█"]));
556    }
557
558    #[test]
559    fn test_renders_two_arrows_one_thumb_at_minimum_height_with_arrows() {
560        let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 3));
561        let mut scrollbar = Scrollbar::default();
562        scrollbar.position(0);
563        scrollbar.content_length(1);
564        scrollbar.render(buffer.area, &mut buffer);
565        assert_buffer_eq!(buffer, Buffer::with_lines(vec!["   ▲", "   █", "   ▼"]));
566    }
567
568    #[test]
569    fn test_no_render_when_content_length_zero() {
570        let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 2));
571        let mut scrollbar = Scrollbar::default();
572        scrollbar.position(0);
573        scrollbar.content_length(0);
574        scrollbar.render(buffer.area, &mut buffer);
575        assert_buffer_eq!(buffer, Buffer::with_lines(vec!["  ", "  "]));
576    }
577
578    #[test]
579    fn test_renders_all_thumbs_when_height_equals_content_length() {
580        let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 2));
581        let mut scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
582        scrollbar.position(0);
583        scrollbar.content_length(2);
584        scrollbar.render(buffer.area, &mut buffer);
585        assert_buffer_eq!(buffer, Buffer::with_lines(vec![" █", " █"]));
586
587        let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 8));
588        let mut scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
589        scrollbar.position(0);
590        scrollbar.content_length(8);
591        scrollbar.render(buffer.area, &mut buffer);
592        assert_buffer_eq!(
593            buffer,
594            Buffer::with_lines(vec![" █", " █", " █", " █", " █", " █", " █", " █"])
595        );
596    }
597
598    #[test]
599    fn test_renders_single_vertical_thumb_when_content_length_square_of_height() {
600        for i in 0..=17 {
601            let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 4));
602            let mut scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
603            scrollbar.position(i);
604            scrollbar.content_length(16);
605            scrollbar.render(buffer.area, &mut buffer);
606            let expected = if i <= 2 {
607                vec![" █", " ║", " ║", " ║"]
608            } else if i <= 7 {
609                vec![" ║", " █", " ║", " ║"]
610            } else if i <= 13 {
611                vec![" ║", " ║", " █", " ║"]
612            } else {
613                vec![" ║", " ║", " ║", " █"]
614            };
615            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
616        }
617    }
618
619    #[test]
620    fn test_renders_single_horizontal_thumb_when_content_length_square_of_width() {
621        for i in 0..=17 {
622            let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
623            let mut scrollbar = Scrollbar::default()
624                .begin_symbol(None)
625                .end_symbol(None)
626                .orientation(ScrollbarOrientation::HorizontalBottom);
627            scrollbar.position(i);
628            scrollbar.content_length(16);
629            scrollbar.render(buffer.area, &mut buffer);
630            let expected = if i <= 2 {
631                vec!["    ", "█═══"]
632            } else if i <= 7 {
633                vec!["    ", "═█══"]
634            } else if i <= 13 {
635                vec!["    ", "══█═"]
636            } else {
637                vec!["    ", "═══█"]
638            };
639            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
640        }
641    }
642
643    #[test]
644    fn test_renders_one_thumb_for_large_content_relative_to_height() {
645        let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
646        let mut scrollbar = Scrollbar::default()
647            .begin_symbol(None)
648            .end_symbol(None)
649            .orientation(ScrollbarOrientation::HorizontalBottom);
650        scrollbar.position(0);
651        scrollbar.content_length(1600);
652        scrollbar.render(buffer.area, &mut buffer);
653        let expected = vec!["    ", "█═══"];
654        assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
655
656        let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
657        let mut scrollbar = Scrollbar::default()
658            .begin_symbol(None)
659            .end_symbol(None)
660            .orientation(ScrollbarOrientation::HorizontalBottom);
661        scrollbar.position(800);
662        scrollbar.content_length(1600);
663        scrollbar.render(buffer.area, &mut buffer);
664        let expected = vec!["    ", "══█═"];
665        assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
666    }
667
668    #[test]
669    fn test_renders_two_thumb_default_symbols_for_content_double_height() {
670        for i in 0..=7 {
671            let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 4));
672            let mut scrollbar = Scrollbar::default().begin_symbol(None).end_symbol(None);
673            scrollbar.position(i);
674            scrollbar.content_length(8);
675            scrollbar.render(buffer.area, &mut buffer);
676            let expected = if i <= 1 {
677                vec![" █", " █", " ║", " ║"]
678            } else if i <= 5 {
679                vec![" ║", " █", " █", " ║"]
680            } else {
681                vec![" ║", " ║", " █", " █"]
682            };
683            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
684        }
685    }
686
687    #[test]
688    fn test_renders_two_thumb_custom_symbols_for_content_double_height() {
689        for i in 0..=7 {
690            let mut buffer = Buffer::empty(Rect::new(0, 0, 2, 4));
691            let mut scrollbar = Scrollbar::default()
692                .symbols(VERTICAL)
693                .begin_symbol(None)
694                .end_symbol(None);
695            scrollbar.position(i);
696            scrollbar.content_length(8);
697            scrollbar.render(buffer.area, &mut buffer);
698            let expected = if i <= 1 {
699                vec![" █", " █", " │", " │"]
700            } else if i <= 5 {
701                vec![" │", " █", " █", " │"]
702            } else {
703                vec![" │", " │", " █", " █"]
704            };
705            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
706        }
707    }
708
709    #[test]
710    fn test_renders_two_thumb_default_symbols_for_content_double_width() {
711        for i in 0..=7 {
712            let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
713            let mut scrollbar = Scrollbar::default()
714                .orientation(ScrollbarOrientation::HorizontalBottom)
715                .begin_symbol(None)
716                .end_symbol(None);
717            scrollbar.position(i);
718            scrollbar.content_length(8);
719            scrollbar.render(buffer.area, &mut buffer);
720            let expected = if i <= 1 {
721                vec!["    ", "██══"]
722            } else if i <= 5 {
723                vec!["    ", "═██═"]
724            } else {
725                vec!["    ", "══██"]
726            };
727            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
728        }
729    }
730
731    #[test]
732    fn test_renders_two_thumb_custom_symbols_for_content_double_width() {
733        for i in 0..=7 {
734            let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
735            let mut scrollbar = Scrollbar::default()
736                .orientation(ScrollbarOrientation::HorizontalBottom)
737                .symbols(HORIZONTAL)
738                .begin_symbol(None)
739                .end_symbol(None);
740            scrollbar.position(i);
741            scrollbar.content_length(8);
742            scrollbar.render(buffer.area, &mut buffer);
743            let expected = if i <= 1 {
744                vec!["    ", "██──"]
745            } else if i <= 5 {
746                vec!["    ", "─██─"]
747            } else {
748                vec!["    ", "──██"]
749            };
750            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
751        }
752    }
753
754    #[test]
755    fn test_rendering_viewport_content_length() {
756        for i in 0..=16 {
757            let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
758            let mut scrollbar = Scrollbar::default()
759                .orientation(ScrollbarOrientation::HorizontalBottom)
760                .begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
761                .end_symbol(Some(DOUBLE_HORIZONTAL.end));
762            scrollbar.position(i);
763            scrollbar.content_length(16);
764            scrollbar.viewport_content_length(4);
765            scrollbar.render(buffer.area, &mut buffer);
766            let expected = if i <= 1 {
767                vec!["        ", "◄██════►"]
768            } else if i <= 5 {
769                vec!["        ", "◄═██═══►"]
770            } else if i <= 9 {
771                vec!["        ", "◄══██══►"]
772            } else if i <= 13 {
773                vec!["        ", "◄═══██═►"]
774            } else {
775                vec!["        ", "◄════██►"]
776            };
777            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
778        }
779
780        for i in 0..=16 {
781            let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
782            let mut scrollbar = Scrollbar::default()
783                .orientation(ScrollbarOrientation::HorizontalBottom)
784                .begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
785                .end_symbol(Some(DOUBLE_HORIZONTAL.end));
786            scrollbar.position(i);
787            scrollbar.content_length(16);
788            scrollbar.viewport_content_length(1);
789            scrollbar.render(buffer.area, &mut buffer);
790            dbg!(i);
791            let expected = if i <= 1 {
792                vec!["        ", "◄█═════►"]
793            } else if i <= 4 {
794                vec!["        ", "◄═█════►"]
795            } else if i <= 7 {
796                vec!["        ", "◄══█═══►"]
797            } else if i <= 11 {
798                vec!["        ", "◄═══█══►"]
799            } else if i <= 14 {
800                vec!["        ", "◄════█═►"]
801            } else {
802                vec!["        ", "◄═════█►"]
803            };
804            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
805        }
806    }
807
808    #[test]
809    fn test_rendering_begin_end_arrows_horizontal_bottom() {
810        for i in 0..=16 {
811            let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
812            let mut scrollbar = Scrollbar::default()
813                .orientation(ScrollbarOrientation::HorizontalBottom)
814                .begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
815                .end_symbol(Some(DOUBLE_HORIZONTAL.end));
816            scrollbar.position(i);
817            scrollbar.content_length(16);
818            scrollbar.render(buffer.area, &mut buffer);
819            let expected = if i <= 1 {
820                vec!["        ", "◄██════►"]
821            } else if i <= 5 {
822                vec!["        ", "◄═██═══►"]
823            } else if i <= 9 {
824                vec!["        ", "◄══██══►"]
825            } else if i <= 13 {
826                vec!["        ", "◄═══██═►"]
827            } else {
828                vec!["        ", "◄════██►"]
829            };
830            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
831        }
832    }
833
834    #[test]
835    fn test_rendering_begin_end_arrows_horizontal_top() {
836        for i in 0..=16 {
837            let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
838            let mut scrollbar = Scrollbar::default()
839                .orientation(ScrollbarOrientation::HorizontalTop)
840                .begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
841                .end_symbol(Some(DOUBLE_HORIZONTAL.end));
842            scrollbar.position(i);
843            scrollbar.content_length(16);
844            scrollbar.render(buffer.area, &mut buffer);
845            let expected = if i <= 1 {
846                vec!["◄██════►", "        "]
847            } else if i <= 5 {
848                vec!["◄═██═══►", "        "]
849            } else if i <= 9 {
850                vec!["◄══██══►", "        "]
851            } else if i <= 13 {
852                vec!["◄═══██═►", "        "]
853            } else {
854                vec!["◄════██►", "        "]
855            };
856            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
857        }
858    }
859
860    #[test]
861    fn test_rendering_only_begin_arrow_horizontal_bottom() {
862        for i in 0..=16 {
863            let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
864            let mut scrollbar = Scrollbar::default()
865                .orientation(ScrollbarOrientation::HorizontalBottom)
866                .begin_symbol(Some(DOUBLE_HORIZONTAL.begin))
867                .end_symbol(None);
868            scrollbar.position(i);
869            scrollbar.content_length(16);
870            scrollbar.render(buffer.area, &mut buffer);
871            let expected = if i <= 1 {
872                vec!["        ", "◄███════"]
873            } else if i <= 5 {
874                vec!["        ", "◄═███═══"]
875            } else if i <= 9 {
876                vec!["        ", "◄══███══"]
877            } else if i <= 13 {
878                vec!["        ", "◄═══███═"]
879            } else {
880                vec!["        ", "◄════███"]
881            };
882            assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
883        }
884    }
885}