From 0f6d3838b1311de4f3ca5165547521292936ad85 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sun, 5 Nov 2023 11:50:59 -0800 Subject: [PATCH] LayoutContext measure() now is layout(). LayoutContext can either persist layout information or be used temporarily for measurement. While this caching is constantly thrown out currently, this is a step towards being able to only re-layout widgets if they've been invalidated. --- examples/animation.rs | 4 +- examples/counter.rs | 4 +- examples/gameui.rs | 42 +++-- examples/style.rs | 4 +- src/animation.rs | 345 +-------------------------------------- src/animation/easings.rs | 342 ++++++++++++++++++++++++++++++++++++++ src/context.rs | 211 +++++++++++++++++------- src/lib.rs | 14 +- src/styles/components.rs | 3 +- src/tree.rs | 29 +++- src/utils.rs | 65 ++++++-- src/value.rs | 19 ++- src/widget.rs | 134 ++++++++++----- src/widgets/align.rs | 53 +++--- src/widgets/button.rs | 24 +-- src/widgets/canvas.rs | 6 +- src/widgets/expand.rs | 24 +-- src/widgets/input.rs | 13 +- src/widgets/label.rs | 13 +- src/widgets/resize.rs | 22 +-- src/widgets/scroll.rs | 160 +++++++++--------- src/widgets/stack.rs | 128 +++++++-------- src/widgets/style.rs | 16 +- src/widgets/tilemap.rs | 6 +- src/window.rs | 48 +++--- src/with_clone.rs | 34 ---- 26 files changed, 963 insertions(+), 800 deletions(-) create mode 100644 src/animation/easings.rs delete mode 100644 src/with_clone.rs diff --git a/examples/animation.rs b/examples/animation.rs index 780dc27..f8c138a 100644 --- a/examples/animation.rs +++ b/examples/animation.rs @@ -3,7 +3,7 @@ use std::time::Duration; use gooey::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn}; use gooey::value::Dynamic; use gooey::widgets::{Button, Label, Stack}; -use gooey::{widgets, Run, WithClone}; +use gooey::{children, Run, WithClone}; fn main() -> gooey::Result { let animation = Dynamic::new(AnimationHandle::new()); @@ -17,7 +17,7 @@ fn main() -> gooey::Result { .on_complete(|| println!("Gooey animations are neat!")) .launch(); - Stack::columns(widgets![ + Stack::columns(children![ Button::new("To 0").on_click(animate_to(&animation, &value, 0)), Label::new(label), Button::new("To 100").on_click(animate_to(&animation, &value, 100)), diff --git a/examples/counter.rs b/examples/counter.rs index d6f851b..43e5f26 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -2,13 +2,13 @@ use std::string::ToString; use gooey::value::Dynamic; use gooey::widgets::{Align, Button, Expand, Label, Resize, Stack}; -use gooey::{widgets, Run}; +use gooey::{children, Run}; use kludgine::figures::units::Lp; fn main() -> gooey::Result { let counter = Dynamic::new(0i32); let label = counter.map_each(ToString::to_string); - Expand::new(Align::centered(Stack::columns(widgets![ + Expand::new(Align::centered(Stack::columns(children![ Resize::width(Lp::points(100), Label::new(label)), Button::new("+").on_click(counter.with_clone(|counter| { move |_| { diff --git a/examples/gameui.rs b/examples/gameui.rs index b905bd6..c112a12 100644 --- a/examples/gameui.rs +++ b/examples/gameui.rs @@ -1,7 +1,7 @@ +use gooey::children; use gooey::value::Dynamic; -use gooey::widget::{HANDLED, IGNORED}; +use gooey::widget::{MakeWidget, HANDLED, IGNORED}; use gooey::widgets::{Canvas, Expand, Input, Label, Scroll, Stack}; -use gooey::{widgets, Run}; use kludgine::app::winit::event::ElementState; use kludgine::app::winit::keyboard::Key; use kludgine::figures::{Point, Rect}; @@ -12,9 +12,26 @@ fn main() -> gooey::Result { let chat_log = Dynamic::new("Chat log goes here.\n".repeat(100)); let chat_message = Dynamic::new(String::new()); - Expand::new(Stack::rows(widgets![ - Expand::new(Stack::columns(widgets![ - Expand::new(Scroll::vertical(Label::new(chat_log.clone()))), + let input = Input::new(chat_message.clone()) + .on_key({ + let chat_log = chat_log.clone(); + move |input| match (input.state, input.logical_key) { + (ElementState::Pressed, Key::Enter) => { + let new_message = chat_message.map_mut(|text| std::mem::take(text)); + chat_log.map_mut(|chat_log| { + chat_log.push_str(&new_message); + chat_log.push('\n'); + }); + HANDLED + } + _ => IGNORED, + } + }) + .make_widget(); + + Expand::new(Stack::rows(children![ + Expand::new(Stack::columns(children![ + Expand::new(Scroll::vertical(Label::new(chat_log))), Expand::weighted( 2, Canvas::new(|context| { @@ -28,19 +45,8 @@ fn main() -> gooey::Result { }) ) ])), - Input::new(chat_message.clone()).on_key(move |input| { - match (input.state, input.logical_key) { - (ElementState::Pressed, Key::Enter) => { - let new_message = chat_message.map_mut(|text| std::mem::take(text)); - chat_log.map_mut(|chat_log| { - chat_log.push_str(&new_message); - chat_log.push('\n'); - }); - HANDLED - } - _ => IGNORED, - } - }), + input.clone(), ])) + .with_next_focus(input) .run() } diff --git a/examples/style.rs b/examples/style.rs index d2ba786..69f203f 100644 --- a/examples/style.rs +++ b/examples/style.rs @@ -1,6 +1,6 @@ use gooey::styles::components::TextColor; use gooey::styles::Styles; -use gooey::widget::{Widget, Widgets}; +use gooey::widget::{Children, MakeWidget, Widget}; use gooey::widgets::stack::Stack; use gooey::widgets::{Button, Style}; use gooey::window::Window; @@ -10,7 +10,7 @@ use kludgine::Color; fn main() -> gooey::Result { Window::for_widget( Stack::rows( - Widgets::new() + Children::new() .with_widget(Button::new("Default")) .with_widget(red_text(Button::new("Styled"))), ) diff --git a/src/animation.rs b/src/animation.rs index 8593af0..e1e7af9 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -36,7 +36,8 @@ //! assert_eq!(reader.get(), 100); //! ``` -use std::f32::consts::PI; +pub mod easings; + use std::fmt::Debug; use std::ops::{ControlFlow, Deref}; use std::panic::{RefUnwindSafe, UnwindSafe}; @@ -48,6 +49,7 @@ use alot::{LotId, Lots}; use kempt::Set; use kludgine::Color; +use crate::animation::easings::Linear; use crate::styles::Component; use crate::value::Dynamic; @@ -825,344 +827,3 @@ pub trait Easing: Debug + Send + Sync + RefUnwindSafe + UnwindSafe + 'static { /// need to be bounded between zero and one. fn ease(&self, progress: ZeroToOne) -> f32; } - -/// An [`Easing`] function that produces a steady, linear transition. -#[derive(Clone, Copy, Debug)] -pub struct Linear; - -impl Easing for Linear { - fn ease(&self, progress: ZeroToOne) -> f32 { - *progress - } -} - -// Think this macro has a long name? This is to ensure rustfmt wraps the closure -// onto its own line. Seriously, try shortening the name and see how it changes -// the formatting. If it keeps all closures on a single line, remove this -// comment and change this back to `declare_easing_function`. -macro_rules! declare_easing_function_implementation { - ($name:ident, $anchor_name:ident, $description:literal, $closure:expr) => { - /// An [`Easing`] function that eases - #[doc = $description] - #[doc = concat!(".\n\nSee for a visualization and more information.")] - #[derive(Clone, Copy, Debug)] - pub struct $name; - - impl $name { - /// Eases - #[doc = $description] - #[doc = concat!(".\n\nSee for a visualization and more information.")] - #[must_use] - pub fn ease(progress: ZeroToOne) -> f32 { - let closure = force_closure_type($closure); - closure(*progress) - } - } - - impl Easing for $name { - fn ease(&self, progress: ZeroToOne) -> f32 { - Self::ease(progress) - } - } - - impl From<$name> for EasingFunction { - fn from(_function: $name) -> Self { - Self::Fn($name::ease) - } - } - }; -} - -// This prevents the closures from requiring the parameter to be type annotated. -fn force_closure_type(f: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 { - f -} - -declare_easing_function_implementation!( - EaseOutSine, - easeOutSine, - "out using a sine wave", - |percent| (percent * PI).sin() / 2. -); - -declare_easing_function_implementation!( - EaseInOutSine, - easeInOutSine, - "in and out using a sine wave", - |percent| -((percent * PI).cos() - 1.) / 2. -); - -fn squared(value: f32) -> f32 { - value * value -} - -declare_easing_function_implementation!( - EaseInQuadradic, - easeInQuad, - "in using a quadradic (x^2) curve", - squared -); - -declare_easing_function_implementation!( - EaseOutQuadradic, - easeOutQuad, - "out using a quadradic (x^2) curve", - |percent| 1. - squared(1. - percent) -); - -declare_easing_function_implementation!( - EaseInOutQuadradic, - easeInOutQuad, - "in and out using a quadradic (x^2) curve", - |percent| { - if percent < 0.5 { - 2. * percent * percent - } else { - 1. - squared(-2. * percent + 2.) / 2. - } - } -); - -fn cubed(value: f32) -> f32 { - value * value * value -} - -declare_easing_function_implementation!( - EaseInCubic, - easeInCubic, - "in using a cubic (x^3) curve", - cubed -); - -declare_easing_function_implementation!( - EaseOutCubic, - easeOutCubic, - "out using a cubic (x^3) curve", - |percent| 1. - cubed(1. - percent) -); - -declare_easing_function_implementation!( - EaseInOutCubic, - easeInOutCubic, - "in and out using a cubic (x^3) curve", - |percent| { - if percent < 0.5 { - 4. * cubed(percent) - } else { - 1. - cubed(-2. * percent + 2.) / 2. - } - } -); - -fn quarted(value: f32) -> f32 { - let sq = squared(value); - squared(sq) -} - -declare_easing_function_implementation!( - EaseInQuartic, - easeInQuart, - "in using a quartic (x^4) curve", - quarted -); - -declare_easing_function_implementation!( - EaseOutQuartic, - easeOutQuart, - "out using a quartic (x^4) curve", - |percent| 1. - quarted(1. - percent) -); - -declare_easing_function_implementation!( - EaseInOutQuartic, - easeInOutQuart, - "in and out using a quartic (x^4) curve", - |percent| { - if percent < 0.5 { - 8. * quarted(percent) - } else { - 1. - quarted(-2. * percent + 2.) / 2. - } - } -); - -fn quinted(value: f32) -> f32 { - let squared = squared(value); - let cubed = squared * value; - squared * cubed -} - -declare_easing_function_implementation!( - EaseInQuintic, - easeInQuint, - "in using a quintic (x^5) curve", - quinted -); - -declare_easing_function_implementation!( - EaseOutQuintic, - easeOutQuint, - "out using a quintic (x^5) curve", - |percent| 1. - quinted(1. - percent) -); - -declare_easing_function_implementation!( - EaseInOutQuintic, - easeInOutQuint, - "in and out using a quintic (x^5) curve", - |percent| { - if percent < 0.5 { - 8. * quinted(percent) - } else { - 1. - quinted(-2. * percent + 2.) / 2. - } - } -); - -declare_easing_function_implementation!( - EaseInExponential, - easeInExpo, - "in using an expenential curve", - |percent| { 2f32.powf(10. * percent - 10.) } -); - -declare_easing_function_implementation!( - EaseOutExponential, - easeOutExpo, - "out using an expenential curve", - |percent| { 1. - 2f32.powf(-10. * percent) } -); - -declare_easing_function_implementation!( - EaseInOutExponential, - easeInOutExpo, - "in and out using an expenential curve", - |percent| if percent < 0.5 { - 2f32.powf(20. * percent - 10.) / 2. - } else { - 2. - 2f32.powf(-20. * percent + 10.) / 2. - } -); - -declare_easing_function_implementation!( - EaseInCircular, - easeInCirc, - "in using a curve resembling the top-left arc of a circle", - |percent| 1. - (1. - squared(percent)).sqrt() -); - -declare_easing_function_implementation!( - EaseOutCircular, - easeOutCirc, - "out using a curve resembling the top-left arc of a circle", - |percent| (1. - squared(percent - 1.)).sqrt() -); - -declare_easing_function_implementation!( - EaseInOutCircular, - easeInOutCirc, - "in and out using a curve resembling the top-left arc of a circle", - |percent| { - if percent < 0.5 { - 1. - (1. - squared(2. * percent)).sqrt() / 2. - } else { - (1. - squared(-2. * percent + 2.)).sqrt() - } - } -); - -const C1: f32 = 1.70158; -const C2: f32 = C1 * 1.525; -const C3: f32 = C1 + 1.; -const C4: f32 = (2. * PI) / 3.; -const C5: f32 = (2. * PI) / 4.5; - -declare_easing_function_implementation!( - EaseInBack, - easeInBack, - "in using a curve that backs away initially", - |percent| { - let squared = squared(percent); - let cubed = squared + percent; - C3 * cubed - C1 * squared - } -); - -declare_easing_function_implementation!( - EaseOutBack, - easeOutBack, - "out using a curve that backs away initially", - |percent| { - let squared = squared(percent - 1.); - let cubed = squared + percent; - 1. + C3 * cubed - C1 * squared - } -); - -declare_easing_function_implementation!( - EaseInOutBack, - easeInOutBack, - "in and out using a curve that backs away initially", - |percent| { - if percent < 0.5 { - (squared(2. * percent) * ((C2 + 1.) * 2. * percent - C2)) / 2. - } else { - (squared(2. * percent - 2.) * ((C2 + 1.) * (percent * 2. - 2.) + C2) + 2.) / 2. - } - } -); - -declare_easing_function_implementation!( - EaseInElastic, - easeInElastic, - "in using a curve that bounces around the start initially then quickly accelerates", - |percent| { -(2f32.powf(10. * percent - 10.) * (percent * 10. - 10.75).sin() * C4) } -); - -declare_easing_function_implementation!( - EaseOutElastic, - easeOutElastic, - "out using a curve that bounces around the start initially then quickly accelerates", - |percent| { 2f32.powf(-10. * percent) * (percent * 10. - 0.75).sin() * C4 + 1. } -); - -declare_easing_function_implementation!( - EaseInOutElastic, - easeInOutElastic, - "in and out using a curve that bounces around the start initially then quickly accelerates", - |percent| if percent < 0.5 { - -(2f32.powf(-20. * percent - 10.) * (percent * 20. - 11.125).sin() * C5 / 2.) - } else { - 2f32.powf(-20. * percent + 10.) * (percent * 20. - 11.125).sin() * C5 / 2. + 1. - } -); - -declare_easing_function_implementation!( - EaseInBounce, - easeInBounce, - "in using a curve that bounces progressively closer as it progresses", - |percent| 1. - EaseOutBounce.ease(ZeroToOne(percent)) -); - -declare_easing_function_implementation!( - EaseOutBounce, - easeOutBounce, - "out using a curve that bounces progressively closer as it progresses", - |percent| { - const N1: f32 = 7.5625; - const D1: f32 = 2.75; - - if percent < 1. / D1 { - N1 * percent * percent - } else if percent < 2. / D1 { - let percent = percent - 1.5; - N1 * (percent / D1) * percent + 0.75 - } else if percent < 2.5 / D1 { - let percent = percent - 2.25; - N1 * (percent / D1) * percent + 0.9375 - } else { - let percent = percent - 2.625; - N1 * (percent / D1) * percent + 0.984_375 - } - } -); diff --git a/src/animation/easings.rs b/src/animation/easings.rs new file mode 100644 index 0000000..54fb3ad --- /dev/null +++ b/src/animation/easings.rs @@ -0,0 +1,342 @@ +//! Built-in [`Easing`] implementations. + +use std::f32::consts::PI; + +use crate::animation::{Easing, EasingFunction, ZeroToOne}; + +/// An [`Easing`] function that produces a steady, linear transition. +#[derive(Clone, Copy, Debug)] +pub struct Linear; + +impl Easing for Linear { + fn ease(&self, progress: ZeroToOne) -> f32 { + *progress + } +} + +macro_rules! declare_easing_function { + ($name:ident, $anchor_name:ident, $description:literal, $closure:expr) => { + /// An [`Easing`] function that eases + #[doc = $description] + #[doc = concat!(".\n\nSee for a visualization and more information.")] + #[derive(Clone, Copy, Debug)] + pub struct $name; + + impl $name { + /// Eases + #[doc = $description] + #[doc = concat!(".\n\nSee for a visualization and more information.")] + #[must_use] + pub fn ease(progress: ZeroToOne) -> f32 { + let closure = force_closure_type($closure); + closure(*progress) + } + } + + impl Easing for $name { + fn ease(&self, progress: ZeroToOne) -> f32 { + Self::ease(progress) + } + } + + impl From<$name> for EasingFunction { + fn from(_function: $name) -> Self { + Self::Fn($name::ease) + } + } + }; +} + +// This prevents the closures from requiring the parameter to be type annotated. +fn force_closure_type(f: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 { + f +} + +declare_easing_function!( + EaseOutSine, + easeOutSine, + "out using a sine wave", + |percent| (percent * PI).sin() / 2. +); + +declare_easing_function!( + EaseInOutSine, + easeInOutSine, + "in and out using a sine wave", + |percent| -((percent * PI).cos() - 1.) / 2. +); + +fn squared(value: f32) -> f32 { + value * value +} + +declare_easing_function!( + EaseInQuadradic, + easeInQuad, + "in using a quadradic (x^2) curve", + squared +); + +declare_easing_function!( + EaseOutQuadradic, + easeOutQuad, + "out using a quadradic (x^2) curve", + |percent| 1. - squared(1. - percent) +); + +declare_easing_function!( + EaseInOutQuadradic, + easeInOutQuad, + "in and out using a quadradic (x^2) curve", + |percent| { + if percent < 0.5 { + 2. * percent * percent + } else { + 1. - squared(-2. * percent + 2.) / 2. + } + } +); + +fn cubed(value: f32) -> f32 { + value * value * value +} + +declare_easing_function!( + EaseInCubic, + easeInCubic, + "in using a cubic (x^3) curve", + cubed +); + +declare_easing_function!( + EaseOutCubic, + easeOutCubic, + "out using a cubic (x^3) curve", + |percent| 1. - cubed(1. - percent) +); + +declare_easing_function!( + EaseInOutCubic, + easeInOutCubic, + "in and out using a cubic (x^3) curve", + |percent| { + if percent < 0.5 { + 4. * cubed(percent) + } else { + 1. - cubed(-2. * percent + 2.) / 2. + } + } +); + +fn quarted(value: f32) -> f32 { + let sq = squared(value); + squared(sq) +} + +declare_easing_function!( + EaseInQuartic, + easeInQuart, + "in using a quartic (x^4) curve", + quarted +); + +declare_easing_function!( + EaseOutQuartic, + easeOutQuart, + "out using a quartic (x^4) curve", + |percent| 1. - quarted(1. - percent) +); + +declare_easing_function!( + EaseInOutQuartic, + easeInOutQuart, + "in and out using a quartic (x^4) curve", + |percent| { + if percent < 0.5 { + 8. * quarted(percent) + } else { + 1. - quarted(-2. * percent + 2.) / 2. + } + } +); + +fn quinted(value: f32) -> f32 { + let squared = squared(value); + let cubed = squared * value; + squared * cubed +} + +declare_easing_function!( + EaseInQuintic, + easeInQuint, + "in using a quintic (x^5) curve", + quinted +); + +declare_easing_function!( + EaseOutQuintic, + easeOutQuint, + "out using a quintic (x^5) curve", + |percent| 1. - quinted(1. - percent) +); + +declare_easing_function!( + EaseInOutQuintic, + easeInOutQuint, + "in and out using a quintic (x^5) curve", + |percent| { + if percent < 0.5 { + 8. * quinted(percent) + } else { + 1. - quinted(-2. * percent + 2.) / 2. + } + } +); + +declare_easing_function!( + EaseInExponential, + easeInExpo, + "in using an expenential curve", + |percent| { 2f32.powf(10. * percent - 10.) } +); + +declare_easing_function!( + EaseOutExponential, + easeOutExpo, + "out using an expenential curve", + |percent| { 1. - 2f32.powf(-10. * percent) } +); + +declare_easing_function!( + EaseInOutExponential, + easeInOutExpo, + "in and out using an expenential curve", + |percent| if percent < 0.5 { + 2f32.powf(20. * percent - 10.) / 2. + } else { + 2. - 2f32.powf(-20. * percent + 10.) / 2. + } +); + +declare_easing_function!( + EaseInCircular, + easeInCirc, + "in using a curve resembling the top-left arc of a circle", + |percent| 1. - (1. - squared(percent)).sqrt() +); + +declare_easing_function!( + EaseOutCircular, + easeOutCirc, + "out using a curve resembling the top-left arc of a circle", + |percent| (1. - squared(percent - 1.)).sqrt() +); + +declare_easing_function!( + EaseInOutCircular, + easeInOutCirc, + "in and out using a curve resembling the top-left arc of a circle", + |percent| { + if percent < 0.5 { + 1. - (1. - squared(2. * percent)).sqrt() / 2. + } else { + (1. - squared(-2. * percent + 2.)).sqrt() + } + } +); + +const C1: f32 = 1.70158; +const C2: f32 = C1 * 1.525; +const C3: f32 = C1 + 1.; +const C4: f32 = (2. * PI) / 3.; +const C5: f32 = (2. * PI) / 4.5; + +declare_easing_function!( + EaseInBack, + easeInBack, + "in using a curve that backs away initially", + |percent| { + let squared = squared(percent); + let cubed = squared + percent; + C3 * cubed - C1 * squared + } +); + +declare_easing_function!( + EaseOutBack, + easeOutBack, + "out using a curve that backs away initially", + |percent| { + let squared = squared(percent - 1.); + let cubed = squared + percent; + 1. + C3 * cubed - C1 * squared + } +); + +declare_easing_function!( + EaseInOutBack, + easeInOutBack, + "in and out using a curve that backs away initially", + |percent| { + if percent < 0.5 { + (squared(2. * percent) * ((C2 + 1.) * 2. * percent - C2)) / 2. + } else { + (squared(2. * percent - 2.) * ((C2 + 1.) * (percent * 2. - 2.) + C2) + 2.) / 2. + } + } +); + +declare_easing_function!( + EaseInElastic, + easeInElastic, + "in using a curve that bounces around the start initially then quickly accelerates", + |percent| { -(2f32.powf(10. * percent - 10.) * (percent * 10. - 10.75).sin() * C4) } +); + +declare_easing_function!( + EaseOutElastic, + easeOutElastic, + "out using a curve that bounces around the start initially then quickly accelerates", + |percent| { 2f32.powf(-10. * percent) * (percent * 10. - 0.75).sin() * C4 + 1. } +); + +declare_easing_function!( + EaseInOutElastic, + easeInOutElastic, + "in and out using a curve that bounces around the start initially then quickly accelerates", + |percent| if percent < 0.5 { + -(2f32.powf(-20. * percent - 10.) * (percent * 20. - 11.125).sin() * C5 / 2.) + } else { + 2f32.powf(-20. * percent + 10.) * (percent * 20. - 11.125).sin() * C5 / 2. + 1. + } +); + +declare_easing_function!( + EaseInBounce, + easeInBounce, + "in using a curve that bounces progressively closer as it progresses", + |percent| 1. - EaseOutBounce.ease(ZeroToOne(percent)) +); + +declare_easing_function!( + EaseOutBounce, + easeOutBounce, + "out using a curve that bounces progressively closer as it progresses", + |percent| { + const N1: f32 = 7.5625; + const D1: f32 = 2.75; + + if percent < 1. / D1 { + N1 * percent * percent + } else if percent < 2. / D1 { + let percent = percent - 1.5; + N1 * (percent / D1) * percent + 0.75 + } else if percent < 2.5 / D1 { + let percent = percent - 2.25; + N1 * (percent / D1) * percent + 0.9375 + } else { + let percent = percent - 2.625; + N1 * (percent / D1) * percent + 0.984_375 + } + } +); diff --git a/src/context.rs b/src/context.rs index 91aa27a..f90d196 100644 --- a/src/context.rs +++ b/src/context.rs @@ -7,7 +7,7 @@ use kludgine::app::winit::event::{ DeviceId, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; use kludgine::figures::units::{Px, UPx}; -use kludgine::figures::{Point, Rect, Size}; +use kludgine::figures::{IntoSigned, Point, Rect, Size}; use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::Kludgine; @@ -50,7 +50,7 @@ impl<'context, 'window> EventContext<'context, 'window> { /// appropriate function to invoke the event. pub fn for_other<'child>( &'child mut self, - widget: &'child ManagedWidget, + widget: ManagedWidget, ) -> EventContext<'child, 'window> { EventContext::new(self.widget.for_other(widget), self.kludgine) } @@ -59,6 +59,7 @@ impl<'context, 'window> EventContext<'context, 'window> { /// context's widget and returns the result. pub fn hit_test(&mut self, location: Point) -> bool { self.current_node + .clone() .lock() .as_widget() .hit_test(location, self) @@ -73,6 +74,7 @@ impl<'context, 'window> EventContext<'context, 'window> { button: MouseButton, ) -> EventHandling { self.current_node + .clone() .lock() .as_widget() .mouse_down(location, device_id, button, self) @@ -82,6 +84,7 @@ impl<'context, 'window> EventContext<'context, 'window> { /// this context's widget and returns the result. pub fn mouse_drag(&mut self, location: Point, device_id: DeviceId, button: MouseButton) { self.current_node + .clone() .lock() .as_widget() .mouse_drag(location, device_id, button, self); @@ -96,6 +99,7 @@ impl<'context, 'window> EventContext<'context, 'window> { button: MouseButton, ) { self.current_node + .clone() .lock() .as_widget() .mouse_up(location, device_id, button, self); @@ -109,16 +113,18 @@ impl<'context, 'window> EventContext<'context, 'window> { input: KeyEvent, is_synthetic: bool, ) -> EventHandling { - self.current_node - .lock() - .as_widget() - .keyboard_input(device_id, input, is_synthetic, self) + self.current_node.clone().lock().as_widget().keyboard_input( + device_id, + input, + is_synthetic, + self, + ) } /// Invokes [`Widget::ime()`](crate::widget::Widget::ime) on this /// context's widget and returns the result. pub fn ime(&mut self, ime: Ime) -> EventHandling { - self.current_node.lock().as_widget().ime(ime, self) + self.current_node.clone().lock().as_widget().ime(ime, self) } /// Invokes [`Widget::mouse_wheel()`](crate::widget::Widget::mouse_wheel) on this @@ -130,19 +136,20 @@ impl<'context, 'window> EventContext<'context, 'window> { phase: TouchPhase, ) -> EventHandling { self.current_node + .clone() .lock() .as_widget() .mouse_wheel(device_id, delta, phase, self) } pub(crate) fn hover(&mut self, location: Point) { - let changes = self.current_node.tree.hover(Some(self.current_node)); + let changes = self.current_node.tree.hover(Some(&self.current_node)); for unhovered in changes.unhovered { - let mut context = self.for_other(&unhovered); + let mut context = self.for_other(unhovered.clone()); unhovered.lock().as_widget().unhover(&mut context); } for hover in changes.hovered { - let mut context = self.for_other(&hover); + let mut context = self.for_other(hover.clone()); hover.lock().as_widget().hover(location, &mut context); } } @@ -152,7 +159,7 @@ impl<'context, 'window> EventContext<'context, 'window> { assert!(changes.hovered.is_empty()); for old_hover in changes.unhovered { - let mut old_hover_context = self.for_other(&old_hover); + let mut old_hover_context = self.for_other(old_hover.clone()); old_hover.lock().as_widget().unhover(&mut old_hover_context); } } @@ -163,7 +170,7 @@ impl<'context, 'window> EventContext<'context, 'window> { let new = match self.current_node.tree.activate(active.as_ref()) { Ok(old) => { if let Some(old) = old { - let mut old_context = self.for_other(&old); + let mut old_context = self.for_other(old.clone()); old.lock().as_widget().deactivate(&mut old_context); } true @@ -175,7 +182,7 @@ impl<'context, 'window> EventContext<'context, 'window> { active .lock() .as_widget() - .activate(&mut self.for_other(&active)); + .activate(&mut self.for_other(active.clone())); } } } @@ -185,7 +192,7 @@ impl<'context, 'window> EventContext<'context, 'window> { let new = match self.current_node.tree.focus(focus.as_ref()) { Ok(old) => { if let Some(old) = old { - let mut old_context = self.for_other(&old); + let mut old_context = self.for_other(old.clone()); old.lock().as_widget().blur(&mut old_context); } true @@ -194,7 +201,10 @@ impl<'context, 'window> EventContext<'context, 'window> { }; if new { if let Some(focus) = focus { - focus.lock().as_widget().focus(&mut self.for_other(&focus)); + focus + .lock() + .as_widget() + .focus(&mut self.for_other(focus.clone())); } } } @@ -254,31 +264,28 @@ pub struct GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> { } impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass> { - /// Returns a new `GraphicsContext` that allows invoking graphics functions - /// for `widget`. - pub fn for_other<'child>( - &'child mut self, - widget: &'child ManagedWidget, - ) -> GraphicsContext<'child, 'window, 'clip, 'gfx, 'pass> { + /// Returns a new instance that borrows from `self`. + pub fn borrowed(&mut self) -> GraphicsContext<'_, 'window, 'clip, 'gfx, 'pass> { GraphicsContext { - widget: self.widget.for_other(widget), - graphics: Exclusive::Borrowed(&mut *self.graphics), + widget: self.widget.borrowed(), + graphics: Exclusive::Borrowed(&mut self.graphics), } } /// Returns a new `GraphicsContext` that allows invoking graphics functions - /// for `widget` and restricts the drawing area to `region`. - /// - /// This is equivalent to calling - /// `self.for_other(widget).clipped_to(region)`. - pub fn for_child<'child>( + /// for `widget`. + pub fn for_other<'child>( &'child mut self, - widget: &'child ManagedWidget, - region: Rect, + widget: ManagedWidget, ) -> GraphicsContext<'child, 'window, 'child, 'gfx, 'pass> { + let widget = self.widget.for_other(widget); + let layout = widget.last_layout().map_or_else( + || Rect::from(self.graphics.clip_rect().size).into_signed(), + |rect| rect - self.graphics.region().origin, + ); GraphicsContext { - widget: self.widget.for_other(widget), - graphics: Exclusive::Owned(self.graphics.clipped_to(region)), + widget, + graphics: Exclusive::Owned(self.graphics.clipped_to(layout)), } } @@ -310,20 +317,10 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' self.draw_focus_ring_using(&self.query_styles(&[&HighlightColor])); } - /// Invokes [`Widget::measure()`](crate::widget::Widget::measure) on this - /// context's widget and returns the result. - pub fn measure(&mut self, available_space: Size) -> Size { - self.current_node - .lock() - .as_widget() - .measure(available_space, self) - } - /// Invokes [`Widget::redraw()`](crate::widget::Widget::redraw) on this /// context's widget. pub fn redraw(&mut self) { - self.current_node.note_rendered_rect(self.graphics.region()); - self.current_node.lock().as_widget().redraw(self); + self.current_node.clone().lock().as_widget().redraw(self); } } @@ -345,6 +342,92 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> DerefMut } } +/// A context to a function that is rendering a widget. +pub struct LayoutContext<'context, 'window, 'clip, 'gfx, 'pass> { + graphics: GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>, + persist_layout: bool, +} + +impl<'context, 'window, 'clip, 'gfx, 'pass> LayoutContext<'context, 'window, 'clip, 'gfx, 'pass> { + pub(crate) fn new( + graphics: &'context mut GraphicsContext<'_, 'window, 'clip, 'gfx, 'pass>, + ) -> Self { + Self { + graphics: graphics.borrowed(), + persist_layout: true, + } + } + + /// Returns a new layout context that does not persist any child layout + /// operations. + /// + /// This type of context is useful for asking widgets to measuree themselves + /// in hypothetical layout conditions while trying to determine the best + /// layout for a composite control. + #[must_use] + pub fn as_temporary(mut self) -> Self { + self.persist_layout = false; + self + } + + /// Returns a new `LayoutContext` that allows invoking layout functions for + /// `widget`. + pub fn for_other<'child, 'widget>( + &'child mut self, + widget: ManagedWidget, + ) -> LayoutContext<'child, 'window, 'child, 'gfx, 'pass> + where + 'widget: 'child, + { + LayoutContext { + graphics: self.graphics.for_other(widget), + persist_layout: self.persist_layout, + } + } + + /// Invokes [`Widget::layout()`](crate::widget::Widget::layout) on this + /// context's widget and returns the result. + pub fn layout(&mut self, available_space: Size) -> Size { + if self.persist_layout { + self.graphics.current_node.reset_child_layouts(); + } + self.graphics + .current_node + .clone() + .lock() + .as_widget() + .layout(available_space, self) + } + + /// Sets the layout for `child` to `layout`. + /// + /// `layout` is relative to the current widget's controls. + pub fn set_child_layout(&mut self, child: &ManagedWidget, layout: Rect) { + // TODO verify that `child` belongs to the current node. + if self.persist_layout { + child.set_layout(layout); + } + } +} + +impl<'context, 'window, 'clip, 'gfx, 'pass> Deref + for LayoutContext<'context, 'window, 'clip, 'gfx, 'pass> +{ + type Target = GraphicsContext<'context, 'window, 'clip, 'gfx, 'pass>; + + fn deref(&self) -> &Self::Target { + &self.graphics + } +} + +impl<'context, 'window, 'clip, 'gfx, 'pass> DerefMut + for LayoutContext<'context, 'window, 'clip, 'gfx, 'pass> +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.graphics + } +} + /// Converts from one context to an [`EventContext`]. pub trait AsEventContext<'window> { /// Returns this context as an [`EventContext`]. @@ -358,11 +441,11 @@ pub trait AsEventContext<'window> { let pushed_widget = context .current_node .tree - .push_boxed(child, Some(context.current_node)); + .push_boxed(child, Some(&context.current_node)); pushed_widget .lock() .as_widget() - .mounted(&mut context.for_other(&pushed_widget)); + .mounted(&mut context.for_other(pushed_widget.clone())); pushed_widget } @@ -372,11 +455,11 @@ pub trait AsEventContext<'window> { context .current_node .tree - .remove_child(child, context.current_node); + .remove_child(child, &context.current_node); child .lock() .as_widget() - .unmounted(&mut context.for_other(child)); + .unmounted(&mut context.for_other(child.clone())); } } @@ -397,7 +480,7 @@ impl<'window> AsEventContext<'window> for GraphicsContext<'_, 'window, '_, '_, ' /// This type provides access to the widget hierarchy from the perspective of a /// specific widget. pub struct WidgetContext<'context, 'window> { - current_node: &'context ManagedWidget, + current_node: ManagedWidget, redraw_status: &'context RedrawStatus, window: &'context mut RunningWindow<'window>, pending_state: PendingState<'context>, @@ -405,14 +488,11 @@ pub struct WidgetContext<'context, 'window> { impl<'context, 'window> WidgetContext<'context, 'window> { pub(crate) fn new( - current_node: &'context ManagedWidget, + current_node: ManagedWidget, redraw_status: &'context RedrawStatus, window: &'context mut RunningWindow<'window>, ) -> Self { Self { - current_node, - redraw_status, - window, pending_state: PendingState::Owned(PendingWidgetState { focus: current_node .tree @@ -423,13 +503,16 @@ impl<'context, 'window> WidgetContext<'context, 'window> { .active_widget() .map(|id| current_node.tree.widget(id)), }), + current_node, + redraw_status, + window, } } /// Returns a new instance that borrows from `self`. pub fn borrowed(&mut self) -> WidgetContext<'_, 'window> { WidgetContext { - current_node: self.current_node, + current_node: self.current_node.clone(), redraw_status: self.redraw_status, window: &mut *self.window, pending_state: self.pending_state.borrowed(), @@ -439,7 +522,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { /// Returns a new context representing `widget`. pub fn for_other<'child>( &'child mut self, - widget: &'child ManagedWidget, + widget: ManagedWidget, ) -> WidgetContext<'child, 'window> { WidgetContext { current_node: widget, @@ -458,10 +541,10 @@ impl<'context, 'window> WidgetContext<'context, 'window> { value.redraw_when_changed(self.handle()); } - /// Returns the region that this widget last rendered at. + /// Returns the last layout of this widget. #[must_use] - pub fn last_rendered_at(&self) -> Option> { - self.current_node.last_rendered_at() + pub fn last_layout(&self) -> Option> { + self.current_node.last_layout() } /// Sets the currently focused widget to this widget. @@ -503,7 +586,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { .pending_state .active .as_ref() - .map_or(true, |active| active != self.current_node) + .map_or(true, |active| active != &self.current_node) { self.pending_state.active = Some(self.current_node.clone()); true @@ -535,7 +618,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { /// Returns true if this widget is currently the active widget. #[must_use] pub fn active(&self) -> bool { - self.pending_state.active.as_ref() == Some(self.current_node) + self.pending_state.active.as_ref() == Some(&self.current_node) } /// Returns true if this widget is currently hovered, even if the cursor is @@ -554,13 +637,13 @@ impl<'context, 'window> WidgetContext<'context, 'window> { /// Returns true if this widget is currently focused for user input. #[must_use] pub fn focused(&self) -> bool { - self.pending_state.focus.as_ref() == Some(self.current_node) + self.pending_state.focus.as_ref() == Some(&self.current_node) } /// Returns the widget this context is for. #[must_use] pub const fn widget(&self) -> &ManagedWidget { - self.current_node + &self.current_node } /// Attaches `styles` to the widget hierarchy for this widget. @@ -585,7 +668,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { pub fn query_styles(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles { self.current_node .tree - .query_styles(self.current_node, query) + .query_styles(&self.current_node, query) } /// Queries the widget hierarchy for a single style component. @@ -599,7 +682,9 @@ impl<'context, 'window> WidgetContext<'context, 'window> { &self, query: &Component, ) -> Component::ComponentType { - self.current_node.tree.query_style(self.current_node, query) + self.current_node + .tree + .query_style(&self.current_node, query) } pub(crate) fn handle(&self) -> WindowHandle { diff --git a/src/lib.rs b/src/lib.rs index 253613a..389dac8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,13 +18,11 @@ pub mod widgets; pub mod window; use std::ops::Sub; -pub use with_clone::WithClone; -mod with_clone; - pub use kludgine; use kludgine::app::winit::error::EventLoopError; use kludgine::figures::units::UPx; pub use names::Name; +pub use utils::WithClone; pub use self::graphics::Graphics; pub use self::tick::{InputState, Tick}; @@ -73,20 +71,20 @@ pub trait Run: Sized { fn run(self) -> crate::Result; } -/// Creates a [`Widgets`](crate::widget::Widgets) instance with the given list +/// Creates a [`Children`](crate::widget::Children) instance with the given list /// of widgets. #[macro_export] -macro_rules! widgets { +macro_rules! children { () => { - $crate::widget::Widgets::new() + $crate::widget::Children::new() }; ($($widget:expr),+) => {{ - let mut widgets = $crate::widget::Widgets::with_capacity($crate::count!($($widget),+ ;)); + let mut widgets = $crate::widget::Children::with_capacity($crate::count!($($widget),+ ;)); $(widgets.push($widget);)+ widgets }}; ($($widget:expr),+ ,) => {{ - $crate::widgets!($($widget),+) + $crate::children!($($widget),+) }}; } diff --git a/src/styles/components.rs b/src/styles/components.rs index d4a147e..d978d09 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -4,7 +4,8 @@ use std::borrow::Cow; use kludgine::figures::units::Lp; use kludgine::Color; -use crate::animation::{EaseInQuadradic, EaseOutQuadradic, EasingFunction}; +use crate::animation::easings::{EaseInQuadradic, EaseOutQuadradic}; +use crate::animation::EasingFunction; use crate::styles::{ComponentDefinition, ComponentName, Dimension, Global, NamedComponent}; /// The [`Dimension`] to use as the size to render text. diff --git a/src/tree.rs b/src/tree.rs index e474728..aabe7b9 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -25,7 +25,7 @@ impl Tree { widget: widget.clone(), children: Vec::new(), parent: parent.map(|parent| parent.id), - last_rendered_location: None, + layout: None, styles: None, })); if let Some(parent) = parent { @@ -44,16 +44,23 @@ impl Tree { data.remove_child(child.id, parent.id); } - pub(crate) fn note_rendered_rect(&self, widget: WidgetId, rect: Rect) { + pub(crate) fn set_layout(&self, widget: WidgetId, rect: Rect) { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); rect.extents(); - data.nodes[widget.0].last_rendered_location = Some(rect); + data.nodes[widget.0].layout = Some(rect); data.render_order.push(widget); + let mut children_to_offset = data.nodes[widget.0].children.clone(); + while let Some(child) = children_to_offset.pop() { + if let Some(layout) = &mut data.nodes[child.0].layout { + layout.origin += rect.origin; + children_to_offset.extend(data.nodes[child.0].children.iter().copied()); + } + } } - pub(crate) fn last_rendered_at(&self, widget: WidgetId) -> Option> { + pub(crate) fn layout(&self, widget: WidgetId) -> Option> { let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); - data.nodes[widget.0].last_rendered_location + data.nodes[widget.0].layout } pub(crate) fn reset_render_order(&self) { @@ -61,6 +68,14 @@ impl Tree { data.render_order.clear(); } + pub(crate) fn reset_child_layouts(&self, parent: WidgetId) { + let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + let children = data.nodes[parent.0].children.clone(); + for child in children { + data.nodes[child.0].layout = None; + } + } + pub(crate) fn hover(&self, new_hover: Option<&ManagedWidget>) -> HoverResults { let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); let hovered = new_hover @@ -139,7 +154,7 @@ impl Tree { let data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); let mut hits = Vec::new(); for id in data.render_order.iter().rev() { - if let Some(last_rendered) = data.nodes[id.0].last_rendered_location { + if let Some(last_rendered) = data.nodes[id.0].layout { if last_rendered.contains(point) { hits.push(ManagedWidget { id: *id, @@ -315,7 +330,7 @@ pub struct Node { pub widget: WidgetInstance, pub children: Vec, pub parent: Option, - pub last_rendered_location: Option>, + pub layout: Option>, pub styles: Option, } diff --git a/src/utils.rs b/src/utils.rs index 06a953a..1e82ab6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,6 +4,56 @@ use std::sync::OnceLock; use kludgine::app::winit::event::Modifiers; use kludgine::app::winit::keyboard::ModifiersState; +/// Invokes the provided macro with a pattern that can be matched using this +/// `macro_rules!` expression: `$($type:ident $field:tt),+`, where `$type` is an +/// identifier to use for the generic parameter and `$field` is the field index +/// inside of the tuple. +macro_rules! impl_all_tuples { + ($macro_name:ident) => { + $macro_name!(T0 0); + $macro_name!(T0 0, T1 1); + $macro_name!(T0 0, T1 1, T2 2); + $macro_name!(T0 0, T1 1, T2 2, T3 3); + $macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4); + $macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4, T5 5); + } +} + +/// Invokes a function with a clone of `self`. +pub trait WithClone: Sized { + /// The type that results from cloning. + type Cloned; + + /// Maps `with` with the results of cloning `self`. + fn with_clone(&self, with: impl FnOnce(Self::Cloned) -> R) -> R; +} + +macro_rules! impl_with_clone { + ($($name:ident $field:tt),+) => { + impl<'a, $($name: Clone,)+> WithClone for ($(&'a $name,)+) + { + type Cloned = ($($name,)+); + + fn with_clone(&self, with: impl FnOnce(Self::Cloned) -> R) -> R { + with(($(self.$field.clone(),)+)) + } + } + }; +} + +impl<'a, T> WithClone for &'a T +where + T: Clone, +{ + type Cloned = T; + + fn with_clone(&self, with: impl FnOnce(Self::Cloned) -> R) -> R { + with((*self).clone()) + } +} + +impl_all_tuples!(impl_with_clone); + pub trait ModifiersExt { fn primary(&self) -> bool; fn word_select(&self) -> bool; @@ -62,18 +112,3 @@ impl Deref for Lazy { self.once.get_or_init(self.init) } } - -/// Invokes the provided macro with a pattern that can be matched using this -/// `macro_rules!` expression: `$($type:ident $field:tt),+`, where `$type` is an -/// identifier to use for the generic parameter and `$field` is the field index -/// inside of the tuple. -macro_rules! impl_all_tuples { - ($macro_name:ident) => { - $macro_name!(T0 0); - $macro_name!(T0 0, T1 1); - $macro_name!(T0 0, T1 1, T2 2); - $macro_name!(T0 0, T1 1, T2 2, T3 3); - $macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4); - $macro_name!(T0 0, T1 1, T2 2, T3 3, T4 4, T5 5); - } -} diff --git a/src/value.rs b/src/value.rs index 89db5be..3c5064b 100644 --- a/src/value.rs +++ b/src/value.rs @@ -463,7 +463,7 @@ impl Value { } /// Maps the current contents to `map` and returns the result. - pub fn map(&mut self, map: impl FnOnce(&T) -> R) -> R { + pub fn map(&self, map: impl FnOnce(&T) -> R) -> R { match self { Value::Constant(value) => map(value), Value::Dynamic(dynamic) => dynamic.map_ref(map), @@ -504,6 +504,17 @@ impl Value { } } } +impl Clone for Value +where + T: Clone, +{ + fn clone(&self) -> Self { + match self { + Self::Constant(arg0) => Self::Constant(arg0.clone()), + Self::Dynamic(arg0) => Self::Dynamic(arg0.clone()), + } + } +} impl Default for Value where @@ -543,3 +554,9 @@ impl IntoValue for Value { self } } + +impl IntoValue> for T { + fn into_value(self) -> Value> { + Value::Constant(Some(self)) + } +} diff --git a/src/widget.rs b/src/widget.rs index 16356f5..c27d16a 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -13,9 +13,10 @@ use kludgine::app::winit::event::{ use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{Point, Rect, Size}; -use crate::context::{AsEventContext, EventContext, GraphicsContext}; +use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::styles::Styles; use crate::tree::{Tree, WidgetId}; +use crate::value::{IntoValue, Value}; use crate::widgets::Style; use crate::window::{RunningWindow, Window, WindowBehavior}; use crate::{ConstraintLimit, Run}; @@ -28,12 +29,12 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { /// Redraw the contents of this widget. fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>); - /// Measure this widget and returns the ideal size based on its contents and + /// Layout this widget and returns the ideal size based on its contents and /// the `available_space`. - fn measure( + fn layout( &mut self, available_space: Size, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size; /// The widget has been mounted into a parent widget. @@ -58,6 +59,14 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { #[allow(unused_variables)] fn unhover(&mut self, context: &mut EventContext<'_, '_>) {} + /// This widget has been targeted to be focused. If this function returns + /// true, the widget will be focused. If false, Gooey will continue + /// searching for another focus target. + #[allow(unused_variables)] + fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool { + false + } + /// The widget has received focus for user input. #[allow(unused_variables)] fn focus(&mut self, context: &mut EventContext<'_, '_>) {} @@ -145,6 +154,21 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { ) -> EventHandling { IGNORED } +} + +impl Run for T +where + T: MakeWidget, +{ + fn run(self) -> crate::Result { + self.make_widget().run() + } +} + +/// A type that can create a widget. +pub trait MakeWidget: Sized { + /// Returns a new widget. + fn make_widget(self) -> WidgetInstance; /// Associates `styles` with this widget. /// @@ -155,25 +179,13 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { { Style::new(styles, self) } -} -impl Run for T -where - T: Widget, -{ - fn run(self) -> crate::Result { - WidgetInstance::new(self).run() - } -} - -/// A type that can create a widget. -pub trait MakeWidget: Sized { - /// Returns a new widget. - fn make_widget(self) -> WidgetInstance; - - /// Runs the widget this type creates as an application. - fn run(self) -> crate::Result { - self.make_widget().run() + /// Sets the widget that should be focused next. + /// + /// Gooey automatically determines reverse tab order by using this same + /// relationship. + fn with_next_focus(self, next_focus: impl IntoValue>) -> WidgetInstance { + self.make_widget().with_next_focus(next_focus) } } @@ -186,6 +198,12 @@ where } } +impl MakeWidget for WidgetInstance { + fn make_widget(self) -> WidgetInstance { + self + } +} + /// A type that represents whether an event has been handled or ignored. pub type EventHandling = ControlFlow; @@ -223,7 +241,10 @@ where /// An instance of a [`Widget`]. #[derive(Clone, Debug)] -pub struct WidgetInstance(Arc>); +pub struct WidgetInstance { + widget: Arc>, + next_focus: Value>>>, +} impl WidgetInstance { /// Returns a new instance containing `widget`. @@ -231,19 +252,46 @@ impl WidgetInstance { where W: Widget, { - Self(Arc::new(Mutex::new(widget))) + Self { + widget: Arc::new(Mutex::new(widget)), + next_focus: Value::default(), + } + } + + /// Sets the widget that should be focused next. + /// + /// Gooey automatically determines reverse tab order by using this same + /// relationship. + #[must_use] + pub fn with_next_focus( + mut self, + next_focus: impl IntoValue>, + ) -> WidgetInstance { + self.next_focus = match next_focus.into_value() { + Value::Constant(maybe_widget) => { + Value::Constant(maybe_widget.map(|widget| widget.widget)) + } + Value::Dynamic(dynamic) => Value::Dynamic( + dynamic + .map_each(|instance| instance.as_ref().map(|instance| instance.widget.clone())), + ), + }; + self } /// Locks the widget for exclusive access. Locking widgets should only be /// done for brief moments of time when you are certain no deadlocks can /// occur due to other widget locks being held. pub fn lock(&self) -> WidgetGuard<'_> { - WidgetGuard(self.0.lock().map_or_else(PoisonError::into_inner, |g| g)) + WidgetGuard( + self.widget + .lock() + .map_or_else(PoisonError::into_inner, |g| g), + ) } -} -impl Run for WidgetInstance { - fn run(self) -> crate::Result { + /// Runs this widget instance as an application. + pub fn run(self) -> crate::Result { Window::::new(self).run() } } @@ -252,7 +300,7 @@ impl Eq for WidgetInstance {} impl PartialEq for WidgetInstance { fn eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.0, &other.0) + Arc::ptr_eq(&self.widget, &other.widget) } } @@ -336,14 +384,14 @@ impl ManagedWidget { self.widget.lock() } - pub(crate) fn note_rendered_rect(&self, rect: Rect) { - self.tree.note_rendered_rect(self.id, rect); + pub(crate) fn set_layout(&self, rect: Rect) { + self.tree.set_layout(self.id, rect); } /// Returns the region that the widget was last rendered at. #[must_use] - pub fn last_rendered_at(&self) -> Option> { - self.tree.last_rendered_at(self.id) + pub fn last_layout(&self) -> Option> { + self.tree.layout(self.id) } /// Returns true if this widget is the currently active widget. @@ -379,6 +427,10 @@ impl ManagedWidget { pub(crate) fn attach_styles(&self, styles: Styles) { self.tree.attach_styles(self.id, styles); } + + pub(crate) fn reset_child_layouts(&self) { + self.tree.reset_child_layouts(self.id); + } } impl PartialEq for ManagedWidget { @@ -426,11 +478,11 @@ impl WidgetGuard<'_> { /// A list of [`Widget`]s. #[derive(Debug, Default)] #[must_use] -pub struct Widgets { +pub struct Children { ordered: Vec, } -impl Widgets { +impl Children { /// Returns an empty list. pub const fn new() -> Self { Self { @@ -476,7 +528,7 @@ impl Widgets { } } -impl FromIterator for Widgets +impl FromIterator for Children where W: MakeWidget, { @@ -487,7 +539,7 @@ where } } -impl Deref for Widgets { +impl Deref for Children { type Target = [WidgetInstance]; fn deref(&self) -> &Self::Target { @@ -497,14 +549,14 @@ impl Deref for Widgets { /// A child widget #[derive(Debug, Clone)] -pub enum ChildWidget { +pub enum WidgetRef { /// An unmounted child widget Unmounted(WidgetInstance), /// A mounted child widget Mounted(ManagedWidget), } -impl ChildWidget { +impl WidgetRef { /// Returns a new unmounted child pub fn new(widget: impl MakeWidget) -> Self { Self::Unmounted(widget.make_widget()) @@ -512,8 +564,8 @@ impl ChildWidget { /// Returns this child, mounting it in the process if necessary. pub fn mounted(&mut self, context: &mut EventContext<'_, '_>) -> ManagedWidget { - if let ChildWidget::Unmounted(instance) = self { - *self = ChildWidget::Mounted(context.push_child(instance.clone())); + if let WidgetRef::Unmounted(instance) = self { + *self = WidgetRef::Mounted(context.push_child(instance.clone())); } let Self::Mounted(widget) = self else { diff --git a/src/widgets/align.rs b/src/widgets/align.rs index 44a6d9d..2e7dfa3 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -3,16 +3,16 @@ use std::fmt::Debug; use kludgine::figures::units::UPx; use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; -use crate::context::{AsEventContext, GraphicsContext}; +use crate::context::{AsEventContext, GraphicsContext, LayoutContext}; use crate::styles::{Edges, FlexibleDimension}; use crate::value::{IntoValue, Value}; -use crate::widget::{ChildWidget, MakeWidget, Widget}; +use crate::widget::{MakeWidget, Widget, WidgetRef}; use crate::ConstraintLimit; /// A widget aligns its contents to its container's boundaries. #[derive(Debug)] pub struct Align { - child: ChildWidget, + child: WidgetRef, edges: Value>, } @@ -21,7 +21,7 @@ impl Align { /// `margin`. pub fn new(margin: impl IntoValue>, widget: impl MakeWidget) -> Self { Self { - child: ChildWidget::new(widget), + child: WidgetRef::new(widget), edges: margin.into_value(), } } @@ -35,7 +35,7 @@ impl Align { fn measure( &mut self, available_space: Size, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Layout { let margin = self.edges.get(); let vertical = FrameInfo::new(context.graphics.scale(), margin.top, margin.bottom); @@ -47,7 +47,7 @@ impl Align { ); let child = self.child.mounted(&mut context.as_event_context()); - let content_size = context.for_other(&child).measure(content_available); + let content_size = context.for_other(child).layout(content_available); let (left, right, width) = horizontal.measure(available_space.width, content_size.width); let (top, bottom, height) = vertical.measure(available_space.height, content_size.height); @@ -127,36 +127,29 @@ impl FrameInfo { impl Widget for Align { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - let layout = self.measure( - Size::new( - ConstraintLimit::Known(context.graphics.size().width), - ConstraintLimit::Known(context.graphics.size().height), - ), - context, - ); let child = self.child.mounted(&mut context.as_event_context()); - context - .for_child( - &child, - Rect::new( - Point::new( - layout.margin.left.into_signed(), - layout.margin.top.into_signed(), - ), - layout.content.into_signed(), - ), - ) - .redraw(); + context.for_other(child).redraw(); } - fn measure( + fn layout( &mut self, available_space: Size, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { - self.measure(available_space, context) - .size() - .into_unsigned() + let child = self.child.mounted(&mut context.as_event_context()); + let layout = self.measure(available_space, context); + context.set_child_layout( + &child, + Rect::new( + Point::new( + layout.margin.left.into_signed(), + layout.margin.top.into_signed(), + ), + layout.content.into_signed(), + ), + ); + + layout.size().into_unsigned() } } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 91391a1..4965e40 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -12,7 +12,7 @@ use kludgine::text::Text; use kludgine::Color; use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; -use crate::context::{EventContext, GraphicsContext, WidgetContext}; +use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::names::Name; use crate::styles::components::{Easing, HighlightColor, IntrinsicPadding, TextColor}; use crate::styles::{ComponentDefinition, ComponentGroup, ComponentName, NamedComponent}; @@ -167,13 +167,8 @@ impl Widget for Button { _button: MouseButton, context: &mut EventContext<'_, '_>, ) { - let changed = if Rect::from( - context - .last_rendered_at() - .expect("must have been rendered") - .size, - ) - .contains(location) + let changed = if Rect::from(context.last_layout().expect("must have been rendered").size) + .contains(location) { context.activate() } else { @@ -197,13 +192,8 @@ impl Widget for Button { context.deactivate(); if let Some(location) = location { - if Rect::from( - context - .last_rendered_at() - .expect("must have been rendered") - .size, - ) - .contains(location) + if Rect::from(context.last_layout().expect("must have been rendered").size) + .contains(location) { context.focus(); @@ -213,10 +203,10 @@ impl Widget for Button { } } - fn measure( + fn layout( &mut self, available_space: Size, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { let padding = context .query_style(&IntrinsicPadding) diff --git a/src/widgets/canvas.rs b/src/widgets/canvas.rs index ebc0b5e..dd8312d 100644 --- a/src/widgets/canvas.rs +++ b/src/widgets/canvas.rs @@ -4,7 +4,7 @@ use std::panic::UnwindSafe; use kludgine::figures::units::UPx; use kludgine::figures::Size; -use crate::context::GraphicsContext; +use crate::context::{GraphicsContext, LayoutContext}; use crate::value::Dynamic; use crate::widget::Widget; use crate::Tick; @@ -50,10 +50,10 @@ impl Widget for Canvas { } } - fn measure( + fn layout( &mut self, available_space: Size, - _context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + _context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { Size::new(available_space.width.max(), available_space.height.max()) } diff --git a/src/widgets/expand.rs b/src/widgets/expand.rs index be78230..eddb348 100644 --- a/src/widgets/expand.rs +++ b/src/widgets/expand.rs @@ -1,8 +1,8 @@ use kludgine::figures::units::UPx; -use kludgine::figures::Size; +use kludgine::figures::{IntoSigned, Rect, Size}; -use crate::context::{AsEventContext, GraphicsContext}; -use crate::widget::{ChildWidget, MakeWidget, Widget}; +use crate::context::{AsEventContext, GraphicsContext, LayoutContext}; +use crate::widget::{MakeWidget, Widget, WidgetRef}; use crate::ConstraintLimit; /// A widget that expands its child widget to fill the parent. @@ -14,7 +14,7 @@ pub struct Expand { /// The weight to use when splitting available space with multiple /// [`Expand`] widgets. pub weight: u8, - child: ChildWidget, + child: WidgetRef, } impl Expand { @@ -22,7 +22,7 @@ impl Expand { #[must_use] pub fn new(child: impl MakeWidget) -> Self { Self { - child: ChildWidget::new(child), + child: WidgetRef::new(child), weight: 1, } } @@ -34,14 +34,14 @@ impl Expand { #[must_use] pub fn weighted(weight: u8, child: impl MakeWidget) -> Self { Self { - child: ChildWidget::new(child), + child: WidgetRef::new(child), weight, } } /// Returns a reference to the child widget. #[must_use] - pub fn child(&self) -> &ChildWidget { + pub fn child(&self) -> &WidgetRef { &self.child } } @@ -49,19 +49,21 @@ impl Expand { impl Widget for Expand { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { let child = self.child.mounted(&mut context.as_event_context()); - context.for_other(&child).redraw(); + context.for_other(child).redraw(); } - fn measure( + fn layout( &mut self, available_space: Size, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { let available_space = Size::new( ConstraintLimit::Known(available_space.width.max()), ConstraintLimit::Known(available_space.height.max()), ); let child = self.child.mounted(&mut context.as_event_context()); - context.for_other(&child).measure(available_space) + let size = context.for_other(child.clone()).layout(available_space); + context.set_child_layout(&child, Rect::from(size.into_signed())); + size } } diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 4670ceb..15c0a50 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -6,7 +6,7 @@ use std::time::Duration; use kludgine::app::winit::event::{Ime, KeyEvent}; use kludgine::app::winit::keyboard::Key; use kludgine::cosmic_text::{Action, Attrs, Buffer, Cursor, Edit, Editor, Metrics, Shaping}; -use kludgine::figures::units::Px; +use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{ FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, }; @@ -14,12 +14,13 @@ use kludgine::shapes::Shape; use kludgine::text::TextOrigin; use kludgine::{Color, Kludgine}; -use crate::context::{EventContext, WidgetContext}; +use crate::context::{EventContext, LayoutContext, WidgetContext}; use crate::styles::components::{HighlightColor, LineHeight, TextColor, TextSize}; use crate::styles::Styles; use crate::utils::ModifiersExt; use crate::value::{Generation, IntoValue, Value}; use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED}; +use crate::ConstraintLimit; const CURSOR_BLINK_DURATION: Duration = Duration::from_millis(500); @@ -303,11 +304,11 @@ impl Widget for Input { ); } - fn measure( + fn layout( &mut self, - available_space: kludgine::figures::Size, - context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>, - ) -> kludgine::figures::Size { + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { let styles = context.query_styles(&[&TextColor]); let editor = self.editor_mut(&mut context.graphics, &styles); let buffer = editor.buffer_mut(); diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 7b0ffcf..0c646f7 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -2,10 +2,11 @@ use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoUnsigned, Point, ScreenScale, Size}; use kludgine::text::{MeasuredText, Text, TextOrigin}; -use crate::context::GraphicsContext; +use crate::context::{GraphicsContext, LayoutContext}; use crate::styles::components::{IntrinsicPadding, TextColor}; use crate::value::{IntoValue, Value}; use crate::widget::Widget; +use crate::ConstraintLimit; /// A read-only text widget. #[derive(Debug)] @@ -51,10 +52,10 @@ impl Widget for Label { } } - fn measure( + fn layout( &mut self, - available_space: Size, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { let padding = context .query_style(&IntrinsicPadding) @@ -64,11 +65,11 @@ impl Widget for Label { self.text.map(|contents| { let measured = context .graphics - .measure_text(Text::from(contents).wrap_at(width)); + .measure_text(Text::from(contents).wrap_at(dbg!(width))); let mut size = measured.size.try_cast().unwrap_or_default(); size += padding * 2; self.prepared_text = Some(measured); - size + dbg!(size) }) } } diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index 2914aa2..39e5925 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -1,9 +1,9 @@ use kludgine::figures::units::UPx; use kludgine::figures::{Fraction, IntoUnsigned, ScreenScale, Size}; -use crate::context::{AsEventContext, GraphicsContext}; +use crate::context::{AsEventContext, GraphicsContext, LayoutContext}; use crate::styles::Dimension; -use crate::widget::{ChildWidget, MakeWidget, Widget}; +use crate::widget::{MakeWidget, Widget, WidgetRef}; use crate::ConstraintLimit; /// A widget that resizes its contained widget to an explicit size. @@ -13,13 +13,13 @@ pub struct Resize { pub width: Option, /// If present, the height to apply to the child widget. pub height: Option, - child: ChildWidget, + child: WidgetRef, } impl Resize { /// Returns a reference to the child widget. #[must_use] - pub fn child(&self) -> &ChildWidget { + pub fn child(&self) -> &WidgetRef { &self.child } @@ -30,7 +30,7 @@ impl Resize { T: Into, { Self { - child: ChildWidget::new(child), + child: WidgetRef::new(child), width: Some(size.width.into()), height: Some(size.height.into()), } @@ -40,7 +40,7 @@ impl Resize { #[must_use] pub fn width(width: impl Into, child: impl MakeWidget) -> Self { Self { - child: ChildWidget::new(child), + child: WidgetRef::new(child), width: Some(width.into()), height: None, } @@ -50,7 +50,7 @@ impl Resize { #[must_use] pub fn height(height: impl Into, child: impl MakeWidget) -> Self { Self { - child: ChildWidget::new(child), + child: WidgetRef::new(child), width: None, height: Some(height.into()), } @@ -60,13 +60,13 @@ impl Resize { impl Widget for Resize { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { let child = self.child.mounted(&mut context.as_event_context()); - context.for_other(&child).redraw(); + context.for_other(child).redraw(); } - fn measure( + fn layout( &mut self, available_space: Size, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { if let (Some(width), Some(height)) = (self.width, self.height) { Size::new( @@ -83,7 +83,7 @@ impl Widget for Resize { ), ); let child = self.child.mounted(&mut context.as_event_context()); - context.for_other(&child).measure(available_space) + context.for_other(child).layout(available_space) } } } diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index 7f1532b..7860da4 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -18,36 +18,13 @@ use crate::styles::{ ComponentDefinition, ComponentGroup, ComponentName, Dimension, NamedComponent, }; use crate::value::Dynamic; -use crate::widget::{EventHandling, MakeWidget, ManagedWidget, Widget, WidgetInstance, HANDLED}; +use crate::widget::{EventHandling, MakeWidget, Widget, WidgetRef, HANDLED}; use crate::{ConstraintLimit, Name}; -#[derive(Debug)] -enum ChildWidget { - Instance(WidgetInstance), - Managed(ManagedWidget), - Mounting, -} - -impl ChildWidget { - pub fn managed(&mut self, context: &mut EventContext<'_, '_>) -> ManagedWidget { - if matches!(self, ChildWidget::Instance(_)) { - let ChildWidget::Instance(instance) = std::mem::replace(self, ChildWidget::Mounting) - else { - unreachable!("just matched") - }; - *self = ChildWidget::Managed(context.push_child(instance)); - } - let ChildWidget::Managed(managed) = self else { - unreachable!("always converted") - }; - managed.clone() - } -} - /// A widget that supports scrolling its contents. #[derive(Debug)] pub struct Scroll { - contents: ChildWidget, + contents: WidgetRef, content_size: Size, control_size: Size, scroll: Dynamic>, @@ -55,13 +32,16 @@ pub struct Scroll { max_scroll: Dynamic>, scrollbar_opacity: Dynamic, scrollbar_opacity_animation: AnimationHandle, + horizontal_bar: ScrollbarInfo, + vertical_bar: ScrollbarInfo, + bar_width: Px, } impl Scroll { /// Returns a new scroll widget containing `contents`. fn construct(contents: impl MakeWidget, enabled: Point) -> Self { Self { - contents: ChildWidget::Instance(contents.make_widget()), + contents: WidgetRef::new(contents), enabled, content_size: Size::default(), control_size: Size::default(), @@ -69,6 +49,9 @@ impl Scroll { max_scroll: Dynamic::new(Point::default()), scrollbar_opacity: Dynamic::default(), scrollbar_opacity_animation: AnimationHandle::new(), + horizontal_bar: ScrollbarInfo::default(), + vertical_bar: ScrollbarInfo::default(), + bar_width: Px::default(), } } @@ -89,12 +72,14 @@ impl Scroll { Self::construct(contents, Point::new(false, true)) } - fn constrain_scroll(&mut self) { + fn constrain_scroll(&mut self) -> (Point, Point) { let scroll = self.scroll.get(); - let clamped = scroll.max(self.max_scroll.get()).min(Point::default()); + let max_scroll = self.max_scroll.get(); + let clamped = scroll.max(max_scroll).min(Point::default()); if clamped != scroll { self.scroll.set(clamped); } + (clamped, max_scroll) } fn show_scrollbars(&mut self, context: &mut EventContext<'_, '_>) { @@ -135,18 +120,62 @@ impl Widget for Scroll { fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) { context.redraw_when_changed(&self.scrollbar_opacity); - self.constrain_scroll(); let Some(visible_rect) = context.graphics.visible_rect() else { return; }; let visible_bottom_right = visible_rect.into_signed().extent(); + + let managed = self.contents.mounted(&mut context.as_event_context()); + context.for_other(managed).redraw(); + + if self.horizontal_bar.amount_hidden > 0 { + context.graphics.draw_shape( + &Shape::filled_rect( + Rect::new( + Point::new( + self.horizontal_bar.offset, + self.control_size.height - self.bar_width, + ), + Size::new(self.horizontal_bar.size, self.bar_width), + ), + Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()), + ), + Point::default(), + None, + None, + ); + } + + if self.vertical_bar.amount_hidden > 0 { + context.graphics.draw_shape( + &Shape::filled_rect( + Rect::new( + Point::new( + visible_bottom_right.x - self.bar_width, + self.vertical_bar.offset, + ), + Size::new(self.bar_width, self.vertical_bar.size), + ), + Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()), + ), + Point::default(), + None, + None, + ); + } + } + + fn layout( + &mut self, + available_space: Size, + context: &mut crate::context::LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { let styles = context.query_styles(&[&ScrollBarThickness]); - let bar_width = styles + self.bar_width = styles .get_or_default(&ScrollBarThickness) .into_px(context.graphics.scale()); - let mut scroll = self.scroll.get(); - let current_max_scroll = self.max_scroll.get(); + let (mut scroll, current_max_scroll) = self.constrain_scroll(); let control_size = context.graphics.region().size; let max_extents = Size::new( @@ -161,25 +190,32 @@ impl Widget for Scroll { ConstraintLimit::Known(control_size.height.into_unsigned()) }, ); - let managed = self.contents.managed(&mut context.as_event_context()); + let managed = self.contents.mounted(&mut context.as_event_context()); let new_content_size = context - .for_other(&managed) - .measure(max_extents) + .for_other(managed.clone()) + .layout(max_extents) .into_signed(); - let horizontal_bar = scrollbar_region(scroll.x, new_content_size.width, control_size.width); + self.horizontal_bar = + scrollbar_region(scroll.x, new_content_size.width, control_size.width); let max_scroll_x = if self.enabled.x { - -horizontal_bar.amount_hidden + -self.horizontal_bar.amount_hidden } else { Px(0) }; - let vertical_bar = scrollbar_region(scroll.y, new_content_size.height, control_size.height); + self.vertical_bar = + scrollbar_region(scroll.y, new_content_size.height, control_size.height); let max_scroll_y = if self.enabled.y { - -vertical_bar.amount_hidden + -self.vertical_bar.amount_hidden } else { Px(0) }; + let new_max_scroll = Point::new(max_scroll_x, max_scroll_y); + if current_max_scroll != new_max_scroll { + self.max_scroll.set(new_max_scroll); + scroll = scroll.max(new_max_scroll); + } // Preserve the current scroll if the widget has resized if self.content_size.width != new_content_size.width @@ -198,54 +234,16 @@ impl Widget for Scroll { scroll.y = max_scroll_y * scroll_pct; } self.scroll.update(scroll); + self.control_size = control_size; + self.content_size = new_content_size; let region = Rect::new( scroll, self.content_size .min(Size::new(Px::MAX, Px::MAX) - scroll.max(Point::default())), ); - context.for_child(&managed, region).redraw(); + context.set_child_layout(&managed, region); - if max_scroll_x != 0 { - context.graphics.draw_shape( - &Shape::filled_rect( - Rect::new( - Point::new(horizontal_bar.offset, control_size.height - bar_width), - Size::new(horizontal_bar.size, bar_width), - ), - Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()), - ), - Point::default(), - None, - None, - ); - } - - if max_scroll_y != 0 { - context.graphics.draw_shape( - &Shape::filled_rect( - Rect::new( - Point::new(visible_bottom_right.x - bar_width, vertical_bar.offset), - Size::new(bar_width, vertical_bar.size), - ), - Color::new_f32(1.0, 1.0, 1.0, *self.scrollbar_opacity.get()), - ), - Point::default(), - None, - None, - ); - } - - self.control_size = control_size; - self.max_scroll - .update(Point::new(max_scroll_x, max_scroll_y)); - } - - fn measure( - &mut self, - available_space: Size, - _context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>, - ) -> Size { Size::new(available_space.width.max(), available_space.height.max()) } @@ -273,7 +271,7 @@ impl Widget for Scroll { } } -#[derive(Default)] +#[derive(Debug, Default)] struct ScrollbarInfo { offset: Px, amount_hidden: Px, diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index acd74a1..a220ea1 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -7,10 +7,10 @@ use alot::{LotId, OrderedLots}; use kludgine::figures::units::{Lp, UPx}; use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; -use crate::context::{AsEventContext, EventContext, GraphicsContext}; +use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::styles::Dimension; use crate::value::{Generation, IntoValue, Value}; -use crate::widget::{ChildWidget, ManagedWidget, Widget, Widgets}; +use crate::widget::{Children, ManagedWidget, Widget, WidgetRef}; use crate::widgets::{Expand, Resize}; use crate::ConstraintLimit; @@ -21,7 +21,7 @@ pub struct Stack { /// The direction to display the children using. pub direction: Value, /// The children widgets that belong to this array. - pub children: Value, + pub children: Value, layout: Layout, layout_generation: Option, // TODO Refactor synced_children into its own type. @@ -32,7 +32,7 @@ impl Stack { /// Returns a new widget with the given direction and widgets. pub fn new( direction: impl IntoValue, - widgets: impl IntoValue, + widgets: impl IntoValue, ) -> Self { let mut direction = direction.into_value(); @@ -48,19 +48,19 @@ impl Stack { } /// Returns a new instance that displays `widgets` in a series of columns. - pub fn columns(widgets: impl IntoValue) -> Self { + pub fn columns(widgets: impl IntoValue) -> Self { Self::new(StackDirection::columns(), widgets) } /// Returns a new instance that displays `widgets` in a series of rows. - pub fn rows(widgets: impl IntoValue) -> Self { + pub fn rows(widgets: impl IntoValue) -> Self { Self::new(StackDirection::rows(), widgets) } fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) { let current_generation = self.children.generation(); if current_generation.map_or_else( - || self.children.map(Widgets::len) != self.layout.children.len(), + || self.children.map(Children::len) != self.layout.children.len(), |gen| Some(gen) != self.layout_generation, ) { self.layout_generation = self.children.generation(); @@ -105,7 +105,7 @@ impl Stack { (child, StackDimension::Exact(size)) } else { ( - ChildWidget::Unmounted(widget.clone()), + WidgetRef::Unmounted(widget.clone()), StackDimension::FitContent, ) }; @@ -131,58 +131,49 @@ impl Stack { impl Widget for Stack { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - self.synchronize_children(&mut context.as_event_context()); - self.layout.update( - Size::new( - ConstraintLimit::Known(context.graphics.size().width), - ConstraintLimit::Known(context.graphics.size().height), - ), - context.graphics.scale(), - |child_index, constraints| { - context - .for_other(&self.synced_children[child_index]) - .measure(constraints) - }, - ); - - for (index, layout) in self.layout.iter().enumerate() { - let child = &self.synced_children[index]; - if layout.size > 0 { - context - .for_child( - child, - Rect::new( - self.layout - .orientation - .make_point(layout.offset, UPx(0)) - .into_signed(), - self.layout - .orientation - .make_size(layout.size, self.layout.other) - .into_signed(), - ), - ) - .redraw(); - } + for child in &self.synced_children { + context.for_other(child.clone()).redraw(); } } - fn measure( + fn layout( &mut self, available_space: Size, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { self.synchronize_children(&mut context.as_event_context()); - self.layout.update( + let content_size = self.layout.update( available_space, context.graphics.scale(), - |child_index, constraints| { - context - .for_other(&self.synced_children[child_index]) - .measure(constraints) + |child_index, constraints, persist| { + let mut context = context.for_other(self.synced_children[child_index].clone()); + if !persist { + context = context.as_temporary(); + } + context.layout(constraints) }, - ) + ); + + for (layout, child) in self.layout.iter().zip(&self.synced_children) { + if layout.size > 0 { + context.set_child_layout( + child, + Rect::new( + self.layout + .orientation + .make_point(layout.offset, UPx(0)) + .into_signed(), + self.layout + .orientation + .make_size(layout.size, self.layout.other) + .into_signed(), + ), + ); + } + } + + content_size } } @@ -387,7 +378,7 @@ impl Layout { &mut self, available: Size, scale: Fraction, - mut measure: impl FnMut(usize, Size) -> Size, + mut measure: impl FnMut(usize, Size, bool) -> Size, ) -> Size { let (space_constraint, other_constraint) = self.orientation.split_size(available); let available_space = space_constraint.max(); @@ -403,6 +394,7 @@ impl Layout { index, self.orientation .make_size(ConstraintLimit::ClippedAfter(remaining), other_constraint), + false, )); self.layouts[index].size = measured; remaining = remaining.saturating_sub(measured); @@ -447,6 +439,7 @@ impl Layout { ConstraintLimit::Known(self.layouts[index].size.into_px(scale).into_unsigned()), other_constraint, ), + true, )); self.other = self.other.max(measured); } @@ -525,22 +518,25 @@ mod tests { flex.push(child.dimension, Fraction::ONE); } - let computed_size = flex.update(available, Fraction::ONE, |index, constraints| { - let (measured_constraint, _other_constraint) = orientation.split_size(constraints); - let child = &children[index]; - let maximum_measured = measured_constraint.max(); - let (measured, other) = match (child.size.cmp(&maximum_measured), child.divisible_by) { - (Ordering::Greater, Some(divisible_by)) => { - let available_divided = maximum_measured / divisible_by; - let rows = ((child.size + divisible_by - 1) / divisible_by + available_divided - - 1) - / available_divided; - (available_divided * divisible_by, child.other * rows) - } - _ => (child.size, child.other), - }; - orientation.make_size(measured, other) - }); + let computed_size = + flex.update(available, Fraction::ONE, |index, constraints, _persist| { + let (measured_constraint, _other_constraint) = orientation.split_size(constraints); + let child = &children[index]; + let maximum_measured = measured_constraint.max(); + let (measured, other) = + match (child.size.cmp(&maximum_measured), child.divisible_by) { + (Ordering::Greater, Some(divisible_by)) => { + let available_divided = maximum_measured / divisible_by; + let rows = ((child.size + divisible_by - 1) / divisible_by + + available_divided + - 1) + / available_divided; + (available_divided * divisible_by, child.other * rows) + } + _ => (child.size, child.other), + }; + orientation.make_size(measured, other) + }); assert_eq!(computed_size, expected_size); let mut offset = UPx(0); for ((index, &child), &expected) in flex.iter().enumerate().zip(expected) { diff --git a/src/widgets/style.rs b/src/widgets/style.rs index 2533dea..b51c146 100644 --- a/src/widgets/style.rs +++ b/src/widgets/style.rs @@ -1,16 +1,16 @@ use kludgine::figures::units::UPx; use kludgine::figures::Size; -use crate::context::{AsEventContext, EventContext, GraphicsContext}; +use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::styles::Styles; -use crate::widget::{ChildWidget, MakeWidget, Widget}; +use crate::widget::{MakeWidget, Widget, WidgetRef}; use crate::ConstraintLimit; /// A widget that applies a set of [`Styles`] to all contained widgets. #[derive(Debug)] pub struct Style { styles: Styles, - child: ChildWidget, + child: WidgetRef, } impl Style { @@ -19,7 +19,7 @@ impl Style { pub fn new(styles: impl Into, child: impl MakeWidget) -> Self { Self { styles: styles.into(), - child: ChildWidget::new(child), + child: WidgetRef::new(child), } } } @@ -31,15 +31,15 @@ impl Widget for Style { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { let child = self.child.mounted(&mut context.as_event_context()); - context.for_other(&child).redraw(); + context.for_other(child).redraw(); } - fn measure( + fn layout( &mut self, available_space: Size, - context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { let child = self.child.mounted(&mut context.as_event_context()); - context.for_other(&child).measure(available_space) + context.for_other(child).layout(available_space) } } diff --git a/src/widgets/tilemap.rs b/src/widgets/tilemap.rs index d6524a8..67e7cf0 100644 --- a/src/widgets/tilemap.rs +++ b/src/widgets/tilemap.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use kludgine::figures::utils::lossy_f64_to_f32; -use crate::context::{EventContext, GraphicsContext}; +use crate::context::{EventContext, GraphicsContext, LayoutContext}; use crate::kludgine::app::winit::event::{DeviceId, KeyEvent, MouseScrollDelta, TouchPhase}; use crate::kludgine::figures::units::UPx; use crate::kludgine::figures::Size; @@ -78,10 +78,10 @@ where } } - fn measure( + fn layout( &mut self, available_space: Size, - _context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + _context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { Size::new(available_space.width.max(), available_space.height.max()) } diff --git a/src/window.rs b/src/window.rs index 8b93d59..fab8104 100644 --- a/src/window.rs +++ b/src/window.rs @@ -20,7 +20,9 @@ use kludgine::figures::{IntoSigned, Point, Rect, Size}; use kludgine::render::Drawing; use kludgine::Kludgine; -use crate::context::{EventContext, Exclusive, GraphicsContext, RedrawStatus, WidgetContext}; +use crate::context::{ + EventContext, Exclusive, GraphicsContext, LayoutContext, RedrawStatus, WidgetContext, +}; use crate::graphics::Graphics; use crate::tree::Tree; use crate::utils::ModifiersExt; @@ -199,22 +201,24 @@ where self.root.tree.reset_render_order(); let graphics = self.contents.new_frame(graphics); let mut context = GraphicsContext { - widget: WidgetContext::new(&self.root, &self.redraw_status, &mut window), + widget: WidgetContext::new(self.root.clone(), &self.redraw_status, &mut window), graphics: Exclusive::Owned(Graphics::new(graphics)), }; - let window_size = context.graphics.size(); - let actual_size = context.measure(Size::new( + let mut layout_context = LayoutContext::new(&mut context); + let window_size = layout_context.graphics.size(); + let actual_size = layout_context.layout(Size::new( ConstraintLimit::ClippedAfter(window_size.width), ConstraintLimit::ClippedAfter(window_size.height), )); let render_size = actual_size.min(window_size); + self.root.set_layout(Rect::from(render_size.into_signed())); if render_size.width < window_size.width || render_size.height < window_size.height { - context + layout_context .clipped_to(Rect::from(render_size.into_signed())) .redraw(); } else { - context.redraw(); + layout_context.redraw(); } } @@ -284,7 +288,7 @@ where let target = self.root.tree.focused_widget().unwrap_or(self.root.id); let target = self.root.tree.widget(target); let mut target = EventContext::new( - WidgetContext::new(&target, &self.redraw_status, &mut window), + WidgetContext::new(target, &self.redraw_status, &mut window), kludgine, ); @@ -320,7 +324,7 @@ where let widget = self.root.tree.widget(widget); let mut widget = EventContext::new( - WidgetContext::new(&widget, &self.redraw_status, &mut window), + WidgetContext::new(widget, &self.redraw_status, &mut window), kludgine, ); recursively_handle_event(&mut widget, |widget| { @@ -334,7 +338,7 @@ where let target = self.root.tree.focused_widget().unwrap_or(self.root.id); let target = self.root.tree.widget(target); let mut target = EventContext::new( - WidgetContext::new(&target, &self.redraw_status, &mut window), + WidgetContext::new(target, &self.redraw_status, &mut window), kludgine, ); @@ -356,24 +360,24 @@ where // Mouse Drag for (button, handler) in state { let mut context = EventContext::new( - WidgetContext::new(handler, &self.redraw_status, &mut window), + WidgetContext::new(handler.clone(), &self.redraw_status, &mut window), kludgine, ); - let last_rendered_at = context.last_rendered_at().expect("passed hit test"); + let last_rendered_at = context.last_layout().expect("passed hit test"); context.mouse_drag(location - last_rendered_at.origin, device_id, *button); } } else { // Hover let mut context = EventContext::new( - WidgetContext::new(&self.root, &self.redraw_status, &mut window), + WidgetContext::new(self.root.clone(), &self.redraw_status, &mut window), kludgine, ); self.mouse_state.widget = None; for widget in self.root.tree.widgets_at_point(location) { - let mut widget_context = context.for_other(&widget); + let mut widget_context = context.for_other(widget.clone()); let relative = location - widget_context - .last_rendered_at() + .last_layout() .expect("passed hit test") .origin; @@ -399,7 +403,7 @@ where ) { if self.mouse_state.widget.take().is_some() { let mut context = EventContext::new( - WidgetContext::new(&self.root, &self.redraw_status, &mut window), + WidgetContext::new(self.root.clone(), &self.redraw_status, &mut window), kludgine, ); context.clear_hover(); @@ -417,7 +421,7 @@ where match state { ElementState::Pressed => { EventContext::new( - WidgetContext::new(&self.root, &self.redraw_status, &mut window), + WidgetContext::new(self.root.clone(), &self.redraw_status, &mut window), kludgine, ) .clear_focus(); @@ -427,12 +431,12 @@ where { if let Some(handler) = recursively_handle_event( &mut EventContext::new( - WidgetContext::new(hovered, &self.redraw_status, &mut window), + WidgetContext::new(hovered.clone(), &self.redraw_status, &mut window), kludgine, ), |context| { - let relative = *location - - context.last_rendered_at().expect("passed hit test").origin; + let relative = + *location - context.last_layout().expect("passed hit test").origin; context.mouse_down(relative, device_id, button) }, ) { @@ -456,12 +460,12 @@ where } let mut context = EventContext::new( - WidgetContext::new(&handler, &self.redraw_status, &mut window), + WidgetContext::new(handler, &self.redraw_status, &mut window), kludgine, ); let relative = if let (Some(last_rendered), Some(location)) = - (context.last_rendered_at(), self.mouse_state.location) + (context.last_layout(), self.mouse_state.location) { Some(location - last_rendered.origin) } else { @@ -496,7 +500,7 @@ fn recursively_handle_event( match each_widget(context) { HANDLED => Some(context.widget().clone()), IGNORED => context.parent().and_then(|parent| { - recursively_handle_event(&mut context.for_other(&parent), each_widget) + recursively_handle_event(&mut context.for_other(parent), each_widget) }), } } diff --git a/src/with_clone.rs b/src/with_clone.rs deleted file mode 100644 index 9050b7b..0000000 --- a/src/with_clone.rs +++ /dev/null @@ -1,34 +0,0 @@ -/// Invokes a function with a clone of `self`. -pub trait WithClone: Sized { - /// The type that results from cloning. - type Cloned; - - /// Maps `with` with the results of cloning `self`. - fn with_clone(&self, with: impl FnOnce(Self::Cloned) -> R) -> R; -} - -macro_rules! impl_with_clone { - ($($name:ident $field:tt),+) => { - impl<'a, $($name: Clone,)+> WithClone for ($(&'a $name,)+) - { - type Cloned = ($($name,)+); - - fn with_clone(&self, with: impl FnOnce(Self::Cloned) -> R) -> R { - with(($(self.$field.clone(),)+)) - } - } - }; -} - -impl<'a, T> WithClone for &'a T -where - T: Clone, -{ - type Cloned = T; - - fn with_clone(&self, with: impl FnOnce(Self::Cloned) -> R) -> R { - with((*self).clone()) - } -} - -impl_all_tuples!(impl_with_clone);