rust-windowing / winit

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

Consider options for redefining locations where the user can drag, resize, close, etc. a window #221

Closed LaylBongers closed 3 years ago

LaylBongers commented 7 years ago

This is most likely low priority but a nice-to-have for making custom styled windows using borderless windows, and still allowing the user to drag a custom rendered title bar. On windows this is implemented using WM_NCHITTEST, which allows the application to define custom areas that can do these operations.

Shookbelf commented 6 years ago

I think a more flexible solution would be to let the application start an interactive move. Wayland clients can do this.

Unfortunately it looks like Windows does only support defining an area where drag and drop is possible.

Shookbelf commented 6 years ago

On X11 it seems like this is done with the wm-spec (also called ewmh) protocol, would it be possible to use it somehow?

francesca64 commented 6 years ago

Do you just need an EWMH hint, or do you really mean a protocol?

Shookbelf commented 6 years ago

I see it's only a spec and not a protocol, so excuse my error. For allowing window movement this message is probably needed. So I think it's a hint.

Please note that I just searched for this and have no prior experience in this regard. But I want to learn more about things like these and work with them.

Do you think I could work on this a little? I do not think it's only relevant for Windows and since it has low priority there would not be too much pressure.

francesca64 commented 6 years ago

It's a client message sent to the root window; a hint is just a window property. I can't say whether or not this qualifies as a protocol without getting needlessly pedantic, but my main reason for mentioning it was because protocols can be a big pain. That's not an issue here, though.

Do you think I could work on this a little?

Feel free! Reviewing/advising other people's PRs is more fun than making my own, to be honest.

Fortunately, winit's X11 util module (authored by yours truly) contains a bunch of convenience/safety wrappers for various Xlib functions. send_client_msg is the one you want for this.

I'd say winit itself is pretty good reference material for learning how to program X11 clients, since I've generally tried to do things in maintainable ways. You can also just ask me things, since what you'll discover is that almost nobody in the past decade and a half has directly touched this stuff, and thus learning resources are super scarce.

Shookbelf commented 6 years ago

Wow, i got great consistency with my initial implementation!

On KWin the window jumps once (not continuous) but as soon as you cancel the move it jumps back.
On Gnome X11 it doesn't do anything at all. On Gnome via XWayland it works very well except that I don't get release events for secondary mouse buttons.

This is so strange. I guess this message is used for e.g. GTK CSD so it has to work somehow.

francesca64 commented 6 years ago

It's hard for me to say much without seeing the code. You can also use hint_is_supported to check if the WM advertises support for that atom (I know the function contains the word "hint", and that I said this isn't a hint, but that doesn't really matter).

I guess this message is used for e.g. GTK CSD so it has to work somehow.

If you ctrl+F for _NET_WM_MOVERESIZE in this file, you'll find some results: https://github.com/GNOME/gtk/blob/d13843ee2a5e2afabe6418cc7e9e744258a3fc5d/gdk/x11/gdksurface-x11.c

Shookbelf commented 6 years ago

Yes, I have looked at some other implementations and can't really spot a relevant difference.
This is what I implemented. I'm calling this function every time I want to start or end a move (e.g. on button and cursor events). I've tried different values for button and mask but haven't got satisfying results.

pub fn start_move(&self, logical_position: LogicalPosition, stop: bool){
    self.grab_cursor(false);
    let (x, y): (i32, i32) = logical_position.to_physical(self.get_hidpi_factor()).into();
    let (root_x, root_y): (i32, i32) = {
        let pos = self.get_inner_position_physical().unwrap();
        (pos.0 + x, pos.1 + y)
    };
    let move_resize_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_MOVERESIZE\0") };

    let button = 0; //ffi::Button1;
    let test= self.xconn.send_client_msg(
        self.xwindow,
        self.root,
        move_resize_atom,
        Some(ffi::SubstructureRedirectMask | ffi::SubstructureNotifyMask),
        [
            root_x as c_long,
            root_y as c_long,
            if stop {11} else {8} as c_long, // _NET_WM_MOVERESIZE_MOVE or _NET_WM_MOVERESIZE_CANCEL
            button as c_long,
            0,
        ],
    ).flush();
}
francesca64 commented 6 years ago

I got it to grab once (though not in a way that was good), but since then, I don't remember what example code I used to do it, and nothing else I've tried has worked... I suspect the problem is in the usage of the messages rather than the messages themselves.

Osspial commented 5 years ago

Reviving this - I'm okay in principle with this getting added, but I'd like to see an API design and/or PR that works across platforms.

Shookbelf commented 5 years ago

In principle I am open to continue my initial attempt at getting this to work on Linux (X11). Unfortunately I am stuck. I would appreciate if anyone can tell me what I am doing wrong.
After your comment, I wanted to look at it again. I looked at this spec page and my implementation again and tried a few things on KWin.

Apparently moving the window with the keyboard works fine after sending the according message. Moving with the mouse produces strange results. I suspect I am supplying the wrong mouse position or button but it should be correct. Maybe someone else can take a look at it.

Resizing from the respective window edges is analogous and should be very easy to implement after the error is found.

daxpedda commented 3 years ago

I started working on this. To explain the issue I found in @Shookbelf's code, self.grab_cursor(false); doesn't execute XUngrabPointer unless the cursor has been grabbed before, but _NET_WM_MOVERESIZE requires the use of XUngrabPointer.

So replacing self.grab_cursor(false); with:

unsafe {
    (self.xconn.xlib.XUngrabPointer)(self.xconn.display, ffi::CurrentTime);
}
self.xconn.flush_requests().unwrap();

fixes this.

@Osspial I would really love to make a PR for this, I'm working on implementing this for Wayland, MacOS and Windows too, but I can't find a way to expose a neat cross-platform API.

The following summary is for an API that only supports moving the window (no resizing) with holding down a mouse button (no keyboard input), and only tested on X11 (testing on Windows incoming, MacOS and Wayland testing has to be done by others):

Wayland Uses wl_shell_surface::move.

X11 Uses _NET_WM_MOVERESIZE_MOVE to tell X11 that the last mouse button pressed should start moving the window until it is released again. _NET_WM_MOVERESIZE_CANCEL should be moved to cancel it, because some buttons don't cancel automatically.

MacOS Uses NSWindow::performWindowDragWithEvent.

Windows Uses WM_NCHITTEST or WM_NCLBUTTONDOWN.

Limitations Feature Wayland X11 MacOS Windows
only left button
only immediately after button press
only stops when released
may prevent button release event
undos cursor grab

So my current approach is to just document all these caveats and let users deal with it, as I don't see an easy way to force this to be used correctly.

PR incoming.

daxpedda commented 3 years ago

See https://github.com/rust-windowing/winit/pull/1840.

maroider commented 3 years ago

There is one thing here that #1840 doesn't address, and that's user-defined resizing. Users could arguably implement something like it by abusing set_cursor_icon and set_outer_position, but I doubt it would be as smooth as a solution which leverages the WM.