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

Issue:
Directional nav wraps instead of scrolling
This can be seen in the icon example:

* Set `directional_nav = DirectionalNav::Cycle;`
* Press Alt+ArrowUp.
* Focus moved down to the bottom visible icon row not the last row.


Discussion / hints:
`tab_nav = TabNav::Cycle;` also does not work in the icon example. Not sure if related.

Edit:
Not related, see issue: #162
Two issues here, directional does not look outside viewport and `lazy` removes the last items.

The lazy issue can be observed with tab nav cycle. Will solve the directional issue only to close this, review other after.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 655ec17d8..b5a5bc8d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -52,6 +52,8 @@ fn version_0_5() -> impl UiNode {
 * Add `WidgetPath::parent_id` helper method.
 * Fix auto scroll-to-focused not working when the focused child does not subscribe to focus change events.
 * Fix auto scroll-to-focused scrolling when large widget is already visible.
+* Add `WidgetInfo::spatial_bounds`.
+* Fix directional navigation cycling only inside viewport now full spatial bounds of scopes.
 
 # 0.4.0
 
diff --git a/crates/zng-app/src/widget/info.rs b/crates/zng-app/src/widget/info.rs
index 114b467b7..0c53ef3a1 100644
--- a/crates/zng-app/src/widget/info.rs
+++ b/crates/zng-app/src/widget/info.rs
@@ -291,8 +291,7 @@ impl WidgetInfoTree {
         let mut spatial_bounds = self.root().outer_bounds().to_box2d();
         for out in frame.out_of_bounds.iter() {
             let b = WidgetInfo::new(self.clone(), *out).inner_bounds().to_box2d();
-            spatial_bounds.min = spatial_bounds.min.min(b.min);
-            spatial_bounds.max = spatial_bounds.max.max(b.max);
+            spatial_bounds = spatial_bounds.union(&b);
         }
         frame.spatial_bounds = spatial_bounds;
 
@@ -1107,11 +1106,11 @@ impl WidgetInfo {
         let mut disabled = None;
 
         for w in self.self_and_ancestors() {
-            let intera = w.interactivity();
-            if intera.contains(Interactivity::BLOCKED) {
+            let interactivity = w.interactivity();
+            if interactivity.contains(Interactivity::BLOCKED) {
                 blocked = Some(path.len());
             }
-            if intera.contains(Interactivity::DISABLED) {
+            if interactivity.contains(Interactivity::DISABLED) {
                 disabled = Some(path.len());
             }
 
@@ -1341,6 +1340,12 @@ impl WidgetInfo {
         info.bounds_info.inner_bounds()
     }
 
+    /// Compute the bounding box that envelops self and descendants inner bounds.
+    pub fn spatial_bounds(&self) -> PxBox {
+        self.out_of_bounds()
+            .fold(self.inner_bounds().to_box2d(), |acc, w| acc.union(&w.inner_bounds().to_box2d()))
+    }
+
     /// Widget inner bounds center in the window space.
     pub fn center(&self) -> PxPoint {
         self.inner_bounds().center()
diff --git a/crates/zng-ext-input/src/focus/focus_info.rs b/crates/zng-ext-input/src/focus/focus_info.rs
index a645bd232..69e36a2f2 100644
--- a/crates/zng-ext-input/src/focus/focus_info.rs
+++ b/crates/zng-ext-input/src/focus/focus_info.rs
@@ -1368,9 +1368,9 @@ impl WidgetFocusInfo {
                 DirectionalNav::Contained => self.directional_next_from(Orientation2D::Above, origin),
                 DirectionalNav::Cycle => {
                     self.directional_next_from(Orientation2D::Above, origin).or_else(|| {
-                        // next up from the same X but from the bottom segment of scope.
+                        // next up from the same X but from the bottom segment of scope spatial bounds.
                         let mut from_pt = origin.center();
-                        from_pt.y = scope.info.inner_bounds().max().y;
+                        from_pt.y = scope.info.spatial_bounds().max.y;
                         self.directional_from(
                             &scope,
                             PxRect::new(from_pt, PxSize::splat(Px(1))).to_box2d(),
@@ -1404,9 +1404,9 @@ impl WidgetFocusInfo {
                 }),
                 DirectionalNav::Contained => self.directional_next_from(Orientation2D::Right, origin),
                 DirectionalNav::Cycle => self.directional_next_from(Orientation2D::Right, origin).or_else(|| {
-                    // next right from the same Y but from the left segment of scope.
+                    // next right from the same Y but from the left segment of scope spatial bounds.
                     let mut from_pt = origin.center();
-                    from_pt.x = scope.info.inner_bounds().min().x;
+                    from_pt.x = scope.info.spatial_bounds().min.x;
                     self.directional_from(
                         &scope,
                         PxRect::new(from_pt, PxSize::splat(Px(1))).to_box2d(),
@@ -1439,9 +1439,9 @@ impl WidgetFocusInfo {
                 }),
                 DirectionalNav::Contained => self.directional_next_from(Orientation2D::Below, origin),
                 DirectionalNav::Cycle => self.directional_next_from(Orientation2D::Below, origin).or_else(|| {
-                    // next down from the same X but from the top segment of scope.
+                    // next down from the same X but from the top segment of scope spatial bounds.
                     let mut from_pt = origin.center();
-                    from_pt.y = scope.info.inner_bounds().min().y;
+                    from_pt.y = scope.info.spatial_bounds().min.y;
                     self.directional_from(
                         &scope,
                         PxRect::new(from_pt, PxSize::splat(Px(1))).to_box2d(),
@@ -1474,9 +1474,9 @@ impl WidgetFocusInfo {
                 }),
                 DirectionalNav::Contained => self.directional_next_from(Orientation2D::Left, origin),
                 DirectionalNav::Cycle => self.directional_next_from(Orientation2D::Left, origin).or_else(|| {
-                    // next left from the same Y but from the right segment of scope.
+                    // next left from the same Y but from the right segment of scope spatial bounds.
                     let mut from_pt = origin.center();
-                    from_pt.x = scope.info.inner_bounds().max().x;
+                    from_pt.x = scope.info.spatial_bounds().max.x;
                     self.directional_from(
                         &scope,
                         PxRect::new(from_pt, PxSize::splat(Px(1))).to_box2d(),


Test patch: