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

Issue:
Restore focus from `modal` focus scope to button that opened it
Right now the focus returns to the background focusable nearest the last click.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2c8216cf..814f8fbe7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 # Unreleased
 
+* Add `return_focus_on_deinit`.
 * Add a copy button to the markdown links popup.
 * Add warning for slow event handlers in debug builds.
 * Add in memory "clipboard" for headless tests that use `CLIPBOARD`.
diff --git a/crates/zng-wgt-input/src/focus.rs b/crates/zng-wgt-input/src/focus.rs
index 475344cdc..85fe8f823 100644
--- a/crates/zng-wgt-input/src/focus.rs
+++ b/crates/zng-wgt-input/src/focus.rs
@@ -4,6 +4,7 @@
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 
+use zng_app::widget::info::WIDGET_INFO_CHANGED_EVENT;
 use zng_ext_input::focus::*;
 use zng_ext_input::gesture::{CLICK_EVENT, GESTURES};
 use zng_ext_input::mouse::MOUSE_INPUT_EVENT;
@@ -488,6 +489,45 @@ pub fn focus_on_init(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl Ui
     })
 }
 
+/// If the widget return focus to the previous focus when it inited.
+///
+/// This can be used with the [`modal`] property to declare *modal dialogs* that return the focus
+/// to the widget that opens the dialog.
+///
+/// [`modal`]: fn@zng_wgt::modal
+#[property(CONTEXT, default(false), widget_impl(FocusableMix<P>))]
+pub fn return_focus_on_deinit(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
+    let enabled = enabled.into_var();
+    let mut return_focus = None;
+    match_node(child, move |_, op| match op {
+        UiNodeOp::Init => {
+            return_focus = FOCUS.focused().with(|p| p.as_ref().map(|p| p.widget_id()));
+        }
+        UiNodeOp::Deinit => {
+            if let Some(id) = return_focus.take() {
+                if enabled.get() {
+                    if let Some(w) = zng_ext_window::WINDOWS.widget_info(id) {
+                        if w.into_focusable(false, false).is_some() {
+                            // can focus on the next update
+                            FOCUS.focus_widget(id, false);
+                            return;
+                        }
+                    }
+                    // try focus after info rebuild.
+                    WIDGET_INFO_CHANGED_EVENT
+                        .on_pre_event(app_hn_once!(|_| {
+                            FOCUS.focus_widget(id, false);
+                        }))
+                        .perm();
+                    // ensure info rebuilds to clear the event at least
+                    WIDGET.update_info();
+                }
+            }
+        }
+        _ => {}
+    })
+}
+
 /// Focusable widget mixin. Enables keyboard focusing on the widget and adds a focused highlight visual.
 #[widget_mixin]
 pub struct FocusableMix<P>(P);
diff --git a/crates/zng/src/focus.rs b/crates/zng/src/focus.rs
index 43977c9fb..ebde8384c 100644
--- a/crates/zng/src/focus.rs
+++ b/crates/zng/src/focus.rs
@@ -139,5 +139,5 @@ pub use zng_wgt_input::focus::{
     alt_focus_scope, directional_nav, focus_click_behavior, focus_highlight, focus_on_init, focus_scope, focus_scope_behavior,
     focus_shortcut, focusable, is_focus_within, is_focus_within_hgl, is_focused, is_focused_hgl, is_return_focus, is_return_focus_within,
     on_blur, on_focus, on_focus_changed, on_focus_enter, on_focus_leave, on_pre_blur, on_pre_focus, on_pre_focus_changed,
-    on_pre_focus_enter, on_pre_focus_leave, skip_directional, tab_index, tab_nav, FocusClickBehavior, FocusableMix,
+    on_pre_focus_enter, on_pre_focus_leave, return_focus_on_deinit, skip_directional, tab_index, tab_nav, FocusClickBehavior, FocusableMix,
 };
diff --git a/examples/focus/src/main.rs b/examples/focus/src/main.rs
index 21fa2749d..602ef4826 100644
--- a/examples/focus/src/main.rs
+++ b/examples/focus/src/main.rs
@@ -5,8 +5,8 @@ use zng::{
     color::filter::drop_shadow,
     focus::{
         alt_focus_scope, directional_nav, focus_click_behavior, focus_scope, focus_shortcut, focusable, is_focused, is_return_focus,
-        tab_index, tab_nav, DirectionalNav, FocusChangedArgs, FocusClickBehavior, FocusRequest, FocusTarget, TabIndex, TabNav,
-        FOCUS_CHANGED_EVENT,
+        return_focus_on_deinit, tab_index, tab_nav, DirectionalNav, FocusChangedArgs, FocusClickBehavior, FocusRequest, FocusTarget,
+        TabIndex, TabNav, FOCUS_CHANGED_EVENT,
     },
     font::FontName,
     gesture::ClickArgs,
@@ -145,6 +145,7 @@ fn functions(window_enabled: ArcVar<bool>) -> impl UiNode {
             disable_window(window_enabled.clone()),
             // Overlay Scope
             Button! {
+                id = "overlay-scope-btn";
                 child = Text!("Overlay Scope");
                 on_click = hn!(|_| {
                     LAYERS.insert(LayerIndex::TOP_MOST, overlay(window_enabled.clone()));
@@ -169,6 +170,7 @@ fn overlay(window_enabled: ArcVar<bool>) -> impl UiNode {
     Container! {
         id = "overlay";
         widget::modal = true;
+        return_focus_on_deinit = true;
         child_align = Align::CENTER;
         child = Container! {
             focus_scope = true;
diff --git a/examples/icon/src/main.rs b/examples/icon/src/main.rs
index a5ee8575c..c33e37f7a 100644
--- a/examples/icon/src/main.rs
+++ b/examples/icon/src/main.rs
@@ -196,6 +196,7 @@ fn expanded_icon(name: &'static str, ico: icon::GlyphIcon, font_mod: &'static st
 
         id = "expanded-icon";
         widget::modal = true;
+        zng::focus::return_focus_on_deinit = true;
         backdrop_blur = 2;
         background_color = light_dark(colors::BLACK.with_alpha(10.pct()), colors::WHITE.with_alpha(10.pct()));
         child_align = Align::CENTER;
diff --git a/examples/layer/src/main.rs b/examples/layer/src/main.rs
index d99db3c97..a5f035883 100644
--- a/examples/layer/src/main.rs
+++ b/examples/layer/src/main.rs
@@ -62,6 +62,7 @@ fn overlay(id: impl Into<WidgetId>, offset: i32) -> impl UiNode {
     Container! {
         id;
         widget::modal = true;
+        zng::focus::return_focus_on_deinit = true;
         background_color = light_dark(colors::BLACK.with_alpha(10.pct()), colors::WHITE.with_alpha(10.pct()));
         child_align = Align::CENTER;
         child = Container! {
diff --git a/examples/window/src/main.rs b/examples/window/src/main.rs
index f171a4a3b..66015803d 100644
--- a/examples/window/src/main.rs
+++ b/examples/window/src/main.rs
@@ -721,6 +721,7 @@ fn close_dialog(mut windows: Vec<WindowId>, state: ArcVar<CloseState>) -> impl U
 
         id = "close-dialog";
         widget::modal = true;
+        zng::focus::return_focus_on_deinit = true;
         backdrop_blur = 2;
         background_color = light_dark(colors::BLACK.with_alpha(10.pct()), colors::WHITE.with_alpha(10.pct()));
         child_align = Align::CENTER;


Test patch: