diff --git a/masonry/src/widget/mod.rs b/masonry/src/widget/mod.rs index 9d4a4ba90..d7434567e 100644 --- a/masonry/src/widget/mod.rs +++ b/masonry/src/widget/mod.rs @@ -32,6 +32,7 @@ mod text_area; mod textbox; mod variable_label; mod widget_arena; +mod zstack; pub use self::image::Image; pub use align::Align; @@ -54,6 +55,7 @@ pub use variable_label::VariableLabel; pub use widget_mut::WidgetMut; pub use widget_pod::WidgetPod; pub use widget_ref::WidgetRef; +pub use zstack::{Alignment, ChildAlignment, HorizontalAlignment, VerticalAlignment, ZStack}; pub(crate) use widget_arena::WidgetArena; pub(crate) use widget_state::WidgetState; diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_bottom_center.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_bottom_center.png new file mode 100644 index 000000000..1ffb8ddc5 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_bottom_center.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0aefc8a4b971e0571485fcb441b3e29923e6fff31325f82e6a6bc6ef65b971e +size 7601 diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_bottom_leading.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_bottom_leading.png new file mode 100644 index 000000000..ad48a640d --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_bottom_leading.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e98191fbddd092f724588a9d66210fb678a769b3b6dd24ba1a2b9748a214e1a3 +size 7446 diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_bottom_trailing.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_bottom_trailing.png new file mode 100644 index 000000000..ed8f66152 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_bottom_trailing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5feac713ecf228341a2a6aea32b0a7d50388d7cd32590a99576318ed069ab62 +size 7445 diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_center_center.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_center_center.png new file mode 100644 index 000000000..f46e00808 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_center_center.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:442e0d2158c4d2d56b15cf33c7994173f6666605d1af8f59d02d35636ca770c9 +size 7606 diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_center_leading.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_center_leading.png new file mode 100644 index 000000000..3bfb0f391 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_center_leading.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08f9230087ea38f7323ae8756ca164a3f25429ed6f59467383d8eb20a00de397 +size 7465 diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_center_trailing.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_center_trailing.png new file mode 100644 index 000000000..4139e6a00 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_center_trailing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8e1cad3dac9b67ce43ac5c99257e1ad12171e05f111abdb925fc6693afee839 +size 7461 diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_default.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_default.png new file mode 100644 index 000000000..f46e00808 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:442e0d2158c4d2d56b15cf33c7994173f6666605d1af8f59d02d35636ca770c9 +size 7606 diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_top_center.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_top_center.png new file mode 100644 index 000000000..c8ab64328 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_top_center.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f710a86dde39a2653e7520cf191802669283b43e81997e782ff7501e5125a70 +size 7276 diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_top_leading.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_top_leading.png new file mode 100644 index 000000000..e6b4d5ce2 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_top_leading.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e31714f30a78e3d34b372d6562eaf78ad30689c7afd0166be10c670510209d1 +size 5873 diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_top_trailing.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_top_trailing.png new file mode 100644 index 000000000..3609c9c59 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignment_top_trailing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79eeba0c9745bf1c41f598d312552f297cb9942b63d8f9748cd824f1d8d04413 +size 7497 diff --git a/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignments_self_aligned.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignments_self_aligned.png new file mode 100644 index 000000000..f0129aaf2 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignments_self_aligned.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7b02562b6e2e018886a7c7e171bc4b1e5f8c60458c0a2964ec3b46aadcc21b1 +size 13346 diff --git a/masonry/src/widget/zstack.rs b/masonry/src/widget/zstack.rs new file mode 100644 index 000000000..f2d40785c --- /dev/null +++ b/masonry/src/widget/zstack.rs @@ -0,0 +1,438 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +#![warn(missing_docs)] + +use crate::{ + vello::Scene, widget::WidgetMut, AccessCtx, BoxConstraints, LayoutCtx, PaintCtx, Point, + QueryCtx, RegisterCtx, Size, Widget, WidgetId, WidgetPod, +}; +use accesskit::{Node, Role}; +use smallvec::SmallVec; +use tracing::trace_span; + +struct Child { + widget: WidgetPod>, + alignment: ChildAlignment, +} + +/// An option specifying how a child widget is aligned within a [`ZStack`]. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ChildAlignment { + /// Specifies that the child should use the global alignment as specified by the parent [`ZStack`] widget. + ParentAligned, + /// Specifies that the child should override the global alignment specified by the parent [`ZStack`] widget. + SelfAligned(Alignment), +} + +/// A widget container that lays the child widgets on top of each other. +/// +/// The alignment of how the children are placed can be specified globally using [`with_alignment`][Self::with_alignment]. +/// Each child can additionally override the global alignment using [`ChildAlignment::SelfAligned`]. +#[derive(Default)] +pub struct ZStack { + children: Vec, + alignment: Alignment, +} + +/// Alignment describes the position of a view laid on top of another view. +/// +/// See also [`VerticalAlignment`] and [`HorizontalAlignment`] for describing only a single axis. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum Alignment { + /// Align to the top leading corner. + TopLeading, + /// Align to the center of the top edge. + Top, + /// Align to the top trailing corner. + TopTrailing, + /// Align to the center of the leading edge. + Leading, + /// Align to the center. + #[default] + Center, + /// Align to the center of the trailing edge. + Trailing, + /// Align to the bottom leading corner. + BottomLeading, + /// Align to the center of the bottom edge. + Bottom, + /// Align to the bottom trailing corner. + BottomTrailing, +} + +/// Describes the vertical position of a view laid on top of another view. +/// See also [Alignment]. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum VerticalAlignment { + /// Align to the top edge. + Top, + /// Align to the center. + #[default] + Center, + /// Align to the bottom edge. + Bottom, +} + +/// Describes the horizontal position of a view laid on top of another view. +/// See also [Alignment]. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum HorizontalAlignment { + /// Align to the leading edge. + Leading, + #[default] + /// Align to the center. + Center, + /// Align to the trailing edge. + Trailing, +} + +// --- MARK: IMPL ALIGNMENTS --- + +impl Alignment { + /// Constructs a new Alignment from a [vertical][VerticalAlignment] and [horizontal][HorizontalAlignment] alignment. + pub fn new(vertical: VerticalAlignment, horizontal: HorizontalAlignment) -> Self { + match (vertical, horizontal) { + (VerticalAlignment::Top, HorizontalAlignment::Leading) => Self::TopLeading, + (VerticalAlignment::Top, HorizontalAlignment::Center) => Self::Top, + (VerticalAlignment::Top, HorizontalAlignment::Trailing) => Self::TopTrailing, + (VerticalAlignment::Center, HorizontalAlignment::Leading) => Self::Leading, + (VerticalAlignment::Center, HorizontalAlignment::Center) => Self::Center, + (VerticalAlignment::Center, HorizontalAlignment::Trailing) => Self::Trailing, + (VerticalAlignment::Bottom, HorizontalAlignment::Leading) => Self::BottomLeading, + (VerticalAlignment::Bottom, HorizontalAlignment::Center) => Self::Bottom, + (VerticalAlignment::Bottom, HorizontalAlignment::Trailing) => Self::BottomTrailing, + } + } + + /// Gets the vertical component of the alignment. + pub fn vertical(self) -> VerticalAlignment { + match self { + Self::Center | Self::Leading | Self::Trailing => VerticalAlignment::Center, + Self::Top | Self::TopLeading | Self::TopTrailing => VerticalAlignment::Top, + Self::Bottom | Self::BottomLeading | Self::BottomTrailing => VerticalAlignment::Bottom, + } + } + + /// Gets the horizontal component of the alignment. + pub fn horizontal(self) -> HorizontalAlignment { + match self { + Self::Center | Self::Top | Self::Bottom => HorizontalAlignment::Center, + Self::Leading | Self::TopLeading | Self::BottomLeading => HorizontalAlignment::Leading, + Self::Trailing | Self::TopTrailing | Self::BottomTrailing => { + HorizontalAlignment::Trailing + } + } + } +} + +impl From for VerticalAlignment { + fn from(value: Alignment) -> Self { + value.vertical() + } +} + +impl From for HorizontalAlignment { + fn from(value: Alignment) -> Self { + value.horizontal() + } +} + +impl From<(VerticalAlignment, HorizontalAlignment)> for Alignment { + fn from((vertical, horizontal): (VerticalAlignment, HorizontalAlignment)) -> Self { + Self::new(vertical, horizontal) + } +} + +impl From for Alignment { + fn from(vertical: VerticalAlignment) -> Self { + Self::new(vertical, HorizontalAlignment::Center) + } +} + +impl From for Alignment { + fn from(horizontal: HorizontalAlignment) -> Self { + Self::new(VerticalAlignment::Center, horizontal) + } +} + +impl From for ChildAlignment { + fn from(value: Alignment) -> Self { + Self::SelfAligned(value) + } +} + +impl Child { + fn new(widget: WidgetPod>, alignment: ChildAlignment) -> Self { + Self { widget, alignment } + } + + fn update_alignment(&mut self, alignment: ChildAlignment) { + self.alignment = alignment; + } +} + +// --- MARK: IMPL ZSTACK --- +impl ZStack { + /// Constructs a new empty `ZStack` widget. + pub fn new() -> Self { + Self::default() + } + + /// Changes the alignment of the children. + pub fn with_alignment(mut self, alignment: impl Into) -> Self { + self.alignment = alignment.into(); + self + } + + /// Appends a child widget to the `ZStack`. + /// The child are placed back to front, in the order they are added. + pub fn with_child(self, child: impl Widget, alignment: impl Into) -> Self { + self.with_child_pod(WidgetPod::new(Box::new(child)), alignment) + } + + /// Appends a child widget with a given `id` to the `ZStack`. + pub fn with_child_id( + self, + child: impl Widget, + id: WidgetId, + alignment: impl Into, + ) -> Self { + self.with_child_pod(WidgetPod::new_with_id(Box::new(child), id), alignment) + } + + /// Appends a child widget pod to the `ZStack`. + /// + /// See also [`Self::with_child`] if the widget is not already wrapped in a [`WidgetPod`]. + pub fn with_child_pod( + mut self, + child: WidgetPod>, + alignment: impl Into, + ) -> Self { + let child = Child::new(child, alignment.into()); + self.children.push(child); + self + } +} + +// --- MARK: WIDGETMUT--- +impl ZStack { + /// Add a child widget to the `ZStack`. + /// The child are placed back to front, in the order they are added. + /// + /// See also [`with_child`][Self::with_child]. + pub fn add_child( + this: &mut WidgetMut<'_, Self>, + child: impl Widget, + alignment: impl Into, + ) { + let child_pod: WidgetPod> = WidgetPod::new(Box::new(child)); + Self::insert_child_pod(this, child_pod, alignment); + } + + /// Add a child widget with a given `id` to the `ZStack`. + /// + /// See [`Self::add_child`] for more details. + pub fn add_child_id( + this: &mut WidgetMut<'_, Self>, + child: impl Widget, + id: WidgetId, + alignment: impl Into, + ) { + let child_pod: WidgetPod> = WidgetPod::new_with_id(Box::new(child), id); + Self::insert_child_pod(this, child_pod, alignment); + } + + /// Add a child widget to the `ZStack`. + pub fn insert_child_pod( + this: &mut WidgetMut<'_, Self>, + widget: WidgetPod>, + alignment: impl Into, + ) { + let child = Child::new(widget, alignment.into()); + this.widget.children.push(child); + this.ctx.children_changed(); + this.ctx.request_layout(); + } + + /// Remove a child from the `ZStack`. + pub fn remove_child(this: &mut WidgetMut<'_, Self>, idx: usize) { + let child = this.widget.children.remove(idx); + this.ctx.remove_child(child.widget); + this.ctx.request_layout(); + } + + /// Get a mutable reference to a child of the `ZStack`. + pub fn child_mut<'t>( + this: &'t mut WidgetMut<'_, Self>, + idx: usize, + ) -> Option>> { + let child = &mut this.widget.children[idx].widget; + Some(this.ctx.get_mut(child)) + } + + /// Change the alignment of the `ZStack`. + /// + /// See also [`with_alignment`][Self::with_alignment]. + pub fn set_alignment(this: &mut WidgetMut<'_, Self>, alignment: impl Into) { + this.widget.alignment = alignment.into(); + this.ctx.request_layout(); + } + + /// Change the alignment of a child of the `ZStack`. + pub fn update_child_alignment( + this: &mut WidgetMut<'_, Self>, + idx: usize, + alignment: impl Into, + ) { + let child = &mut this.widget.children[idx]; + child.update_alignment(alignment.into()); + this.ctx.request_layout(); + } +} + +// --- MARK: IMPL WIDGET--- +impl Widget for ZStack { + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + // First pass: calculate the smallest bounds needed to layout the children. + let mut max_size = bc.min(); + let loosened_bc = bc.loosen(); + for child in &mut self.children { + let child_size = ctx.run_layout(&mut child.widget, &loosened_bc); + + max_size.width = child_size.width.max(max_size.width); + max_size.height = child_size.height.max(max_size.height); + } + + // Second pass: place the children given the calculated max_size bounds. + for child in &mut self.children { + let child_size = ctx.child_size(&child.widget); + + let end = max_size - child_size; + let end = Point::new(end.width, end.height); + + let center = Point::new(end.x / 2., end.y / 2.); + + let child_alignment = match child.alignment { + ChildAlignment::SelfAligned(alignment) => alignment, + ChildAlignment::ParentAligned => self.alignment, + }; + + let origin = match child_alignment { + Alignment::TopLeading => Point::ZERO, + Alignment::Top => Point::new(center.x, 0.), + Alignment::TopTrailing => Point::new(end.x, 0.), + Alignment::Leading => Point::new(0., center.y), + Alignment::Center => center, + Alignment::Trailing => Point::new(end.x, center.y), + Alignment::BottomLeading => Point::new(0., end.y), + Alignment::Bottom => Point::new(center.x, end.y), + Alignment::BottomTrailing => end, + }; + + ctx.place_child(&mut child.widget, origin); + } + + max_size + } + + fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut Scene) {} + + fn register_children(&mut self, ctx: &mut RegisterCtx) { + for child in self.children.iter_mut().map(|x| &mut x.widget) { + ctx.register_child(child); + } + } + + fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { + self.children + .iter() + .map(|child| &child.widget) + .map(|widget_pod| widget_pod.id()) + .collect() + } + + fn accessibility_role(&self) -> Role { + Role::GenericContainer + } + + fn accessibility(&mut self, _ctx: &mut AccessCtx, _node: &mut Node) {} + + fn make_trace_span(&self, ctx: &QueryCtx<'_>) -> tracing::Span { + trace_span!("ZStack", id = ctx.widget_id().trace()) + } +} + +// --- MARK: TESTS --- +#[cfg(test)] +mod tests { + use vello::peniko::color::palette; + + use super::*; + use crate::assert_render_snapshot; + use crate::testing::TestHarness; + use crate::widget::{Label, SizedBox}; + + #[test] + fn zstack_alignments_parent_aligned() { + let widget = ZStack::new() + .with_child( + SizedBox::new(Label::new("Background")) + .width(200.) + .height(100.) + .background(palette::css::BLUE) + .border(palette::css::TEAL, 2.), + ChildAlignment::ParentAligned, + ) + .with_child( + SizedBox::new(Label::new("Foreground")) + .background(palette::css::RED) + .border(palette::css::PINK, 2.), + ChildAlignment::ParentAligned, + ); + + let mut harness = TestHarness::create(widget); + assert_render_snapshot!(harness, "zstack_alignment_default"); + + let vertical_cases = [ + ("top", VerticalAlignment::Top), + ("center", VerticalAlignment::Center), + ("bottom", VerticalAlignment::Bottom), + ]; + + let horizontal_cases = [ + ("leading", HorizontalAlignment::Leading), + ("center", HorizontalAlignment::Center), + ("trailing", HorizontalAlignment::Trailing), + ]; + + let all_cases = vertical_cases + .into_iter() + .flat_map(|vert| horizontal_cases.map(|hori| (vert, hori))); + + for (vertical, horizontal) in all_cases { + harness.edit_root_widget(|mut zstack| { + let mut zstack = zstack.downcast::(); + ZStack::set_alignment(&mut zstack, (vertical.1, horizontal.1)); + }); + assert_render_snapshot!( + harness, + &format!("zstack_alignment_{}_{}", vertical.0, horizontal.0) + ); + } + } + + #[test] + fn zstack_alignments_self_aligned() { + let widget = ZStack::new() + .with_alignment(Alignment::Center) + .with_child(Label::new("ParentAligned"), ChildAlignment::ParentAligned) + .with_child(Label::new("TopLeading"), Alignment::TopLeading) + .with_child(Label::new("TopTrailing"), Alignment::TopTrailing) + .with_child(Label::new("BottomLeading"), Alignment::BottomLeading) + .with_child(Label::new("BottomTrailing"), Alignment::BottomTrailing); + + let mut harness = TestHarness::create(widget); + assert_render_snapshot!(harness, "zstack_alignments_self_aligned"); + } +} diff --git a/xilem/examples/http_cats.rs b/xilem/examples/http_cats.rs index fd351f586..2b28a0fdc 100644 --- a/xilem/examples/http_cats.rs +++ b/xilem/examples/http_cats.rs @@ -9,6 +9,7 @@ use std::sync::Arc; +use masonry::widget::{Alignment, LineBreaking}; use vello::peniko::{Blob, Image}; use winit::dpi::LogicalSize; use winit::error::EventLoopError; @@ -16,8 +17,8 @@ use winit::window::Window; use xilem::core::fork; use xilem::core::one_of::OneOf3; use xilem::view::{ - button, flex, image, inline_prose, portal, prose, sized_box, spinner, worker, Axis, FlexExt, - FlexSpacer, Padding, + button, flex, image, inline_prose, portal, prose, sized_box, spinner, worker, zstack, Axis, + FlexExt, FlexSpacer, Padding, ZStackExt, }; use xilem::{palette, EventLoop, EventLoopBuilder, TextAlignment, WidgetView, Xilem}; @@ -196,12 +197,21 @@ impl Status { .text_size(20.) .alignment(TextAlignment::Middle), FlexSpacer::Fixed(10.), - image, - // TODO: Overlay on top of the image? - // HACK: Trailing padding workaround scrollbar covering content - // HACK: Bottom padding to workaround https://github.com/linebender/parley/issues/165 - sized_box(prose("Copyright ©️ https://http.cat").alignment(TextAlignment::End)) - .padding(Padding::new(0., 15., 10., 0.)), + zstack(( + image, + sized_box( + sized_box( + prose("Copyright ©️ https://http.cat") + .line_break_mode(LineBreaking::Clip) + .alignment(TextAlignment::End), + ) + .padding(4.) + .rounded(4.) + .background(palette::css::BLACK.multiply_alpha(0.5)), + ) + .padding((30., 42., 0., 0.)) + .alignment(Alignment::TopTrailing), + )), )) .main_axis_alignment(xilem::view::MainAxisAlignment::Start) } diff --git a/xilem/src/view/mod.rs b/xilem/src/view/mod.rs index b879de45f..fd8e39eeb 100644 --- a/xilem/src/view/mod.rs +++ b/xilem/src/view/mod.rs @@ -47,3 +47,6 @@ pub use textbox::*; mod portal; pub use portal::*; + +mod zstack; +pub use zstack::*; diff --git a/xilem/src/view/zstack.rs b/xilem/src/view/zstack.rs new file mode 100644 index 000000000..0905372d6 --- /dev/null +++ b/xilem/src/view/zstack.rs @@ -0,0 +1,378 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +#![warn(missing_docs)] + +use std::marker::PhantomData; + +use crate::{ + core::{ + AppendVec, DynMessage, ElementSplice, Mut, SuperElement, View, ViewElement, ViewMarker, + ViewSequence, + }, + Pod, ViewCtx, WidgetView, +}; +use masonry::{ + widget::{self, Alignment, ChildAlignment, WidgetMut}, + Widget, +}; +use xilem_core::{MessageResult, ViewId}; + +/// A widget that lays out its children on top of each other. +/// The children are laid out back to front. +/// +/// # Example +/// +/// This example shows how to add two text labels on top of each other. +/// +/// ``` +/// use xilem::WidgetView; +/// use xilem::view::{zstack, label, button}; +/// +/// fn view() -> impl WidgetView { +/// zstack(( +/// label("Background"), +/// button("Click me", |_| {}) +/// )) +/// } +/// ``` +pub fn zstack>(sequence: Seq) -> ZStack { + ZStack { + sequence, + alignment: Alignment::default(), + } +} + +/// A view container that lays the child widgets on top of each other. +/// +/// See [`zstack`] for more details. +#[must_use = "View values do nothing unless provided to Xilem."] +pub struct ZStack { + sequence: Seq, + alignment: Alignment, +} + +impl ZStack { + /// Changes the alignment of the children. + pub fn alignment(mut self, alignment: impl Into) -> Self { + self.alignment = alignment.into(); + self + } +} + +impl ViewMarker for ZStack {} +impl View for ZStack +where + State: 'static, + Action: 'static, + Seq: ZStackSequence, +{ + type Element = Pod; + + type ViewState = Seq::SeqState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let mut elements = AppendVec::default(); + let mut widget = widget::ZStack::new().with_alignment(self.alignment); + let seq_state = self.sequence.seq_build(ctx, &mut elements); + for child in elements.into_inner() { + widget = widget.with_child_pod(child.widget.inner, child.alignment); + } + (ctx.new_pod(widget), seq_state) + } + + fn rebuild( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut, + ) { + if self.alignment != prev.alignment { + widget::ZStack::set_alignment(&mut element, self.alignment); + } + + let mut splice = ZStackSplice::new(element); + self.sequence + .seq_rebuild(&prev.sequence, view_state, ctx, &mut splice); + debug_assert!(splice.scratch.is_empty()); + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + let mut splice = ZStackSplice::new(element); + self.sequence.seq_teardown(view_state, ctx, &mut splice); + debug_assert!(splice.scratch.into_inner().is_empty()); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.sequence + .seq_message(view_state, id_path, message, app_state) + } +} + +// --- MARK: ZStackExt --- + +/// A trait that extends a [`WidgetView`] with methods to provide parameters for a parent [`ZStack`]. +pub trait ZStackExt: WidgetView { + /// Applies [`ChildAlignment`] to this view. + /// This allows the view to override the default [`Alignment`] of the parent [`ZStack`]. + /// This can only be used on views that are direct children of a [`ZStack`]. + fn alignment(self, alignment: impl Into) -> ZStackItem + where + State: 'static, + Action: 'static, + Self: Sized, + { + zstack_item(self, alignment) + } +} + +impl> ZStackExt for V {} + +/// A wrapper around a [`WidgetView`], with a specified [`ChildAlignment`]. +/// This struct is most often constructed indrectly using [`ZStackExt::alignment`]. +pub struct ZStackItem { + view: V, + alignment: ChildAlignment, + phantom: PhantomData (State, Action)>, +} + +/// Constructs a new `ZStackItem`. +/// See also [`ZStackExt::alignment`], for constructing a `ZStackItem` from an existing view. +pub fn zstack_item( + view: V, + alignment: impl Into, +) -> ZStackItem +where + State: 'static, + Action: 'static, + V: WidgetView, +{ + ZStackItem { + view, + alignment: alignment.into(), + phantom: PhantomData, + } +} + +impl ViewMarker for ZStackItem {} + +impl View for ZStackItem +where + State: 'static, + Action: 'static, + V: WidgetView, +{ + type Element = ZStackElement; + + type ViewState = V::ViewState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (pod, state) = self.view.build(ctx); + ( + ZStackElement::new(ctx.boxed_pod(pod), self.alignment), + state, + ) + } + + fn rebuild( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut, + ) { + { + if self.alignment != prev.alignment { + widget::ZStack::update_child_alignment( + &mut element.parent, + element.idx, + self.alignment, + ); + } + let mut child = widget::ZStack::child_mut(&mut element.parent, element.idx) + .expect("ZStackWrapper always has a widget child"); + self.view + .rebuild(&prev.view, view_state, ctx, child.downcast()); + } + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut, + ) { + let mut child = widget::ZStack::child_mut(&mut element.parent, element.idx) + .expect("ZStackWrapper always has a widget child"); + self.view.teardown(view_state, ctx, child.downcast()); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.view.message(view_state, id_path, message, app_state) + } +} + +// --- MARK: ZStackElement --- + +/// A struct implementing [`ViewElement`] for a `ZStack`. +pub struct ZStackElement { + widget: Pod>, + alignment: ChildAlignment, +} + +/// A mutable version of `ZStackElement`. +pub struct ZStackElementMut<'w> { + parent: WidgetMut<'w, widget::ZStack>, + idx: usize, +} + +impl ZStackElement { + fn new(widget: Pod>, alignment: ChildAlignment) -> Self { + Self { widget, alignment } + } +} + +impl ViewElement for ZStackElement { + type Mut<'a> = ZStackElementMut<'a>; +} + +impl SuperElement for ZStackElement { + fn upcast(_ctx: &mut ViewCtx, child: Self) -> Self { + child + } + + fn with_downcast_val( + mut this: Mut, + f: impl FnOnce(Mut) -> R, + ) -> (Self::Mut<'_>, R) { + let r = { + let parent = this.parent.reborrow_mut(); + let reborrow = ZStackElementMut { + idx: this.idx, + parent, + }; + f(reborrow) + }; + (this, r) + } +} + +impl SuperElement, ViewCtx> for ZStackElement { + fn upcast(ctx: &mut ViewCtx, child: Pod) -> Self { + Self::new(ctx.boxed_pod(child), ChildAlignment::ParentAligned) + } + + fn with_downcast_val( + mut this: Mut, + f: impl FnOnce(Mut>) -> R, + ) -> (Self::Mut<'_>, R) { + let ret = { + let mut child = widget::ZStack::child_mut(&mut this.parent, this.idx) + .expect("This is supposed to be a widget"); + let downcast = child.downcast(); + f(downcast) + }; + + (this, ret) + } +} + +// MARK: Sequence + +/// A trait implementing `ViewSequence` for `ZStackElement`. +pub trait ZStackSequence: + ViewSequence +{ +} + +impl ZStackSequence for Seq where + Seq: ViewSequence +{ +} + +// MARK: Splice + +/// An implementation of [`ElementSplice`] for `ZStackElement`. +pub struct ZStackSplice<'w> { + idx: usize, + element: WidgetMut<'w, widget::ZStack>, + scratch: AppendVec, +} + +impl<'w> ZStackSplice<'w> { + fn new(element: WidgetMut<'w, widget::ZStack>) -> Self { + Self { + idx: 0, + element, + scratch: AppendVec::default(), + } + } +} + +impl ElementSplice for ZStackSplice<'_> { + fn with_scratch(&mut self, f: impl FnOnce(&mut AppendVec) -> R) -> R { + let ret = f(&mut self.scratch); + for element in self.scratch.drain() { + widget::ZStack::insert_child_pod( + &mut self.element, + element.widget.inner, + element.alignment, + ); + self.idx += 1; + } + ret + } + + fn insert(&mut self, element: ZStackElement) { + widget::ZStack::insert_child_pod( + &mut self.element, + element.widget.inner, + element.alignment, + ); + self.idx += 1; + } + + fn mutate(&mut self, f: impl FnOnce(Mut) -> R) -> R { + let child = ZStackElementMut { + parent: self.element.reborrow_mut(), + idx: self.idx, + }; + let ret = f(child); + self.idx += 1; + ret + } + + fn skip(&mut self, n: usize) { + self.idx += n; + } + + fn delete(&mut self, f: impl FnOnce(Mut) -> R) -> R { + let ret = { + let child = ZStackElementMut { + parent: self.element.reborrow_mut(), + idx: self.idx, + }; + f(child) + }; + widget::ZStack::remove_child(&mut self.element, self.idx); + ret + } +}