From 96d407ddc263e36412f87751cd3c8b502b8cf9d1 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sun, 12 Nov 2023 13:37:32 -0800 Subject: [PATCH] Container, query_parent_style --- examples/containers.rs | 45 +++++++ examples/gameui.rs | 22 ++-- examples/theme.rs | 2 + src/context.rs | 34 +++++- src/styles.rs | 117 +++++++++++++++++- src/styles/components.rs | 6 +- src/tree.rs | 30 +++-- src/value.rs | 14 +++ src/widget.rs | 127 +++++++++++++++++-- src/widgets.rs | 2 + src/widgets/align.rs | 7 +- src/widgets/button.rs | 2 +- src/widgets/container.rs | 258 +++++++++++++++++++++++++++++++++++++++ src/widgets/expand.rs | 8 +- src/widgets/label.rs | 66 +++++----- src/widgets/resize.rs | 6 +- src/window.rs | 39 +++++- 17 files changed, 702 insertions(+), 83 deletions(-) create mode 100644 examples/containers.rs create mode 100644 src/widgets/container.rs diff --git a/examples/containers.rs b/examples/containers.rs new file mode 100644 index 0000000..8fafd1d --- /dev/null +++ b/examples/containers.rs @@ -0,0 +1,45 @@ +use gooey::value::Dynamic; +use gooey::widget::{MakeWidget, WidgetInstance}; +use gooey::widgets::{Button, Label}; +use gooey::window::ThemeMode; +use gooey::Run; + +fn main() -> gooey::Result { + let theme_mode = Dynamic::default(); + set_of_containers(1, theme_mode.clone()) + .into_window() + .with_theme_mode(theme_mode) + .run() +} + +fn set_of_containers(repeat: usize, theme_mode: Dynamic) -> WidgetInstance { + let inner = if let Some(remaining_iters) = repeat.checked_sub(1) { + set_of_containers(remaining_iters, theme_mode) + } else { + Button::new("Toggle Theme Mode") + .on_click(move |_| { + theme_mode.map_mut(|mode| mode.toggle()); + }) + .make_widget() + }; + Label::new("Lowest") + .and( + Label::new("Low") + .and( + Label::new("Mid") + .and( + Label::new("High") + .and(Label::new("Highest").and(inner).into_rows().contain()) + .into_rows() + .contain(), + ) + .into_rows() + .contain(), + ) + .into_rows() + .contain(), + ) + .into_rows() + .contain() + .make_widget() +} diff --git a/examples/gameui.rs b/examples/gameui.rs index 4a6cb89..50c5a7e 100644 --- a/examples/gameui.rs +++ b/examples/gameui.rs @@ -1,6 +1,6 @@ use gooey::value::Dynamic; use gooey::widget::{MakeWidget, HANDLED, IGNORED}; -use gooey::widgets::{Input, Label, Space, Stack}; +use gooey::widgets::{Input, Label, Space}; use gooey::Run; use kludgine::app::winit::event::ElementState; use kludgine::app::winit::keyboard::Key; @@ -10,13 +10,11 @@ fn main() -> gooey::Result { let chat_log = Dynamic::new("Chat log goes here.\n".repeat(100)); let chat_message = Dynamic::new(String::new()); - Stack::rows( - Stack::columns( - Label::new(chat_log.clone()) - .vertical_scroll() - .expand() - .and(Space::colored(Color::RED).expand_weighted(2)), - ) + Label::new(chat_log.clone()) + .vertical_scroll() + .expand() + .and(Space::colored(Color::RED).expand_weighted(2)) + .into_columns() .expand() .and(Input::new(chat_message.clone()).on_key(move |input| { match (input.state, input.logical_key) { @@ -30,8 +28,8 @@ fn main() -> gooey::Result { } _ => IGNORED, } - })), - ) - .expand() - .run() + })) + .into_rows() + .expand() + .run() } diff --git a/examples/theme.rs b/examples/theme.rs index bdbc92d..c03ca97 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -8,6 +8,7 @@ use gooey::widget::MakeWidget; use gooey::widgets::{Input, Label, Scroll, Slider, Stack, Themed}; use gooey::window::ThemeMode; use gooey::Run; +use kludgine::figures::units::Lp; use kludgine::Color; const PRIMARY_HUE: f32 = 240.; @@ -67,6 +68,7 @@ fn main() -> gooey::Result { )), ), ) + .pad_by(Lp::points(16)) .expand() .into_window() .with_theme_mode(theme_mode) diff --git a/src/context.rs b/src/context.rs index 8de02aa..297f33f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -924,7 +924,25 @@ 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, self) + .query_styles(&self.current_node, query, false, self) + } + + /// Queries the widget hierarchy for matching style components, starting + /// with this widget's parent. + /// + /// This function traverses up the widget hierarchy looking for the + /// components being requested. The resulting styles will contain the values + /// from the closest matches in the widget hierarchy. + /// + /// For style components to be found, they must have previously been + /// [attached](Self::attach_styles). The [`Style`](crate::widgets::Style) + /// widget is provided as a convenient way to attach styles into the widget + /// hierarchy. + #[must_use] + pub fn query_parent_styles(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles { + self.current_node + .tree + .query_styles(&self.current_node, query, true, self) } /// Queries the widget hierarchy for a single style component. @@ -940,7 +958,19 @@ impl<'context, 'window> WidgetContext<'context, 'window> { ) -> Component::ComponentType { self.current_node .tree - .query_style(&self.current_node, query, self) + .query_style(&self.current_node, query, false, self) + } + + /// Queries the widget hierarchy for a single style component, starting with + /// this widget's parent. + #[must_use] + pub fn query_parent_style( + &self, + query: &Component, + ) -> Component::ComponentType { + self.current_node + .tree + .query_style(&self.current_node, query, true, self) } pub(crate) fn handle(&self) -> WindowHandle { diff --git a/src/styles.rs b/src/styles.rs index e097a03..a377b6e 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -197,6 +197,9 @@ pub enum Component { VisualOrder(VisualOrder), /// A description of what widgets should be focusable. FocusableWidgets(FocusableWidgets), + /// A description of the depth of a + /// [`Container`](crate::widgets::Container). + ContainerLevel(ContainerLevel), } impl From for Component { @@ -705,21 +708,48 @@ impl NamedComponent for Cow<'_, ComponentName> { pub struct Edges { /// The left edge pub left: T, - /// The right edge - pub right: T, /// The top edge pub top: T, + /// The right edge + pub right: T, /// The bottom edge pub bottom: T, } impl Edges { /// Returns the sum of the parts as a [`Size`]. - pub fn size(&self) -> Size + pub fn size(self) -> Size where T: Add + Copy, { - Size::new(self.left + self.right, self.top + self.bottom) + Size::new(self.width(), self.height()) + } + + /// Returns a new set of edges produced by calling `map` with each of the + /// edges. + pub fn map(self, mut map: impl FnMut(T) -> U) -> Edges { + Edges { + left: map(self.left), + top: map(self.top), + right: map(self.right), + bottom: map(self.bottom), + } + } + + /// Returns the sum of the left and right edges. + pub fn width(self) -> T + where + T: Add, + { + self.left + self.right + } + + /// Returns the sum of the top and bottom edges. + pub fn height(self) -> T + where + T: Add, + { + self.top + self.bottom } } @@ -821,12 +851,42 @@ impl IntoValue> for FlexibleDimension { } } +impl IntoValue> for Dimension { + fn into_value(self) -> Value> { + FlexibleDimension::Dimension(self).into_value() + } +} + +impl IntoValue> for Px { + fn into_value(self) -> Value> { + Dimension::from(self).into_value() + } +} + +impl IntoValue> for Lp { + fn into_value(self) -> Value> { + Dimension::from(self).into_value() + } +} + impl IntoValue> for Dimension { fn into_value(self) -> Value> { Value::Constant(Edges::from(self)) } } +impl IntoValue> for Px { + fn into_value(self) -> Value> { + Dimension::from(self).into_value() + } +} + +impl IntoValue> for Lp { + fn into_value(self) -> Value> { + Dimension::from(self).into_value() + } +} + /// A set of light and dark [`Theme`]s. #[derive(Clone, Debug)] pub struct ThemePair { @@ -1465,3 +1525,52 @@ impl TryFrom for FocusableWidgets { } } } + +/// A description of the level of depth a +/// [`Container`](crate::widgets::Container) is nested at. +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] +pub enum ContainerLevel { + /// The lowest container level. + #[default] + Lowest, + /// The second lowest container level. + Low, + /// The mid-level container level. + Mid, + /// The second-highest container level. + High, + /// The highest container level. + Highest, +} + +impl ContainerLevel { + /// Returns the next container level, or None if already at the highet + /// level. + #[must_use] + pub const fn next(self) -> Option { + match self { + Self::Lowest => Some(Self::Low), + Self::Low => Some(Self::Mid), + Self::Mid => Some(Self::High), + Self::High => Some(Self::Highest), + Self::Highest => None, + } + } +} + +impl From for Component { + fn from(value: ContainerLevel) -> Self { + Self::ContainerLevel(value) + } +} + +impl TryFrom for ContainerLevel { + type Error = Component; + + fn try_from(value: Component) -> Result { + match value { + Component::ContainerLevel(level) => Ok(level), + other => Err(other), + } + } +} diff --git a/src/styles/components.rs b/src/styles/components.rs index 54d4724..7e68fda 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -26,9 +26,11 @@ use crate::styles::{Dimension, FocusableWidgets, VisualOrder}; /// /// This component whose default value is a color from the current theme. /// ThemedComponent(Color, "themed_component", .primary.color) /// /// This component is a color whose default value is the currently defined `TextColor`. -/// DependentComponent(Color, "dependent_component", |context| context.query_style(&TextColor)) +/// DependentComponent(Color, "dependent_component", @TextColor) /// /// This component defaults to picking a contrasting color between `TextColor` and `SurfaceColor` /// ContrastingColor(Color, "contrasting_color", contrasting!(ThemedComponent, TextColor, SurfaceColor)) +/// /// This component shows how to use a closure for nearly infinite flexibility in computing the default value. +/// ClosureDefaultComponent(Color, "closure_component", |context| context.query_style(&TextColor)) /// } /// } /// ``` @@ -72,7 +74,7 @@ macro_rules! define_components { ($type:ty, contrasting!($bg:ident, $($fg:ident),+ $(,)?)) => { define_components!($type, |context| { use $crate::styles::ColorExt; - let styles = context.query_styles(&[&$bg, $(&$fg),*]); + let styles = context.query_parent_styles(&[&$bg, $(&$fg),*]); styles.get(&$bg, context).most_contrasting(&[ $(styles.get(&$fg, context)),+ ]) diff --git a/src/tree.rs b/src/tree.rs index b7cb811..69188a1 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -317,24 +317,29 @@ impl Tree { &self, perspective: &ManagedWidget, query: &[&dyn ComponentDefaultvalue], + skip_current: bool, context: &WidgetContext<'_, '_>, ) -> Styles { self.data .lock() .map_or_else(PoisonError::into_inner, |g| g) - .query_styles(perspective.id(), query, context) + .query_styles(perspective.id(), query, skip_current, context) } pub fn query_style( &self, perspective: &ManagedWidget, component: &Component, + skip_self: bool, context: &WidgetContext<'_, '_>, ) -> Component::ComponentType { - self.data + let result = self + .data .lock() .map_or_else(PoisonError::into_inner, |g| g) - .query_style(perspective.id(), component, context) + .query_style(perspective.id(), component, skip_self, context); + + result.unwrap_or_else(|| component.default_value(context)) } } @@ -423,13 +428,17 @@ impl TreeData { &self, mut perspective: WidgetId, query: &[&dyn ComponentDefaultvalue], + skip_self: bool, context: &WidgetContext<'_, '_>, ) -> Styles { let mut query = query.iter().map(|n| n.name()).collect::>(); let mut resolved = Styles::new(); + let mut skip_next = skip_self; while !query.is_empty() { let node = &self.nodes[&perspective]; - if let Some(styles) = &node.styles { + if skip_next { + skip_next = false; + } else if let Some(styles) = &node.styles { styles.map_tracked(context, |styles| { query.retain(|name| { if let Some(component) = styles.get_named(name) { @@ -451,12 +460,16 @@ impl TreeData { &self, mut perspective: WidgetId, query: &Component, + skip_self: bool, context: &WidgetContext<'_, '_>, - ) -> Component::ComponentType { + ) -> Option { let name = query.name(); + let mut skip_next = skip_self; loop { let node = &self.nodes[&perspective]; - if let Some(styles) = &node.styles { + if skip_next { + skip_next = false; + } else if let Some(styles) = &node.styles { match styles.map_tracked(context, |styles| { if let Some(component) = styles.get_named(&name) { let Ok(value) = @@ -469,15 +482,16 @@ impl TreeData { } Ok(None) }) { - Ok(Some(value)) => return value, + Ok(Some(value)) => return Some(value), Ok(None) => {} Err(()) => break, } } + let Some(parent) = node.parent else { break }; perspective = parent; } - query.default_value(context) + None } } diff --git a/src/value.rs b/src/value.rs index 4d6ed51..3a08a6a 100644 --- a/src/value.rs +++ b/src/value.rs @@ -954,6 +954,20 @@ impl Value { } } + /// Returns a new value that is updated using `U::from(T.clone())` each time + /// `self` is updated. + #[must_use] + pub fn map_each(&self, mut map: F) -> Value + where + F: for<'a> FnMut(&'a T) -> R + Send + 'static, + R: Send + 'static, + { + match self { + Value::Constant(value) => Value::Constant(map(value)), + Value::Dynamic(dynamic) => Value::Dynamic(dynamic.map_each(map)), + } + } + /// Returns a clone of the currently stored value. pub fn get(&self) -> T where diff --git a/src/widget.rs b/src/widget.rs index 92da181..c570711 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -13,12 +13,16 @@ use kludgine::app::winit::event::{ }; use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size}; +use kludgine::Color; -use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; -use crate::styles::{IntoComponentValue, NamedComponent, Styles, ThemePair, VisualOrder}; +use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext}; +use crate::styles::{ + ContainerLevel, Dimension, Edges, IntoComponentValue, NamedComponent, Styles, ThemePair, + VisualOrder, +}; use crate::tree::Tree; use crate::value::{IntoValue, Value}; -use crate::widgets::{Align, Expand, Scroll, Style}; +use crate::widgets::{Align, Container, Expand, Scroll, Stack, Style}; use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior}; use crate::{ConstraintLimit, Run}; @@ -173,6 +177,33 @@ where } } +/// The layout of a [wrapped](WrapperWidget) child widget. +#[derive(Clone, Copy, Debug)] +pub struct WrappedLayout { + /// The region the child widget occupies within its parent. + pub child: Rect, + /// The size the wrapper widget should report as.q + pub size: Size, +} + +impl From> for WrappedLayout { + fn from(child: Rect) -> Self { + WrappedLayout { + child, + size: child.size.into_unsigned(), + } + } +} + +impl From> for WrappedLayout { + fn from(size: Size) -> Self { + WrappedLayout { + child: size.into(), + size: size.into_unsigned(), + } + } +} + /// A [`Widget`] that contains a single child. pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { /// Returns the child widget. @@ -185,14 +216,48 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, - ) -> Rect { + ) -> WrappedLayout { let child = self.child_mut().mounted(&mut context.as_event_context()); - context + let adjusted_space = self.adjust_child_constraint(available_space, context); + let size = context .for_other(&child) - .layout(available_space) - .into_signed() - .into() + .layout(adjusted_space) + .into_signed(); + + self.position_child(size, available_space, context) + } + + /// Returns the adjusted contraints to use when laying out the child. + #[allow(unused_variables)] + #[must_use] + fn adjust_child_constraint( + &mut self, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { + available_space + } + + /// Returns the layout after positioning the child that occupies `size`. + #[allow(unused_variables)] + #[must_use] + fn position_child( + &mut self, + size: Size, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> WrappedLayout { + size.into() + } + + /// Returns the background color to render behind the wrapped widget. + #[allow(unused_variables)] + #[must_use] + fn background_color(&mut self, context: &WidgetContext<'_, '_>) -> Option { + // WidgetBackground is already filled, so we don't need to do anything + // else by default. + None } /// The widget has been mounted into a parent widget. @@ -323,6 +388,11 @@ where } fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + let background_color = self.background_color(context); + if let Some(color) = background_color { + context.gfx.fill(color); + } + let child = self.child_mut().mounted(&mut context.as_event_context()); context.for_other(&child).redraw(); } @@ -335,8 +405,8 @@ where let child = self.child_mut().mounted(&mut context.as_event_context()); let layout = self.layout_child(available_space, context); - context.set_child_layout(&child, layout); - layout.size.into_unsigned() + context.set_child_layout(&child, layout.child); + layout.size } fn mounted(&mut self, context: &mut EventContext<'_, '_>) { @@ -577,6 +647,31 @@ pub trait MakeWidget: Sized { fn widget_ref(self) -> WidgetRef { WidgetRef::new(self) } + + /// Wraps `self` in a [`Container`]. + fn contain(self) -> Container { + Container::new(self) + } + + /// Wraps `self` in a [`Container`] with the specified level. + fn contain_level(self, level: impl IntoValue) -> Container { + self.contain().contain_level(level) + } + + /// Returns a new widget that renders `color` behind `self`. + fn background_color(self, color: impl IntoValue) -> Container { + self.contain().pad_by(Px(0)).background_color(color) + } + + /// Wraps `self` with the default padding. + fn pad(self) -> Container { + self.contain().transparent() + } + + /// Wraps `self` with the specified padding. + fn pad_by(self, padding: impl IntoValue>) -> Container { + self.contain().transparent().pad_by(padding) + } } /// A type that can create a [`WidgetInstance`] with a preallocated @@ -1096,6 +1191,18 @@ impl Children { pub fn truncate(&mut self, length: usize) { self.ordered.truncate(length); } + + /// Returns `self` as a vertical [`Stack`] of rows. + #[must_use] + pub fn into_rows(self) -> Stack { + Stack::rows(self) + } + + /// Returns `self` as a horizontal [`Stack`] of columns. + #[must_use] + pub fn into_columns(self) -> Stack { + Stack::columns(self) + } } impl FromIterator for Children diff --git a/src/widgets.rs b/src/widgets.rs index e1a0adf..72e1789 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -3,6 +3,7 @@ mod align; pub mod button; mod canvas; +pub mod container; mod expand; mod input; pub mod label; @@ -19,6 +20,7 @@ mod tilemap; pub use align::Align; pub use button::Button; pub use canvas::Canvas; +pub use container::Container; pub use expand::Expand; pub use input::Input; pub use label::Label; diff --git a/src/widgets/align.rs b/src/widgets/align.rs index 0fe9f79..f7c7106 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -6,7 +6,7 @@ use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenS use crate::context::{AsEventContext, LayoutContext}; use crate::styles::{Edges, FlexibleDimension}; use crate::value::{IntoValue, Value}; -use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; +use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; use crate::ConstraintLimit; /// A widget aligns its contents to its container's boundaries. @@ -107,8 +107,8 @@ impl Align { Layout { margin: Edges { left, - right, top, + right, bottom, }, content: Size::new(width, height), @@ -186,7 +186,7 @@ impl WrapperWidget for Align { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, - ) -> Rect { + ) -> WrappedLayout { let layout = self.measure(available_space, context); Rect::new( @@ -196,6 +196,7 @@ impl WrapperWidget for Align { ), layout.content.into_signed(), ) + .into() } } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 0c0ea95..5451cdc 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -315,7 +315,7 @@ impl Widget for Button { define_components! { Button { /// The background color of the button. - ButtonBackground(Color, "background_color", |context| context.query_style(&OpaqueWidgetColor)) + ButtonBackground(Color, "background_color", @OpaqueWidgetColor) /// The background color of the button when it is active (depressed). ButtonActiveBackground(Color, "active_background_color", .surface.color) /// The background color of the button when the mouse cursor is hovering over diff --git a/src/widgets/container.rs b/src/widgets/container.rs new file mode 100644 index 0000000..9e452ba --- /dev/null +++ b/src/widgets/container.rs @@ -0,0 +1,258 @@ +//! A visual container widget. + +use kludgine::figures::units::Px; +use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size}; +use kludgine::Color; + +use crate::context::{GraphicsContext, LayoutContext, WidgetContext}; +use crate::styles::components::{IntrinsicPadding, SurfaceColor}; +use crate::styles::{Component, ContainerLevel, Dimension, Edges, Styles}; +use crate::value::{IntoValue, Value}; +use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; +use crate::ConstraintLimit; + +/// A visual container widget, optionally applying padding and a background +/// color. +/// +/// # Background Color Selection +/// +/// This widget has three different modes for coloring its background: +/// +/// - [`ContainerBackground::Auto`]: The background color is automatically +/// selected by using the [next](ContainerLevel::next) level from the next +/// parent container in the hierarchy. +/// +/// If the previous container is [`ContainerLevel::Highest`] or the previous +/// parent container uses a color instead of a level, +/// [`ContainerLevel::Lowest`] will be used. +/// - [`ContainerBackground::Color`]: The specified color will be drawn. +/// - [`ContainerBackground::Level`]: The +/// [`SurfaceTheme`](crate::styles::SurfaceTheme) container color associated +/// with the given level will be used. +#[derive(Debug)] +pub struct Container { + /// The configured background selection. + pub background: Value, + /// Padding to surround the contained widget. + /// + /// If this is None, a uniform surround of [`IntrinsicPadding`] will be + /// applied. + pub padding: Option>>, + child: WidgetRef, + effective_background: Option, +} + +/// A strategy of applying a background to a [`Container`]. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] +pub enum ContainerBackground { + /// Automatically select a [`ContainerLevel`] by picking the + /// [next](ContainerLevel::next) level after the previous parent + /// [`Container`]. + /// + /// If no parent container is found or a parent container is found with a + /// [color](Self::Color) background, [`ContainerLevel::Lowest`] will be + /// used. See [`Self::Level`] for more information. + #[default] + Auto, + /// Fills the background with the specified color. + Color(Color), + /// Applies the [`SurfaceTheme`][st] color + /// corresponding with the given level. + /// + /// | [`ContainerLevel`] | [`SurfaceTheme`][st] property | + /// |--------------------|-------------------------------| + /// | [`Lowest`][ll] | [`lowest_container`][llc] | + /// | [`Low`][lo] | [`low_container`][loc] | + /// | [`Low`][mi] | [`container`][mic] | + /// | [`High`][hi] | [`high_container`][hic] | + /// | [`Highest`][hh] | [`highest_container`][hhc] | + /// + /// [st]: crate::styles::SurfaceTheme + /// [ll]: ContainerLevel::Lowest + /// [llc]: crate::styles::SurfaceTheme::lowest_container + /// [lo]: ContainerLevel::Low + /// [loc]: crate::styles::SurfaceTheme::low_container + /// [mi]: ContainerLevel::Mid + /// [mic]: crate::styles::SurfaceTheme::container + /// [hi]: ContainerLevel::High + /// [hic]: crate::styles::SurfaceTheme::high_container + /// [hh]: ContainerLevel::Highest + /// [hhc]: crate::styles::SurfaceTheme::highest_container + Level(ContainerLevel), +} + +impl From for ContainerBackground { + fn from(value: ContainerLevel) -> Self { + Self::Level(value) + } +} + +impl From for ContainerBackground { + fn from(value: Color) -> Self { + Self::Color(value) + } +} + +impl Container { + /// Returns a new container wrapping `child` with default padding and a + /// background color automatically selected by the theme. + /// + /// See [`ContainerBackground::Auto`] for more information about automatic + /// coloring. + #[must_use] + pub fn new(child: impl MakeWidget) -> Self { + Self { + padding: None, + effective_background: None, + background: Value::default(), + child: WidgetRef::new(child), + } + } + + /// Pads the contained widget with `padding`, returning the updated + /// container. + #[must_use] + pub fn pad_by(mut self, padding: impl IntoValue>) -> Self { + self.padding = Some(padding.into_value()); + self + } + + /// Sets this container to render no background color, and then returns the + /// updated container. + #[must_use] + pub fn transparent(mut self) -> Self { + self.background = Value::Constant(ContainerBackground::Color(Color::CLEAR_WHITE)); + self + } + + /// Sets this container to use the specific container level, and then + /// returns the updated container. + #[must_use] + pub fn contain_level(mut self, level: impl IntoValue) -> Container { + self.background = level + .into_value() + .map_each(|level| ContainerBackground::from(*level)); + self + } + + /// Sets this container to render the specified `color` background, and then + /// returns the updated container. + #[must_use] + pub fn background_color(mut self, color: impl IntoValue) -> Self { + self.background = color + .into_value() + .map_each(|color| ContainerBackground::from(*color)); + self + } + + fn padding(&self, context: &GraphicsContext<'_, '_, '_, '_, '_>) -> Edges { + match &self.padding { + Some(padding) => padding.get(), + None => Edges::from(context.query_style(&IntrinsicPadding)), + } + .map(|dim| dim.into_px(context.gfx.scale())) + } +} + +impl WrapperWidget for Container { + fn child_mut(&mut self) -> &mut WidgetRef { + &mut self.child + } + + fn background_color(&mut self, context: &WidgetContext<'_, '_>) -> Option { + let background = match self.background.get() { + ContainerBackground::Color(color) => EffectiveBackground::Color(color), + ContainerBackground::Level(level) => EffectiveBackground::Level(level), + ContainerBackground::Auto => EffectiveBackground::Level( + match context.query_parent_style(&CurrentContainerBackground) { + EffectiveBackground::Color(_) => ContainerLevel::default(), + EffectiveBackground::Level(level) => level.next().unwrap_or_default(), + }, + ), + }; + + if self.effective_background != Some(background) { + context.attach_styles(Styles::new().with(&CurrentContainerBackground, background)); + self.effective_background = Some(background); + } + + Some(match background { + EffectiveBackground::Color(color) => color, + EffectiveBackground::Level(level) => match level { + ContainerLevel::Lowest => context.theme().surface.lowest_container, + ContainerLevel::Low => context.theme().surface.low_container, + ContainerLevel::Mid => context.theme().surface.container, + ContainerLevel::High => context.theme().surface.high_container, + ContainerLevel::Highest => context.theme().surface.highest_container, + }, + }) + } + + fn adjust_child_constraint( + &mut self, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { + let padding_amount = self + .padding(context) + .size() + .into_px(context.gfx.scale()) + .into_unsigned(); + Size::new( + available_space.width - padding_amount.width, + available_space.height - padding_amount.height, + ) + } + + fn position_child( + &mut self, + size: Size, + _available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> WrappedLayout { + let padding = self.padding(context); + let padded = size + padding.size(); + + WrappedLayout { + child: Rect::new(Point::new(padding.left, padding.top), size), + size: padded.into_unsigned(), + } + } +} + +/// The selected background configuration of a [`Container`]. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum EffectiveBackground { + /// The container rendered using the specified level's theme color. + Level(ContainerLevel), + /// The container rendered using the specified color. + Color(Color), +} + +impl TryFrom for EffectiveBackground { + type Error = Component; + + fn try_from(value: Component) -> Result { + match value { + Component::Color(color) => Ok(EffectiveBackground::Color(color)), + Component::ContainerLevel(level) => Ok(EffectiveBackground::Level(level)), + other => Err(other), + } + } +} + +impl From for Component { + fn from(value: EffectiveBackground) -> Self { + match value { + EffectiveBackground::Level(level) => Self::ContainerLevel(level), + EffectiveBackground::Color(color) => Self::Color(color), + } + } +} + +define_components! { + Container { + /// The container background behind the current widget. + CurrentContainerBackground(EffectiveBackground, "background", |context| EffectiveBackground::Color(context.query_style(&SurfaceColor))) + } +} diff --git a/src/widgets/expand.rs b/src/widgets/expand.rs index 1fa2366..4bb28a0 100644 --- a/src/widgets/expand.rs +++ b/src/widgets/expand.rs @@ -1,8 +1,8 @@ -use kludgine::figures::units::{Px, UPx}; -use kludgine::figures::{IntoSigned, Rect, Size}; +use kludgine::figures::units::UPx; +use kludgine::figures::{IntoSigned, Size}; use crate::context::{AsEventContext, LayoutContext}; -use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; +use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; use crate::widgets::Space; use crate::ConstraintLimit; @@ -71,7 +71,7 @@ impl WrapperWidget for Expand { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, - ) -> Rect { + ) -> WrappedLayout { let available_space = Size::new( ConstraintLimit::Known(available_space.width.max()), ConstraintLimit::Known(available_space.height.max()), diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 3226a1b..332e96b 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -3,6 +3,7 @@ use kludgine::figures::units::{Px, UPx}; use kludgine::figures::{IntoUnsigned, Point, ScreenScale, Size}; use kludgine::text::{MeasuredText, Text, TextOrigin}; +use kludgine::Color; use crate::context::{GraphicsContext, LayoutContext}; use crate::styles::components::{IntrinsicPadding, TextColor}; @@ -15,7 +16,7 @@ use crate::ConstraintLimit; pub struct Label { /// The contents of the label. pub text: Value, - prepared_text: Option>, + prepared_text: Option<(MeasuredText, Px, Color)>, } impl Label { @@ -26,6 +27,31 @@ impl Label { prepared_text: None, } } + + fn prepared_text( + &mut self, + context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + color: Color, + width: Px, + ) -> &MeasuredText { + match &self.prepared_text { + Some((_, prepared_width, prepared_color)) + if *prepared_color == color && *prepared_width == width => {} + _ => { + let measured = self.text.map(|text| { + context + .gfx + .measure_text(Text::new(text, color).wrap_at(width)) + }); + self.prepared_text = Some((measured, width, color)); + } + } + + self.prepared_text + .as_ref() + .map(|(prepared, _, _)| prepared) + .expect("always initialized") + } } impl Widget for Label { @@ -34,25 +60,13 @@ impl Widget for Label { let size = context.gfx.region().size; let center = Point::from(size) / 2; - let styles = context.query_styles(&[&TextColor]); + let text_color = context.query_style(&TextColor); - if let Some(measured) = &self.prepared_text { - context - .gfx - .draw_measured_text(measured, TextOrigin::Center, center, None, None); - } else { - let text_color = styles.get(&TextColor, context); - self.text.map(|contents| { - context.gfx.draw_text( - Text::new(contents, text_color) - .wrap_at(size.width) - .origin(TextOrigin::Center), - center, - None, - None, - ); - }); - } + let prepared_text = self.prepared_text(context, text_color, size.width); + + context + .gfx + .draw_measured_text(prepared_text, TextOrigin::Center, center, None, None); } fn layout( @@ -67,15 +81,11 @@ impl Widget for Label { .into_unsigned(); let color = styles.get(&TextColor, context); let width = available_space.width.max().try_into().unwrap_or(Px::MAX); - self.text.map(|contents| { - let measured = context - .gfx - .measure_text(Text::new(contents, color).wrap_at(width)); - let mut size = measured.size.try_cast().unwrap_or_default(); - size += padding * 2; - self.prepared_text = Some(measured); - size - }) + let prepared = self.prepared_text(context, color, width); + + let mut size = prepared.size.try_cast().unwrap_or_default(); + size += padding * 2; + size } } diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index 7dc81e3..af7b73a 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -1,9 +1,9 @@ use kludgine::figures::units::UPx; -use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Rect, ScreenScale, Size}; +use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, ScreenScale, Size}; use crate::context::{AsEventContext, LayoutContext}; use crate::styles::DimensionRange; -use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; +use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; use crate::ConstraintLimit; /// A widget that resizes its contained widget to an explicit size. @@ -66,7 +66,7 @@ impl WrapperWidget for Resize { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, - ) -> Rect { + ) -> WrappedLayout { let child = self.child.mounted(&mut context.as_event_context()); let size = if let (Some(width), Some(height)) = (self.width.exact_dimension(), self.height.exact_dimension()) diff --git a/src/window.rs b/src/window.rs index 7261433..0dbbf13 100644 --- a/src/window.rs +++ b/src/window.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::ffi::OsStr; -use std::ops::{Deref, DerefMut}; +use std::ops::{Deref, DerefMut, Not}; use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::path::Path; use std::string::ToString; @@ -283,7 +283,7 @@ struct GooeyWindow { max_inner_size: Option>, theme: Option>, current_theme: ThemePair, - theme_mode: Dynamic, + theme_mode: Value, transparent: bool, } @@ -439,9 +439,10 @@ where let theme_mode = match context.settings.borrow_mut().theme_mode.take() { Some(Value::Dynamic(dynamic)) => { dynamic.update(window.theme().into()); - dynamic + Value::Dynamic(dynamic) } - Some(Value::Constant(_)) | None => Dynamic::new(window.theme().into()), + Some(Value::Constant(mode)) => Value::Constant(mode), + None => Value::dynamic(window.theme().into()), }; let transparent = context.settings.borrow().transparent; let mut behavior = T::initialize( @@ -512,7 +513,7 @@ where ), gfx: Exclusive::Owned(Graphics::new(graphics)), }; - context.redraw_when_changed(&self.theme_mode); + self.theme_mode.redraw_when_changed(&context); let mut layout_context = LayoutContext::new(&mut context); let window_size = layout_context.gfx.size(); @@ -994,7 +995,9 @@ where window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { - self.theme_mode.update(window.theme().into()); + if let Value::Dynamic(theme_mode) = &self.theme_mode { + theme_mode.update(window.theme().into()); + } } fn event( @@ -1067,6 +1070,30 @@ pub enum ThemeMode { Dark, } +impl ThemeMode { + /// Returns the opposite mode of `self`. + #[must_use] + pub const fn inverse(self) -> Self { + match self { + ThemeMode::Light => Self::Dark, + ThemeMode::Dark => Self::Light, + } + } + + /// Updates `self` with its [inverse](Self::inverse). + pub fn toggle(&mut self) { + *self = !*self; + } +} + +impl Not for ThemeMode { + type Output = Self; + + fn not(self) -> Self::Output { + self.inverse() + } +} + impl From for ThemeMode { fn from(value: window::Theme) -> Self { match value {