Closed UkoeHB closed 1 year ago
Incidentally, this would fix a pretty serious bug in the cyperpunk example... if you go to settings then click the spot where the 'exit game' button is in the main menu, then the app will close! (I thought the game crashed lmao)
After using the library for a few days I'm no longer convinced my solution in this issue is ideal. I do think it's necessary to have more efficient mapping to avoid iterating over all widgets, but it might be possible to do bespoke solutions via query filters instead of in bevy_lunex. For example, if you encounter perf issues due to too many clickable widgets (e.g. you have an inventory system with 1000 items, but only some of those widgets are visible), you can add a Visible
component to visible widgets.
I implemented a couple high-level filters in my initial button-clicking implementation: OnClick
callbacks and InteractionBarrier
components (which are necessary to prevent clicks falling through widget surfaces to buttons behind them - e.g. for popups where you want the background widget to stay visible but not clickable).
//todo: assumes all widgets are in MainUI
pub(crate) fn apply_on_click(
mut commands : Commands,
mouse_button_input : Res<Input<MouseButton>>,
main_ui : Query<&UiTree, With<MainUI>>,
main_cursor : Query<&Cursor, With<MainCursor>>,
barrier_widgets : Query<&Widget, With<InteractionBarrier>>,
clickable_widgets : Query<(&Widget, &Callback<OnClick>)>,
){
// check if the mouse was just clicked
if !mouse_button_input.just_pressed(MouseButton::Left) { return; }
// prep
let main_ui = main_ui.get_single().unwrap();
let main_cursor = main_cursor.get_single().unwrap();
let cursor_position = main_cursor.position_world().as_lunex(main_ui.offset);
// find top-most barrier widget under the mouse
let mut depth_limit: Option<f32> = None;
for widget in barrier_widgets.iter()
{
// check visibility
let widget_branch = widget.fetch(&main_ui).unwrap();
if !widget_branch.is_visible() { continue; }
// check if barrier widget is above the top-most barrier widget under the mouse
let widget_depth = widget_branch.get_depth();
if let Some(top) = depth_limit { if top > widget_depth { continue; } }
// check if widget is under the mouse
if !widget.contains_position(&main_ui, &cursor_position).unwrap() { continue; }
// save this widget as best barrier
depth_limit = Some(widget_depth);
}
// give up if click is not on an interaction barrier
let Some(mut depth_limit) = depth_limit else { return; };
// find top-most clickable widget
let mut top_callback: Option<&Callback<OnClick>> = None;
for (widget, onclick_callback) in clickable_widgets.iter()
{
// check visibility
let widget_branch = widget.fetch(&main_ui).unwrap();
if !widget_branch.is_visible() { continue; }
// check if widget is above the current depth limit
let widget_depth = widget_branch.get_depth();
if depth_limit > widget_depth { continue; }
// check if widget is under the mouse
if !widget.contains_position(&main_ui, &cursor_position).unwrap() { continue; }
// save this widget as best candidate
depth_limit = widget_depth;
top_callback = Some(onclick_callback);
}
// queue onclick callback for topmost visible widget under the click
if let Some(callback) = top_callback { commands.add(callback.clone()) }
}
The cyberpunk example has this code, where you iterate over all widgets to detect which ones are underneath the cursor. I'd like to propose a more efficient approach.
New rules: 1) Only widgets at the same level in a tree are sorted. A child widget of one branch is always sorted higher than a child widget of another branch if the first branch is sorted above the second branch. (this may already be a rule idk) 2) The bounding box of a widget always automatically expands/shrinks to encompass all child widgets.
Now, to identify which widget is under the cursor, you find the highest-sorted widget at the base of the tree that contains the cursor. Then the highest sorted child widget of that widget under the cursor, and so on until you reach the terminal widget under the cursor. If that widget does not 'interact' with the cursor state (is non-blocking [e.g. invisible], has no registered handlers for the cursor state [e.g. an invisible list widget may respond to mouse wheel events but wants clicks and hovers to fall through], and does not have any rules propagating the cursor state to parent widgets [this may be redundant if you need a handler to propagate to parents]), then backtrack to the next candidate. Continue back-tracking until you find a viable candidate to consume the cursor state/event.
Re: 'propagating cursor interactions to parent widgets' Since only the most-viable candidate (the top-most widget under the cursor) is selected for cursor interactions, if you want multiple widgets to react to a cursor (e.g. a stack of widgets that are affected by cursor hover or click), then we need a way to propagate cursor interactions to associated widgets. I think you could have 'interaction bundles' or maybe 'interaction channels' or maybe just entity relations that propagate cursor events as in bevy_event_listener.
To round off this proposal, you could register event handlers to each widget and implement an event handling algorithm to do all of the above automatically.