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::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()
}

View file

@ -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)

View file

@ -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<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 {

View file

@ -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<Color> for Component {
@ -705,21 +708,48 @@ impl NamedComponent for Cow<'_, ComponentName> {
pub struct Edges<T = FlexibleDimension> {
/// 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<T> Edges<T> {
/// Returns the sum of the parts as a [`Size`].
pub fn size(&self) -> Size<T>
pub fn size(self) -> Size<T>
where
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 {
fn into_value(self) -> Value<Edges<Dimension>> {
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.
#[derive(Clone, Debug)]
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.
/// 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)),+
])

View file

@ -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<Component: ComponentDefinition>(
&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::<Vec<_>>();
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<Component::ComponentType> {
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
}
}

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.
pub fn get(&self) -> T
where

View file

@ -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<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.
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<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Rect<Px> {
) -> 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<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.
@ -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<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
@ -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<W> FromIterator<W> for Children

View file

@ -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;

View file

@ -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<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Rect<kludgine::figures::units::Px> {
) -> WrappedLayout {
let layout = self.measure(available_space, context);
Rect::new(
@ -196,6 +196,7 @@ impl WrapperWidget for Align {
),
layout.content.into_signed(),
)
.into()
}
}

View file

@ -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

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::{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<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Rect<Px> {
) -> WrappedLayout {
let available_space = Size::new(
ConstraintLimit::Known(available_space.width.max()),
ConstraintLimit::Known(available_space.height.max()),

View file

@ -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<String>,
prepared_text: Option<MeasuredText<Px>>,
prepared_text: Option<(MeasuredText<Px>, 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<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 {
@ -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
}
}

View file

@ -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<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Rect<kludgine::figures::units::Px> {
) -> 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())

View file

@ -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<T> {
max_inner_size: Option<Size<UPx>>,
theme: Option<DynamicReader<ThemePair>>,
current_theme: ThemePair,
theme_mode: Dynamic<ThemeMode>,
theme_mode: Value<ThemeMode>,
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<window::Theme> for ThemeMode {
fn from(value: window::Theme) -> Self {
match value {