]> git.mdlowis.com Git - proto/labwc.git/commitdiff
xdg: handle initially maximized xdg-shell views better
authorJohn Lindgren <john@jlindgren.net>
Tue, 2 Jul 2024 11:24:29 +0000 (07:24 -0400)
committerJohan Malm <johanmalm@users.noreply.github.com>
Fri, 19 Jul 2024 21:48:39 +0000 (22:48 +0100)
Currently, initially maximized (or fullscreen) xdg-shell views exhibit
one of two issues:

 - some (e.g. GTK and Qt apps) paint an initial frame un-maximized
   (before the "map" event) and only maximize in a later commit

 - others (e.g. foot) maximize immediately without flicker, but never
   store a valid natural size, so we end up using a fallback (640x480)

Under KWin, neither of these issues occur, so I looked into what labwc
is doing wrong. It seems that:

 - wlroots internally sends an initial configure event with a size of
   0x0 to all xdg-shell views. This requests the client to set its own
   preferred (a.k.a. natural) size.

 - For an initially maximized/fullscreen view, the initial configure
   event should contain the maximized/fullscreen size rather than 0x0.
   In labwc, this means we have to call wlr_xdg_toplevel_set_size()
   earlier, i.e. from the new_surface event. Tracing with WAYLAND_DEBUG
   shows that the initial configure event now has the correct geometry,
   matching KWin behavior. With this change, GTK and Qt apps no longer
   paint an incorrect un-maximized frame.

 - However, this means that all xdg-shell views now suffer from the same
   issue as foot, where we never receive a commit with the un-maximized
   (natural) geometry. The correct way to get the natural geometry seems
   to be to wait until we want to un-maximize, and send a configure
   event of 0x0 at that point.

Sending a configure event of 0x0 when un-maximizing is a bit annoying as
it breaks some assumptions in labwc code. In particular:

 - view->natural_geometry may now be unknown (0x0), requiring various
   wlr_box_empty() checks sprinkled around. I added these in all the
   obvious places, but there could be some code paths that I missed.

 - Positioning the newly un-maximized view within view_maximize() no
   longer works since we don't know the natural size. Instead we have to
   run the positioning logic from the surface commit handler. This
   results in some extra complexity, especially for interactive move.
   See the new do_late_positioning() function in xdg.c.

Some TODOs/FIXMEs (non-blocking in my opinion):

 - The view_wants_decorations() check is now duplicated in both the
   new_surface and map event handlers. I'm not sure if this is necessary
   but it seemed like the safest approach for now. More testing would be
   nice, particularly with various combinations of config and client SSD
   preferences.

 - Aside from the interactive move case, the "late positioning" logic
   always centers the view when un-maximizing, and does not invoke any
   of the smart placement logic. If we want to invoke smart placement
   here, I'd appreciate someone with more knowledge of that code to take
   a look and figure out how to do that correctly.

include/labwc.h
include/view.h
src/interactive.c
src/view.c
src/xdg.c
src/xwayland.c

index 0aa4526faa391e817decc5ebf5b2867c6a360dd9..21cab83b07db688853e8f1d522a4e7d6525ec190 100644 (file)
@@ -484,6 +484,15 @@ void seat_set_pressed(struct seat *seat, struct view *view,
 void seat_reset_pressed(struct seat *seat);
 void seat_output_layout_changed(struct seat *seat);
 
+/**
+ * interactive_anchor_to_cursor() - repositions the view to remain
+ * underneath the cursor when its size changes during interactive move.
+ *
+ * geometry->{width,height} are provided by the caller.
+ * geometry->{x,y} are computed by this function.
+ */
+void interactive_anchor_to_cursor(struct view *view, struct wlr_box *geometry);
+
 void interactive_begin(struct view *view, enum input_mode mode, uint32_t edges);
 void interactive_finish(struct view *view);
 void interactive_cancel(struct view *view);
index b4f90e6a4d48c5cc108d26aab1dbeee8c43a3f52..637b00dfd3b716e1cc7a014b41bc0aaa4378c877 100644 (file)
@@ -451,6 +451,7 @@ void view_moved(struct view *view);
 void view_minimize(struct view *view, bool minimized);
 bool view_compute_centered_position(struct view *view,
        const struct wlr_box *ref, int w, int h, int *x, int *y);
+void view_set_fallback_natural_geometry(struct view *view);
 void view_store_natural_geometry(struct view *view);
 
 /**
index 813289aa00906ec7b3428d6837adb009675a463f..ae5a697278694941c1bffbc63d555769cac1f195 100644 (file)
@@ -21,6 +21,16 @@ max_move_scale(double pos_cursor, double pos_current,
        return pos_new;
 }
 
+void
+interactive_anchor_to_cursor(struct view *view, struct wlr_box *geometry)
+{
+       struct wlr_cursor *cursor = view->server->seat.cursor;
+       geometry->x = max_move_scale(cursor->x, view->current.x,
+               view->current.width, geometry->width);
+       geometry->y = max_move_scale(cursor->y, view->current.y,
+               view->current.height, geometry->height);
+}
+
 void
 interactive_begin(struct view *view, enum input_mode mode, uint32_t edges)
 {
@@ -61,14 +71,17 @@ interactive_begin(struct view *view, enum input_mode mode, uint32_t edges)
                         * width/height.
                         * Don't reset tiled state yet since we may want
                         * to keep it (in the snap-to-maximize case).
+                        *
+                        * If the natural geometry is unknown (possible
+                        * with xdg-shell views), then we set a size of
+                        * 0x0 here and determine the correct geometry
+                        * later. See do_late_positioning() in xdg.c.
                         */
-                       geometry = view->natural_geometry;
-                       geometry.x = max_move_scale(seat->cursor->x,
-                               view->current.x, view->current.width,
-                               geometry.width);
-                       geometry.y = max_move_scale(seat->cursor->y,
-                               view->current.y, view->current.height,
-                               geometry.height);
+                       geometry.width = view->natural_geometry.width;
+                       geometry.height = view->natural_geometry.height;
+                       if (!wlr_box_empty(&geometry)) {
+                               interactive_anchor_to_cursor(view, &geometry);
+                       }
 
                        view_set_shade(view, false);
                        view_set_untiled(view);
index 3486b19ea7bb5174d5a77ecddc327ea1873c08ab..6a0f9806761b9064682be8a51b87400cf26a69d2 100644 (file)
@@ -816,8 +816,8 @@ adjust_floating_geometry(struct view *view, struct wlr_box *geometry,
                &geometry->x, &geometry->y);
 }
 
-static void
-set_fallback_geometry(struct view *view)
+void
+view_set_fallback_natural_geometry(struct view *view)
 {
        view->natural_geometry.width = LAB_FALLBACK_WIDTH;
        view->natural_geometry.height = LAB_FALLBACK_HEIGHT;
@@ -840,18 +840,14 @@ view_store_natural_geometry(struct view *view)
                return;
        }
 
-       /**
-        * If an application was started maximized or fullscreened, its
-        * natural_geometry width/height may still be zero (or very small
-        * values) in which case we set some fallback values. This is the case
-        * with foot and some Qt/Tk applications.
+       /*
+        * Note that for xdg-shell views that start fullscreen or maximized,
+        * we end up storing a natural geometry of 0x0. This is intentional.
+        * When leaving fullscreen or unmaximizing, we pass 0x0 to the
+        * xdg-toplevel configure event, which means the application should
+        * choose its own size.
         */
-       if (view->pending.width < LAB_MIN_VIEW_WIDTH
-                       || view->pending.height < LAB_MIN_VIEW_HEIGHT) {
-               set_fallback_geometry(view);
-       } else {
-               view->natural_geometry = view->pending;
-       }
+       view->natural_geometry = view->pending;
 }
 
 int
@@ -948,8 +944,11 @@ view_apply_natural_geometry(struct view *view)
        assert(view_is_floating(view));
 
        struct wlr_box geometry = view->natural_geometry;
-       adjust_floating_geometry(view, &geometry,
-               /* midpoint_visibility */ false);
+       /* Only adjust natural geometry if known (not 0x0) */
+       if (!wlr_box_empty(&geometry)) {
+               adjust_floating_geometry(view, &geometry,
+                       /* midpoint_visibility */ false);
+       }
        view_move_resize(view, geometry);
 }
 
@@ -1247,6 +1246,18 @@ view_maximize(struct view *view, enum view_axis axis,
                        view_invalidate_last_layout_geometry(view);
                }
        }
+
+       /*
+        * When natural geometry is unknown (0x0) for an xdg-shell view,
+        * we normally send a configure event of 0x0 to get the client's
+        * preferred size, but this doesn't work if unmaximizing only
+        * one axis. So in that corner case, set a fallback geometry.
+        */
+       if ((axis == VIEW_AXIS_HORIZONTAL || axis == VIEW_AXIS_VERTICAL)
+                       && wlr_box_empty(&view->natural_geometry)) {
+               view_set_fallback_natural_geometry(view);
+       }
+
        set_maximized(view, axis);
        if (view_is_floating(view)) {
                view_apply_natural_geometry(view);
index 84d03bd3ac3a8286b612ede4b4f00b37ae60b590..fc4b4d24623e5d6ff8241dcf0aa5a70083834fa7 100644 (file)
--- a/src/xdg.c
+++ b/src/xdg.c
@@ -75,6 +75,26 @@ handle_new_popup(struct wl_listener *listener, void *data)
        xdg_popup_create(view, wlr_popup);
 }
 
+static void
+do_late_positioning(struct view *view)
+{
+       struct server *server = view->server;
+       if (server->input_mode == LAB_INPUT_STATE_MOVE
+                       && view == server->grabbed_view) {
+               /* Keep view underneath cursor */
+               interactive_anchor_to_cursor(view, &view->pending);
+               /* Update grab offsets */
+               server->grab_x = server->seat.cursor->x;
+               server->grab_y = server->seat.cursor->y;
+               server->grab_box = view->pending;
+       } else {
+               /* TODO: smart placement? */
+               view_compute_centered_position(view, NULL,
+                       view->pending.width, view->pending.height,
+                       &view->pending.x, &view->pending.y);
+       }
+}
+
 static void
 handle_commit(struct wl_listener *listener, void *data)
 {
@@ -90,6 +110,20 @@ handle_commit(struct wl_listener *listener, void *data)
 
        struct wlr_box size;
        wlr_xdg_surface_get_geometry(xdg_surface, &size);
+       bool update_required = false;
+
+       /*
+        * If we didn't know the natural size when leaving fullscreen or
+        * unmaximizing, then the pending size will be 0x0. In this case,
+        * the pending x/y is also unset and we still need to position
+        * the window.
+        */
+       if (wlr_box_empty(&view->pending)) {
+               view->pending.width = size.width;
+               view->pending.height = size.height;
+               do_late_positioning(view);
+               update_required = true;
+       }
 
        /*
         * Qt applications occasionally fail to call set_window_geometry
@@ -113,8 +147,9 @@ handle_commit(struct wl_listener *listener, void *data)
        }
 
        struct wlr_box *current = &view->current;
-       bool update_required = current->width != size.width
-               || current->height != size.height;
+       if (current->width != size.width || current->height != size.height) {
+               update_required = true;
+       }
 
        uint32_t serial = view->pending_configure_serial;
        if (serial > 0 && serial == xdg_surface->current.configure_serial) {
@@ -327,7 +362,15 @@ static void
 xdg_toplevel_view_configure(struct view *view, struct wlr_box geo)
 {
        uint32_t serial = 0;
-       view_adjust_size(view, &geo.width, &geo.height);
+
+       /*
+        * Leave a size of 0x0 unchanged; this has special meaning in
+        * an xdg-toplevel configure event and requests the application
+        * to choose its own preferred size.
+        */
+       if (!wlr_box_empty(&geo)) {
+               view_adjust_size(view, &geo.width, &geo.height);
+       }
 
        /*
         * We do not need to send a configure request unless the size
@@ -569,11 +612,12 @@ xdg_toplevel_view_map(struct view *view)
        struct wlr_xdg_surface *xdg_surface = xdg_surface_from_view(view);
        wlr_scene_node_set_enabled(&view->scene_tree->node, true);
        if (!view->been_mapped) {
-               struct wlr_xdg_toplevel_requested *requested =
-                       &xdg_toplevel_from_view(view)->requested;
-
                init_foreign_toplevel(view);
 
+               /*
+                * FIXME: is this needed or is the earlier logic in
+                * xdg_surface_new() enough?
+                */
                if (view_wants_decorations(view)) {
                        view_set_ssd_mode(view, LAB_SSD_MODE_FULL);
                } else {
@@ -581,8 +625,7 @@ xdg_toplevel_view_map(struct view *view)
                }
 
                /*
-                * Set initial "pending" dimensions (may be modified by
-                * view_set_fullscreen/view_maximize() below). "Current"
+                * Set initial "pending" dimensions. "Current"
                 * dimensions remain zero until handle_commit().
                 */
                if (wlr_box_empty(&view->pending)) {
@@ -594,22 +637,11 @@ xdg_toplevel_view_map(struct view *view)
 
                /*
                 * Set initial "pending" position for floating views.
-                * Do this before view_set_fullscreen/view_maximize() so
-                * that the position is saved with the natural geometry.
-                *
-                * FIXME: the natural geometry is not saved if either
-                * handle_request_fullscreen/handle_request_maximize()
-                * is called before map (try "foot --maximized").
                 */
                if (view_is_floating(view)) {
                        set_initial_position(view);
                }
 
-               set_fullscreen_from_request(view, requested);
-               view_maximize(view, requested->maximized ?
-                       VIEW_AXIS_BOTH : VIEW_AXIS_NONE,
-                       /*store_natural_geometry*/ true);
-
                /*
                 * Set initial "current" position directly before
                 * calling view_moved() to reduce flicker
@@ -838,6 +870,31 @@ xdg_toplevel_new(struct wl_listener *listener, void *data)
        CONNECT_SIGNAL(xdg_surface, xdg_toplevel_view, new_popup);
 
        wl_list_insert(&server->views, &view->link);
+
+       /* FIXME: is view_wants_decorations() reliable this early? */
+       if (view_wants_decorations(view)) {
+               view_set_ssd_mode(view, LAB_SSD_MODE_FULL);
+       } else {
+               view_set_ssd_mode(view, LAB_SSD_MODE_NONE);
+       }
+
+       /*
+        * Handle initial fullscreen/maximize requests. This needs to be
+        * done early (before map) in order to send the correct size to
+        * the initial configure event and avoid flicker.
+        *
+        * Note that at this point, wlroots has already scheduled (but
+        * not yet sent) the initial configure event with a size of 0x0.
+        * In normal (non-fullscreen/maximized) cases, the zero size
+        * requests the application to choose its own size.
+        */
+       if (toplevel->requested.fullscreen) {
+               set_fullscreen_from_request(view, &toplevel->requested);
+       }
+       if (toplevel->requested.maximized) {
+               view_maximize(view, VIEW_AXIS_BOTH,
+                       /*store_natural_geometry*/ true);
+       }
 }
 
 void
index 0c6a6c14f136d5761f9b1c38edebf01afe04f0e9..9332f31d943fa86a2d2de85f5f742ba4bcadfb8d 100644 (file)
@@ -613,6 +613,21 @@ handle_map_request(struct wl_listener *listener, void *data)
         */
 }
 
+static void
+check_natural_geometry(struct view *view)
+{
+       /*
+        * Some applications (example: Thonny) don't set a reasonable
+        * un-maximized size when started maximized. Try to detect this
+        * and set a fallback size.
+        */
+       if (!view_is_floating(view)
+                       && (view->natural_geometry.width < LAB_MIN_VIEW_WIDTH
+                       || view->natural_geometry.height < LAB_MIN_VIEW_HEIGHT)) {
+               view_set_fallback_natural_geometry(view);
+       }
+}
+
 static void
 set_initial_position(struct view *view,
                struct wlr_xwayland_surface *xwayland_surface)
@@ -731,6 +746,7 @@ xwayland_view_map(struct view *view)
        }
 
        if (!view->been_mapped) {
+               check_natural_geometry(view);
                set_initial_position(view, xwayland_surface);
                /*
                 * When mapping the view for the first time, visual