diff --git a/masonry/screenshots/label_line_break_modes.png b/masonry/screenshots/label_line_break_modes.png index 644d68c97..5fa0019c0 100644 Binary files a/masonry/screenshots/label_line_break_modes.png and b/masonry/screenshots/label_line_break_modes.png differ diff --git a/masonry/src/tests/paint.rs b/masonry/src/tests/paint.rs index 2fc65de6a..02d307d0e 100644 --- a/masonry/src/tests/paint.rs +++ b/masonry/src/tests/paint.rs @@ -124,8 +124,8 @@ fn paint_clipping() { let parent = NewWidget::new( ModularWidget::new(()) .measure_fn(|_, _, _, _, _, _| SQUARE_SIZE) - .layout_fn(|_, ctx, _, size| { - ctx.set_clip_path(size.to_rect()); + .layout_fn(|_, ctx, _, _size| { + ctx.set_clips_contents(true); }) .paint_fn(move |_, ctx, _, scene| { fill(scene, &ctx.content_box(), Color::WHITE); diff --git a/masonry/src/widgets/canvas.rs b/masonry/src/widgets/canvas.rs index 688c0c3f3..8b59fbaeb 100644 --- a/masonry/src/widgets/canvas.rs +++ b/masonry/src/widgets/canvas.rs @@ -116,7 +116,7 @@ impl Widget for Canvas { ctx.submit_action::(CanvasSizeChanged { size }); } // We clip the contents we draw. - ctx.set_clip_path(size.to_rect()); + ctx.set_clips_contents(true); } fn paint(&mut self, _: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, scene: &mut Scene) { diff --git a/masonry/src/widgets/label.rs b/masonry/src/widgets/label.rs index 30259a93a..36d986fc3 100644 --- a/masonry/src/widgets/label.rs +++ b/masonry/src/widgets/label.rs @@ -430,12 +430,8 @@ impl Widget for Label { let baseline = 0.; // TODO: Use actual baseline, at least for single line text ctx.set_baseline_offset(baseline); - if *line_break_mode == LineBreaking::Clip { - let border_box = size.to_rect() + ctx.border_box_insets(); - ctx.set_clip_path(border_box); - } else { - ctx.clear_clip_path(); - } + let needs_clip = *line_break_mode == LineBreaking::Clip; + ctx.set_clips_contents(needs_clip); } fn paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, scene: &mut Scene) { diff --git a/masonry/src/widgets/portal.rs b/masonry/src/widgets/portal.rs index 9d75cbb72..d2aca1c3d 100644 --- a/masonry/src/widgets/portal.rs +++ b/masonry/src/widgets/portal.rs @@ -715,7 +715,7 @@ impl Widget for Portal { self.set_viewport_pos_raw(size, content_size, self.viewport_pos); // TODO - recompute portal progress - ctx.set_clip_path(size.to_rect()); + ctx.set_clips_contents(true); ctx.place_child(&mut self.child, Point::ZERO); diff --git a/masonry/src/widgets/prose.rs b/masonry/src/widgets/prose.rs index 24f581eff..aaf7d1f57 100644 --- a/masonry/src/widgets/prose.rs +++ b/masonry/src/widgets/prose.rs @@ -156,12 +156,7 @@ impl Widget for Prose { ctx.run_layout(&mut self.text, size); ctx.place_child(&mut self.text, Point::ORIGIN); - if self.clip { - let border_box = size.to_rect() + ctx.border_box_insets(); - ctx.set_clip_path(border_box); - } else { - ctx.clear_clip_path(); - } + ctx.set_clips_contents(self.clip); } fn paint(&mut self, _ctx: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, _scene: &mut Scene) { diff --git a/masonry/src/widgets/text_input.rs b/masonry/src/widgets/text_input.rs index c8b46af2e..63a84e29b 100644 --- a/masonry/src/widgets/text_input.rs +++ b/masonry/src/widgets/text_input.rs @@ -275,11 +275,7 @@ impl Widget for TextInput { ctx.place_child(&mut self.placeholder, child_origin); } - if self.clip { - ctx.set_clip_path(size.to_rect()); - } else { - ctx.clear_clip_path(); - } + ctx.set_clips_contents(self.clip); } fn pre_paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, scene: &mut Scene) { diff --git a/masonry/src/widgets/virtual_scroll.rs b/masonry/src/widgets/virtual_scroll.rs index 23703d161..c45a85d38 100644 --- a/masonry/src/widgets/virtual_scroll.rs +++ b/masonry/src/widgets/virtual_scroll.rs @@ -685,7 +685,7 @@ impl Widget for VirtualScroll { } fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { - ctx.set_clip_path(size.to_rect()); + ctx.set_clips_contents(true); // The number of loaded items before the anchor let mut height_before_anchor = 0.; let mut total_height = 0.; diff --git a/masonry_core/src/core/contexts.rs b/masonry_core/src/core/contexts.rs index f50ff95df..9c8fe886c 100644 --- a/masonry_core/src/core/contexts.rs +++ b/masonry_core/src/core/contexts.rs @@ -941,45 +941,32 @@ impl LayoutCtx<'_> { self.get_child_state(child).layout_border_box_size } - /// Sets the widget's clip path in the widget's content-box coordinate space. + /// Sets whether the [clip shape] is applied to the widget and its children. /// - /// A widget's clip path will have two effects: + /// A widget's clip shape will have two effects: /// - It serves as a mask for painting operations of this widget and its children. /// Note that while all painting done by children will be clipped by this path, /// only the painting done in [`paint`] by this widget itself will be clipped. /// The remaining painting done in [`pre_paint`] and [`post_paint`] will not be clipped. - /// - Pointer events must be inside this path to reach the widget's children. + /// - Pointer events must be inside that shape to reach the widget's children. /// /// [`paint`]: Widget::paint /// [`pre_paint`]: Widget::pre_paint /// [`post_paint`]: Widget::post_paint - pub fn set_clip_path(&mut self, path: Rect) { - // Translate the clip path to the widget's border-box coordinate space. - let path = path + self.widget_state.border_box_translation(); - // We intentionally always log this because clip paths are: - // 1) Relatively rare in the tree - // 2) An easy potential source of items not being visible when expected - trace!("set_clip_path {path:?}"); - self.widget_state.clip_path = Some(path); - // TODO - Updating the clip path may have - // other knock-on effects we'd need to document. - self.widget_state.request_accessibility = true; - self.widget_state.needs_accessibility = true; - self.widget_state.needs_paint = true; - } + /// [clip shape]: crate::doc::masonry_concepts#clip-shape. + pub fn set_clips_contents(&mut self, clips: bool) { + if self.widget_state.clips_contents != clips { + self.widget_state.clips_contents = clips; - /// Removes the widget's clip path. - /// - /// See [`LayoutCtx::set_clip_path`] for details. - pub fn clear_clip_path(&mut self) { - trace!("clear_clip_path"); - self.widget_state.clip_path = None; - // TODO - Updating the clip path may have - // other knock-on effects we'd need to document. - self.widget_state.request_accessibility = true; - self.widget_state.needs_accessibility = true; - self.widget_state.needs_paint = true; + self.widget_state.request_accessibility = true; + self.widget_state.needs_accessibility = true; + self.widget_state.needs_paint = true; + self.global_state.needs_pointer_pass = true; + } } + + // TODO - Add set_clip_shape(impl Shape) method + // TODO - Add clear_clip_shape() method } impl ComposeCtx<'_> { @@ -1139,16 +1126,12 @@ impl_context_method!( border_box_baseline - self.widget_state.border_box_insets.y1 } - /// The clip path of the widget, if any was set. - /// - /// The returned clip path will be in this widget's content-box coordinate space. + /// Whether the widget clips its own contents and that of its children. /// /// For more information, see - /// [`LayoutCtx::set_clip_path`](crate::core::LayoutCtx::set_clip_path). - pub fn clip_path(&self) -> Option { - // Translate the clip path to the widget's content-box coordinate space. - let translation = self.widget_state.border_box_translation(); - self.widget_state.clip_path.map(|path| path - translation) + /// [`LayoutCtx::set_clips_contents`](crate::core::LayoutCtx::set_clips_contents). + pub fn clips_contents(&self) -> bool { + self.widget_state.clips_contents } /// Returns the [`Vec2`] for translating between this widget's diff --git a/masonry_core/src/core/widget.rs b/masonry_core/src/core/widget.rs index ff11a4ecb..9ad7ba4e5 100644 --- a/masonry_core/src/core/widget.rs +++ b/masonry_core/src/core/widget.rs @@ -367,8 +367,9 @@ pub trait Widget: AsDynWidget + Any { /// /// This is where box shadow, background, and borders are painted. /// - /// This method is not constrained by the clip defined in [`LayoutCtx::set_clip_path`], - /// and can paint things outside the clip. + /// This method is not restricted by the [clip shape]. + /// + /// [clip shape]: crate::doc::masonry_concepts#clip-shape. fn pre_paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, scene: &mut Scene) { pre_paint(ctx, props, scene); } @@ -381,8 +382,9 @@ pub trait Widget: AsDynWidget + Any { /// Final paint method, which paints on top of the widget's children. /// - /// This method is not constrained by the clip defined in [`LayoutCtx::set_clip_path`], - /// and can paint things outside the clip. + /// This method is not restricted by the [clip shape]. + /// + /// [clip shape]: crate::doc::masonry_concepts#clip-shape. fn post_paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, scene: &mut Scene) { } @@ -562,9 +564,9 @@ pub fn find_widget_under_pointer<'c>( let local_pos = ctx.window_transform().inverse() * pos; - if let Some(clip) = ctx.clip_path() - && !clip.contains(local_pos) - { + let is_inside_clip_shape = ctx.content_box().contains(local_pos); + + if ctx.clips_contents() && !is_inside_clip_shape { return None; } @@ -580,6 +582,7 @@ pub fn find_widget_under_pointer<'c>( } } + // TODO - Use some variant of clip shape instead. // If no child is under pointer, test the current widget. if ctx.accepts_pointer_interaction() && ctx.border_box().contains(local_pos) { Some(WidgetRef { widget, ctx }) diff --git a/masonry_core/src/core/widget_state.rs b/masonry_core/src/core/widget_state.rs index 8b7417203..aabdf1e7e 100644 --- a/masonry_core/src/core/widget_state.rs +++ b/masonry_core/src/core/widget_state.rs @@ -119,14 +119,17 @@ pub(crate) struct WidgetState { /// The pixel-snapped position of the baseline in the parent's border-box coordinate space. pub(crate) baseline_y: f64, - // TODO - Use general Shape - // Currently Kurbo doesn't really provide a type that lets us - // efficiently hold an arbitrary shape. - /// The widget's clip path in the widget's border-box coordinate space. - /// - /// This clips the painting of `Widget::paint` and all the painting of children. - /// It does not clip this widget's `Widget::pre_paint` nor `Widget::post_paint`. - pub(crate) clip_path: Option, + // TODO - Add clip_shape + // TODO - Use more efficient type and avoid allocating by default. + // /// The widget's clip path in the widget's border-box coordinate space. + // /// + // /// This clips the painting of `Widget::paint` and all the painting of children. + // /// It does not clip this widget's `Widget::pre_paint` nor `Widget::post_paint`. + // pub(crate) clip_path: Option, + // + /// Whether the widget and its children's scenes are clipped by the + /// [clip shape](crate::doc::masonry_concepts#clip-shape). + pub(crate) clips_contents: bool, /// Local transform used during the mapping of this widget's border-box coordinate space /// to the parent's border-box coordinate space. @@ -275,7 +278,7 @@ impl WidgetState { bounding_box: Rect::ZERO, layout_baseline_offset: 0.0, baseline_y: 0.0, - clip_path: Option::default(), + clips_contents: false, transform: options.transform, window_transform: Affine::IDENTITY, scroll_translation: Vec2::ZERO, @@ -405,24 +408,45 @@ impl WidgetState { ) } - /// Returns the result of intersecting the widget's clip path (if any) with the given rect. + /// Returns the portion of the given rect, if any, that is within the widget's "clip space". /// - /// Both the argument and the result are in window coordinates. + /// If the widget doesn't clip its children, returns the input rect. + /// If the clip path is a non-rectangle, uses the clip paths' bounding box. + /// If the given rect doesn't intersect with the clipping box, returns `None`. /// - /// Returns `None` if the given rect is clipped out. - pub(crate) fn clip_child(&self, child_rect: Rect) -> Option { - if let Some(clip_path) = self.clip_path { - let clip_path_global = self.window_transform.transform_rect_bbox(clip_path); - if clip_path_global.overlaps(child_rect) { - Some(clip_path_global.intersect(child_rect)) - } else { - None - } + /// Both the argument and the result are in window coordinates. + pub(crate) fn clipped_child_box(&self, child_box: Rect) -> Option { + if !self.clips_contents { + return Some(child_box); + } + + let clip_rect = self.border_box_size().to_rect(); + + let bounding_box_global = self.window_transform.transform_rect_bbox(clip_rect); + + if bounding_box_global.overlaps(child_box) { + Some(bounding_box_global.intersect(child_box)) } else { - Some(child_rect) + None } } + /// The [clip shape] in border-box space. + /// + /// A widget's clip shape will have two effects: + /// - It serves as a mask for painting operations of this widget and its children. + /// Note that while all painting done by children will be clipped by this path, + /// only the painting done in `Widget::paint` by this widget itself will be clipped. + /// The remaining painting done in `Widget::pre_paint` and `Widget::post_paint` will not be clipped. + /// - Pointer events must be inside that shape to reach the widget's children. + /// + /// This currently returns the border-box rect if `clips_contents` is true and `None` otherwise, but in the future we may want to support more complex clip shapes, in which case this method would need to be updated. + /// + /// [clip shape]: crate::doc::masonry_concepts#clip-shape. + pub(crate) fn clip_shape(&self) -> Rect { + self.border_box_size().to_rect() - self.border_box_translation() + } + pub(crate) fn needs_rewrite_passes(&self) -> bool { self.needs_layout || self.needs_compose diff --git a/masonry_core/src/doc/masonry_concepts.md b/masonry_core/src/doc/masonry_concepts.md index c8e8a21d8..ba4dfbda6 100644 --- a/masonry_core/src/doc/masonry_concepts.md +++ b/masonry_core/src/doc/masonry_concepts.md @@ -213,6 +213,21 @@ Generally you'll want to convert any window coordinate space geometry into the w Then easily operate on that geometry and finally convert the results back to the window's coordinate space. +## Clip shape + +Widgets have a shape, usually one that matches their visual appearance, which has two purposes: + +- Pointer events outside of that shape will not affect the pointer. +- If the widget is set to clip its contents, pointer events outside the clip shape won't affect the children either. +- If the widget is set to clip its contents, its scene and the children's scenes will be painted inside of the clip shape. + +Currently, the clip shape is hardcoded to be a rect with the widget's size and position. + + + + + + ## Layers A Masonry application is composed of layers. diff --git a/masonry_core/src/passes/accessibility.rs b/masonry_core/src/passes/accessibility.rs index c8648c320..1bb7ccdc0 100644 --- a/masonry_core/src/passes/accessibility.rs +++ b/masonry_core/src/passes/accessibility.rs @@ -123,7 +123,7 @@ fn build_access_node( if ctx.is_disabled() { node.set_disabled(); } - if ctx.widget_state.clip_path.is_some() { + if ctx.widget_state.clips_contents { node.set_clips_children(); } if ctx.accepts_focus() && !ctx.is_disabled() { diff --git a/masonry_core/src/passes/compose.rs b/masonry_core/src/passes/compose.rs index 9a100b4f7..6e3433b64 100644 --- a/masonry_core/src/passes/compose.rs +++ b/masonry_core/src/passes/compose.rs @@ -71,7 +71,9 @@ fn compose_widget( ); let parent_bounding_box = parent_state.bounding_box; - if let Some(child_bounding_box) = parent_state.clip_child(node.item.state.bounding_box) { + if let Some(child_bounding_box) = + parent_state.clipped_child_box(node.item.state.bounding_box) + { parent_state.bounding_box = parent_bounding_box.union(child_bounding_box); } diff --git a/masonry_core/src/passes/paint.rs b/masonry_core/src/passes/paint.rs index c62149f88..2080c8c58 100644 --- a/masonry_core/src/passes/paint.rs +++ b/masonry_core/src/passes/paint.rs @@ -80,7 +80,7 @@ fn paint_widget( let transform = state .window_transform .pre_translate(state.border_box_translation()); - let has_clip = state.clip_path.is_some(); + let clips_contents = state.clips_contents; if !is_stashed { let Some((pre_scene, scene, _)) = &mut scene_cache.get(&id) else { debug_panic!( @@ -91,9 +91,10 @@ fn paint_widget( complete_scene.append(pre_scene, Some(transform)); - if let Some(clip) = state.clip_path { - // The clip path is stored in border-box space, so need just window transform. - complete_scene.push_clip_layer(Fill::NonZero, state.window_transform, &clip); + if clips_contents { + let clip_shape = state.clip_shape(); + // The clip shape is in border-box space, so need just window transform. + complete_scene.push_clip_layer(Fill::NonZero, state.window_transform, &clip_shape); } complete_scene.append(scene, Some(transform)); @@ -127,7 +128,7 @@ fn paint_widget( stroke(complete_scene, &rect, color, BORDER_WIDTH); } - if has_clip { + if clips_contents { complete_scene.pop_layer(); }