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

Issue:
Don't hide tooltip when it is shown by accessibility request
Calling `ACCESS.show_tooltip` should keep the tooltip visible until call to `hide_tooltip`.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9909de10b..56c50cd9c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
 # Unreleased
 
+* Fix `tooltip` showing instead of `disabled_tooltip` in contexts that disable the widget after a slight delay.
+* Fix tooltip opened by `ACCESS.show_tooltip` closing immediately on mouse leave.
 
 # 0.10.3
 
diff --git a/crates/zng-ext-clipboard/src/lib.rs b/crates/zng-ext-clipboard/src/lib.rs
index de537b138..0f4a75826 100644
--- a/crates/zng-ext-clipboard/src/lib.rs
+++ b/crates/zng-ext-clipboard/src/lib.rs
@@ -216,6 +216,9 @@ impl CLIPBOARD {
             .map(|s| s.map(|s| Txt::from_str(&s)))
     }
     /// Sets the text string on the clipboard after the current update.
+    ///
+    /// Returns a response var that updates to `Ok(true)` is the text is put on the clipboard,
+    /// `Ok(false)` if another request made on the same update pass replaces this one or `Err(ClipboardError)`.
     pub fn set_text(&self, txt: impl Into<Txt>) -> ResponseVar<Result<bool, ClipboardError>> {
         CLIPBOARD_SV.write().text.request(txt.into())
     }
@@ -244,6 +247,9 @@ impl CLIPBOARD {
     }
 
     /// Set the image on the clipboard after the current update, if it is loaded.
+    ///
+    /// Returns a response var that updates to `Ok(true)` is the text is put on the clipboard,
+    /// `Ok(false)` if another request made on the same update pass replaces this one or `Err(ClipboardError)`.
     pub fn set_image(&self, img: Img) -> ResponseVar<Result<bool, ClipboardError>> {
         CLIPBOARD_SV.write().image.request(img)
     }
@@ -254,6 +260,9 @@ impl CLIPBOARD {
     }
 
     /// Sets the file list on the clipboard after the current update.
+    ///
+    /// Returns a response var that updates to `Ok(true)` is the text is put on the clipboard,
+    /// `Ok(false)` if another request made on the same update pass replaces this one or `Err(ClipboardError)`.
     pub fn set_file_list(&self, list: impl Into<Vec<PathBuf>>) -> ResponseVar<Result<bool, ClipboardError>> {
         CLIPBOARD_SV.write().file_list.request(list.into())
     }
