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);