diff --git a/Cargo.lock b/Cargo.lock index f949c4d5..73690db7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -883,6 +883,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "dissimilar" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" + [[package]] name = "document-features" version = "0.2.10" @@ -2536,6 +2542,7 @@ dependencies = [ "rand_chacha 0.9.0", "ratatui-core", "ratatui-crossterm", + "ratatui-macros", "ratatui-termion", "ratatui-termwiz", "ratatui-widgets", @@ -2587,6 +2594,15 @@ dependencies = [ "rstest", ] +[[package]] +name = "ratatui-macros" +version = "0.7.0-alpha.0" +dependencies = [ + "ratatui-core", + "ratatui-widgets", + "trybuild", +] + [[package]] name = "ratatui-termion" version = "0.1.0-alpha.1" @@ -2996,6 +3012,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3250,6 +3275,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "target-triple" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" + [[package]] name = "tempfile" version = "3.16.0" @@ -3264,6 +3295,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminfo" version = "0.8.0" @@ -3505,11 +3545,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -3518,6 +3573,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -3660,6 +3717,22 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "trybuild" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b812699e0c4f813b872b373a4471717d9eb550da14b311058a4d9cf4173cbca6" +dependencies = [ + "dissimilar", + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 4b7c73a2..98450658 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ default-members = [ "ratatui-crossterm", # this is not included as it doesn't compile on windows # "ratatui-termion", + "ratatui-macros", "ratatui-termwiz", "ratatui-widgets", "examples/apps/*", @@ -37,6 +38,7 @@ pretty_assertions = "1.4.1" ratatui = { path = "ratatui", version = "0.30.0-alpha.1" } ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.2" } ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.1" } +ratatui-macros = { path = "ratatui-macros", version = "0.7.0-alpha.0" } ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.1" } ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.1" } ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.1" } diff --git a/ratatui-macros/.rustfmt.toml b/ratatui-macros/.rustfmt.toml new file mode 100644 index 00000000..b7d70c3e --- /dev/null +++ b/ratatui-macros/.rustfmt.toml @@ -0,0 +1,2 @@ +format_macro_bodies = true +format_macro_matchers = true diff --git a/ratatui-macros/CHANGELOG.md b/ratatui-macros/CHANGELOG.md new file mode 100644 index 00000000..0919b218 --- /dev/null +++ b/ratatui-macros/CHANGELOG.md @@ -0,0 +1,117 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.6.0](https://github.com/ratatui/ratatui-macros/compare/v0.5.0...v0.6.0) - 2024-10-21 + +### Other + +- *(deps)* bump the cargo-dependencies group with 2 updates ([#73](https://github.com/ratatui/ratatui-macros/pull/73)) +- *(deps)* bump ratatui from 0.28.0 to 0.28.1 in the cargo-dependencies group ([#70](https://github.com/ratatui/ratatui-macros/pull/70)) + +## [0.5.0] - 2024-08-12 + +### 🐛 Bug Fixes + +- Bump version to 0.5.0 + +## [0.4.4](https://github.com/ratatui-org/ratatui-macros/compare/v0.4.3...v0.4.4) - 2024-08-09 + +### Other + +- *(deps)* bump ratatui to 0.28.0 ([#66](https://github.com/ratatui-org/ratatui-macros/pull/66)) +- *(deps)* bump trybuild from 1.0.98 to 1.0.99 in the cargo-dependencies group ([#65](https://github.com/ratatui-org/ratatui-macros/pull/65)) +- *(deps)* bump trybuild from 1.0.97 to 1.0.98 in the cargo-dependencies group ([#62](https://github.com/ratatui-org/ratatui-macros/pull/62)) + +## [0.4.3](https://github.com/ratatui-org/ratatui-macros/compare/v0.4.2...v0.4.3) - 2024-07-22 + +### Added + +- allow span macro to accept a bare expression ([#61](https://github.com/ratatui-org/ratatui-macros/pull/61)) + +### Other + +- *(deps)* bump trybuild from 1.0.96 to 1.0.97 in the cargo-dependencies group ([#59](https://github.com/ratatui-org/ratatui-macros/pull/59)) + +## [0.4.2](https://github.com/ratatui-org/ratatui-macros/compare/v0.4.1...v0.4.2) - 2024-06-29 + +### Added + +- Use `::ratatui` instead of `ratatui` ([#54](https://github.com/ratatui-org/ratatui-macros/pull/54)) +- Add row! macro ([#52](https://github.com/ratatui-org/ratatui-macros/pull/52)) + +### Other + +- Update README with row! documentation ([#56](https://github.com/ratatui-org/ratatui-macros/pull/56)) +- Make doc examples shorter by removing duplicate imports ([#55](https://github.com/ratatui-org/ratatui-macros/pull/55)) + +## [0.4.1](https://github.com/ratatui-org/ratatui-macros/compare/v0.4.0...v0.4.1) - 2024-06-24 + +### Other + +- *(deps)* bump ratatui from 0.26.3 to 0.27.0 in the cargo-dependencies group ([#51](https://github.com/ratatui-org/ratatui-macros/pull/51)) +- Update dependabot.yml to group dependencies ([#50](https://github.com/ratatui-org/ratatui-macros/pull/50)) +- *(deps)* bump ratatui from 0.26.2 to 0.26.3 ([#48](https://github.com/ratatui-org/ratatui-macros/pull/48)) +- *(deps)* bump trybuild from 1.0.95 to 1.0.96 ([#47](https://github.com/ratatui-org/ratatui-macros/pull/47)) + +## [0.4.0](https://github.com/ratatui-org/ratatui-macros/compare/v0.3.1...v0.4.0) - 2024-05-15 + +### Added + +- *(layout)* [**breaking**] Use `*=` instead of `=*` ([#45](https://github.com/ratatui-org/ratatui-macros/pull/45)) + +## [0.3.1](https://github.com/ratatui-org/ratatui-macros/compare/v0.3.0...v0.3.1) - 2024-05-13 + +### Added + +- Better error messages for `span!` macro ([#43](https://github.com/ratatui-org/ratatui-macros/pull/43)) + +### Fixed + +- downgrade ratatui to 0.26.2 ([#41](https://github.com/ratatui-org/ratatui-macros/pull/41)) + +### Other + +- Update authors to ratatui developers ([#44](https://github.com/ratatui-org/ratatui-macros/pull/44)) + +## [0.3.0](https://github.com/ratatui-org/ratatui-macros/compare/v0.2.4...v0.3.0) - 2024-05-09 + +### Added + +- Use release-plz ([#38](https://github.com/ratatui-org/ratatui-macros/pull/38)) +- Add text! macro ([#36](https://github.com/ratatui-org/ratatui-macros/pull/36)) +- Add fill constraint ([#34](https://github.com/ratatui-org/ratatui-macros/pull/34)) +- [**breaking**] Remove color `palette!` macro ([#32](https://github.com/ratatui-org/ratatui-macros/pull/32)) +- Replace raw! and styled! with span! macro ([#30](https://github.com/ratatui-org/ratatui-macros/pull/30)) +- Add `line!` attribute macro ([#29](https://github.com/ratatui-org/ratatui-macros/pull/29)) +- *(text)* add raw! and styled! macros ([#4](https://github.com/ratatui-org/ratatui-macros/pull/4)) + +### Fixed + +- Update repo url in Cargo.toml ([#39](https://github.com/ratatui-org/ratatui-macros/pull/39)) + +### Other + +- Use `.areas(area)` instead of `.split(area).to_vec().try_into().unwrap()` ([#37](https://github.com/ratatui-org/ratatui-macros/pull/37)) +- Update README.md with short description of span and line macros ([#33](https://github.com/ratatui-org/ratatui-macros/pull/33)) +- format using cargo +nightly fmt ([#31](https://github.com/ratatui-org/ratatui-macros/pull/31)) +- *(deps)* bump ratatui from 0.27.0-alpha.3 to 0.27.0-alpha.5 ([#27](https://github.com/ratatui-org/ratatui-macros/pull/27)) +- *(deps)* bump trybuild from 1.0.91 to 1.0.93 ([#28](https://github.com/ratatui-org/ratatui-macros/pull/28)) +- *(deps)* bump ratatui from 0.27.0-alpha.2 to 0.27.0-alpha.3 ([#24](https://github.com/ratatui-org/ratatui-macros/pull/24)) +- *(deps)* bump trybuild from 1.0.90 to 1.0.91 ([#23](https://github.com/ratatui-org/ratatui-macros/pull/23)) +- *(deps)* bump trybuild from 1.0.88 to 1.0.90 ([#20](https://github.com/ratatui-org/ratatui-macros/pull/20)) +- *(deps)* bump ratatui from 0.27.0-alpha.0 to 0.27.0-alpha.2 ([#22](https://github.com/ratatui-org/ratatui-macros/pull/22)) +- *(deps)* bump mio from 0.8.10 to 0.8.11 ([#18](https://github.com/ratatui-org/ratatui-macros/pull/18)) +- *(deps)* bump ratatui from 0.26.0-alpha.1 to 0.27.0-alpha.0 ([#19](https://github.com/ratatui-org/ratatui-macros/pull/19)) +- add cargo husky pre-commit hook ([#8](https://github.com/ratatui-org/ratatui-macros/pull/8)) +- Create dependabot.yml ([#7](https://github.com/ratatui-org/ratatui-macros/pull/7)) +- use rust cache to cache deps ([#6](https://github.com/ratatui-org/ratatui-macros/pull/6)) +- readme tweaks ([#5](https://github.com/ratatui-org/ratatui-macros/pull/5)) +- Update README.md +- Update README.md +- Update README.md +- Update README.md +- Add link to ratatui diff --git a/ratatui-macros/Cargo.toml b/ratatui-macros/Cargo.toml new file mode 100644 index 00000000..485ddd2a --- /dev/null +++ b/ratatui-macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ratatui-macros" +version = "0.7.0-alpha.0" +edition = "2021" +authors = ["The Ratatui Developers"] +description = "Macros for Ratatui" +license = "MIT" +repository = "https://github.com/ratatui/ratatui" +documentation = "https://docs.rs/ratatui-macros" +keywords = ["ratatui", "macros", "tui", "ui"] +categories = ["command-line-interface"] + +[dependencies] +ratatui-core.workspace = true +ratatui-widgets.workspace = true + +[dev-dependencies] +trybuild = { version = "1.0.103", features = ["diff"] } diff --git a/ratatui-macros/LICENSE b/ratatui-macros/LICENSE new file mode 100644 index 00000000..b1780ce4 --- /dev/null +++ b/ratatui-macros/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2024 Dheepak Krishnamurthy +Copyright (c) 2025 The Ratatui Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ratatui-macros/README.md b/ratatui-macros/README.md new file mode 100644 index 00000000..c675cb26 --- /dev/null +++ b/ratatui-macros/README.md @@ -0,0 +1,200 @@ +# Ratatui Macros + +[![Crates.io badge]][ratatui_macros crate] [![License badge]](./LICENSE) +[![Docs.rs badge]][API Docs] [![CI Badge]][CI Status] +[![Crate Downloads badge]][ratatui_macros crate] + +`ratatui-macros` is a Rust crate that provides easy-to-use macros for simplifying boilerplate +associated with creating UI using [Ratatui]. + +This is an experimental playground for us to explore macros that would be useful to have in Ratatui +proper. + +## Features + +- Constraint-based Layouts: Easily define layout constraints such as fixed, percentage, minimum, and + maximum sizes, as well as ratios. +- Directional Layouts: Specify layouts as either horizontal or vertical with simple macro commands. +- Span and Line macros: Make it easier to create spans and lines with styling. + +## Getting Started + +To use `ratatui-macros` in your Rust project, add it as a dependency in your `Cargo.toml`: + +```shell +cargo add ratatui-macros +``` + +Then, import the macros in your Rust file: + +```rust +use ratatui_macros::{ + constraint, + constraints, + horizontal, + vertical, + span, + line, +}; +``` + +### Layout + +If you are new to Ratatui, check out the [Layout concepts] article on the Ratatui website before proceeding. + +Use the `constraints!` macro to define layout constraints: + +```rust +# use ratatui_core::layout::Constraint; +use ratatui_macros::constraints; + +assert_eq!( + constraints![==50, ==30%, >=3, <=1, ==1/2, *=1], + [ + Constraint::Length(50), + Constraint::Percentage(30), + Constraint::Min(3), + Constraint::Max(1), + Constraint::Ratio(1, 2), + Constraint::Fill(1), + ] +) +``` + +```rust +# use ratatui_core::layout::Constraint; +use ratatui_macros::constraints; + +assert_eq!( + constraints![==1/4; 4], + [ + Constraint::Ratio(1, 4), + Constraint::Ratio(1, 4), + Constraint::Ratio(1, 4), + Constraint::Ratio(1, 4), + ] +) +``` + +Use the `constraint!` macro to define individual constraints: + +```rust +# use ratatui_core::layout::Constraint; +use ratatui_macros::constraint; + +assert_eq!( + constraint!(==50), + Constraint::Length(50), +) +``` + +Create vertical and horizontal layouts using the `vertical!` and `horizontal!` macros: + +```rust +# use ratatui_core::layout::Rect; +use ratatui_macros::{vertical, horizontal}; + +let area = Rect { x: 0, y: 0, width: 10, height: 10 }; + +let [main, bottom] = vertical![==100%, >=3].areas(area); + +assert_eq!(bottom.y, 7); +assert_eq!(bottom.height, 3); + +let [left, main, right] = horizontal![>=3, ==100%, >=3].areas(area); + +assert_eq!(left.width, 3); +assert_eq!(right.width, 3); +``` + +## Spans + +The `span!` macro create raw and styled `Span`s. They each take a format string and arguments. +`span!` accepts as the first parameter any value that can be converted to a `Style` followed by a +`;` followed by the format string and arguments. + +```rust +# use ratatui_core::style::{Color, Modifier, Style, Stylize}; +use ratatui_macros::span; + +let name = "world!"; +let raw_greeting = span!("hello {name}"); +let styled_greeting = span!(Style::new().green(); "hello {name}"); +let styled_greeting = span!(Color::Green; "hello {name}"); +let styled_greeting = span!(Modifier::BOLD; "hello {name}"); +``` + +## Line + +The `line!` macro creates a `Line` that contains a sequence of spans. It is similar to the `vec!` +macro. + +```rust +use ratatui_macros::line; + +let name = "world!"; +let line = line!["hello", format!("{name}")]; +let line = line!["bye"; 2]; +``` + +## Text + +The `text!` macro creates a `Text` that contains a sequence of lines. It is similar to the `vec!` +macro. + +```rust +use ratatui_macros::{span, line, text}; + +let name = "world!"; +let text = text!["hello", format!("{name}")]; +let text = text!["bye"; 2]; +``` + +It is even possible to use `span!` and `line!` in the `text!` macro: + +```rust +# use ratatui_core::style::{Modifier, Stylize}; +use ratatui_macros::{span, line, text}; +let name = "Bye!!!"; +let text = text![line!["hello", "world".bold()], span!(Modifier::BOLD; "{name}")]; +``` + +## Row + +The `row!` macro creates a `Row` that contains a sequence of `Cell`. It is similar to the `vec!` +macro. A `Row` represents a sequence of `Cell`s in a single row of a table. + +```rust +use ratatui_macros::row; + +let rows = [ + row!["hello", "world"], + row!["goodbye", "world"], +]; +``` + +It is even possible to use `span!`, `line!` and `text!` in the `row!` macro: + +```rust +# use ratatui_core::style::{Modifier, Stylize}; +use ratatui_macros::{span, line, text, row}; +let name = "Bye!!!"; +let text = row![text![line!["hello", "world".bold()]], span!(Modifier::BOLD; "{name}")]; +``` + +## Contributing + +Contributions to `ratatui-macros` are welcome! Whether it's submitting a bug report, a feature +request, or a pull request, all forms of contributions are valued and appreciated. + +[Crates.io badge]: https://img.shields.io/crates/v/ratatui-macros?logo=rust&style=flat-square +[License badge]: https://img.shields.io/crates/l/ratatui-macros +[CI Badge]: + https://img.shields.io/github/actions/workflow/status/ratatui-org/ratatui-macros/ci.yml?logo=github&style=flat-square +[Docs.rs badge]: https://img.shields.io/docsrs/ratatui-macros?logo=rust&style=flat-square +[Crate Downloads badge]: https://img.shields.io/crates/d/ratatui-macros?logo=rust&style=flat-square +[ratatui_macros crate]: https://crates.io/crates/ratatui-macros +[API Docs]: https://docs.rs/ratatui-macros +[CI Status]: https://github.com/kdheepak/ratatui-macros/actions +[Ratatui]: https://github.com/ratatui-org/ratatui +[Layout concepts]: https://ratatui.rs/concepts/layout diff --git a/ratatui-macros/src/layout.rs b/ratatui-macros/src/layout.rs new file mode 100644 index 00000000..d543531d --- /dev/null +++ b/ratatui-macros/src/layout.rs @@ -0,0 +1,197 @@ +/// Creates a single constraint. +/// +/// If creating an array of constraints, you probably want to use +/// [`constraints!`] instead. +/// +/// # Examples +/// +/// ``` +/// # use ratatui_core::layout::Constraint; +/// use ratatui_macros::constraint; +/// assert_eq!(constraint!(>= 3 + 4), Constraint::Min(7)); +/// assert_eq!(constraint!(<= 3 + 4), Constraint::Max(7)); +/// assert_eq!(constraint!(== 1 / 3), Constraint::Ratio(1, 3)); +/// assert_eq!(constraint!(== 3), Constraint::Length(3)); +/// assert_eq!(constraint!(== 10 %), Constraint::Percentage(10)); +/// assert_eq!(constraint!(*= 1), Constraint::Fill(1)); +/// ``` +#[macro_export] +macro_rules! constraint { + (== $token:tt %) => { + $crate::ratatui_core::layout::Constraint::Percentage($token) + }; + (>= $expr:expr) => { + $crate::ratatui_core::layout::Constraint::Min($expr) + }; + (<= $expr:expr) => { + $crate::ratatui_core::layout::Constraint::Max($expr) + }; + (== $num:tt / $denom:tt) => { + $crate::ratatui_core::layout::Constraint::Ratio($num as u32, $denom as u32) + }; + (== $expr:expr) => { + $crate::ratatui_core::layout::Constraint::Length($expr) + }; + (*= $expr:expr) => { + $crate::ratatui_core::layout::Constraint::Fill($expr) + }; +} + +/// Creates an array of constraints. +/// +/// See [`constraint!`] for more information. +/// +/// If you want to solve the constraints, see +/// [`vertical!`] and [`horizontal!`] macros. +/// +/// # Examples +/// +/// ```rust +/// use ratatui_macros::constraints; +/// assert_eq!(constraints![==5, ==30%, >=3, <=1, ==1/2].len(), 5); +/// assert_eq!(constraints![==5; 5].len(), 5); +/// ``` +/// +/// ```rust +/// # use ratatui_core::layout::Constraint; +/// # use ratatui_macros::constraints; +/// assert_eq!( +/// constraints![==50, ==30%, >=3, <=1, ==1/2, *=1], +/// [ +/// Constraint::Length(50), +/// Constraint::Percentage(30), +/// Constraint::Min(3), +/// Constraint::Max(1), +/// Constraint::Ratio(1, 2), +/// Constraint::Fill(1), +/// ] +/// ) +/// ``` +#[macro_export] +macro_rules! constraints { + // Note: this implementation forgoes speed for the sake of simplicity. Adding variations of the + // comma and semicolon rules for each constraint type would be faster, but would result in a lot + // of duplicated code. + + // Cannot start the constraints macro with a , + ([ , $($rest:tt)* ] -> () []) => { + compile_error!("No rules expected the token `,` while trying to match the end of the macro") + }; + + // Comma finishes a constraint element, so parse it and continue. + // When a comma is encountered, it marks the end of a constraint element, so this rule is responsible + // for parsing the constraint expression up to the comma and continuing the parsing process. + // It accumulated the $partial contains a Constraint and is parsed using a separate $crate::constraint! macro. + // The constraint is then appended to the list of parsed constraints. + // + // [ , $($rest:tt)* ] -> In the rule matcher, this pattern matches a comma followed + // by the rest of the tokens. The comma signals the end of + // the current constraint element. + // ($($partial:tt)*) -> In the rule matcher, this contains the partial tokens + // accumulated so far for the current constraint element. + // [$($parsed:tt)* ] -> This contains the constraints that have been successfully + // parsed so far. + // $crate::constraint!($($partial)*) -> This macro call parses and expands the accumulated + // partial tokens into a single Constraint expression. + // [$($parsed)* $crate::constraint!(...)] -> Appends the newly parsed constraint to the list of + // already parsed constraints. + ([ , $($rest:tt)* ] -> ($($partial:tt)*) [ $($parsed:tt)* ]) => { + $crate::constraints!([$($rest)*] -> () [$($parsed)* $crate::constraint!($($partial)*) ,]) + }; + + // Semicolon indicates that there's repetition. The trailing comma is required because the 'entrypoint' + // rule adds a trailing comma. + // This rule is triggered when a semicolon is encountered, indicating that there is repetition of + // constraints. It handles the repetition logic by parsing the count and generating an array of + // constraints using the $crate::constraint! macro. + // + // [ ; $count:expr , ] -> In the rule matcher, this pattern matches a semicolon + // followed by an expression representing the count, and a + // trailing comma. + // ($($partial:tt)*) -> In the rule matcher, this contains the partial tokens + // accumulated so far for the current constraint element. + // This represents everything before the ; + // [] -> There will be no existed parsed constraints when using ; + // $crate::constraint!($($partial)*) -> This macro call parses and expands the accumulated + // partial tokens into a single Constraint expression. + // [$crate::constraint!(...) ; $count] -> Generates an array of constraints by repeating the + // parsed constraint count number of times. + ([ ; $count:expr , ] -> ($($partial:tt)*) []) => { + [$crate::constraint!($($partial)*); $count] + }; + + // Pull the first token (which can't be a comma or semicolon) onto the accumulator. + // if first token is a comma or semicolon, previous rules will match before this rule + // + // [ $head:tt $($rest:tt)* ] -> In the rule matcher, this pulls a single `head` token + // out of the previous rest, and puts + // the remaining into `rest` + // [ $($rest)* ] -> This is what is fed back into the `constraints!` macro + // as the first segment for the match rule + // + // ($($partial:tt)*) -> In the rule matcher, this contains previous partial + // tokens that will make up a `Constraint` expression + // ($($partial)* $head) -> This combines head with the previous partial tokens + // i.e. this is the accumulated tokens + // + // [ $($parsed:tt)* ] -> In the rule matcher, this contains all parsed exprs + // [$($parsed)* ] -> These are passed on to the next match untouched. + ([ $head:tt $($rest:tt)* ] -> ($($partial:tt)*) [ $($parsed:tt)* ]) => { + $crate::constraints!([$($rest)*] -> ($($partial)* $head) [$($parsed)* ]) + }; + + // This rule is triggered when there are no more input tokens to process. It signals the end of the + // macro invocation and outputs the parsed constraints as a final array. + ([$(,)?] -> () [ $( $parsed:tt )* ]) => { + [$($parsed)*] + }; + + // Entrypoint where there's no comma at the end. + // We add a comma to make sure there's always a trailing comma. + // Right-hand side will accumulate the actual `Constraint` literals. + ($( $constraint:tt )+) => { + $crate::constraints!([ $($constraint)+ , ] -> () []) + }; +} + +/// Creates a vertical layout with specified constraints. +/// +/// It accepts a series of constraints and applies them to create a vertical layout. The constraints +/// can include fixed sizes, minimum and maximum sizes, percentages, and ratios. +/// +/// See [`constraint!`] or [`constraints!`] for more information. +/// +/// # Examples +/// +/// ``` +/// // Vertical layout with a fixed size and a percentage constraint +/// use ratatui_macros::vertical; +/// vertical![== 50, == 30%]; +/// ``` +#[macro_export] +macro_rules! vertical { + ($( $constraint:tt )+) => { + $crate::ratatui_core::layout::Layout::vertical($crate::constraints!( $($constraint)+ )) + }; +} + +/// Creates a horizontal layout with specified constraints. +/// +/// It takes a series of constraints and applies them to create a horizontal layout. The constraints +/// can include fixed sizes, minimum and maximum sizes, percentages, and ratios. +/// +/// See [`constraint!`] or [`constraints!`] for more information. +/// +/// # Examples +/// +/// ``` +/// // Horizontal layout with a ratio constraint and a minimum size constraint +/// use ratatui_macros::horizontal; +/// horizontal![== 1/3, >= 100]; +/// ``` +#[macro_export] +macro_rules! horizontal { + ($( $constraint:tt )+) => { + $crate::ratatui_core::layout::Layout::horizontal($crate::constraints!( $($constraint)+ )) + }; +} diff --git a/ratatui-macros/src/lib.rs b/ratatui-macros/src/lib.rs new file mode 100644 index 00000000..fdf88ea2 --- /dev/null +++ b/ratatui-macros/src/lib.rs @@ -0,0 +1,10 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +mod layout; +mod line; +mod row; +mod span; +mod text; + +// Re-export the core crate to use the types in macros +pub use ratatui_core; diff --git a/ratatui-macros/src/line.rs b/ratatui-macros/src/line.rs new file mode 100644 index 00000000..c326c4a9 --- /dev/null +++ b/ratatui-macros/src/line.rs @@ -0,0 +1,97 @@ +/// A macro for creating a [`Line`] using vec! syntax. +/// +/// `line!` is similar to the [`vec!`] macro, but it returns a [`Line`] instead of a `Vec`. +/// +/// # Examples +/// +/// * Create a [`Line`] containing a vector of [`Span`]s: +/// +/// ```rust +/// # use ratatui_core::style::Stylize; +/// use ratatui_macros::line; +/// +/// let line = line!["hello", "world"]; +/// let line = line!["hello".red(), "world".red().bold()]; +/// ``` +/// +/// * Create a [`Line`] from a given [`Span`] repeated some amount of times: +/// +/// ```rust +/// # use ratatui_macros::line; +/// let line = line!["hello"; 2]; +/// ``` +/// +/// * Use [`span!`] macro inside [`line!`] macro for formatting. +/// +/// ```rust +/// # use ratatui_core::style::Modifier; +/// use ratatui_macros::{line, span}; +/// +/// let line = line![span!("hello {}", "world"), span!(Modifier::BOLD; "goodbye {}", "world")]; +/// ``` +/// +/// [`Line`]: crate::text::Line +/// [`Span`]: crate::text::Span +#[macro_export] +macro_rules! line { + () => { + $crate::ratatui_core::text::Line::default() + }; + ($span:expr; $n:expr) => { + $crate::ratatui_core::text::Line::from(vec![$span.into(); $n]) + }; + ($($span:expr),+ $(,)?) => {{ + $crate::ratatui_core::text::Line::from(vec![ + $( + $span.into(), + )+ + ]) + }}; +} + +#[cfg(test)] +mod tests { + use ratatui_core::text::{Line, Span}; + + #[test] + fn line_literal() { + let line = line!["hello", "world"]; + assert_eq!(line, Line::from(vec!["hello".into(), "world".into()])); + } + + #[test] + fn line_raw_instead_of_literal() { + let line = line![Span::raw("hello"), "world"]; + assert_eq!(line, Line::from(vec!["hello".into(), "world".into()])); + } + + #[test] + fn line_vec_count_syntax() { + let line = line!["hello"; 2]; + assert_eq!(line, Line::from(vec!["hello".into(), "hello".into()])); + } + + #[test] + fn line_vec_count_syntax_with_span() { + let line = line![crate::span!("hello"); 2]; + assert_eq!(line, Line::from(vec!["hello".into(), "hello".into()])); + } + + #[test] + fn line_empty() { + let line = line![]; + assert_eq!(line, Line::default()); + } + + #[test] + fn line_single_span() { + let line = line![Span::raw("foo")]; + assert_eq!(line, Line::from(vec!["foo".into()])); + } + + #[test] + fn line_repeated_span() { + let line = line![Span::raw("foo"); 2]; + assert_eq!(line, Line::from(vec!["foo".into(), "foo".into()])); + } +} diff --git a/ratatui-macros/src/row.rs b/ratatui-macros/src/row.rs new file mode 100644 index 00000000..feb80c50 --- /dev/null +++ b/ratatui-macros/src/row.rs @@ -0,0 +1,139 @@ +/// A macro for creating a [`Row`] using vec! syntax. +/// +/// `row!` is similar to the [`vec!`] macro, but it returns a [`Row`] instead of a `Vec`. +/// +/// # Examples +/// +/// * Create a [`Row`] containing a vector of [`Cell`]s: +/// +/// ```rust +/// # use ratatui_core::style::Stylize; +/// use ratatui_macros::row; +/// +/// let row = row!["hello", "world"]; +/// let row = row!["hello".red(), "world".red().bold()]; +/// ``` +/// +/// * Create an empty [`Row`]: +/// +/// ```rust +/// # use ratatui_macros::row; +/// let empty_row = row![]; +/// ``` +/// +/// * Create a [`Row`] from a given [`Cell`] repeated some amount of times: +/// +/// ```rust +/// # use ratatui_macros::row; +/// let row = row!["hello"; 2]; +/// ``` +/// +/// * Use [`text!`], [`line!`] or [`span!`] macro inside [`row!`] macro. +/// +/// ```rust +/// # use ratatui_core::style::{Modifier}; +/// use ratatui_macros::{row, line, text, span}; +/// +/// let row = row![ +/// line!["hello", "world"], span!(Modifier::BOLD; "goodbye {}", "world"), +/// text!["hello", "world"], +/// ]; +/// ``` +/// +/// [`Row`]: crate::widgets::Row +/// [`Cell`]: crate::widgets::Cell +#[macro_export] +macro_rules! row { + () => { + ::ratatui_widgets::table::Row::default() + }; + ($cell:expr; $n:expr) => { + ::ratatui_widgets::table::Row::new(vec![::ratatui_widgets::table::Cell::from($cell); $n]) + }; + ($($cell:expr),+ $(,)?) => {{ + ::ratatui_widgets::table::Row::new(vec![ + $( + ::ratatui_widgets::table::Cell::from($cell), + )+ + ]) + }}; +} + +#[cfg(test)] +mod tests { + + use ratatui_core::text::Text; + use ratatui_widgets::table::{Cell, Row}; + + #[test] + fn row_literal() { + let row = row!["hello", "world"]; + assert_eq!( + row, + Row::new(vec![Cell::from("hello"), Cell::from("world")]) + ); + } + + #[test] + fn row_empty() { + let row = row![]; + assert_eq!(row, Row::default()); + } + + #[test] + fn row_single_cell() { + let row = row![Cell::from("foo")]; + assert_eq!(row, Row::new(vec![Cell::from("foo")])); + } + + #[test] + fn row_repeated_cell() { + let row = row![Cell::from("foo"); 2]; + assert_eq!(row, Row::new(vec![Cell::from("foo"), Cell::from("foo")])); + } + + #[test] + fn row_explicit_use_of_span_and_line() { + let row = row![crate::line!("hello"), crate::span!["world"]]; + assert_eq!( + row, + Row::new(vec![Cell::from("hello"), Cell::from("world")]) + ); + } + + #[test] + fn row_vec_count_syntax() { + let row = row!["hello"; 2]; + assert_eq!( + row, + Row::new(vec![Cell::from("hello"), Cell::from("hello")]) + ); + } + + #[test] + fn multiple_rows() { + use crate::text; + let rows = [ + row!["Find File", text!["ctrl+f"].right_aligned()], + row!["Open recent", text!["ctrl+r"].right_aligned()], + row!["Open config", text!["ctrl+k"].right_aligned()], + ]; + assert_eq!( + rows, + [ + Row::new([ + Cell::from("Find File"), + Cell::from(Text::raw("ctrl+f").right_aligned()), + ]), + Row::new([ + Cell::from("Open recent"), + Cell::from(Text::raw("ctrl+r").right_aligned()), + ]), + Row::new([ + Cell::from("Open config"), + Cell::from(Text::raw("ctrl+k").right_aligned()), + ]), + ] + ); + } +} diff --git a/ratatui-macros/src/span.rs b/ratatui-macros/src/span.rs new file mode 100644 index 00000000..1d25c9fe --- /dev/null +++ b/ratatui-macros/src/span.rs @@ -0,0 +1,245 @@ +/// A macro for creating a [`Span`] using formatting syntax. +/// +/// `span!` is similar to the [`format!`] macro, but it returns a [`Span`] instead of a `String`. In +/// addition, it also accepts an expression for the first argument, which will be converted to a +/// string using the [`format!`] macro. +/// +/// If semicolon follows the first argument, then the first argument is a [`Style`] and a styled +/// [`Span`] will be created. Otherwise, the [`Span`] will be created as a raw span (i.e. with style +/// set to `Style::default()`). +/// +/// # Examples +/// +/// ```rust +/// # use ratatui_core::style::{Color, Modifier, Style, Stylize}; +/// use ratatui_macros::span; +/// +/// let content = "content"; +/// +/// // expression +/// let span = span!(content); +/// +/// // format string +/// let span = span!("test content"); +/// let span = span!("test {}", "content"); +/// let span = span!("{} {}", "test", "content"); +/// let span = span!("test {content}"); +/// let span = span!("test {content}", content = "content"); +/// +/// // with format specifiers +/// let span = span!("test {:4}", 123); +/// let span = span!("test {:04}", 123); +/// +/// let style = Style::new().green(); +/// +/// // styled expression +/// let span = span!(style; content); +/// +/// // styled format string +/// let span = span!(style; "test content"); +/// let span = span!(style; "test {}", "content"); +/// let span = span!(style; "{} {}", "test", "content"); +/// let span = span!(style; "test {content}"); +/// let span = span!(style; "test {content}", content = "content"); +/// +/// // accepts any type that is convertible to Style +/// let span = span!(Style::new().green(); "test {content}"); +/// let span = span!(Color::Green; "test {content}"); +/// let span = span!(Modifier::BOLD; "test {content}"); +/// +/// // with format specifiers +/// let span = span!(style; "test {:4}", 123); +/// let span = span!(style; "test {:04}", 123); +/// ``` +/// +/// # Note +/// +/// The first parameter must be a formatting specifier followed by a comma OR anything that can be +/// converted into a [`Style`] followed by a semicolon. +/// +/// For example, the following will fail to compile: +/// +/// ```compile_fail +/// # use ratatui::prelude::*; +/// # use ratatui_macros::span; +/// let span = span!(Modifier::BOLD, "hello world"); +/// ``` +/// +/// But this will work: +/// +/// ```rust +/// # use ratatui_core::style::{Modifier}; +/// # use ratatui_macros::span; +/// let span = span!(Modifier::BOLD; "hello world"); +/// ``` +/// +/// The following will fail to compile: +/// +/// ```compile_fail +/// # use ratatui::prelude::*; +/// # use ratatui_macros::span; +/// let span = span!("hello", "world"); +/// ``` +/// +/// But this will work: +/// +/// ```rust +/// # use ratatui_macros::span; +/// let span = span!("hello {}", "world"); +/// ``` +/// +/// [`Color`]: crate::style::Color +/// [`Style`]: crate::style::Style +/// [`Span`]: crate::text::Span +/// [`Style`]: crate::style::Style +#[macro_export] +macro_rules! span { + ($string:literal) => { + $crate::ratatui_core::text::Span::raw(format!($string)) + }; + ($string:literal, $($arg:tt)*) => { + $crate::ratatui_core::text::Span::raw(format!($string, $($arg)*)) + }; + ($expr:expr) => { + $crate::ratatui_core::text::Span::raw(format!("{}", $expr)) + }; + ($style:expr, $($arg:tt)*) => { + compile_error!("first parameter must be a formatting specifier followed by a comma OR a `Style` followed by a semicolon") + }; + ($style:expr; $string:literal) => { + $crate::ratatui_core::text::Span::styled(format!($string), $style) + }; + ($style:expr; $string:literal, $($arg:tt)*) => { + $crate::ratatui_core::text::Span::styled(format!($string, $($arg)*), $style) + }; + ($style:expr; $expr:expr) => { + $crate::ratatui_core::text::Span::styled(format!("{}", $expr), $style) + }; +} + +#[cfg(test)] +mod tests { + use ratatui_core::{ + style::{Color, Modifier, Style, Stylize}, + text::Span, + }; + + #[test] + fn raw() { + let test = "test"; + let content = "content"; + let number = 123; + + // literal + let span = span!("test content"); + assert_eq!(span, Span::raw("test content")); + + // string + let span = span!("test {}", "content"); + assert_eq!(span, Span::raw("test content")); + + // string variable + let span = span!("test {}", content); + assert_eq!(span, Span::raw("test content")); + + // string variable in the format string + let span = span!("test {content}"); + assert_eq!(span, Span::raw("test content")); + + // named variable + let span = span!("test {content}", content = "content"); + assert_eq!(span, Span::raw("test content")); + + // named variable pointing at a local variable + let span = span!("test {content}", content = content); + assert_eq!(span, Span::raw("test content")); + + // two strings + let span = span!("{} {}", "test", "content"); + assert_eq!(span, Span::raw("test content")); + + // two string variables + let span = span!("{test} {content}"); + assert_eq!(span, Span::raw("test content")); + + // a number + let span = span!("test {number}"); + assert_eq!(span, Span::raw("test 123")); + + // a number with a format specifier + let span = span!("test {number:04}"); + assert_eq!(span, Span::raw("test 0123")); + + // directly pass a number expression + let span = span!(number); + assert_eq!(span, Span::raw("123")); + + // directly pass a string expression + let span = span!(test); + assert_eq!(span, Span::raw("test")); + } + + #[test] + fn styled() { + const STYLE: Style = Style::new().fg(Color::Green); + + let test = "test"; + let content = "content"; + let number = 123; + + // literal + let span = span!(STYLE; "test content"); + assert_eq!(span, Span::styled("test content", STYLE)); + + // string + let span = span!(STYLE; "test {}", "content"); + assert_eq!(span, Span::styled("test content", STYLE)); + + // string variable + let span = span!(STYLE; "test {}", content); + assert_eq!(span, Span::styled("test content", STYLE)); + + // string variable in the format string + let span = span!(STYLE; "test {content}"); + assert_eq!(span, Span::styled("test content", STYLE)); + + // named variable + let span = span!(STYLE; "test {content}", content = "content"); + assert_eq!(span, Span::styled("test content", STYLE)); + + // named variable pointing at a local variable + let span = span!(STYLE; "test {content}", content = content); + assert_eq!(span, Span::styled("test content", STYLE)); + + // two strings + let span = span!(STYLE; "{} {}", "test", "content"); + assert_eq!(span, Span::styled("test content", STYLE)); + + // two string variables + let span = span!(STYLE; "{test} {content}"); + assert_eq!(span, Span::styled("test content", STYLE)); + + // a number + let span = span!(STYLE; "test {number}"); + assert_eq!(span, Span::styled("test 123", STYLE)); + + // a number with a format specifier + let span = span!(STYLE; "test {number:04}"); + assert_eq!(span, Span::styled("test 0123", STYLE)); + + // accepts any type that is convertible to Style + let span = span!(Color::Green; "test {content}"); + assert_eq!(span, Span::styled("test content", STYLE)); + + let span = span!(Modifier::BOLD; "test {content}"); + assert_eq!(span, Span::styled("test content", Style::new().bold())); + + // directly pass a number expression + let span = span!(STYLE; number); + assert_eq!(span, Span::styled("123", STYLE)); + + // directly pass a string expression + let span = span!(STYLE; test); + assert_eq!(span, Span::styled("test", STYLE)); + } +} diff --git a/ratatui-macros/src/text.rs b/ratatui-macros/src/text.rs new file mode 100644 index 00000000..b206ecbd --- /dev/null +++ b/ratatui-macros/src/text.rs @@ -0,0 +1,71 @@ +/// A macro for creating a [`Text`] using vec! syntax. +/// +/// `text!` is similar to the [`vec!`] macro, but it returns a [`Text`] instead of a `Vec`. +/// +/// # Examples +/// +/// * Create a [`Text`] containing a vector of [`Line`]s: +/// +/// ```rust +/// # use ratatui_core::style::Stylize; +/// use ratatui_macros::text; +/// +/// let text = text!["hello", "world"]; +/// let text = text!["hello".red(), "world".red().bold()]; +/// ``` +/// +/// * Create a [`text`] from a given [`Line`] repeated some amount of times: +/// +/// ```rust +/// # use ratatui_macros::text; +/// let text = text!["hello"; 2]; +/// ``` +/// +/// * Use [`line!`] or [`span!`] macro inside [`text!`] macro. +/// +/// ```rust +/// # use ratatui_core::style::{Modifier}; +/// use ratatui_macros::{line, text, span}; +/// +/// let text = text![line!["hello", "world"], span!(Modifier::BOLD; "goodbye {}", "world")]; +/// ``` +/// +/// [`Text`]: crate::text::Text +/// [`Line`]: crate::text::Line +/// [`Span`]: crate::text::Span +#[macro_export] +macro_rules! text { + () => { + ratatui_core::text::Text::default() + }; + ($line:expr; $n:expr) => { + ratatui_core::text::Text::from(vec![$line.into(); $n]) + }; + ($($line:expr),+ $(,)?) => {{ + ratatui_core::text::Text::from(vec![ + $( + $line.into(), + )+ + ]) + }}; +} + +#[cfg(test)] +mod tests { + use ratatui_core::text::Text; + + #[test] + fn text() { + // literal + let text = text!["hello", "world"]; + assert_eq!(text, Text::from(vec!["hello".into(), "world".into()])); + + // explicit use of span and line + let text = text![crate::line!("hello"), crate::span!["world"]]; + assert_eq!(text, Text::from(vec!["hello".into(), "world".into()])); + + // vec count syntax + let text = text!["hello"; 2]; + assert_eq!(text, Text::from(vec!["hello".into(), "hello".into()])); + } +} diff --git a/ratatui-macros/tests/macros.rs b/ratatui-macros/tests/macros.rs new file mode 100644 index 00000000..814242b6 --- /dev/null +++ b/ratatui-macros/tests/macros.rs @@ -0,0 +1,104 @@ +use ratatui_core::layout::{Constraint, Rect}; +use ratatui_macros::{constraints, horizontal, vertical}; + +#[test] +fn layout_constraints_macro() { + let rect = Rect { + x: 0, + y: 0, + width: 10, + height: 10, + }; + + let [rect1, rect2] = vertical![==7, <=3].split(rect).to_vec().try_into().unwrap(); + assert_eq!(rect1, Rect::new(0, 0, 10, 7)); + assert_eq!(rect2, Rect::new(0, 7, 10, 3)); + + let [rect1, rect2] = horizontal![==7, <=3] + .split(rect) + .to_vec() + .try_into() + .unwrap(); + assert_eq!(rect1, Rect::new(0, 0, 7, 10)); + assert_eq!(rect2, Rect::new(7, 0, 3, 10)); + + let one = 1; + let two = 2; + let ten = 10; + let zero = 0; + let [a, b, c, d, e, f] = horizontal![==one, >=one, <=one, == 1 / two, == ten %, >=zero] + .split(rect) + .to_vec() + .try_into() + .unwrap(); + + assert_eq!(a, Rect::new(0, 0, 1, 10)); + assert_eq!(b, Rect::new(1, 0, 1, 10)); + assert_eq!(c, Rect::new(2, 0, 1, 10)); + assert_eq!(d, Rect::new(3, 0, 5, 10)); + assert_eq!(e, Rect::new(8, 0, 1, 10)); + assert_eq!(f, Rect::new(9, 0, 1, 10)); + + let one = 1; + let two = 2; + let ten = 10; + let zero = 0; + let [a, b, c, d, e, f] = horizontal![ + == one*one, // expr allowed here + >= one+zero, // expr allowed here + <= one-zero, // expr allowed here + == 1/two, // only single token allowed in numerator and denominator + == ten%, // only single token allowed before % + >= zero // no trailing comma + ] + .split(rect) + .to_vec() + .try_into() + .unwrap(); + + assert_eq!(a, Rect::new(0, 0, 1, 10)); + assert_eq!(b, Rect::new(1, 0, 1, 10)); + assert_eq!(c, Rect::new(2, 0, 1, 10)); + assert_eq!(d, Rect::new(3, 0, 5, 10)); + assert_eq!(e, Rect::new(8, 0, 1, 10)); + assert_eq!(f, Rect::new(9, 0, 1, 10)); + + let [a, b, c, d, e] = constraints![ >=0, ==1, <=5, ==10%, ==1/2 ]; + assert_eq!(a, Constraint::Min(0)); + assert_eq!(b, Constraint::Length(1)); + assert_eq!(c, Constraint::Max(5)); + assert_eq!(d, Constraint::Percentage(10)); + assert_eq!(e, Constraint::Ratio(1, 2)); + + let [a, b, c, d, e] = constraints![ >=0; 5 ]; + assert_eq!(a, Constraint::Min(0)); + assert_eq!(b, Constraint::Min(0)); + assert_eq!(c, Constraint::Min(0)); + assert_eq!(d, Constraint::Min(0)); + assert_eq!(e, Constraint::Min(0)); + + let [a, b, c, d, e] = constraints![ <=0; 5 ]; + assert_eq!(a, Constraint::Max(0)); + assert_eq!(b, Constraint::Max(0)); + assert_eq!(c, Constraint::Max(0)); + assert_eq!(d, Constraint::Max(0)); + assert_eq!(e, Constraint::Max(0)); + + let [a, b] = constraints![ ==0; 2 ]; + assert_eq!(a, Constraint::Length(0)); + assert_eq!(b, Constraint::Length(0)); + + let [a, b] = constraints![ == 50%; 2 ]; + assert_eq!(a, Constraint::Percentage(50)); + assert_eq!(b, Constraint::Percentage(50)); + + let [a, b] = constraints![ == 1/2; 2 ]; + assert_eq!(a, Constraint::Ratio(1, 2)); + assert_eq!(b, Constraint::Ratio(1, 2)); +} + +#[test] +fn fails() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/fails.rs"); +} diff --git a/ratatui-macros/tests/ui/fails.rs b/ratatui-macros/tests/ui/fails.rs new file mode 100644 index 00000000..987e431e --- /dev/null +++ b/ratatui-macros/tests/ui/fails.rs @@ -0,0 +1,20 @@ +use ratatui_core::layout::Constraint; +use ratatui_macros::{constraints, span}; + +fn main() { + constraints![,]; + + // TODO: Make this compiler error pass + let [a, b] = constraints![ + == 1/2, + == 2, + ]; + assert_eq!(a, Constraint::Ratio(1, 2)); + assert_eq!(b, Constraint::Length(2)); + + let [a, b, c] = constraints![ == 1, == 10%, == 2; 4]; + + let _ = span!(Modifier::BOLD, "hello world"); + + let _ = span!("hello", "hello world"); +} diff --git a/ratatui-macros/tests/ui/fails.stderr b/ratatui-macros/tests/ui/fails.stderr new file mode 100644 index 00000000..cd7a6985 --- /dev/null +++ b/ratatui-macros/tests/ui/fails.stderr @@ -0,0 +1,58 @@ +error: No rules expected the token `,` while trying to match the end of the macro + --> tests/ui/fails.rs:5:5 + | +5 | constraints![,]; + | ^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `$crate::constraints` which comes from the expansion of the macro `constraints` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: unexpected end of macro invocation + --> tests/ui/fails.rs:8:18 + | +8 | let [a, b] = constraints![ + | __________________^ +9 | | == 1/2, +10 | | == 2, +11 | | ]; + | |_____^ missing tokens in macro arguments + | +note: while trying to match `==` + --> src/layout.rs + | + | (== $token:tt %) => { + | ^^ + = note: this error originates in the macro `$crate::constraints` which comes from the expansion of the macro `constraints` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: no rules expected `;` + --> tests/ui/fails.rs:15:53 + | +15 | let [a, b, c] = constraints![ == 1, == 10%, == 2; 4]; + | ^ no rules expected this token in macro call + | +note: while trying to match `%` + --> src/layout.rs + | + | (== $token:tt %) => { + | ^ + +error: first parameter must be a formatting specifier followed by a comma OR a `Style` followed by a semicolon + --> tests/ui/fails.rs:17:13 + | +17 | let _ = span!(Modifier::BOLD, "hello world"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `span` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: argument never used + --> tests/ui/fails.rs:19:28 + | +19 | let _ = span!("hello", "hello world"); + | ------- ^^^^^^^^^^^^^ argument never used + | | + | formatting specifier missing + +error[E0527]: pattern requires 2 elements but array has 3 + --> tests/ui/fails.rs:8:9 + | +8 | let [a, b] = constraints![ + | ^^^^^^ expected 3 elements diff --git a/ratatui/Cargo.toml b/ratatui/Cargo.toml index a6c49ab7..79986dae 100644 --- a/ratatui/Cargo.toml +++ b/ratatui/Cargo.toml @@ -25,8 +25,9 @@ rustdoc-args = ["--cfg", "docsrs"] #! ## By default, we enable the crossterm backend as this is a reasonable choice for most applications ## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature -## which allows you to set the underline color of text. -default = ["crossterm", "underline-color", "all-widgets"] +## which allows you to set the underline color of text and the `macros` feature which provides +## some useful macros. +default = ["crossterm", "underline-color", "all-widgets", "macros"] #! Generally an application will only use one backend, so you should only enable one of the following features: ## enables the [`CrosstermBackend`](backend::CrosstermBackend) backend and adds a dependency on [`crossterm`]. crossterm = ["dep:ratatui-crossterm"] @@ -52,6 +53,10 @@ scrolling-regions = [ "ratatui-termwiz?/scrolling-regions", ] +## enables the [`macros`](macros) module which provides some useful macros for creating spans, +## lines, text, and layouts +macros = ["dep:ratatui-macros"] + ## enables all widgets. all-widgets = ["widget-calendar"] @@ -102,6 +107,7 @@ itertools.workspace = true palette = { version = "0.7.6", optional = true } ratatui-core = { workspace = true } ratatui-crossterm = { workspace = true, optional = true } +ratatui-macros = { workspace = true, optional = true } ratatui-termwiz = { workspace = true, optional = true } ratatui-widgets = { workspace = true } serde = { workspace = true, optional = true } diff --git a/ratatui/src/lib.rs b/ratatui/src/lib.rs index abeac52b..61fa89ce 100644 --- a/ratatui/src/lib.rs +++ b/ratatui/src/lib.rs @@ -335,6 +335,8 @@ pub use ratatui_core::{ /// re-export the `crossterm` crate so that users don't have to add it as a dependency #[cfg(feature = "crossterm")] pub use ratatui_crossterm::crossterm; +#[cfg(feature = "macros")] +pub use ratatui_macros as macros; /// re-export the `termion` crate so that users don't have to add it as a dependency #[cfg(all(not(windows), feature = "termion"))] pub use ratatui_termion::termion;