Container, query_parent_style

This commit is contained in:
Jonathan Johnson 2023-11-12 13:37:32 -08:00
parent 2a50bb32d4
commit 96d407ddc2
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
17 changed files with 702 additions and 83 deletions

45
examples/containers.rs Normal file
View file

@ -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<ThemeMode>) -> 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()
}

View file

@ -1,6 +1,6 @@
use gooey::value::Dynamic; use gooey::value::Dynamic;
use gooey::widget::{MakeWidget, HANDLED, IGNORED}; use gooey::widget::{MakeWidget, HANDLED, IGNORED};
use gooey::widgets::{Input, Label, Space, Stack}; use gooey::widgets::{Input, Label, Space};
use gooey::Run; use gooey::Run;
use kludgine::app::winit::event::ElementState; use kludgine::app::winit::event::ElementState;
use kludgine::app::winit::keyboard::Key; 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_log = Dynamic::new("Chat log goes here.\n".repeat(100));
let chat_message = Dynamic::new(String::new()); let chat_message = Dynamic::new(String::new());
Stack::rows( Label::new(chat_log.clone())
Stack::columns( .vertical_scroll()
Label::new(chat_log.clone()) .expand()
.vertical_scroll() .and(Space::colored(Color::RED).expand_weighted(2))
.expand() .into_columns()
.and(Space::colored(Color::RED).expand_weighted(2)),
)
.expand() .expand()
.and(Input::new(chat_message.clone()).on_key(move |input| { .and(Input::new(chat_message.clone()).on_key(move |input| {
match (input.state, input.logical_key) { match (input.state, input.logical_key) {
@ -30,8 +28,8 @@ fn main() -> gooey::Result {
} }
_ => IGNORED, _ => IGNORED,
} }
})), }))
) .into_rows()
.expand() .expand()
.run() .run()
} }

View file

@ -8,6 +8,7 @@ use gooey::widget::MakeWidget;
use gooey::widgets::{Input, Label, Scroll, Slider, Stack, Themed}; use gooey::widgets::{Input, Label, Scroll, Slider, Stack, Themed};
use gooey::window::ThemeMode; use gooey::window::ThemeMode;
use gooey::Run; use gooey::Run;
use kludgine::figures::units::Lp;
use kludgine::Color; use kludgine::Color;
const PRIMARY_HUE: f32 = 240.; const PRIMARY_HUE: f32 = 240.;
@ -67,6 +68,7 @@ fn main() -> gooey::Result {
)), )),
), ),
) )
.pad_by(Lp::points(16))
.expand() .expand()
.into_window() .into_window()
.with_theme_mode(theme_mode) .with_theme_mode(theme_mode)

View file

