Keep floating window geometry with output changes
10145e82f4531e91c3095043d9889c867c85ff6e
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}") |