Repository: slint-ui/slint (language: rust)

Issue:
AccessKit node bounds are never clipped
### Bug Description

When an element contained in a scroll view is only partially visible, its AccessKit `Node::bounds` should be set accordingly.

## To reproduce

- Run the crud example
- Scroll the list view a bit so that the first item is partially hidden
- Observe with an accessibility inspector that the first item's bounding box goes outside of the item's parent bounding box.

### Reproducible Code (if applicable)

```slint
Unmodified crud example exhibits the bug.
```


### Environment Details

- Slint Version: master branch
- Platform/OS: Windows 11
- Programming Language: Rust
- Backend/Renderer: winit


### Product Impact

_No response_


Discussion / hints:
I wonder, should they really be clipped or should elements like `Flickable` set [`set_clips_children`](https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.set_clips_children) ?
If I recall correctly, we discussed that children should retain their bounds but the clipping element should set `set_clips_children`.

Fix patch:
diff --git a/internal/backends/qt/qt_widgets/button.rs b/internal/backends/qt/qt_widgets/button.rs
index 1aaf5b6a3b2..00b8cb651d7 100644
--- a/internal/backends/qt/qt_widgets/button.rs
+++ b/internal/backends/qt/qt_widgets/button.rs
@@ -431,6 +431,10 @@ impl Item for NativeButton {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeButton {
diff --git a/internal/backends/qt/qt_widgets/checkbox.rs b/internal/backends/qt/qt_widgets/checkbox.rs
index fe677adf43f..3a077c4637e 100644
--- a/internal/backends/qt/qt_widgets/checkbox.rs
+++ b/internal/backends/qt/qt_widgets/checkbox.rs
@@ -183,6 +183,10 @@ impl Item for NativeCheckBox {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeCheckBox {
diff --git a/internal/backends/qt/qt_widgets/combobox.rs b/internal/backends/qt/qt_widgets/combobox.rs
index 6f9fffc22ab..cb0f5d0efea 100644
--- a/internal/backends/qt/qt_widgets/combobox.rs
+++ b/internal/backends/qt/qt_widgets/combobox.rs
@@ -149,6 +149,10 @@ impl Item for NativeComboBox {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeComboBox {
@@ -268,6 +272,10 @@ impl Item for NativeComboBoxPopup {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeComboBoxPopup {
diff --git a/internal/backends/qt/qt_widgets/groupbox.rs b/internal/backends/qt/qt_widgets/groupbox.rs
index 186987901a3..587c5fe3cd9 100644
--- a/internal/backends/qt/qt_widgets/groupbox.rs
+++ b/internal/backends/qt/qt_widgets/groupbox.rs
@@ -243,6 +243,10 @@ impl Item for NativeGroupBox {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeGroupBox {
diff --git a/internal/backends/qt/qt_widgets/lineedit.rs b/internal/backends/qt/qt_widgets/lineedit.rs
index aa7d74e6ace..77a331cf448 100644
--- a/internal/backends/qt/qt_widgets/lineedit.rs
+++ b/internal/backends/qt/qt_widgets/lineedit.rs
@@ -164,6 +164,10 @@ impl Item for NativeLineEdit {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeLineEdit {
diff --git a/internal/backends/qt/qt_widgets/listviewitem.rs b/internal/backends/qt/qt_widgets/listviewitem.rs
index 31a88b7ab6c..8fbebbe71bd 100644
--- a/internal/backends/qt/qt_widgets/listviewitem.rs
+++ b/internal/backends/qt/qt_widgets/listviewitem.rs
@@ -202,6 +202,10 @@ impl Item for NativeStandardListViewItem {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeStandardListViewItem {
diff --git a/internal/backends/qt/qt_widgets/progress_indicator.rs b/internal/backends/qt/qt_widgets/progress_indicator.rs
index 42a64008c67..f7d63c88cc1 100644
--- a/internal/backends/qt/qt_widgets/progress_indicator.rs
+++ b/internal/backends/qt/qt_widgets/progress_indicator.rs
@@ -138,6 +138,10 @@ impl Item for NativeProgressIndicator {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeProgressIndicator {
diff --git a/internal/backends/qt/qt_widgets/scrollview.rs b/internal/backends/qt/qt_widgets/scrollview.rs
index 2deaffa905e..4e1a006b13e 100644
--- a/internal/backends/qt/qt_widgets/scrollview.rs
+++ b/internal/backends/qt/qt_widgets/scrollview.rs
@@ -454,6 +454,10 @@ impl Item for NativeScrollView {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeScrollView {
diff --git a/internal/backends/qt/qt_widgets/slider.rs b/internal/backends/qt/qt_widgets/slider.rs
index f9668c2ed83..c54877ea79a 100644
--- a/internal/backends/qt/qt_widgets/slider.rs
+++ b/internal/backends/qt/qt_widgets/slider.rs
@@ -372,6 +372,10 @@ impl Item for NativeSlider {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl NativeSlider {
diff --git a/internal/backends/qt/qt_widgets/spinbox.rs b/internal/backends/qt/qt_widgets/spinbox.rs
index 7d3ec7fe43b..112a4e4593c 100644
--- a/internal/backends/qt/qt_widgets/spinbox.rs
+++ b/internal/backends/qt/qt_widgets/spinbox.rs
@@ -347,6 +347,10 @@ impl Item for NativeSpinBox {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeSpinBox {
diff --git a/internal/backends/qt/qt_widgets/tableheadersection.rs b/internal/backends/qt/qt_widgets/tableheadersection.rs
index 3bfe7ee4434..7fd96126204 100644
--- a/internal/backends/qt/qt_widgets/tableheadersection.rs
+++ b/internal/backends/qt/qt_widgets/tableheadersection.rs
@@ -163,6 +163,10 @@ impl Item for NativeTableHeaderSection {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeTableHeaderSection {
diff --git a/internal/backends/qt/qt_widgets/tabwidget.rs b/internal/backends/qt/qt_widgets/tabwidget.rs
index 8cac8b5b402..2a6e4fdc43d 100644
--- a/internal/backends/qt/qt_widgets/tabwidget.rs
+++ b/internal/backends/qt/qt_widgets/tabwidget.rs
@@ -324,6 +324,10 @@ impl Item for NativeTabWidget {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeTabWidget {
@@ -546,6 +550,10 @@ impl Item for NativeTab {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for NativeTab {
diff --git a/internal/backends/winit/accesskit.rs b/internal/backends/winit/accesskit.rs
index 374d69bb8fd..6795bbe8d67 100644
--- a/internal/backends/winit/accesskit.rs
+++ b/internal/backends/winit/accesskit.rs
@@ -507,6 +507,10 @@ impl NodeCollection {
             node.set_hidden();
         }
 
+        if item.borrow().as_ref().clips_children() {
+            node.set_clips_children();
+        }
+
         let geometry = item.geometry();
         let absolute_origin = item.map_to_window(geometry.origin) + window_position.to_vector();
         let physical_origin = (absolute_origin * scale_factor).cast::<f64>();
diff --git a/internal/core/input.rs b/internal/core/input.rs
index 1e7a9075ec8..a1f909f890a 100644
--- a/internal/core/input.rs
+++ b/internal/core/input.rs
@@ -650,7 +650,7 @@ pub(crate) fn send_exit_events(
             *p -= g.origin.to_vector();
         }
         if !contains || clipped {
-            if crate::item_rendering::is_clipping_item(item.borrow()) {
+            if item.borrow().as_ref().clips_children() {
                 clipped = true;
             }
             item.borrow().as_ref().input_event(MouseEvent::Exit, window_adapter, &item);
@@ -759,7 +759,7 @@ fn send_mouse_event_to_item(
     event_for_children.translate(-geom.origin.to_vector());
 
     let filter_result = if mouse_event.position().is_some_and(|p| geom.contains(p))
-        || crate::item_rendering::is_clipping_item(item)
+        || item.as_ref().clips_children()
     {
         item.as_ref().input_event_filter_before_children(
             event_for_children,
diff --git a/internal/core/item_rendering.rs b/internal/core/item_rendering.rs
index 629360d50ec..6b6075962b2 100644
--- a/internal/core/item_rendering.rs
+++ b/internal/core/item_rendering.rs
@@ -181,13 +181,6 @@ impl<T: Clone> ItemCache<T> {
     }
 }
 
-/// Return true if the item might be a clipping item
-pub fn is_clipping_item(item: Pin<ItemRef>) -> bool {
-    //(FIXME: there should be some flag in the vtable instead of down-casting)
-    ItemRef::downcast_pin::<Flickable>(item).is_some()
-        || ItemRef::downcast_pin::<Clip>(item).is_some_and(|clip_item| clip_item.as_ref().clip())
-}
-
 /// Renders the children of the item with the specified index into the renderer.
 pub fn render_item_children(
     renderer: &mut dyn ItemRenderer,
@@ -208,7 +201,7 @@ pub fn render_item_children(
             // Don't render items that are clipped, with the exception of the Clip or Flickable since
             // they themselves clip their content.
             let render_result = if do_draw
-               || is_clipping_item(item)
+               || item.as_ref().clips_children()
                // HACK, the geometry of the box shadow does not include the shadow, because when the shadow is the root for repeated elements it would translate the children
                || ItemRef::downcast_pin::<BoxShadow>(item).is_some()
             {
@@ -270,7 +263,7 @@ pub fn item_children_bounding_rect(
                 bounding_rect = bounding_rect.union(&clipped_item_geometry);
             }
 
-            if !is_clipping_item(item) {
+            if !item.as_ref().clips_children() {
                 bounding_rect = bounding_rect.union(&item_children_bounding_rect(
                     component,
                     index as isize,
@@ -592,7 +585,7 @@ impl CachedItemBoundingBoxAndTransform {
     ) -> Self {
         let geometry = item_rc.geometry();
 
-        if is_clipping_item(item_rc.borrow()) {
+        if item_rc.borrow().as_ref().clips_children() {
             return Self::ClipItem { geometry };
         }
 
diff --git a/internal/core/item_tree.rs b/internal/core/item_tree.rs
index 98299933404..f336036dda0 100644
--- a/internal/core/item_tree.rs
+++ b/internal/core/item_tree.rs
@@ -355,7 +355,7 @@ impl ItemRc {
         let geometry = self.geometry().translate(parent_geometry.origin.to_vector());
 
         let item = self.borrow();
-        if crate::item_rendering::is_clipping_item(item) {
+        if item.as_ref().clips_children() {
             clip = geometry.intersection(&clip).unwrap_or_default();
         }
 
diff --git a/internal/core/items.rs b/internal/core/items.rs
index b33b37cf31e..0c7238a1736 100644
--- a/internal/core/items.rs
+++ b/internal/core/items.rs
@@ -178,6 +178,8 @@ pub struct ItemVTable {
         self_rc: &ItemRc,
         geometry: LogicalRect,
     ) -> LogicalRect,
+
+    pub clips_children: extern "C" fn(core::pin::Pin<VRef<ItemVTable>>) -> bool,
 }
 
 /// Alias for `vtable::VRef<ItemVTable>` which represent a pointer to a `dyn Item` with
@@ -257,6 +259,10 @@ impl Item for Empty {
         geometry.size = LogicalSize::zero();
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for Empty {
@@ -344,6 +350,10 @@ impl Item for Rectangle {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl RenderRectangle for Rectangle {
@@ -440,6 +450,10 @@ impl Item for BasicBorderRectangle {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl RenderBorderRectangle for BasicBorderRectangle {
@@ -549,6 +563,10 @@ impl Item for BorderRectangle {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl RenderBorderRectangle for BorderRectangle {
@@ -683,6 +701,10 @@ impl Item for Clip {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        self.clip()
+    }
 }
 
 impl Clip {
@@ -779,6 +801,10 @@ impl Item for Opacity {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl Opacity {
@@ -892,6 +918,10 @@ impl Item for Layer {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for Layer {
@@ -985,6 +1015,10 @@ impl Item for Rotate {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for Rotate {
@@ -1121,6 +1155,10 @@ impl Item for WindowItem {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl RenderRectangle for WindowItem {
@@ -1256,6 +1294,10 @@ impl Item for ContextMenu {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ContextMenu {
@@ -1369,6 +1411,10 @@ impl Item for BoxShadow {
             .outer_rect(euclid::SideOffsets2D::from_length_all_same(self.blur()))
             .translate(LogicalVector::from_lengths(self.offset_x(), self.offset_y()))
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for BoxShadow {
diff --git a/internal/core/items/component_container.rs b/internal/core/items/component_container.rs
index 69d0f0a60b4..df1ca455015 100644
--- a/internal/core/items/component_container.rs
+++ b/internal/core/items/component_container.rs
@@ -230,6 +230,10 @@ impl Item for ComponentContainer {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl RenderRectangle for ComponentContainer {
diff --git a/internal/core/items/flickable.rs b/internal/core/items/flickable.rs
index a6205f10f98..b28de892f72 100644
--- a/internal/core/items/flickable.rs
+++ b/internal/core/items/flickable.rs
@@ -155,6 +155,10 @@ impl Item for Flickable {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        true
+    }
 }
 
 impl ItemConsts for Flickable {
diff --git a/internal/core/items/image.rs b/internal/core/items/image.rs
index 18e4e5b8120..fd39dcc0b76 100644
--- a/internal/core/items/image.rs
+++ b/internal/core/items/image.rs
@@ -117,6 +117,10 @@ impl Item for ImageItem {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl RenderImage for ImageItem {
@@ -263,6 +267,10 @@ impl Item for ClippedImage {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl RenderImage for ClippedImage {
diff --git a/internal/core/items/input_items.rs b/internal/core/items/input_items.rs
index fb8202cb319..01f413ac01a 100644
--- a/internal/core/items/input_items.rs
+++ b/internal/core/items/input_items.rs
@@ -238,6 +238,10 @@ impl Item for TouchArea {
         geometry.size = LogicalSize::zero();
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for TouchArea {
@@ -359,6 +363,10 @@ impl Item for FocusScope {
         geometry.size = LogicalSize::zero();
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for FocusScope {
@@ -557,6 +565,10 @@ impl Item for SwipeGestureHandler {
         geometry.size = LogicalSize::zero();
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for SwipeGestureHandler {
diff --git a/internal/core/items/path.rs b/internal/core/items/path.rs
index b5b3203ca53..e6ccff148a6 100644
--- a/internal/core/items/path.rs
+++ b/internal/core/items/path.rs
@@ -139,6 +139,10 @@ impl Item for Path {
     ) -> LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl Path {
diff --git a/internal/core/items/text.rs b/internal/core/items/text.rs
index db0313356aa..6bdaa6753cd 100644
--- a/internal/core/items/text.rs
+++ b/internal/core/items/text.rs
@@ -133,6 +133,10 @@ impl Item for ComplexText {
     ) -> LogicalRect {
         self.text_bounding_rect(window_adapter, geometry.cast()).cast()
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for ComplexText {
@@ -307,6 +311,10 @@ impl Item for SimpleText {
     ) -> LogicalRect {
         self.text_bounding_rect(window_adapter, geometry.cast()).cast()
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for SimpleText {
@@ -1005,6 +1013,10 @@ impl Item for TextInput {
         ));
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl ItemConsts for TextInput {
diff --git a/internal/core/menus.rs b/internal/core/menus.rs
index b832b0ef300..712a8d7810f 100644
--- a/internal/core/menus.rs
+++ b/internal/core/menus.rs
@@ -198,6 +198,10 @@ impl crate::items::Item for MenuItem {
     ) -> crate::lengths::LogicalRect {
         geometry
     }
+
+    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
+        false
+    }
 }
 
 impl crate::items::ItemConsts for MenuItem {


Test patch: