mirror of
https://github.com/danbulant/cushy
synced 2026-06-17 13:31:07 +00:00
Container Shadows
This commit is contained in:
parent
87fa4a3478
commit
a7efe9a3d5
7 changed files with 786 additions and 64 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -623,7 +623,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "figures"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/figures#5b84202e60b52cc2c57c0e49031fe1a0ff87b9ec"
|
||||
source = "git+https://github.com/khonsulabs/figures#aaee0e99cf5c3ad5a7e907819eceeef9b897fe13"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"euclid",
|
||||
|
|
@ -1062,7 +1062,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
|||
[[package]]
|
||||
name = "kludgine"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#32b7e160966c58d129657f0762ecb79a4ce482a3"
|
||||
source = "git+https://github.com/khonsulabs/kludgine#52f58c39b0ab4e1e280d27f7228e8ccd9a377f50"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"alot",
|
||||
|
|
@ -2135,9 +2135,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.39"
|
||||
version = "2.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
|
||||
checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -3011,9 +3011,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.26"
|
||||
version = "0.5.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67b5f0a4e7a27a64c651977932b9dc5667ca7fc31ac44b03ed37a0cf42fdfff"
|
||||
checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
|
|
|||
79
examples/container-shadow.rs
Normal file
79
examples/container-shadow.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
use gooey::styles::components::CornerRadius;
|
||||
use gooey::styles::Dimension;
|
||||
use gooey::value::{Dynamic, MapEachCloned};
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::container::ContainerShadow;
|
||||
use gooey::widgets::slider::Slidable;
|
||||
use gooey::widgets::Space;
|
||||
use gooey::Run;
|
||||
use kludgine::figures::units::Lp;
|
||||
use kludgine::figures::{Point, Size};
|
||||
use kludgine::shapes::CornerRadii;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let top_left = Dynamic::new(Lp::mm(1));
|
||||
let top_right = Dynamic::new(Lp::mm(1));
|
||||
let bottom_right = Dynamic::new(Lp::mm(1));
|
||||
let bottom_left = Dynamic::new(Lp::mm(1));
|
||||
let corners = (&top_left, &top_right, &bottom_right, &bottom_left).map_each_cloned(
|
||||
|(top_left, top_right, bottom_right, bottom_left)| {
|
||||
CornerRadii {
|
||||
top_left,
|
||||
top_right,
|
||||
bottom_right,
|
||||
bottom_left,
|
||||
}
|
||||
.map(Dimension::from)
|
||||
},
|
||||
);
|
||||
|
||||
let offset_x = Dynamic::new(Lp::ZERO);
|
||||
let offset_y = Dynamic::new(Lp::ZERO);
|
||||
let offset = (&offset_x, &offset_y).map_each_cloned(|(x, y)| Point::new(x, y));
|
||||
|
||||
let radius = Dynamic::new(Lp::mm(1));
|
||||
let spread = Dynamic::new(Lp::mm(1));
|
||||
|
||||
let shadow = (&offset, &radius, &spread).map_each_cloned(|(offset, radius, spread)| {
|
||||
ContainerShadow::new(offset)
|
||||
.blur_radius(radius)
|
||||
.spread(spread)
|
||||
});
|
||||
|
||||
"Corner Radii"
|
||||
.h3()
|
||||
.and("Top Left")
|
||||
.and(top_left.slider_between(Lp::ZERO, Lp::inches(1)))
|
||||
.and("Top right")
|
||||
.and(top_right.slider_between(Lp::ZERO, Lp::inches(1)))
|
||||
.and("Bottom Right")
|
||||
.and(bottom_right.slider_between(Lp::ZERO, Lp::inches(1)))
|
||||
.and("Bottom Left")
|
||||
.and(bottom_left.slider_between(Lp::ZERO, Lp::inches(1)))
|
||||
.and("Shadow".h3())
|
||||
.and("Offset X")
|
||||
.and(offset_x.slider_between(Lp::inches_f(-0.5), Lp::inches_f(0.5)))
|
||||
.and("Offset Y")
|
||||
.and(offset_y.slider_between(Lp::inches_f(-0.5), Lp::inches_f(0.5)))
|
||||
.and("Radius")
|
||||
.and(radius.slider_between(Lp::ZERO, Lp::inches_f(0.5)))
|
||||
.and("Spread")
|
||||
.and(spread.slider_between(Lp::ZERO, Lp::inches_f(0.5)))
|
||||
.into_rows()
|
||||
.and(
|
||||
"Preview"
|
||||
.h3()
|
||||
.and(
|
||||
Space::clear()
|
||||
.size(Size::squared(Lp::inches(2)))
|
||||
.contain()
|
||||
.shadow(shadow)
|
||||
.with(&CornerRadius, corners),
|
||||
)
|
||||
.into_rows(),
|
||||
)
|
||||
.into_columns()
|
||||
.contain()
|
||||
.centered()
|
||||
.run()
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
use gooey::value::Dynamic;
|
||||
use gooey::widget::{MakeWidget, WidgetInstance};
|
||||
use gooey::widgets::container::ContainerShadow;
|
||||
use gooey::window::ThemeMode;
|
||||
use gooey::{Gooey, Run};
|
||||
use kludgine::figures::units::Lp;
|
||||
use kludgine::figures::Point;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let theme_mode = Dynamic::default();
|
||||
|
|
@ -30,17 +33,31 @@ fn set_of_containers(repeat: usize, theme_mode: Dynamic<ThemeMode>) -> WidgetIns
|
|||
"Mid"
|
||||
.and(
|
||||
"High"
|
||||
.and("Highest".and(inner).into_rows().contain())
|
||||
.and(
|
||||
"Highest"
|
||||
.and(inner)
|
||||
.into_rows()
|
||||
.contain()
|
||||
.shadow(drop_shadow()),
|
||||
)
|
||||
.into_rows()
|
||||
.contain(),
|
||||
.contain()
|
||||
.shadow(drop_shadow()),
|
||||
)
|
||||
.into_rows()
|
||||
.contain(),
|
||||
.contain()
|
||||
.shadow(drop_shadow()),
|
||||
)
|
||||
.into_rows()
|
||||
.contain(),
|
||||
.contain()
|
||||
.shadow(drop_shadow()),
|
||||
)
|
||||
.into_rows()
|
||||
.contain()
|
||||
.shadow(drop_shadow())
|
||||
.make_widget()
|
||||
}
|
||||
|
||||
fn drop_shadow() -> ContainerShadow<Lp> {
|
||||
ContainerShadow::new(Point::new(Lp::ZERO, Lp::mm(1))).spread(Lp::mm_f(1.))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -774,6 +774,14 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> LayoutContext<'context, 'window, 'cl
|
|||
}
|
||||
}
|
||||
|
||||
impl<'context, 'window, 'clip, 'gfx, 'pass> AsEventContext<'window>
|
||||
for LayoutContext<'context, 'window, 'clip, 'gfx, 'pass>
|
||||
{
|
||||
fn as_event_context(&mut self) -> EventContext<'_, 'window> {
|
||||
self.graphics.as_event_context()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'context, 'window, 'clip, 'gfx, 'pass> Deref
|
||||
for LayoutContext<'context, 'window, 'clip, 'gfx, 'pass>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1172,6 +1172,22 @@ impl<T> Edges<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<Unit> Zero for Edges<Unit>
|
||||
where
|
||||
Unit: Zero,
|
||||
{
|
||||
const ZERO: Self = Self {
|
||||
left: Unit::ZERO,
|
||||
top: Unit::ZERO,
|
||||
right: Unit::ZERO,
|
||||
bottom: Unit::ZERO,
|
||||
};
|
||||
|
||||
fn is_zero(&self) -> bool {
|
||||
self.left.is_zero() && self.top.is_zero() && self.right.is_zero() && self.bottom.is_zero()
|
||||
}
|
||||
}
|
||||
|
||||
impl Edges<Dimension> {
|
||||
/// Returns a new instance with `dimension` for every edge.
|
||||
#[must_use]
|
||||
|
|
|
|||
|
|
@ -1822,14 +1822,17 @@ impl WidgetRef {
|
|||
}
|
||||
|
||||
/// Returns this child, mounting it in the process if necessary.
|
||||
pub fn mount_if_needed(&mut self, context: &mut EventContext<'_, '_>) {
|
||||
pub fn mount_if_needed<'window>(&mut self, context: &mut impl AsEventContext<'window>) {
|
||||
if let WidgetRef::Unmounted(instance) = self {
|
||||
*self = WidgetRef::Mounted(context.push_child(instance.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns this child, mounting it in the process if necessary.
|
||||
pub fn mounted(&mut self, context: &mut EventContext<'_, '_>) -> ManagedWidget {
|
||||
pub fn mounted<'window>(
|
||||
&mut self,
|
||||
context: &mut impl AsEventContext<'window>,
|
||||
) -> ManagedWidget {
|
||||
self.mount_if_needed(context);
|
||||
|
||||
let Self::Mounted(widget) = self else {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
//! A visual container widget.
|
||||
|
||||
use kludgine::figures::units::Px;
|
||||
use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{
|
||||
Abs, Angle, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Zero,
|
||||
};
|
||||
use kludgine::shapes::{CornerRadii, PathBuilder, Shape};
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
||||
use crate::styles::components::{IntrinsicPadding, SurfaceColor};
|
||||
use crate::styles::components::{CornerRadius, 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::value::{Dynamic, IntoValue, Value};
|
||||
use crate::widget::{MakeWidget, RootBehavior, Widget, WidgetInstance, WidgetRef};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A visual container widget, optionally applying padding and a background
|
||||
|
|
@ -38,8 +41,10 @@ pub struct Container {
|
|||
/// If this is None, a uniform surround of [`IntrinsicPadding`] will be
|
||||
/// applied.
|
||||
pub padding: Option<Value<Edges<Dimension>>>,
|
||||
/// The shadow to apply behind the container's background.
|
||||
pub shadow: Value<ContainerShadow>,
|
||||
child: WidgetRef,
|
||||
effective_background: Option<EffectiveBackground>,
|
||||
applied_background: Option<EffectiveBackground>,
|
||||
}
|
||||
|
||||
/// A strategy of applying a background to a [`Container`].
|
||||
|
|
@ -103,8 +108,9 @@ impl Container {
|
|||
pub fn new(child: impl MakeWidget) -> Self {
|
||||
Self {
|
||||
padding: None,
|
||||
effective_background: None,
|
||||
applied_background: None,
|
||||
background: Value::default(),
|
||||
shadow: Value::default(),
|
||||
child: WidgetRef::new(child),
|
||||
}
|
||||
}
|
||||
|
|
@ -145,6 +151,13 @@ impl Container {
|
|||
self
|
||||
}
|
||||
|
||||
/// Renders `shadow` behind the container's background.
|
||||
#[must_use]
|
||||
pub fn shadow(mut self, shadow: impl IntoValue<ContainerShadow>) -> Self {
|
||||
self.shadow = shadow.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
fn padding(&self, context: &GraphicsContext<'_, '_, '_, '_, '_>) -> Edges<Px> {
|
||||
match &self.padding {
|
||||
Some(padding) => padding.get(),
|
||||
|
|
@ -152,24 +165,13 @@ impl Container {
|
|||
}
|
||||
.map(|dim| dim.into_px(context.gfx.scale()))
|
||||
}
|
||||
}
|
||||
|
||||
impl WrapperWidget for Container {
|
||||
fn child_mut(&mut self) -> &mut WidgetRef {
|
||||
&mut self.child
|
||||
fn effective_shadow(&self, context: &WidgetContext<'_, '_>) -> ContainerShadow {
|
||||
self.shadow.invalidate_when_changed(context);
|
||||
self.shadow.get()
|
||||
}
|
||||
|
||||
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> {
|
||||
fn effective_background_color(&mut self, context: &WidgetContext<'_, '_>) -> kludgine::Color {
|
||||
let background = match self.background.get() {
|
||||
ContainerBackground::Color(color) => EffectiveBackground::Color(color),
|
||||
ContainerBackground::Level(level) => EffectiveBackground::Level(level),
|
||||
|
|
@ -181,12 +183,12 @@ impl WrapperWidget for Container {
|
|||
}
|
||||
};
|
||||
|
||||
if self.effective_background != Some(background) {
|
||||
if self.applied_background != Some(background) {
|
||||
context.attach_styles(Styles::new().with(&CurrentContainerBackground, background));
|
||||
self.effective_background = Some(background);
|
||||
self.applied_background = Some(background);
|
||||
}
|
||||
|
||||
Some(match background {
|
||||
match background {
|
||||
EffectiveBackground::Color(color) => color,
|
||||
EffectiveBackground::Level(level) => match level {
|
||||
ContainerLevel::Lowest => context.theme().surface.lowest_container,
|
||||
|
|
@ -195,43 +197,412 @@ impl WrapperWidget for 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Container {
|
||||
fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fmt.debug_struct("Container")
|
||||
.field("background", &self.background)
|
||||
.field("padding", &self.padding)
|
||||
.field("shadow", &self.shadow)
|
||||
.field("child", &self.child)
|
||||
.finish()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
let background = self.effective_background_color(context);
|
||||
if background.alpha() > 0 {
|
||||
let shadow = self.effective_shadow(context).into_px(context.gfx.scale());
|
||||
|
||||
let child_shadow_offset = shadow.offset.min(Point::ZERO).abs();
|
||||
let child_size = context.gfx.region().size - shadow.spread * 2 - shadow.offset.abs();
|
||||
let child_area = Rect::new(child_shadow_offset + shadow.spread, child_size);
|
||||
|
||||
let corner_radii = context.get(&CornerRadius).into_px(context.gfx.scale());
|
||||
|
||||
// check if the shadow would be obscured before we try to draw it.
|
||||
if child_area.origin != Point::ZERO || child_size != context.gfx.region().size {
|
||||
render_shadow(&child_area, &corner_radii, &shadow, background, context);
|
||||
}
|
||||
|
||||
context.gfx.draw_shape(&Shape::filled_round_rect(
|
||||
child_area,
|
||||
corner_radii,
|
||||
background,
|
||||
));
|
||||
}
|
||||
|
||||
let child = self.child.mounted(context);
|
||||
context.for_other(&child).redraw();
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let child = self.child.mounted(context);
|
||||
|
||||
let padding = self.padding(context).into_upx(context.gfx.scale());
|
||||
let padding_amount = padding.size();
|
||||
let shadow = self.effective_shadow(context).into_px(context.gfx.scale());
|
||||
let shadow_spread = shadow.spread.into_unsigned();
|
||||
|
||||
let child_shadow_offset_amount = shadow.offset.abs().into_unsigned();
|
||||
let child_size = context.for_other(&child).layout(
|
||||
available_space - padding_amount - child_shadow_offset_amount - shadow_spread * 2,
|
||||
);
|
||||
|
||||
let child_shadow_offset = shadow.offset.min(Point::ZERO).abs().into_unsigned();
|
||||
context.set_child_layout(
|
||||
&child,
|
||||
Rect::new(
|
||||
Point::new(padding.left, padding.top) + child_shadow_offset + shadow_spread,
|
||||
child_size,
|
||||
)
|
||||
.into_signed(),
|
||||
);
|
||||
|
||||
child_size + padding_amount + child_shadow_offset_amount + shadow_spread * 2
|
||||
}
|
||||
|
||||
fn root_behavior(
|
||||
&mut self,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> Option<(RootBehavior, WidgetInstance)> {
|
||||
// TODO adjust for shadow, but we need to potentially merge multiple
|
||||
// dimensions into one.
|
||||
let mut padding = self
|
||||
.padding
|
||||
.as_ref()
|
||||
.map(|padding| padding.get().into_px(context.kludgine.scale()))
|
||||
.unwrap_or_default();
|
||||
let shadow = self
|
||||
.effective_shadow(context)
|
||||
.into_px(context.kludgine.scale());
|
||||
|
||||
if shadow.offset.x >= 0 {
|
||||
padding.right += shadow.offset.x;
|
||||
} else {
|
||||
padding.left += shadow.offset.x.abs();
|
||||
}
|
||||
|
||||
if shadow.spread > 0 {
|
||||
padding += Edges::from(shadow.spread);
|
||||
}
|
||||
|
||||
let behavior = if padding.is_zero() {
|
||||
RootBehavior::PassThrough
|
||||
} else {
|
||||
RootBehavior::Pad(padding.map(Dimension::from))
|
||||
};
|
||||
Some((behavior, self.child.widget().clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn render_shadow(
|
||||
child_area: &Rect<Px>,
|
||||
corner_radii: &CornerRadii<Px>,
|
||||
shadow: &ContainerShadow<Px>,
|
||||
background: Color,
|
||||
context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
|
||||
) {
|
||||
let shadow_color = shadow.color.unwrap_or_else(|| context.theme_pair().shadow);
|
||||
let shadow_color =
|
||||
shadow_color.with_alpha_f32(shadow_color.alpha_f32() * background.alpha_f32());
|
||||
|
||||
let gradient_size = (shadow.spread + shadow.blur_radius)
|
||||
.min(child_area.size.width)
|
||||
.min(child_area.size.height);
|
||||
if gradient_size > 0 {
|
||||
let mut solid_area = Rect::new(
|
||||
Point::squared(gradient_size),
|
||||
child_area.size - shadow.blur_radius * 2,
|
||||
);
|
||||
solid_area.origin += shadow.offset.max(Point::ZERO);
|
||||
|
||||
let transparent = shadow_color.with_alpha(0);
|
||||
let solid_left = solid_area.origin.x;
|
||||
let solid_right = solid_area.origin.x + solid_area.size.width;
|
||||
let solid_top = solid_area.origin.y;
|
||||
let solid_bottom = solid_area.origin.y + solid_area.size.height;
|
||||
|
||||
let solid_left_at_top = solid_area.origin.x + corner_radii.top_left;
|
||||
let solid_left_at_bottom = solid_area.origin.x + corner_radii.bottom_left;
|
||||
let solid_right_at_top =
|
||||
solid_area.origin.x + solid_area.size.width - corner_radii.top_right;
|
||||
let solid_right_at_bottom =
|
||||
solid_area.origin.x + solid_area.size.width - corner_radii.bottom_right;
|
||||
|
||||
let solid_top_at_left = solid_area.origin.y + corner_radii.top_left;
|
||||
let solid_bottom_at_left =
|
||||
solid_area.origin.y + solid_area.size.height - corner_radii.bottom_left;
|
||||
let solid_top_at_right = solid_area.origin.y + corner_radii.top_right;
|
||||
let solid_bottom_at_right =
|
||||
solid_area.origin.y + solid_area.size.height - corner_radii.bottom_right;
|
||||
|
||||
// Top
|
||||
if solid_left_at_top < solid_right_at_top {
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new((
|
||||
Point::new(solid_left_at_top, solid_top_at_left),
|
||||
shadow_color,
|
||||
))
|
||||
.line_to((Point::new(solid_left_at_top, solid_top), shadow_color))
|
||||
.line_to((
|
||||
Point::new(solid_left_at_top, solid_top - gradient_size),
|
||||
transparent,
|
||||
))
|
||||
.line_to((
|
||||
Point::new(solid_right_at_top, solid_top - gradient_size),
|
||||
transparent,
|
||||
))
|
||||
.line_to((Point::new(solid_right_at_top, solid_top), shadow_color))
|
||||
.line_to((
|
||||
Point::new(solid_right_at_top, solid_top_at_right),
|
||||
shadow_color,
|
||||
))
|
||||
.close()
|
||||
.filled(),
|
||||
);
|
||||
}
|
||||
|
||||
// Top Right
|
||||
shadow_arc(
|
||||
Point::new(solid_right_at_top, solid_top_at_right),
|
||||
corner_radii.top_right,
|
||||
gradient_size,
|
||||
shadow_color,
|
||||
transparent,
|
||||
Angle::degrees(270),
|
||||
context,
|
||||
);
|
||||
|
||||
// Right
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new((Point::new(solid_right, solid_top_at_right), shadow_color))
|
||||
.line_to((
|
||||
Point::new(solid_right + gradient_size, solid_top_at_right),
|
||||
transparent,
|
||||
))
|
||||
.line_to((
|
||||
Point::new(solid_right + gradient_size, solid_bottom_at_right),
|
||||
transparent,
|
||||
))
|
||||
.line_to((Point::new(solid_right, solid_bottom_at_right), shadow_color))
|
||||
.close()
|
||||
.filled(),
|
||||
);
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new((
|
||||
Point::new(solid_right_at_top, solid_top_at_right),
|
||||
shadow_color,
|
||||
))
|
||||
.line_to((Point::new(solid_right, solid_top_at_right), shadow_color))
|
||||
.line_to((Point::new(solid_right, solid_bottom_at_right), shadow_color))
|
||||
.line_to((
|
||||
Point::new(solid_right_at_bottom, solid_bottom_at_right),
|
||||
shadow_color,
|
||||
))
|
||||
.close()
|
||||
.filled(),
|
||||
);
|
||||
|
||||
// Bottom Right
|
||||
shadow_arc(
|
||||
Point::new(solid_right_at_bottom, solid_bottom_at_right),
|
||||
corner_radii.bottom_right,
|
||||
gradient_size,
|
||||
shadow_color,
|
||||
transparent,
|
||||
Angle::degrees(0),
|
||||
context,
|
||||
);
|
||||
|
||||
// Bottom
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new((
|
||||
Point::new(solid_left_at_bottom, solid_bottom_at_left),
|
||||
shadow_color,
|
||||
))
|
||||
.line_to((Point::new(solid_left_at_bottom, solid_bottom), shadow_color))
|
||||
.line_to((
|
||||
Point::new(solid_left_at_bottom, solid_bottom + gradient_size),
|
||||
transparent,
|
||||
))
|
||||
.line_to((
|
||||
Point::new(solid_right_at_bottom, solid_bottom + gradient_size),
|
||||
transparent,
|
||||
))
|
||||
.line_to((
|
||||
Point::new(solid_right_at_bottom, solid_bottom),
|
||||
shadow_color,
|
||||
))
|
||||
.line_to((
|
||||
Point::new(solid_right_at_bottom, solid_bottom_at_right),
|
||||
shadow_color,
|
||||
))
|
||||
.close()
|
||||
.filled(),
|
||||
);
|
||||
|
||||
// Bottom Left
|
||||
shadow_arc(
|
||||
Point::new(solid_left_at_bottom, solid_bottom_at_left),
|
||||
corner_radii.bottom_left,
|
||||
gradient_size,
|
||||
shadow_color,
|
||||
transparent,
|
||||
Angle::degrees(90),
|
||||
context,
|
||||
);
|
||||
|
||||
// Left
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new((
|
||||
Point::new(solid_left - gradient_size, solid_top_at_left),
|
||||
transparent,
|
||||
))
|
||||
.line_to((Point::new(solid_left, solid_top_at_left), shadow_color))
|
||||
.line_to((Point::new(solid_left, solid_bottom_at_left), shadow_color))
|
||||
.line_to((
|
||||
Point::new(solid_left - gradient_size, solid_bottom_at_left),
|
||||
transparent,
|
||||
))
|
||||
.close()
|
||||
.filled(),
|
||||
);
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new((Point::new(solid_left, solid_top_at_left), shadow_color))
|
||||
.line_to((
|
||||
Point::new(solid_left_at_top, solid_top_at_left),
|
||||
shadow_color,
|
||||
))
|
||||
.line_to((
|
||||
Point::new(solid_left_at_bottom, solid_bottom_at_left),
|
||||
shadow_color,
|
||||
))
|
||||
.line_to((Point::new(solid_left, solid_bottom_at_left), shadow_color))
|
||||
.close()
|
||||
.filled(),
|
||||
);
|
||||
|
||||
// Top Left
|
||||
shadow_arc(
|
||||
Point::new(solid_left_at_top, solid_top_at_left),
|
||||
corner_radii.top_left,
|
||||
gradient_size,
|
||||
shadow_color,
|
||||
transparent,
|
||||
Angle::degrees(180),
|
||||
context,
|
||||
);
|
||||
|
||||
// Center
|
||||
context.gfx.draw_shape(
|
||||
&PathBuilder::new((
|
||||
Point::new(solid_left_at_top, solid_top_at_left),
|
||||
shadow_color,
|
||||
))
|
||||
.line_to((
|
||||
Point::new(solid_right_at_top, solid_top_at_right),
|
||||
shadow_color,
|
||||
))
|
||||
.line_to((
|
||||
Point::new(solid_right_at_bottom, solid_bottom_at_right),
|
||||
shadow_color,
|
||||
))
|
||||
.line_to((
|
||||
Point::new(solid_left_at_bottom, solid_bottom_at_left),
|
||||
shadow_color,
|
||||
))
|
||||
.close()
|
||||
.filled(),
|
||||
);
|
||||
} else {
|
||||
context.gfx.draw_shape(&Shape::filled_round_rect(
|
||||
Rect::new(shadow.offset.max(Point::ZERO), child_area.size),
|
||||
*corner_radii,
|
||||
shadow_color,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a gradiented arc, quantized into sections to compensate for
|
||||
/// `lyon_geom` having directional fill tesselator. If a single pair of arcs
|
||||
/// joined by line segements is tesselated, the gradient "leans" in the
|
||||
/// orientation of `FillOptions::sweep_orientation` and doesn't look properly
|
||||
/// circular.
|
||||
fn shadow_arc(
|
||||
origin: Point<Px>,
|
||||
radius: Px,
|
||||
gradient: Px,
|
||||
solid_color: Color,
|
||||
transparent_color: Color,
|
||||
start_angle: Angle,
|
||||
context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
|
||||
) {
|
||||
let full_radius = radius + gradient;
|
||||
let mut current_outer_arc = rotate_point(
|
||||
origin,
|
||||
Point::new(origin.x + full_radius, origin.y),
|
||||
start_angle,
|
||||
);
|
||||
let mut current_inner_arc =
|
||||
rotate_point(origin, Point::new(origin.x + radius, origin.y), start_angle);
|
||||
let mut angle = Angle::degrees(0);
|
||||
|
||||
while angle < Angle::degrees(90) {
|
||||
angle += Angle::degrees(5);
|
||||
|
||||
let outer_arc = rotate_point(
|
||||
origin,
|
||||
Point::new(origin.x + full_radius, origin.y),
|
||||
start_angle + angle,
|
||||
);
|
||||
if outer_arc == current_outer_arc {
|
||||
continue;
|
||||
}
|
||||
|
||||
let inner_arc = rotate_point(
|
||||
origin,
|
||||
Point::new(origin.x + radius, origin.y),
|
||||
start_angle + angle,
|
||||
);
|
||||
|
||||
let mut path = PathBuilder::new((current_inner_arc, solid_color));
|
||||
path = path
|
||||
.line_to((current_outer_arc, transparent_color))
|
||||
.line_to((outer_arc, transparent_color))
|
||||
.line_to((inner_arc, solid_color));
|
||||
if inner_arc != current_inner_arc {
|
||||
path = path.line_to((current_inner_arc, solid_color));
|
||||
}
|
||||
context.gfx.draw_shape(&path.close().filled());
|
||||
|
||||
if inner_arc != current_inner_arc {
|
||||
let mut path = PathBuilder::new((origin, solid_color));
|
||||
path = path
|
||||
.line_to((current_inner_arc, solid_color))
|
||||
.line_to((inner_arc, solid_color))
|
||||
.line_to((origin, solid_color));
|
||||
context.gfx.draw_shape(&path.close().filled());
|
||||
}
|
||||
|
||||
current_outer_arc = outer_arc;
|
||||
current_inner_arc = inner_arc;
|
||||
}
|
||||
}
|
||||
|
||||
fn rotate_point(origin: Point<Px>, point: Point<Px>, angle: Angle) -> Point<Px> {
|
||||
let cos = angle.into_raidans_f().cos();
|
||||
let sin = angle.into_raidans_f().sin();
|
||||
let d = point - origin;
|
||||
origin + Point::new(d.x * cos - d.y * sin, d.y * cos + d.x * sin)
|
||||
}
|
||||
|
||||
/// The selected background configuration of a [`Container`].
|
||||
|
|
@ -276,3 +647,231 @@ define_components! {
|
|||
CurrentContainerBackground(EffectiveBackground, "background", |context| EffectiveBackground::Color(context.get(&SurfaceColor)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A shadow for a [`Container`].
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct ContainerShadow<Unit = Dimension> {
|
||||
/// The color of the shadow to use for the solid area.
|
||||
///
|
||||
/// This color will be faded to transparent if there is any blur on the
|
||||
/// shadow.
|
||||
pub color: Option<Color>,
|
||||
/// The offset of the shadow.
|
||||
pub offset: Point<Unit>,
|
||||
/// The radius of the blur.
|
||||
pub blur_radius: Unit,
|
||||
/// An additional amount of space the blur should be expanded across in all
|
||||
/// directions. This increases the physical space of the shadow.
|
||||
pub spread: Unit,
|
||||
}
|
||||
|
||||
impl<Unit> ContainerShadow<Unit> {
|
||||
/// Returns a new shadow that is offset underneath its contents.
|
||||
pub fn new(offset: Point<Unit>) -> Self
|
||||
where
|
||||
Unit: Default,
|
||||
{
|
||||
Self {
|
||||
color: None,
|
||||
offset,
|
||||
blur_radius: Unit::default(),
|
||||
spread: Unit::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the shadow color and returns self.
|
||||
#[must_use]
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the blur radius and returns self.
|
||||
#[must_use]
|
||||
pub fn blur_radius(mut self, radius: Unit) -> Self {
|
||||
self.blur_radius = radius;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the spread radius and returns self.
|
||||
#[must_use]
|
||||
pub fn spread(mut self, spread: Unit) -> Self {
|
||||
self.spread = spread;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Unit> ScreenScale for ContainerShadow<Unit>
|
||||
where
|
||||
Unit: ScreenScale<Lp = Lp, Px = Px, UPx = UPx>,
|
||||
{
|
||||
type Lp = ContainerShadow<Lp>;
|
||||
type Px = ContainerShadow<Px>;
|
||||
type UPx = ContainerShadow<UPx>;
|
||||
|
||||
fn into_px(self, scale: kludgine::figures::Fraction) -> Self::Px {
|
||||
ContainerShadow {
|
||||
color: self.color,
|
||||
offset: self.offset.into_px(scale),
|
||||
blur_radius: self.blur_radius.into_px(scale),
|
||||
spread: self.spread.into_px(scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_px(px: Self::Px, scale: kludgine::figures::Fraction) -> Self {
|
||||
Self {
|
||||
color: px.color,
|
||||
offset: Point::from_px(px.offset, scale),
|
||||
blur_radius: Unit::from_px(px.blur_radius, scale),
|
||||
spread: Unit::from_px(px.spread, scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_upx(self, scale: kludgine::figures::Fraction) -> Self::UPx {
|
||||
ContainerShadow {
|
||||
color: self.color,
|
||||
offset: self.offset.into_upx(scale),
|
||||
blur_radius: self.blur_radius.into_upx(scale),
|
||||
spread: self.spread.into_upx(scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_upx(px: Self::UPx, scale: kludgine::figures::Fraction) -> Self {
|
||||
Self {
|
||||
color: px.color,
|
||||
offset: Point::from_upx(px.offset, scale),
|
||||
blur_radius: Unit::from_upx(px.blur_radius, scale),
|
||||
spread: Unit::from_upx(px.spread, scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_lp(self, scale: kludgine::figures::Fraction) -> Self::Lp {
|
||||
ContainerShadow {
|
||||
color: self.color,
|
||||
offset: self.offset.into_lp(scale),
|
||||
blur_radius: self.blur_radius.into_lp(scale),
|
||||
spread: self.spread.into_lp(scale),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_lp(lp: Self::Lp, scale: kludgine::figures::Fraction) -> Self {
|
||||
Self {
|
||||
color: lp.color,
|
||||
offset: Point::from_lp(lp.offset, scale),
|
||||
blur_radius: Unit::from_lp(lp.blur_radius, scale),
|
||||
spread: Unit::from_lp(lp.spread, scale),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Px> for ContainerShadow {
|
||||
fn from(value: Px) -> Self {
|
||||
Self::from(Dimension::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Lp> for ContainerShadow {
|
||||
fn from(value: Lp) -> Self {
|
||||
Self::from(Dimension::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Dimension> for ContainerShadow {
|
||||
fn from(spread: Dimension) -> Self {
|
||||
Self::default().spread(spread)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point<Lp>> for ContainerShadow {
|
||||
fn from(offset: Point<Lp>) -> Self {
|
||||
Self::from(offset.map(Dimension::from))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point<Px>> for ContainerShadow {
|
||||
fn from(offset: Point<Px>) -> Self {
|
||||
Self::from(offset.map(Dimension::from))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Point<Dimension>> for ContainerShadow {
|
||||
fn from(size: Point<Dimension>) -> Self {
|
||||
Self::new(size)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue<ContainerShadow> for Dimension {
|
||||
fn into_value(self) -> Value<ContainerShadow> {
|
||||
ContainerShadow::from(self).into_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue<ContainerShadow> for Point<Px> {
|
||||
fn into_value(self) -> Value<ContainerShadow> {
|
||||
ContainerShadow::from(self).into_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue<ContainerShadow> for Point<Lp> {
|
||||
fn into_value(self) -> Value<ContainerShadow> {
|
||||
ContainerShadow::from(self).into_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue<ContainerShadow> for Point<Dimension> {
|
||||
fn into_value(self) -> Value<ContainerShadow> {
|
||||
ContainerShadow::from(self).into_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue<ContainerShadow> for ContainerShadow<Px> {
|
||||
fn into_value(self) -> Value<ContainerShadow> {
|
||||
ContainerShadow {
|
||||
color: self.color,
|
||||
offset: self.offset.map(Dimension::from),
|
||||
blur_radius: Dimension::from(self.blur_radius),
|
||||
spread: Dimension::from(self.spread),
|
||||
}
|
||||
.into_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContainerShadow<Lp>> for ContainerShadow {
|
||||
fn from(value: ContainerShadow<Lp>) -> Self {
|
||||
ContainerShadow {
|
||||
color: value.color,
|
||||
offset: value.offset.map(Dimension::from),
|
||||
blur_radius: Dimension::from(value.blur_radius),
|
||||
spread: Dimension::from(value.spread),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContainerShadow<Px>> for ContainerShadow {
|
||||
fn from(value: ContainerShadow<Px>) -> Self {
|
||||
ContainerShadow {
|
||||
color: value.color,
|
||||
offset: value.offset.map(Dimension::from),
|
||||
blur_radius: Dimension::from(value.blur_radius),
|
||||
spread: Dimension::from(value.spread),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue<ContainerShadow> for ContainerShadow<Lp> {
|
||||
fn into_value(self) -> Value<ContainerShadow> {
|
||||
ContainerShadow::<Dimension>::from(self).into_value()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue<ContainerShadow> for Dynamic<ContainerShadow<Px>> {
|
||||
fn into_value(self) -> Value<ContainerShadow> {
|
||||
Value::Dynamic(self.map_each_cloned(ContainerShadow::<Dimension>::from))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue<ContainerShadow> for Dynamic<ContainerShadow<Lp>> {
|
||||
fn into_value(self) -> Value<ContainerShadow> {
|
||||
Value::Dynamic(self.map_each_cloned(ContainerShadow::<Dimension>::from))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue