bevy_egui icon indicating copy to clipboard operation
bevy_egui copied to clipboard

Cannot set `winit` cursor icon in Bevy

Open JeanMertz opened this issue 1 year ago β€’ 6 comments

I have a relatively simple app, which has a few egui windows, and a regular Bevy 2d canvas to which I paint some shapes. I also have a system that queries &mut Window and then changes window.cursor.icon = CursorIcon::Hand.

This didn't seem to work, even though there is an actual window_settings example in Bevy that does something similar, and works as expected.

After some debugging, I noticed that disabling this plugin allows me to set the cursor in Bevy again.

I should note that when I'm setting the cursor, the pointer is not hovering over (or interacting with) any egui windows.

Is there a way to get this to work as expected?

JeanMertz avatar Nov 23 '23 12:11 JeanMertz

I should note that I tried to set the cursor using

ctx.set_cursor_icon(bevy_egui::egui::CursorIcon::PointingHand);

And I can see the cursor change for one frame, but then change back again. I can only assume this is because my non-ui system doesn't run on every tick (it doesn't need to), but egui β€” being an immediate mode GUI library β€” expects this to be called each frame, and resets it back to the default cursor if the cursor isn't explicitly changed on a given frame.

Also, aside from the resetting issue, there is also the architectural issue of my system, that doesn't involve anything related to any egui windows, now needs to know about EguiContexts, which isn't what I'd want to see happen (e.g. I want to contain the need/usage of EguiContexts to any system that explicitly deals with the egui UI parts of my app).

JeanMertz avatar Nov 23 '23 12:11 JeanMertz

I've worked around this for now by introducing a new Cursor resource, which other systems can change, and then the egui rendering system sets the correct cursor on every frame.

It works, but it might be considered a workaround, not the actual solution. I'll let you be the judge of that, feel free to close this ticket if this is working as intended and the global resource-approach is the correct one.

JeanMertz avatar Nov 23 '23 12:11 JeanMertz

I'm facing the same issue. This plugin hijacks the cursor control and it becomes egui responsibility, even in windows that are non egui related. I'd argue that this is indeed a bug, since egui context should be isolated from bevy context. Not sure if this is in the radar, but would be nice to have a definitive solution for that.

lgrossi avatar Jan 16 '24 01:01 lgrossi

Also facing this! @JeanMertz I used your solution, but now egui is of course not able to change the icon when the cursor is resizing an egui window, etc. Did you have a way around this?

wiggleforlife avatar Mar 19 '24 23:03 wiggleforlife

@wiggleforlife I do not, as I haven't had a need for custom cursors in egui. One solution that might work is checking if "egui wants focus", and then not applying my above logic. There's an example in the bevy_pancam plugin on how to check if egui has focus or not:

https://github.com/johanhelsing/bevy_pancam/blob/205f07ba164e5d6602c198440f7bfe4713b336d4/src/lib.rs#L40-L58

JeanMertz avatar Mar 20 '24 11:03 JeanMertz

I did do a "workaround" too. I've added a [Resource] into bevy_egui which lets you hijack its processing of the cursor.

I cloned the bevy_egui library and added this resource:

/// What the cursor should be when it isn't over a window.
#[derive(Resource, Default)]
pub struct EguiDefaultCursor(pub bevy::prelude::CursorIcon);

You'd initialize it:

impl Plugin for EguiPlugin {
    fn build(&self, app: &mut App) {
        let world = &mut app.world;
        // ...
        world.init_resource::<EguiDefaultCursor>();
       // ...
    }
}

Then the hijacking happens here in systems.rs:

/// Reads Egui output.
pub fn process_output_system(
    // ...
    default_cursor: Res<EguiDefaultCursor>,
) {
    for mut context in contexts.iter_mut() {
        // ...

        // Check if we're over ANY egui windows with the mouse.
        let egui_using_cursor = (!{
            ctx.is_pointer_over_area() || ctx.is_using_pointer() || {
                ctx.pointer_latest_pos()
                    .map(|vec2| {
                        // Manually check if we're in the top ribbon panel
                        // (the above two checks don't sense panels).
                        vec2.y < 147.0
                    })
                    .unwrap_or_default()
            }
        })
        .then(|| default_cursor.0);

        let mut set_icon = || {
            context.window.cursor.icon = egui_using_cursor.unwrap_or_else(|| {
                egui_to_winit_cursor_icon(platform_output.cursor_icon)
                    .unwrap_or(bevy::window::CursorIcon::Default)
            });
        };
        
        // ...

        
    }
}

Likely most people won't have a top panel, so you can skip the pointer_latest_pos check.

Then in your code, you set the cursor that you want in the [EguiDefaultCursor] resource.

You do end up having bevy_egui as a pretty low level dependency. Pretty hacky but works for me.

dimvoly avatar Jul 10 '24 11:07 dimvoly