diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd47e9a6d..78e8263a63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ You can find its changes [documented below](#070---2021-01-01). - Make `Parse` work better with floats and similar types ([#2148] by [@superfell]) - Added `compute_max_intrinsic` method to the `Widget` trait, which determines the maximum useful dimension of the widget ([#2172] by [@sjoshid]) - Windows: Dark mode support for the title bar ([#2196] by [@dristic]) +- `ZStack` widget ([#2235] by [@xarvic]) ### Changed @@ -103,6 +104,7 @@ You can find its changes [documented below](#070---2021-01-01). - `SizedBox` now supports using `Key` for specifying size ([#2151] by [@GoldsteinE]) - `RadioGroup` widgets are now constructed with new `row()`, `column()`, and `for_axis()` methods ([#2157] by [@twitchyliquid64]) - Replace `info_span!` with `trace_span!` ([#2203] by [@NickLarsenNZ]) +- `WidgetPod::event` propagates handled mouse events to active children ([#2235] by [@xarvic]) ### Deprecated @@ -856,6 +858,7 @@ Last release without a changelog :( [#2195]: https://github.com/linebender/druid/pull/2195 [#2196]: https://github.com/linebender/druid/pull/2196 [#2203]: https://github.com/linebender/druid/pull/2203 +[#2235]: https://github.com/linebender/druid/pull/2235 [Unreleased]: https://github.com/linebender/druid/compare/v0.7.0...master [0.7.0]: https://github.com/linebender/druid/compare/v0.6.0...v0.7.0 diff --git a/druid/examples/z_stack.rs b/druid/examples/z_stack.rs new file mode 100644 index 0000000000..b736c8e206 --- /dev/null +++ b/druid/examples/z_stack.rs @@ -0,0 +1,62 @@ +// Copyright 2019 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A simple test of overlapping widgets. + +// On Windows platform, don't show a console when opening the app. +#![windows_subsystem = "windows"] + +use druid::widget::prelude::*; +use druid::widget::{Button, Label, ZStack}; +use druid::{AppLauncher, Data, Lens, UnitPoint, Vec2, WindowDesc}; + +#[derive(Clone, Data, Lens)] +struct State { + counter: usize, +} + +pub fn main() { + // describe the main window + let main_window = WindowDesc::new(build_root_widget()) + .title("Hello World!") + .window_size((400.0, 400.0)); + + // create the initial app state + let initial_state: State = State { counter: 0 }; + + // start the application. Here we pass in the application state. + AppLauncher::with_window(main_window) + .log_to_console() + .launch(initial_state) + .expect("Failed to launch application"); +} + +fn build_root_widget() -> impl Widget { + ZStack::new( + Button::from_label(Label::dynamic(|state: &State, _| { + format!( + "Very large button with text! Count up (currently {})", + state.counter + ) + })) + .on_click(|_, state: &mut State, _| state.counter += 1), + ) + .with_child( + Button::new("Reset").on_click(|_, state: &mut State, _| state.counter = 0), + Vec2::new(1.0, 1.0), + Vec2::ZERO, + UnitPoint::LEFT, + Vec2::new(10.0, 0.0), + ) +} diff --git a/druid/src/contexts.rs b/druid/src/contexts.rs index 66ef6c9963..74fa360b8f 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -854,7 +854,7 @@ impl LifeCycleCtx<'_, '_> { } } -impl LayoutCtx<'_, '_> { +impl<'a, 'b> LayoutCtx<'a, 'b> { /// Set explicit paint [`Insets`] for this widget. /// /// You are not required to set explicit paint bounds unless you need @@ -885,6 +885,18 @@ impl LayoutCtx<'_, '_> { trace!("set_baseline_offset {}", baseline); self.widget_state.baseline_offset = baseline } + + /// Creates a new LayoutCtx for a widget that maybe is hidden by another widget. + /// + /// If ignore is `true` the child will not set its hot state to `true` even if the cursor + /// is inside its bounds. + pub fn ignore_hot<'c>(&'c mut self, ignore: bool) -> LayoutCtx<'c, 'b> { + LayoutCtx { + state: &mut self.state, + widget_state: &mut self.widget_state, + mouse_pos: if ignore { None } else { self.mouse_pos }, + } + } } impl PaintCtx<'_, '_, '_> { diff --git a/druid/src/core.rs b/druid/src/core.rs index ed127a7b6e..4f32aa701c 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -574,6 +574,10 @@ impl> WidgetPod { self.state.needs_window_origin = false; self.state.is_expecting_set_origin_call = true; + //TODO: this does not work! + // self.layout_rect().origin().to_vec2() + self.viewport_offset() is the old position which + // changes after set origin. Therefore changing hot state must happen after the root widget + // set its origin. let child_mouse_pos = ctx .mouse_pos .map(|pos| pos - self.layout_rect().origin().to_vec2() + self.viewport_offset()); @@ -662,7 +666,12 @@ impl> WidgetPod { } // TODO: factor as much logic as possible into monomorphic functions. - if ctx.is_handled { + if ctx.is_handled + && !matches!( + event, + Event::MouseDown(_) | Event::MouseUp(_) | Event::MouseMove(_) | Event::Wheel(_) + ) + { // This function is called by containers to propagate an event from // containers to children. Non-recurse events will be invoked directly // from other points in the library. @@ -740,7 +749,11 @@ impl> WidgetPod { &mut self.state, ctx.state, rect, - Some(mouse_event.pos), + if !ctx.is_handled { + Some(mouse_event.pos) + } else { + None + }, data, env, ); @@ -759,7 +772,11 @@ impl> WidgetPod { &mut self.state, ctx.state, rect, - Some(mouse_event.pos), + if !ctx.is_handled { + Some(mouse_event.pos) + } else { + None + }, data, env, ); @@ -778,7 +795,11 @@ impl> WidgetPod { &mut self.state, ctx.state, rect, - Some(mouse_event.pos), + if !ctx.is_handled { + Some(mouse_event.pos) + } else { + None + }, data, env, ); @@ -800,7 +821,11 @@ impl> WidgetPod { &mut self.state, ctx.state, rect, - Some(mouse_event.pos), + if !ctx.is_handled { + Some(mouse_event.pos) + } else { + None + }, data, env, ); diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 9523b54840..b791e3f9a4 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -63,6 +63,7 @@ mod view_switcher; #[allow(clippy::module_inception)] mod widget; mod widget_ext; +mod z_stack; pub use self::image::Image; pub use added::Added; @@ -109,6 +110,7 @@ pub use widget::{Widget, WidgetId}; #[doc(hidden)] pub use widget_ext::WidgetExt; pub use widget_wrapper::WidgetWrapper; +pub use z_stack::ZStack; /// The types required to implement a `Widget`. /// diff --git a/druid/src/widget/z_stack.rs b/druid/src/widget/z_stack.rs new file mode 100644 index 0000000000..8f1d721895 --- /dev/null +++ b/druid/src/widget/z_stack.rs @@ -0,0 +1,180 @@ +use crate::{ + BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, + Point, Rect, Size, UnitPoint, UpdateCtx, Vec2, Widget, WidgetExt, WidgetPod, +}; + +/// A container that stacks its children on top of each other. +/// +/// The container has a baselayer which has the lowest z-index and determines the size of the +/// container. +pub struct ZStack { + layers: Vec>, +} + +struct ZChild { + child: WidgetPod>>, + relative_size: Vec2, + absolute_size: Vec2, + position: UnitPoint, + offset: Vec2, +} + +impl ZStack { + /// Creates a new ZStack with a baselayer. + /// + /// The baselayer is used by the ZStack to determine its own size. + pub fn new(base_layer: impl Widget + 'static) -> Self { + Self { + layers: vec![ZChild { + child: WidgetPod::new(base_layer.boxed()), + + relative_size: Vec2::new(1.0, 1.0), + absolute_size: Vec2::ZERO, + position: UnitPoint::CENTER, + offset: Vec2::ZERO, + }], + } + } + + /// Builder-style method to add a new child to the Z-Stack. + /// + /// The child is added directly above the base layer. + /// + /// `relative_size` is the space the child is allowed to take up relative to its parent. The + /// values are between 0 and 1. + /// `absolute_size` is a fixed amount of pixels added to `relative_size`. + /// + /// `position` is the alignment of the child inside the remaining space of its parent. + /// + /// `offset` is a fixed amount of pixels added to `position`. + pub fn with_child( + mut self, + child: impl Widget + 'static, + relative_size: Vec2, + absolute_size: Vec2, + position: UnitPoint, + offset: Vec2, + ) -> Self { + let next_index = self.layers.len() - 1; + self.layers.insert( + next_index, + ZChild { + child: WidgetPod::new(child.boxed()), + relative_size, + absolute_size, + position, + offset, + }, + ); + self + } + + /// Builder-style method to add a new child to the Z-Stack. + /// + /// The child is added directly above the base layer, is positioned in the center and has no + /// size constrains. + pub fn with_centered_child(self, child: impl Widget + 'static) -> Self { + self.with_aligned_child(child, UnitPoint::CENTER) + } + + /// Builder-style method to add a new child to the Z-Stack. + /// + /// The child is added directly above the base layer, uses the given alignment and has no + /// size constrains. + pub fn with_aligned_child(self, child: impl Widget + 'static, alignment: UnitPoint) -> Self { + self.with_child( + child, + Vec2::new(1.0, 1.0), + Vec2::ZERO, + alignment, + Vec2::ZERO, + ) + } +} + +impl Widget for ZStack { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { + let is_pointer_event = matches!( + event, + Event::MouseDown(_) | Event::MouseMove(_) | Event::MouseUp(_) | Event::Wheel(_) + ); + + for layer in self.layers.iter_mut() { + layer.child.event(ctx, event, data, env); + + if is_pointer_event && layer.child.is_hot() { + ctx.set_handled(); + } + } + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + for layer in self.layers.iter_mut() { + layer.child.lifecycle(ctx, event, data, env); + } + } + + fn update(&mut self, ctx: &mut UpdateCtx, _: &T, data: &T, env: &Env) { + for layer in self.layers.iter_mut().rev() { + layer.child.update(ctx, data, env); + } + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { + //Layout base layer + + let base_layer = self.layers.last_mut().unwrap(); + let base_size = base_layer.child.layout(ctx, bc, data, env); + + //Layout other layers + let other_layers = self.layers.len() - 1; + + for layer in self.layers.iter_mut().take(other_layers) { + let max_size = layer.resolve_max_size(base_size); + layer + .child + .layout(ctx, &BoxConstraints::new(Size::ZERO, max_size), data, env); + } + + //Set origin for all Layers and calculate paint insets + let mut previous_child_hot = false; + let mut paint_rect = Rect::ZERO; + + for layer in self.layers.iter_mut() { + let mut inner_ctx = ctx.ignore_hot(previous_child_hot); + + let remaining = base_size - layer.child.layout_rect().size(); + let origin = layer.resolve_point(remaining); + layer.child.set_origin(&mut inner_ctx, data, env, origin); + + paint_rect = paint_rect.union(layer.child.paint_rect()); + previous_child_hot |= layer.child.is_hot(); + } + + ctx.set_paint_insets(paint_rect - base_size.to_rect()); + ctx.set_baseline_offset(self.layers.last().unwrap().child.baseline_offset()); + + base_size + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { + //Painters algorithm (Painting back to front) + for layer in self.layers.iter_mut().rev() { + layer.child.paint(ctx, data, env); + } + } +} + +impl ZChild { + fn resolve_max_size(&self, availible: Size) -> Size { + self.absolute_size.to_size() + + Size::new( + availible.width * self.relative_size.x, + availible.height * self.relative_size.y, + ) + } + + fn resolve_point(&self, remaining_space: Size) -> Point { + (self.position.resolve(remaining_space.to_rect()).to_vec2() + self.offset).to_point() + } +}