wgui: pass motion events further, add consume_mouse_events parameter to widgets (Closes #306), always reverse iter events

This commit is contained in:
Aleksander
2025-12-18 22:02:17 +01:00
parent 5463b6490d
commit 9dbd86db39
12 changed files with 124 additions and 61 deletions

View File

@@ -3,6 +3,7 @@
<elements>
<div
consume_mouse_events="1"
id="root"
new_pass="1"
width="100%"

View File

@@ -6,6 +6,7 @@
and WINDOW_DECORATION_HEADER_HEIGHT in the source code accordingly
-->
<rectangle
consume_mouse_events="1"
flex_direction="column"
round="16"
border="2"

View File

@@ -66,6 +66,10 @@ _They can be used in any widget/component._
_Set to 0 if you want to exclude this widget from altering the event state_
`consume_mouse_events`: "1" | "0"
_Used in case of overlapping pop-ups or windows, most notably applied to various backgrounds_
`new_pass`: "1" | "0"
_Set to 1 if you want to render overlapping pop-ups to properly render your widgets in order. Wgui renders with as few Vulkan drawcalls as possible, so this is your responsibility._

View File

@@ -127,7 +127,7 @@ impl Color {
}
#[must_use]
pub fn with_alpha(&self, n: f32) -> 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);
}

View File

@@ -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 {

View File

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

View File

@@ -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() }))
}
}

View File

@@ -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.

View File

@@ -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<dyn WidgetObj>,
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<dyn WidgetObj>) -> Self {
fn new(flags: WidgetStateFlags, obj: Box<dyn WidgetObj>) -> 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<U1: 'static, U2: 'static>(
&mut self,
call_data: &mut InvokeData<'_, '_, U1, U2>,
kind: event::EventListenerKind,
metadata: CallbackMetadata,
) -> anyhow::Result<()> {
) -> anyhow::Result<InvokeListenersResult> {
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::<U1, U2>(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<InvokeListenersResult> = 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(())
}
}

View File

@@ -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(),
}),
)
}
}

View File

@@ -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) {

View File

@@ -50,7 +50,7 @@ impl<T> OverlayWindowData<T> {
}
}
#[derive(Debug, Clone, Copy, IntegerId, PartialEq)]
#[derive(Debug, Clone, Copy, IntegerId, PartialEq, Eq)]
pub enum OverlayCategory {
Internal,
Keyboard,