diff --git a/Cargo.lock b/Cargo.lock index a6019e1..0e20781 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/examples/container-shadow.rs b/examples/container-shadow.rs new file mode 100644 index 0000000..81a6685 --- /dev/null +++ b/examples/container-shadow.rs @@ -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() +} diff --git a/examples/containers.rs b/examples/containers.rs index ca44803..94d52ab 100644 --- a/examples/containers.rs +++ b/examples/containers.rs @@ -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) -> 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 { + ContainerShadow::new(Point::new(Lp::ZERO, Lp::mm(1))).spread(Lp::mm_f(1.)) +} diff --git a/src/context.rs b/src/context.rs index 1b171c7..fff5ef8 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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> { diff --git a/src/styles.rs b/src/styles.rs index ceeb358..29ab27e 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -1172,6 +1172,22 @@ impl Edges { } } +impl Zero for Edges +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 { /// Returns a new instance with `dimension` for every edge. #[must_use] diff --git a/src/widget.rs b/src/widget.rs index 1e3da00..c3dd010 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -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 { diff --git a/src/widgets/container.rs b/src/widgets/container.rs index 7c1d966..07d8f38 100644 --- a/src/widgets/container.rs +++ b/src/widgets/container.rs @@ -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>>, + /// The shadow to apply behind the container's background. + pub shadow: Value, child: WidgetRef, - effective_background: Option, + applied_background: Option, } /// 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) -> Self { + self.shadow = shadow.into_value(); + self + } + fn padding(&self, context: &GraphicsContext<'_, '_, '_, '_, '_>) -> Edges { 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 { - Some( - self.padding - .as_ref() - .map_or(RootBehavior::PassThrough, |padding| { - RootBehavior::Pad(padding.get()) - }), - ) - } - - fn background_color(&mut self, context: &WidgetContext<'_, '_>) -> Option { + 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, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, - ) -> Size { - 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, - _available_space: Size, - context: &mut LayoutContext<'_, '_, '_, '_, '_>, - ) -> WrappedLayout { - let padding = self.padding(context); - let padded = size + padding.size(); - - WrappedLayout { - child: Rect::new(Point::new(padding.left, padding.top), size), - size: padded.into_unsigned(), } } +} +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, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { + 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, + corner_radii: &CornerRadii, + shadow: &ContainerShadow, + 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, + 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, point: Point, angle: Angle) -> Point { + 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 { + /// 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, + /// The offset of the shadow. + pub offset: Point, + /// 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 ContainerShadow { + /// Returns a new shadow that is offset underneath its contents. + pub fn new(offset: Point) -> 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 ScreenScale for ContainerShadow +where + Unit: ScreenScale, +{ + type Lp = ContainerShadow; + type Px = ContainerShadow; + type UPx = ContainerShadow; + + 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 for ContainerShadow { + fn from(value: Px) -> Self { + Self::from(Dimension::from(value)) + } +} + +impl From for ContainerShadow { + fn from(value: Lp) -> Self { + Self::from(Dimension::from(value)) + } +} + +impl From for ContainerShadow { + fn from(spread: Dimension) -> Self { + Self::default().spread(spread) + } +} + +impl From> for ContainerShadow { + fn from(offset: Point) -> Self { + Self::from(offset.map(Dimension::from)) + } +} + +impl From> for ContainerShadow { + fn from(offset: Point) -> Self { + Self::from(offset.map(Dimension::from)) + } +} + +impl From> for ContainerShadow { + fn from(size: Point) -> Self { + Self::new(size) + } +} + +impl IntoValue for Dimension { + fn into_value(self) -> Value { + ContainerShadow::from(self).into_value() + } +} + +impl IntoValue for Point { + fn into_value(self) -> Value { + ContainerShadow::from(self).into_value() + } +} + +impl IntoValue for Point { + fn into_value(self) -> Value { + ContainerShadow::from(self).into_value() + } +} + +impl IntoValue for Point { + fn into_value(self) -> Value { + ContainerShadow::from(self).into_value() + } +} + +impl IntoValue for ContainerShadow { + fn into_value(self) -> Value { + 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> for ContainerShadow { + fn from(value: ContainerShadow) -> 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> for ContainerShadow { + fn from(value: ContainerShadow) -> 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 for ContainerShadow { + fn into_value(self) -> Value { + ContainerShadow::::from(self).into_value() + } +} + +impl IntoValue for Dynamic> { + fn into_value(self) -> Value { + Value::Dynamic(self.map_each_cloned(ContainerShadow::::from)) + } +} + +impl IntoValue for Dynamic> { + fn into_value(self) -> Value { + Value::Dynamic(self.map_each_cloned(ContainerShadow::::from)) + } +}