view: rework saving/restoring geometry across layout changes

After several iterations, this is basically a complete re-work. The old
implementation was difficult to follow and sometimes failed to restore
fullscreen/maximized/tiled geometry correctly, since it was based
entirely on natural (floating) geometry.

The new implementation:
 - always saves the actual (pending) geometry at first layout change
 - explicitly tracks whether a view has moved between outputs
 - consolidates invalidating the saved geometry into one place, rather
   than having lots of invalidate() calls sprinkled everywhere
This commit is contained in:
John Lindgren 2026-01-11 13:03:23 -05:00 committed by Hiroaki Yamamoto
parent c1c156ef39
commit 20929c0484
3 changed files with 55 additions and 131 deletions

View file

@ -215,11 +215,14 @@ struct view {
struct wlr_box natural_geometry;
/*
* Whenever an output layout change triggers a view relocation, the
* last pending position (or natural geometry) will be saved so the
* view may be restored to its original location on a subsequent layout
* change.
* last pending position will be saved so the view may be restored
* to its original location on a subsequent layout change.
*/
struct wlr_box last_layout_geometry;
/* Set temporarily when moving view due to layout change */
bool adjusting_for_layout_change;
/* True if original output was disconnected or disabled */
bool lost_output_due_to_layout_change;
/* used by xdg-shell views */
uint32_t pending_configure_serial;
@ -533,7 +536,6 @@ bool view_titlebar_visible(struct view *view);
void view_set_ssd_mode(struct view *view, enum lab_ssd_mode mode);
void view_set_decorations(struct view *view, enum lab_ssd_mode mode, bool force_ssd);
void view_toggle_fullscreen(struct view *view);
void view_invalidate_last_layout_geometry(struct view *view);
void view_adjust_for_layout_change(struct view *view);
void view_move_to_edge(struct view *view, enum lab_edge direction, bool snap_to_windows);
void view_grow_to_edge(struct view *view, enum lab_edge direction);

View file

@ -92,9 +92,6 @@ interactive_begin(struct view *view, enum input_mode mode, enum lab_edge edges)
/* Store natural geometry at start of move */
view_store_natural_geometry(view);
if (view_is_floating(view)) {
view_invalidate_last_layout_geometry(view);
}
/* Prevent region snapping when just moving via A-Left mousebind */
seat->region_prevent_snap = keyboard_get_all_modifiers(seat);
@ -111,12 +108,6 @@ interactive_begin(struct view *view, enum input_mode mode, enum lab_edge edges)
return;
}
/*
* Resizing overrides any attempt to restore window
* geometries altered by layout changes.
*/
view_invalidate_last_layout_geometry(view);
/*
* If tiled or maximized in only one direction, reset
* tiled state and un-maximize the relevant axes, but

View file

@ -588,6 +588,21 @@ view_move_resize(struct view *view, struct wlr_box geo)
if (view->impl->configure) {
view->impl->configure(view, geo);
}
/*
* If the move/resize was user-initiated (rather than due to
* output layout change), then invalidate the saved geometry.
*
* TODO: consider also updating view->output here for floating
* views (based on view->pending) rather than waiting until
* view_moved(). This might eliminate some race conditions with
* view_adjust_for_layout_change(), which uses view->pending.
* Not sure if it might have other side-effects though.
*/
if (!view->adjusting_for_layout_change) {
view->last_layout_geometry = (struct wlr_box){0};
view->lost_output_due_to_layout_change = false;
}
}
void
@ -1405,9 +1420,6 @@ view_maximize(struct view *view, enum view_axis axis)
* a maximized view.
*/
interactive_cancel(view);
if (store_natural_geometry && view_is_floating(view)) {
view_invalidate_last_layout_geometry(view);
}
}
/*
@ -1695,7 +1707,6 @@ view_set_fullscreen(struct view *view, bool fullscreen)
*/
interactive_cancel(view);
view_store_natural_geometry(view);
view_invalidate_last_layout_geometry(view);
}
set_fullscreen(view, fullscreen);
@ -1713,139 +1724,62 @@ view_set_fullscreen(struct view *view, bool fullscreen)
cursor_update_focus(view->server);
}
static bool
last_layout_geometry_is_valid(struct view *view)
{
return view->last_layout_geometry.width > 0
&& view->last_layout_geometry.height > 0;
}
static void
update_last_layout_geometry(struct view *view)
{
/*
* Only update an invalid last-layout geometry to prevent a series of
* successive layout changes from continually replacing the "preferred"
* location with whatever location the view currently holds. The
* "preferred" location should be whatever state was set by user
* interaction, not automatic responses to layout changes.
*/
if (last_layout_geometry_is_valid(view)) {
return;
}
if (view_is_floating(view)) {
view->last_layout_geometry = view->pending;
} else if (!wlr_box_empty(&view->natural_geometry)) {
view->last_layout_geometry = view->natural_geometry;
} else {
/* e.g. initially-maximized window */
view->last_layout_geometry =
view_get_fallback_natural_geometry(view);
}
}
static bool
apply_last_layout_geometry(struct view *view, bool force_update)
{
/* Only apply a valid last-layout geometry */
if (!last_layout_geometry_is_valid(view)) {
return false;
}
/*
* Unless forced, the last-layout geometry is only applied
* when the relevant view geometry is distinct.
*/
if (!force_update) {
struct wlr_box *relevant = view_is_floating(view) ?
&view->pending : &view->natural_geometry;
if (wlr_box_equal(relevant, &view->last_layout_geometry)) {
return false;
}
}
view->natural_geometry = view->last_layout_geometry;
adjust_floating_geometry(view, &view->natural_geometry,
/* midpoint_visibility */ true);
return true;
}
void
view_invalidate_last_layout_geometry(struct view *view)
{
assert(view);
view->last_layout_geometry.width = 0;
view->last_layout_geometry.height = 0;
}
void
view_adjust_for_layout_change(struct view *view)
{
assert(view);
bool is_floating = view_is_floating(view);
bool use_natural = false;
view->adjusting_for_layout_change = true;
if (!output_is_usable(view->output)) {
/* A view losing an output should have a last-layout geometry */
update_last_layout_geometry(view);
}
/* Capture a pointer to the last-layout geometry (only if valid) */
struct wlr_box *last_geometry = NULL;
if (last_layout_geometry_is_valid(view)) {
last_geometry = &view->last_layout_geometry;
view->lost_output_due_to_layout_change = true;
}
/*
* Save the view's geometry if this is the first layout change
* since a user-initiated move/resize. Do not save it again for
* subsequent layout changes, since the point is to be able to
* restore to the original location after multiple changes
* (e.g. output disconnected and then reconnected).
*
* Note that it's important to do this even if an output change
* is not (yet) required, to properly handle cases of multiple
* outputs being disconnected/reconnected in any order. In that
* case, there can be multiple layout change events, and a view
* can be moved first and only later lose its own output.
*/
if (wlr_box_empty(&view->last_layout_geometry)) {
view->last_layout_geometry = view->pending;
}
/*
* Check if an output change is required:
* - Floating views are always mapped to the nearest output
* - Any view without a usable output needs to be repositioned
* - Any view with a valid last-layout geometry might be better
* positioned on another output
* - Any fullscreen/tiled/maximized view which lost its output
* due to a layout change should be moved back if the output
* is reconnected.
*
* If so, try to find the "nearest" output (in terms of layout
* coordinates) to the saved geometry. Other strategies might be
* possible here -- for example, trying to match the same physical
* output the view was on previously -- but this is simplest.
*/
if (is_floating || last_geometry || !output_is_usable(view->output)) {
/* Move the view to an appropriate output, if needed */
bool output_changed = view_discover_output(view, last_geometry);
/*
* Try to apply the last-layout to the natural geometry
* (adjusting to ensure that it fits on the screen). This is
* forced if the output has changed, but will be done
* opportunistically even on the same output if the last-layout
* geometry is different from the view's governing geometry.
*/
if (apply_last_layout_geometry(view, output_changed)) {
use_natural = true;
}
/*
* Whether or not the view has moved, the layout has changed.
* Ensure that the view now has a valid last-layout geometry.
*/
update_last_layout_geometry(view);
if (is_floating || view->lost_output_due_to_layout_change) {
view_discover_output(view, &view->last_layout_geometry);
}
if (!is_floating) {
view_apply_special_geometry(view);
} else if (use_natural) {
/*
* Move the window to its natural location, because
* we are trying to restore a prior layout.
*/
view_apply_natural_geometry(view);
} else {
/* Otherwise, just ensure the view is on screen. */
struct wlr_box geometry = view->pending;
if (adjust_floating_geometry(view, &geometry,
/* midpoint_visibility */ true)) {
view_move_resize(view, geometry);
}
/* Restore saved geometry, ensuring view is on-screen */
struct wlr_box geometry = view->last_layout_geometry;
adjust_floating_geometry(view, &geometry,
/* midpoint_visibility */ true);
view_move_resize(view, geometry);
}
view_update_outputs(view);
view->adjusting_for_layout_change = false;
}
void
@ -2134,7 +2068,6 @@ view_snap_to_edge(struct view *view, enum lab_edge edge,
} else if (store_natural_geometry) {
/* store current geometry as new natural_geometry */
view_store_natural_geometry(view);
view_invalidate_last_layout_geometry(view);
}
view_set_untiled(view);
view_set_output(view, output);
@ -2168,7 +2101,6 @@ view_snap_to_region(struct view *view, struct region *region)
} else if (store_natural_geometry) {
/* store current geometry as new natural_geometry */
view_store_natural_geometry(view);
view_invalidate_last_layout_geometry(view);
}
view_set_untiled(view);
view->tiled_region = region;
@ -2181,7 +2113,6 @@ view_move_to_output(struct view *view, struct output *output)
{
assert(view);
view_invalidate_last_layout_geometry(view);
view_set_output(view, output);
if (view_is_floating(view)) {
struct wlr_box output_area = output_usable_area_in_layout_coords(output);