diff --git a/crates/kas-core/src/core/impls.rs b/crates/kas-core/src/core/impls.rs index 5b05f78ac..5fa4c5cc3 100644 --- a/crates/kas-core/src/core/impls.rs +++ b/crates/kas-core/src/core/impls.rs @@ -39,33 +39,26 @@ pub fn _send( do_handle_event = true; } else { - if event.is_reusable() { - is_used = widget.steal_event(cx, data, &id, &event); - } - if !is_used { - cx.assert_post_steal_unused(); - - if let Some(index) = widget.find_child_index(&id) { - let translation = widget.translation(); - let mut _found = false; - widget.as_node(data).for_child(index, |mut node| { - is_used = node._send(cx, id.clone(), event.clone() + translation); - _found = true; - }); - - #[cfg(debug_assertions)] - if !_found { - // This is an error in the widget. It's unlikely and not fatal - // so we ignore in release builds. - log::error!( - "_send: {} found index {index} for {id} but not child", - IdentifyWidget(widget.widget_name(), widget.id_ref()) - ); - } - - if let Some(scroll) = cx.post_send(index) { - widget.handle_scroll(cx, data, scroll); - } + if let Some(index) = widget.find_child_index(&id) { + let translation = widget.translation(); + let mut _found = false; + widget.as_node(data).for_child(index, |mut node| { + is_used = node._send(cx, id.clone(), event.clone() + translation); + _found = true; + }); + + #[cfg(debug_assertions)] + if !_found { + // This is an error in the widget. It's unlikely and not fatal + // so we ignore in release builds. + log::error!( + "_send: {} found index {index} for {id} but not child", + IdentifyWidget(widget.widget_name(), widget.id_ref()) + ); + } + + if let Some(scroll) = cx.post_send(index) { + widget.handle_scroll(cx, data, scroll); } } diff --git a/crates/kas-core/src/core/widget.rs b/crates/kas-core/src/core/widget.rs index 7898d92d2..1a6ee04ba 100644 --- a/crates/kas-core/src/core/widget.rs +++ b/crates/kas-core/src/core/widget.rs @@ -176,27 +176,6 @@ pub trait Events: Widget + Sized { Unused } - /// Potentially steal an event before it reaches a child - /// - /// This is an optional event handler (see [documentation](crate::event)). - /// - /// The method should *either* return [`Used`] or return [`Unused`] without - /// modifying `cx`; attempting to do otherwise (e.g. by calling - /// [`EventCx::set_scroll`] or leaving a message on the stack when returning - /// [`Unused`]) will result in a panic. - /// - /// Default implementation: return [`Unused`]. - fn steal_event( - &mut self, - cx: &mut EventCx, - data: &Self::Data, - id: &Id, - event: &Event, - ) -> IsUsed { - let _ = (cx, data, id, event); - Unused - } - /// Handler for messages from children/descendants /// /// This is the secondary event handler (see [documentation](crate::event)). diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs index 96c765b2f..78de08977 100644 --- a/crates/kas-core/src/event/cx/cx_pub.rs +++ b/crates/kas-core/src/event/cx/cx_pub.rs @@ -172,19 +172,26 @@ impl EventState { /// Disabled status applies to all descendants and blocks reception of /// events ([`Unused`] is returned automatically when the /// recipient or any ancestor is disabled). - pub fn set_disabled(&mut self, w_id: Id, state: bool) { + /// + /// Disabling a widget clears navigation, selection and key focus when the + /// target is disabled, and also cancels press/pan grabs. + pub fn set_disabled(&mut self, target: Id, disable: bool) { + if disable { + self.clear_events(&target); + } + for (i, id) in self.disabled.iter().enumerate() { - if w_id == id { - if !state { - self.redraw(w_id); + if target == id { + if !disable { + self.redraw(target); self.disabled.remove(i); } return; } } - if state { - self.action(&w_id, Action::REDRAW); - self.disabled.push(w_id); + if disable { + self.action(&target, Action::REDRAW); + self.disabled.push(target); } } @@ -468,6 +475,10 @@ impl EventState { /// grabs targets the widget to depress, or when a keyboard binding is used /// to activate a widget (for the duration of the key-press). /// + /// Assumption: this method will only be called by handlers of a grab (i.e. + /// recipients of [`Event::PressStart`] after initiating a successful grab, + /// [`Event::PressMove`] or [`Event::PressEnd`]). + /// /// Queues a redraw and returns `true` if the depress target changes, /// otherwise returns `false`. pub fn set_grab_depress(&mut self, source: PressSource, target: Option) -> bool { @@ -497,8 +508,8 @@ impl EventState { redraw } - /// Returns true if `id` or any descendant has a mouse or touch grab - pub fn any_pin_on(&self, id: &Id) -> bool { + /// Returns true if there is a mouse or touch grab on `id` or any descendant of `id` + pub fn any_grab_on(&self, id: &Id) -> bool { if self .mouse_grab .as_ref() diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs index 635c77e87..530ef4a89 100644 --- a/crates/kas-core/src/event/cx/mod.rs +++ b/crates/kas-core/src/event/cx/mod.rs @@ -78,6 +78,7 @@ struct MouseGrab { start_id: Id, depress: Option, details: GrabDetails, + cancel: bool, } impl<'a> EventCx<'a> { @@ -111,6 +112,7 @@ struct TouchGrab { coord: Coord, mode: GrabMode, pan_grab: (u16, u16), + cancel: bool, } impl TouchGrab { @@ -336,26 +338,81 @@ impl EventState { } } + #[inline] + fn get_touch_index(&self, touch_id: u64) -> Option { + self.touch_grab + .iter() + .enumerate() + .find_map(|(i, grab)| (grab.id == touch_id).then_some(i)) + } + #[inline] fn get_touch(&mut self, touch_id: u64) -> Option<&mut TouchGrab> { self.touch_grab.iter_mut().find(|grab| grab.id == touch_id) } // Clears touch grab and pan grab and redraws - fn remove_touch(&mut self, touch_id: u64) -> Option { - for i in 0..self.touch_grab.len() { - if self.touch_grab[i].id == touch_id { - let grab = self.touch_grab.remove(i); - log::trace!( - "remove_touch: touch_id={touch_id}, start_id={}", - grab.start_id - ); - self.opt_action(grab.depress.clone(), Action::REDRAW); - self.remove_pan_grab(grab.pan_grab); - return Some(grab); + // + // Returns the grab. Panics on out-of-bounds error. + fn remove_touch(&mut self, index: usize) -> TouchGrab { + let mut grab = self.touch_grab.remove(index); + log::trace!( + "remove_touch: touch_id={}, start_id={}", + grab.id, + grab.start_id + ); + self.opt_action(grab.depress.clone(), Action::REDRAW); + self.remove_pan_grab(grab.pan_grab); + self.action(Id::ROOT, grab.flush_click_move()); + grab + } + + /// Clear all active events on `target` + fn clear_events(&mut self, target: &Id) { + if let Some(id) = self.sel_focus.as_ref() { + if target.is_ancestor_of(id) { + if let Some(pending) = self.pending_sel_focus.as_mut() { + if pending.target.as_ref() == Some(id) { + pending.target = None; + pending.key_focus = false; + } + } else { + self.pending_sel_focus = Some(PendingSelFocus { + target: None, + key_focus: false, + source: FocusSource::Synthetic, + }); + } + } + } + + if let Some(id) = self.nav_focus.as_ref() { + if target.is_ancestor_of(id) { + if matches!(&self.pending_nav_focus, PendingNavFocus::Set { ref target, .. } if target.as_ref() == Some(id)) + { + self.pending_nav_focus = PendingNavFocus::None; + } + + if matches!(self.pending_nav_focus, PendingNavFocus::None) { + self.pending_nav_focus = PendingNavFocus::Set { + target: None, + source: FocusSource::Synthetic, + }; + } + } + } + + if let Some(grab) = self.mouse_grab.as_mut() { + if grab.start_id == target { + grab.cancel = true; + } + } + + for grab in self.touch_grab.iter_mut() { + if grab.start_id == target { + grab.cancel = true; } } - None } } @@ -485,7 +542,10 @@ impl<'a> EventCx<'a> { // Clears mouse grab and pan grab, resets cursor and redraws fn remove_mouse_grab(&mut self, success: bool) -> Option<(Id, Event)> { if let Some(grab) = self.mouse_grab.take() { - log::trace!("remove_mouse_grab: start_id={}", grab.start_id); + log::trace!( + "remove_mouse_grab: start_id={}, success={success}", + grab.start_id + ); self.window.set_cursor_icon(self.hover_icon); self.opt_action(grab.depress.clone(), Action::REDRAW); if let GrabDetails::Pan(g) = grab.details { @@ -506,12 +566,6 @@ impl<'a> EventCx<'a> { } } - pub(crate) fn assert_post_steal_unused(&self) { - if self.scroll != Scroll::None || self.messages.has_any() { - panic!("steal_event affected EventCx and returned Unused"); - } - } - pub(crate) fn post_send(&mut self, index: usize) -> Option { self.last_child = Some(index); (self.scroll != Scroll::None).then_some(self.scroll) diff --git a/crates/kas-core/src/event/cx/platform.rs b/crates/kas-core/src/event/cx/platform.rs index ffb69a266..d976c396d 100644 --- a/crates/kas-core/src/event/cx/platform.rs +++ b/crates/kas-core/src/event/cx/platform.rs @@ -140,9 +140,38 @@ impl EventState { } cx.flush_mouse_grab_motion(); - for i in 0..cx.touch_grab.len() { + if cx + .mouse_grab + .as_ref() + .map(|grab| grab.cancel) + .unwrap_or(false) + { + if let Some((id, event)) = cx.remove_mouse_grab(false) { + cx.send_event(win.as_node(data), id, event); + } + } + + let mut i = 0; + while i < cx.touch_grab.len() { let action = cx.touch_grab[i].flush_click_move(); cx.state.action |= action; + + if cx.touch_grab[i].cancel { + let grab = cx.remove_touch(i); + + let press = Press { + source: PressSource::Touch(grab.id), + id: grab.cur_id, + coord: grab.coord, + }; + let event = Event::PressEnd { + press, + success: false, + }; + cx.send_event(win.as_node(data), grab.start_id, event); + } else { + i += 1; + } } for gi in 0..cx.pan_grab.len() { @@ -560,8 +589,8 @@ impl<'a> EventCx<'a> { } } ev @ (TouchPhase::Ended | TouchPhase::Cancelled) => { - if let Some(mut grab) = self.remove_touch(touch.id) { - self.action(Id::ROOT, grab.flush_click_move()); + if let Some(index) = self.get_touch_index(touch.id) { + let grab = self.remove_touch(index); if grab.mode == GrabMode::Grab { let id = grab.cur_id.clone(); diff --git a/crates/kas-core/src/event/cx/press.rs b/crates/kas-core/src/event/cx/press.rs index 211ad3c20..c5aa72ebb 100644 --- a/crates/kas-core/src/event/cx/press.rs +++ b/crates/kas-core/src/event/cx/press.rs @@ -194,6 +194,7 @@ impl GrabBuilder { if grab.start_id != id || grab.button != button || grab.details.is_pan() != mode.is_pan() + || grab.cancel { return Unused; } @@ -209,6 +210,7 @@ impl GrabBuilder { start_id: id.clone(), depress: Some(id.clone()), details, + cancel: false, }); } if let Some(icon) = cursor { @@ -217,7 +219,7 @@ impl GrabBuilder { } PressSource::Touch(touch_id) => { if let Some(grab) = cx.get_touch(touch_id) { - if grab.mode.is_pan() != mode.is_pan() { + if grab.mode.is_pan() != mode.is_pan() || grab.cancel { return Unused; } @@ -240,6 +242,7 @@ impl GrabBuilder { coord, mode, pan_grab, + cancel: false, }); } } diff --git a/crates/kas-core/src/event/events.rs b/crates/kas-core/src/event/events.rs index 32c494912..611ff0e60 100644 --- a/crates/kas-core/src/event/events.rs +++ b/crates/kas-core/src/event/events.rs @@ -292,14 +292,21 @@ impl Event { /// Pass to disabled widgets? /// - /// Disabled status should disable input handling but not prevent other - /// notifications. + /// When a widget is disabled: + /// + /// - New input events (`Command`, `PressStart`, `Scroll`) are not passed + /// - Continuing input actions (`PressMove`, `PressEnd`) are passed (or + /// the input sequence may be terminated). + /// - New focus notifications are not passed + /// - Focus-loss notifications are passed + /// - Requested events like `Timer` are passed pub fn pass_when_disabled(&self) -> bool { use Event::*; match self { Command(_, _) => false, - Key(_, _) | Scroll(_) | Pan { .. } => false, - CursorMove { .. } | PressStart { .. } | PressMove { .. } | PressEnd { .. } => false, + Key(_, _) | Scroll(_) => false, + CursorMove { .. } | PressStart { .. } => false, + Pan { .. } | PressMove { .. } | PressEnd { .. } => true, Timer(_) | PopupClosed(_) => true, NavFocus { .. } | SelFocus(_) | KeyFocus | MouseHover(_) => false, LostNavFocus | LostKeyFocus | LostSelFocus => true, diff --git a/crates/kas-core/src/event/mod.rs b/crates/kas-core/src/event/mod.rs index cf508624d..48408f49e 100644 --- a/crates/kas-core/src/event/mod.rs +++ b/crates/kas-core/src/event/mod.rs @@ -22,10 +22,7 @@ //! inhibit calling of [`Events::handle_event`] on this widget (but still //! unwind, calling [`Events::handle_event`] on ancestors)). //! 3. Traverse *down* the widget tree from its root to the target according to -//! the [`Id`]. On each node (excluding the target), -//! -//! - Call [`Events::steal_event`]; if this method "steals" the event, -//! skip to step 5. +//! the [`Id`]. //! 4. In the normal case (when the target is not disabled and the event is //! not stolen), [`Events::handle_event`] is called on the target. //! 5. If the message stack is not empty, call [`Events::handle_messages`] on diff --git a/crates/kas-macros/src/make_layout.rs b/crates/kas-macros/src/make_layout.rs index 77a78d530..7ffb9204a 100644 --- a/crates/kas-macros/src/make_layout.rs +++ b/crates/kas-macros/src/make_layout.rs @@ -219,18 +219,6 @@ impl Tree { } impl #impl_generics ::kas::Events for #impl_target { - fn steal_event( - &mut self, - _: &mut ::kas::event::EventCx, - _: &Self::Data, - _: &::kas::Id, - _: &::kas::event::Event, - ) -> ::kas::event::IsUsed { - #[cfg(debug_assertions)] - #core_path.status.require_rect(&#core_path.id); - ::kas::event::Unused - } - fn handle_event( &mut self, _: &mut ::kas::event::EventCx, diff --git a/crates/kas-macros/src/widget.rs b/crates/kas-macros/src/widget.rs index 27af60616..8cbd2f473 100644 --- a/crates/kas-macros/src/widget.rs +++ b/crates/kas-macros/src/widget.rs @@ -846,18 +846,6 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul }, }; - let fn_steal_event = quote! { - fn steal_event( - &mut self, - _: &mut ::kas::event::EventCx, - _: &Self::Data, - _: &::kas::Id, - _: &::kas::event::Event, - ) -> ::kas::event::IsUsed { - #require_rect - ::kas::event::Unused - } - }; let fn_handle_event = quote! { fn handle_event( &mut self, @@ -880,17 +868,6 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul events_impl.items.push(Verbatim(fn_handle_hover)); - if let Some((index, _)) = item_idents - .iter() - .find(|(_, ident)| *ident == "steal_event") - { - if let ImplItem::Fn(f) = &mut events_impl.items[*index] { - f.block.stmts.insert(0, require_rect.clone()); - } - } else { - events_impl.items.push(Verbatim(fn_steal_event)); - } - if let Some((index, _)) = item_idents .iter() .find(|(_, ident)| *ident == "handle_event") @@ -912,7 +889,6 @@ pub fn widget(attr_span: Span, mut args: WidgetArgs, scope: &mut Scope) -> Resul impl #impl_generics ::kas::Events for #impl_target { #fn_navigable #fn_handle_hover - #fn_steal_event #fn_handle_event } }); diff --git a/crates/kas-widgets/src/edit.rs b/crates/kas-widgets/src/edit.rs index d07b6e262..b5639bf5c 100644 --- a/crates/kas-widgets/src/edit.rs +++ b/crates/kas-widgets/src/edit.rs @@ -817,8 +817,13 @@ impl_scope! { } } Event::Scroll(delta) => { + // In single-line mode we do not handle purely vertical + // scrolling; this improves compatibility with Spinner. + let is_single = !self.multi_line(); let delta2 = match delta { + ScrollDelta::LineDelta(x, _) if x == 0.0 && is_single => return Unused, ScrollDelta::LineDelta(x, y) => cx.config().event().scroll_distance((x, y)), + ScrollDelta::PixelDelta(Offset(0, _)) if is_single => return Unused, ScrollDelta::PixelDelta(coord) => coord, }; self.pan_delta(cx, delta2) diff --git a/crates/kas-widgets/src/spinner.rs b/crates/kas-widgets/src/spinner.rs index 2e886caa7..57021b7f0 100644 --- a/crates/kas-widgets/src/spinner.rs +++ b/crates/kas-widgets/src/spinner.rs @@ -301,23 +301,23 @@ impl_scope! { impl Events for Self { type Data = A; - fn steal_event(&mut self, cx: &mut EventCx, data: &A, _: &Id, event: &Event) -> IsUsed { + fn handle_event(&mut self, cx: &mut EventCx, data: &A, event: Event) -> IsUsed { let btn = match event { Event::Command(cmd, code) => match cmd { Command::Down => { - cx.depress_with_key(self.b_down.id(), *code); + cx.depress_with_key(self.b_down.id(), code); SpinBtn::Down } Command::Up => { - cx.depress_with_key(self.b_up.id(), *code); + cx.depress_with_key(self.b_up.id(), code); SpinBtn::Up } _ => return Unused, }, Event::Scroll(ScrollDelta::LineDelta(_, y)) => { - if *y > 0.0 { + if y > 0.0 { SpinBtn::Up - } else if *y < 0.0 { + } else if y < 0.0 { SpinBtn::Down } else { return Unused;