rust-windowing / winit

Window handling library in pure Rust
https://docs.rs/winit/
Apache License 2.0
4.84k stars 902 forks source link

Borderless fullscreen mode in X11 "focus toggle" behavior; focus is lost when switching workspaces #2841

Open EspenHa opened 1 year ago

EspenHa commented 1 year ago

Problem

This problem originally appeared in neovide, see https://github.com/neovide/neovide/issues/1488 . I'm not completely sure that the issue I'm reporting here is the same as what others have reported. But my issue is at least 100% reproducible on my system.

In short, the bug is that winit applications do no properly handle window switching / focus gained / focus lost, in borderless fullscreen mode in X11.

In my case, my system uses manjaro, XFCE, lightdm. I have keybindings to switch workspaces with super+{hjkl}, similarly to vim keybindings.

When switching workspace from a borderless fullscreen winit application (e.g. neovide) with super+{hjkl}, if there is another window present on the workspace I immediately switched to, then when switching back, the application will not be focused. On the other hand, if there was no windows on that workspace everything works, even if I switched to another workspace with a window and then back. Furthermore, focus would also be "toggled" back if I repeat the window switching (i.e. super+j, super+k, super+j, super+k would leave neovide in the correct state).

Minimal example

(This example is identical to https://github.com/rust-windowing/winit/blob/master/examples/key_binding.rs except that it runs in borderless fullscreen mode.)

#![allow(clippy::single_match)]

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
use winit::{
    dpi::LogicalSize,
    event::{ElementState, Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    keyboard::{Key, ModifiersState},
    // WARNING: This is not available on all platforms (for example on the web).
    platform::modifier_supplement::KeyEventExtModifierSupplement,
    window::WindowBuilder,
};

#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
fn main() {
    println!("This example is not supported on this platform");
}

#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
fn main() {
    simple_logger::SimpleLogger::new().init().unwrap();
    let event_loop = EventLoop::new();

    let _window = WindowBuilder::new()
        .with_inner_size(LogicalSize::new(400.0, 200.0))
        .build(&event_loop)
        .unwrap();

    let handle = _window.current_monitor();
    _window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(handle)));

    let mut modifiers = ModifiersState::default();

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

        match event {
            Event::WindowEvent { event, .. } => match event {
                WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
                WindowEvent::ModifiersChanged(new) => {
                    modifiers = new.state();
                }
                WindowEvent::KeyboardInput { event, .. } => {
                    if event.state == ElementState::Pressed && !event.repeat {
                        match event.key_without_modifiers().as_ref() {
                            Key::Character("1") => {
                                if modifiers.shift_key() {
                                    println!("Shift + 1 | logical_key: {:?}", event.logical_key);
                                } else {
                                    println!("1");
                                }
                            }
                            _ => (),
                        }
                    }
                }
                _ => (),
            },
            _ => (),
        };
    });
}
ntd commented 1 year ago

Here the border has nothing to do with the keybinding issue.

I just launched cargo run --example key_binding on 7500a8823065cbf4280a63072bc26c05a735e507 multiple times, sometimes the keypress works and sometimes it does not.

When it does not work, the window is focused but the example does not react when pressing 1. Clicking does not solve, but unfocusing and refocusing does.

Arch linux current on x11 (I think... loginctl show-session 2 -p Type returns Type=x11), cargo 1.70.0, xfwm 4.18.0.

emmanueltouzery commented 1 year ago

Is it maybe the same problem described in another bug, of stuck modifier keys? Try maybe to press the control, alt, and windows keys to "fix" the focus.

ntd commented 1 year ago

Is it maybe the same problem described in another bug, of stuck modifier keys? Try maybe to press the control, alt, and windows keys to "fix" the focus.

I also met this issue in neovide/neovide#1488, and only in neovide. There @fredizzimo suggested to report the results of the key_binding example here.

It is definitely a real bug, not a stuck key, a busy CPU, an USB disconnection or anything like that.

emmanueltouzery commented 1 year ago

I have definitely seen in neovide a bug that looked like a focus issue but was in fact due to a bug somewhere of modifiers not being reset (while the hardware was completely fine). I could convince myself that was the cause by seeing that pressing modifier keys fixed the issue (was an effective workaround).

ntd commented 1 year ago

The same issue is exposed by the key_binding example, where no modifiers are needed, so this rules out neovide and modifiers.

I just uploaded a video on YouTube that shows the problem: https://youtu.be/JhjIQXQCpaM

fredizzimo commented 1 year ago

The stuck modifiers is another issue but,

@emmanueltouzery, at least one variation of the stuck modifiers seems to fixed when using the latest Winit master, as confirmed here https://github.com/neovide/neovide/issues/1631. We do have other reports in the repository, but I don't think those cases have been tested with the latest Winit yet, so we don't know if there still are cases left were it does not work.

fredizzimo commented 1 year ago

I think this Neovide issue is relevant for the case that @ntd is seeing https://github.com/neovide/neovide/issues/1558

It includes a lot of details, including some workarounds by modifying Winit.

rodrigorc commented 9 months ago

I've been debugging this, and I can confirm that this is caused by EventProcessor::active_window being None when the loop is run. Focusing the window out and in again fixes it, as it does dragging the window.

The simples workaround I've found is to do make the window invisible and then visible again, when using X11. This seems to refocus the window fixing the issue:

match &event {
    winit::event::Event::NewEvents(winit::event::StartCause::Init) => {
        use easy_imgui_window::winit::raw_window_handle::{
            HasWindowHandle,
            RawWindowHandle::{Xcb, Xlib},
        };
        if let Ok(h) = main_window.window_handle() {
            if matches!(h.as_raw(), Xcb(_) | Xlib(_)) {
                w.set_visible(false);
                w.set_visible(true);
            }
        }
    }
fredizzimo commented 5 months ago

It has been confirmed that this is fixed on the Neovide side. But I'm not sure what fixed it.

rodrigorc commented 2 months ago

I can confirm that version 0.30 has fixed this issue. The public API is quite different, so not all projects will be able to switch quickly, though.

madsmtm commented 2 months ago

I'll close the issue then, thanks for reporting it!

rodrigorc commented 2 months ago

I think I was wrong, it is not fully fixed, I'm using 0.30.5 in Linux, and sometimes my window starts focused, sometimes it does not.

By tracing the events when the window is first created I'm getting:

I'm don't see any pattern with that intermittent message, it looks like a race to me.

Sorry for the noise, @madsmtm, maybe this issue should be reopened?

rodrigorc commented 2 months ago

This has been nagging me all day, I was sure it worked before...

As it happens, this issue is "window manager" dependent! I tested a few window managers, with a regular window and a program of mine, and this is the behavior:

The others reports above seem to use XFCE, (nobody uses twm) so everything points to a weird behavior (bug?) in Xfwm4.

About the fullscreen cases, my guess is that fullscreen may trigger a compositor-off mode in the WM, so same explanation.

About the cause, it looks like this happens because we are not getting the proper xinput2::XI_FocusIn event when the window is first activated.

The obvious solution is to listen for a regular FocusIn event:

diff --git src/platform_impl/linux/x11/window.rs src/platform_impl/linux/x11/window.rs
index ea7c0235..115ce890 100644
--- src/platform_impl/linux/x11/window.rs
+++ src/platform_impl/linux/x11/window.rs
@@ -265,7 +265,8 @@ impl UnownedWindow {
                 | EventMask::BUTTON_PRESS
                 | EventMask::BUTTON_RELEASE
                 | EventMask::POINTER_MOTION
-                | EventMask::PROPERTY_CHANGE;
+                | EventMask::PROPERTY_CHANGE
+                | EventMask::FOCUS_CHANGE;

             aux = aux.event_mask(event_mask).border_pixel(0);
diff --git src/platform_impl/linux/x11/event_processor.rs src/platform_impl/linux/x11/event_processor.rs
index 9c6b95a2..158397ad 100644
--- src/platform_impl/linux/x11/event_processor.rs
+++ src/platform_impl/linux/x11/event_processor.rs
@@ -172,6 +172,16 @@ impl EventProcessor {
             xlib::PropertyNotify => self.property_notify(xev.as_ref(), &mut callback),
             xlib::VisibilityNotify => self.visibility_notify(xev.as_ref(), &mut callback),
             xlib::Expose => self.expose(xev.as_ref(), &mut callback),
+            xlib::FocusIn | xlib::FocusOut => {
+                let focus = event_type == xlib::FocusIn;
+                let xev: &XAnyEvent = xev.as_ref();
+                let window = xev.window as xproto::Window;
+                let window_id = mkwid(window);
+                self.with_window(window, |window| window.shared_state_lock().has_focus = focus);
+                let event = Event::WindowEvent { window_id, event: WindowEvent::Focused(focus) };
+                callback(&self.target, event);
+            }
+
             // Note that in compose/pre-edit sequences, we'll always receive KeyRelease events.
             ty @ xlib::KeyPress | ty @ xlib::KeyRelease => {
                 let state = if ty == xlib::KeyPress {

This works for Xfwm4, but it duplicates every focus message except the first one: FocusIn plus xinput2::XI_FocusIn.

It still fails for Twm and the No-WindowManager cases, though.

Another alternative, inspired by how Gtk and others work is to just listen for key presses (we are doing that anyways). If a window gets a keyboard event and it is not focused... well, it should be!

I don't know if this should be in xinput_key_input or in xinput2_raw_key_input, but the XIRawEvent struct lacks the window field, so I used the other one:

diff --git src/platform_impl/linux/x11/event_processor.rs src/platform_impl/linux/x11/event_processor.rs
index 9c6b95a2..8f1510b0 100644
@@ -886,10 +886,24 @@ fn xinput_key_input<T: 'static, F>(

         let window = match self.active_window {
             Some(window) => window,
-            None => return,
+            None => *self.active_window.insert(xev.window as xproto::Window),
         };

         let window_id = mkwid(window);
+        let focused = self.with_window(window, |window| {
+            let mut shared_state_lock = window.shared_state_lock();
+            if !shared_state_lock.has_focus {
+                shared_state_lock.has_focus = true;
+                true
+            } else {
+                false
+            }
+        });
+        if focused == Some(true) {
+            let event = Event::WindowEvent { window_id, event: WindowEvent::Focused(true) };
+            callback(&self.target, event);
+        }
+
         let device_id = mkdid(util::VIRTUAL_CORE_KEYBOARD);

         let keycode = xev.keycode as _;

This seems to work great for every case, even without a Window Manager and multiple Winit top level windows! (In this case the focus just follows the mouse).

Naturally you may consider that this is a bug in Xfwm4, and that the Twm and no-WM scenarios are not supported, thus it is not worth fixing it here. That is up to you, winit developers. I'm just glad I solved the mystery.

rodrigorc commented 2 months ago

An additional detail about this issue... I think that for the Xfwm scenario there is a race between mapping the just created window (that usually gets focused more or less immediately) and getting the XInput2::FocusIn event.

As it happens, in winit/src/platform_impl/linux/x11/window.rs, UnownedWindow::new() it does many things, among them:

Changing the order to:

That is, selecting XInput2 events before mapping the window, fixes the issue definitely, at least for Xfwm.

This doesn't fix the issue with Twm or without WM cases, though, so the focus-on-keyboard hack from my previous message is still worth considering, IMHO.