@ -924,7 +924,25 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
pub fn query_styles(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles { pub fn query_styles(&self, query: &[&dyn ComponentDefaultvalue]) -> Styles {
self.current_node self.current_node
.tree .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. /// Queries the widget hierarchy for a single style component.
@ -940,7 +958,19 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
) -> Component::ComponentType { ) -> Component::ComponentType {
self.current_node self.current_node
.tree .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<Component: ComponentDefinition>(
&self,
query: &Component,
) -> Component::ComponentType {
self.current_node
.tree
.query_style(&self.current_node, query, true, self)
} }
pub(crate) fn handle(&self) -> WindowHandle { pub(crate) fn handle(&self) -> WindowHandle {

View file

@ -197,6 +197,9 @@ pub enum Component {
VisualOrder(VisualOrder), VisualOrder(VisualOrder),
/// A description of what widgets should be focusable. /// A description of what widgets should be focusable.
FocusableWidgets(FocusableWidgets), FocusableWidgets(FocusableWidgets),
/// A description of the depth of a
/// [`Container`](crate::widgets::Container).
ContainerLevel(ContainerLevel),
} }
impl From<Color> for Component { impl From<Color> for Component {
@ -705,21 +708,48 @@ impl NamedComponent for Cow<'_, ComponentName> {
pub struct Edges<T = FlexibleDimension> { pub struct Edges<T = FlexibleDimension> {
/// The left edge /// The left edge
pub left: T, pub left: T,
/// The right edge
pub right: T,
/// The top edge /// The top edge
pub top: T, pub top: T,
/// The right edge
pub right: T,
/// The bottom edge /// The bottom edge
pub bottom: T, pub bottom: T,
} }
impl<T> Edges<T> { impl<T> Edges<T> {
/// Returns the sum of the parts as a [`Size`]. /// Returns the sum of the parts as a [`Size`].
pub fn size(&self) -> Size<T> pub fn size(self) -> Size<T>
where where
T: Add<Output = T> + Copy, T: Add<Output = T> + 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<U>(self, mut map: impl FnMut(T) -> U) -> Edges<U> {
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<Output = T>,
{
self.left + self.right
}
/// Returns the sum of the top and bottom edges.
pub fn height(self) -> T
where
T: Add<Output = T>,
{
self.top + self.bottom
} }
} }
@ -821,12 +851,42 @@ impl IntoValue<Edges<FlexibleDimension>> for FlexibleDimension {
} }
} }
impl IntoValue<Edges<FlexibleDimension>> for Dimension {
fn into_value(self) -> Value<Edges<FlexibleDimension>> {
FlexibleDimension::Dimension(self).into_value()
}
}
impl IntoValue<Edges<FlexibleDimension>> for Px {
fn into_value(self) -> Value<Edges<FlexibleDimension>> {
Dimension::from(self).into_value()
}
}
impl IntoValue<Edges<FlexibleDimension>> for Lp {
fn into_value(self) -> Value<Edges<FlexibleDimension>> {
Dimension::from(self).into_value()
}
}
impl IntoValue<Edges<Dimension>> for Dimension { impl IntoValue<Edges<Dimension>> for Dimension {
fn into_value(self) -> Value<Edges<Dimension>> { fn into_value(self) -> Value<Edges<Dimension>> {
Value::Constant(Edges::from(self)) Value::Constant(Edges::from(self))
} }
} }
impl IntoValue<Edges<Dimension>> for Px {
fn into_value(self) -> Value<Edges<Dimension>> {
Dimension::from(self).into_value()
}
}
impl IntoValue<Edges<Dimension>> for Lp {
fn into_value(self) -> Value<Edges<Dimension>> {
Dimension::from(self).into_value()
}
}
/// A set of light and dark [`Theme`]s. /// A set of light and dark [`Theme`]s.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ThemePair { pub struct ThemePair {
@ -1465,3 +1525,52 @@ impl TryFrom<Component> 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<Self> {
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<ContainerLevel> for Component {
fn from(value: ContainerLevel) -> Self {
Self::ContainerLevel(value)
}
}
impl TryFrom<Component> for ContainerLevel {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::ContainerLevel(level) => Ok(level),
other => Err(other),
}
}
}

View file

@ -26,9 +26,11 @@ use crate::styles::{Dimension, FocusableWidgets, VisualOrder};
/// /// This component whose default value is a color from the current theme. /// /// This component whose default value is a color from the current theme.
/// ThemedComponent(Color, "themed_component", .primary.color) /// ThemedComponent(Color, "themed_component", .primary.color)
/// /// This component is a color whose default value is the currently defined `TextColor`. /// /// 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` /// /// This component defaults to picking a contrasting color between `TextColor` and `SurfaceColor`
/// ContrastingColor(Color, "contrasting_color", contrasting!(ThemedComponent, TextColor, 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),+ $(,)?)) => { ($type:ty, contrasting!($bg:ident, $($fg:ident),+ $(,)?)) => {
define_components!($type, |context| { define_components!($type, |context| {
use $crate::styles::ColorExt; 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(&$bg, context).most_contrasting(&[
$(styles.get(&$fg, context)),+ $(styles.get(&$fg, context)),+
]) ])

View file

@ -317,24 +317,29 @@ impl Tree {
&self, &self,
perspective: &ManagedWidget, perspective: &ManagedWidget,
query: &[&dyn ComponentDefaultvalue], query: &[&dyn ComponentDefaultvalue],
skip_current: bool,
context: &WidgetContext<'_, '_>, context: &WidgetContext<'_, '_>,
) -> Styles { ) -> Styles {
self.data self.data
.lock() .lock()
.map_or_else(PoisonError::into_inner, |g| g) .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<Component: ComponentDefinition>( pub fn query_style<Component: ComponentDefinition>(
&self, &self,
perspective: &ManagedWidget, perspective: &ManagedWidget,
component: &Component, component: &Component,
skip_self: bool,
context: &WidgetContext<'_, '_>, context: &WidgetContext<'_, '_>,
) -> Component::ComponentType { ) -> Component::ComponentType {
self.data let result = self
.data
.lock() .lock()
.map_or_else(PoisonError::into_inner, |g| g) .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, &self,
mut perspective: WidgetId, mut perspective: WidgetId,
query: &[&dyn ComponentDefaultvalue], query: &[&dyn ComponentDefaultvalue],
skip_self: bool,
context: &WidgetContext<'_, '_>, context: &WidgetContext<'_, '_>,
) -> Styles { ) -> Styles {
let mut query = query.iter().map(|n| n.name()).collect::<Vec<_>>(); let mut query = query.iter().map(|n| n.name()).collect::<Vec<_>>();
let mut resolved = Styles::new(); let mut resolved = Styles::new();
let mut skip_next = skip_self;
while !query.is_empty() { while !query.is_empty() {
let node = &self.nodes[&perspective]; 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| { styles.map_tracked(context, |styles| {
query.retain(|name| { query.retain(|name| {
if let Some(component) = styles.get_named(name) { if let Some(component) = styles.get_named(name) {
@ -451,12 +460,16 @@ impl TreeData {
&self, &self,
mut perspective: WidgetId, mut perspective: WidgetId,
query: &Component, query: &Component,
skip_self: bool,
context: &WidgetContext<'_, '_>, context: &WidgetContext<'_, '_>,
) -> Component::ComponentType { ) -> Option<Component::ComponentType> {
let name = query.name(); let name = query.name();
let mut skip_next = skip_self;
loop { loop {
let node = &self.nodes[&perspective]; 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| { match styles.map_tracked(context, |styles| {
if let Some(component) = styles.get_named(&name) { if let Some(component) = styles.get_named(&name) {
let Ok(value) = let Ok(value) =
@ -469,15 +482,16 @@ impl TreeData {
} }
Ok(None) Ok(None)
}) { }) {
Ok(Some(value)) => return value, Ok(Some(value)) => return Some(value),
Ok(None) => {} Ok(None) => {}
Err(()) => break, Err(()) => break,
} }
} }
let Some(parent) = node.parent else { break }; let Some(parent) = node.parent else { break };
perspective = parent; perspective = parent;
} }
query.default_value(context) None
} }
} }

View file

@ -954,6 +954,20 @@ impl<T> Value<T> {
} }
} }
/// Returns a new value that is updated using `U::from(T.clone())` each time
/// `self` is updated.
#[must_use]
pub fn map_each<R, F>(&self, mut map: F) -> Value<R>
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. /// Returns a clone of the currently stored value.
pub fn get(&self) -> T pub fn get(&self) -> T
where where

View file

@ -13,12 +13,16 @@ use kludgine::app::winit::event::{
}; };
use kludgine::figures::units::{Px, UPx}; use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size}; use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, Size};
use kludgine::Color;
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext};
use crate::styles::{IntoComponentValue, NamedComponent, Styles, ThemePair, VisualOrder}; use crate::styles::{
ContainerLevel, Dimension, Edges, IntoComponentValue, NamedComponent, Styles, ThemePair,
VisualOrder,
};
use crate::tree::Tree; use crate::tree::Tree;
use crate::value::{IntoValue, Value}; 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::window::{RunningWindow, ThemeMode, Window, WindowBehavior};
use crate::{ConstraintLimit, Run}; 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<Px>,
/// The size the wrapper widget should report as.q
pub size: Size<UPx>,
}
impl From<Rect<Px>> for WrappedLayout {
fn from(child: Rect<Px>) -> Self {
WrappedLayout {
child,
size: child.size.into_unsigned(),
}
}
}
impl From<Size<Px>> for WrappedLayout {
fn from(size: Size<Px>) -> Self {
WrappedLayout {
child: size.into(),
size: size.into_unsigned(),
}
}
}
/// A [`Widget`] that contains a single child. /// A [`Widget`] that contains a single child.
pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static {
/// Returns the child widget. /// Returns the child widget.
@ -185,14 +216,48 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static {
&mut self, &mut self,
available_space: Size<ConstraintLimit>, available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>, context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Rect<Px> { ) -> WrappedLayout {
let child = self.child_mut().mounted(&mut context.as_event_context()); 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) .for_other(&child)
.layout(available_space) .layout(adjusted_space)
.into_signed() .into_signed();
.into()
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<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<ConstraintLimit> {
available_space
}
/// Returns the layout after positioning the child that occupies `size`.
#[allow(unused_variables)]
#[must_use]
fn position_child(
&mut self,
size: Size<Px>,
available_space: Size<ConstraintLimit>,
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<Color> {
// 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. /// The widget has been mounted into a parent widget.
@ -323,6 +388,11 @@ where
} }
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { 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()); let child = self.child_mut().mounted(&mut context.as_event_context());
context.for_other(&child).redraw(); context.for_other(&child).redraw();
} }
@ -335,8 +405,8 @@ where
let child = self.child_mut().mounted(&mut context.as_event_context()); let child = self.child_mut().mounted(&mut context.as_event_context());
let layout = self.layout_child(available_space, context); let layout = self.layout_child(available_space, context);
context.set_child_layout(&child, layout); context.set_child_layout(&child, layout.child);
layout.size.into_unsigned() layout.size
} }
fn mounted(&mut self, context: &mut EventContext<'_, '_>) { fn mounted(&mut self, context: &mut EventContext<'_, '_>) {
@ -577,6 +647,31 @@ pub trait MakeWidget: Sized {
fn widget_ref(self) -> WidgetRef { fn widget_ref(self) -> WidgetRef {
WidgetRef::new(self) 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<ContainerLevel>) -> Container {
self.contain().contain_level(level)
}
/// Returns a new widget that renders `color` behind `self`.
fn background_color(self, color: impl IntoValue<Color>) -> 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<Edges<Dimension>>) -> Container {
self.contain().transparent().pad_by(padding)
}
} }
/// A type that can create a [`WidgetInstance`] with a preallocated /// A type that can create a [`WidgetInstance`] with a preallocated
@ -1096,6 +1191,18 @@ impl Children {
pub fn truncate(&mut self, length: usize) { pub fn truncate(&mut self, length: usize) {
self.ordered.truncate(length); 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<W> FromIterator<W> for Children impl<W> FromIterator<W> for Children

View file

@ -3,6 +3,7 @@
mod align; mod align;
pub mod button; pub mod button;
mod canvas; mod canvas;
pub mod container;
mod expand; mod expand;
mod input; mod input;
pub mod label; pub mod label;
@ -19,6 +20,7 @@ mod tilemap;
pub use align::Align; pub use align::Align;
pub use button::Button; pub use button::Button;
pub use canvas::Canvas; pub use canvas::Canvas;
pub use container::Container;
pub use expand::Expand; pub use expand::Expand;
pub use input::Input; pub use input::Input;
pub use label::Label; pub use label::Label;

View file

@ -6,7 +6,7 @@ use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenS
use crate::context::{AsEventContext, LayoutContext}; use crate::context::{AsEventContext, LayoutContext};
use crate::styles::{Edges, FlexibleDimension}; use crate::styles::{Edges, FlexibleDimension};
use crate::value::{IntoValue, Value}; use crate::value::{IntoValue, Value};
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget};
use crate::ConstraintLimit; use crate::ConstraintLimit;
/// A widget aligns its contents to its container's boundaries. /// A widget aligns its contents to its container's boundaries.
@ -107,8 +107,8 @@ impl Align {
Layout { Layout {
margin: Edges { margin: Edges {
left, left,
right,
top, top,
right,
bottom, bottom,
}, },
content: Size::new(width, height), content: Size::new(width, height),
@ -186,7 +186,7 @@ impl WrapperWidget for Align {
&mut self, &mut self,
available_space: Size<ConstraintLimit>, available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>, context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Rect<kludgine::figures::units::Px> { ) -> WrappedLayout {
let layout = self.measure(available_space, context); let layout = self.measure(available_space, context);
Rect::new( Rect::new(
@ -196,6 +196,7 @@ impl WrapperWidget for Align {
), ),
layout.content.into_signed(), layout.content.into_signed(),
) )
.into()
} }
} }

View file

@ -315,7 +315,7 @@ impl Widget for Button {
define_components! { define_components! {
Button { Button {
/// The background color of the 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). /// The background color of the button when it is active (depressed).
ButtonActiveBackground(Color, "active_background_color", .surface.color) ButtonActiveBackground(Color, "active_background_color", .surface.color)
/// The background color of the button when the mouse cursor is hovering over /// The background color of the button when the mouse cursor is hovering over

258
src/widgets/container.rs Normal file
View file

@ -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<ContainerBackground>,
/// Padding to surround the contained widget.
///
/// If this is None, a uniform surround of [`IntrinsicPadding`] will be
/// applied.
pub padding: Option<Value<Edges<Dimension>>>,
child: WidgetRef,
effective_background: Option<EffectiveBackground>,
}
/// 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<ContainerLevel> for ContainerBackground {
fn from(value: ContainerLevel) -> Self {
Self::Level(value)
}
}
impl From<Color> 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<Edges<Dimension>>) -> 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<ContainerLevel>) -> 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<Color>) -> Self {
self.background = color
.into_value()
.map_each(|color| ContainerBackground::from(*color));
self
}
fn padding(&self, context: &GraphicsContext<'_, '_, '_, '_, '_>) -> Edges<Px> {
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<kludgine::Color> {
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<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<ConstraintLimit> {
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<Px>,
_available_space: Size<ConstraintLimit>,
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<Component> for EffectiveBackground {
type Error = Component;
fn try_from(value: Component) -> Result<Self, Self::Error> {
match value {
Component::Color(color) => Ok(EffectiveBackground::Color(color)),
Component::ContainerLevel(level) => Ok(EffectiveBackground::Level(level)),
other => Err(other),
}
}
}
impl From<EffectiveBackground> 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)))
}
}

View file

@ -1,8 +1,8 @@
use kludgine::figures::units::{Px, UPx}; use kludgine::figures::units::UPx;
use kludgine::figures::{IntoSigned, Rect, Size}; use kludgine::figures::{IntoSigned, Size};
use crate::context::{AsEventContext, LayoutContext}; use crate::context::{AsEventContext, LayoutContext};
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget};
use crate::widgets::Space; use crate::widgets::Space;
use crate::ConstraintLimit; use crate::ConstraintLimit;
@ -71,7 +71,7 @@ impl WrapperWidget for Expand {
&mut self, &mut self,
available_space: Size<ConstraintLimit>, available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>, context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Rect<Px> { ) -> WrappedLayout {
let available_space = Size::new( let available_space = Size::new(
ConstraintLimit::Known(available_space.width.max()), ConstraintLimit::Known(available_space.width.max()),
ConstraintLimit::Known(available_space.height.max()), ConstraintLimit::Known(available_space.height.max()),

View file

@ -3,6 +3,7 @@
use kludgine::figures::units::{Px, UPx}; use kludgine::figures::units::{Px, UPx};
use kludgine::figures::{IntoUnsigned, Point, ScreenScale, Size}; use kludgine::figures::{IntoUnsigned, Point, ScreenScale, Size};
use kludgine::text::{MeasuredText, Text, TextOrigin}; use kludgine::text::{MeasuredText, Text, TextOrigin};
use kludgine::Color;
use crate::context::{GraphicsContext, LayoutContext}; use crate::context::{GraphicsContext, LayoutContext};
use crate::styles::components::{IntrinsicPadding, TextColor}; use crate::styles::components::{IntrinsicPadding, TextColor};
@ -15,7 +16,7 @@ use crate::ConstraintLimit;
pub struct Label { pub struct Label {
/// The contents of the label. /// The contents of the label.
pub text: Value<String>, pub text: Value<String>,
prepared_text: Option<MeasuredText<Px>>, prepared_text: Option<(MeasuredText<Px>, Px, Color)>,
} }
impl Label { impl Label {
@ -26,6 +27,31 @@ impl Label {
prepared_text: None, prepared_text: None,
} }
} }
fn prepared_text(
&mut self,
context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
color: Color,
width: Px,
) -> &MeasuredText<Px> {
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 { impl Widget for Label {
@ -34,25 +60,13 @@ impl Widget for Label {
let size = context.gfx.region().size; let size = context.gfx.region().size;
let center = Point::from(size) / 2; 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 { let prepared_text = self.prepared_text(context, text_color, size.width);
context
.gfx context
.draw_measured_text(measured, TextOrigin::Center, center, None, None); .gfx
} else { .draw_measured_text(prepared_text, TextOrigin::Center, center, None, None);
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,
);
});
}
} }
fn layout( fn layout(
@ -67,15 +81,11 @@ impl Widget for Label {
.into_unsigned(); .into_unsigned();
let color = styles.get(&TextColor, context); let color = styles.get(&TextColor, context);
let width = available_space.width.max().try_into().unwrap_or(Px::MAX); let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
self.text.map(|contents| { let prepared = self.prepared_text(context, color, width);
let measured = context
.gfx let mut size = prepared.size.try_cast().unwrap_or_default();
.measure_text(Text::new(contents, color).wrap_at(width)); size += padding * 2;
let mut size = measured.size.try_cast().unwrap_or_default(); size
size += padding * 2;
self.prepared_text = Some(measured);
size
})
} }
} }

View file

@ -1,9 +1,9 @@
use kludgine::figures::units::UPx; 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::context::{AsEventContext, LayoutContext};
use crate::styles::DimensionRange; use crate::styles::DimensionRange;
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget};
use crate::ConstraintLimit; use crate::ConstraintLimit;
/// A widget that resizes its contained widget to an explicit size. /// A widget that resizes its contained widget to an explicit size.
@ -66,7 +66,7 @@ impl WrapperWidget for Resize {
&mut self, &mut self,
available_space: Size<ConstraintLimit>, available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>, context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Rect<kludgine::figures::units::Px> { ) -> WrappedLayout {
let child = self.child.mounted(&mut context.as_event_context()); let child = self.child.mounted(&mut context.as_event_context());
let size = if let (Some(width), Some(height)) = let size = if let (Some(width), Some(height)) =
(self.width.exact_dimension(), self.height.exact_dimension()) (self.width.exact_dimension(), self.height.exact_dimension())

View file

@ -4,7 +4,7 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut, Not};
use std::panic::{AssertUnwindSafe, UnwindSafe}; use std::panic::{AssertUnwindSafe, UnwindSafe};
use std::path::Path; use std::path::Path;
use std::string::ToString; use std::string::ToString;
@ -283,7 +283,7 @@ struct GooeyWindow<T> {
max_inner_size: Option<Size<UPx>>, max_inner_size: Option<Size<UPx>>,
theme: Option<DynamicReader<ThemePair>>, theme: Option<DynamicReader<ThemePair>>,
current_theme: ThemePair, current_theme: ThemePair,
theme_mode: Dynamic<ThemeMode>, theme_mode: Value<ThemeMode>,
transparent: bool, transparent: bool,
} }
@ -439,9 +439,10 @@ where
let theme_mode = match context.settings.borrow_mut().theme_mode.take() { let theme_mode = match context.settings.borrow_mut().theme_mode.take() {
Some(Value::Dynamic(dynamic)) => { Some(Value::Dynamic(dynamic)) => {
dynamic.update(window.theme().into()); 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 transparent = context.settings.borrow().transparent;
let mut behavior = T::initialize( let mut behavior = T::initialize(
@ -512,7 +513,7 @@ where
), ),
gfx: Exclusive::Owned(Graphics::new(graphics)), 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 mut layout_context = LayoutContext::new(&mut context);
let window_size = layout_context.gfx.size(); let window_size = layout_context.gfx.size();
@ -994,7 +995,9 @@ where
window: kludgine::app::Window<'_, WindowCommand>, window: kludgine::app::Window<'_, WindowCommand>,
_kludgine: &mut Kludgine, _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( fn event(
@ -1067,6 +1070,30 @@ pub enum ThemeMode {
Dark, 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<window::Theme> for ThemeMode { impl From<window::Theme> for ThemeMode {
fn from(value: window::Theme) -> Self { fn from(value: window::Theme) -> Self {
match value { match value {