diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76a39a08..6f5d988b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: DavidAnson/markdownlint-cli2-action@v17 + - uses: DavidAnson/markdownlint-cli2-action@v18 with: globs: | '**/*.md' @@ -94,7 +94,7 @@ jobs: - uses: taiki-e/install-action@cargo-llvm-cov - uses: Swatinem/rust-cache@v2 - run: cargo xtask coverage - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/Cargo.lock b/Cargo.lock index 37903824..ed18cb18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -367,8 +367,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets", ] @@ -401,9 +403,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", "clap_derive", @@ -422,9 +424,9 @@ dependencies = [ [[package]] name = "clap-verbosity-flag" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e099138e1807662ff75e2cebe4ae2287add879245574489f9b1588eb5e5564ed" +checksum = "34c77f67047557f62582784fd7482884697731b2932c7d37ced54bce2312e1e2" dependencies = [ "clap", "log", @@ -432,9 +434,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -664,6 +666,41 @@ dependencies = [ "phf", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.85", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.85", +] + [[package]] name = "deltae" version = "0.3.2" @@ -1175,6 +1212,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1209,10 +1252,14 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "instability" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e" dependencies = [ + "darling", + "indoc", + "pretty_assertions", + "proc-macro2", "quote", "syn 2.0.85", ] @@ -1561,9 +1608,9 @@ dependencies = [ [[package]] name = "octocrab" -version = "0.41.2" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2dfd11f6efbd39491d71a3864496f0b6f45e2d01b73b26c55d631c4e0dafaef" +checksum = "5235d5839910001bef2c3df99a88688c7c781e5b1fd5fe40c5d8fa8bd786ac5a" dependencies = [ "arc-swap", "async-trait", @@ -1596,6 +1643,7 @@ dependencies = [ "tower-http", "tracing", "url", + "web-time", ] [[package]] @@ -1909,9 +1957,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -2492,18 +2540,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -3357,6 +3405,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "wezterm-bidi" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 2db23a6b..e846b7ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,14 +27,14 @@ rust-version = "1.74.0" bitflags = "2.6.0" document-features = "0.2.7" indoc = "2.0.5" -instability = "0.3.1" +instability = "0.3.3" itertools = "0.13.0" pretty_assertions = "1.4.1" ratatui = { path = "ratatui" } ratatui-core = { path = "ratatui-core" } ratatui-widgets = { path = "ratatui-widgets" } rstest = "0.23.0" -serde = { version = "1.0.214", features = ["derive"] } +serde = { version = "1.0.215", features = ["derive"] } strum = { version = "0.26.3", features = ["derive"] } unicode-segmentation = "1.12.0" # See for information about why we pin unicode-width diff --git a/bacon.toml b/bacon.toml index 72fdc9fe..0de68f5f 100644 --- a/bacon.toml +++ b/bacon.toml @@ -8,26 +8,17 @@ default_job = "check" [jobs.check] -command = ["cargo", "check", "--all-features", "--color", "always"] +command = ["cargo", "check", "--all-features"] need_stdout = false [jobs.check-all] -command = [ - "cargo", - "check", - "--all-targets", - "--all-features", - "--color", - "always", -] +command = ["cargo", "check", "--all-targets", "--all-features"] need_stdout = false [jobs.check-crossterm] command = [ "cargo", "check", - "--color", - "always", "--all-targets", "--no-default-features", "--features", @@ -39,8 +30,6 @@ need_stdout = false command = [ "cargo", "check", - "--color", - "always", "--all-targets", "--no-default-features", "--features", @@ -52,8 +41,6 @@ need_stdout = false command = [ "cargo", "check", - "--color", - "always", "--all-targets", "--no-default-features", "--features", @@ -62,34 +49,15 @@ command = [ need_stdout = false [jobs.clippy] -command = ["cargo", "clippy", "--all-targets", "--color", "always"] +command = ["cargo", "clippy", "--all-targets"] need_stdout = false [jobs.test] -command = [ - "cargo", - "test", - "--all-features", - "--color", - "always", - "--", - "--color", - "always", # see https://github.com/Canop/bacon/issues/124 -] +command = ["cargo", "test", "--all-features"] need_stdout = true [jobs.test-unit] -command = [ - "cargo", - "test", - "--lib", - "--all-features", - "--color", - "always", - "--", - "--color", - "always", # see https://github.com/Canop/bacon/issues/124 -] +command = ["cargo", "test", "--lib", "--all-features"] need_stdout = true [jobs.doc] @@ -100,8 +68,6 @@ command = [ "-Zunstable-options", "-Zrustdoc-scrape-examples", "--all-features", - "--color", - "always", "--no-deps", ] env.RUSTDOCFLAGS = "--cfg docsrs" @@ -117,8 +83,6 @@ command = [ "-Zunstable-options", "-Zrustdoc-scrape-examples", "--all-features", - "--color", - "always", "--no-deps", "--open", ] @@ -134,8 +98,6 @@ command = [ "--output-path", "target/lcov.info", "--all-features", - "--color", - "always", ] [jobs.coverage-unit-tests-only] @@ -147,8 +109,6 @@ command = [ "target/lcov.info", "--lib", "--all-features", - "--color", - "always", ] [jobs.hack] @@ -160,8 +120,6 @@ command = [ "--each-feature", # "--all-targets", "--workspace", - "--color", - "always", ] # You may define here keybindings that would be specific to diff --git a/ratatui-core/src/lib.rs b/ratatui-core/src/lib.rs index fa1ae2cd..56c60d52 100644 --- a/ratatui-core/src/lib.rs +++ b/ratatui-core/src/lib.rs @@ -1,3 +1,10 @@ +// show the feature flags in the generated documentation +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png", + html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico" +)] //! **ratatui-core** is the core library of the [ratatui] project, //! providing the essential building blocks for creating rich terminal user interfaces in Rust. //! diff --git a/ratatui-core/src/widgets/stateful_widget.rs b/ratatui-core/src/widgets/stateful_widget.rs index b4b5df0c..aea2d6f2 100644 --- a/ratatui-core/src/widgets/stateful_widget.rs +++ b/ratatui-core/src/widgets/stateful_widget.rs @@ -121,7 +121,7 @@ pub trait StatefulWidget { /// If you don't need this then you probably want to implement [`Widget`] instead. /// /// [`Widget`]: super::Widget - type State; + type State: ?Sized; /// 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); @@ -159,4 +159,23 @@ mod tests { widget.render(buf.area, &mut buf, &mut state); assert_eq!(buf, Buffer::with_lines(["Hello world "])); } + + struct Bytes; + + /// A widget with an unsized state type. + impl StatefulWidget for Bytes { + type State = [u8]; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let slice = std::str::from_utf8(state).unwrap(); + Line::from(format!("Bytes: {slice}")).render(area, buf); + } + } + + #[rstest] + fn render_unsized_state_type(mut buf: Buffer) { + let widget = Bytes; + let state = b"hello"; + widget.render(buf.area, &mut buf, &mut state.clone()); + assert_eq!(buf, Buffer::with_lines(["Bytes: hello "])); + } } diff --git a/ratatui-widgets/src/lib.rs b/ratatui-widgets/src/lib.rs index 90933d93..68b50f73 100644 --- a/ratatui-widgets/src/lib.rs +++ b/ratatui-widgets/src/lib.rs @@ -1,3 +1,10 @@ +// show the feature flags in the generated documentation +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png", + html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico" +)] #![warn(missing_docs)] //! **ratatui-widgets** contains all the widgets that were previously part of the [Ratatui] crate. //! It is meant to be used in conjunction with `ratatui`, which provides the core functionality diff --git a/ratatui/Cargo.toml b/ratatui/Cargo.toml index ee3b1d79..a31ee49d 100644 --- a/ratatui/Cargo.toml +++ b/ratatui/Cargo.toml @@ -43,7 +43,7 @@ fakeit = "1.1" font8x8 = "0.3.1" futures = "0.3.30" indoc = "2" -octocrab = "0.41.0" +octocrab = "0.42.0" pretty_assertions = "1.4.0" rand = "0.8.5" rand_chacha = "0.3.1" diff --git a/ratatui/src/lib.rs b/ratatui/src/lib.rs index f426bcd9..b20d36ca 100644 --- a/ratatui/src/lib.rs +++ b/ratatui/src/lib.rs @@ -1,3 +1,11 @@ +// show the feature flags in the generated documentation +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png", + html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico" +)] +#![warn(missing_docs)] //! ![Demo](https://github.com/ratatui/ratatui/blob/87ae72dbc756067c97f6400d3e2a58eeb383776e/examples/demo2-destroy.gif?raw=true) //! //!
@@ -318,13 +326,6 @@ //! [Forum]: https://forum.ratatui.rs //! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3 -// show the feature flags in the generated documentation -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![doc( - html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png", - html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico" -)] - /// re-export the `crossterm` crate so that users don't have to add it as a dependency #[cfg(feature = "crossterm")] pub use crossterm; diff --git a/ratatui/src/widgets/stateful_widget_ref.rs b/ratatui/src/widgets/stateful_widget_ref.rs index 35f4337a..2080179c 100644 --- a/ratatui/src/widgets/stateful_widget_ref.rs +++ b/ratatui/src/widgets/stateful_widget_ref.rs @@ -61,7 +61,7 @@ pub trait StatefulWidgetRef { /// If you don't need this then you probably want to implement [`WidgetRef`] instead. /// /// [`WidgetRef`]: super::WidgetRef - type State; + type State: ?Sized; /// 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); @@ -70,7 +70,7 @@ pub trait StatefulWidgetRef { /// Blanket implementation of `StatefulWidgetRef` for `&W` where `W` implements `StatefulWidget`. /// /// This allows you to render a stateful widget by reference. -impl StatefulWidgetRef for &W +impl StatefulWidgetRef for &W where for<'a> &'a W: StatefulWidget, { @@ -119,4 +119,38 @@ mod tests { widget.render_ref(buf.area, &mut buf, &mut state); assert_eq!(buf, Buffer::with_lines(["Hello world "])); } + + #[rstest] + fn render_stateful_widget_ref_with_unsized_state(mut buf: Buffer) { + struct Bytes; + + impl StatefulWidgetRef for Bytes { + type State = [u8]; + fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let slice = std::str::from_utf8(state).unwrap(); + Line::from(format!("Bytes: {slice}")).render(area, buf); + } + } + let widget = Bytes; + let state = b"hello"; + widget.render_ref(buf.area, &mut buf, &mut state.clone()); + assert_eq!(buf, Buffer::with_lines(["Bytes: hello "])); + } + + #[rstest] + fn render_stateful_widget_with_unsized_state(mut buf: Buffer) { + struct Bytes; + impl StatefulWidget for &Bytes { + type State = [u8]; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let slice = std::str::from_utf8(state).unwrap(); + Line::from(format!("Bytes: {slice}")).render(area, buf); + } + } + let widget = &Bytes; + let mut state = b"hello".to_owned(); + let state = state.as_mut_slice(); + widget.render_ref(buf.area, &mut buf, state); + assert_eq!(buf, Buffer::with_lines(["Bytes: hello "])); + } } diff --git a/ratatui/tests/stateful_widget_ref_dyn.rs b/ratatui/tests/stateful_widget_ref_dyn.rs new file mode 100644 index 00000000..8f307954 --- /dev/null +++ b/ratatui/tests/stateful_widget_ref_dyn.rs @@ -0,0 +1,91 @@ +#![cfg(feature = "unstable-widget-ref")] + +use std::{ + any::{type_name, Any}, + cell::RefCell, +}; + +use pretty_assertions::assert_eq; +use ratatui::widgets::StatefulWidgetRef; +use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget}; + +trait AnyWindow: StatefulWidgetRef { + fn title(&self) -> &str { + type_name::() + } +} + +struct Window1; + +struct Window1State { + pub value: u32, +} + +impl AnyWindow for Window1 {} + +impl StatefulWidgetRef for Window1 { + type State = dyn Any; + + fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let state = state.downcast_mut::().expect("window1 state"); + Line::from(format!("{}, u32: {}", self.title(), state.value)).render(area, buf); + } +} + +struct Window2; + +struct Window2State { + pub value: String, +} + +impl AnyWindow for Window2 {} + +impl StatefulWidgetRef for Window2 { + type State = dyn Any; + + fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let state = state.downcast_mut::().expect("window2 state"); + Line::from(format!("{}, String: {}", self.title(), state.value)).render(area, buf); + } +} + +type BoxedWindow = Box; +type BoxedState = Box>; + +#[test] +fn render_dyn_widgets() { + let windows: Vec<(BoxedWindow, BoxedState)> = vec![ + ( + Box::new(Window1), + Box::new(RefCell::new(Window1State { value: 32 })), + ), + ( + Box::new(Window2), + Box::new(RefCell::new(Window2State { + value: "Some".to_string(), + })), + ), + ( + Box::new(Window1), + Box::new(RefCell::new(Window1State { value: 42 })), + ), + ]; + + let mut buf = Buffer::empty(Rect::new(0, 0, 50, 3)); + + let mut area = Rect::new(0, 0, 50, 1); + for (w, s) in &windows { + let mut s = s.borrow_mut(); + w.render_ref(area, &mut buf, &mut *s); + area.y += 1; + } + + assert_eq!( + buf, + Buffer::with_lines([ + "stateful_widget_ref_dyn::Window1, u32: 32 ", + "stateful_widget_ref_dyn::Window2, String: Some ", + "stateful_widget_ref_dyn::Window1, u32: 42 ", + ]) + ); +} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 3b8210f6..4d4d0cf5 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -6,9 +6,9 @@ license.workspace = true [dependencies] cargo_metadata = "0.18.1" -clap = { version = "4.5.20", features = ["derive"] } +clap = { version = "4.5.21", features = ["derive"] } clap-cargo = { version = "0.14.1", features = ["cargo_metadata"] } -clap-verbosity-flag = "2.2.2" +clap-verbosity-flag = "2.2.3" color-eyre = "0.6.3" duct = "0.13.7" tracing = "0.1.40"