cushy/src/widgets/container.rs
Jonathan Johnson e15ae59c5c
Refactored root resize behavior
Closes #84, Closes #77, Closes #78
2023-11-25 12:00:59 -08:00

270 lines
9.7 KiB
Rust

//! A visual container widget.
use kludgine::figures::units::Px;
use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size};
use kludgine::Color;
use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext};
use crate::styles::components::{IntrinsicPadding, SurfaceColor};
use crate::styles::{Component, ContainerLevel, Dimension, Edges, RequireInvalidation, Styles};
use crate::value::{IntoValue, Value};
use crate::widget::{MakeWidget, RootBehavior, 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.get(&IntrinsicPadding)),
}
.map(|dim| dim.into_px(context.gfx.scale()))
}
}
impl WrapperWidget for Container {
fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.child
}
fn root_behavior(&mut self, _context: &mut EventContext<'_, '_>) -> Option<RootBehavior> {
Some(
self.padding
.as_ref()
.map_or(RootBehavior::PassThrough, |padding| {
RootBehavior::Pad(padding.get())
}),
)
}
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.get(&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_constraints(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<ConstraintLimit> {
let padding_amount = self.padding(context).size().into_upx(context.gfx.scale());
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),
}
}
}
impl RequireInvalidation for EffectiveBackground {
fn requires_invalidation(&self) -> bool {
false
}
}
define_components! {
Container {
/// The container background behind the current widget.
CurrentContainerBackground(EffectiveBackground, "background", |context| EffectiveBackground::Color(context.get(&SurfaceColor)))
}
}