Keep floating window geometry with output changes

10145e82f4531e91c3095043d9889c867c85ff6e
Alexis Sellier committed ago 1 parent 20eaf8cf
swm.c +21 -4
6694 6694
6695 6695
    if (m->lock_surface)
6696 6696
        client_set_scale(m->lock_surface->surface, m->wlr_output->scale);
6697 6697
}
6698 6698
6699 -
/* Return whether a fullscreen client must follow an output geometry change. */
6699 +
/* Return whether a client needs explicit placement after an output change. */
6700 6700
static bool client_follows_output_geometry(client_t *c, monitor_t *m) {
6701 -
    return c->mon == m && c->is_fullscreen;
6701 +
    return c->mon == m && (c->is_fullscreen || c->is_floating);
6702 6702
}
6703 6703
6704 6704
/* Reconcile display layout changes with workspaces, windows, focus, and color state. */
6705 6705
void update_monitors(struct wl_listener *listener, void *data) {
6706 6706
    /*
6743 6743
    /* Cover all windows while the session is locked. */
6744 6744
    wlr_scene_node_set_position(&locked_bg->node, sgeom.x, sgeom.y);
6745 6745
    wlr_scene_rect_set_size(locked_bg, sgeom.width, sgeom.height);
6746 6746
6747 6747
    wl_list_for_each(m, &mons, link) {
6748 +
        struct wlr_box old = m->m;
6749 +
6748 6750
        if (!m->wlr_output->enabled)
6749 6751
            continue;
6750 6752
        config_head = wlr_output_configuration_head_v1_create(config, m->wlr_output);
6751 6753
6752 6754
        /* Read the display's position in the combined desktop. */
6753 6755
        wlr_output_layout_get_box(output_layout, m->wlr_output, &m->m);
6754 6756
        m->layout_x = m->m.x;
6755 6757
        m->layout_y = m->m.y;
6756 6758
        m->w        = m->m;
6759 +
        /* Keep current and saved window positions relative to the output. */
6760 +
        wl_list_for_each(c, &clients, link) {
6761 +
            if (c->mon != m)
6762 +
                continue;
6763 +
            c->geom.x += m->m.x - old.x;
6764 +
            c->geom.y += m->m.y - old.y;
6765 +
            if (c->is_fullscreen) {
6766 +
                c->prev.x += m->m.x - old.x;
6767 +
                c->prev.y += m->m.y - old.y;
6768 +
            }
6769 +
            if (c->is_max_stacked) {
6770 +
                c->maxstack_prev.x += m->m.x - old.x;
6771 +
                c->maxstack_prev.y += m->m.y - old.y;
6772 +
            }
6773 +
        }
6757 6774
        wlr_scene_output_set_position(m->scene_output, m->m.x, m->m.y);
6758 6775
6759 6776
        wlr_scene_node_set_position(&m->fullscreen_bg->node, m->m.x, m->m.y);
6760 6777
        wlr_scene_rect_set_size(m->fullscreen_bg, m->m.width, m->m.height);
6761 6778
6779 6796
        /* Sync clients of this output's workspace (it may have been
6780 6797
         * hidden or shown elsewhere before this display appeared). */
6781 6798
        monitor_set_scale(m);
6782 6799
        /* Keep windows on their workspace when displays are connected. */
6783 6800
        arrange(m);
6784 -
        /* Resize fullscreen windows to fill their display. */
6801 +
        /* Fit floating windows and fill the output with fullscreen windows. */
6785 6802
        wl_list_for_each(c, &clients, link) {
6786 6803
            if (client_follows_output_geometry(c, m))
6787 -
                resize(c, m->m, 0);
6804 +
                resize(c, c->is_fullscreen ? m->m : c->geom, 0);
6788 6805
        }
6789 6806
6790 6807
        /* Reapply color correction in case this display was just re-enabled. */
6791 6808
        m->gamma_lut_changed = 1;
6792 6809
test/clients.py +53 -0
5 5
from __future__ import annotations
6 6
7 7
import ctypes
8 8
import mmap
9 9
import os
10 +
import re
10 11
import struct
11 12
import subprocess
12 13
import sys
13 14
import tempfile
14 15
import time
675 676
        raise Failure("output did not power back on")
676 677
    power.destroy()
677 678
    connection.close()
678 679
679 680
681 +
def floating_output_geometry() -> None:
682 +
    """Keep floating windows visible when an output moves or shrinks."""
683 +
684 +
    connection = Connection(WlCompositor, WlShm, XdgWmBase)
685 +
    window = Window(connection, "output-float", 0xFF336699, fixed_size=True)
686 +
    connection.roundtrips(4)
687 +
688 +
    def geometry() -> tuple[int, ...]:
689 +
        """Read this window's published outer rectangle."""
690 +
691 +
        window.toplevel.set_title("output-float")
692 +
        connection.roundtrips(3)
693 +
        contents = (Path(os.environ["XDG_RUNTIME_DIR"]) / "swm-windows").read_text()
694 +
        match = re.search(r"^(-?\d+),(-?\d+) (\d+)x(\d+) output-float$", contents, re.MULTILINE)
695 +
        if not match:
696 +
            raise Failure("floating window has no published geometry")
697 +
        return tuple(map(int, match.groups()))
698 +
699 +
    before = geometry()
700 +
    subprocess.run(["wlr-randr", "--output", "HEADLESS-1", "--pos", "100,80"], check=True)
701 +
    moved = geometry()
702 +
    if moved != (before[0] + 100, before[1] + 80, *before[2:]):
703 +
        raise Failure(f"floating window did not follow output: {before} -> {moved}")
704 +
    window.toplevel.set_fullscreen(None)
705 +
    connection.roundtrips(4)
706 +
    subprocess.run(["wlr-randr", "--output", "HEADLESS-1", "--pos", "0,0"], check=True)
707 +
    connection.roundtrips(4)
708 +
    window.toplevel.unset_fullscreen()
709 +
    connection.roundtrips(4)
710 +
    if geometry() != before:
711 +
        raise Failure(f"fullscreen restore geometry did not follow the output: {before} -> {geometry()}")
712 +
    window.destroy()
713 +
    parent = Window(connection, "output-parent", 0xFF224466, fullscreen=True)
714 +
    connection.roundtrips(4)
715 +
    window = Window(connection, "output-float", 0xFF336699, parent=parent.toplevel)
716 +
    connection.roundtrips(4)
717 +
    output = subprocess.run(["wlr-randr"], check=True, text=True, capture_output=True).stdout
718 +
    scale = re.search(r"Scale: ([\d.]+)", output).group(1)
719 +
    subprocess.run(["wlr-randr", "--output", "HEADLESS-1", "--scale", "8"], check=True)
720 +
    connection.roundtrips(4)
721 +
    x, y, width, height = geometry()
722 +
    if x >= parent.width or y >= parent.height or x + width <= 0 or y + height <= 0:
723 +
        raise Failure("floating window is outside the resized output")
724 +
    subprocess.run(["wlr-randr", "--output", "HEADLESS-1", "--scale", scale], check=True)
725 +
    connection.roundtrips(4)
726 +
    window.destroy()
727 +
    parent.destroy()
728 +
    connection.close()
729 +
730 +
680 731
def output_management(arguments: list[str]) -> None:
681 732
    """Exercise output-management through the installed reference client."""
682 733
683 734
    command = ["wlr-randr"]
684 735
    if arguments and arguments[0] in {
698 749
            subprocess.run([*command, "--dryrun"], check=True)
699 750
    else:
700 751
        command += ["--output", "HEADLESS-1", "--on"]
701 752
        subprocess.run([*command, "--dryrun"], check=True)
702 753
    subprocess.run(command, check=True)
754 +
    if not arguments:
755 +
        floating_output_geometry()
703 756
    if arguments and arguments[0] == "enable-second":
704 757
        state = subprocess.run(["wlr-randr"], check=True, text=True, capture_output=True).stdout
705 758
        for expected in ("Position: 120,80", "Transform: 90", "Scale: 2.000000"):
706 759
            if expected not in state:
707 760
                raise Failure(f"re-enabled output lost configuration: {expected}")