@@ -268,6 +277,9 @@ impl CLIPBOARD {
     /// Set a custom data on the clipboard.
     ///
     /// The current view-process must support `data_type` after the current update.
+    ///
+    /// Returns a response var that updates to `Ok(true)` is the text is put on the clipboard,
+    /// `Ok(false)` if another request made on the same update pass replaces this one or `Err(ClipboardError)`.
     pub fn set_extension(&self, data_type: impl Into<Txt>, data: IpcBytes) -> ResponseVar<Result<bool, ClipboardError>> {
         CLIPBOARD_SV.write().ext.request((data_type.into(), data))
     }
diff --git a/crates/zng-wgt-tooltip/src/lib.rs b/crates/zng-wgt-tooltip/src/lib.rs
index c60d1e56d..9ea44a982 100644
--- a/crates/zng-wgt-tooltip/src/lib.rs
+++ b/crates/zng-wgt-tooltip/src/lib.rs
@@ -13,7 +13,10 @@ zng_wgt::enable_widget_macros!();
 
 use std::time::Duration;
 
-use zng_app::{access::ACCESS_TOOLTIP_EVENT, widget::OnVarArgs};
+use zng_app::{
+    access::ACCESS_TOOLTIP_EVENT,
+    widget::{info::INTERACTIVITY_CHANGED_EVENT, OnVarArgs},
+};
 use zng_ext_input::{
     focus::FOCUS_CHANGED_EVENT,
     gesture::CLICK_EVENT,
@@ -105,7 +108,8 @@ fn tooltip_node(child: impl UiNode, tip: impl IntoVar<WidgetFn<TooltipArgs>>, di
                 WIDGET
                     .sub_var(&tip)
                     .sub_event(&MOUSE_HOVERED_EVENT)
-                    .sub_event(&ACCESS_TOOLTIP_EVENT);
+                    .sub_event(&ACCESS_TOOLTIP_EVENT)
+                    .sub_event(&INTERACTIVITY_CHANGED_EVENT);
             }
             UiNodeOp::Deinit => {
                 child.deinit();
@@ -145,6 +149,10 @@ fn tooltip_node(child: impl UiNode, tip: impl IntoVar<WidgetFn<TooltipArgs>>, di
                             check_cursor = true;
                         }
                     }
+                } else if let Some(args) = INTERACTIVITY_CHANGED_EVENT.on(update) {
+                    if disabled_only != args.new_interactivity(WIDGET.id()).is_disabled() {
+                        show_hide = Some(false);
+                    }
                 }
 
                 if let Some(show) = show_hide {
@@ -235,11 +243,18 @@ fn tooltip_node(child: impl UiNode, tip: impl IntoVar<WidgetFn<TooltipArgs>>, di
         }
 
         if open {
+            let anchor_id = WIDGET.id();
+            let (is_access_open, anchor_var, duration_var) =
+                if check_cursor && !MOUSE.hovered().with(|p| matches!(p, Some(p) if p.contains(anchor_id))) {
+                    (true, ACCESS_TOOLTIP_ANCHOR_VAR, ACCESS_TOOLTIP_DURATION_VAR)
+                } else {
+                    (false, TOOLTIP_ANCHOR_VAR, TOOLTIP_DURATION_VAR)
+                };
+
             let popup = tip.get()(TooltipArgs {
                 anchor_id: WIDGET.id(),
                 disabled: disabled_only,
             });
-            let anchor_id = WIDGET.id();
             let popup = match_widget(popup, move |c, op| match op {
                 UiNodeOp::Init => {
                     c.init();
@@ -271,6 +286,10 @@ fn tooltip_node(child: impl UiNode, tip: impl IntoVar<WidgetFn<TooltipArgs>>, di
                     c.event(update);
 
                     if let Some(args) = MOUSE_HOVERED_EVENT.on(update) {
+                        if is_access_open {
+                            return;
+                        }
+
                         let tooltip_id = match c.with_context(WidgetUpdateMode::Ignore, || WIDGET.id()) {
                             Some(id) => id,
                             None => {
@@ -290,12 +309,6 @@ fn tooltip_node(child: impl UiNode, tip: impl IntoVar<WidgetFn<TooltipArgs>>, di
                 _ => {}
             });
 
-            let (anchor_var, duration_var) = if check_cursor && !MOUSE.hovered().with(|p| matches!(p, Some(p) if p.contains(anchor_id))) {
-                (ACCESS_TOOLTIP_ANCHOR_VAR, ACCESS_TOOLTIP_DURATION_VAR)
-            } else {
-                (TOOLTIP_ANCHOR_VAR, TOOLTIP_DURATION_VAR)
-            };
-
             pop_state = POPUP.open_config(popup, anchor_var, TOOLTIP_CONTEXT_CAPTURE_VAR.get());
             pop_state.subscribe(UpdateOp::Update, anchor_id).perm();
 
@@ -423,10 +436,13 @@ pub fn tooltip_duration(child: impl UiNode, duration: impl IntoVar<Duration>) ->
 /// This duration is used instead of [`tooltip_duration`] when the tooltip is shown by commands such as [`ACCESS.show_tooltip`]
 /// and the cursor is not over the widget.
 ///
+/// Zero means until [`ACCESS.hide_tooltip`], is 5 seconds by default.
+///
 /// This property sets the [`ACCESS_TOOLTIP_DURATION_VAR`].
 ///
 /// [`tooltip_duration`]: fn@tooltip_duration
 /// [`ACCESS.show_tooltip`]: zng_app::access::ACCESS::show_tooltip
+/// [`ACCESS.hide_tooltip`]: zng_app::access::ACCESS::hide_tooltip
 #[property(CONTEXT, default(ACCESS_TOOLTIP_DURATION_VAR))]
 pub fn access_tooltip_duration(child: impl UiNode, duration: impl IntoVar<Duration>) -> impl UiNode {
     with_context_var(child, ACCESS_TOOLTIP_DURATION_VAR, duration)
diff --git a/examples/icon/src/main.rs b/examples/icon/src/main.rs
index 36123227a..a5ee8575c 100644
--- a/examples/icon/src/main.rs
+++ b/examples/icon/src/main.rs
@@ -12,7 +12,6 @@ use zng::{
     font::FontName,
     gesture::{on_click, ClickArgs},
     icon::{self, GlyphIcon, Icon},
-    label::Label,
     layout::{align, margin, padding},
     prelude::*,
     scroll::{lazy, LazyMode, ScrollMode},
@@ -290,22 +289,37 @@ fn size_label(size: Txt) -> impl UiNode {
 }
 
 fn code_copy(label: Txt, code: Txt) -> impl UiNode {
-    let copied = var(false);
-    Label! {
-        txt = label;
-        font_family = FontName::monospace();
+    let enabled = var(true);
+    let copy_status = var(Txt::from(""));
+    Button! {
+        style_fn = zng::button::LightStyle!();
+        padding = 2;
+
+        container::child_start = ICONS.get("copy"), 4;
+        child = Text!(label);
+
+        text::font_family = FontName::monospace();
         mouse::cursor = mouse::CursorIcon::Pointer;
-        widget::enabled = copied.map(|&c| !c);
+        widget::enabled = enabled.clone();
+
         tooltip = Tip!(Text!("copy {code}"));
-        tip::disabled_tooltip = Tip!(Text!("copied!"));
-        container::child_start = ICONS.get("copy"), 4;
-        on_click = async_hn!(copied, code, |_| {
-            if clipboard::CLIPBOARD.set_text(code).wait_rsp().await.is_ok() {
-                ACCESS.show_tooltip(WINDOW.id(), WIDGET.id());
-                copied.set(true);
-                task::deadline(2.secs()).await;
-                copied.set(false);
+        tip::disabled_tooltip = Tip!(Text!(copy_status.clone()));
+
+        on_click = async_hn!(enabled, code, copy_status, |_| {
+            copy_status.set("copying..");
+            enabled.set(false);
+            ACCESS.show_tooltip(WINDOW.id(), WIDGET.id());
+
+            match clipboard::CLIPBOARD.set_text(code).wait_rsp().await {
+                Ok(copied) => {
+                    debug_assert!(copied); // no other clipboard request
+                    copy_status.set("copied!");
+                },
+                Err(e) => copy_status.set(formatx!("error: {e}")),
             }
+
+            task::deadline(2.secs()).await;
+            enabled.set(true);
         });
         when *#gesture::is_hovered {
             background_color = text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));


Test patch: