tui_scrollbar/
lib.rs

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