Skip to main content

tui_scrollbar/scrollbar/
mod.rs

1//! Rendering and interaction for proportional scrollbars.
2//!
3//! This module provides the widget and interaction helpers. Glyph configuration lives in
4//! [`crate::GlyphSet`], and the pure scrollbar geometry lives in [`crate::ScrollMetrics`].
5//!
6//! # Local model
7//!
8//! 1. [`ScrollBar`] stores orientation, logical lengths, offset, styles, glyphs, and interaction
9//!    behavior.
10//! 2. [`ScrollMetrics`] converts the current lengths and offset into thumb geometry.
11//! 3. Rendering chooses track, thumb, and arrow glyphs from [`GlyphSet`].
12//! 4. Input helpers return [`ScrollCommand`] values for the application to apply.
13//!
14//! The scrollbar renders only a single row or column. If you provide a larger [`Rect`], it will
15//! still render into the first row/column of that area.
16//!
17//! ## Layout choices
18//!
19//! The widget treats the provided area as the track container. When arrows are enabled, one cell
20//! at each end is reserved for the endcaps and the remaining inner area is used for the thumb.
21//!
22//! Arrow endcaps are optional. When enabled, they consume one cell at the start/end of the track,
23//! and the thumb renders inside the remaining inner area.
24//!
25//! ## Interaction choices
26//!
27//! - The widget is stateless: it renders from inputs and returns commands instead of mutating
28//!   scroll offsets. This keeps control with the application.
29//! - Dragging stores a grab offset in subcells so the thumb does not jump under the pointer.
30//! - Arrow endcaps consume track space; the inner track is used for metrics and hit testing so
31//!   thumb math stays consistent regardless of arrows.
32//!
33//! Drag operations store a "grab offset" in subcells (1/8 of a cell; see [`crate::SUBCELL`]) so the
34//! thumb does not jump when the pointer starts dragging; subsequent drag events subtract that
35//! offset to keep the grab point stable.
36//!
37//! Wheel events are ignored unless their axis matches the scrollbar orientation. Positive deltas
38//! scroll down/right.
39//!
40//! The example below renders a vertical scrollbar into a buffer. It demonstrates how the widget
41//! uses `content_len`, `viewport_len`, and `offset` to decide the thumb size and position.
42//!
43//! ```rust
44//! use ratatui_core::buffer::Buffer;
45//! use ratatui_core::layout::Rect;
46//! use ratatui_core::widgets::Widget;
47//! use tui_scrollbar::{ScrollBar, ScrollLengths};
48//!
49//! let area = Rect::new(0, 0, 1, 4);
50//! let lengths = ScrollLengths {
51//!     content_len: 120,
52//!     viewport_len: 40,
53//! };
54//! let scrollbar = ScrollBar::vertical(lengths).offset(20);
55//!
56//! let mut buffer = Buffer::empty(area);
57//! scrollbar.render(area, &mut buffer);
58//! ```
59//!
60//! [`Rect`]: ratatui_core::layout::Rect
61
62use ratatui_core::layout::Rect;
63use ratatui_core::style::{Color, Style};
64
65use crate::glyphs::GlyphSet;
66
67mod interaction;
68mod render;
69
70/// Axis the scrollbar is laid out on.
71///
72/// Orientation determines whether the track length is derived from height or width.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ScrollBarOrientation {
75    /// A vertical scrollbar that fills a single column.
76    Vertical,
77    /// A horizontal scrollbar that fills a single row.
78    Horizontal,
79}
80
81/// Behavior when the user clicks on the track outside the thumb.
82///
83/// Page clicks move by `viewport_len`. Jump-to-click centers the thumb near the click.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum TrackClickBehavior {
86    /// Move by one viewport length toward the click position.
87    Page,
88    /// Jump the thumb toward the click position.
89    JumpToClick,
90}
91
92/// Which arrow endcaps to render on the track.
93#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
94pub enum ScrollBarArrows {
95    /// Do not render arrow endcaps.
96    #[default]
97    None,
98    /// Render the arrow at the start of the track (top/left).
99    Start,
100    /// Render the arrow at the end of the track (bottom/right).
101    End,
102    /// Render arrows at both ends of the track.
103    Both,
104}
105
106impl ScrollBarArrows {
107    const fn has_start(self) -> bool {
108        matches!(self, Self::Start | Self::Both)
109    }
110
111    const fn has_end(self) -> bool {
112        matches!(self, Self::End | Self::Both)
113    }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117enum ArrowHit {
118    Start,
119    End,
120}
121
122#[derive(Debug, Clone, Copy)]
123struct ArrowLayout {
124    track_area: Rect,
125    start: Option<(u16, u16)>,
126    end: Option<(u16, u16)>,
127}
128
129/// A proportional scrollbar widget with fractional thumb rendering.
130///
131/// # Method map
132///
133/// ## Construction
134///
135/// - [`Self::new`]
136/// - [`Self::orientation`]
137/// - [`Self::vertical`]
138/// - [`Self::horizontal`]
139///
140/// ## Position and lengths
141///
142/// - [`Self::content_len`]
143/// - [`Self::viewport_len`]
144/// - [`Self::offset`]
145///
146/// ## Appearance
147///
148/// - [`Self::track_style`]
149/// - [`Self::thumb_style`]
150/// - [`Self::arrow_style`]
151/// - [`Self::glyph_set`]
152/// - [`Self::arrows`]
153///
154/// ## Interaction
155///
156/// - [`Self::handle_event`]
157/// - [`Self::handle_mouse_event`], when a crossterm feature is enabled
158/// - [`Self::track_click_behavior`]
159/// - [`Self::scroll_step`]
160///
161/// # Important
162///
163/// - `content_len` and `viewport_len` are in logical units.
164/// - Zero values are treated as 1.
165/// - The scrollbar renders into a single row or column.
166///
167/// # Behavior
168///
169/// The thumb length is proportional to `viewport_len / content_len` and clamped to at least one
170/// full cell for usability. When `content_len <= viewport_len`, the thumb fills the track. Areas
171/// with zero width or height render nothing.
172///
173/// Arrow endcaps, when enabled, consume one cell at the start/end of the track. The thumb and
174/// track render in the remaining inner area. Clicking an arrow steps the offset by `scroll_step`.
175///
176/// # Styling
177///
178/// Track glyphs use `track_style`. Thumb glyphs use `thumb_style`. Arrow endcaps use
179/// `arrow_style`, which defaults to white on dark gray.
180///
181/// Scrollbar glyphs are terminal characters. For visible track glyphs, thumb blocks, and arrow
182/// symbols, `Style::fg` colors the glyph itself and `Style::bg` colors the cell behind it. The
183/// default [`GlyphSet::minimal`] track renders spaces, so only the track background is visible in
184/// empty track cells. Visible track glyph sets, such as [`GlyphSet::box_drawing`] and
185/// [`GlyphSet::unicode`], can use foreground color for the track line. Thumb glyphs are block
186/// characters, so `Style::fg` is usually the useful knob for thumb color; `Style::bg` still colors
187/// the rest of the cell. With partial thumb glyphs, especially on a visible line track such as
188/// [`GlyphSet::box_drawing`], that background can show at the ends of the thumb. Match the thumb
189/// background to the track background unless that contrast is intentional.
190///
191/// ```rust
192/// use ratatui_core::style::{Color, Style};
193/// use tui_scrollbar::{ScrollBar, ScrollBarArrows, ScrollLengths};
194///
195/// let lengths = ScrollLengths {
196///     content_len: 120,
197///     viewport_len: 30,
198/// };
199/// let scrollbar = ScrollBar::vertical(lengths)
200///     .arrows(ScrollBarArrows::Both)
201///     .track_style(Style::new().bg(Color::Black))
202///     .thumb_style(Style::new().fg(Color::Rgb(255, 158, 100)))
203///     .arrow_style(Style::new().fg(Color::Yellow).bg(Color::Black));
204/// ```
205///
206/// # State
207///
208/// This widget is stateless. Pointer drag state lives in [`crate::ScrollBarInteraction`].
209///
210/// # Examples
211///
212/// Minimal rendering only needs an area, lengths, an offset, and a buffer.
213///
214/// ```rust
215/// use ratatui_core::buffer::Buffer;
216/// use ratatui_core::layout::Rect;
217/// use ratatui_core::widgets::Widget;
218/// use tui_scrollbar::{ScrollBar, ScrollLengths};
219///
220/// let area = Rect::new(0, 0, 1, 5);
221/// let lengths = ScrollLengths {
222///     content_len: 200,
223///     viewport_len: 40,
224/// };
225/// let scrollbar = ScrollBar::vertical(lengths).offset(60);
226///
227/// let mut buffer = Buffer::empty(area);
228/// scrollbar.render(area, &mut buffer);
229/// ```
230///
231/// ## Updating offsets on input
232///
233/// This is the typical pattern for pointer handling: feed events to the scrollbar and apply the
234/// returned command to your stored offset.
235///
236/// ```rust,no_run
237/// use ratatui_core::layout::Rect;
238/// use tui_scrollbar::{
239///     PointerButton, PointerEvent, PointerEventKind, ScrollBar, ScrollBarInteraction,
240///     ScrollCommand, ScrollEvent, ScrollLengths,
241/// };
242///
243/// let area = Rect::new(0, 0, 1, 10);
244/// let lengths = ScrollLengths {
245///     content_len: 400,
246///     viewport_len: 80,
247/// };
248/// let scrollbar = ScrollBar::vertical(lengths).offset(0);
249/// let mut interaction = ScrollBarInteraction::new();
250/// let mut offset = 0;
251///
252/// let event = ScrollEvent::Pointer(PointerEvent {
253///     column: 0,
254///     row: 3,
255///     kind: PointerEventKind::Down,
256///     button: PointerButton::Primary,
257/// });
258///
259/// if let Some(ScrollCommand::SetOffset(next)) =
260///     scrollbar.handle_event(area, event, &mut interaction)
261/// {
262///     offset = next;
263/// }
264/// # let _ = offset;
265/// ```
266///
267/// ## Track click behavior
268///
269/// Choose between classic page jumps or jump-to-click behavior.
270///
271/// ```rust
272/// use tui_scrollbar::{ScrollBar, ScrollLengths, TrackClickBehavior};
273///
274/// let lengths = ScrollLengths {
275///     content_len: 10,
276///     viewport_len: 5,
277/// };
278/// let scrollbar =
279///     ScrollBar::vertical(lengths).track_click_behavior(TrackClickBehavior::JumpToClick);
280/// ```
281///
282/// ## Arrow endcaps
283///
284/// Arrow endcaps are optional. When enabled, they reserve one cell at each end of the track.
285///
286/// ```rust
287/// use tui_scrollbar::{ScrollBar, ScrollBarArrows, ScrollLengths};
288///
289/// let lengths = ScrollLengths {
290///     content_len: 120,
291///     viewport_len: 24,
292/// };
293/// let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::Both);
294/// ```
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct ScrollBar {
297    orientation: ScrollBarOrientation,
298    content_len: usize,
299    viewport_len: usize,
300    offset: usize,
301    track_style: Style,
302    thumb_style: Style,
303    arrow_style: Option<Style>,
304    glyph_set: GlyphSet,
305    arrows: ScrollBarArrows,
306    track_click_behavior: TrackClickBehavior,
307    scroll_step: usize,
308}
309
310impl ScrollBar {
311    /// Creates a scrollbar with the given orientation and lengths.
312    ///
313    /// Use [`Self::vertical`] or [`Self::horizontal`] when the orientation is known at the call
314    /// site.
315    ///
316    /// Zero lengths are treated as 1.
317    ///
318    /// ```rust
319    /// use tui_scrollbar::{ScrollBar, ScrollBarOrientation, ScrollLengths};
320    ///
321    /// let lengths = ScrollLengths {
322    ///     content_len: 120,
323    ///     viewport_len: 40,
324    /// };
325    /// let scrollbar = ScrollBar::new(ScrollBarOrientation::Vertical, lengths);
326    /// ```
327    pub fn new(orientation: ScrollBarOrientation, lengths: crate::ScrollLengths) -> Self {
328        Self {
329            orientation,
330            content_len: lengths.content_len,
331            viewport_len: lengths.viewport_len,
332            offset: 0,
333            track_style: Style::new().bg(Color::DarkGray),
334            thumb_style: Style::new().fg(Color::White).bg(Color::DarkGray),
335            arrow_style: Some(Style::new().fg(Color::White).bg(Color::DarkGray)),
336            glyph_set: GlyphSet::default(),
337            arrows: ScrollBarArrows::default(),
338            track_click_behavior: TrackClickBehavior::Page,
339            scroll_step: 1,
340        }
341    }
342
343    /// Creates a vertical scrollbar with the given content and viewport lengths.
344    ///
345    /// The track length is derived from the render area's height.
346    ///
347    /// ```rust
348    /// use tui_scrollbar::{ScrollBar, ScrollLengths};
349    ///
350    /// let lengths = ScrollLengths {
351    ///     content_len: 120,
352    ///     viewport_len: 40,
353    /// };
354    /// let scrollbar = ScrollBar::vertical(lengths);
355    /// ```
356    pub fn vertical(lengths: crate::ScrollLengths) -> Self {
357        Self::new(ScrollBarOrientation::Vertical, lengths)
358    }
359
360    /// Creates a horizontal scrollbar with the given content and viewport lengths.
361    ///
362    /// The track length is derived from the render area's width.
363    ///
364    /// ```rust
365    /// use tui_scrollbar::{ScrollBar, ScrollLengths};
366    ///
367    /// let lengths = ScrollLengths {
368    ///     content_len: 120,
369    ///     viewport_len: 40,
370    /// };
371    /// let scrollbar = ScrollBar::horizontal(lengths);
372    /// ```
373    pub fn horizontal(lengths: crate::ScrollLengths) -> Self {
374        Self::new(ScrollBarOrientation::Horizontal, lengths)
375    }
376
377    /// Sets the scrollbar orientation.
378    ///
379    /// This is mostly useful when sharing a builder chain and choosing the orientation later.
380    ///
381    /// ```rust
382    /// use tui_scrollbar::{ScrollBar, ScrollBarOrientation, ScrollLengths};
383    ///
384    /// let lengths = ScrollLengths {
385    ///     content_len: 120,
386    ///     viewport_len: 40,
387    /// };
388    /// let scrollbar = ScrollBar::vertical(lengths).orientation(ScrollBarOrientation::Horizontal);
389    /// ```
390    pub const fn orientation(mut self, orientation: ScrollBarOrientation) -> Self {
391        self.orientation = orientation;
392        self
393    }
394
395    /// Sets the total scrollable content length in logical units.
396    ///
397    /// Larger values shrink the thumb, while smaller values enlarge it.
398    ///
399    /// Zero values are treated as 1.
400    ///
401    /// ```rust
402    /// use tui_scrollbar::{ScrollBar, ScrollLengths};
403    ///
404    /// let lengths = ScrollLengths {
405    ///     content_len: 120,
406    ///     viewport_len: 40,
407    /// };
408    /// let scrollbar = ScrollBar::vertical(lengths).content_len(240);
409    /// ```
410    pub const fn content_len(mut self, content_len: usize) -> Self {
411        self.content_len = content_len;
412        self
413    }
414
415    /// Sets the visible viewport length in logical units.
416    ///
417    /// When `viewport_len >= content_len`, the thumb fills the track.
418    ///
419    /// Zero values are treated as 1.
420    ///
421    /// ```rust
422    /// use tui_scrollbar::{ScrollBar, ScrollLengths};
423    ///
424    /// let lengths = ScrollLengths {
425    ///     content_len: 120,
426    ///     viewport_len: 40,
427    /// };
428    /// let scrollbar = ScrollBar::vertical(lengths).viewport_len(60);
429    /// ```
430    pub const fn viewport_len(mut self, viewport_len: usize) -> Self {
431        self.viewport_len = viewport_len;
432        self
433    }
434
435    /// Sets the current scroll offset in logical units.
436    ///
437    /// Offsets are clamped to `content_len - viewport_len` during rendering and input handling,
438    /// not when this builder is called.
439    ///
440    /// ```rust
441    /// use tui_scrollbar::{ScrollBar, ScrollLengths};
442    ///
443    /// let lengths = ScrollLengths {
444    ///     content_len: 120,
445    ///     viewport_len: 40,
446    /// };
447    /// let scrollbar = ScrollBar::vertical(lengths).offset(30);
448    /// ```
449    pub const fn offset(mut self, offset: usize) -> Self {
450        self.offset = offset;
451        self
452    }
453
454    /// Sets the style applied to track glyphs.
455    ///
456    /// Track styling applies only to cells where the thumb is not rendered.
457    ///
458    /// ```rust
459    /// use ratatui_core::style::{Color, Style};
460    /// use tui_scrollbar::{ScrollBar, ScrollLengths};
461    ///
462    /// let lengths = ScrollLengths {
463    ///     content_len: 120,
464    ///     viewport_len: 40,
465    /// };
466    /// let scrollbar = ScrollBar::vertical(lengths).track_style(Style::new().bg(Color::Black));
467    /// ```
468    pub const fn track_style(mut self, style: Style) -> Self {
469        self.track_style = style;
470        self
471    }
472
473    /// Sets the style applied to thumb glyphs.
474    ///
475    /// Thumb styling applies to full and partial thumb cells. Thumb glyphs are block characters,
476    /// so `Style::fg` usually controls the visible thumb color. Use `Style::bg` only when the
477    /// cell behind the glyph should differ from the track. On partial thumb cells, the background
478    /// can show at the thumb ends.
479    ///
480    /// ```rust
481    /// use ratatui_core::style::{Color, Style};
482    /// use tui_scrollbar::{ScrollBar, ScrollLengths};
483    ///
484    /// let lengths = ScrollLengths {
485    ///     content_len: 120,
486    ///     viewport_len: 40,
487    /// };
488    /// let scrollbar =
489    ///     ScrollBar::vertical(lengths).thumb_style(Style::new().fg(Color::Rgb(255, 158, 100)));
490    /// ```
491    pub const fn thumb_style(mut self, style: Style) -> Self {
492        self.thumb_style = style;
493        self
494    }
495
496    /// Sets the style applied to arrow glyphs.
497    ///
498    /// Arrow endcaps render only when enabled with [`Self::arrows`]. If no arrow style is
499    /// configured internally, arrows fall back to the track style.
500    ///
501    /// ```rust
502    /// use ratatui_core::style::{Color, Style};
503    /// use tui_scrollbar::{ScrollBar, ScrollBarArrows, ScrollLengths};
504    ///
505    /// let lengths = ScrollLengths {
506    ///     content_len: 120,
507    ///     viewport_len: 40,
508    /// };
509    /// let scrollbar = ScrollBar::vertical(lengths)
510    ///     .arrows(ScrollBarArrows::Both)
511    ///     .arrow_style(Style::new().fg(Color::Yellow).bg(Color::Black));
512    /// ```
513    pub const fn arrow_style(mut self, style: Style) -> Self {
514        self.arrow_style = Some(style);
515        self
516    }
517
518    /// Selects the glyph set used to render the track and thumb.
519    ///
520    /// [`GlyphSet::symbols_for_legacy_computing`] uses [Symbols for Legacy Computing] for 1/8th
521    /// upper/right fills. Use [`GlyphSet::unicode`] if you want to avoid the legacy supplement, or
522    /// [`GlyphSet::box_drawing`] when you want a visible line track.
523    ///
524    /// [Symbols for Legacy Computing]: https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing
525    ///
526    /// ```rust
527    /// use tui_scrollbar::{GlyphSet, ScrollBar, ScrollLengths};
528    ///
529    /// let lengths = ScrollLengths {
530    ///     content_len: 120,
531    ///     viewport_len: 40,
532    /// };
533    /// let scrollbar = ScrollBar::vertical(lengths).glyph_set(GlyphSet::unicode());
534    /// ```
535    pub const fn glyph_set(mut self, glyph_set: GlyphSet) -> Self {
536        self.glyph_set = glyph_set;
537        self
538    }
539
540    /// Sets which arrow endcaps are rendered.
541    ///
542    /// Each enabled arrow reserves one cell at the start or end of the track.
543    ///
544    /// ```rust
545    /// use tui_scrollbar::{ScrollBar, ScrollBarArrows, ScrollLengths};
546    ///
547    /// let lengths = ScrollLengths {
548    ///     content_len: 120,
549    ///     viewport_len: 40,
550    /// };
551    /// let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::Both);
552    /// ```
553    pub const fn arrows(mut self, arrows: ScrollBarArrows) -> Self {
554        self.arrows = arrows;
555        self
556    }
557
558    /// Sets behavior for clicks on the track outside the thumb.
559    ///
560    /// Use [`TrackClickBehavior::Page`] for classic page-up/down behavior, or
561    /// [`TrackClickBehavior::JumpToClick`] to move the thumb toward the click.
562    ///
563    /// This does not affect clicks on the thumb or arrow endcaps.
564    ///
565    /// ```rust
566    /// use tui_scrollbar::{ScrollBar, ScrollLengths, TrackClickBehavior};
567    ///
568    /// let lengths = ScrollLengths {
569    ///     content_len: 120,
570    ///     viewport_len: 40,
571    /// };
572    /// let scrollbar =
573    ///     ScrollBar::vertical(lengths).track_click_behavior(TrackClickBehavior::JumpToClick);
574    /// ```
575    pub const fn track_click_behavior(mut self, behavior: TrackClickBehavior) -> Self {
576        self.track_click_behavior = behavior;
577        self
578    }
579
580    /// Sets the scroll step used for wheel events.
581    ///
582    /// The wheel delta is multiplied by this value (in your logical units) and then clamped. A
583    /// step of 0 is normalized to 1.
584    ///
585    /// ```rust
586    /// use tui_scrollbar::{ScrollBar, ScrollLengths};
587    ///
588    /// let lengths = ScrollLengths {
589    ///     content_len: 120,
590    ///     viewport_len: 40,
591    /// };
592    /// let scrollbar = ScrollBar::vertical(lengths).scroll_step(8);
593    /// ```
594    pub fn scroll_step(mut self, step: usize) -> Self {
595        self.scroll_step = step.max(1);
596        self
597    }
598
599    /// Computes the inner track area and arrow cell positions for this orientation.
600    fn arrow_layout(&self, area: Rect) -> ArrowLayout {
601        let mut track_area = area;
602        let (start, end) = match self.orientation {
603            ScrollBarOrientation::Vertical => {
604                let start_enabled = self.arrows.has_start() && area.height > 0;
605                let end_enabled = self.arrows.has_end() && area.height > start_enabled as u16;
606                let start = start_enabled.then_some((area.x, area.y));
607                let end = end_enabled
608                    .then_some((area.x, area.y.saturating_add(area.height).saturating_sub(1)));
609                if start_enabled {
610                    track_area.y = track_area.y.saturating_add(1);
611                    track_area.height = track_area.height.saturating_sub(1);
612                }
613                if end_enabled {
614                    track_area.height = track_area.height.saturating_sub(1);
615                }
616                (start, end)
617            }
618            ScrollBarOrientation::Horizontal => {
619                let start_enabled = self.arrows.has_start() && area.width > 0;
620                let end_enabled = self.arrows.has_end() && area.width > start_enabled as u16;
621                let start = start_enabled.then_some((area.x, area.y));
622                let end = end_enabled
623                    .then_some((area.x.saturating_add(area.width).saturating_sub(1), area.y));
624                if start_enabled {
625                    track_area.x = track_area.x.saturating_add(1);
626                    track_area.width = track_area.width.saturating_sub(1);
627                }
628                if end_enabled {
629                    track_area.width = track_area.width.saturating_sub(1);
630                }
631                (start, end)
632            }
633        };
634
635        ArrowLayout {
636            track_area,
637            start,
638            end,
639        }
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use ratatui_core::style::{Color, Style};
646
647    use super::*;
648    use crate::ScrollLengths;
649    use crate::glyphs::GlyphSet;
650
651    #[test]
652    fn builder_methods_update_fields() {
653        let lengths = ScrollLengths {
654            content_len: 10,
655            viewport_len: 4,
656        };
657        let track_style = Style::new().fg(Color::Red);
658        let thumb_style = Style::new().bg(Color::Blue);
659        let arrow_style = Style::new().fg(Color::Green);
660        let glyphs = GlyphSet::unicode();
661
662        let scrollbar = ScrollBar::new(ScrollBarOrientation::Vertical, lengths)
663            .orientation(ScrollBarOrientation::Horizontal)
664            .content_len(20)
665            .viewport_len(5)
666            .offset(3)
667            .track_style(track_style)
668            .thumb_style(thumb_style)
669            .arrow_style(arrow_style)
670            .glyph_set(glyphs.clone())
671            .arrows(ScrollBarArrows::End)
672            .track_click_behavior(TrackClickBehavior::JumpToClick)
673            .scroll_step(0);
674
675        assert_eq!(scrollbar.orientation, ScrollBarOrientation::Horizontal);
676        assert_eq!(scrollbar.content_len, 20);
677        assert_eq!(scrollbar.viewport_len, 5);
678        assert_eq!(scrollbar.offset, 3);
679        assert_eq!(scrollbar.track_style, track_style);
680        assert_eq!(scrollbar.thumb_style, thumb_style);
681        assert_eq!(scrollbar.arrow_style, Some(arrow_style));
682        assert_eq!(scrollbar.glyph_set, glyphs);
683        assert_eq!(scrollbar.arrows, ScrollBarArrows::End);
684        assert_eq!(
685            scrollbar.track_click_behavior,
686            TrackClickBehavior::JumpToClick
687        );
688        assert_eq!(scrollbar.scroll_step, 1);
689    }
690
691    #[test]
692    fn constructors_set_orientation() {
693        let lengths = ScrollLengths {
694            content_len: 10,
695            viewport_len: 4,
696        };
697        let vertical = ScrollBar::vertical(lengths);
698        let horizontal = ScrollBar::horizontal(lengths);
699
700        assert_eq!(vertical.orientation, ScrollBarOrientation::Vertical);
701        assert_eq!(horizontal.orientation, ScrollBarOrientation::Horizontal);
702    }
703
704    #[test]
705    fn reserves_track_cells_for_arrows() {
706        let lengths = ScrollLengths {
707            content_len: 10,
708            viewport_len: 4,
709        };
710        let scrollbar = ScrollBar::vertical(lengths).arrows(ScrollBarArrows::Both);
711        let area = Rect::new(0, 0, 1, 5);
712        let layout = scrollbar.arrow_layout(area);
713
714        assert_eq!(layout.track_area.height, 3);
715        assert_eq!(layout.start, Some((area.x, area.y)));
716        assert_eq!(
717            layout.end,
718            Some((area.x, area.y.saturating_add(area.height).saturating_sub(1)))
719        );
720    }
721
722    #[test]
723    fn reserves_track_cells_for_horizontal_arrows() {
724        let lengths = ScrollLengths {
725            content_len: 10,
726            viewport_len: 4,
727        };
728        let scrollbar = ScrollBar::horizontal(lengths).arrows(ScrollBarArrows::Both);
729        let area = Rect::new(0, 0, 5, 1);
730        let layout = scrollbar.arrow_layout(area);
731
732        assert_eq!(layout.track_area.width, 3);
733        assert_eq!(layout.start, Some((area.x, area.y)));
734        assert_eq!(
735            layout.end,
736            Some((area.x.saturating_add(area.width).saturating_sub(1), area.y))
737        );
738    }
739}