diff --git a/masonry/src/widget/mod.rs b/masonry/src/widget/mod.rs index 64ba3cc03..d7434567e 100644 --- a/masonry/src/widget/mod.rs +++ b/masonry/src/widget/mod.rs @@ -55,7 +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, HorizontalAlignment, VerticalAlignment, ZStack}; +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_alignments_self_aligned.png b/masonry/src/widget/screenshots/masonry__widget__zstack__tests__zstack_alignments_self_aligned.png new file mode 100644 index 000000000..7f555d33f --- /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:77c51ecd65204dc8156786dd9f0194f30e0fe5120e0a4c13061a9e21401d5391 +size 13593 diff --git a/masonry/src/widget/zstack.rs b/masonry/src/widget/zstack.rs index 7c2151f47..2a5fe7c1e 100644 --- a/masonry/src/widget/zstack.rs +++ b/masonry/src/widget/zstack.rs @@ -11,6 +11,13 @@ use tracing::trace_span; struct Child { widget: WidgetPod>, + alignment: ChildAlignment, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ChildAlignment { + ParentAligned, + SelfAligned(Alignment), } /// A widget container that lays the child widgets on top of each other. @@ -136,6 +143,22 @@ impl From for Alignment { } } +impl From for ChildAlignment { + fn from(value: Alignment) -> Self { + ChildAlignment::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. @@ -151,16 +174,25 @@ impl ZStack { /// 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) -> Self { - self.with_child_pod(WidgetPod::new(Box::new(child))) + pub fn with_child(self, child: impl Widget, alignment: impl Into) -> Self { + self.with_child_pod(WidgetPod::new(Box::new(child)), alignment) } - pub fn with_child_id(self, child: impl Widget, id: WidgetId) -> Self { - self.with_child_pod(WidgetPod::new_with_id(Box::new(child), id)) + 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) } - pub fn with_child_pod(mut self, child: WidgetPod>) -> Self { - let child = Child { widget: child }; + 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 } @@ -172,19 +204,32 @@ impl 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) { + 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); + Self::insert_child_pod(this, child_pod, alignment); } - pub fn add_child_id(this: &mut WidgetMut<'_, Self>, child: impl Widget, id: WidgetId) { + 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); + 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>) { - let child = Child { widget }; + 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(); @@ -211,6 +256,16 @@ impl ZStack { this.widget.alignment = alignment.into(); this.ctx.request_layout(); } + + 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--- @@ -236,7 +291,12 @@ impl Widget for ZStack { let center = Point::new(end.x / 2., end.y / 2.); - let origin = match self.alignment { + 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.), @@ -292,7 +352,7 @@ mod tests { use crate::widget::{Label, SizedBox}; #[test] - fn zstack_alignments() { + fn zstack_alignments_parent_aligned() { let widget = ZStack::new() .with_child( SizedBox::new(Label::new("Background")) @@ -300,11 +360,13 @@ mod tests { .height(100.) .background(Color::BLUE) .border(Color::TEAL, 2.), + ChildAlignment::ParentAligned, ) .with_child( SizedBox::new(Label::new("Foreground")) .background(Color::RED) .border(Color::PINK, 2.), + ChildAlignment::ParentAligned, ); let mut harness = TestHarness::create(widget); @@ -337,4 +399,18 @@ mod tests { ); } } + + #[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 e96b3e9f3..dff4faed1 100644 --- a/xilem/examples/http_cats.rs +++ b/xilem/examples/http_cats.rs @@ -18,7 +18,7 @@ use xilem::core::fork; use xilem::core::one_of::OneOf3; use xilem::view::{ button, flex, image, inline_prose, portal, prose, sized_box, spinner, worker, zstack, Axis, - FlexExt, FlexSpacer, Padding, + FlexExt, FlexSpacer, Padding, ZStackExt, }; use xilem::{Color, EventLoop, EventLoopBuilder, TextAlignment, WidgetView, Xilem}; @@ -210,9 +210,9 @@ impl Status { .background(Color::BLACK.multiply_alpha(0.5)), ) // HACK: Trailing padding workaround scrollbar covering content - .padding((30., 42., 0., 0.)), - )) - .alignment(Alignment::TopTrailing), + .padding((30., 42., 0., 0.)) + .alignment(Alignment::TopTrailing), + )), )) .main_axis_alignment(xilem::view::MainAxisAlignment::Start) } diff --git a/xilem/src/view/zstack.rs b/xilem/src/view/zstack.rs index 20de30e05..902c7f3a2 100644 --- a/xilem/src/view/zstack.rs +++ b/xilem/src/view/zstack.rs @@ -1,17 +1,20 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 +use std::marker::PhantomData; + use crate::{ core::{ AppendVec, DynMessage, ElementSplice, Mut, SuperElement, View, ViewElement, ViewMarker, ViewSequence, }, - Pod, ViewCtx, + Pod, ViewCtx, WidgetView, }; use masonry::{ - widget::{self, Alignment, WidgetMut}, + 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. @@ -71,7 +74,7 @@ where 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.0.inner); + widget = widget.with_child_pod(child.widget.inner, child.alignment); } (ctx.new_pod(widget), seq_state) } @@ -107,23 +110,133 @@ where fn message( &self, view_state: &mut Self::ViewState, - id_path: &[crate::core::ViewId], + id_path: &[ViewId], message: DynMessage, app_state: &mut State, - ) -> crate::core::MessageResult { + ) -> MessageResult { self.sequence .seq_message(view_state, id_path, message, app_state) } } +// --- MARK: ZStackExt --- + +pub trait ZStackExt: WidgetView { + fn alignment(self, alignment: impl Into) -> ZStackItem + where + State: 'static, + Action: 'static, + Self: Sized, + { + zstack_item(self, alignment) + } +} + +impl> ZStackExt for V {} + +pub struct ZStackItem { + view: V, + alignment: ChildAlignment, + phantom: PhantomData (State, Action)>, +} + +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 --- -pub struct ZStackElement(Pod>); +pub struct ZStackElement { + widget: Pod>, + alignment: ChildAlignment, +} 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>; } @@ -151,7 +264,7 @@ impl SuperElement for ZStackElement { impl SuperElement, ViewCtx> for ZStackElement { fn upcast(ctx: &mut ViewCtx, child: Pod) -> Self { - ZStackElement(ctx.boxed_pod(child)) + ZStackElement::new(ctx.boxed_pod(child), ChildAlignment::ParentAligned) } fn with_downcast_val( @@ -202,14 +315,22 @@ impl ElementSplice for StackSplice<'_> { 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.0.inner); + 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.0.inner); + widget::ZStack::insert_child_pod( + &mut self.element, + element.widget.inner, + element.alignment, + ); self.idx += 1; }