From 5463b6490d06147dc65845f3bc297eb3f9fedc29 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Thu, 18 Dec 2025 20:54:19 +0100 Subject: [PATCH 1/2] wgui: fix stuck mouse release animation state (Closes #296) --- wgui/src/components/button.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/wgui/src/components/button.rs b/wgui/src/components/button.rs index 985dce5..c185220 100644 --- a/wgui/src/components/button.rs +++ b/wgui/src/components/button.rs @@ -343,16 +343,6 @@ fn register_event_mouse_release( state.sticky_down = !state.sticky_down; } - anim_hover( - rect, - event_data.widget_data, - &state.colors, - common.state.get_node_boundary(event_data.node_id), - 1.0, - false, - state.sticky_down, - ); - common.alterables.trigger_haptics(); common.alterables.mark_redraw(); @@ -362,6 +352,16 @@ fn register_event_mouse_release( if state.hovered && let Some(on_click) = &state.on_click { + anim_hover( + rect, + event_data.widget_data, + &state.colors, + common.state.get_node_boundary(event_data.node_id), + 1.0, + false, + state.sticky_down, + ); + on_click(common, ButtonClickEvent {})?; } Ok(EventResult::Consumed) From 9dbd86db39f734c0d2034570b3eb6911fc01cae6 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Thu, 18 Dec 2025 22:02:17 +0100 Subject: [PATCH 2/2] wgui: pass motion events further, add `consume_mouse_events` parameter to widgets (Closes #306), always reverse iter events --- .../assets/gui/view/popup_window.xml | 1 + wgui/assets/wgui/window_frame.xml | 1 + wgui/doc/widgets.md | 4 + wgui/src/drawing.rs | 4 +- wgui/src/layout.rs | 33 ++++---- wgui/src/parser/mod.rs | 11 ++- wgui/src/widget/div.rs | 4 +- wgui/src/widget/label.rs | 16 ++-- wgui/src/widget/mod.rs | 84 ++++++++++++++----- wgui/src/widget/rectangle.rs | 13 +-- wgui/src/widget/sprite.rs | 12 ++- wlx-overlay-s/src/windowing/window.rs | 2 +- 12 files changed, 124 insertions(+), 61 deletions(-) diff --git a/dash-frontend/assets/gui/view/popup_window.xml b/dash-frontend/assets/gui/view/popup_window.xml index 8d2cf37..011895d 100644 --- a/dash-frontend/assets/gui/view/popup_window.xml +++ b/dash-frontend/assets/gui/view/popup_window.xml @@ -3,6 +3,7 @@
Self { + pub const fn with_alpha(&self, n: f32) -> Self { Self { r: self.r, g: self.g, @@ -267,7 +267,7 @@ fn draw_widget( let mut widget_state = widget.state(); - if widget_state.new_pass { + if widget_state.flags.new_pass { state.primitives.push(RenderPrimitive::NewPass); } diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs index b6dcd66..5a7174b 100644 --- a/wgui/src/layout.rs +++ b/wgui/src/layout.rs @@ -11,7 +11,7 @@ use crate::{ drawing::{self, ANSI_BOLD_CODE, ANSI_RESET_CODE, Boundary, push_scissor_stack, push_transform_stack}, event::{self, CallbackDataCommon, EventAlterables}, globals::WguiGlobals, - widget::{self, EventParams, EventResult, WidgetObj, WidgetState, div::WidgetDiv}, + widget::{self, EventParams, EventResult, WidgetObj, WidgetState, WidgetStateFlags, div::WidgetDiv}, }; use glam::{Vec2, vec2}; @@ -377,7 +377,6 @@ impl Layout { event_result: &mut EventResult, alterables: &mut EventAlterables, user_data: &mut (&'a mut U1, &'a mut U2), - reverse: bool, ) -> anyhow::Result<()> { let count = self.state.tree.child_count(parent_node_id); @@ -387,17 +386,10 @@ impl Layout { Ok(!event_result.can_propagate()) }; - if reverse { - for idx in (0..count).rev() { - if iter(idx)? { - break; - } - } - } else { - for idx in 0..count { - if iter(idx)? { - break; - } + // reverse iter + for idx in (0..count).rev() { + if iter(idx)? { + break; } } @@ -448,11 +440,8 @@ impl Layout { style, ); - // topmost widgets are iterated in reverse order - let reverse_iter = is_root_node; - // check children first - self.push_event_children(node_id, event, event_result, alterables, user_data, reverse_iter)?; + self.push_event_children(node_id, event, event_result, alterables, user_data)?; if event_result.can_propagate() { let mut params = EventParams { @@ -532,7 +521,10 @@ impl Layout { &mut state.nodes, None, // no parent WidgetState { - interactable: false, + flags: WidgetStateFlags { + interactable: false, + ..Default::default() + }, ..WidgetDiv::create() }, taffy::Style { @@ -547,7 +539,10 @@ impl Layout { &mut state.nodes, Some(tree_root_node), WidgetState { - interactable: false, + flags: WidgetStateFlags { + interactable: false, + ..Default::default() + }, ..WidgetDiv::create() }, taffy::Style { diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs index cd137ee..ea22263 100644 --- a/wgui/src/parser/mod.rs +++ b/wgui/src/parser/mod.rs @@ -831,14 +831,21 @@ fn parse_widget_universal(ctx: &mut ParserContext, widget: &WidgetPair, attribs: } "new_pass" => { if let Some(num) = parse_i32(&pair.value) { - widget.widget.state().new_pass = num != 0; + widget.widget.state().flags.new_pass = num != 0; } else { print_invalid_attrib(&pair.attrib, &pair.value); } } "interactable" => { if let Some(num) = parse_i32(&pair.value) { - widget.widget.state().interactable = num != 0; + widget.widget.state().flags.interactable = num != 0; + } else { + print_invalid_attrib(&pair.attrib, &pair.value); + } + } + "consume_mouse_events" => { + if let Some(num) = parse_i32(&pair.value) { + widget.widget.state().flags.consume_mouse_events = num != 0; } else { print_invalid_attrib(&pair.attrib, &pair.value); } diff --git a/wgui/src/widget/div.rs b/wgui/src/widget/div.rs index 55a636e..6dc5ffb 100644 --- a/wgui/src/widget/div.rs +++ b/wgui/src/widget/div.rs @@ -1,6 +1,6 @@ use slotmap::Key; -use crate::layout::WidgetID; +use crate::{layout::WidgetID, widget::WidgetStateFlags}; use super::{WidgetObj, WidgetState}; @@ -10,7 +10,7 @@ pub struct WidgetDiv { impl WidgetDiv { pub fn create() -> WidgetState { - WidgetState::new(Box::new(Self { id: WidgetID::null() })) + WidgetState::new(WidgetStateFlags::default(), Box::new(Self { id: WidgetID::null() })) } } diff --git a/wgui/src/widget/label.rs b/wgui/src/widget/label.rs index ef73ecd..9f7465e 100644 --- a/wgui/src/widget/label.rs +++ b/wgui/src/widget/label.rs @@ -11,6 +11,7 @@ use crate::{ i18n::Translation, layout::WidgetID, renderer_vk::text::TextStyle, + widget::WidgetStateFlags, }; use super::{WidgetObj, WidgetState}; @@ -53,12 +54,15 @@ impl WidgetLabel { ); } - WidgetState::new(Box::new(Self { - params, - buffer: Rc::new(RefCell::new(buffer)), - last_boundary: Boundary::default(), - id: WidgetID::null(), - })) + WidgetState::new( + WidgetStateFlags::default(), + Box::new(Self { + params, + buffer: Rc::new(RefCell::new(buffer)), + last_boundary: Boundary::default(), + id: WidgetID::null(), + }), + ) } // set text without layout/re-render update. diff --git a/wgui/src/widget/mod.rs b/wgui/src/widget/mod.rs index 54abea7..dd118f5 100644 --- a/wgui/src/widget/mod.rs +++ b/wgui/src/widget/mod.rs @@ -76,12 +76,31 @@ impl WidgetData { } } +pub struct WidgetStateFlags { + pub interactable: bool, + + // consume any incoming mouse event which is hovered at given widget + pub consume_mouse_events: bool, + + // force a new render pass before rendering this widget + pub new_pass: bool, +} + +impl Default for WidgetStateFlags { + fn default() -> Self { + Self { + interactable: true, + consume_mouse_events: false, + new_pass: false, + } + } +} + pub struct WidgetState { pub data: WidgetData, pub obj: Box, pub event_listeners: EventListenerCollection, - pub interactable: bool, - pub new_pass: bool, // force a new render pass + pub flags: WidgetStateFlags, } impl WidgetState { @@ -91,7 +110,7 @@ impl WidgetState { (data, obj) } - fn new(obj: Box) -> Self { + fn new(flags: WidgetStateFlags, obj: Box) -> Self { Self { data: WidgetData { hovered: 0, @@ -104,8 +123,7 @@ impl WidgetState { }, obj, event_listeners: EventListenerCollection::default(), - interactable: true, - new_pass: false, + flags, } } } @@ -241,13 +259,19 @@ struct InvokeData<'a, 'b, U1: 'static, U2: 'static> { params: &'a mut EventParams<'a>, } +#[must_use] +enum InvokeListenersResult { + NobodyListened, + AtLeastOneCalled, +} + impl WidgetState { fn invoke_listeners( &mut self, call_data: &mut InvokeData<'_, '_, U1, U2>, kind: event::EventListenerKind, metadata: CallbackMetadata, - ) -> anyhow::Result<()> { + ) -> anyhow::Result { let mut data = CallbackData { obj: self.obj.as_mut(), widget_data: &mut self.data, @@ -261,13 +285,17 @@ impl WidgetState { alterables: call_data.params.alterables, }; + let mut res = InvokeListenersResult::NobodyListened; + for listener in self.event_listeners.iter_filtered::(kind) { let new_result = listener.call_with(&mut common, &mut data, call_data.user_data)?; + res = InvokeListenersResult::AtLeastOneCalled; // Consider all listeners on this widget, even if we had a Consume. // Store the highest value for return. *call_data.event_result = call_data.event_result.merge(new_result); } - Ok(()) + + Ok(res) } pub fn get_scroll_shift_smooth(&self, info: &ScrollbarInfo, l: &taffy::Layout, timestep_alpha: f32) -> (Vec2, bool) { @@ -449,29 +477,31 @@ impl WidgetState { params, }; + let mut res: Option = None; + match &event { Event::MouseDown(e) => { if hovered && self.data.set_device_pressed(e.device, true) { - self.invoke_listeners( + res = Some(self.invoke_listeners( &mut invoke_data, EventListenerKind::MousePress, CallbackMetadata::MouseButton(event::MouseButton { index: e.index, pos: e.pos, }), - )?; + )?); } } Event::MouseUp(e) => { if self.data.set_device_pressed(e.device, false) { - self.invoke_listeners( + res = Some(self.invoke_listeners( &mut invoke_data, EventListenerKind::MouseRelease, CallbackMetadata::MouseButton(event::MouseButton { index: e.index, pos: e.pos, }), - )?; + )?); } } Event::MouseMotion(e) => { @@ -479,18 +509,20 @@ impl WidgetState { if hover_state_changed { if self.data.is_hovered() { - self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseEnter, CallbackMetadata::None)?; + res = + Some(self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseEnter, CallbackMetadata::None)?); } else { - self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseLeave, CallbackMetadata::None)?; + res = + Some(self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseLeave, CallbackMetadata::None)?); } - } else if hovered { - self.invoke_listeners( + } else { + res = Some(self.invoke_listeners( &mut invoke_data, EventListenerKind::MouseMotion, CallbackMetadata::MousePosition(event::MousePosition { pos: e.pos }), - )?; + )?); - if self.interactable { + if self.flags.interactable { *invoke_data.event_result = invoke_data.event_result.merge(EventResult::Pass); } } @@ -503,17 +535,29 @@ impl WidgetState { } Event::MouseLeave(e) => { if self.data.set_device_hovered(e.device, false) { - self.invoke_listeners(&mut invoke_data, MouseLeave, CallbackMetadata::None)?; + res = Some(self.invoke_listeners(&mut invoke_data, MouseLeave, CallbackMetadata::None)?); } } Event::InternalStateChange(e) => { - self.invoke_listeners( + res = Some(self.invoke_listeners( &mut invoke_data, InternalStateChange, CallbackMetadata::Custom(e.metadata), - )?; + )?); } } + + if let Some(res) = res { + match res { + InvokeListenersResult::NobodyListened => { + if hovered && self.flags.consume_mouse_events { + *invoke_data.event_result = EventResult::Consumed; + } + } + InvokeListenersResult::AtLeastOneCalled => {} + } + } + Ok(()) } } diff --git a/wgui/src/widget/rectangle.rs b/wgui/src/widget/rectangle.rs index 6d462f0..e10778f 100644 --- a/wgui/src/widget/rectangle.rs +++ b/wgui/src/widget/rectangle.rs @@ -3,7 +3,7 @@ use slotmap::Key; use crate::{ drawing::{self, GradientMode, PrimitiveExtent}, layout::WidgetID, - widget::util::WLength, + widget::{WidgetStateFlags, util::WLength}, }; use super::{WidgetObj, WidgetState}; @@ -27,10 +27,13 @@ pub struct WidgetRectangle { impl WidgetRectangle { pub fn create(params: WidgetRectangleParams) -> WidgetState { - WidgetState::new(Box::new(Self { - params, - id: WidgetID::null(), - })) + WidgetState::new( + WidgetStateFlags::default(), + Box::new(Self { + params, + id: WidgetID::null(), + }), + ) } } diff --git a/wgui/src/widget/sprite.rs b/wgui/src/widget/sprite.rs index 08f362f..c344b28 100644 --- a/wgui/src/widget/sprite.rs +++ b/wgui/src/widget/sprite.rs @@ -11,6 +11,7 @@ use crate::{ DEFAULT_METRICS, custom_glyph::{CustomGlyph, CustomGlyphData}, }, + widget::WidgetStateFlags, }; use super::{WidgetObj, WidgetState}; @@ -29,10 +30,13 @@ pub struct WidgetSprite { impl WidgetSprite { pub fn create(params: WidgetSpriteParams) -> WidgetState { - WidgetState::new(Box::new(Self { - params, - id: WidgetID::null(), - })) + WidgetState::new( + WidgetStateFlags::default(), + Box::new(Self { + params, + id: WidgetID::null(), + }), + ) } pub fn set_color(&mut self, color: drawing::Color) { diff --git a/wlx-overlay-s/src/windowing/window.rs b/wlx-overlay-s/src/windowing/window.rs index 556aa9e..afd8668 100644 --- a/wlx-overlay-s/src/windowing/window.rs +++ b/wlx-overlay-s/src/windowing/window.rs @@ -50,7 +50,7 @@ impl OverlayWindowData { } } -#[derive(Debug, Clone, Copy, IntegerId, PartialEq)] +#[derive(Debug, Clone, Copy, IntegerId, PartialEq, Eq)] pub enum OverlayCategory { Internal, Keyboard,