diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4342e7d06..9001a7c71 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -57,6 +57,10 @@ jobs: cargo install mdbook-variables - name: Regenerate Example Images + run: | + CAPTURE=1 cargo test -p cushy --examples + + - name: Regenerate Guide Example Images run: | CAPTURE=1 cargo test -p guide-examples --examples diff --git a/examples/theme.rs b/examples/theme.rs index 72891fed0..6d3833299 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -1,9 +1,9 @@ use std::fmt::Write; -use cushy::styles::components::{TextColor, WidgetBackground}; +use cushy::styles::components::{TextColor, TextSize, WidgetBackground}; use cushy::styles::{ - ColorScheme, ColorSchemeBuilder, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, - ThemePair, + ColorScheme, ColorSchemeBuilder, ColorSource, ColorTheme, Dimension, FixedTheme, SurfaceTheme, + Theme, ThemePair, }; use cushy::value::{Destination, Dynamic, MapEachCloned, Source}; use cushy::widget::MakeWidget; @@ -13,14 +13,17 @@ use cushy::widgets::input::InputValue; use cushy::widgets::slider::Slidable; use cushy::widgets::Space; use cushy::window::ThemeMode; -use cushy::{Open, PendingApp}; +use cushy::{Cushy, Open, PendingApp}; use figures::units::Lp; use kludgine::Color; use palette::OklabHue; fn main() -> cushy::Result { let app = PendingApp::default(); + theme_editor(app.cushy().clone()).into_window().run_in(app) +} +fn theme_editor(cushy: Cushy) -> impl MakeWidget { let (theme_mode, theme_switcher) = dark_mode_picker(); let scheme = Scheme::from(ColorScheme::default()); @@ -79,7 +82,6 @@ fn main() -> cushy::Result { .and(editors.neutral.1) .and(editors.neutral_variant.1) .and("Copy to Clipboard".into_button().on_click({ - let cushy = app.cushy().clone(); move |_| { if let Some(mut clipboard) = cushy.clipboard_guard() { let builder = color_scheme_builder.get(); @@ -115,9 +117,7 @@ fn main() -> cushy::Result { .themed(theme) .pad() .expand() - .into_window() .themed_mode(theme_mode) - .run_in(app) } struct Scheme { @@ -436,6 +436,7 @@ fn color_theme(theme: Dynamic, label: &str) -> impl MakeWidget { fn swatch(background: Dynamic, label: &str, text: Dynamic) -> impl MakeWidget { label .with(&TextColor, text) + .with(&TextSize, Dimension::Lp(Lp::points(8))) .with(&WidgetBackground, background) .fit_horizontally() .fit_vertically() @@ -492,3 +493,9 @@ impl FormatRust for ColorSchemeBuilder { } } } + +#[test] +fn runs() { + let theme_editor = || theme_editor(Cushy::default()); + cushy::example!(theme_editor, 1600, 900).untested_still_frame(); +} diff --git a/guide/guide-examples/examples/align.rs b/guide/guide-examples/examples/align.rs index 09a42a304..4665fa819 100644 --- a/guide/guide-examples/examples/align.rs +++ b/guide/guide-examples/examples/align.rs @@ -6,7 +6,6 @@ use cushy::styles::ThemePair; use cushy::widget::MakeWidget; use cushy::widgets::grid::{GridDimension, GridWidgets}; use cushy::widgets::{Grid, Space}; -use guide_examples::book_example; // ANCHOR: content fn content() -> impl MakeWidget { @@ -84,7 +83,7 @@ fn main() { let theme = ThemePair::default(); let container_color = theme.dark.surface.low_container; let primary = theme.dark.primary.color; - book_example!(align).still_frame(|recorder| { + cushy::example!(align).still_frame(|recorder| { const LEFT: u32 = 145; const RIGHT: u32 = 705; const H_CENTER: u32 = (RIGHT + LEFT) / 2; diff --git a/guide/guide-examples/examples/composition-makewidget.rs b/guide/guide-examples/examples/composition-makewidget.rs index a3e06ab60..d9938c5ab 100644 --- a/guide/guide-examples/examples/composition-makewidget.rs +++ b/guide/guide-examples/examples/composition-makewidget.rs @@ -41,7 +41,7 @@ fn composition_makewidget() -> impl cushy::widget::MakeWidget { } fn main() { - guide_examples::book_example!(composition_makewidget).untested_still_frame(); + cushy::example!(composition_makewidget).untested_still_frame(); } #[test] diff --git a/guide/guide-examples/examples/composition-widget.rs b/guide/guide-examples/examples/composition-widget.rs index f83e4d80c..80fcb7363 100644 --- a/guide/guide-examples/examples/composition-widget.rs +++ b/guide/guide-examples/examples/composition-widget.rs @@ -110,7 +110,7 @@ fn composition_widget() -> impl cushy::widget::MakeWidget { } fn main() { - guide_examples::book_example!(composition_widget).untested_still_frame(); + cushy::example!(composition_widget).untested_still_frame(); } #[test] diff --git a/guide/guide-examples/examples/composition-wrapperwidget.rs b/guide/guide-examples/examples/composition-wrapperwidget.rs index 7c515d603..52d2b6962 100644 --- a/guide/guide-examples/examples/composition-wrapperwidget.rs +++ b/guide/guide-examples/examples/composition-wrapperwidget.rs @@ -117,7 +117,7 @@ fn composition_wrapperwidget() -> impl cushy::widget::MakeWidget { } fn main() { - guide_examples::book_example!(composition_wrapperwidget).untested_still_frame(); + cushy::example!(composition_wrapperwidget).untested_still_frame(); } #[test] diff --git a/guide/guide-examples/examples/hello-world.rs b/guide/guide-examples/examples/hello-world.rs index e9744d565..9da39482a 100644 --- a/guide/guide-examples/examples/hello-world.rs +++ b/guide/guide-examples/examples/hello-world.rs @@ -12,5 +12,5 @@ fn book() { "Hello, World!" } - guide_examples::book_example!(hello_world).untested_still_frame(); + cushy::example!(hello_world).untested_still_frame(); } diff --git a/guide/guide-examples/examples/intro.rs b/guide/guide-examples/examples/intro.rs index 55974d5e7..5b5afa237 100644 --- a/guide/guide-examples/examples/intro.rs +++ b/guide/guide-examples/examples/intro.rs @@ -39,7 +39,7 @@ fn book() { name_input.and(greeting).into_rows() } - guide_examples::book_example!(intro).animated(|animation| { + cushy::example!(intro).animated(|animation| { animation.wait_for(Duration::from_secs(1)).unwrap(); animation .animate_text_input("Ferris 🦀", Duration::from_secs(1)) diff --git a/guide/guide-examples/examples/thread-progress.rs b/guide/guide-examples/examples/thread-progress.rs index ad27315c4..ea9d285f1 100644 --- a/guide/guide-examples/examples/thread-progress.rs +++ b/guide/guide-examples/examples/thread-progress.rs @@ -3,7 +3,6 @@ use std::time::Duration; use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::widgets::progress::Progressable; -use guide_examples::book_example; fn thread_progress() -> impl MakeWidget { // ANCHOR: example @@ -23,7 +22,7 @@ fn thread_progress() -> impl MakeWidget { } fn main() { - book_example!(thread_progress).animated(|recorder| { + cushy::example!(thread_progress).animated(|recorder| { recorder.wait_for(Duration::from_secs(2)).unwrap(); }); } diff --git a/guide/guide-examples/src/lib.rs b/guide/guide-examples/src/lib.rs index af132293f..8b1378917 100644 --- a/guide/guide-examples/src/lib.rs +++ b/guide/guide-examples/src/lib.rs @@ -1,141 +1 @@ -use std::panic::AssertUnwindSafe; -use std::path::PathBuf; -use cushy::figures::units::Px; -use cushy::figures::Size; -use cushy::widget::MakeWidget; -use cushy::widgets::container::ContainerShadow; -use cushy::window::{AnimationRecorder, Rgba8, VirtualRecorder, VirtualRecorderBuilder}; - -pub struct BookExampleBuilder { - name: &'static str, - recorder: VirtualRecorderBuilder, -} - -impl BookExampleBuilder { - pub fn finish(self) -> BookExample { - BookExample { - name: self.name, - recorder: self.recorder.finish().expect("error creating recorder"), - } - } - - pub fn untested_still_frame(self) { - self.finish().untested_still_frame() - } - - pub fn prepare_with(self, prepare: Prepare) -> BookExample - where - Prepare: FnOnce(&mut VirtualRecorder), - { - self.finish().prepare_with(prepare) - } - - pub fn still_frame(self, test: Test) - where - Test: FnOnce(&mut VirtualRecorder), - { - self.finish().still_frame(test); - } - - pub fn animated(self, test: Test) - where - Test: FnOnce(&mut AnimationRecorder<'_, Rgba8>), - { - self.finish().animated(test); - } -} - -fn target_dir() -> PathBuf { - let target_dir = std::env::current_dir() - .expect("missing current dir") - .parent() - .expect("missing guide folder") - .join("src") - .join("examples"); - assert!( - target_dir.is_dir(), - "current directory is not guide-examples" - ); - - target_dir -} - -pub struct BookExample { - name: &'static str, - recorder: VirtualRecorder, -} - -impl BookExample { - pub fn build(name: &'static str, interface: impl MakeWidget) -> BookExampleBuilder { - BookExampleBuilder { - name, - recorder: interface - .contain() - .shadow(ContainerShadow::drop(Px::new(16))) - .width(Px::new(750)) - .build_recorder() - .with_alpha() - .resize_to_fit() - .size(Size::new(750, 432)), - } - } - - pub fn untested_still_frame(self) { - self.still_frame(|_| {}); - } - - pub fn prepare_with(mut self, prepare: Prepare) -> Self - where - Prepare: FnOnce(&mut VirtualRecorder), - { - prepare(&mut self.recorder); - self - } - - pub fn still_frame(mut self, test: Test) - where - Test: FnOnce(&mut VirtualRecorder), - { - let capture = std::env::var("CAPTURE").is_ok(); - let errored = - std::panic::catch_unwind(AssertUnwindSafe(|| test(&mut self.recorder))).is_err(); - if errored || capture { - let path = target_dir().join(format!("{}.png", self.name)); - self.recorder - .image() - .save(&path) - .expect("error saving file"); - println!("Wrote {}", path.display()); - - if errored { - std::process::exit(-1); - } - } - } - - pub fn animated(mut self, test: Test) - where - Test: FnOnce(&mut AnimationRecorder<'_, Rgba8>), - { - let mut animation = self.recorder.record_animated_png(60); - let capture = std::env::var("CAPTURE").is_ok(); - let errored = std::panic::catch_unwind(AssertUnwindSafe(|| test(&mut animation))).is_err(); - if errored || capture { - let path = target_dir().join(format!("{}.png", self.name)); - animation.write_to(&path).expect("error saving file"); - println!("Wrote {}", path.display()); - - if errored { - std::process::exit(-1); - } - } - } -} - -#[macro_export] -macro_rules! book_example { - ($name:ident) => { - guide_examples::BookExample::build(stringify!($name), $name()) - }; -} diff --git a/src/example.rs b/src/example.rs new file mode 100644 index 000000000..b4b91ea14 --- /dev/null +++ b/src/example.rs @@ -0,0 +1,164 @@ +use std::panic::AssertUnwindSafe; +use std::path::PathBuf; + +use cushy::figures::units::Px; +use cushy::figures::Size; +use cushy::widget::MakeWidget; +use cushy::widgets::container::ContainerShadow; +use cushy::window::{AnimationRecorder, Rgba8, VirtualRecorder, VirtualRecorderBuilder}; + +pub struct ExampleBuilder { + name: &'static str, + recorder: VirtualRecorderBuilder, +} + +impl ExampleBuilder { + #[must_use] + pub fn finish(self) -> Example { + Example { + name: self.name, + recorder: self.recorder.finish().expect("error creating recorder"), + } + } + + pub fn untested_still_frame(self) { + self.finish().untested_still_frame(); + } + + pub fn prepare_with(self, prepare: Prepare) -> Example + where + Prepare: FnOnce(&mut VirtualRecorder), + { + self.finish().prepare_with(prepare) + } + + pub fn still_frame(self, test: Test) + where + Test: FnOnce(&mut VirtualRecorder), + { + self.finish().still_frame(test); + } + + pub fn animated(self, test: Test) + where + Test: FnOnce(&mut AnimationRecorder<'_, Rgba8>), + { + self.finish().animated(test); + } +} + +fn target_dir() -> PathBuf { + let current_dir = std::env::current_dir().expect("missing current dir"); + let mut target_dir = current_dir.join("guide").join("src").join("examples"); + if !target_dir.is_dir() { + target_dir = current_dir + .parent() + .expect("missing guide folder") + .join("src") + .join("examples"); + } + assert!( + target_dir.is_dir(), + "current directory is not guide-examples or the root directory" + ); + + target_dir +} + +pub struct Example { + name: &'static str, + recorder: VirtualRecorder, +} + +impl Example { + pub fn build( + name: &'static str, + interface: impl MakeWidget, + width: u16, + height: Option, + ) -> ExampleBuilder { + let mut contents = interface + .contain() + .shadow(ContainerShadow::drop(Px::new(16))) + .width(Px::new(i32::from(width))); + if let Some(height) = height { + contents = contents.height(Px::new(i32::from(height))); + } + ExampleBuilder { + name, + recorder: contents + .build_recorder() + .with_alpha() + .resize_to_fit() + .size(Size::new( + u32::from(width), + u32::from(height.unwrap_or(432)), + )), + } + } + + pub fn untested_still_frame(self) { + self.still_frame(|_| {}); + } + + #[must_use] + pub fn prepare_with(mut self, prepare: Prepare) -> Self + where + Prepare: FnOnce(&mut VirtualRecorder), + { + prepare(&mut self.recorder); + self + } + + pub fn still_frame(mut self, test: Test) + where + Test: FnOnce(&mut VirtualRecorder), + { + let capture = std::env::var("CAPTURE").is_ok(); + let errored = + std::panic::catch_unwind(AssertUnwindSafe(|| test(&mut self.recorder))).is_err(); + if errored || capture { + let path = target_dir().join(format!("{}.png", self.name)); + self.recorder + .image() + .save(&path) + .expect("error saving file"); + println!("Wrote {}", path.display()); + + if errored { + std::process::exit(-1); + } + } + } + + pub fn animated(mut self, test: Test) + where + Test: FnOnce(&mut AnimationRecorder<'_, Rgba8>), + { + let mut animation = self.recorder.record_animated_png(60); + let capture = std::env::var("CAPTURE").is_ok(); + let errored = std::panic::catch_unwind(AssertUnwindSafe(|| test(&mut animation))).is_err(); + if errored || capture { + let path = target_dir().join(format!("{}.png", self.name)); + animation.write_to(&path).expect("error saving file"); + println!("Wrote {}", path.display()); + + if errored { + std::process::exit(-1); + } + } + } +} + +#[macro_export] +macro_rules! example { + ($name:ident) => { + $crate::example!($name, 750) + }; + ($name:ident, $width:expr) => { + $crate::example::Example::build(stringify!($name), $name(), $width, None) + }; + ($name:ident, $width:expr, $height:expr) => { + $crate::example::Example::build(stringify!($name), $name(), $width, Some($height)) + }; +} diff --git a/src/lib.rs b/src/lib.rs index ebcea991c..b8e1d0df6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,9 @@ pub mod value; pub mod widget; pub mod widgets; pub mod window; + +#[doc(hidden)] +pub mod example; use std::ops::{Add, AddAssign, Sub, SubAssign}; #[cfg(feature = "tokio")]