diff --git a/Cargo.lock b/Cargo.lock index 6d73255..12ee340 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,6 +154,34 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attribute-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c94f43ede6f25dab1dea046bff84d85dea61bd49aba7a9011ad66c0d449077b" +dependencies = [ + "attribute-derive-macro", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b409e2b2d2dc206d2c0ad3575a93f001ae21a1593e2d0c69b69c308e63f3b422" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow 0.8.1", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -316,6 +344,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "collection_literals" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" + [[package]] name = "color_quant" version = "1.1.0" @@ -338,6 +372,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -416,6 +462,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "derive-where" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146398d62142a0f35248a608f17edf0dde57338354966d6e41d0eb2d16980ccb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -437,6 +494,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "equivalent" version = "1.0.1" @@ -658,6 +721,7 @@ version = "0.1.0" dependencies = [ "ahash", "alot", + "gooey-macros", "intentional", "interner", "kempt", @@ -668,6 +732,20 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "gooey-macros" +version = "0.1.0" +dependencies = [ + "attribute-derive", + "insta", + "manyhow 0.9.0", + "prettyplease", + "proc-macro2", + "quote", + "quote-use", + "syn", +] + [[package]] name = "gpu-alloc" version = "0.6.0" @@ -792,6 +870,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "insta" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", + "yaml-rust", +] + [[package]] name = "intentional" version = "0.1.0" @@ -804,6 +895,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c60687056b35a996f2213287048a7092d801b61df5fee3bd5bd9bf6f17a2d0" +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -938,6 +1035,12 @@ dependencies = [ "redox_syscall 0.4.1", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.11" @@ -1001,6 +1104,52 @@ dependencies = [ "libc", ] +[[package]] +name = "manyhow" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b76546495d933baa165075b95c0a15e8f7ef75e53f56b19b7144d80fd52bd" +dependencies = [ + "manyhow-macros 0.8.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aebef87880bafc898c6bed1435e8fdc58634275ff97693a4bb96ad561c73c43" +dependencies = [ + "manyhow-macros 0.9.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "manyhow-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba072c0eadade3160232e70893311f1f8903974488096e2eb8e48caba2f0cf1" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "manyhow-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f74cc8a0d8b05a7e919011c78a2744e7dea66567c05fb046666f3bae383d8d04" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1442,6 +1591,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" version = "2.0.0" @@ -1451,6 +1610,17 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + [[package]] name = "proc-macro2" version = "1.0.69" @@ -1484,6 +1654,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quote-use" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b5abe3fe82fdeeb93f44d66a7b444dedf2e4827defb0a8e69c437b2de2ef94" +dependencies = [ + "quote", + "quote-use-macros", + "syn", +] + +[[package]] +name = "quote-use-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ea44c7e20f16017a76a245bb42188517e13d16dcb1aa18044bc406cdc3f4af" +dependencies = [ + "derive-where", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "rand" version = "0.8.5" @@ -1711,6 +1904,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "similar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" + [[package]] name = "siphasher" version = "0.3.11" @@ -2698,6 +2897,15 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yazi" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 9792341..f8dbfa8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "gooey" version = "0.1.0" @@ -22,6 +24,7 @@ tracing-subscriber = { version = "0.3", optional = true, features = [ ] } palette = "0.7.3" ahash = "0.8.6" +gooey-macros = { version = "0.1.0", path = "gooey-macros" } # [patch."https://github.com/khonsulabs/kludgine"] diff --git a/examples/button.rs b/examples/button.rs index 587c46c..170ffa6 100644 --- a/examples/button.rs +++ b/examples/button.rs @@ -1,17 +1,28 @@ use gooey::value::Dynamic; +use gooey::widget::MakeWidget; +use gooey::widgets::button::ButtonOutline; use gooey::widgets::Button; use gooey::Run; +use kludgine::Color; // begin rustme snippet: readme fn main() -> gooey::Result { // Create a dynamic usize. - let count = Dynamic::new(0_usize); + let count = Dynamic::new(0_isize); // Create a new button with a label that is produced by mapping the contents // of `count`. Button::new(count.map_each(ToString::to_string)) // Set the `on_click` callback to a closure that increments the counter. .on_click(count.with_clone(|count| move |_| count.set(count.get() + 1))) + .and( + // Creates a second, outlined button + Button::new(count.map_each(ToString::to_string)) + // Set the `on_click` callback to a closure that decrements the counter. + .on_click(count.with_clone(|count| move |_| count.set(count.get() - 1))) + .with(&ButtonOutline, Color::DARKRED), + ) + .into_columns() // Run the button as an an application. .run() } diff --git a/gooey-macros/Cargo.toml b/gooey-macros/Cargo.toml new file mode 100644 index 0000000..a38fa69 --- /dev/null +++ b/gooey-macros/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "gooey-macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +attribute-derive = "0.8.1" +manyhow = "0.9.0" +proc-macro2 = "1.0.69" +quote = "1.0.33" +quote-use = "0.7.2" +syn = "2.0.39" + +[dev-dependencies] +insta = "1.34.0" +prettyplease = "0.2.15" diff --git a/gooey-macros/src/animation.rs b/gooey-macros/src/animation.rs new file mode 100644 index 0000000..2ef277a --- /dev/null +++ b/gooey-macros/src/animation.rs @@ -0,0 +1,66 @@ +use manyhow::bail; +use quote::ToTokens; +use syn::{Field, ItemStruct}; + +use crate::*; + +pub fn linear_interpolate( + ItemStruct { + ident, + generics, + fields, + .. + }: ItemStruct, +) -> Result { + if let Some(generic) = generics.type_params().next() { + bail!(generic, "generics not supported"); + } + + let fields = match fields { + syn::Fields::Unit => bail!(ident, "unit structs are not supported"), + fields => fields + .into_iter() + .enumerate() + .map(|(idx, Field { ident, .. })| { + let ident = ident + .map(ToTokens::into_token_stream) + .unwrap_or_else(|| proc_macro2::Literal::usize_unsuffixed(idx).into_token_stream()); + quote!(#ident: ::gooey::animation::LinearInterpolate::lerp(&self.#ident, &__target.#ident, __percent),) + }), + }; + + Ok(quote! { + impl ::gooey::animation::LinearInterpolate for #ident { + fn lerp(&self, __target: &Self, __percent: f32) -> Self { + #ident{#(#fields)*} + } + } + }) +} + +#[cfg(test)] +macro_rules! expansion_snapshot { + (#[derive($fn:expr)]$($tokens:tt)*) => {{ + use insta::assert_snapshot; + use prettyplease::unparse; + use syn::{parse2, parse_quote}; + let input = parse_quote!($($tokens)*); + let output = $fn(input).unwrap(); + assert_snapshot!(unparse(&parse2(output).unwrap())) + }}; +} + +#[test] +fn test() { + expansion_snapshot! { + #[derive(linear_interpolate)] + struct HelloWorld { + fielda: Hello, + fieldb: World, + } + }; + expansion_snapshot! { + #[derive(linear_interpolate)] + struct HelloWorld(Hello, World); + }; +} diff --git a/gooey-macros/src/lib.rs b/gooey-macros/src/lib.rs new file mode 100644 index 0000000..051be8c --- /dev/null +++ b/gooey-macros/src/lib.rs @@ -0,0 +1,7 @@ +use manyhow::{manyhow, Result}; +use quote_use::quote_use as quote; +use proc_macro2::TokenStream; +mod animation; + +#[manyhow(proc_macro_derive(LinearInterpolate))] +pub use animation::linear_interpolate; diff --git a/gooey-macros/src/snapshots/gooey_macros__animation__test-2.snap b/gooey-macros/src/snapshots/gooey_macros__animation__test-2.snap new file mode 100644 index 0000000..117910f --- /dev/null +++ b/gooey-macros/src/snapshots/gooey_macros__animation__test-2.snap @@ -0,0 +1,21 @@ +--- +source: gooey-macros/src/animation.rs +expression: unparse(&parse2(output).unwrap()) +--- +impl ::gooey::animation::LinearInterpolate for HelloWorld { + fn lerp(&self, __target: &Self, __percent: f32) -> Self { + HelloWorld { + 0: ::gooey::animation::LinearInterpolate::lerp( + &self.0, + &__target.0, + __percent, + ), + 1: ::gooey::animation::LinearInterpolate::lerp( + &self.1, + &__target.1, + __percent, + ), + } + } +} + diff --git a/gooey-macros/src/snapshots/gooey_macros__animation__test.snap b/gooey-macros/src/snapshots/gooey_macros__animation__test.snap new file mode 100644 index 0000000..8a133d1 --- /dev/null +++ b/gooey-macros/src/snapshots/gooey_macros__animation__test.snap @@ -0,0 +1,21 @@ +--- +source: src/animation.rs +expression: unparse(&parse2(output).unwrap()) +--- +impl ::gooey::animation::LinearInterpolate for HelloWorld { + fn lerp(&self, __target: &Self, __percent: f32) -> Self { + HelloWorld { + fielda: ::gooey::animation::LinearInterpolate::lerp( + &self.fielda, + &__target.fielda, + __percent, + ), + fieldb: ::gooey::animation::LinearInterpolate::lerp( + &self.fieldb, + &__target.fieldb, + __percent, + ), + } + } +} + diff --git a/src/animation.rs b/src/animation.rs index f159181..ba99798 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -616,6 +616,8 @@ pub trait LinearInterpolate: PartialEq { fn lerp(&self, target: &Self, percent: f32) -> Self; } +pub use gooey_macros::LinearInterpolate; + macro_rules! impl_lerp_for_int { ($type:ident, $unsigned:ident, $float:ident) => { impl LinearInterpolate for $type { diff --git a/src/lib.rs b/src/lib.rs index 5f3ddd0..7309d9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,9 @@ #![warn(clippy::pedantic, missing_docs)] #![allow(clippy::module_name_repetitions, clippy::missing_errors_doc)] +// for proc-macros +extern crate self as gooey; + #[macro_use] mod utils; diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 5d69f06..c55b5ce 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -3,11 +3,12 @@ use std::panic::UnwindSafe; use std::time::Duration; use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton}; -use kludgine::figures::units::{Px, UPx}; +use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; +use kludgine::shapes::StrokeOptions; use kludgine::Color; -use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; +use crate::animation::{AnimationHandle, AnimationTarget, LinearInterpolate, Spawn}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::styles::components::{ AutoFocusableControls, Easing, IntrinsicPadding, OpaqueWidgetColor, SurfaceColor, TextColor, @@ -28,11 +29,17 @@ pub struct Button { pub enabled: Value, currently_enabled: bool, buttons_pressed: usize, - background_color: Option>, - text_color: Option>, + active_style: Option>, color_animation: AnimationHandle, } +#[derive(Debug, PartialEq, Eq, Clone, Copy, LinearInterpolate)] +struct ButtonStyle { + background: Color, + foreground: Color, + outline: Color, +} + impl Button { /// Returns a new button with the provided label. pub fn new(content: impl MakeWidget) -> Self { @@ -42,8 +49,7 @@ impl Button { enabled: Value::Constant(true), currently_enabled: true, buttons_pressed: 0, - background_color: None, - text_color: None, + active_style: None, color_animation: AnimationHandle::default(), } } @@ -77,62 +83,63 @@ impl Button { } fn update_colors(&mut self, context: &WidgetContext<'_, '_>, immediate: bool) { - let (background_color, text_color) = match () { - () if !self.enabled.get() => ( - context.get(&ButtonDisabledBackground), - context.get(&ButtonDisabledForeground), - ), + let new_style = match () { + () if !self.enabled.get() => ButtonStyle { + background: context.get(&ButtonDisabledBackground), + foreground: context.get(&ButtonDisabledForeground), + outline: context.get(&ButtonDisabledOutline), + }, // TODO this probably should use actual style. - () if context.is_default() => ( - context.theme().primary.color, - context.theme().primary.on_color, - ), - () if context.active() => ( - context.get(&ButtonActiveBackground), - context.get(&ButtonActiveForeground), - ), - () if context.hovered() => ( - context.get(&ButtonHoverBackground), - context.get(&ButtonHoverForeground), - ), - () => ( - context.get(&ButtonBackground), - context.get(&ButtonForeground), - ), + () if context.is_default() => ButtonStyle { + background: context.theme().primary.color, + foreground: context.theme().primary.on_color, + outline: Color::CLEAR_BLACK, + }, + () if context.active() => ButtonStyle { + background: context.get(&ButtonActiveBackground), + foreground: context.get(&ButtonActiveForeground), + outline: context.get(&ButtonActiveOutline), + }, + () if context.hovered() => ButtonStyle { + background: context.get(&ButtonHoverBackground), + foreground: context.get(&ButtonHoverForeground), + outline: context.get(&ButtonHoverOutline), + }, + () => ButtonStyle { + background: context.get(&ButtonBackground), + foreground: context.get(&ButtonForeground), + outline: context.get(&ButtonOutline), + }, }; - match (immediate, &self.background_color, &self.text_color) { - (false, Some(bg), Some(text)) => { - self.color_animation = ( - bg.transition_to(background_color), - text.transition_to(text_color), - ) + match (immediate, &self.active_style) { + (false, Some(style)) => { + self.color_animation = (style.transition_to(new_style)) .over(Duration::from_millis(150)) .with_easing(context.get(&Easing)) .spawn(); } - (true, Some(bg), Some(text)) => { - bg.update(background_color); - text.update(text_color); + (true, Some(style)) => { + style.update(new_style); self.color_animation.clear(); } _ => { - self.background_color = Some(Dynamic::new(background_color)); - let text_color = Dynamic::new(text_color); - self.text_color = Some(text_color.clone()); - context.attach_styles(Styles::new().with(&TextColor, text_color)); + let new_style = Dynamic::new(new_style); + let foreground = new_style.map_each(|s| s.foreground); + self.active_style = Some(new_style); + context.attach_styles(Styles::new().with(&TextColor, foreground)); } } } - fn current_background(&mut self, context: &WidgetContext<'_, '_>) -> Color { - if self.background_color.is_none() { + fn current_style(&mut self, context: &WidgetContext<'_, '_>) -> ButtonStyle { + if self.active_style.is_none() { self.update_colors(context, false); } - let background_color = self.background_color.as_ref().expect("always initialized"); - context.redraw_when_changed(background_color); - background_color.get() + let style = self.active_style.as_ref().expect("always initialized"); + context.redraw_when_changed(style); + style.get() } } @@ -149,8 +156,10 @@ impl Widget for Button { self.enabled.redraw_when_changed(context); - let background_color = self.current_background(context); - context.gfx.fill(background_color); + let style = self.current_style(context); + context.gfx.fill(style.background); + + context.stroke_outline::(style.outline, StrokeOptions::default()); if context.focused() { context.draw_focus_ring(); @@ -322,5 +331,15 @@ define_components! { /// The foreground color of the button when the mouse cursor is hovering over /// it. ButtonDisabledForeground(Color, "disabled_foreground_color", contrasting!(ButtonDisabledBackground, ButtonForeground, TextColor, SurfaceColor)) + /// The outline color of the button. + ButtonOutline(Color, "outline_color", Color::CLEAR_BLACK) + /// The outline color of the button when it is active (depressed). + ButtonActiveOutline(Color, "active_outline_color", contrasting!(ButtonActiveBackground, ButtonOutline, TextColor, SurfaceColor)) + /// The outline color of the button when the mouse cursor is hovering over + /// it. + ButtonHoverOutline(Color, "hover_outline_color", contrasting!(ButtonHoverBackground, ButtonOutline, TextColor, SurfaceColor)) + /// The outline color of the button when the mouse cursor is hovering over + /// it. + ButtonDisabledOutline(Color, "disabled_outline_color", contrasting!(ButtonDisabledBackground, ButtonOutline, TextColor, SurfaceColor)) } } diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 286c35e..e15ec30 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -321,10 +321,7 @@ impl Widget for Input { if window_focused && cursor_state.visible { context.gfx.draw_shape( &Shape::filled_rect( - Rect::new( - location, - Size::new(Px(1), line_height), - ), + Rect::new(location, Size::new(Px(1), line_height)), highlight, // TODO cursor should be a bold color, highlight probably not. This should have its own color. ), padding,