Skip to main content

tui_scrollbar/
lib.rs

1//! Smooth, fractional scrollbars for Ratatui. Part of the [tui-widgets] suite by [Joshka].
2//!
3//! ![ScrollBar demo](https://vhs.charm.sh/vhs-21HzyozMOar6SYjVDBrpOb.gif)
4//!
5//! [![Crate badge]][Crate]
6//! [![Docs Badge]][Docs]
7//! [![Deps Badge]][Dependency Status]
8//! [![License Badge]][License]
9//! [![Coverage Badge]][Coverage]
10//! [![Discord Badge]][Ratatui Discord]
11//!
12//! [GitHub Repository] · [API Docs] · [Examples] · [Changelog] · [Contributing] · [Crate source]
13//!
14//! Use this crate when you want scrollbars that communicate position and size more precisely than
15//! full-cell glyphs. The widget renders into a [`Buffer`] for a given [`Rect`] and stays reusable
16//! by implementing [`Widget`] for `&ScrollBar`.
17//!
18//! # Feature highlights
19//!
20//! - Fractional thumbs: render 1/8th-cell steps for clearer position/size feedback.
21//! - Arrow endcaps: optional start/end arrows with click-to-step support.
22//! - Backend-agnostic input: handle pointer + wheel events without tying to a backend.
23//! - Stateless rendering: render via [`Widget`] for `&ScrollBar` with external state.
24//! - Metrics-first: [`ScrollMetrics`] exposes pure geometry for testing and hit testing.
25//!
26//! # Why not Ratatui's scrollbar?
27//!
28//! Ratatui's built-in scrollbar favors simple full-cell glyphs and a stateful widget workflow.
29//! This crate chooses fractional glyphs for more precise thumbs, keeps rendering stateless, and
30//! exposes a small interaction API plus pure metrics so apps can control behavior explicitly.
31//!
32//! # Installation
33//!
34//! ```shell
35//! cargo add tui-scrollbar
36//! ```
37//!
38//! # Important
39//!
40//! - Zero lengths are treated as 1.
41//! - Arrow endcaps are disabled by default; configure them with [`ScrollBarArrows`].
42//! - The default [`GlyphSet`] hides the track using spaces; use [`GlyphSet::box_drawing`] or
43//!   [`GlyphSet::unicode`] for a visible track.
44//! - The default glyphs use [Symbols for Legacy Computing] for missing upper/right eighth blocks.
45//!   Use [`GlyphSet::unicode`] if you need only standard Unicode block elements.
46//!
47//! # Quick start
48//!
49//! This example renders a vertical [`ScrollBar`] into a [`Buffer`] using a fixed track size and
50//! offset. Use it as a minimal template when you just need a thumb and track on screen.
51//!
52//! ```rust
53//! use ratatui_core::buffer::Buffer;
54//! use ratatui_core::layout::Rect;
55//! use ratatui_core::widgets::Widget;
56//! use tui_scrollbar::{ScrollBar, ScrollBarArrows, ScrollLengths};
57//!
58//! let area = Rect::new(0, 0, 1, 6);
59//! let lengths = ScrollLengths {
60//!     content_len: 120,
61//!     viewport_len: 30,
62//! };
63//! let scrollbar = ScrollBar::vertical(lengths)
64//!     .arrows(ScrollBarArrows::Both)
65//!     .offset(45);
66//!
67//! let mut buffer = Buffer::empty(area);
68//! scrollbar.render(area, &mut buffer);
69//! ```
70//!
71//! # Conceptual overview
72//!
73//! The scrollbar works in three pieces:
74//!
75//! 1. Your app owns `content_len`, `viewport_len`, and `offset` (lengths along the scroll axis).
76//! 2. [`ScrollMetrics`] converts those values into a thumb position and size.
77//! 3. [`ScrollBar`] renders the track + thumb using fractional glyphs.
78//!
79//! Most apps update `offset` in response to input events and re-render each frame.
80//!
81//! ## Units and subcell conversions
82//!
83//! `content_len`, `viewport_len`, and `offset` are measured in logical units along the scroll
84//! axis. For many apps, those units are items or lines. The ratio between `viewport_len` and
85//! `content_len` is what matters, so any consistent unit works.
86//!
87//! # Styling
88//!
89//! Style the track, thumb, and arrow endcaps directly on [`ScrollBar`]. See [`ScrollBar`] for a
90//! full method map and more focused examples.
91//!
92//! Scrollbar glyphs are terminal characters. For visible track glyphs, thumb blocks, and arrow
93//! symbols, `Style::fg` colors the glyph itself and `Style::bg` colors the cell behind it. The
94//! default [`GlyphSet::minimal`] track renders spaces, so only the track background is visible in
95//! empty track cells. Visible track glyph sets, such as [`GlyphSet::box_drawing`] and
96//! [`GlyphSet::unicode`], can use foreground color for the track line. Thumb glyphs are block
97//! characters, so `Style::fg` is usually the useful knob for thumb color; `Style::bg` still colors
98//! the rest of the cell. With partial thumb glyphs, especially on a visible line track such as
99//! [`GlyphSet::box_drawing`], that background can show at the ends of the thumb. Match the thumb
100//! background to the track background unless that contrast is intentional.
101//!
102//! ```rust
103//! use ratatui_core::style::{Color, Style};
104//! use tui_scrollbar::{ScrollBar, ScrollBarArrows, ScrollLengths};
105//!
106//! let lengths = ScrollLengths {
107//!     content_len: 120,
108//!     viewport_len: 30,
109//! };
110//! let scrollbar = ScrollBar::vertical(lengths)
111//!     .arrows(ScrollBarArrows::Both)
112//!     .track_style(Style::new().bg(Color::Black))
113//!     .thumb_style(Style::new().fg(Color::Rgb(255, 158, 100)))
114//!     .arrow_style(Style::new().fg(Color::Yellow).bg(Color::Black));
115//! ```
116//!
117//! # Layout integration
118//!
119//! This example shows how to reserve a column for a vertical [`ScrollBar`] alongside your content.
120//! Use the same pattern for a horizontal [`ScrollBar`] by splitting rows instead of columns.
121//!
122//! ```rust,no_run
123//! use ratatui_core::buffer::Buffer;
124//! use ratatui_core::layout::{Constraint, Layout, Rect};
125//! use ratatui_core::widgets::Widget;
126//! use tui_scrollbar::{ScrollBar, ScrollLengths};
127//!
128//! let area = Rect::new(0, 0, 12, 6);
129//! let [content_area, bar_area] = area.layout(&Layout::horizontal([
130//!     Constraint::Fill(1),
131//!     Constraint::Length(1),
132//! ]));
133//!
134//! let lengths = ScrollLengths {
135//!     content_len: 400,
136//!     viewport_len: 80,
137//! };
138//! let scrollbar = ScrollBar::vertical(lengths).offset(0);
139//!
140//! let mut buffer = Buffer::empty(area);
141//! scrollbar.render(bar_area, &mut buffer);
142//! ```
143//!
144//! # Interaction loop
145//!
146//! This pattern assumes you have enabled mouse capture in your terminal backend and have the
147//! scrollbar [`Rect`] (`bar_area`) from your layout each frame. Keep a [`ScrollBarInteraction`] in
148//! your app state so drag operations persist across draws. Mouse events are handled via
149//! [`ScrollBar::handle_mouse_event`], which returns a [`ScrollCommand`] to apply.
150//!
151//! ```rust,no_run
152//! use ratatui_core::layout::Rect;
153//! use tui_scrollbar::{ScrollBar, ScrollBarInteraction, ScrollCommand, ScrollLengths};
154//!
155//! let bar_area = Rect::new(0, 0, 1, 10);
156//! let lengths = ScrollLengths {
157//!     content_len: 400,
158//!     viewport_len: 80,
159//! };
160//! let scrollbar = ScrollBar::vertical(lengths).offset(0);
161//! let mut interaction = ScrollBarInteraction::new();
162//! let mut offset = 0;
163//!
164//! # #[cfg(any(feature = "crossterm_0_28", feature = "crossterm_0_29"))]
165//! # {
166//! # use tui_scrollbar::crossterm::event::{self, Event};
167//! if let Event::Mouse(event) = event::read()? {
168//!     if let Some(ScrollCommand::SetOffset(next)) =
169//!         scrollbar.handle_mouse_event(bar_area, event, &mut interaction)
170//!     {
171//!         offset = next;
172//!     }
173//! }
174//! # }
175//! # let _ = offset;
176//! # Ok::<(), std::io::Error>(())
177//! ```
178//!
179//! # Metrics-first workflow
180//!
181//! This example shows how to compute thumb geometry without rendering via [`ScrollMetrics`]. It's
182//! useful for testing, hit testing, or when you want to inspect thumb sizing directly.
183//!
184//! ```rust
185//! use tui_scrollbar::{ScrollLengths, ScrollMetrics, SUBCELL};
186//!
187//! let track_cells = 12;
188//! let viewport_len = track_cells * SUBCELL;
189//! let content_len = viewport_len * 6;
190//! let lengths = ScrollLengths {
191//!     content_len,
192//!     viewport_len,
193//! };
194//! let metrics = ScrollMetrics::new(lengths, 0, track_cells as u16);
195//! assert!(metrics.thumb_len() >= SUBCELL);
196//! ```
197//!
198//! # Glyph selection
199//!
200//! [`GlyphSet`] controls the track and thumb characters. The default glyphs include
201//! [Symbols for Legacy Computing] so the thumb can render upper/right partial fills that are
202//! missing from the standard block set. Use [`GlyphSet::box_drawing`] for a visible line track, or
203//! [`GlyphSet::unicode`] when the terminal font should avoid [Symbols for Legacy Computing]
204//! glyphs.
205//!
206//! ```rust
207//! use tui_scrollbar::{GlyphSet, ScrollBar, ScrollLengths};
208//!
209//! let lengths = ScrollLengths {
210//!     content_len: 10,
211//!     viewport_len: 5,
212//! };
213//! let scrollbar = ScrollBar::vertical(lengths).glyph_set(GlyphSet::unicode());
214//! ```
215//!
216//! # API map
217//!
218//! ## Widgets
219//!
220//! - [`ScrollBar`]: renders vertical or horizontal scrollbars with fractional thumbs.
221//!
222//! ## Supporting types
223//!
224//! - [`ScrollBarInteraction`]: drag capture state for pointer interaction.
225//! - [`ScrollMetrics`]: pure math for thumb sizing and hit testing.
226//! - [`GlyphSet`]: glyph selection for track and thumb rendering.
227//! - [`ScrollBarArrows`]: arrow endcap configuration.
228//!
229//! ## Enums and events
230//!
231//! - [`ScrollBarOrientation`], [`ScrollBarArrows`], [`TrackClickBehavior`]
232//! - [`ScrollEvent`], [`ScrollCommand`]
233//! - [`PointerEvent`], [`PointerEventKind`], [`PointerButton`]
234//! - [`ScrollWheel`], [`ScrollAxis`]
235//!
236//! # See also
237//!
238//! - [tui-scrollbar examples]
239//! - [`scrollbar_styled` example]
240//! - [`scrollbar_mouse` example]
241//! - [`scrollbar` example]
242//! - [`Widget`]
243//! - [Ratatui]
244//!
245//! # More widgets
246//!
247//! For the full suite of widgets, see [tui-widgets].
248//!
249//! [Ratatui]: https://crates.io/crates/ratatui
250//! [Crate]: https://crates.io/crates/tui-scrollbar
251//! [Docs]: https://docs.rs/tui-scrollbar/
252//! [Dependency Status]: https://deps.rs/repo/github/ratatui/tui-widgets
253//! [Coverage]: https://app.codecov.io/gh/ratatui/tui-widgets
254//! [Ratatui Discord]: https://discord.gg/pMCEU9hNEj
255//! [Crate badge]: https://img.shields.io/crates/v/tui-scrollbar?logo=rust&style=flat
256//! [Docs Badge]: https://img.shields.io/docsrs/tui-scrollbar?logo=rust&style=flat
257//! [Deps Badge]: https://deps.rs/repo/github/ratatui/tui-widgets/status.svg?style=flat
258//! [License Badge]: https://img.shields.io/crates/l/tui-scrollbar?style=flat
259//! [License]: https://github.com/ratatui/tui-widgets/blob/main/LICENSE-MIT
260//! [Coverage Badge]:
261//!     https://img.shields.io/codecov/c/github/ratatui/tui-widgets?logo=codecov&style=flat
262//! [Discord Badge]: https://img.shields.io/discord/1070692720437383208?logo=discord&style=flat
263//! [GitHub Repository]: https://github.com/ratatui/tui-widgets
264//! [API Docs]: https://docs.rs/tui-scrollbar/
265//! [Examples]: https://github.com/ratatui/tui-widgets/tree/main/tui-scrollbar/examples
266//! [Changelog]: https://github.com/ratatui/tui-widgets/blob/main/tui-scrollbar/CHANGELOG.md
267//! [Contributing]: https://github.com/ratatui/tui-widgets/blob/main/CONTRIBUTING.md
268//! [Crate source]: https://github.com/ratatui/tui-widgets/blob/main/tui-scrollbar/src/lib.rs
269//! [`scrollbar_mouse` example]: https://github.com/ratatui/tui-widgets/tree/main/tui-scrollbar/examples/scrollbar_mouse.rs
270//! [`scrollbar_styled` example]: https://github.com/ratatui/tui-widgets/tree/main/tui-scrollbar/examples/scrollbar_styled.rs
271//! [`scrollbar` example]: https://github.com/ratatui/tui-widgets/tree/main/tui-scrollbar/examples/scrollbar.rs
272//! [tui-scrollbar examples]: https://github.com/ratatui/tui-widgets/tree/main/tui-scrollbar/examples
273//! [`Buffer`]: ratatui_core::buffer::Buffer
274//! [`Rect`]: ratatui_core::layout::Rect
275//! [`Widget`]: ratatui_core::widgets::Widget
276//! [Symbols for Legacy Computing]: https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing
277//!
278//! [Joshka]: https://github.com/joshka
279//! [tui-widgets]: https://crates.io/crates/tui-widgets
280#![cfg_attr(docsrs, doc = "\n# Feature flags\n")]
281#![cfg_attr(docsrs, doc = document_features::document_features!())]
282#![deny(missing_docs)]
283
284mod glyphs;
285mod input;
286mod lengths;
287mod metrics;
288mod scrollbar;
289
290/// Re-export of the selected crossterm version.
291///
292/// See `tui_scrollbar::crossterm` for the version selection rules.
293#[cfg(all(feature = "crossterm_0_28", not(feature = "crossterm_0_29")))]
294pub use ::crossterm_0_28 as crossterm;
295/// Re-export of the selected crossterm version.
296///
297/// This crate supports multiple crossterm versions via feature flags:
298///
299/// - `crossterm` selects the latest supported crossterm version (currently 0.29).
300/// - `crossterm_0_28` selects `crossterm` 0.28.
301/// - `crossterm_0_29` selects `crossterm` 0.29.
302///
303/// When both 0.28 and 0.29 are enabled, this re-export points to 0.29. Downstream code can use
304/// `tui_scrollbar::crossterm::event::*` without needing to match the dependency name/version
305/// selection logic.
306#[cfg(feature = "crossterm_0_29")]
307pub use ::crossterm_0_29 as crossterm;
308
309pub use crate::glyphs::GlyphSet;
310pub use crate::input::{
311    PointerButton, PointerEvent, PointerEventKind, ScrollAxis, ScrollBarInteraction, ScrollCommand,
312    ScrollEvent, ScrollWheel,
313};
314pub use crate::lengths::ScrollLengths;
315pub use crate::metrics::{CellFill, HitTest, ScrollMetrics, SUBCELL};
316pub use crate::scrollbar::{ScrollBar, ScrollBarArrows, ScrollBarOrientation, TrackClickBehavior};