From da19370d7a450da246e94660e4a52bab70af69cb Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Thu, 23 Nov 2023 16:44:40 +0000
Subject: [PATCH 01/11] Add note not to directly implement internal widget
 methods

---
 crates/kas-core/src/core/widget.rs | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/crates/kas-core/src/core/widget.rs b/crates/kas-core/src/core/widget.rs
index 3220da99d..977168c5b 100644
--- a/crates/kas-core/src/core/widget.rs
+++ b/crates/kas-core/src/core/widget.rs
@@ -383,11 +383,15 @@ pub trait Widget: Layout {
     }
 
     /// Internal method: configure recursively
+    ///
+    /// Do not implement this method directly!
     #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
     #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
     fn _configure(&mut self, cx: &mut ConfigCx, data: &Self::Data, id: Id);
 
     /// Internal method: update recursively
+    ///
+    /// Do not implement this method directly!
     #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
     #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
     fn _update(&mut self, cx: &mut ConfigCx, data: &Self::Data);
@@ -397,6 +401,8 @@ pub trait Widget: Layout {
     /// If `disabled`, widget `id` does not receive the `event`. Widget `id` is
     /// the first disabled widget (may be an ancestor of the original target);
     /// ancestors of `id` are not disabled.
+    ///
+    /// Do not implement this method directly!
     #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
     #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
     fn _send(
@@ -412,6 +418,8 @@ pub trait Widget: Layout {
     ///
     /// Behaves as if an event had been sent to `id`, then the widget had pushed
     /// `msg` to the message stack. Widget `id` or any ancestor may handle.
+    ///
+    /// Do not implement this method directly!
     #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
     #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
     fn _replay(&mut self, cx: &mut EventCx, data: &Self::Data, id: Id, msg: Erased);
@@ -419,6 +427,8 @@ pub trait Widget: Layout {
     /// Internal method: search for the previous/next navigation target
     ///
     /// `focus`: the current focus or starting point.
+    ///
+    /// Do not implement this method directly!
     #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
     #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
     fn _nav_next(

From 4f74808eaf26ff7a6fea62888b9e672a59a15bc8 Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Thu, 23 Nov 2023 19:50:39 +0000
Subject: [PATCH 02/11] Rename PAYLOAD_ -> TIMER_

---
 crates/kas-core/src/event/components.rs | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs
index bb87ca320..849e2b482 100644
--- a/crates/kas-core/src/event/components.rs
+++ b/crates/kas-core/src/event/components.rs
@@ -15,8 +15,8 @@ use crate::{Action, Id};
 use kas_macros::impl_default;
 use std::time::{Duration, Instant};
 
-const PAYLOAD_SELECT: u64 = 1 << 60;
-const PAYLOAD_GLIDE: u64 = (1 << 60) + 1;
+const TIMER_SELECT: u64 = 1 << 60;
+const TIMER_GLIDE: u64 = (1 << 60) + 1;
 const GLIDE_POLL_MS: u64 = 3;
 const GLIDE_MAX_SAMPLES: usize = 8;
 
@@ -326,10 +326,10 @@ impl ScrollComponent {
                 let timeout = cx.config().scroll_flick_timeout();
                 let pan_dist_thresh = cx.config().pan_dist_thresh();
                 if self.glide.press_end(timeout, pan_dist_thresh) {
-                    cx.request_timer_update(id.clone(), PAYLOAD_GLIDE, Duration::new(0, 0), true);
+                    cx.request_timer_update(id.clone(), TIMER_GLIDE, Duration::new(0, 0), true);
                 }
             }
-            Event::TimerUpdate(pl) if pl == PAYLOAD_GLIDE => {
+            Event::TimerUpdate(pl) if pl == TIMER_GLIDE => {
                 // Momentum/glide scrolling: update per arbitrary step time until movment stops.
                 let timeout = cx.config().scroll_flick_timeout();
                 let decay = cx.config().scroll_flick_decay();
@@ -338,7 +338,7 @@ impl ScrollComponent {
 
                     if self.glide.vel != Vec2::ZERO {
                         let dur = Duration::from_millis(GLIDE_POLL_MS);
-                        cx.request_timer_update(id.clone(), PAYLOAD_GLIDE, dur, true);
+                        cx.request_timer_update(id.clone(), TIMER_GLIDE, dur, true);
                         cx.set_scroll(Scroll::Scrolled);
                     }
                 }
@@ -416,7 +416,7 @@ impl TextInput {
                     PressSource::Touch(touch_id) => {
                         self.touch_phase = TouchPhase::Start(touch_id, press.coord);
                         let delay = cx.config().touch_select_delay();
-                        cx.request_timer_update(w_id.clone(), PAYLOAD_SELECT, delay, false);
+                        cx.request_timer_update(w_id.clone(), TIMER_SELECT, delay, false);
                         None
                     }
                     PressSource::Mouse(..) if cx.config_enable_mouse_text_pan() => {
@@ -474,11 +474,11 @@ impl TextInput {
                         || matches!(press.source, PressSource::Mouse(..) if cx.config_enable_mouse_text_pan()))
                 {
                     self.touch_phase = TouchPhase::None;
-                    cx.request_timer_update(w_id, PAYLOAD_GLIDE, Duration::new(0, 0), true);
+                    cx.request_timer_update(w_id, TIMER_GLIDE, Duration::new(0, 0), true);
                 }
                 Action::None
             }
-            Event::TimerUpdate(pl) if pl == PAYLOAD_SELECT => {
+            Event::TimerUpdate(pl) if pl == TIMER_SELECT => {
                 match self.touch_phase {
                     TouchPhase::Start(touch_id, coord) => {
                         self.touch_phase = TouchPhase::Cursor(touch_id);
@@ -493,13 +493,13 @@ impl TextInput {
                     _ => Action::None,
                 }
             }
-            Event::TimerUpdate(pl) if pl == PAYLOAD_GLIDE => {
+            Event::TimerUpdate(pl) if pl == TIMER_GLIDE => {
                 // Momentum/glide scrolling: update per arbitrary step time until movment stops.
                 let timeout = cx.config().scroll_flick_timeout();
                 let decay = cx.config().scroll_flick_decay();
                 if let Some(delta) = self.glide.step(timeout, decay) {
                     let dur = Duration::from_millis(GLIDE_POLL_MS);
-                    cx.request_timer_update(w_id, PAYLOAD_GLIDE, dur, true);
+                    cx.request_timer_update(w_id, TIMER_GLIDE, dur, true);
                     Action::Pan(delta)
                 } else {
                     Action::None

From 0d8983838a18e1bd32cac41ebce0350708f52b3d Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Thu, 23 Nov 2023 19:54:46 +0000
Subject: [PATCH 03/11] Remove `first` parameter of request_timer_update

---
 crates/kas-core/src/event/components.rs | 10 +++++-----
 crates/kas-core/src/event/cx/cx_pub.rs  |  8 ++++----
 crates/kas-widgets/src/menu/menubar.rs  |  2 +-
 crates/kas-widgets/src/scroll_bar.rs    |  2 +-
 examples/clock.rs                       |  4 ++--
 examples/stopwatch.rs                   |  4 ++--
 6 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs
index 849e2b482..4325ae863 100644
--- a/crates/kas-core/src/event/components.rs
+++ b/crates/kas-core/src/event/components.rs
@@ -326,7 +326,7 @@ impl ScrollComponent {
                 let timeout = cx.config().scroll_flick_timeout();
                 let pan_dist_thresh = cx.config().pan_dist_thresh();
                 if self.glide.press_end(timeout, pan_dist_thresh) {
-                    cx.request_timer_update(id.clone(), TIMER_GLIDE, Duration::new(0, 0), true);
+                    cx.request_timer_update(id.clone(), TIMER_GLIDE, Duration::new(0, 0));
                 }
             }
             Event::TimerUpdate(pl) if pl == TIMER_GLIDE => {
@@ -338,7 +338,7 @@ impl ScrollComponent {
 
                     if self.glide.vel != Vec2::ZERO {
                         let dur = Duration::from_millis(GLIDE_POLL_MS);
-                        cx.request_timer_update(id.clone(), TIMER_GLIDE, dur, true);
+                        cx.request_timer_update(id.clone(), TIMER_GLIDE, dur);
                         cx.set_scroll(Scroll::Scrolled);
                     }
                 }
@@ -416,7 +416,7 @@ impl TextInput {
                     PressSource::Touch(touch_id) => {
                         self.touch_phase = TouchPhase::Start(touch_id, press.coord);
                         let delay = cx.config().touch_select_delay();
-                        cx.request_timer_update(w_id.clone(), TIMER_SELECT, delay, false);
+                        cx.request_timer_update(w_id.clone(), TIMER_SELECT, delay);
                         None
                     }
                     PressSource::Mouse(..) if cx.config_enable_mouse_text_pan() => {
@@ -474,7 +474,7 @@ impl TextInput {
                         || matches!(press.source, PressSource::Mouse(..) if cx.config_enable_mouse_text_pan()))
                 {
                     self.touch_phase = TouchPhase::None;
-                    cx.request_timer_update(w_id, TIMER_GLIDE, Duration::new(0, 0), true);
+                    cx.request_timer_update(w_id, TIMER_GLIDE, Duration::new(0, 0));
                 }
                 Action::None
             }
@@ -499,7 +499,7 @@ impl TextInput {
                 let decay = cx.config().scroll_flick_decay();
                 if let Some(delta) = self.glide.step(timeout, decay) {
                     let dur = Duration::from_millis(GLIDE_POLL_MS);
-                    cx.request_timer_update(w_id, TIMER_GLIDE, dur, true);
+                    cx.request_timer_update(w_id, TIMER_GLIDE, dur);
                     Action::Pan(delta)
                 } else {
                     Action::None
diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs
index 5f2f9b633..6f4a105a9 100644
--- a/crates/kas-core/src/event/cx/cx_pub.rs
+++ b/crates/kas-core/src/event/cx/cx_pub.rs
@@ -200,16 +200,16 @@ impl EventState {
     /// Requesting an update with `delay == 0` is valid, except from an
     /// [`Event::TimerUpdate`] handler (where it may cause an infinite loop).
     ///
-    /// If multiple updates with the same `id` and `payload` are requested,
-    /// these are merged (using the earliest time if `first` is true).
-    pub fn request_timer_update(&mut self, id: Id, payload: u64, delay: Duration, first: bool) {
+    /// Multiple timer requests with the same `id` and `payload` are merged
+    /// (choosing the earliest time).
+    pub fn request_timer_update(&mut self, id: Id, payload: u64, delay: Duration) {
         let time = Instant::now() + delay;
         if let Some(row) = self
             .time_updates
             .iter_mut()
             .find(|row| row.1 == id && row.2 == payload)
         {
-            if (first && row.0 <= time) || (!first && row.0 >= time) {
+            if row.0 <= time {
                 return;
             }
 
diff --git a/crates/kas-widgets/src/menu/menubar.rs b/crates/kas-widgets/src/menu/menubar.rs
index a6cef08a3..fa2efe33e 100644
--- a/crates/kas-widgets/src/menu/menubar.rs
+++ b/crates/kas-widgets/src/menu/menubar.rs
@@ -193,7 +193,7 @@ impl_scope! {
                         } else if id != self.delayed_open {
                             cx.set_nav_focus(id.clone(), FocusSource::Pointer);
                             let delay = cx.config().menu_delay();
-                            cx.request_timer_update(self.id(), id.as_u64(), delay, true);
+                            cx.request_timer_update(self.id(), id.as_u64(), delay);
                             self.delayed_open = Some(id);
                         }
                     } else {
diff --git a/crates/kas-widgets/src/scroll_bar.rs b/crates/kas-widgets/src/scroll_bar.rs
index 473833c11..b32efeae4 100644
--- a/crates/kas-widgets/src/scroll_bar.rs
+++ b/crates/kas-widgets/src/scroll_bar.rs
@@ -203,7 +203,7 @@ impl_scope! {
         fn force_visible(&mut self, cx: &mut EventState) {
             self.force_visible = true;
             let delay = cx.config().touch_select_delay();
-            cx.request_timer_update(self.id(), 0, delay, false);
+            cx.request_timer_update(self.id(), 0, delay);
         }
 
         #[inline]
diff --git a/examples/clock.rs b/examples/clock.rs
index 29534958a..8341c74b2 100644
--- a/examples/clock.rs
+++ b/examples/clock.rs
@@ -125,7 +125,7 @@ impl_scope! {
         type Data = ();
 
         fn configure(&mut self, cx: &mut ConfigCx) {
-            cx.request_timer_update(self.id(), 0, Duration::new(0, 0), true);
+            cx.request_timer_update(self.id(), 0, Duration::new(0, 0));
         }
 
         fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
@@ -142,7 +142,7 @@ impl_scope! {
                         .expect("invalid font_id");
                     let ns = 1_000_000_000 - (self.now.time().nanosecond() % 1_000_000_000);
                     log::info!("Requesting update in {}ns", ns);
-                    cx.request_timer_update(self.id(), 0, Duration::new(0, ns), true);
+                    cx.request_timer_update(self.id(), 0, Duration::new(0, ns));
                     cx.redraw(self);
                     Used
                 }
diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs
index 394381f2b..344362055 100644
--- a/examples/stopwatch.rs
+++ b/examples/stopwatch.rs
@@ -46,7 +46,7 @@ fn make_window() -> Box<dyn kas::Widget<Data = ()>> {
                             self.elapsed += now - last;
                             self.last = Some(now);
                             cx.update(self.as_node(data));
-                            cx.request_timer_update(self.id(), 0, Duration::new(0, 1), true);
+                            cx.request_timer_update(self.id(), 0, Duration::new(0, 1));
                         }
                         Used
                     }
@@ -64,7 +64,7 @@ fn make_window() -> Box<dyn kas::Widget<Data = ()>> {
                         self.elapsed += now - last;
                     } else {
                         self.last = Some(now);
-                        cx.request_timer_update(self.id(), 0, Duration::new(0, 0), true);
+                        cx.request_timer_update(self.id(), 0, Duration::new(0, 0));
                     }
                 }
             }

From e5314ca31b79eb8957d14d0c18aade1b931f8f2c Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Thu, 23 Nov 2023 20:00:38 +0000
Subject: [PATCH 04/11] Rename timer_update -> timer

---
 crates/kas-core/src/event/components.rs  | 39 +++++++++++-------------
 crates/kas-core/src/event/cx/cx_pub.rs   | 10 +++---
 crates/kas-core/src/event/cx/cx_shell.rs |  2 +-
 crates/kas-core/src/event/events.rs      | 10 +++---
 crates/kas-widgets/src/menu/menubar.rs   |  4 +--
 crates/kas-widgets/src/scroll_bar.rs     |  4 +--
 examples/clock.rs                        |  8 ++---
 examples/stopwatch.rs                    |  6 ++--
 8 files changed, 39 insertions(+), 44 deletions(-)

diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs
index 4325ae863..874313835 100644
--- a/crates/kas-core/src/event/components.rs
+++ b/crates/kas-core/src/event/components.rs
@@ -249,7 +249,7 @@ impl ScrollComponent {
     /// Use an event to scroll, if possible
     ///
     /// Consumes the following events: `Command`, `Scroll`, `PressStart`,
-    /// `PressMove`, `PressEnd`, `TimerUpdate(pl)` where `pl == (1<<60) + 1`.
+    /// `PressMove`, `PressEnd`, `Timer(pl)` where `pl == (1<<60) + 1`.
     /// May request timer updates.
     ///
     /// Implements scroll by Home/End, Page Up/Down and arrow keys, by mouse
@@ -326,10 +326,10 @@ impl ScrollComponent {
                 let timeout = cx.config().scroll_flick_timeout();
                 let pan_dist_thresh = cx.config().pan_dist_thresh();
                 if self.glide.press_end(timeout, pan_dist_thresh) {
-                    cx.request_timer_update(id.clone(), TIMER_GLIDE, Duration::new(0, 0));
+                    cx.request_timer(id.clone(), TIMER_GLIDE, Duration::new(0, 0));
                 }
             }
-            Event::TimerUpdate(pl) if pl == TIMER_GLIDE => {
+            Event::Timer(pl) if pl == TIMER_GLIDE => {
                 // Momentum/glide scrolling: update per arbitrary step time until movment stops.
                 let timeout = cx.config().scroll_flick_timeout();
                 let decay = cx.config().scroll_flick_decay();
@@ -338,7 +338,7 @@ impl ScrollComponent {
 
                     if self.glide.vel != Vec2::ZERO {
                         let dur = Duration::from_millis(GLIDE_POLL_MS);
-                        cx.request_timer_update(id.clone(), TIMER_GLIDE, dur);
+                        cx.request_timer(id.clone(), TIMER_GLIDE, dur);
                         cx.set_scroll(Scroll::Scrolled);
                     }
                 }
@@ -399,7 +399,7 @@ impl TextInput {
     /// Handle input events
     ///
     /// Consumes the following events: `PressStart`, `PressMove`, `PressEnd`,
-    /// `TimerUpdate(pl)` where `pl == 1<<60 || pl == (1<<60)+1`.
+    /// `Timer(pl)` where `pl == 1<<60 || pl == (1<<60)+1`.
     /// May request press grabs and timer updates.
     ///
     /// Implements scrolling and text selection behaviour, excluding handling of
@@ -416,7 +416,7 @@ impl TextInput {
                     PressSource::Touch(touch_id) => {
                         self.touch_phase = TouchPhase::Start(touch_id, press.coord);
                         let delay = cx.config().touch_select_delay();
-                        cx.request_timer_update(w_id.clone(), TIMER_SELECT, delay);
+                        cx.request_timer(w_id.clone(), TIMER_SELECT, delay);
                         None
                     }
                     PressSource::Mouse(..) if cx.config_enable_mouse_text_pan() => {
@@ -474,32 +474,27 @@ impl TextInput {
                         || matches!(press.source, PressSource::Mouse(..) if cx.config_enable_mouse_text_pan()))
                 {
                     self.touch_phase = TouchPhase::None;
-                    cx.request_timer_update(w_id, TIMER_GLIDE, Duration::new(0, 0));
+                    cx.request_timer(w_id, TIMER_GLIDE, Duration::new(0, 0));
                 }
                 Action::None
             }
-            Event::TimerUpdate(pl) if pl == TIMER_SELECT => {
-                match self.touch_phase {
-                    TouchPhase::Start(touch_id, coord) => {
-                        self.touch_phase = TouchPhase::Cursor(touch_id);
-                        Action::Focus {
-                            coord: Some(coord),
-                            action: SelectionAction::new(true, !cx.modifiers().shift_key(), 1),
-                        }
+            Event::Timer(pl) if pl == TIMER_SELECT => match self.touch_phase {
+                TouchPhase::Start(touch_id, coord) => {
+                    self.touch_phase = TouchPhase::Cursor(touch_id);
+                    Action::Focus {
+                        coord: Some(coord),
+                        action: SelectionAction::new(true, !cx.modifiers().shift_key(), 1),
                     }
-                    // Note: if the TimerUpdate were from another requester it
-                    // should technically be Unused, but it doesn't matter
-                    // so long as other consumers match this first.
-                    _ => Action::None,
                 }
-            }
-            Event::TimerUpdate(pl) if pl == TIMER_GLIDE => {
+                _ => Action::None,
+            },
+            Event::Timer(pl) if pl == TIMER_GLIDE => {
                 // Momentum/glide scrolling: update per arbitrary step time until movment stops.
                 let timeout = cx.config().scroll_flick_timeout();
                 let decay = cx.config().scroll_flick_decay();
                 if let Some(delta) = self.glide.step(timeout, decay) {
                     let dur = Duration::from_millis(GLIDE_POLL_MS);
-                    cx.request_timer_update(w_id, TIMER_GLIDE, dur);
+                    cx.request_timer(w_id, TIMER_GLIDE, dur);
                     Action::Pan(delta)
                 } else {
                     Action::None
diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs
index 6f4a105a9..36b046137 100644
--- a/crates/kas-core/src/event/cx/cx_pub.rs
+++ b/crates/kas-core/src/event/cx/cx_pub.rs
@@ -188,21 +188,21 @@ impl EventState {
         }
     }
 
-    /// Schedule an update
+    /// Schedule a timed update
     ///
     /// Widget updates may be used for animation and timed responses. See also
     /// [`Draw::animate`](crate::draw::Draw::animate) for animation.
     ///
-    /// Widget `id` will receive [`Event::TimerUpdate`] with this `payload` at
+    /// Widget `id` will receive [`Event::Timer`] with this `payload` at
     /// approximately `time = now + delay` (or possibly a little later due to
     /// frame-rate limiters and processing time).
     ///
     /// Requesting an update with `delay == 0` is valid, except from an
-    /// [`Event::TimerUpdate`] handler (where it may cause an infinite loop).
+    /// [`Event::Timer`] handler (where it may cause an infinite loop).
     ///
     /// Multiple timer requests with the same `id` and `payload` are merged
     /// (choosing the earliest time).
-    pub fn request_timer_update(&mut self, id: Id, payload: u64, delay: Duration) {
+    pub fn request_timer(&mut self, id: Id, payload: u64, delay: Duration) {
         let time = Instant::now() + delay;
         if let Some(row) = self
             .time_updates
@@ -216,7 +216,7 @@ impl EventState {
             row.0 = time;
             log::trace!(
                 target: "kas_core::event",
-                "request_timer_update: update {id} at now+{}ms",
+                "request_timer: update {id} at now+{}ms",
                 delay.as_millis()
             );
         } else {
diff --git a/crates/kas-core/src/event/cx/cx_shell.rs b/crates/kas-core/src/event/cx/cx_shell.rs
index 60948aee1..cfc0e81cb 100644
--- a/crates/kas-core/src/event/cx/cx_shell.rs
+++ b/crates/kas-core/src/event/cx/cx_shell.rs
@@ -268,7 +268,7 @@ impl<'a> EventCx<'a> {
             }
 
             let update = self.time_updates.pop().unwrap();
-            self.send_event(widget.re(), update.1, Event::TimerUpdate(update.2));
+            self.send_event(widget.re(), update.1, Event::Timer(update.2));
         }
 
         self.time_updates.sort_by(|a, b| b.0.cmp(&a.0)); // reverse sort
diff --git a/crates/kas-core/src/event/events.rs b/crates/kas-core/src/event/events.rs
index fc13bbfe6..02d4eb31d 100644
--- a/crates/kas-core/src/event/events.rs
+++ b/crates/kas-core/src/event/events.rs
@@ -173,10 +173,10 @@ pub enum Event {
     /// Update from a timer
     ///
     /// This event is received after requesting timed wake-up(s)
-    /// (see [`EventState::request_timer_update`]).
+    /// (see [`EventState::request_timer`]).
     ///
-    /// The `u64` payload is copied from [`EventState::request_timer_update`].
-    TimerUpdate(u64),
+    /// The `u64` payload is copied from [`EventState::request_timer`].
+    Timer(u64),
     /// Notification that a popup has been closed
     ///
     /// This is sent to the popup when closed.
@@ -302,7 +302,7 @@ impl Event {
             Command(_, _) => false,
             Key(_, _) | Scroll(_) | Pan { .. } => false,
             CursorMove { .. } | PressStart { .. } | PressMove { .. } | PressEnd { .. } => false,
-            TimerUpdate(_) | PopupClosed(_) => true,
+            Timer(_) | PopupClosed(_) => true,
             NavFocus { .. } | SelFocus(_) | KeyFocus | MouseHover(_) => false,
             LostNavFocus | LostKeyFocus | LostSelFocus => true,
         }
@@ -323,7 +323,7 @@ impl Event {
             Command(_, _) | Scroll(_) | Pan { .. } => true,
             CursorMove { .. } | PressStart { .. } => true,
             PressMove { .. } | PressEnd { .. } => false,
-            TimerUpdate(_) | PopupClosed(_) => false,
+            Timer(_) | PopupClosed(_) => false,
             NavFocus { .. } | LostNavFocus => false,
             SelFocus(_) | LostSelFocus => false,
             KeyFocus | LostKeyFocus => false,
diff --git a/crates/kas-widgets/src/menu/menubar.rs b/crates/kas-widgets/src/menu/menubar.rs
index fa2efe33e..c9419310e 100644
--- a/crates/kas-widgets/src/menu/menubar.rs
+++ b/crates/kas-widgets/src/menu/menubar.rs
@@ -132,7 +132,7 @@ impl_scope! {
     impl<Data, D: Directional> Events for MenuBar<Data, D> {
         fn handle_event(&mut self, cx: &mut EventCx, data: &Data, event: Event) -> IsUsed {
             match event {
-                Event::TimerUpdate(id_code) => {
+                Event::Timer(id_code) => {
                     if let Some(id) = self.delayed_open.clone() {
                         if id.as_u64() == id_code {
                             self.set_menu_path(cx, data, Some(&id), false);
@@ -193,7 +193,7 @@ impl_scope! {
                         } else if id != self.delayed_open {
                             cx.set_nav_focus(id.clone(), FocusSource::Pointer);
                             let delay = cx.config().menu_delay();
-                            cx.request_timer_update(self.id(), id.as_u64(), delay);
+                            cx.request_timer(self.id(), id.as_u64(), delay);
                             self.delayed_open = Some(id);
                         }
                     } else {
diff --git a/crates/kas-widgets/src/scroll_bar.rs b/crates/kas-widgets/src/scroll_bar.rs
index b32efeae4..e1fa07e96 100644
--- a/crates/kas-widgets/src/scroll_bar.rs
+++ b/crates/kas-widgets/src/scroll_bar.rs
@@ -203,7 +203,7 @@ impl_scope! {
         fn force_visible(&mut self, cx: &mut EventState) {
             self.force_visible = true;
             let delay = cx.config().touch_select_delay();
-            cx.request_timer_update(self.id(), 0, delay);
+            cx.request_timer(self.id(), 0, delay);
         }
 
         #[inline]
@@ -325,7 +325,7 @@ impl_scope! {
 
         fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
             match event {
-                Event::TimerUpdate(_) => {
+                Event::Timer(_) => {
                     self.force_visible = false;
                     cx.redraw(self);
                     Used
diff --git a/examples/clock.rs b/examples/clock.rs
index 8341c74b2..e678b2842 100644
--- a/examples/clock.rs
+++ b/examples/clock.rs
@@ -8,7 +8,7 @@
 //! Demonstrates low-level drawing and timer handling.
 //!
 //! Note that two forms of animation are possible: calling `draw.draw_device().animate();`
-//! in `fn Clock::draw`, or using `Event::TimerUpdate`. We use the latter since
+//! in `fn Clock::draw`, or using `Event::Timer`. We use the latter since
 //! it lets us draw at 1 FPS with exactly the right frame time.
 
 extern crate chrono;
@@ -125,12 +125,12 @@ impl_scope! {
         type Data = ();
 
         fn configure(&mut self, cx: &mut ConfigCx) {
-            cx.request_timer_update(self.id(), 0, Duration::new(0, 0));
+            cx.request_timer(self.id(), 0, Duration::new(0, 0));
         }
 
         fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
             match event {
-                Event::TimerUpdate(0) => {
+                Event::Timer(0) => {
                     self.now = Local::now();
                     let date = self.now.format("%Y-%m-%d").to_string();
                     let time = self.now.format("%H:%M:%S").to_string();
@@ -142,7 +142,7 @@ impl_scope! {
                         .expect("invalid font_id");
                     let ns = 1_000_000_000 - (self.now.time().nanosecond() % 1_000_000_000);
                     log::info!("Requesting update in {}ns", ns);
-                    cx.request_timer_update(self.id(), 0, Duration::new(0, ns));
+                    cx.request_timer(self.id(), 0, Duration::new(0, ns));
                     cx.redraw(self);
                     Used
                 }
diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs
index 344362055..4f51a1972 100644
--- a/examples/stopwatch.rs
+++ b/examples/stopwatch.rs
@@ -40,13 +40,13 @@ fn make_window() -> Box<dyn kas::Widget<Data = ()>> {
             }
             fn handle_event(&mut self, cx: &mut EventCx, data: &(), event: Event) -> IsUsed {
                 match event {
-                    Event::TimerUpdate(0) => {
+                    Event::Timer(0) => {
                         if let Some(last) = self.last {
                             let now = Instant::now();
                             self.elapsed += now - last;
                             self.last = Some(now);
                             cx.update(self.as_node(data));
-                            cx.request_timer_update(self.id(), 0, Duration::new(0, 1));
+                            cx.request_timer(self.id(), 0, Duration::new(0, 1));
                         }
                         Used
                     }
@@ -64,7 +64,7 @@ fn make_window() -> Box<dyn kas::Widget<Data = ()>> {
                         self.elapsed += now - last;
                     } else {
                         self.last = Some(now);
-                        cx.request_timer_update(self.id(), 0, Duration::new(0, 0));
+                        cx.request_timer(self.id(), 0, Duration::new(0, 0));
                     }
                 }
             }

From c6e761279c443e557b211ced0f65fbbc2e3d3833 Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Thu, 30 Nov 2023 08:39:02 +0000
Subject: [PATCH 05/11] Add AdaptConfigCx, AdaptEventCx

---
 crates/kas-widgets/Cargo.toml         |   1 +
 crates/kas-widgets/src/adapt/adapt.rs | 197 +++++++++++++++++++++-----
 crates/kas-widgets/src/adapt/mod.rs   |   2 +-
 examples/calculator.rs                |   4 +-
 4 files changed, 167 insertions(+), 37 deletions(-)

diff --git a/crates/kas-widgets/Cargo.toml b/crates/kas-widgets/Cargo.toml
index 3003034ec..e2fe243f6 100644
--- a/crates/kas-widgets/Cargo.toml
+++ b/crates/kas-widgets/Cargo.toml
@@ -29,6 +29,7 @@ unicode-segmentation = "1.7"
 thiserror = "1.0.23"
 image = { version = "0.24.1", optional = true }
 kas-macros = { version = "0.14.0-alpha", path = "../kas-macros" }
+linear-map = "1.2.0"
 
 # We must rename this package since macros expect kas to be in scope:
 kas = { version = "0.14.0-alpha", package = "kas-core", path = "../kas-core" }
diff --git a/crates/kas-widgets/src/adapt/adapt.rs b/crates/kas-widgets/src/adapt/adapt.rs
index b573276af..d8b9259e8 100644
--- a/crates/kas-widgets/src/adapt/adapt.rs
+++ b/crates/kas-widgets/src/adapt/adapt.rs
@@ -6,8 +6,111 @@
 //! Adapt widget
 
 use kas::prelude::*;
+use linear_map::LinearMap;
 use std::fmt::Debug;
 use std::marker::PhantomData;
+use std::time::Duration;
+
+/// An [`EventCx`] with embedded [`Id`]
+///
+/// NOTE: this is a temporary design: it may be expanded or integrated with
+/// `EventCx` in the future.
+#[autoimpl(Deref, DerefMut using self.cx)]
+pub struct AdaptEventCx<'a: 'b, 'b> {
+    cx: &'b mut EventCx<'a>,
+    id: Id,
+}
+
+impl<'a: 'b, 'b> AdaptEventCx<'a, 'b> {
+    #[inline]
+    fn new(cx: &'b mut EventCx<'a>, id: Id) -> Self {
+        AdaptEventCx { cx, id }
+    }
+
+    /// Check whether this widget is disabled
+    #[inline]
+    pub fn is_disabled(&self) -> bool {
+        self.cx.is_disabled(&self.id)
+    }
+
+    /// Set/unset disabled status for this widget
+    #[inline]
+    pub fn set_disabled(&mut self, state: bool) {
+        self.cx.set_disabled(self.id.clone(), state);
+    }
+
+    /// Schedule a timed update
+    ///
+    /// This widget will receive an update for timer `timer_id` at
+    /// approximately `time = now + delay` (or possibly a little later due to
+    /// frame-rate limiters and processing time).
+    ///
+    /// Requesting an update with `delay == 0` is valid except from a timer
+    /// handler where it might cause an infinite loop.
+    ///
+    /// Multiple timer requests with the same `timer_id` are merged
+    /// (choosing the earliest time).
+    #[inline]
+    pub fn request_timer(&mut self, timer_id: u64, delay: Duration) {
+        self.cx.request_timer(self.id.clone(), timer_id, delay);
+    }
+}
+
+/// A [`ConfigCx`] with embedded [`Id`]
+///
+/// NOTE: this is a temporary design: it may be expanded or integrated with
+/// `ConfigCx` in the future.
+#[autoimpl(Deref, DerefMut using self.cx)]
+pub struct AdaptConfigCx<'a: 'b, 'b> {
+    cx: &'b mut ConfigCx<'a>,
+    id: Id,
+}
+
+impl<'a: 'b, 'b> AdaptConfigCx<'a, 'b> {
+    #[inline]
+    fn new(cx: &'b mut ConfigCx<'a>, id: Id) -> Self {
+        AdaptConfigCx { cx, id }
+    }
+
+    /// Check whether this widget is disabled
+    #[inline]
+    pub fn is_disabled(&self) -> bool {
+        self.cx.is_disabled(&self.id)
+    }
+
+    /// Set/unset disabled status for this widget
+    #[inline]
+    pub fn set_disabled(&mut self, state: bool) {
+        self.cx.set_disabled(self.id.clone(), state);
+    }
+
+    /// Enable `alt_bypass` for layer
+    ///
+    /// This may be called by a child widget during configure to enable or
+    /// disable alt-bypass for the access-key layer containing its access keys.
+    /// This allows access keys to be used as shortcuts without the Alt
+    /// key held. See also [`EventState::new_access_layer`].
+    #[inline]
+    pub fn enable_alt_bypass(&mut self, alt_bypass: bool) {
+        self.cx.enable_alt_bypass(&self.id, alt_bypass);
+    }
+
+    /// Schedule a timed update
+    ///
+    /// This widget will receive an update for timer `timer_id` at
+    /// approximately `time = now + delay` (or possibly a little later due to
+    /// frame-rate limiters and processing time).
+    ///
+    /// Requesting an update with `delay == 0` is valid except from a timer
+    /// handler where it might cause an infinite loop.
+    ///
+    /// Multiple timer requests with the same `timer_id` are merged
+    /// (choosing the earliest time).
+    #[inline]
+    pub fn request_timer(&mut self, timer_id: u64, delay: Duration) {
+        self.cx.request_timer(self.id.clone(), timer_id, delay);
+    }
+}
 
 impl_scope! {
     /// Data adaption node
@@ -25,9 +128,10 @@ impl_scope! {
         state: S,
         #[widget(&self.state)]
         inner: W,
-        event_handler: Option<Box<dyn Fn(&mut EventCx, &A, &mut S, Event) -> IsUsed>>,
-        message_handlers: Vec<Box<dyn Fn(&mut EventCx, &A, &mut S) -> bool>>,
-        update_handler: Option<Box<dyn Fn(&mut ConfigCx, &A, &mut S)>>,
+        configure_handler: Option<Box<dyn Fn(&mut AdaptConfigCx, &mut S)>>,
+        update_handler: Option<Box<dyn Fn(&mut AdaptConfigCx, &A, &mut S)>>,
+        timer_handlers: LinearMap<u64, Box<dyn Fn(&mut AdaptEventCx, &A, &mut S) -> bool>>,
+        message_handlers: Vec<Box<dyn Fn(&mut AdaptEventCx, &A, &mut S) -> bool>>,
     }
 
     impl Self {
@@ -38,21 +142,44 @@ impl_scope! {
                 core: Default::default(),
                 state,
                 inner,
-                event_handler: None,
-                message_handlers: vec![],
+                configure_handler: None,
                 update_handler: None,
+                timer_handlers: LinearMap::new(),
+                message_handlers: vec![],
             }
         }
 
-        /// Set a custom event handler
+        /// Add a handler to be called on configuration
+        pub fn on_configure<F>(mut self, handler: F) -> Self
+        where
+            F: Fn(&mut AdaptConfigCx, &mut S) + 'static,
+        {
+            debug_assert!(self.configure_handler.is_none());
+            self.configure_handler = Some(Box::new(handler));
+            self
+        }
+
+        /// Add a handler to be called on update of input data
+        ///
+        /// Children will be updated after the handler is called.
+        pub fn on_update<F>(mut self, handler: F) -> Self
+        where
+            F: Fn(&mut AdaptConfigCx, &A, &mut S) + 'static,
+        {
+            debug_assert!(self.update_handler.is_none());
+            self.update_handler = Some(Box::new(handler));
+            self
+        }
+
+        /// Set a timer handler
         ///
-        /// The closure should return [`Used`] if state was updated.
-        pub fn on_event<H>(mut self, handler: H) -> Self
+        /// The closure should return `true` if state was updated.
+        pub fn on_timer<H>(mut self, timer_id: u64, handler: H) -> Self
         where
-            H: Fn(&mut EventCx, &A, &mut S, Event) -> IsUsed + 'static,
+            H: Fn(&mut AdaptEventCx, &A, &mut S) -> bool + 'static,
         {
-            debug_assert!(self.event_handler.is_none());
-            self.event_handler = Some(Box::new(handler));
+            debug_assert!(self.timer_handlers.get(&timer_id).is_none());
+            self.timer_handlers.insert(timer_id, Box::new(handler));
             self
         }
 
@@ -61,7 +188,7 @@ impl_scope! {
         /// Children will be updated whenever this handler is invoked.
         ///
         /// Where multiple message types must be handled or access to the
-        /// [`EventCx`] is required, use [`Self::on_messages`] instead.
+        /// [`AdaptEventCx`] is required, use [`Self::on_messages`] instead.
         pub fn on_message<M, H>(self, handler: H) -> Self
         where
             M: Debug + 'static,
@@ -82,50 +209,52 @@ impl_scope! {
         /// The closure should return `true` if state was updated.
         pub fn on_messages<H>(mut self, handler: H) -> Self
         where
-            H: Fn(&mut EventCx, &A, &mut S) -> bool + 'static,
+            H: Fn(&mut AdaptEventCx, &A, &mut S) -> bool + 'static,
         {
             self.message_handlers.push(Box::new(handler));
             self
         }
-
-        /// Add a handler to be called on update of input data
-        ///
-        /// Children will be updated after the handler is called.
-        pub fn on_update<F>(mut self, update_handler: F) -> Self
-        where
-            F: Fn(&mut ConfigCx, &A, &mut S) + 'static,
-        {
-            debug_assert!(self.update_handler.is_none());
-            self.update_handler = Some(Box::new(update_handler));
-            self
-        }
     }
 
     impl Events for Self {
         type Data = A;
 
+        fn configure(&mut self, cx: &mut ConfigCx) {
+            if let Some(handler) = self.configure_handler.as_ref() {
+                let mut cx = AdaptConfigCx::new(cx, self.id());
+                handler(&mut cx, &mut self.state);
+            }
+        }
+
         fn update(&mut self, cx: &mut ConfigCx, data: &A) {
             if let Some(handler) = self.update_handler.as_ref() {
-                handler(cx, data, &mut self.state);
+                let mut cx = AdaptConfigCx::new(cx, self.id());
+                handler(&mut cx, data, &mut self.state);
             }
         }
 
         fn handle_event(&mut self, cx: &mut EventCx, data: &Self::Data, event: Event) -> IsUsed {
-            if let Some(handler) = self.event_handler.as_ref() {
-                let is_used = handler(cx, data, &mut self.state, event);
-                if is_used.into() {
-                    cx.update(self.as_node(data));
+            match event {
+                Event::Timer(timer_id) => {
+                    if let Some(handler) = self.timer_handlers.get(&timer_id) {
+                        let mut cx = AdaptEventCx::new(cx, self.id());
+                        if handler(&mut cx, data, &mut self.state) {
+                            cx.update(self.as_node(data));
+                        }
+                        Used
+                    } else {
+                        Unused
+                    }
                 }
-                is_used
-            } else {
-                Unused
+                _ => Unused,
             }
         }
 
         fn handle_messages(&mut self, cx: &mut EventCx, data: &A) {
             let mut update = false;
+            let mut cx = AdaptEventCx::new(cx, self.id());
             for handler in self.message_handlers.iter() {
-                update |= handler(cx, data, &mut self.state);
+                update |= handler(&mut cx, data, &mut self.state);
             }
             if update {
                 cx.update(self.as_node(data));
diff --git a/crates/kas-widgets/src/adapt/mod.rs b/crates/kas-widgets/src/adapt/mod.rs
index abd2caacb..912a86a47 100644
--- a/crates/kas-widgets/src/adapt/mod.rs
+++ b/crates/kas-widgets/src/adapt/mod.rs
@@ -11,7 +11,7 @@ mod adapt_widget;
 mod reserve;
 mod with_label;
 
-pub use adapt::{Adapt, Map};
+pub use adapt::{Adapt, AdaptConfigCx, AdaptEventCx, Map};
 pub use adapt_events::OnUpdate;
 pub use adapt_widget::*;
 #[doc(inline)] pub use kas::hidden::MapAny;
diff --git a/examples/calculator.rs b/examples/calculator.rs
index 0ceef22b5..cea34d0ea 100644
--- a/examples/calculator.rs
+++ b/examples/calculator.rs
@@ -59,11 +59,11 @@ fn calc_ui() -> Window<()> {
 
     let ui = Adapt::new(kas::column![display, buttons], Calculator::new())
         .on_message(|_, calc, key| calc.handle(key))
-        .on_configure(|cx, adapt| {
+        .on_configure(|cx, _| {
             cx.disable_nav_focus(true);
 
             // Enable key bindings without Alt held:
-            cx.enable_alt_bypass(adapt.id_ref(), true);
+            cx.enable_alt_bypass(true);
         });
 
     Window::new(ui, "Calculator")

From 578ebbc86cbcc3c620a1c83d2d4c314a7a1668a2 Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Thu, 30 Nov 2023 08:39:14 +0000
Subject: [PATCH 06/11] Remove DataKey bound on MatrixData::ColKey, RowKey

---
 crates/kas-view/src/data_traits.rs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/crates/kas-view/src/data_traits.rs b/crates/kas-view/src/data_traits.rs
index 7d3b9911e..0863e6e4e 100644
--- a/crates/kas-view/src/data_traits.rs
+++ b/crates/kas-view/src/data_traits.rs
@@ -173,9 +173,9 @@ pub trait ListData: SharedData {
 #[autoimpl(for<T: trait + ?Sized> &T, &mut T, std::rc::Rc<T>, std::sync::Arc<T>, Box<T>)]
 pub trait MatrixData: SharedData {
     /// Column key type
-    type ColKey: DataKey;
+    type ColKey;
     /// Row key type
-    type RowKey: DataKey;
+    type RowKey;
 
     type ColKeyIter<'b>: Iterator<Item = Self::ColKey>
     where

From 8546b5f4958718868806d177e89d46fe74ccffb2 Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Thu, 30 Nov 2023 12:30:28 +0000
Subject: [PATCH 07/11] Add EventCx::nav_next with quick fix for scroll != None

---
 crates/kas-core/src/event/cx/cx_shell.rs |  4 +-
 crates/kas-core/src/event/cx/mod.rs      | 48 ++++++++++++++++--------
 2 files changed, 34 insertions(+), 18 deletions(-)

diff --git a/crates/kas-core/src/event/cx/cx_shell.rs b/crates/kas-core/src/event/cx/cx_shell.rs
index cfc0e81cb..f8e294525 100644
--- a/crates/kas-core/src/event/cx/cx_shell.rs
+++ b/crates/kas-core/src/event/cx/cx_shell.rs
@@ -474,7 +474,7 @@ impl<'a> EventCx<'a> {
                         // No mouse grab but have a hover target
                         if self.config.mouse_nav_focus() {
                             if let Some(id) =
-                                win._nav_next(self, data, Some(&start_id), NavAdvance::None)
+                                self.nav_next(win.as_node(data), Some(&start_id), NavAdvance::None)
                             {
                                 self.set_nav_focus(id, FocusSource::Pointer);
                             }
@@ -502,7 +502,7 @@ impl<'a> EventCx<'a> {
                         if let Some(id) = start_id.as_ref() {
                             if self.config.touch_nav_focus() {
                                 if let Some(id) =
-                                    win._nav_next(self, data, Some(id), NavAdvance::None)
+                                    self.nav_next(win.as_node(data), Some(id), NavAdvance::None)
                                 {
                                     self.set_nav_focus(id, FocusSource::Pointer);
                                 }
diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs
index af61edfc5..7339456ad 100644
--- a/crates/kas-core/src/event/cx/mod.rs
+++ b/crates/kas-core/src/event/cx/mod.rs
@@ -454,7 +454,7 @@ impl<'a> EventCx<'a> {
         }
 
         if let Some(id) = target {
-            if let Some(id) = widget._nav_next(self, Some(&id), NavAdvance::None) {
+            if let Some(id) = self.nav_next(widget.re(), Some(&id), NavAdvance::None) {
                 self.set_nav_focus(id, FocusSource::Key);
             }
             let event = Event::Command(Command::Activate, Some(code));
@@ -516,18 +516,8 @@ impl<'a> EventCx<'a> {
         self.scroll = Scroll::None;
     }
 
-    // Wrapper around Self::send; returns true when event is used
-    #[inline]
-    fn send_event(&mut self, widget: Node<'_>, id: Id, event: Event) -> bool {
-        self.messages.set_base();
-        let used = self.send_event_impl(widget, id, event);
-        self.last_child = None;
-        self.scroll = Scroll::None;
-        used
-    }
-
-    // Send an event; possibly leave messages on the stack
-    fn send_event_impl(&mut self, mut widget: Node<'_>, mut id: Id, event: Event) -> bool {
+    // Call Widget::_send; returns true when event is used
+    fn send_event(&mut self, mut widget: Node<'_>, mut id: Id, event: Event) -> bool {
         debug_assert!(self.scroll == Scroll::None);
         debug_assert!(self.last_child.is_none());
         self.messages.set_base();
@@ -547,7 +537,11 @@ impl<'a> EventCx<'a> {
             }
         }
 
-        widget._send(self, id, disabled, event) == Used
+        let used = widget._send(self, id, disabled, event) == Used;
+
+        self.last_child = None;
+        self.scroll = Scroll::None;
+        used
     }
 
     fn send_popup_first(&mut self, mut widget: Node<'_>, id: Option<Id>, event: Event) {
@@ -568,6 +562,28 @@ impl<'a> EventCx<'a> {
         }
     }
 
+    // Call Widget::_nav_next
+    fn nav_next(
+        &mut self,
+        mut widget: Node<'_>,
+        focus: Option<&Id>,
+        advance: NavAdvance,
+    ) -> Option<Id> {
+        debug_assert!(self.scroll == Scroll::None);
+        debug_assert!(self.last_child.is_none());
+        self.messages.set_base();
+        log::trace!(target: "kas_core::event", "nav_next: focus={focus:?}, advance={advance:?}");
+
+        let result = widget._nav_next(self, focus, advance);
+
+        // Ignore residual values
+        self.last_child = None;
+        self.scroll = Scroll::None;
+        assert!(!self.messages.has_any());
+
+        result
+    }
+
     // Clear old hover, set new hover, send events.
     // If there is a popup, only permit descendands of that.
     fn set_hover(&mut self, mut widget: Node<'_>, mut w_id: Option<Id>) {
@@ -691,9 +707,9 @@ impl<'a> EventCx<'a> {
         // Whether to restart from the beginning on failure
         let restart = focus.is_some();
 
-        let mut opt_id = widget._nav_next(self, focus.as_ref(), advance);
+        let mut opt_id = self.nav_next(widget.re(), focus.as_ref(), advance);
         if restart && opt_id.is_none() {
-            opt_id = widget._nav_next(self, None, advance);
+            opt_id = self.nav_next(widget.re(), None, advance);
         }
 
         log::trace!(

From 969f09fa9d0c32ca93602677e06a2572bd7522e7 Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Thu, 30 Nov 2023 12:45:47 +0000
Subject: [PATCH 08/11] Pass ConfigCx to Widget::_nav_next instead of EventCx

This is a more robust fix. We could instead actually handle
EventCx::scroll values from _nav_next but we don't need to.
---
 crates/kas-core/src/core/impls.rs       |  6 +++---
 crates/kas-core/src/core/node.rs        |  6 +++---
 crates/kas-core/src/core/widget.rs      |  2 +-
 crates/kas-core/src/event/components.rs | 16 +++++++++++++---
 crates/kas-core/src/event/cx/mod.rs     | 13 ++-----------
 crates/kas-core/src/hidden.rs           |  2 +-
 crates/kas-macros/src/widget.rs         |  4 ++--
 crates/kas-view/src/list_view.rs        |  6 +++---
 crates/kas-view/src/matrix_view.rs      |  6 +++---
 9 files changed, 31 insertions(+), 30 deletions(-)

diff --git a/crates/kas-core/src/core/impls.rs b/crates/kas-core/src/core/impls.rs
index 500f9349c..6bf37d988 100644
--- a/crates/kas-core/src/core/impls.rs
+++ b/crates/kas-core/src/core/impls.rs
@@ -5,7 +5,7 @@
 
 //! Widget method implementations
 
-use crate::event::{Event, EventCx, FocusSource, IsUsed, Scroll, Unused, Used};
+use crate::event::{ConfigCx, Event, EventCx, FocusSource, IsUsed, Scroll, Unused, Used};
 #[cfg(debug_assertions)] use crate::util::IdentifyWidget;
 use crate::{Erased, Events, Id, Layout, NavAdvance, Node, Widget};
 
@@ -132,7 +132,7 @@ pub fn _replay<W: Events>(
 /// Generic implementation of [`Widget::_nav_next`]
 pub fn _nav_next<W: Events>(
     widget: &mut W,
-    cx: &mut EventCx,
+    cx: &mut ConfigCx,
     data: &<W as Widget>::Data,
     focus: Option<&Id>,
     advance: NavAdvance,
@@ -143,7 +143,7 @@ pub fn _nav_next<W: Events>(
 
 fn nav_next(
     mut widget: Node<'_>,
-    cx: &mut EventCx,
+    cx: &mut ConfigCx,
     focus: Option<&Id>,
     advance: NavAdvance,
     navigable: bool,
diff --git a/crates/kas-core/src/core/node.rs b/crates/kas-core/src/core/node.rs
index ecd512c83..c7584e299 100644
--- a/crates/kas-core/src/core/node.rs
+++ b/crates/kas-core/src/core/node.rs
@@ -38,7 +38,7 @@ trait NodeT {
     fn _replay(&mut self, cx: &mut EventCx, id: Id, msg: Erased);
     fn _nav_next(
         &mut self,
-        cx: &mut EventCx,
+        cx: &mut ConfigCx,
         focus: Option<&Id>,
         advance: NavAdvance,
     ) -> Option<Id>;
@@ -102,7 +102,7 @@ impl<'a, T> NodeT for (&'a mut dyn Widget<Data = T>, &'a T) {
     }
     fn _nav_next(
         &mut self,
-        cx: &mut EventCx,
+        cx: &mut ConfigCx,
         focus: Option<&Id>,
         advance: NavAdvance,
     ) -> Option<Id> {
@@ -369,7 +369,7 @@ impl<'a> Node<'a> {
     // NOTE: public on account of ListView
     pub fn _nav_next(
         &mut self,
-        cx: &mut EventCx,
+        cx: &mut ConfigCx,
         focus: Option<&Id>,
         advance: NavAdvance,
     ) -> Option<Id> {
diff --git a/crates/kas-core/src/core/widget.rs b/crates/kas-core/src/core/widget.rs
index 977168c5b..3c3ff8d19 100644
--- a/crates/kas-core/src/core/widget.rs
+++ b/crates/kas-core/src/core/widget.rs
@@ -433,7 +433,7 @@ pub trait Widget: Layout {
     #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
     fn _nav_next(
         &mut self,
-        cx: &mut EventCx,
+        cx: &mut ConfigCx,
         data: &Self::Data,
         focus: Option<&Id>,
         advance: NavAdvance,
diff --git a/crates/kas-core/src/event/components.rs b/crates/kas-core/src/event/components.rs
index 874313835..d8fe2f2f7 100644
--- a/crates/kas-core/src/event/components.rs
+++ b/crates/kas-core/src/event/components.rs
@@ -198,13 +198,23 @@ impl ScrollComponent {
     ///
     /// Returns [`Action::REGION_MOVED`] when the scroll offset changes.
     pub fn focus_rect(&mut self, cx: &mut EventCx, rect: Rect, window_rect: Rect) -> Action {
+        let action = self.self_focus_rect(rect, window_rect);
+        cx.set_scroll(Scroll::Rect(rect - self.offset));
+        action
+    }
+
+    /// Scroll self to make the given `rect` visible
+    ///
+    /// This is identical to [`Self::focus_rect`] except that it does not call
+    /// [`EventCx::set_scroll`], thus will not affect ancestors.
+    #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
+    #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
+    pub fn self_focus_rect(&mut self, rect: Rect, window_rect: Rect) -> Action {
         self.glide.stop();
         let v = rect.pos - window_rect.pos;
         let off = Offset::conv(rect.size) - Offset::conv(window_rect.size);
         let offset = self.offset.max(v + off).min(v);
-        let action = self.set_offset(offset);
-        cx.set_scroll(Scroll::Rect(rect - self.offset));
-        action
+        self.set_offset(offset)
     }
 
     /// Handle a [`Scroll`] action
diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs
index 7339456ad..2656eb3c5 100644
--- a/crates/kas-core/src/event/cx/mod.rs
+++ b/crates/kas-core/src/event/cx/mod.rs
@@ -563,25 +563,16 @@ impl<'a> EventCx<'a> {
     }
 
     // Call Widget::_nav_next
+    #[inline]
     fn nav_next(
         &mut self,
         mut widget: Node<'_>,
         focus: Option<&Id>,
         advance: NavAdvance,
     ) -> Option<Id> {
-        debug_assert!(self.scroll == Scroll::None);
-        debug_assert!(self.last_child.is_none());
-        self.messages.set_base();
         log::trace!(target: "kas_core::event", "nav_next: focus={focus:?}, advance={advance:?}");
 
-        let result = widget._nav_next(self, focus, advance);
-
-        // Ignore residual values
-        self.last_child = None;
-        self.scroll = Scroll::None;
-        assert!(!self.messages.has_any());
-
-        result
+        widget._nav_next(&mut self.config_cx(), focus, advance)
     }
 
     // Clear old hover, set new hover, send events.
diff --git a/crates/kas-core/src/hidden.rs b/crates/kas-core/src/hidden.rs
index 9281b18f8..eae3e45ee 100644
--- a/crates/kas-core/src/hidden.rs
+++ b/crates/kas-core/src/hidden.rs
@@ -191,7 +191,7 @@ impl_scope! {
 
         fn _nav_next(
             &mut self,
-            cx: &mut EventCx,
+            cx: &mut ConfigCx,
             _: &A,
             focus: Option<&Id>,
             advance: NavAdvance,
diff --git a/crates/kas-macros/src/widget.rs b/crates/kas-macros/src/widget.rs
index 1c60649ed..d0a50aa6b 100644
--- a/crates/kas-macros/src/widget.rs
+++ b/crates/kas-macros/src/widget.rs
@@ -599,7 +599,7 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul
 
                 fn _nav_next(
                     &mut self,
-                    cx: &mut ::kas::event::EventCx,
+                    cx: &mut ::kas::event::ConfigCx,
                     data: &Self::Data,
                     focus: Option<&::kas::Id>,
                     advance: ::kas::NavAdvance,
@@ -1141,7 +1141,7 @@ fn widget_recursive_methods(core_path: &Toks) -> Toks {
 
         fn _nav_next(
             &mut self,
-            cx: &mut ::kas::event::EventCx,
+            cx: &mut ::kas::event::ConfigCx,
             data: &Self::Data,
             focus: Option<&::kas::Id>,
             advance: ::kas::NavAdvance,
diff --git a/crates/kas-view/src/list_view.rs b/crates/kas-view/src/list_view.rs
index ef7b831fc..5224499ac 100644
--- a/crates/kas-view/src/list_view.rs
+++ b/crates/kas-view/src/list_view.rs
@@ -812,7 +812,7 @@ impl_scope! {
         // Non-standard implementation to allow mapping new children
         fn _nav_next(
             &mut self,
-            cx: &mut EventCx,
+            cx: &mut ConfigCx,
             data: &A,
             focus: Option<&Id>,
             advance: NavAdvance,
@@ -855,10 +855,10 @@ impl_scope! {
                     last_data
                 };
 
-                let act = self.scroll.focus_rect(cx, solver.rect(data_index), self.core.rect);
+                let act = self.scroll.self_focus_rect(solver.rect(data_index), self.core.rect);
                 if !act.is_empty() {
                     cx.action(&self, act);
-                    self.update_widgets(&mut cx.config_cx(), data);
+                    self.update_widgets(cx, data);
                 }
 
                 let index = data_index % usize::conv(self.cur_len);
diff --git a/crates/kas-view/src/matrix_view.rs b/crates/kas-view/src/matrix_view.rs
index dac8fa20a..ed79e1a42 100644
--- a/crates/kas-view/src/matrix_view.rs
+++ b/crates/kas-view/src/matrix_view.rs
@@ -780,7 +780,7 @@ impl_scope! {
         // Non-standard implementation to allow mapping new children
         fn _nav_next(
             &mut self,
-            cx: &mut EventCx,
+            cx: &mut ConfigCx,
             data: &A,
             focus: Option<&Id>,
             advance: NavAdvance,
@@ -833,10 +833,10 @@ impl_scope! {
                     (d_cols - 1, d_rows - 1)
                 };
 
-                let action = self.scroll.focus_rect(cx, solver.rect(ci, ri), self.core.rect);
+                let action = self.scroll.self_focus_rect(solver.rect(ci, ri), self.core.rect);
                 if !action.is_empty() {
                     cx.action(&self, action);
-                    solver = self.update_widgets(&mut cx.config_cx(), data);
+                    solver = self.update_widgets(cx, data);
                 }
 
                 let index = solver.data_to_child(ci, ri);

From b341c8a81d4f3cc946f10b6dad9c62c07148ce63 Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Fri, 1 Dec 2023 16:17:29 +0000
Subject: [PATCH 09/11] Better handling of Action::REGION_MOVED

Fixes a potential debug_assert! on unhandled REGION_MOVED
from next nav focus.
---
 crates/kas-core/src/event/cx/cx_shell.rs | 39 +++++++++++-------------
 crates/kas-core/src/event/cx/mod.rs      |  1 -
 2 files changed, 17 insertions(+), 23 deletions(-)

diff --git a/crates/kas-core/src/event/cx/cx_shell.rs b/crates/kas-core/src/event/cx/cx_shell.rs
index f8e294525..944f5f39a 100644
--- a/crates/kas-core/src/event/cx/cx_shell.rs
+++ b/crates/kas-core/src/event/cx/cx_shell.rs
@@ -54,7 +54,6 @@ impl EventState {
             time_updates: vec![],
             fut_messages: vec![],
             pending_update: None,
-            region_moved: false,
             pending_sel_focus: None,
             pending_nav_focus: PendingNavFocus::None,
             pending_cmds: Default::default(),
@@ -95,7 +94,7 @@ impl EventState {
         self.new_access_layer(id.clone(), false);
 
         ConfigCx::new(sizer, self).configure(win.as_node(data), id);
-        self.region_moved = true;
+        self.action |= Action::REGION_MOVED;
     }
 
     /// Get the next resume time
@@ -134,11 +133,6 @@ impl EventState {
         win: &mut Window<A>,
         data: &A,
     ) -> Action {
-        if self.action.contains(Action::REGION_MOVED) {
-            self.region_moved = true;
-            self.action.remove(Action::REGION_MOVED);
-        }
-
         self.with(shell, window, messages, |cx| {
             while let Some((id, wid)) = cx.popup_removed.pop() {
                 cx.send_event(win.as_node(data), id, Event::PopupClosed(wid));
@@ -200,24 +194,12 @@ impl EventState {
                     win.as_node(data)
                         .find_node(&id, |node| cx.configure(node, id.clone()));
 
-                    cx.region_moved = true;
+                    cx.action |= Action::REGION_MOVED;
                 } else {
                     win.as_node(data).find_node(&id, |node| cx.update(node));
                 }
             }
 
-            if cx.region_moved {
-                cx.region_moved = false;
-
-                // Update hovered widget
-                let hover = win.find_id(data, cx.last_mouse_coord);
-                cx.set_hover(win.as_node(data), hover);
-
-                for grab in cx.touch_grab.iter_mut() {
-                    grab.cur_id = win.find_id(data, grab.coord);
-                }
-            }
-
             if let Some(pending) = cx.pending_sel_focus.take() {
                 cx.set_sel_focus(win.as_node(data), pending);
             }
@@ -239,9 +221,22 @@ impl EventState {
                 cx.send_event(win.as_node(data), id, Event::Command(cmd, None));
             }
 
-            // Poll futures last. This means that any newly pushed future should
-            // get polled from the same update() call.
+            // Poll futures almost last. This means that any newly pushed future
+            // should get polled from the same update() call.
             cx.poll_futures(win.as_node(data));
+
+            // Finally, clear the region_moved flag.
+            if cx.action.contains(Action::REGION_MOVED) {
+                cx.action.remove(Action::REGION_MOVED);
+
+                // Update hovered widget
+                let hover = win.find_id(data, cx.last_mouse_coord);
+                cx.set_hover(win.as_node(data), hover);
+
+                for grab in cx.touch_grab.iter_mut() {
+                    grab.cur_id = win.find_id(data, grab.coord);
+                }
+            }
         });
 
         if self.hover_icon != self.old_hover_icon && self.mouse_grab.is_none() {
diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs
index 2656eb3c5..ee86b6bfb 100644
--- a/crates/kas-core/src/event/cx/mod.rs
+++ b/crates/kas-core/src/event/cx/mod.rs
@@ -213,7 +213,6 @@ pub struct EventState {
     fut_messages: Vec<(Id, Pin<Box<dyn Future<Output = Erased>>>)>,
     // Widget requiring update (and optionally configure)
     pending_update: Option<(Id, bool)>,
-    region_moved: bool,
     // Optional new target for selection focus. bool is true if this also gains key focus.
     pending_sel_focus: Option<PendingSelFocus>,
     pending_nav_focus: PendingNavFocus,

From 03678d003fa519a53a702bc952ae1581228632c2 Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Fri, 1 Dec 2023 16:19:56 +0000
Subject: [PATCH 10/11] List/MatrixView: fix infinite loop in _nav_next

This could happen when children do not support key-nav
---
 crates/kas-view/src/list_view.rs   | 6 ++++++
 crates/kas-view/src/matrix_view.rs | 7 +++++++
 2 files changed, 13 insertions(+)

diff --git a/crates/kas-view/src/list_view.rs b/crates/kas-view/src/list_view.rs
index 5224499ac..b6dd4cdf0 100644
--- a/crates/kas-view/src/list_view.rs
+++ b/crates/kas-view/src/list_view.rs
@@ -837,6 +837,7 @@ impl_scope! {
                 NavAdvance::Reverse(_) => true,
             };
 
+            let mut starting_child = child;
             loop {
                 let solver = self.position_solver();
                 let last_data = data.len() - 1;
@@ -869,6 +870,11 @@ impl_scope! {
                 }
 
                 child = Some(index);
+                if starting_child == child {
+                    return None;
+                } else if starting_child.is_none() {
+                    starting_child = child;
+                }
             }
         }
     }
diff --git a/crates/kas-view/src/matrix_view.rs b/crates/kas-view/src/matrix_view.rs
index ed79e1a42..8172266ec 100644
--- a/crates/kas-view/src/matrix_view.rs
+++ b/crates/kas-view/src/matrix_view.rs
@@ -805,6 +805,7 @@ impl_scope! {
                 NavAdvance::Reverse(_) => true,
             };
 
+            let mut starting_child = child;
             loop {
                 let mut solver = self.position_solver();
                 let (d_cols, d_rows) = data.len();
@@ -845,7 +846,13 @@ impl_scope! {
                 {
                     return Some(id);
                 }
+
                 child = Some(index);
+                if starting_child == child {
+                    return None;
+                } else if starting_child.is_none() {
+                    starting_child = child;
+                }
             }
         }
     }

From f132619b8d601d775855cf0b4274609aca2d8888 Mon Sep 17 00:00:00 2001
From: Diggory Hardy <git@dhardy.name>
Date: Sun, 3 Dec 2023 11:52:10 +0000
Subject: [PATCH 11/11] Fix handling of changed KeyFocus via mouse

Also improvements to SelFocus, NavFocus code.
---
 crates/kas-core/src/event/cx/cx_pub.rs   |  6 +-
 crates/kas-core/src/event/cx/cx_shell.rs |  9 +--
 crates/kas-core/src/event/cx/mod.rs      | 73 +++++++++++-------------
 crates/kas-widgets/src/edit.rs           |  2 +-
 4 files changed, 41 insertions(+), 49 deletions(-)

diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs
index 36b046137..b4a8ad43b 100644
--- a/crates/kas-core/src/event/cx/cx_pub.rs
+++ b/crates/kas-core/src/event/cx/cx_pub.rs
@@ -399,7 +399,7 @@ impl EventState {
     #[inline]
     pub fn request_key_focus(&mut self, target: Id, source: FocusSource) {
         self.pending_sel_focus = Some(PendingSelFocus {
-            target,
+            target: Some(target),
             key_focus: true,
             source,
         });
@@ -421,13 +421,13 @@ impl EventState {
     #[inline]
     pub fn request_sel_focus(&mut self, target: Id, source: FocusSource) {
         if let Some(ref pending) = self.pending_sel_focus {
-            if pending.target == target {
+            if pending.target.as_ref() == Some(&target) {
                 return;
             }
         }
 
         self.pending_sel_focus = Some(PendingSelFocus {
-            target,
+            target: Some(target),
             key_focus: false,
             source,
         });
diff --git a/crates/kas-core/src/event/cx/cx_shell.rs b/crates/kas-core/src/event/cx/cx_shell.rs
index 944f5f39a..d4c62ff5b 100644
--- a/crates/kas-core/src/event/cx/cx_shell.rs
+++ b/crates/kas-core/src/event/cx/cx_shell.rs
@@ -200,10 +200,6 @@ impl EventState {
                 }
             }
 
-            if let Some(pending) = cx.pending_sel_focus.take() {
-                cx.set_sel_focus(win.as_node(data), pending);
-            }
-
             match std::mem::take(&mut cx.pending_nav_focus) {
                 PendingNavFocus::None => (),
                 PendingNavFocus::Set { target, source } => {
@@ -216,6 +212,11 @@ impl EventState {
                 } => cx.next_nav_focus_impl(win.as_node(data), target, reverse, source),
             }
 
+            // Update sel focus after nav focus:
+            if let Some(pending) = cx.pending_sel_focus.take() {
+                cx.set_sel_focus(win.as_node(data), pending);
+            }
+
             while let Some((id, cmd)) = cx.pending_cmds.pop_front() {
                 log::trace!(target: "kas_core::event", "sending pending command {cmd:?} to {id}");
                 cx.send_event(win.as_node(data), id, Event::Command(cmd, None));
diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs
index ee86b6bfb..280a466c3 100644
--- a/crates/kas-core/src/event/cx/mod.rs
+++ b/crates/kas-core/src/event/cx/mod.rs
@@ -143,8 +143,9 @@ struct PanGrab {
     coords: [(Coord, Coord); MAX_PAN_GRABS],
 }
 
+#[derive(Debug)]
 struct PendingSelFocus {
-    target: Id,
+    target: Option<Id>,
     key_focus: bool,
     source: FocusSource,
 }
@@ -233,6 +234,22 @@ impl EventState {
         }
     }
 
+    fn clear_key_focus(&mut self) {
+        if self.key_focus {
+            if let Some(ref mut pending) = self.pending_sel_focus {
+                if pending.target == self.sel_focus {
+                    pending.key_focus = false;
+                }
+            } else {
+                self.pending_sel_focus = Some(PendingSelFocus {
+                    target: None,
+                    key_focus: false,
+                    source: FocusSource::Synthetic,
+                });
+            }
+        }
+    }
+
     fn set_pan_on(
         &mut self,
         id: Id,
@@ -607,12 +624,10 @@ impl<'a> EventCx<'a> {
             source,
         } = pending;
 
-        log::trace!("set_sel_focus: target={target}, key_focus={key_focus}");
-        // The widget probably already has nav focus, but anyway:
-        self.set_nav_focus(target.clone(), FocusSource::Synthetic);
+        log::trace!("set_sel_focus: target={target:?}, key_focus={key_focus}");
 
         if target == self.sel_focus {
-            self.key_focus = self.key_focus || key_focus;
+            self.key_focus = target.is_some() && (self.key_focus || key_focus);
             return;
         }
 
@@ -627,11 +642,16 @@ impl<'a> EventCx<'a> {
         }
 
         self.key_focus = key_focus;
-        self.sel_focus = Some(target.clone());
+        self.sel_focus = target.clone();
 
-        self.send_event(widget.re(), target.clone(), Event::SelFocus(source));
-        if key_focus {
-            self.send_event(widget, target, Event::KeyFocus);
+        if let Some(id) = target {
+            // The widget probably already has nav focus, but anyway:
+            self.set_nav_focus(id.clone(), FocusSource::Synthetic);
+
+            self.send_event(widget.re(), id.clone(), Event::SelFocus(source));
+            if key_focus {
+                self.send_event(widget, id, Event::KeyFocus);
+            }
         }
     }
 
@@ -641,16 +661,13 @@ impl<'a> EventCx<'a> {
             return;
         }
 
+        self.clear_key_focus();
+
         if let Some(old) = self.nav_focus.take() {
             self.action(&old, Action::REDRAW);
             self.send_event(widget.re(), old, Event::LostNavFocus);
         }
 
-        if let Some(id) = self.key_focus() {
-            self.key_focus = false;
-            self.send_event(widget.re(), id, Event::LostKeyFocus);
-        }
-
         self.nav_focus = target.clone();
         log::debug!(target: "kas_core::event", "nav_focus = {target:?}");
         if let Some(id) = target {
@@ -702,32 +719,6 @@ impl<'a> EventCx<'a> {
             opt_id = self.nav_next(widget.re(), None, advance);
         }
 
-        log::trace!(
-            target: "kas_core::event",
-            "next_nav_focus: nav_focus={opt_id:?}",
-        );
-        if opt_id == self.nav_focus {
-            return;
-        }
-
-        if let Some(old) = self.nav_focus.take() {
-            self.redraw(&old);
-            self.send_event(widget.re(), old, Event::LostNavFocus);
-        }
-
-        if let Some(id) = self.key_focus() {
-            self.key_focus = false;
-            self.send_event(widget.re(), id, Event::LostKeyFocus);
-        }
-
-        self.nav_focus = opt_id.clone();
-        if let Some(id) = opt_id {
-            log::debug!(target: "kas_core::event", "nav_focus = Some({id})");
-            self.redraw(id.clone());
-            self.send_event(widget, id, Event::NavFocus(source));
-        } else {
-            log::debug!(target: "kas_core::event", "nav_focus = None");
-            // Most likely an error occurred
-        }
+        self.set_nav_focus_impl(widget, opt_id, source);
     }
 }
diff --git a/crates/kas-widgets/src/edit.rs b/crates/kas-widgets/src/edit.rs
index 1050cd338..17a1dd17d 100644
--- a/crates/kas-widgets/src/edit.rs
+++ b/crates/kas-widgets/src/edit.rs
@@ -661,7 +661,7 @@ impl_scope! {
                     if !self.has_key_focus {
                         cx.request_key_focus(self.id(), source);
                     }
-                    if !self.class.multi_line() {
+                    if source == FocusSource::Key && !self.class.multi_line() {
                         self.selection.clear();
                         self.selection.set_edit_pos(self.text.str_len());
                         cx.redraw(self);