chore(core): move core types to ratatui-core (#1460)

The buffer, layout, style, symbols, text, and the top level of widgets
modules are moved to ratatui-core. This is the first step in
modularizing the library so that the core types can be used in other
projects without the need for the backend / widgets types.

This helps reduce the need for updating other crates as often due to
semver changes outside of the core types.

---------

Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
This commit is contained in:
Josh McKinney 2024-11-05 22:10:49 -08:00 committed by GitHub
parent 0a47ebd94b
commit 98df774d7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1073 additions and 928 deletions

View file

@ -10,6 +10,8 @@ GitHub with a [breaking change] label.
This is a quick summary of the sections below:
- [Unreleased](#unreleased)
- The `From` impls for backend types are now replaced with more specific traits
- [v0.29.0](#v0290)
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
- Removed public fields from `Rect` iterators
@ -104,7 +106,7 @@ let crossterm_attribute = crossterm::style::types::Attribute::Bold;
+ let crossterm_attribute = ratatui_modifier.into_crossterm();
```
Similar conversions for `ContentStyle` -> `Style` and `Attributes` -> `Modifier` exist for
Similar conversions for `ContentStyle` -> `Style` and `Attributes` -> `Modifier` exist for
Crossterm and the various Termion and Termwiz types as well.
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0)

35
Cargo.lock generated
View file

@ -2090,9 +2090,7 @@ version = "0.29.0"
dependencies = [
"argh",
"bitflags 2.6.0",
"cassowary",
"color-eyre",
"compact_str",
"criterion",
"crossterm",
"document-features",
@ -2102,13 +2100,12 @@ dependencies = [
"indoc",
"instability",
"itertools 0.13.0",
"lru",
"octocrab",
"palette",
"paste",
"pretty_assertions",
"rand 0.8.5",
"rand_chacha 0.3.1",
"ratatui-core",
"rstest",
"serde",
"serde_json",
@ -2121,6 +2118,28 @@ dependencies = [
"tracing-appender",
"tracing-subscriber",
"unicode-segmentation",
"unicode-width 0.2.0",
]
[[package]]
name = "ratatui-core"
version = "0.3.0"
dependencies = [
"bitflags 2.6.0",
"cassowary",
"compact_str",
"indoc",
"instability",
"itertools 0.13.0",
"lru",
"palette",
"paste",
"pretty_assertions",
"rstest",
"serde",
"serde_json",
"strum",
"unicode-segmentation",
"unicode-truncate",
"unicode-width 0.2.0",
]
@ -2454,18 +2473,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.213"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.213"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [
"proc-macro2",
"quote",

View file

@ -1,7 +1,7 @@
[workspace]
resolver = "2"
members = ["ratatui", "xtask"]
default-members = ["ratatui"]
members = ["ratatui", "ratatui-core", "xtask"]
default-members = ["ratatui", "ratatui-core"]
[workspace.package]
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
@ -23,6 +23,20 @@ exclude = [
edition = "2021"
rust-version = "1.74.0"
[workspace.dependencies]
bitflags = "2.6.0"
indoc = "2.0.5"
instability = "0.3.1"
itertools = "0.13.0"
pretty_assertions = "1.4.1"
ratatui-core = { path = "ratatui-core" }
rstest = "0.23.0"
serde = { version = "1.0.214", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
unicode-segmentation = "1.12.0"
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
unicode-width = "=0.2.0"
# Improve benchmark consistency
[profile.bench]
codegen-units = 1

70
ratatui-core/Cargo.toml Normal file
View file

@ -0,0 +1,70 @@
[package]
name = "ratatui-core"
description = """
Core types and traits for the Ratatui Terminal UI library.
Widget libraries should use this crate. Applications should use the main Ratatui crate.
"""
version = "0.3.0"
authors.workspace = true
documentation.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
readme.workspace = true
license.workspace = true
exclude.workspace = true
edition.workspace = true
rust-version.workspace = true
[features]
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
palette = ["dep:palette"]
## enables the backend code that sets the underline color. Underline color is only supported by
## the Crossterm backend, and is not supported on Windows 7.
underline-color = []
## enables serialization and deserialization of style and color types using the [`serde`] crate.
## This is useful if you want to save themes to a file.
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
## enable all unstable features.
unstable = ["unstable-widget-ref"]
## enables the [`WidgetRef`] and [`StatefulWidgetRef`] traits which are experimental and may change
## in the future.
##
## [`WidgetRef`]: widgets::WidgetRef
## [`StatefulWidgetRef`]: widgets::StatefulWidgetRef
unstable-widget-ref = []
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
compact_str = "0.8.0"
instability.workspace = true
indoc.workspace = true
itertools.workspace = true
lru = "0.12.0"
palette = { version = "0.7.6", optional = true }
paste = "1.0.2"
serde = { workspace = true, optional = true }
strum.workspace = true
unicode-segmentation.workspace = true
unicode-truncate = "1"
unicode-width.workspace = true
[dev-dependencies]
rstest.workspace = true
pretty_assertions.workspace = true
serde_json = "1.0.132"
[lints.clippy]
# we often split up a module into multiple files with the main type in a file named after the
# module, so we want to allow this pattern
module_inception = "allow"

34
ratatui-core/README.md Normal file
View file

@ -0,0 +1,34 @@
# ratatui-core
[![Crates.io](https://img.shields.io/crates/v/ratatui-core)](https://crates.io/crates/ratatui-core)
[![Documentation](https://docs.rs/ratatui-core/badge.svg)](https://docs.rs/ratatui-core)
[![License](https://img.shields.io/crates/l/ratatui-core)](../LICENSE)
## Overview
**ratatui-core** is the core library of the [ratatui](https://github.com/ratatui/ratatui) project,
providing the essential building blocks for creating rich terminal user interfaces in Rust.
### Why ratatui-core?
The `ratatui-core` crate is split from the main [`ratatui`](https://crates.io/crates/ratatui) crate
to offer better stability for widget library authors. Widget libraries should generally depend on
`ratatui-core`, benefiting from a stable API and reducing the need for frequent updates.
Applications, on the other hand, should depend on the main `ratatui` crate, which includes built-in
widgets and additional features.
## Installation
Add `ratatui-core` to your `Cargo.toml`:
```shell
cargo add ratatui-core
```
## Contributing
We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md) guide for more details on how to get involved.
## License
This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.

View file

@ -23,7 +23,7 @@ use crate::{
/// # Examples:
///
/// ```
/// use ratatui::{
/// use ratatui_core::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// style::{Color, Style},
@ -168,7 +168,7 @@ impl Buffer {
/// # Examples
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// };
@ -199,7 +199,7 @@ impl Buffer {
/// # Examples
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// style::{Color, Style},
@ -227,7 +227,7 @@ impl Buffer {
/// # Examples
///
/// ```
/// use ratatui::{buffer::Buffer, layout::Rect};
/// use ratatui_core::{buffer::Buffer, layout::Rect};
///
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
/// // Global coordinates to the top corner of this buffer's area
@ -239,7 +239,7 @@ impl Buffer {
/// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// use ratatui::{buffer::Buffer, layout::Rect};
/// use ratatui_core::{buffer::Buffer, layout::Rect};
///
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
@ -282,7 +282,7 @@ impl Buffer {
/// # Examples
///
/// ```
/// use ratatui::{buffer::Buffer, layout::Rect};
/// use ratatui_core::{buffer::Buffer, layout::Rect};
///
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
@ -295,7 +295,7 @@ impl Buffer {
/// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// use ratatui::{buffer::Buffer, layout::Rect};
/// use ratatui_core::{buffer::Buffer, layout::Rect};
///
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
/// let buffer = Buffer::empty(rect);
@ -526,7 +526,7 @@ impl<P: Into<Position>> Index<P> for Buffer {
/// # Examples
///
/// ```
/// use ratatui::{
/// use ratatui_core::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// };
@ -556,7 +556,7 @@ impl<P: Into<Position>> IndexMut<P> for Buffer {
/// # Examples
///
/// ```
/// use ratatui::{
/// use ratatui_core::{
/// buffer::{Buffer, Cell},
/// layout::{Position, Rect},
/// };

View file

@ -26,7 +26,7 @@ use strum::EnumIs;
/// `Constraint` provides helper methods to create lists of constraints from various input formats.
///
/// ```rust
/// use ratatui::layout::Constraint;
/// use ratatui_core::layout::Constraint;
///
/// // Create a layout with specified lengths for each element
/// let constraints = Constraint::from_lengths([10, 20, 10]);
@ -224,7 +224,7 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_lengths([1, 2, 3]);
@ -242,7 +242,7 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
@ -260,7 +260,7 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_percentages([25, 50, 25]);
@ -278,7 +278,7 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_maxes([1, 2, 3]);
@ -296,7 +296,7 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_mins([1, 2, 3]);
@ -314,7 +314,7 @@ impl Constraint {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # let area = Rect::default();
/// let constraints = Constraint::from_fills([1, 2, 3]);
@ -337,7 +337,7 @@ impl From<u16> for Constraint {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Direction, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Direction, Layout, Rect};
///
/// # let area = Rect::default();
/// let layout = Layout::new(Direction::Vertical, [1, 2, 3]).split(area);

View file

@ -148,20 +148,18 @@ impl From<i16> for Spacing {
/// # Example
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::{Constraint, Direction, Layout, Rect},
/// widgets::Paragraph,
/// Frame,
/// text::Text,
/// widgets::Widget,
/// };
///
/// fn render(frame: &mut Frame, area: Rect) {
/// let layout = Layout::new(
/// Direction::Vertical,
/// [Constraint::Length(5), Constraint::Min(0)],
/// )
/// .split(Rect::new(0, 0, 10, 10));
/// frame.render_widget(Paragraph::new("foo"), layout[0]);
/// frame.render_widget(Paragraph::new("bar"), layout[1]);
/// fn render(area: Rect, buf: &mut ratatui_core::buffer::Buffer) {
/// let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
/// let [left, right] = layout.areas(area);
/// Text::from("foo").render(left, buf);
/// Text::from("bar").render(right, buf);
/// }
/// ```
///
@ -206,7 +204,7 @@ impl Layout {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Direction, Layout};
/// use ratatui_core::layout::{Constraint, Direction, Layout};
///
/// Layout::new(
/// Direction::Horizontal,
@ -240,7 +238,7 @@ impl Layout {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout};
/// use ratatui_core::layout::{Constraint, Layout};
///
/// let layout = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
/// ```
@ -260,7 +258,7 @@ impl Layout {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout};
/// use ratatui_core::layout::{Constraint, Layout};
///
/// let layout = Layout::horizontal([Constraint::Length(5), Constraint::Min(0)]);
/// ```
@ -289,7 +287,7 @@ impl Layout {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Direction, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Direction, Layout, Rect};
///
/// let layout = Layout::default()
/// .direction(Direction::Horizontal)
@ -324,7 +322,7 @@ impl Layout {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// let layout = Layout::default()
/// .constraints([
@ -369,7 +367,7 @@ impl Layout {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// let layout = Layout::default()
/// .constraints([Constraint::Min(0)])
@ -391,7 +389,7 @@ impl Layout {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// let layout = Layout::default()
/// .constraints([Constraint::Min(0)])
@ -410,7 +408,7 @@ impl Layout {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Constraint, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// let layout = Layout::default()
/// .constraints([Constraint::Min(0)])
@ -442,7 +440,7 @@ impl Layout {
/// In this example, the items in the layout will be aligned to the start.
///
/// ```rust
/// use ratatui::layout::{Constraint::*, Flex, Layout};
/// use ratatui_core::layout::{Constraint::*, Flex, Layout};
///
/// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).flex(Flex::Start);
/// ```
@ -451,7 +449,7 @@ impl Layout {
/// space.
///
/// ```rust
/// use ratatui::layout::{Constraint::*, Flex, Layout};
/// use ratatui_core::layout::{Constraint::*, Flex, Layout};
///
/// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).flex(Flex::Legacy);
/// ```
@ -479,7 +477,7 @@ impl Layout {
/// In this example, the spacing between each item in the layout is set to 2 cells.
///
/// ```rust
/// use ratatui::layout::{Constraint::*, Layout};
/// use ratatui_core::layout::{Constraint::*, Layout};
///
/// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).spacing(2);
/// ```
@ -488,7 +486,7 @@ impl Layout {
/// three segments will have an overlapping border.
///
/// ```rust
/// use ratatui::layout::{Constraint::*, Layout};
/// use ratatui_core::layout::{Constraint::*, Layout};
/// let layout = Layout::horizontal([Length(20), Length(20), Length(20)]).spacing(-1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
@ -515,16 +513,14 @@ impl Layout {
/// # Examples
///
/// ```rust
/// use ratatui::{layout::{Layout, Constraint}, Frame};
/// use ratatui_core::layout::{Layout, Constraint, Rect};
///
/// # fn render(frame: &mut Frame) {
/// let area = frame.area();
/// let area = Rect::new(0, 0, 10, 10);
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = layout.areas(area);
///
/// // or explicitly specify the number of constraints:
/// let areas = layout.areas::<2>(area);
/// # }
pub fn areas<const N: usize>(&self, area: Rect) -> [Rect; N] {
let (areas, _) = self.split_with_spacers(area);
areas.as_ref().try_into().expect("invalid number of rects")
@ -548,17 +544,16 @@ impl Layout {
/// # Examples
///
/// ```rust
/// use ratatui::{layout::{Layout, Constraint}, Frame};
/// use ratatui_core::layout::{Constraint, Layout, Rect};
///
/// # fn render(frame: &mut Frame) {
/// let area = frame.area();
/// let area = Rect::new(0, 0, 10, 10);
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = layout.areas(area);
/// let [before, inbetween, after] = layout.spacers(area);
///
/// // or explicitly specify the number of constraints:
/// let spacers = layout.spacers::<2>(area);
/// # }
/// let spacers = layout.spacers::<3>(area);
/// ```
pub fn spacers<const N: usize>(&self, area: Rect) -> [Rect; N] {
let (_, spacers) = self.split_with_spacers(area);
spacers
@ -588,7 +583,7 @@ impl Layout {
/// # Examples
///
/// ```
/// use ratatui::layout::{Constraint, Direction, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Direction, Layout, Rect};
/// let layout = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)])
@ -620,7 +615,7 @@ impl Layout {
/// # Examples
///
/// ```
/// use ratatui::layout::{Constraint, Direction, Layout, Rect};
/// use ratatui_core::layout::{Constraint, Direction, Layout, Rect};
///
/// let (areas, spacers) = Layout::default()
/// .direction(Direction::Vertical)
@ -1413,8 +1408,12 @@ mod tests {
use crate::{
buffer::Buffer,
layout::{Constraint, Constraint::*, Direction, Flex, Layout, Rect},
widgets::{Paragraph, Widget},
layout::{
Constraint::{self, *},
Direction, Flex, Layout, Rect,
},
text::Text,
widgets::Widget,
};
/// Test that the given constraints applied to the given area result in the expected layout.
@ -1436,7 +1435,7 @@ mod tests {
let mut buffer = Buffer::empty(area);
for (c, &area) in ('a'..='z').take(constraints.len()).zip(layout.iter()) {
let s = c.to_string().repeat(area.width as usize);
Paragraph::new(s).render(area, &mut buffer);
Text::from(s).render(area, &mut buffer);
}
assert_eq!(buffer, Buffer::with_lines([expected]));
}

View file

@ -12,7 +12,7 @@ use crate::layout::Rect;
/// # Examples
///
/// ```
/// use ratatui::layout::{Position, Rect};
/// use ratatui_core::layout::{Position, Rect};
///
/// // the following are all equivalent
/// let position = Position { x: 1, y: 2 };

View file

@ -64,7 +64,7 @@ impl Rect {
/// # Examples
///
/// ```
/// use ratatui::layout::Rect;
/// use ratatui_core::layout::Rect;
///
/// let rect = Rect::new(1, 2, 3, 4);
/// ```
@ -213,7 +213,7 @@ impl Rect {
/// # Examples
///
/// ```rust
/// use ratatui::layout::{Position, Rect};
/// use ratatui_core::layout::{Position, Rect};
///
/// let rect = Rect::new(1, 2, 3, 4);
/// assert!(rect.contains(Position { x: 1, y: 2 }));
@ -243,12 +243,11 @@ impl Rect {
/// # Examples
///
/// ```rust
/// use ratatui::{layout::Rect, Frame};
/// use ratatui_core::layout::Rect;
///
/// # fn render(frame: &mut Frame) {
/// let area = frame.area();
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
/// # }
/// let area = Rect::new(0, 0, 100, 100);
/// let rect = Rect::new(80, 80, 30, 30).clamp(area);
/// assert_eq!(rect, Rect::new(70, 70, 30, 30));
/// ```
#[must_use = "method returns the modified value"]
pub fn clamp(self, other: Self) -> Self {
@ -264,7 +263,7 @@ impl Rect {
/// # Example
///
/// ```
/// use ratatui::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// for row in area.rows() {
@ -281,14 +280,11 @@ impl Rect {
/// # Example
///
/// ```
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// widgets::{Block, Borders, Widget},
/// };
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// if let Some(left) = area.columns().next() {
/// Block::new().borders(Borders::LEFT).render(left, buf);
/// for (i, column) in area.columns().enumerate() {
/// Text::from(format!("{}", i)).render(column, buf);
/// }
/// }
/// ```
@ -303,7 +299,7 @@ impl Rect {
/// # Example
///
/// ```
/// use ratatui::{buffer::Buffer, layout::Rect};
/// use ratatui_core::{buffer::Buffer, layout::Rect};
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// for position in area.positions() {
@ -320,7 +316,7 @@ impl Rect {
/// # Examples
///
/// ```
/// use ratatui::layout::Rect;
/// use ratatui_core::layout::Rect;
///
/// let rect = Rect::new(1, 2, 3, 4);
/// let position = rect.as_position();

7
ratatui-core/src/lib.rs Normal file
View file

@ -0,0 +1,7 @@
#![doc = include_str!("../README.md")]
pub mod buffer;
pub mod layout;
pub mod style;
pub mod symbols;
pub mod text;
pub mod widgets;

View file

@ -13,7 +13,7 @@
//! ## Example
//!
//! ```
//! use ratatui::{
//! use ratatui_core::{
//! style::{Color, Modifier, Style},
//! text::Span,
//! };
@ -43,10 +43,9 @@
//! ## Example
//!
//! ```
//! use ratatui::{
//! use ratatui_core::{
//! style::{Color, Modifier, Style, Stylize},
//! text::Span,
//! widgets::Paragraph,
//! text::{Span, Text},
//! };
//!
//! assert_eq!(
@ -61,8 +60,8 @@
//! );
//!
//! assert_eq!(
//! Paragraph::new("hello").red().on_blue().bold(),
//! Paragraph::new("hello").style(
//! Text::from("hello").red().on_blue().bold(),
//! Text::from("hello").style(
//! Style::default()
//! .fg(Color::Red)
//! .bg(Color::Blue)
@ -97,7 +96,7 @@ bitflags! {
/// ## Examples
///
/// ```rust
/// use ratatui::style::Modifier;
/// use ratatui_core::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
@ -133,7 +132,7 @@ impl fmt::Debug for Modifier {
/// Style lets you control the main characteristics of the displayed elements.
///
/// ```rust
/// use ratatui::style::{Color, Modifier, Style};
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// Style::default()
/// .fg(Color::Black)
@ -144,7 +143,7 @@ impl fmt::Debug for Modifier {
/// Styles can also be created with a [shorthand notation](crate::style#using-style-shorthands).
///
/// ```rust
/// use ratatui::style::{Style, Stylize};
/// use ratatui_core::style::{Style, Stylize};
///
/// Style::new().black().on_green().italic().bold();
/// ```
@ -155,7 +154,7 @@ impl fmt::Debug for Modifier {
/// anywhere that accepts `Into<Style>`.
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Color, Modifier, Style},
/// text::Line,
/// };
@ -174,7 +173,7 @@ impl fmt::Debug for Modifier {
/// just S3.
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Color, Modifier, Style},
@ -214,7 +213,7 @@ impl fmt::Debug for Modifier {
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// use ratatui::{
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Color, Modifier, Style},
@ -304,7 +303,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// use ratatui::style::{Color, Style};
/// use ratatui_core::style::{Color, Style};
///
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
@ -321,7 +320,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// use ratatui::style::{Color, Style};
/// use ratatui_core::style::{Color, Style};
///
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
@ -346,7 +345,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// use ratatui::style::{Color, Modifier, Style};
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// let style = Style::default()
/// .underline_color(Color::Blue)
@ -375,7 +374,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// use ratatui::style::{Modifier, Style};
/// use ratatui_core::style::{Modifier, Style};
///
/// let style = Style::default().add_modifier(Modifier::BOLD);
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
@ -397,7 +396,7 @@ impl Style {
/// ## Examples
///
/// ```rust
/// use ratatui::style::{Modifier, Style};
/// use ratatui_core::style::{Modifier, Style};
///
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
@ -420,7 +419,7 @@ impl Style {
///
/// ## Examples
/// ```
/// use ratatui::style::{Color, Modifier, Style};
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
@ -506,7 +505,7 @@ impl From<Color> for Style {
/// # Example
///
/// ```rust
/// use ratatui::style::{Color, Style};
/// use ratatui_core::style::{Color, Style};
///
/// let style = Style::from(Color::Red);
/// ```
@ -521,7 +520,7 @@ impl From<(Color, Color)> for Style {
/// # Example
///
/// ```rust
/// use ratatui::style::{Color, Style};
/// use ratatui_core::style::{Color, Style};
///
/// // red foreground, blue background
/// let style = Style::from((Color::Red, Color::Blue));
@ -544,7 +543,7 @@ impl From<Modifier> for Style {
/// # Example
///
/// ```rust
/// use ratatui::style::{Style, Modifier};
/// use ratatui_core::style::{Style, Modifier};
///
/// // add bold and italic
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
@ -559,7 +558,7 @@ impl From<(Modifier, Modifier)> for Style {
/// # Example
///
/// ```rust
/// use ratatui::style::{Modifier, Style};
/// use ratatui_core::style::{Modifier, Style};
///
/// // add bold and italic, remove dim
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
@ -579,7 +578,7 @@ impl From<(Color, Modifier)> for Style {
/// # Example
///
/// ```rust
/// use ratatui::style::{Color, Modifier, Style};
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// // red foreground, add bold and italic
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
@ -597,7 +596,7 @@ impl From<(Color, Color, Modifier)> for Style {
/// # Example
///
/// ```rust
/// use ratatui::style::{Color, Modifier, Style};
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// // red foreground, blue background, add bold and italic
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
@ -614,7 +613,7 @@ impl From<(Color, Color, Modifier, Modifier)> for Style {
/// # Example
///
/// ```rust
/// use ratatui::style::{Color, Modifier, Style};
/// use ratatui_core::style::{Color, Modifier, Style};
///
/// // red foreground, blue background, add bold and italic, remove dim
/// let style = Style::from((

View file

@ -44,7 +44,7 @@ use crate::style::stylize::{ColorDebug, ColorDebugKind};
/// ```
/// use std::str::FromStr;
///
/// use ratatui::style::Color;
/// use ratatui_core::style::Color;
///
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
/// assert_eq!("red".parse(), Ok(Color::Red));
@ -112,14 +112,12 @@ pub enum Color {
/// Notably versions of Windows Terminal prior to Windows 10 and macOS Terminal.app do not
/// support this.
///
/// If the terminal does not support true color, code using the [`TermwizBackend`] will
/// If the terminal does not support true color, code using the `TermwizBackend` will
/// fallback to the default text color. Crossterm and Termion do not have this capability and
/// the display will be unpredictable (e.g. Terminal.app may display glitched blinking text).
/// See <https://github.com/ratatui/ratatui/issues/475> for an example of this problem.
///
/// See also: <https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit>
///
/// [`TermwizBackend`]: crate::backend::TermwizBackend
Rgb(u8, u8, u8),
/// An 8-bit 256 color.
///
@ -170,7 +168,7 @@ impl<'de> serde::Deserialize<'de> for Color {
/// ```
/// use std::str::FromStr;
///
/// use ratatui::style::Color;
/// use ratatui_core::style::Color;
///
/// #[derive(Debug, serde::Deserialize)]
/// struct Theme {
@ -265,7 +263,7 @@ impl std::error::Error for ParseColorError {}
/// ```
/// use std::str::FromStr;
///
/// use ratatui::style::Color;
/// use ratatui_core::style::Color;
///
/// let color: Color = Color::from_str("blue").unwrap();
/// assert_eq!(color, Color::Blue);
@ -385,7 +383,8 @@ impl Color {
/// # Examples
///
/// ```
/// use ratatui::{palette::Hsl, style::Color};
/// use palette::Hsl;
/// use ratatui_core::style::Color;
///
/// // Minimum Lightness is black
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.0));
@ -437,7 +436,8 @@ impl Color {
/// # Examples
///
/// ```
/// use ratatui::{palette::Hsluv, style::Color};
/// use palette::Hsluv;
/// use ratatui_core::style::Color;
///
/// // Minimum Lightness is black
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 100.0, 0.0));

View file

@ -403,7 +403,7 @@
//! # Example
//!
//! ```rust
//! use ratatui::style::{
//! use ratatui_core::style::{
//! palette::material::{BLUE, RED},
//! Color,
//! };

View file

@ -268,7 +268,7 @@
//! # Example
//!
//! ```rust
//! use ratatui::style::{
//! use ratatui_core::style::{
//! palette::tailwind::{BLUE, RED},
//! Color,
//! };

View file

@ -15,7 +15,7 @@ use crate::style::Color;
///
/// ```
/// use palette::Srgb;
/// use ratatui::style::Color;
/// use ratatui_core::style::Color;
///
/// let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
@ -36,7 +36,7 @@ impl<T: IntoStimulus<u8>> From<Srgb<T>> for Color {
///
/// ```
/// use palette::LinSrgb;
/// use ratatui::style::Color;
/// use ratatui_core::style::Color;
///
/// let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(255, 0, 0));

View file

@ -195,8 +195,8 @@ macro_rules! modifier {
/// by `not_`). The `reset()` method is also provided to reset the style.
///
/// # Examples
/// ```
/// use ratatui::{
/// ```ignore
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::Line,
/// widgets::{Block, Paragraph},

View file

@ -1,25 +1,25 @@
//! Primitives for styled text.
//!
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish, those
//! strings may be associated to a set of styles. `ratatui` has three ways to represent them:
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//!
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Line`].
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`] is
//! a [`Line`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
//! primitives when you need additional styling capabilities.
//!
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
//! its `title` property (which is a [`Line`] under the hood):
//! For example, for the `Block` widget, all the following calls are valid to set its `title`
//! property (which is a [`Line`] under the hood):
//!
//! ```rust
//! use ratatui::{
//! ```rust,ignore
//! use ratatui_core::{
//! style::{Color, Style},
//! text::{Line, Span},
//! widgets::Block,

View file

@ -28,7 +28,7 @@ impl<'a> StyledGrapheme<'a> {
}
}
pub(crate) fn is_whitespace(&self) -> bool {
pub fn is_whitespace(&self) -> bool {
let symbol = self.symbol;
symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP
}

View file

@ -75,7 +75,7 @@ use crate::{
/// [`Style`].
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::{Line, Span},
/// };
@ -102,7 +102,7 @@ use crate::{
/// methods of the [`Stylize`] trait.
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::Line,
/// };
@ -121,7 +121,7 @@ use crate::{
/// ignored and the line is truncated.
///
/// ```rust
/// use ratatui::{layout::Alignment, text::Line};
/// use ratatui_core::{layout::Alignment, text::Line};
///
/// let line = Line::from("Hello world!").alignment(Alignment::Right);
/// let line = Line::from("Hello world!").centered();
@ -134,13 +134,12 @@ use crate::{
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// style::{Style, Stylize},
/// text::Line,
/// widgets::Widget,
/// Frame,
/// };
///
/// # fn render(area: Rect, buf: &mut Buffer) {
@ -148,19 +147,23 @@ use crate::{
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
/// line.render(area, buf);
/// # }
/// ```
///
/// Or you can use the `render_widget` method on the `Frame` in a `Terminal::draw` closure.
///
/// ```rust,ignore
/// # use ratatui::{Frame, layout::Rect, text::Line};
/// # fn draw(frame: &mut Frame, area: Rect) {
/// // in a terminal.draw closure
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
/// let line = Line::from("Hello world!");
/// frame.render_widget(line, area);
/// # }
/// ```
/// ## Rendering Lines with a Paragraph widget
///
/// Usually apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it
/// Usually apps will use the `Paragraph` widget instead of rendering a [`Line`] directly as it
/// provides more functionality.
///
/// ```rust
/// ```rust,ignore
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
@ -177,7 +180,6 @@ use crate::{
/// # }
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Stylize`]: crate::style::Stylize
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Line<'a> {
@ -241,7 +243,7 @@ impl<'a> Line<'a> {
/// ```rust
/// use std::borrow::Cow;
///
/// use ratatui::text::Line;
/// use ratatui_core::text::Line;
///
/// Line::raw("test content");
/// Line::raw(String::from("test content"));
@ -272,7 +274,7 @@ impl<'a> Line<'a> {
/// ```rust
/// use std::borrow::Cow;
///
/// use ratatui::{
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Line,
/// };
@ -304,7 +306,7 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::{style::Stylize, text::Line};
/// use ratatui_core::{style::Stylize, text::Line};
///
/// let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
/// let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {}", i)));
@ -332,7 +334,7 @@ impl<'a> Line<'a> {
///
/// # Examples
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Line,
/// };
@ -356,7 +358,7 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::{layout::Alignment, text::Line};
/// use ratatui_core::{layout::Alignment, text::Line};
///
/// let mut line = Line::from("Hi, what's up?");
/// assert_eq!(None, line.alignment);
@ -382,7 +384,7 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Line;
/// use ratatui_core::text::Line;
///
/// let line = Line::from("Hi, what's up?").left_aligned();
/// ```
@ -400,7 +402,7 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Line;
/// use ratatui_core::text::Line;
///
/// let line = Line::from("Hi, what's up?").centered();
/// ```
@ -418,7 +420,7 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Line;
/// use ratatui_core::text::Line;
///
/// let line = Line::from("Hi, what's up?").right_aligned();
/// ```
@ -432,7 +434,7 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::{style::Stylize, text::Line};
/// use ratatui_core::{style::Stylize, text::Line};
///
/// let line = Line::from(vec!["Hello".blue(), " world!".green()]);
/// assert_eq!(12, line.width());
@ -454,7 +456,7 @@ impl<'a> Line<'a> {
/// ```rust
/// use std::iter::Iterator;
///
/// use ratatui::{
/// use ratatui_core::{
/// style::{Color, Style},
/// text::{Line, StyledGrapheme},
/// };
@ -498,7 +500,7 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Color, Modifier},
/// text::Line,
/// };
@ -527,7 +529,7 @@ impl<'a> Line<'a> {
///
/// ```rust
/// # let style = Style::default().yellow();
/// use ratatui::{
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Line,
/// };
@ -559,7 +561,7 @@ impl<'a> Line<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::{Line, Span};
/// use ratatui_core::text::{Line, Span};
///
/// let mut line = Line::from("Hello, ");
/// line.push_span(Span::raw("world!"));

View file

@ -4,23 +4,23 @@ use crate::text::Text;
/// A wrapper around a string that is masked when displayed.
///
/// The masked string is displayed as a series of the same character.
/// This might be used to display a password field or similar secure data.
/// The masked string is displayed as a series of the same character. This might be used to display
/// a password field or similar secure data.
///
/// # Examples
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// text::Masked,
/// widgets::{Paragraph, Widget},
/// text::{Masked, Text},
/// widgets::Widget,
/// };
///
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
/// let password = Masked::new("12345", 'x');
///
/// Paragraph::new(password).render(buffer.area, &mut buffer);
/// Text::from(password).render(buffer.area, &mut buffer);
/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
/// ```
#[derive(Default, Clone, Eq, PartialEq, Hash)]

View file

@ -42,7 +42,7 @@ use crate::{
/// any type convertible to [`Cow<str>`].
///
/// ```rust
/// use ratatui::text::Span;
/// use ratatui_core::text::Span;
///
/// let span = Span::raw("test content");
/// let span = Span::raw(String::from("test content"));
@ -56,7 +56,7 @@ use crate::{
/// the [`Stylize`] trait.
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
@ -73,7 +73,7 @@ use crate::{
/// defined in the [`Stylize`] trait.
///
/// ```rust
/// use ratatui::{style::Stylize, text::Span};
/// use ratatui_core::{style::Stylize, text::Span};
///
/// let span = Span::raw("test content").green().on_yellow().italic();
/// let span = Span::raw(String::from("test content"))
@ -82,11 +82,11 @@ use crate::{
/// .italic();
/// ```
///
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Usually
/// apps will use the [`Paragraph`] widget instead of rendering `Span` directly, as it handles text
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Often
/// apps will use the `Paragraph` widget instead of rendering `Span` directly, as it handles text
/// wrapping and alignment for you.
///
/// ```rust
/// ```rust,ignore
/// use ratatui::{style::Stylize, Frame};
///
/// # fn render_frame(frame: &mut Frame) {
@ -94,7 +94,6 @@ use crate::{
/// # }
/// ```
/// [`Line`]: crate::text::Line
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Stylize`]: crate::style::Stylize
/// [`Cow<str>`]: std::borrow::Cow
#[derive(Default, Clone, Eq, PartialEq, Hash)]
@ -125,7 +124,7 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Span;
/// use ratatui_core::text::Span;
///
/// Span::raw("test content");
/// Span::raw(String::from("test content"));
@ -151,7 +150,7 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
@ -183,7 +182,7 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Span;
/// use ratatui_core::text::Span;
///
/// let mut span = Span::default().content("content");
/// ```
@ -209,7 +208,7 @@ impl<'a> Span<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
@ -234,7 +233,7 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
@ -260,7 +259,7 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Span,
/// };
@ -295,7 +294,7 @@ impl<'a> Span<'a> {
/// ```rust
/// use std::iter::Iterator;
///
/// use ratatui::{
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::{Span, StyledGrapheme},
/// };
@ -332,7 +331,7 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// use ratatui::style::Stylize;
/// use ratatui_core::style::Stylize;
///
/// let line = "Test Content".green().italic().into_left_aligned_line();
/// ```
@ -352,7 +351,7 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// use ratatui::style::Stylize;
/// use ratatui_core::style::Stylize;
///
/// let line = "Test Content".green().italic().into_centered_line();
/// ```
@ -372,7 +371,7 @@ impl<'a> Span<'a> {
/// # Example
///
/// ```rust
/// use ratatui::style::Stylize;
/// use ratatui_core::style::Stylize;
///
/// let line = "Test Content".green().italic().into_right_aligned_line();
/// ```

View file

@ -68,7 +68,7 @@ use crate::{
/// ```rust
/// use std::{borrow::Cow, iter};
///
/// use ratatui::{
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::{Line, Span, Text},
/// };
@ -108,7 +108,7 @@ use crate::{
/// [`Stylize`] trait.
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Color, Modifier, Style, Stylize},
/// text::{Line, Text},
/// };
@ -129,7 +129,7 @@ use crate::{
/// Lines composing the text can also be individually aligned with [`Line::alignment`].
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// layout::Alignment,
/// text::{Line, Text},
/// };
@ -146,19 +146,23 @@ use crate::{
///
/// ## Rendering Text
/// `Text` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`] or to a
/// [`Frame`].
/// `Frame`.
///
/// ```rust
/// # use ratatui::{buffer::Buffer, layout::Rect};
/// use ratatui::{text::Text, widgets::Widget, Frame};
/// # use ratatui_core::{buffer::Buffer, layout::Rect};
/// use ratatui_core::{text::Text, widgets::Widget};
///
/// // within another widget's `render` method:
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let text = Text::from("The first line\nThe second line");
/// text.render(area, buf);
/// # }
/// ```
///
/// // within a terminal.draw closure:
/// Or you can use the `render_widget` method on a `Frame` within a `Terminal::draw` closure.
///
/// ```rust,ignore
/// # use ratatui::{Frame, layout::Rect, text::Text};
/// # fn draw(frame: &mut Frame, area: Rect) {
/// let text = Text::from("The first line\nThe second line");
/// frame.render_widget(text, area);
@ -167,10 +171,10 @@ use crate::{
///
/// ## Rendering Text with a Paragraph Widget
///
/// Usually apps will use the [`Paragraph`] widget instead of rendering a `Text` directly as it
/// Usually apps will use the `Paragraph` widget instead of rendering a `Text` directly as it
/// provides more functionality.
///
/// ```rust
/// ```rust,ignore
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
@ -189,7 +193,6 @@ use crate::{
///
/// [`Paragraph`]: crate::widgets::Paragraph
/// [`Stylize`]: crate::style::Stylize
/// [`Frame`]: crate::Frame
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Text<'a> {
/// The alignment of this text.
@ -228,7 +231,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Text;
/// use ratatui_core::text::Text;
///
/// Text::raw("The first line\nThe second line");
/// Text::raw(String::from("The first line\nThe second line"));
@ -254,7 +257,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Color, Modifier, Style},
/// text::Text,
/// };
@ -280,7 +283,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Text;
/// use ratatui_core::text::Text;
///
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
@ -294,7 +297,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Text;
/// use ratatui_core::text::Text;
///
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
@ -316,7 +319,7 @@ impl<'a> Text<'a> {
///
/// # Examples
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Style, Stylize},
/// text::Text,
/// };
@ -348,7 +351,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Color, Modifier},
/// text::Text,
/// };
@ -381,7 +384,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// style::{Color, Modifier, Style},
/// text::Text,
/// };
@ -412,7 +415,7 @@ impl<'a> Text<'a> {
/// Set alignment to the whole text.
///
/// ```rust
/// use ratatui::{layout::Alignment, text::Text};
/// use ratatui_core::{layout::Alignment, text::Text};
///
/// let mut text = Text::from("Hi, what's up?");
/// assert_eq!(None, text.alignment);
@ -425,7 +428,7 @@ impl<'a> Text<'a> {
/// Set a default alignment and override it on a per line basis.
///
/// ```rust
/// use ratatui::{
/// use ratatui_core::{
/// layout::Alignment,
/// text::{Line, Text},
/// };
@ -466,7 +469,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Text;
/// use ratatui_core::text::Text;
///
/// let text = Text::from("Hi, what's up?").left_aligned();
/// ```
@ -486,7 +489,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Text;
/// use ratatui_core::text::Text;
///
/// let text = Text::from("Hi, what's up?").centered();
/// ```
@ -506,7 +509,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::Text;
/// use ratatui_core::text::Text;
///
/// let text = Text::from("Hi, what's up?").right_aligned();
/// ```
@ -533,7 +536,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::{Line, Span, Text};
/// use ratatui_core::text::{Line, Span, Text};
///
/// let mut text = Text::from("Hello, world!");
/// text.push_line(Line::from("How are you?"));
@ -552,7 +555,7 @@ impl<'a> Text<'a> {
/// # Examples
///
/// ```rust
/// use ratatui::text::{Span, Text};
/// use ratatui_core::text::{Span, Text};
///
/// let mut text = Text::from("Hello, world!");
/// text.push_span(Span::from("How are you?"));

693
ratatui-core/src/widgets.rs Normal file
View file

@ -0,0 +1,693 @@
#![warn(missing_docs)]
//! The `widgets` module contains the `Widget` and `StatefulWidget` traits, which are used to
//! render UI elements on the screen.
use crate::{buffer::Buffer, layout::Rect, style::Style};
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
///
/// Prior to Ratatui 0.26.0, widgets generally were created for each frame as they were consumed
/// during rendering. This meant that they were not meant to be stored but used as *commands* to
/// draw common figures in the UI.
///
/// Starting with Ratatui 0.26.0, all the internal widgets implement Widget for a reference to
/// themselves. This allows you to store a reference to a widget and render it later. Widget crates
/// should consider also doing this to allow for more flexibility in how widgets are used.
///
/// In Ratatui 0.26.0, we also added an unstable [`WidgetRef`] trait and implemented this on all the
/// internal widgets. In addition to the above benefit of rendering references to widgets, this also
/// allows you to render boxed widgets. This is useful when you want to store a collection of
/// widgets with different types. You can then iterate over the collection and render each widget.
/// See <https://github.com/ratatui/ratatui/issues/1287> for more information.
///
/// In general where you expect a widget to immutably work on its data, we recommended to implement
/// `Widget` for a reference to the widget (`impl Widget for &MyWidget`). If you need to store state
/// between draw calls, implement `StatefulWidget` if you want the Widget to be immutable, or
/// implement `Widget` for a mutable reference to the widget (`impl Widget for &mut MyWidget`) if
/// you want the widget to be mutable. The mutable widget pattern is used infrequently in apps, but
/// can be quite useful.
///
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
/// Widget is also implemented for `&str` and `String` types.
///
/// # Examples
///
/// ```rust,ignore
/// use ratatui::{
/// backend::TestBackend,
/// widgets::{Clear, Widget},
/// Terminal,
/// };
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
///
/// terminal.draw(|frame| {
/// frame.render_widget(Clear, frame.area());
/// });
/// ```
///
/// It's common to render widgets inside other widgets:
///
/// ```rust
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
///
/// struct MyWidget;
///
/// impl Widget for MyWidget {
/// fn render(self, area: Rect, buf: &mut Buffer) {
/// Line::raw("Hello").render(area, buf);
/// }
/// }
/// ```
pub trait Widget {
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized;
}
/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
/// between two draw calls.
///
/// Most widgets can be drawn directly based on the input parameters. However, some features may
/// require some kind of associated state to be implemented.
///
/// For example, the `List` widget can highlight the item currently selected. This can be translated
/// in an offset, which is the number of elements to skip in order to have the selected item within
/// the viewport currently allocated to this widget. The widget can therefore only provide the
/// following behavior: whenever the selected item is out of the viewport scroll to a predefined
/// position (making the selected item the last viewable item or the one in the middle for example).
/// Nonetheless, if the widget has access to the last computed offset then it can implement a
/// natural scrolling experience where the last offset is reused until the selected item is out of
/// the viewport.
///
/// ## Examples
///
/// ```rust,ignore
/// use std::io;
///
/// use ratatui::{
/// backend::TestBackend,
/// widgets::{List, ListItem, ListState, StatefulWidget, Widget},
/// Terminal,
/// };
///
/// // Let's say we have some events to display.
/// struct Events {
/// // `items` is the state managed by your application.
/// items: Vec<String>,
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
/// // item as well as the offset computed during the previous draw call (used to implement
/// // natural scrolling).
/// state: ListState,
/// }
///
/// impl Events {
/// fn new(items: Vec<String>) -> Events {
/// Events {
/// items,
/// state: ListState::default(),
/// }
/// }
///
/// pub fn set_items(&mut self, items: Vec<String>) {
/// self.items = items;
/// // We reset the state as the associated items have changed. This effectively reset
/// // the selection as well as the stored offset.
/// self.state = ListState::default();
/// }
///
/// // Select the next item. This will not be reflected until the widget is drawn in the
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
/// pub fn next(&mut self) {
/// let i = match self.state.selected() {
/// Some(i) => {
/// if i >= self.items.len() - 1 {
/// 0
/// } else {
/// i + 1
/// }
/// }
/// None => 0,
/// };
/// self.state.select(Some(i));
/// }
///
/// // Select the previous item. This will not be reflected until the widget is drawn in the
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
/// pub fn previous(&mut self) {
/// let i = match self.state.selected() {
/// Some(i) => {
/// if i == 0 {
/// self.items.len() - 1
/// } else {
/// i - 1
/// }
/// }
/// None => 0,
/// };
/// self.state.select(Some(i));
/// }
///
/// // Unselect the currently selected item if any. The implementation of `ListState` makes
/// // sure that the stored offset is also reset.
/// pub fn unselect(&mut self) {
/// self.state.select(None);
/// }
/// }
///
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
///
/// let mut events = Events::new(vec![String::from("Item 1"), String::from("Item 2")]);
///
/// loop {
/// terminal.draw(|f| {
/// // The items managed by the application are transformed to something
/// // that is understood by ratatui.
/// let items: Vec<ListItem> = events
/// .items
/// .iter()
/// .map(|i| ListItem::new(i.as_str()))
/// .collect();
/// // The `List` widget is then built with those items.
/// let list = List::new(items);
/// // Finally the widget is rendered using the associated state. `events.state` is
/// // effectively the only thing that we will "remember" from this draw call.
/// f.render_stateful_widget(list, f.size(), &mut events.state);
/// });
///
/// // In response to some input events or an external http request or whatever:
/// events.next();
/// }
/// ```
pub trait StatefulWidget {
/// State associated with the stateful widget.
///
/// If you don't need this then you probably want to implement [`Widget`] instead.
type State;
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom stateful widget.
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}
/// A `WidgetRef` is a trait that allows rendering a widget by reference.
///
/// This trait is useful when you want to store a reference to a widget and render it later. It also
/// allows you to render boxed widgets.
///
/// Boxed widgets allow you to store widgets with a type that is not known at compile time. This is
/// useful when you want to store a collection of widgets with different types. You can then iterate
/// over the collection and render each widget.
///
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal widgets. It
/// is currently marked as unstable as we are still evaluating the API and may make changes in the
/// future. See <https://github.com/ratatui/ratatui/issues/1287> for more information.
///
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
///
/// A blanket implementation of `WidgetRef` for `Option<W>` where `W` implements `WidgetRef` is
/// provided. This is a convenience approach to make it easier to attach child widgets to parent
/// widgets. It allows you to render an optional widget by reference.
///
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// text::Line,
/// widgets::{Widget, WidgetRef},
/// };
///
/// struct Greeting;
///
/// struct Farewell;
///
/// impl WidgetRef for Greeting {
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
/// Line::raw("Hello").render(area, buf);
/// }
/// }
///
/// /// Only needed for backwards compatibility
/// impl Widget for Greeting {
/// fn render(self, area: Rect, buf: &mut Buffer) {
/// self.render_ref(area, buf);
/// }
/// }
///
/// impl WidgetRef for Farewell {
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
/// Line::raw("Goodbye").right_aligned().render(area, buf);
/// }
/// }
///
/// /// Only needed for backwards compatibility
/// impl Widget for Farewell {
/// fn render(self, area: Rect, buf: &mut Buffer) {
/// self.render_ref(area, buf);
/// }
/// }
///
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let greeting = Greeting;
/// let farewell = Farewell;
///
/// // these calls do not consume the widgets, so they can be used again later
/// greeting.render_ref(area, buf);
/// farewell.render_ref(area, buf);
///
/// // a collection of widgets with different types
/// let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(greeting), Box::new(farewell)];
/// for widget in widgets {
/// widget.render_ref(area, buf);
/// }
/// # }
/// # }
/// ```
#[instability::unstable(feature = "widget-ref")]
pub trait WidgetRef {
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.
fn render_ref(&self, area: Rect, buf: &mut Buffer);
}
/// This allows you to render a widget by reference.
impl<W: WidgetRef> Widget for &W {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// A blanket implementation of `WidgetExt` for `Option<W>` where `W` implements `WidgetRef`.
///
/// This is a convenience implementation that makes it easy to attach child widgets to parent
/// widgets. It allows you to render an optional widget by reference.
///
/// The internal widgets use this pattern to render the optional `Block` widgets that are included
/// on most widgets.
/// Blanket implementation of `WidgetExt` for `Option<W>` where `W` implements `WidgetRef`.
///
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// text::Line,
/// widgets::{Widget, WidgetRef},
/// };
///
/// struct Parent {
/// child: Option<Child>,
/// }
///
/// struct Child;
///
/// impl WidgetRef for Child {
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
/// Line::raw("Hello from child").render(area, buf);
/// }
/// }
///
/// impl WidgetRef for Parent {
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
/// self.child.render_ref(area, buf);
/// }
/// }
/// # }
/// ```
impl<W: WidgetRef> WidgetRef for Option<W> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
if let Some(widget) = self {
widget.render_ref(area, buf);
}
}
}
/// A `StatefulWidgetRef` is a trait that allows rendering a stateful widget by reference.
///
/// This is the stateful equivalent of `WidgetRef`. It is useful when you want to store a reference
/// to a stateful widget and render it later. It also allows you to render boxed stateful widgets.
///
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal stateful
/// widgets. It is currently marked as unstable as we are still evaluating the API and may make
/// changes in the future. See <https://github.com/ratatui/ratatui/issues/1287> for more
/// information.
///
/// A blanket implementation of `StatefulWidget` for `&W` where `W` implements `StatefulWidgetRef`
/// is provided.
///
/// See the documentation for [`WidgetRef`] for more information on boxed widgets.
/// See the documentation for [`StatefulWidget`] for more information on stateful widgets.
///
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui_core::{
/// buffer::Buffer,
/// layout::Rect,
/// style::Stylize,
/// text::Line,
/// widgets::{StatefulWidget, StatefulWidgetRef, Widget},
/// };
///
/// struct PersonalGreeting;
///
/// impl StatefulWidgetRef for PersonalGreeting {
/// type State = String;
/// fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
/// Line::raw(format!("Hello {}", state)).render(area, buf);
/// }
/// }
///
/// impl StatefulWidget for PersonalGreeting {
/// type State = String;
/// fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
/// (&self).render_ref(area, buf, state);
/// }
/// }
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// let widget = PersonalGreeting;
/// let mut state = "world".to_string();
/// widget.render(area, buf, &mut state);
/// }
/// # }
/// ```
#[instability::unstable(feature = "widget-ref")]
pub trait StatefulWidgetRef {
/// State associated with the stateful widget.
///
/// If you don't need this then you probably want to implement [`WidgetRef`] instead.
type State;
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom stateful widget.
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}
// Note: while StatefulWidgetRef is marked as unstable, the blanket implementation of StatefulWidget
// cannot be implemented as W::State is effectively pub(crate) and not accessible from outside the
// crate. Once stabilized, this blanket implementation can be added and the specific implementations
// on Table and List can be removed.
//
// /// Blanket implementation of `StatefulWidget` for `&W` where `W` implements `StatefulWidgetRef`.
// ///
// /// This allows you to render a stateful widget by reference.
// impl<W: StatefulWidgetRef> StatefulWidget for &W {
// type State = W::State;
// fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// StatefulWidgetRef::render_ref(self, area, buf, state);
// }
// }
/// Renders a string slice as a widget.
///
/// This implementation allows a string slice (`&str`) to act as a widget, meaning it can be drawn
/// onto a [`Buffer`] in a specified [`Rect`]. The slice represents a static string which can be
/// rendered by reference, thereby avoiding the need for string cloning or ownership transfer when
/// drawing the text to the screen.
impl Widget for &str {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// Provides the ability to render a string slice by reference.
///
/// This trait implementation ensures that a string slice, which is an immutable view over a
/// `String`, can be drawn on demand without requiring ownership of the string itself. It utilizes
/// the default text style when rendering onto the provided [`Buffer`] at the position defined by
/// [`Rect`].
impl WidgetRef for &str {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
/// Renders a `String` object as a widget.
///
/// This implementation enables an owned `String` to be treated as a widget, which can be rendered
/// on a [`Buffer`] within the bounds of a given [`Rect`].
impl Widget for String {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// Provides the ability to render a `String` by reference.
///
/// This trait allows for a `String` to be rendered onto the [`Buffer`], similarly using the default
/// style settings. It ensures that an owned `String` can be rendered efficiently by reference,
/// without the need to give up ownership of the underlying text.
impl WidgetRef for String {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
#[cfg(test)]
mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::text::Line;
#[fixture]
fn buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 20, 1))
}
mod widget {
use super::*;
struct Greeting;
impl Widget for Greeting {
fn render(self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
#[rstest]
fn render(mut buf: Buffer) {
let widget = Greeting;
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
}
mod widget_ref {
use super::*;
struct Greeting;
struct Farewell;
impl WidgetRef for Greeting {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
impl WidgetRef for Farewell {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Goodbye").right_aligned().render(area, buf);
}
}
#[rstest]
fn render_ref(mut buf: Buffer) {
let widget = Greeting;
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
/// Ensure that the blanket implementation of `Widget` for `&W` where `W` implements
/// `WidgetRef` works as expected.
#[rstest]
fn blanket_render(mut buf: Buffer) {
let widget = &Greeting;
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn box_render_ref(mut buf: Buffer) {
let widget: Box<dyn WidgetRef> = Box::new(Greeting);
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn vec_box_render(mut buf: Buffer) {
let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Greeting), Box::new(Farewell)];
for widget in widgets {
widget.render_ref(buf.area, &mut buf);
}
assert_eq!(buf, Buffer::with_lines(["Hello Goodbye"]));
}
}
#[fixture]
fn state() -> String {
"world".to_string()
}
mod stateful_widget {
use super::*;
struct PersonalGreeting;
impl StatefulWidget for PersonalGreeting {
type State = String;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Line::from(format!("Hello {state}")).render(area, buf);
}
}
#[rstest]
fn render(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting;
widget.render(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
}
mod stateful_widget_ref {
use super::*;
struct PersonalGreeting;
impl StatefulWidgetRef for PersonalGreeting {
type State = String;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Line::from(format!("Hello {state}")).render(area, buf);
}
}
#[rstest]
fn render_ref(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting;
widget.render_ref(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
// Note this cannot be tested until the blanket implementation of StatefulWidget for &W
// where W implements StatefulWidgetRef is added. (see the comment in the blanket
// implementation for more).
// /// This test is to ensure that the blanket implementation of `StatefulWidget` for `&W`
// where /// `W` implements `StatefulWidgetRef` works as expected.
// #[rstest]
// fn stateful_widget_blanket_render(mut buf: Buffer, mut state: String) {
// let widget = &PersonalGreeting;
// widget.render(buf.area, &mut buf, &mut state);
// assert_eq!(buf, Buffer::with_lines(["Hello world "]));
// }
#[rstest]
fn box_render_render(mut buf: Buffer, mut state: String) {
let widget = Box::new(PersonalGreeting);
widget.render_ref(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
}
mod option_widget_ref {
use super::*;
struct Greeting;
impl WidgetRef for Greeting {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
#[rstest]
fn render_ref_some(mut buf: Buffer) {
let widget = Some(Greeting);
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn render_ref_none(mut buf: Buffer) {
let widget: Option<Greeting> = None;
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([" "]));
}
}
mod str {
use super::*;
#[rstest]
fn render(mut buf: Buffer) {
"hello world".render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_area(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
"hello world, just hello".render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
"hello world".render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render(mut buf: Buffer) {
Some("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render_ref(mut buf: Buffer) {
Some("hello world").render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
}
mod string {
use super::*;
#[rstest]
fn render(mut buf: Buffer) {
String::from("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_area(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
String::from("hello world, just hello").render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
String::from("hello world").render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render(mut buf: Buffer) {
Some(String::from("hello world")).render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render_ref(mut buf: Buffer) {
Some(String::from("hello world")).render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
}
}

View file

@ -15,25 +15,21 @@ edition.workspace = true
rust-version.workspace = true
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
compact_str = "0.8.0"
bitflags.workspace = true
crossterm = { version = "0.28.1", optional = true }
document-features = { version = "0.2.7", optional = true }
indoc = "2"
instability = "0.3.1"
itertools = "0.13"
lru = "0.12.0"
paste = "1.0.2"
instability.workspace = true
itertools.workspace = true
palette = { version = "0.7.6", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
ratatui-core = { workspace = true, features = ["unstable-widget-ref"] }
serde = { workspace = true, optional = true }
strum.workspace = true
termwiz = { version = "0.22.0", optional = true }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-truncate = "1"
unicode-segmentation.workspace = true
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
unicode-width = "=0.2.0"
unicode-width.workspace = true
[target.'cfg(not(windows))'.dependencies]
# termion is not supported on Windows
@ -124,13 +120,13 @@ termwiz = ["dep:termwiz"]
#! The following optional features are available for all backends:
## enables serialization and deserialization of style and color types using the [`serde`] crate.
## This is useful if you want to save themes to a file.
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
serde = ["dep:serde", "ratatui-core/serde"]
## enables the [`border!`] macro.
macros = []
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
palette = ["dep:palette"]
palette = ["ratatui-core/palette", "dep:palette"]
## Use terminal scrolling regions to make some operations less prone to
## flickering. (i.e. Terminal::insert_before).
@ -149,7 +145,7 @@ widget-calendar = ["dep:time"]
## enables the backend code that sets the underline color.
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
## and is not supported on Windows 7.
underline-color = ["dep:crossterm"]
underline-color = ["dep:crossterm", "ratatui-core/underline-color"]
#! The following features are unstable and may change in the future:
@ -166,9 +162,12 @@ unstable = [
## See [Issue 293](https://github.com/ratatui/ratatui/issues/293) for more details.
unstable-rendered-line-info = []
## Enables the [`WidgetRef`](widgets::WidgetRef) and [`StatefulWidgetRef`](widgets::StatefulWidgetRef) traits which are experimental and may change in
## the future.
unstable-widget-ref = []
## enables the [`WidgetRef`] and [`StatefulWidgetRef`] traits which are experimental and may change
## in the future.
##
## [`WidgetRef`]: widgets::WidgetRef
## [`StatefulWidgetRef`]: widgets::StatefulWidgetRef
unstable-widget-ref = ["ratatui-core/unstable-widget-ref"]
## Enables getting access to backends' writers.
unstable-backend-writer = []

View file

@ -10,13 +10,13 @@ use std::{error::Error, io};
use crate::{
backend::{Backend, WindowSize},
buffer::Cell,
layout::Size,
layout::{Position, Size},
style::{Color, Modifier, Style},
termwiz::{
caps::Capabilities,
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
surface::{Change, CursorVisibility, Position},
surface::{Change, CursorVisibility, Position as TermwizPosition},
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
},
};
@ -117,8 +117,8 @@ impl Backend for TermwizBackend {
for (x, y, cell) in content {
self.buffered_terminal.add_changes(vec![
Change::CursorPosition {
x: Position::Absolute(x as usize),
y: Position::Absolute(y as usize),
x: TermwizPosition::Absolute(x as usize),
y: TermwizPosition::Absolute(y as usize),
},
Change::Attribute(AttributeChange::Foreground(cell.fg.into_termwiz())),
Change::Attribute(AttributeChange::Background(cell.bg.into_termwiz())),
@ -192,19 +192,16 @@ impl Backend for TermwizBackend {
Ok(())
}
fn get_cursor_position(&mut self) -> io::Result<crate::layout::Position> {
fn get_cursor_position(&mut self) -> io::Result<Position> {
let (x, y) = self.buffered_terminal.cursor_position();
Ok((x as u16, y as u16).into())
Ok(Position::new(x as u16, y as u16))
}
fn set_cursor_position<P: Into<crate::layout::Position>>(
&mut self,
position: P,
) -> io::Result<()> {
let crate::layout::Position { x, y } = position.into();
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let Position { x, y } = position.into();
self.buffered_terminal.add_change(Change::CursorPosition {
x: Position::Absolute(x as usize),
y: Position::Absolute(y as usize),
x: TermwizPosition::Absolute(x as usize),
y: TermwizPosition::Absolute(y as usize),
});
Ok(())

View file

@ -144,7 +144,7 @@ impl TestBackend {
#[track_caller]
pub fn assert_buffer(&self, expected: &Buffer) {
// TODO: use assert_eq!()
crate::assert_buffer_eq!(&self.buffer, expected);
ratatui_core::assert_buffer_eq!(&self.buffer, expected);
}
/// Asserts that the `TestBackend`'s scrollback buffer is equal to the expected buffer.

View file

@ -344,11 +344,9 @@ pub use termion;
pub use termwiz;
pub mod backend;
pub mod buffer;
pub mod layout;
pub use ratatui_core::{buffer, layout};
pub mod prelude;
pub mod style;
pub mod symbols;
pub use ratatui_core::{style, symbols};
mod terminal;
pub mod text;
pub use ratatui_core::text;
pub mod widgets;

View file

@ -39,6 +39,10 @@ mod sparkline;
mod table;
mod tabs;
pub use ratatui_core::widgets::{StatefulWidget, Widget};
#[instability::unstable(feature = "widget-ref")]
pub use ratatui_core::widgets::{StatefulWidgetRef, WidgetRef};
pub use self::{
barchart::{Bar, BarChart, BarGroup},
block::{Block, BorderType, Padding},
@ -54,693 +58,3 @@ pub use self::{
table::{Cell, HighlightSpacing, Row, Table, TableState},
tabs::Tabs,
};
use crate::{buffer::Buffer, layout::Rect, style::Style};
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
///
/// Prior to Ratatui 0.26.0, widgets generally were created for each frame as they were consumed
/// during rendering. This meant that they were not meant to be stored but used as *commands* to
/// draw common figures in the UI.
///
/// Starting with Ratatui 0.26.0, all the internal widgets implement Widget for a reference to
/// themselves. This allows you to store a reference to a widget and render it later. Widget crates
/// should consider also doing this to allow for more flexibility in how widgets are used.
///
/// In Ratatui 0.26.0, we also added an unstable [`WidgetRef`] trait and implemented this on all the
/// internal widgets. In addition to the above benefit of rendering references to widgets, this also
/// allows you to render boxed widgets. This is useful when you want to store a collection of
/// widgets with different types. You can then iterate over the collection and render each widget.
/// See <https://github.com/ratatui/ratatui/issues/1287> for more information.
///
/// In general where you expect a widget to immutably work on its data, we recommended to implement
/// `Widget` for a reference to the widget (`impl Widget for &MyWidget`). If you need to store state
/// between draw calls, implement `StatefulWidget` if you want the Widget to be immutable, or
/// implement `Widget` for a mutable reference to the widget (`impl Widget for &mut MyWidget`) if
/// you want the widget to be mutable. The mutable widget pattern is used infrequently in apps, but
/// can be quite useful.
///
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
/// Widget is also implemented for `&str` and `String` types.
///
/// # Examples
///
/// ```rust,no_run
/// use ratatui::{
/// backend::TestBackend,
/// widgets::{Clear, Widget},
/// Terminal,
/// };
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
///
/// terminal.draw(|frame| {
/// frame.render_widget(Clear, frame.area());
/// });
/// ```
///
/// It's common to render widgets inside other widgets:
///
/// ```rust
/// use ratatui::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
///
/// struct MyWidget;
///
/// impl Widget for MyWidget {
/// fn render(self, area: Rect, buf: &mut Buffer) {
/// Line::raw("Hello").render(area, buf);
/// }
/// }
/// ```
pub trait Widget {
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized;
}
/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
/// between two draw calls.
///
/// Most widgets can be drawn directly based on the input parameters. However, some features may
/// require some kind of associated state to be implemented.
///
/// For example, the [`List`] widget can highlight the item currently selected. This can be
/// translated in an offset, which is the number of elements to skip in order to have the selected
/// item within the viewport currently allocated to this widget. The widget can therefore only
/// provide the following behavior: whenever the selected item is out of the viewport scroll to a
/// predefined position (making the selected item the last viewable item or the one in the middle
/// for example). Nonetheless, if the widget has access to the last computed offset then it can
/// implement a natural scrolling experience where the last offset is reused until the selected
/// item is out of the viewport.
///
/// ## Examples
///
/// ```rust,no_run
/// use std::io;
///
/// use ratatui::{
/// backend::TestBackend,
/// widgets::{List, ListItem, ListState, StatefulWidget, Widget},
/// Terminal,
/// };
///
/// // Let's say we have some events to display.
/// struct Events {
/// // `items` is the state managed by your application.
/// items: Vec<String>,
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
/// // item as well as the offset computed during the previous draw call (used to implement
/// // natural scrolling).
/// state: ListState,
/// }
///
/// impl Events {
/// fn new(items: Vec<String>) -> Events {
/// Events {
/// items,
/// state: ListState::default(),
/// }
/// }
///
/// pub fn set_items(&mut self, items: Vec<String>) {
/// self.items = items;
/// // We reset the state as the associated items have changed. This effectively reset
/// // the selection as well as the stored offset.
/// self.state = ListState::default();
/// }
///
/// // Select the next item. This will not be reflected until the widget is drawn in the
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
/// pub fn next(&mut self) {
/// let i = match self.state.selected() {
/// Some(i) => {
/// if i >= self.items.len() - 1 {
/// 0
/// } else {
/// i + 1
/// }
/// }
/// None => 0,
/// };
/// self.state.select(Some(i));
/// }
///
/// // Select the previous item. This will not be reflected until the widget is drawn in the
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
/// pub fn previous(&mut self) {
/// let i = match self.state.selected() {
/// Some(i) => {
/// if i == 0 {
/// self.items.len() - 1
/// } else {
/// i - 1
/// }
/// }
/// None => 0,
/// };
/// self.state.select(Some(i));
/// }
///
/// // Unselect the currently selected item if any. The implementation of `ListState` makes
/// // sure that the stored offset is also reset.
/// pub fn unselect(&mut self) {
/// self.state.select(None);
/// }
/// }
///
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
///
/// let mut events = Events::new(vec![String::from("Item 1"), String::from("Item 2")]);
///
/// loop {
/// terminal.draw(|f| {
/// // The items managed by the application are transformed to something
/// // that is understood by ratatui.
/// let items: Vec<ListItem> = events
/// .items
/// .iter()
/// .map(|i| ListItem::new(i.as_str()))
/// .collect();
/// // The `List` widget is then built with those items.
/// let list = List::new(items);
/// // Finally the widget is rendered using the associated state. `events.state` is
/// // effectively the only thing that we will "remember" from this draw call.
/// f.render_stateful_widget(list, f.size(), &mut events.state);
/// });
///
/// // In response to some input events or an external http request or whatever:
/// events.next();
/// }
/// ```
pub trait StatefulWidget {
/// State associated with the stateful widget.
///
/// If you don't need this then you probably want to implement [`Widget`] instead.
type State;
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom stateful widget.
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}
/// A `WidgetRef` is a trait that allows rendering a widget by reference.
///
/// This trait is useful when you want to store a reference to a widget and render it later. It also
/// allows you to render boxed widgets.
///
/// Boxed widgets allow you to store widgets with a type that is not known at compile time. This is
/// useful when you want to store a collection of widgets with different types. You can then iterate
/// over the collection and render each widget.
///
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal widgets. It
/// is currently marked as unstable as we are still evaluating the API and may make changes in the
/// future. See <https://github.com/ratatui/ratatui/issues/1287> for more information.
///
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
///
/// A blanket implementation of `WidgetRef` for `Option<W>` where `W` implements `WidgetRef` is
/// provided. This is a convenience approach to make it easier to attach child widgets to parent
/// widgets. It allows you to render an optional widget by reference.
///
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// text::Line,
/// widgets::{Widget, WidgetRef},
/// };
///
/// struct Greeting;
///
/// struct Farewell;
///
/// impl WidgetRef for Greeting {
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
/// Line::raw("Hello").render(area, buf);
/// }
/// }
///
/// /// Only needed for backwards compatibility
/// impl Widget for Greeting {
/// fn render(self, area: Rect, buf: &mut Buffer) {
/// self.render_ref(area, buf);
/// }
/// }
///
/// impl WidgetRef for Farewell {
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
/// Line::raw("Goodbye").right_aligned().render(area, buf);
/// }
/// }
///
/// /// Only needed for backwards compatibility
/// impl Widget for Farewell {
/// fn render(self, area: Rect, buf: &mut Buffer) {
/// self.render_ref(area, buf);
/// }
/// }
///
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let greeting = Greeting;
/// let farewell = Farewell;
///
/// // these calls do not consume the widgets, so they can be used again later
/// greeting.render_ref(area, buf);
/// farewell.render_ref(area, buf);
///
/// // a collection of widgets with different types
/// let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(greeting), Box::new(farewell)];
/// for widget in widgets {
/// widget.render_ref(area, buf);
/// }
/// # }
/// # }
/// ```
#[instability::unstable(feature = "widget-ref")]
pub trait WidgetRef {
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.
fn render_ref(&self, area: Rect, buf: &mut Buffer);
}
/// This allows you to render a widget by reference.
impl<W: WidgetRef> Widget for &W {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// A blanket implementation of `WidgetExt` for `Option<W>` where `W` implements `WidgetRef`.
///
/// This is a convenience implementation that makes it easy to attach child widgets to parent
/// widgets. It allows you to render an optional widget by reference.
///
/// The internal widgets use this pattern to render the optional `Block` widgets that are included
/// on most widgets.
/// Blanket implementation of `WidgetExt` for `Option<W>` where `W` implements `WidgetRef`.
///
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// text::Line,
/// widgets::{Widget, WidgetRef},
/// };
///
/// struct Parent {
/// child: Option<Child>,
/// }
///
/// struct Child;
///
/// impl WidgetRef for Child {
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
/// Line::raw("Hello from child").render(area, buf);
/// }
/// }
///
/// impl WidgetRef for Parent {
/// fn render_ref(&self, area: Rect, buf: &mut Buffer) {
/// self.child.render_ref(area, buf);
/// }
/// }
/// # }
/// ```
impl<W: WidgetRef> WidgetRef for Option<W> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
if let Some(widget) = self {
widget.render_ref(area, buf);
}
}
}
/// A `StatefulWidgetRef` is a trait that allows rendering a stateful widget by reference.
///
/// This is the stateful equivalent of `WidgetRef`. It is useful when you want to store a reference
/// to a stateful widget and render it later. It also allows you to render boxed stateful widgets.
///
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal stateful
/// widgets. It is currently marked as unstable as we are still evaluating the API and may make
/// changes in the future. See <https://github.com/ratatui/ratatui/issues/1287> for more
/// information.
///
/// A blanket implementation of `StatefulWidget` for `&W` where `W` implements `StatefulWidgetRef`
/// is provided.
///
/// See the documentation for [`WidgetRef`] for more information on boxed widgets.
/// See the documentation for [`StatefulWidget`] for more information on stateful widgets.
///
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{
/// buffer::Buffer,
/// layout::Rect,
/// style::Stylize,
/// text::Line,
/// widgets::{StatefulWidget, StatefulWidgetRef, Widget},
/// };
///
/// struct PersonalGreeting;
///
/// impl StatefulWidgetRef for PersonalGreeting {
/// type State = String;
/// fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
/// Line::raw(format!("Hello {}", state)).render(area, buf);
/// }
/// }
///
/// impl StatefulWidget for PersonalGreeting {
/// type State = String;
/// fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
/// (&self).render_ref(area, buf, state);
/// }
/// }
///
/// fn render(area: Rect, buf: &mut Buffer) {
/// let widget = PersonalGreeting;
/// let mut state = "world".to_string();
/// widget.render(area, buf, &mut state);
/// }
/// # }
/// ```
#[instability::unstable(feature = "widget-ref")]
pub trait StatefulWidgetRef {
/// State associated with the stateful widget.
///
/// If you don't need this then you probably want to implement [`WidgetRef`] instead.
type State;
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom stateful widget.
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
}
// Note: while StatefulWidgetRef is marked as unstable, the blanket implementation of StatefulWidget
// cannot be implemented as W::State is effectively pub(crate) and not accessible from outside the
// crate. Once stabilized, this blanket implementation can be added and the specific implementations
// on Table and List can be removed.
//
// /// Blanket implementation of `StatefulWidget` for `&W` where `W` implements `StatefulWidgetRef`.
// ///
// /// This allows you to render a stateful widget by reference.
// impl<W: StatefulWidgetRef> StatefulWidget for &W {
// type State = W::State;
// fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// StatefulWidgetRef::render_ref(self, area, buf, state);
// }
// }
/// Renders a string slice as a widget.
///
/// This implementation allows a string slice (`&str`) to act as a widget, meaning it can be drawn
/// onto a [`Buffer`] in a specified [`Rect`]. The slice represents a static string which can be
/// rendered by reference, thereby avoiding the need for string cloning or ownership transfer when
/// drawing the text to the screen.
impl Widget for &str {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// Provides the ability to render a string slice by reference.
///
/// This trait implementation ensures that a string slice, which is an immutable view over a
/// `String`, can be drawn on demand without requiring ownership of the string itself. It utilizes
/// the default text style when rendering onto the provided [`Buffer`] at the position defined by
/// [`Rect`].
impl WidgetRef for &str {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
/// Renders a `String` object as a widget.
///
/// This implementation enables an owned `String` to be treated as a widget, which can be rendered
/// on a [`Buffer`] within the bounds of a given [`Rect`].
impl Widget for String {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// Provides the ability to render a `String` by reference.
///
/// This trait allows for a `String` to be rendered onto the [`Buffer`], similarly using the default
/// style settings. It ensures that an owned `String` can be rendered efficiently by reference,
/// without the need to give up ownership of the underlying text.
impl WidgetRef for String {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
#[cfg(test)]
mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::text::Line;
#[fixture]
fn buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 20, 1))
}
mod widget {
use super::*;
struct Greeting;
impl Widget for Greeting {
fn render(self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
#[rstest]
fn render(mut buf: Buffer) {
let widget = Greeting;
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
}
mod widget_ref {
use super::*;
struct Greeting;
struct Farewell;
impl WidgetRef for Greeting {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
impl WidgetRef for Farewell {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Goodbye").right_aligned().render(area, buf);
}
}
#[rstest]
fn render_ref(mut buf: Buffer) {
let widget = Greeting;
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
/// Ensure that the blanket implementation of `Widget` for `&W` where `W` implements
/// `WidgetRef` works as expected.
#[rstest]
fn blanket_render(mut buf: Buffer) {
let widget = &Greeting;
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn box_render_ref(mut buf: Buffer) {
let widget: Box<dyn WidgetRef> = Box::new(Greeting);
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn vec_box_render(mut buf: Buffer) {
let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Greeting), Box::new(Farewell)];
for widget in widgets {
widget.render_ref(buf.area, &mut buf);
}
assert_eq!(buf, Buffer::with_lines(["Hello Goodbye"]));
}
}
#[fixture]
fn state() -> String {
"world".to_string()
}
mod stateful_widget {
use super::*;
struct PersonalGreeting;
impl StatefulWidget for PersonalGreeting {
type State = String;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Line::from(format!("Hello {state}")).render(area, buf);
}
}
#[rstest]
fn render(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting;
widget.render(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
}
mod stateful_widget_ref {
use super::*;
struct PersonalGreeting;
impl StatefulWidgetRef for PersonalGreeting {
type State = String;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Line::from(format!("Hello {state}")).render(area, buf);
}
}
#[rstest]
fn render_ref(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting;
widget.render_ref(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
// Note this cannot be tested until the blanket implementation of StatefulWidget for &W
// where W implements StatefulWidgetRef is added. (see the comment in the blanket
// implementation for more).
// /// This test is to ensure that the blanket implementation of `StatefulWidget` for `&W`
// where /// `W` implements `StatefulWidgetRef` works as expected.
// #[rstest]
// fn stateful_widget_blanket_render(mut buf: Buffer, mut state: String) {
// let widget = &PersonalGreeting;
// widget.render(buf.area, &mut buf, &mut state);
// assert_eq!(buf, Buffer::with_lines(["Hello world "]));
// }
#[rstest]
fn box_render_render(mut buf: Buffer, mut state: String) {
let widget = Box::new(PersonalGreeting);
widget.render_ref(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
}
mod option_widget_ref {
use super::*;
struct Greeting;
impl WidgetRef for Greeting {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
#[rstest]
fn render_ref_some(mut buf: Buffer) {
let widget = Some(Greeting);
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn render_ref_none(mut buf: Buffer) {
let widget: Option<Greeting> = None;
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([" "]));
}
}
mod str {
use super::*;
#[rstest]
fn render(mut buf: Buffer) {
"hello world".render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_area(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
"hello world, just hello".render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
"hello world".render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render(mut buf: Buffer) {
Some("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render_ref(mut buf: Buffer) {
Some("hello world").render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
}
mod string {
use super::*;
#[rstest]
fn render(mut buf: Buffer) {
String::from("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_area(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
String::from("hello world, just hello").render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
String::from("hello world").render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render(mut buf: Buffer) {
Some(String::from("hello world")).render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render_ref(mut buf: Buffer) {
Some(String::from("hello world")).render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
}
}