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

Issue:
Implement visibility change event properties
Based on `VISIBILITY_CHANGED_EVENT` based event property.
  - on_show, on_hide, on_collapse.
  - on_show, should also fire once after first render?


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 64f2d3dd6..e45a0ffe1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 # Unreleased
 
+* Fix visibility and transform events losing track of a widget after info rebuild.
+* Add visibility changed event properties, `on_visibility_changed`, `on_show`, `on_collapse` and others.
+* Add `VisibilityChangedArgs` helper methods for tracking changes for a specific widget.
 * Fix doc links in inherited properties fetched from redirected original pages.
 * Fix `cargo zng res` not getting explicit metadata from lib crates.
 * Implement `--verbose` for `cargo zng res`.
diff --git a/crates/zng-app/src/widget/info.rs b/crates/zng-app/src/widget/info.rs
index 009730e83..9b1e6e9f7 100644
--- a/crates/zng-app/src/widget/info.rs
+++ b/crates/zng-app/src/widget/info.rs
@@ -329,11 +329,13 @@ impl WidgetInfoTree {
 
             TRANSFORM_CHANGED_EVENT.notify(TransformChangedArgs::now(self.clone(), changes));
         }
+        drop(frame); // wgt.visibility can read frame
 
         let mut changes = IdMap::new();
         VISIBILITY_CHANGED_EVENT.visit_subscribers(|wid| {
             if let Some(wgt) = self.get(wid) {
                 let visibility = wgt.visibility();
+                let mut frame = self.0.frame.write();
                 match frame.visibility_changed_subs.entry(wid) {
                     IdEntry::Occupied(mut e) => {
                         let prev = e.insert(visibility);
@@ -348,8 +350,10 @@ impl WidgetInfoTree {
             }
         });
         if !changes.is_empty() {
-            if (frame.visibility_changed_subs.len() - changes.len()) > 500 {
-                frame
+            if (self.0.frame.read().visibility_changed_subs.len() - changes.len()) > 500 {
+                self.0
+                    .frame
+                    .write()
                     .visibility_changed_subs
                     .retain(|k, _| VISIBILITY_CHANGED_EVENT.is_subscriber(*k));
             }
diff --git a/crates/zng-app/src/widget/info/builder.rs b/crates/zng-app/src/widget/info/builder.rs
index 61a87cdc5..2f9439196 100644
--- a/crates/zng-app/src/widget/info/builder.rs
+++ b/crates/zng-app/src/widget/info/builder.rs
@@ -301,16 +301,22 @@ impl WidgetInfoBuilder {
         let generation;
         let widget_count_offsets;
         let spatial_bounds;
+        let transform_changed_subs;
+        let visibility_changed_subs;
 
         if let Some(t) = &previous_tree {
             let t = t.0.frame.read();
             generation = t.stats.generation.wrapping_add(1);
             widget_count_offsets = t.widget_count_offsets.clone();
             spatial_bounds = t.spatial_bounds;
+            transform_changed_subs = t.transform_changed_subs.clone();
+            visibility_changed_subs = t.visibility_changed_subs.clone();
         } else {
             generation = 0;
             widget_count_offsets = ParallelSegmentOffsets::default();
             spatial_bounds = PxBox::zero();
+            transform_changed_subs = IdMap::new();
+            visibility_changed_subs = IdMap::new();
         }
 
         let mut lookup = IdMap::new();
@@ -342,8 +348,8 @@ impl WidgetInfoBuilder {
                 scale_factor: self.scale_factor,
                 spatial_bounds,
                 widget_count_offsets,
-                transform_changed_subs: IdMap::new(),
-                visibility_changed_subs: IdMap::new(),
+                transform_changed_subs,
+                visibility_changed_subs,
                 view_process_gen: ViewProcessGen::INVALID,
             }),
 
@@ -601,6 +607,49 @@ impl InteractivityChangedArgs {
     }
 }
 
+impl VisibilityChangedArgs {
+    /// Gets the previous and new visibility for the widget, if it has changed.
+    pub fn change(&self, widget_id: WidgetId) -> Option<(Visibility, Visibility)> {
+        let prev = *self.changed.get(&widget_id)?;
+        let new = self.tree.get(widget_id)?.visibility();
+        Some((prev, new))
+    }
+
+    /// Gets the previous visibility of the widget, if it has changed.
+    pub fn prev_vis(&self, widget_id: WidgetId) -> Option<Visibility> {
+        self.changed.get(&widget_id).copied()
+    }
+
+    /// Gets the new visibility of the widget, if it has changed.
+    pub fn new_vis(&self, widget_id: WidgetId) -> Option<Visibility> {
+        self.change(widget_id).map(|(_, n)| n)
+    }
+
+    /// Widget was visible or hidden, now is collapsed.
+    pub fn is_collapse(&self, widget_id: WidgetId) -> bool {
+        matches!(
+            self.change(widget_id),
+            Some((Visibility::Visible | Visibility::Hidden, Visibility::Collapsed))
+        )
+    }
+
+    /// Widget was visible or collapsed, now is hidden.
+    pub fn is_hide(&self, widget_id: WidgetId) -> bool {
+        matches!(
+            self.change(widget_id),
+            Some((Visibility::Visible | Visibility::Collapsed, Visibility::Hidden))
+        )
+    }
+
+    /// Widget was not hidden or collapsed, now is visible.
+    pub fn is_show(&self, widget_id: WidgetId) -> bool {
+        matches!(
+            self.change(widget_id),
+            Some((Visibility::Hidden | Visibility::Collapsed, Visibility::Visible))
+        )
+    }
+}
+
 /// Info about the input inline connecting rows of the widget.
 #[derive(Clone, Debug, Default, PartialEq)]
 pub struct WidgetInlineMeasure {
diff --git a/crates/zng-wgt/src/visibility_props.rs b/crates/zng-wgt/src/visibility_props.rs
index 56ad8af1c..7f81fcca7 100644
--- a/crates/zng-wgt/src/visibility_props.rs
+++ b/crates/zng-wgt/src/visibility_props.rs
@@ -184,4 +184,33 @@ event_property! {
         args: info::TransformChangedArgs,
         filter: |a| a.offset(WIDGET.id()).unwrap_or_default() != PxVector::zero(),
     }
+
+    /// Widget visibility changed.
+    pub fn visibility_changed {
+        event: info::VISIBILITY_CHANGED_EVENT,
+        args: info::VisibilityChangedArgs,
+    }
+
+    /// Widget visibility changed to collapsed.
+    pub fn collapse {
+        event: info::VISIBILITY_CHANGED_EVENT,
+        args: info::VisibilityChangedArgs,
+        filter: |a| a.is_collapse(WIDGET.id()),
+    }
+
+    /// Widget visibility changed to hidden.
+    pub fn hide {
+        event: info::VISIBILITY_CHANGED_EVENT,
+        args: info::VisibilityChangedArgs,
+        filter: |a| a.is_hide(WIDGET.id()),
+    }
+
+    /// Widget visibility changed to visible.
+    ///
+    /// Note that widgets are **already marked visible** before the first render so this event does not fire on init.
+    pub fn show {
+        event: info::VISIBILITY_CHANGED_EVENT,
+        args: info::VisibilityChangedArgs,
+        filter: |a| a.is_show(WIDGET.id()),
+    }
 }
diff --git a/crates/zng/src/widget.rs b/crates/zng/src/widget.rs
index 43d7284c5..4131c0e7d 100644
--- a/crates/zng/src/widget.rs
+++ b/crates/zng/src/widget.rs
@@ -50,12 +50,13 @@ pub use zng_app::widget::node::ZIndex;
 pub use zng_wgt::{
     auto_hide, border, border_align, border_over, clip_to_bounds, corner_radius, corner_radius_fit, enabled, hit_test_mode, inline,
     interactive, is_collapsed, is_disabled, is_enabled, is_hidden, is_hit_testable, is_inited, is_mobile, is_visible, modal,
-    modal_included, modal_includes, on_block, on_blocked_changed, on_deinit, on_disable, on_enable, on_enabled_changed, on_info_init,
-    on_init, on_interactivity_changed, on_move, on_node_op, on_pre_block, on_pre_blocked_changed, on_pre_deinit, on_pre_disable,
-    on_pre_enable, on_pre_enabled_changed, on_pre_init, on_pre_interactivity_changed, on_pre_move, on_pre_node_op,
-    on_pre_transform_changed, on_pre_unblock, on_pre_update, on_pre_vis_disable, on_pre_vis_enable, on_pre_vis_enabled_changed,
-    on_transform_changed, on_unblock, on_update, on_vis_disable, on_vis_enable, on_vis_enabled_changed, parallel, visibility, wgt_fn,
-    z_index, EditorRequestArgs, OnNodeOpArgs, WeakWidgetFn, Wgt, WidgetFn, EDITORS, IS_MOBILE_VAR,
+    modal_included, modal_includes, on_block, on_blocked_changed, on_collapse, on_deinit, on_disable, on_enable, on_enabled_changed,
+    on_hide, on_info_init, on_init, on_interactivity_changed, on_move, on_node_op, on_pre_block, on_pre_blocked_changed, on_pre_collapse,
+    on_pre_deinit, on_pre_disable, on_pre_enable, on_pre_enabled_changed, on_pre_hide, on_pre_init, on_pre_interactivity_changed,
+    on_pre_move, on_pre_node_op, on_pre_show, on_pre_transform_changed, on_pre_unblock, on_pre_update, on_pre_vis_disable,
+    on_pre_vis_enable, on_pre_vis_enabled_changed, on_pre_visibility_changed, on_show, on_transform_changed, on_unblock, on_update,
+    on_vis_disable, on_vis_enable, on_vis_enabled_changed, on_visibility_changed, parallel, visibility, wgt_fn, z_index, EditorRequestArgs,
+    OnNodeOpArgs, WeakWidgetFn, Wgt, WidgetFn, EDITORS, IS_MOBILE_VAR,
 };
 
 pub use zng_wgt_fill::{


Test patch: