panel: per-element interactibility

This commit is contained in:
galister
2025-10-31 17:32:10 +09:00
parent 01d11e8485
commit fa562f7b12
19 changed files with 423 additions and 361 deletions

View File

@@ -1,6 +1,6 @@
use crate::{
animation::{Animation, AnimationEasing},
components::{self, Component, ComponentBase, ComponentTrait, InitData, tooltip::ComponentTooltip},
components::{self, tooltip::ComponentTooltip, Component, ComponentBase, ComponentTrait, InitData},
drawing::{self, Boundary, Color},
event::{CallbackDataCommon, EventListenerCollection, EventListenerID, EventListenerKind},
i18n::Translation,
@@ -10,10 +10,10 @@ use crate::{
util::centered_matrix,
},
widget::{
ConstructEssentials, EventResult, WidgetData,
label::{WidgetLabel, WidgetLabelParams},
rectangle::{WidgetRectangle, WidgetRectangleParams},
util::WLength,
ConstructEssentials, EventResult, WidgetData,
},
};
use glam::{Mat4, Vec3};

View File

@@ -8,14 +8,14 @@ use std::{
use crate::{
animation::Animations,
components::{Component, InitData},
drawing::{self, ANSI_BOLD_CODE, ANSI_RESET_CODE, Boundary, push_scissor_stack, push_transform_stack},
drawing::{self, push_scissor_stack, push_transform_stack, Boundary, ANSI_BOLD_CODE, ANSI_RESET_CODE},
event::{self, CallbackDataCommon, EventAlterables},
globals::WguiGlobals,
widget::{self, EventParams, EventResult, WidgetObj, WidgetState, div::WidgetDiv},
widget::{self, div::WidgetDiv, EventParams, EventResult, WidgetObj, WidgetState},
};
use glam::{Vec2, vec2};
use slotmap::{HopSlotMap, SecondaryMap, new_key_type};
use glam::{vec2, Vec2};
use slotmap::{new_key_type, HopSlotMap, SecondaryMap};
use taffy::{NodeId, TaffyTree, TraversePartialTree};
new_key_type! {
@@ -328,26 +328,21 @@ impl Layout {
)
}
fn push_event_children<U1: 'static, U2: 'static>(
fn push_event_children<'a, U1: 'static, U2: 'static>(
&self,
parent_node_id: taffy::NodeId,
event: &event::Event,
event_result: &mut EventResult,
alterables: &mut EventAlterables,
user_data: &mut (&mut U1, &mut U2),
user_data: &mut (&'a mut U1, &'a mut U2),
reverse: bool,
) -> anyhow::Result<EventResult> {
let mut event_result = EventResult::Pass;
) -> anyhow::Result<()> {
let count = self.state.tree.child_count(parent_node_id);
let mut iter = |idx: usize| -> anyhow::Result<bool> {
let child_id = self.state.tree.get_child_id(parent_node_id, idx);
let child_result = self.push_event_widget(child_id, event, alterables, user_data, false)?;
if child_result != EventResult::Pass {
event_result = child_result;
return Ok(true);
}
Ok(false)
self.push_event_widget(child_id, event, event_result, alterables, user_data, false)?;
Ok(!event_result.can_propagate())
};
if reverse {
@@ -364,17 +359,18 @@ impl Layout {
}
}
Ok(event_result)
Ok(())
}
fn push_event_widget<U1: 'static, U2: 'static>(
fn push_event_widget<'a, U1: 'static, U2: 'static>(
&self,
node_id: taffy::NodeId,
event: &event::Event,
event_result: &mut EventResult,
alterables: &mut EventAlterables,
user_data: &mut (&mut U1, &mut U2),
user_data: &mut (&'a mut U1, &'a mut U2),
is_root_node: bool,
) -> anyhow::Result<EventResult> {
) -> anyhow::Result<()> {
let l = self.state.tree.layout(node_id)?;
let Some(widget_id) = self.state.tree.get_node_context(node_id).copied() else {
anyhow::bail!("invalid widget ID");
@@ -410,9 +406,9 @@ impl Layout {
let reverse_iter = is_root_node;
// check children first
let mut evt_result = self.push_event_children(node_id, event, alterables, user_data, reverse_iter)?;
self.push_event_children(node_id, event, event_result, alterables, user_data, reverse_iter)?;
if evt_result == EventResult::Pass {
if event_result.can_propagate() {
let mut params = EventParams {
state: &self.state,
layout: l,
@@ -421,10 +417,7 @@ impl Layout {
style,
};
let this_evt_result = widget.process_event(widget_id, node_id, event, user_data, &mut params)?;
if this_evt_result != EventResult::Pass {
evt_result = this_evt_result;
}
widget.process_event(widget_id, node_id, event, event_result, user_data, &mut params)?;
}
if scissor_pushed {
@@ -432,7 +425,7 @@ impl Layout {
}
alterables.transform_stack.pop();
Ok(evt_result)
Ok(())
}
pub const fn check_toggle_needs_redraw(&mut self) -> bool {
@@ -458,12 +451,19 @@ impl Layout {
event: &event::Event,
user1: &mut U1,
user2: &mut U2,
) -> anyhow::Result<()> {
) -> anyhow::Result<EventResult> {
let mut alterables = EventAlterables::default();
let _event_result =
self.push_event_widget(self.tree_root_node, event, &mut alterables, &mut (user1, user2), true)?;
let mut event_result = EventResult::NoHit;
self.push_event_widget(
self.tree_root_node,
event,
&mut event_result,
&mut alterables,
&mut (user1, user2),
true,
)?;
self.process_alterables(alterables)?;
Ok(())
Ok(event_result)
}
pub fn new(globals: WguiGlobals, params: &LayoutParams) -> anyhow::Result<Self> {
@@ -485,7 +485,10 @@ impl Layout {
&mut state.widgets,
&mut state.nodes,
None, // no parent
WidgetDiv::create(),
WidgetState {
interactable: false,
..WidgetDiv::create()
},
taffy::Style {
size,
..Default::default()
@@ -497,7 +500,10 @@ impl Layout {
&mut state.widgets,
&mut state.nodes,
Some(tree_root_node),
WidgetDiv::create(),
WidgetState {
interactable: false,
..WidgetDiv::create()
},
taffy::Style {
size,
..Default::default()

View File

@@ -8,7 +8,7 @@ mod widget_rectangle;
mod widget_sprite;
use crate::{
assets::{AssetPath, AssetPathOwned, normalize_path},
assets::{normalize_path, AssetPath, AssetPathOwned},
components::{Component, ComponentWeak},
drawing::{self},
globals::WguiGlobals,
@@ -782,6 +782,14 @@ fn parse_widget_universal(ctx: &mut ParserContext, widget_id: WidgetID, attribs:
// Attach a specific widget to name-ID map (just like getElementById)
ctx.insert_id(&pair.value, widget_id);
}
"interactable" => {
if let Ok(0) = &pair.value.parse::<i32>() {
log::info!("setting {widget_id:?} to noninteractable.");
ctx.layout.state.widgets.get(widget_id).unwrap().state().interactable = false;
} else {
print_invalid_attrib(&pair.attrib, &pair.value);
}
}
_ => {}
}
}
@@ -1073,4 +1081,4 @@ fn parse_document_root(
}
Ok(())
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
layout::WidgetID,
parser::{AttribPair, ParserContext, ParserFile, parse_children, parse_widget_universal, style::parse_style},
parser::{parse_children, parse_widget_universal, style::parse_style, AttribPair, ParserContext, ParserFile},
widget::div::WidgetDiv,
};
@@ -19,4 +19,4 @@ pub fn parse_widget_div<'a>(
parse_children(file, ctx, node, widget.id)?;
Ok(widget.id)
}
}

View File

@@ -10,7 +10,7 @@ use crate::{
globals::Globals,
i18n::{I18n, Translation},
layout::WidgetID,
renderer_vk::text::{FONT_SYSTEM, TextStyle},
renderer_vk::text::{TextStyle, FONT_SYSTEM},
};
use super::{WidgetObj, WidgetState};

View File

@@ -7,7 +7,7 @@ use crate::{
drawing::{self, PrimitiveExtent},
event::{
self, CallbackData, CallbackDataCommon, CallbackMetadata, Event, EventAlterables, EventListenerCollection,
EventListenerKind::{InternalStateChange, MouseEnter, MouseLeave, MouseMotion, MousePress, MouseRelease},
EventListenerKind::{self, InternalStateChange, MouseLeave},
MouseWheelEvent,
},
layout::{Layout, LayoutState, WidgetID},
@@ -78,6 +78,7 @@ pub struct WidgetState {
pub data: WidgetData,
pub obj: Box<dyn WidgetObj>,
pub event_listeners: EventListenerCollection,
pub interactable: bool,
}
impl WidgetState {
@@ -100,6 +101,7 @@ impl WidgetState {
},
obj,
event_listeners: EventListenerCollection::default(),
interactable: true,
}
}
}
@@ -164,10 +166,27 @@ pub struct EventParams<'a> {
pub layout: &'a taffy::Layout,
}
#[derive(Eq, PartialEq)]
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd)]
pub enum EventResult {
Pass, // widget acknowledged it and allows the event to pass further
Consumed, // widget triggered an action, do not pass further
NoHit, // event was pushed but has not found a listener (yet)
Pass, // widget acknowledged it and allows the event to propagate further
Consumed, // widget triggered an action, do not propagate further
}
impl EventResult {
#[must_use]
pub const fn can_propagate(self) -> bool {
!matches!(self, EventResult::Consumed)
}
#[must_use]
pub fn merge(self, other: Self) -> Self {
if self > other {
self
} else {
other
}
}
}
fn get_scroll_enabled(style: &taffy::Style) -> (bool, bool) {
@@ -212,30 +231,46 @@ impl dyn WidgetObj {
}
}
macro_rules! call_event {
($self:ident, $widget_id:ident, $node_id:ident, $params:ident, $kind:ident, $u1:ty, $u2:ty, $user_data:expr, $metadata:expr) => {
for listener in $self.event_listeners.iter_filtered::<$u1, $u2>($kind) {
let mut data = CallbackData {
obj: $self.obj.as_mut(),
widget_data: &mut $self.data,
$widget_id,
$node_id,
metadata: $metadata,
};
let mut common = CallbackDataCommon {
state: $params.state,
alterables: $params.alterables,
};
let result = listener.call_with(&mut common, &mut data, $user_data)?;
if result == EventResult::Consumed {
return Ok(EventResult::Consumed);
}
}
};
struct InvokeData<'a, 'b, U1: 'static, U2: 'static> {
widget_id: WidgetID,
node_id: taffy::NodeId,
event_result: &'a mut EventResult,
user_data: &'a mut (&'b mut U1, &'b mut U2),
params: &'a mut EventParams<'a>,
}
impl WidgetState {
fn invoke_listeners<U1: 'static, U2: 'static>(
&mut self,
call_data: &mut InvokeData<'_, '_, U1, U2>,
kind: event::EventListenerKind,
metadata: CallbackMetadata,
) -> anyhow::Result<()> {
let mut data = CallbackData {
obj: self.obj.as_mut(),
widget_data: &mut self.data,
widget_id: call_data.widget_id,
node_id: call_data.node_id,
metadata,
};
let mut common = CallbackDataCommon {
state: call_data.params.state,
alterables: call_data.params.alterables,
};
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)?;
// 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);
if !call_data.event_result.can_propagate() {
break;
}
}
Ok(())
}
pub fn get_scroll_shift_smooth(&self, info: &ScrollbarInfo, l: &taffy::Layout, timestep_alpha: f32) -> (Vec2, bool) {
let currently_animating = self.data.scrolling_cur != self.data.scrolling_cur_prev;
@@ -384,53 +419,48 @@ impl WidgetState {
true
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::cognitive_complexity)]
pub fn process_event<'a, U1: 'static, U2: 'static>(
pub fn process_event<'a, 'b, U1: 'static, U2: 'static>(
&mut self,
widget_id: WidgetID,
node_id: taffy::NodeId,
event: &Event,
user_data: &mut (&mut U1, &mut U2),
event_result: &'a mut EventResult,
user_data: &'a mut (&'b mut U1, &'b mut U2),
params: &'a mut EventParams<'a>,
) -> anyhow::Result<EventResult> {
) -> anyhow::Result<()> {
let hovered = event.test_mouse_within_transform(params.alterables.transform_stack.get());
let mut invoke_data = InvokeData {
widget_id,
node_id,
event_result,
user_data,
params,
};
match &event {
Event::MouseDown(e) => {
if hovered && self.data.set_device_pressed(e.device, true) {
call_event!(
self,
widget_id,
node_id,
params,
MousePress,
U1,
U2,
user_data,
self.invoke_listeners(
&mut invoke_data,
EventListenerKind::MousePress,
CallbackMetadata::MouseButton(event::MouseButton {
index: e.index,
pos: e.pos
})
);
pos: e.pos,
}),
)?;
}
}
Event::MouseUp(e) => {
if self.data.set_device_pressed(e.device, false) {
call_event!(
self,
widget_id,
node_id,
params,
MouseRelease,
U1,
U2,
user_data,
self.invoke_listeners(
&mut invoke_data,
EventListenerKind::MouseRelease,
CallbackMetadata::MouseButton(event::MouseButton {
index: e.index,
pos: e.pos,
})
);
}),
)?;
}
}
Event::MouseMotion(e) => {
@@ -438,79 +468,42 @@ impl WidgetState {
if hover_state_changed {
if self.data.is_hovered() {
call_event!(
self,
widget_id,
node_id,
params,
MouseEnter,
U1,
U2,
user_data,
CallbackMetadata::None
);
self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseEnter, CallbackMetadata::None)?;
} else {
call_event!(
self,
widget_id,
node_id,
params,
MouseLeave,
U1,
U2,
user_data,
CallbackMetadata::None
);
self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseLeave, CallbackMetadata::None)?;
}
} else if hovered {
self.invoke_listeners(
&mut invoke_data,
EventListenerKind::MouseMotion,
CallbackMetadata::MousePosition(event::MousePosition { pos: e.pos }),
)?;
call_event!(
self,
widget_id,
node_id,
params,
MouseMotion,
U1,
U2,
user_data,
CallbackMetadata::MousePosition(event::MousePosition { pos: e.pos })
);
if self.interactable {
*invoke_data.event_result = invoke_data.event_result.merge(EventResult::Pass);
}
}
}
Event::MouseWheel(e) => {
if hovered && self.process_wheel(params, e) {
return Ok(EventResult::Consumed);
if hovered && self.process_wheel(invoke_data.params, e) {
*invoke_data.event_result = EventResult::Consumed;
return Ok(());
}
}
Event::MouseLeave(e) => {
if self.data.set_device_hovered(e.device, false) {
call_event!(
self,
widget_id,
node_id,
params,
MouseLeave,
U1,
U2,
user_data,
CallbackMetadata::None
);
self.invoke_listeners(&mut invoke_data, MouseLeave, CallbackMetadata::None)?;
}
}
Event::InternalStateChange(e) => {
call_event!(
self,
widget_id,
node_id,
params,
self.invoke_listeners(
&mut invoke_data,
InternalStateChange,
U1,
U2,
user_data,
CallbackMetadata::Custom(e.metadata)
);
CallbackMetadata::Custom(e.metadata),
)?;
}
}
Ok(EventResult::Pass)
Ok(())
}
}

View File

@@ -7,8 +7,8 @@ use crate::{
drawing::{self, PrimitiveExtent},
layout::WidgetID,
renderer_vk::text::{
DEFAULT_METRICS, FONT_SYSTEM,
custom_glyph::{CustomGlyph, CustomGlyphData},
DEFAULT_METRICS, FONT_SYSTEM,
},
};