nicopap / ui-navigation

A UI navigation algoritm implemented in rust currently targeting the bevy ui library
97 stars 13 forks source link

'Entity with `Focused` component must also have a `Focusable` component error when adding Focusable to non-UI element #34

Open PetoMPP opened 1 year ago

PetoMPP commented 1 year ago

Hey, I encountered a problem some days ago when trying to make sprite entities with Focusable. The bundle I'm trying to spawn looks as following:

#[derive(Bundle)]
pub struct FighterBundle {
    fighter: Fighter,
    style: FighterStyle,
    sprite: SpriteBundle,
    focusable: Focusable,
}

Unfortunately, after spawning the bundle and moving mouse over game window program panics with thread 'Compute Task Pool (0)' panicked at 'Entity with Focusedcomponent must also have a Focusable component: QueryDoesNotMatch(3v0)', ...\bevy-ui-navigation-0.24.0\src\systems.rs:396:56. This issue happens regardless of existence of FocusableButtons in the World or input_mapping.focus_follows_mouse value. During the testing I noticed that the entity spawned with bundle has the Focusable component, but after mouse move it's replaced by Focused component instead of added to it.

nicopap commented 1 year ago

Hello. I see what's going on here.

For a sprite-based focus system, you need to do a bit more work than the base UI case.

You can see an example implementation for handling mouse input in the infinite_upgrades.rs example:

https://github.com/nicopap/ui-navigation/blob/7b8d54f5120c5e0c8a5d2baf6e734a09cb9b75ac/examples/infinite_upgrades.rs#L187-L245

You need to add it as a system as follow:

https://github.com/nicopap/ui-navigation/blob/7b8d54f5120c5e0c8a5d2baf6e734a09cb9b75ac/examples/infinite_upgrades.rs#L20-L38

To be fair, the documentation is very misleading on this point. So I'll leave this issue open as a reminder to update the docs to fit the way you are supposed to be using bevy-ui-navigation.

Also note you can't both have ui navigation and sprite navigation plugin running at the same time. This should be an easy fix, but I never came across the need, so I didn't realize this is a needed feature…

Make sure to not add DefaultNavigationPlugins, but instead NavigationPlugin::new(), and manually add the input handling as shown in infinite_upgrade

PetoMPP commented 1 year ago

Thanks a lot for the reply! I managed to apply the changes so it works now, but I had to navigate between entities and buttons, so I had come up with following mouse handling system, the rest is done as in the snippets you've provided. This solution doesn't care about camera movement and is using Resource to store last cursor location. It also uses Sizeable component, which is just a trait implementation for ScreenSize. It's not definitely most optimized approach, but it's doing its job ;)

fn mouse_pointer_system(
    primary_windows: Query<&Window, With<PrimaryWindow>>,
    cameras: Query<(&GlobalTransform, &Camera)>,
    mut last_location: ResMut<LastPointerLocation>,
    mouse_buttons: Res<Input<MouseButton>>,
    focusables: Query<(
        &GlobalTransform,
        Option<&Sizeable>,
        Option<&Node>,
        Entity,
        &Focusable,
    )>,
    focused: Query<Entity, With<Focused>>,
    mut nav_cmds: EventWriter<NavRequest>,
) {
    let (camera_transform, camera) = cameras.single();
    let window = primary_windows.single();
    if let Some(cursor_position) = window.cursor_position() {
        let just_released = mouse_buttons.just_released(MouseButton::Left);
        match cursor_position == **last_location {
            true => {
                if !just_released {
                    return;
                }
            }
            false => **last_location = cursor_position,
        }
        if let Some(cursor_position_world) =
            camera.viewport_to_world_2d(camera_transform, cursor_position)
        {
            let focused = match focused.get_single() {
                Ok(c) => c,
                Err(_) => return,
            };
            let cursor_position = Vec2::new(cursor_position.x, window.height() - cursor_position.y);
            let under_mouse = focusables
                .iter()
                .filter(|(_, _, _, _, f)| f.state() != FocusState::Blocked)
                .filter(|(t, s, n, _, _)| {
                    if let Some(sizeable) = s {
                        if is_in_sizeable(cursor_position_world, t, *sizeable) {
                            return true;
                        }
                    }
                    if let Some(node) = n {
                        if is_in_sizeable(cursor_position, t, *node) {
                            return true;
                        }
                    }
                    false
                })
                .max_by_key(|elem| FloatOrd(elem.0.translation().z))
                .map(|(_, _, _, e, _)| e);

            let to_target = match under_mouse {
                Some(c) => c,
                None => return,
            };

            if just_released {
                nav_cmds.send(NavRequest::Action);
            }
            if to_target != focused {
                nav_cmds.send(NavRequest::FocusOn(to_target));
            }
        }
    }
}