protocols: expose output-management, honor resizes standalone

Advertise zwlr_output_manager_v1 with the single head, published on
setup and re-published from the commit path whenever the size lands
(resizes apply lazily, so the commit -- not the request -- is where
the new size becomes observable).

Client sizes apply standalone through the normal resize path (tabs
reflow, swapchain follows); nested refuses anything but the live
size since the host owns the geometry. Disabling, scale and
transform changes are refused; superseded configs are destroyed
after feedback.

Standalone output_resize now owns the canvas size so client resizes
reach the lazy modeset. wltest gains --output-mgmt; smoke tests
cover the global, head listing, and an 800x600 apply with tab
reflow.
This commit is contained in:
Andrew Hamilton 2026-09-04 10:52:16 +10:00
parent fcd3bca53b
commit d8164ce315
6 changed files with 1158 additions and 3 deletions

171
kick.c
View file

@ -96,6 +96,7 @@
#include <wlr/types/wlr_output.h>
#include <wlr/types/wlr_output_layout.h>
#include <wlr/types/wlr_output_power_management_v1.h>
#include <wlr/types/wlr_output_management_v1.h>
#include <wlr/types/wlr_pointer.h>
#include <wlr/types/wlr_pointer_constraints_v1.h>
#include <wlr/types/wlr_pointer_gestures_v1.h>
@ -393,6 +394,13 @@ struct kick_server {
struct wlr_output_power_manager_v1 *output_power_mgr;
struct wl_listener output_power_set_mode;
bool output_powered;
/* Output management (kanshi/wdisplays reconfigure the output): the
single server output is published as a head; client size requests
are honored standalone and refused nested (the host owns the size).
Scale/transform changes and disabling are unsupported. */
struct wlr_output_manager_v1 *output_mgr;
struct wl_listener output_mgr_apply;
struct wl_listener output_mgr_test;
/* Gamma control (night-light tools like wlsunset/gammastep): a client
sets a per-output ramp; wlroots rejects outputs with no LUT (e.g.
the headless output) itself, so the handler only commits ramps for
@ -870,6 +878,9 @@ static const char *foreign_str(const char *s);
static struct kick_window *tab_current_window(struct kick_tab *tab);
/* Toplevel-icon body lives with the overlay code; called from teardown. */
static void icon_proto_release(struct kick_window *w);
/* Output-management publish lives with the modeset handlers; called from
output setup and every resize. */
static void output_mgmt_publish(void);
static struct kick_tab *pick_successor(struct kick_tab *tab,
bool prefer_right);
static void remove_tab(struct kick_tab *tab);
@ -2479,6 +2490,15 @@ static void output_resize(int width, int height) {
if (height < 50) height = 50;
if (!server.output || !server.allocator) return;
/* Standalone has no host window tracking the size: the canvas follows
the request so the lazy modeset in apply_pending_output_resize
picks it up at render (nested is a no-op: the args already are the
host window's size). */
if (server.standalone) {
host.width = width;
host.height = height;
}
/* The new size is applied to the output lazily at render time (see
apply_pending_output_resize); here only the scene geometry and the
clients are told, so configure-time stays free of modesets and
@ -2706,6 +2726,10 @@ server.output = output;
server.overlay_damage_rect = wlr_scene_rect_create(
&server.scene->tree, w, h, (float[]){ 0.0f, 0.0f, 0.0f, 0.0f });
}
/* Publish the fresh output to management clients (kanshi/wdisplays
learn the head and its modes/size on bind). */
output_mgmt_publish();
}
/* Nearest live tab, scanning right first (prefer_right) or left first.
@ -6193,6 +6217,141 @@ static void handle_output_power_set_mode(struct wl_listener *listener,
kick_request_redraw();
}
/* ===================== Output management =====================
kanshi/wdisplays-style clients list the single head and request sizes.
Standalone honors custom sizes through the normal resize path (tabs
reflow, swapchain follows lazily at render); nested refuses anything
but the live size because the host window owns the geometry. Anything
beyond size (disabling, scale, transform) is refused everywhere: one
fixed output per session. Successful applies republish so listening
clients converge. */
/* Broadcast the live output as the manager's current configuration;
wlroots creates/updates/destroys heads from it and only notifies on
change. No-op until both manager and output exist. */
static void output_mgmt_publish(void) {
if (!server.output_mgr || !server.output)
return;
struct wlr_output_configuration_v1 *config =
wlr_output_configuration_v1_create();
if (!config)
return;
if (!wlr_output_configuration_head_v1_create(config, server.output)) {
wlr_output_configuration_v1_destroy(config);
return;
}
wlr_output_manager_v1_set_configuration(server.output_mgr, config);
}
/* Re-publish after a commit changed the output size. Resizes land
lazily (host configures, client applies), so the commit -- not the
request -- is where the new size becomes observable. */
static void output_mgmt_sync(void) {
if (!server.output_mgr || !server.output)
return;
static int last_w = -1, last_h = -1;
if (server.output->width == last_w && server.output->height == last_h)
return;
last_w = server.output->width;
last_h = server.output->height;
output_mgmt_publish();
}
/* The config head addressing our output, if the client included it. */
static struct wlr_output_configuration_head_v1 *mgmt_head_for_output(
struct wlr_output_configuration_v1 *config) {
if (!server.output)
return NULL;
struct wlr_output_configuration_head_v1 *h;
wl_list_for_each(h, &config->heads, link) {
if (h->state.output == server.output)
return h;
}
return NULL;
}
/* Requested size, preferring an explicit mode over a custom one. */
static void mgmt_head_size(struct wlr_output_configuration_head_v1 *h,
int *w, int *hh) {
if (h->state.mode) {
*w = h->state.mode->width;
*hh = h->state.mode->height;
} else {
*w = h->state.custom_mode.width;
*hh = h->state.custom_mode.height;
}
}
/* Whether a client config is one we can honor: our output, still
enabled, same scale/transform, sane size. Nested additionally
requires the live size (the host owns it). */
static bool mgmt_config_ok(struct wlr_output_configuration_v1 *config,
int *w, int *hh) {
struct wlr_output_configuration_head_v1 *h =
mgmt_head_for_output(config);
if (!h || !h->state.enabled || !server.output)
return false;
mgmt_head_size(h, w, hh);
if (*w < 50 || *hh < 50 || *w > 16384 || *hh > 16384)
return false;
if (h->state.scale != (float)server.output->scale ||
h->state.transform != server.output->transform)
return false;
if (!server.standalone &&
(*w != server.output->width || *hh != server.output->height))
return false;
return true;
}
static void handle_output_mgr_test(struct wl_listener *listener,
void *data) {
(void)listener;
struct wlr_output_configuration_v1 *config = data;
int w = 0, hh = 0;
bool ok = mgmt_config_ok(config, &w, &hh);
if (ok && server.standalone) {
/* Honest modeset probe on top of the sanity check. */
struct wlr_output_state state;
wlr_output_state_init(&state);
wlr_output_state_set_custom_mode(&state, w, hh, 0);
ok = wlr_output_test_state(server.output, &state);
wlr_output_state_finish(&state);
}
kick_log(KICK_LOG_DEBUG, "[output] test %dx%d %s\n", w, hh,
ok ? "ok" : "failed");
if (ok)
wlr_output_configuration_v1_send_succeeded(config);
else
wlr_output_configuration_v1_send_failed(config);
wlr_output_configuration_v1_destroy(config);
}
static void handle_output_mgr_apply(struct wl_listener *listener,
void *data) {
(void)listener;
struct wlr_output_configuration_v1 *config = data;
int w = 0, hh = 0;
bool ok = mgmt_config_ok(config, &w, &hh);
if (ok && server.standalone) {
struct wlr_output_state state;
wlr_output_state_init(&state);
wlr_output_state_set_custom_mode(&state, w, hh, 0);
ok = wlr_output_test_state(server.output, &state);
wlr_output_state_finish(&state);
}
if (ok) {
if (server.standalone)
output_resize(w, hh);
output_mgmt_publish();
kick_log(KICK_LOG_INFO, "[output] apply %dx%d\n", w, hh);
wlr_output_configuration_v1_send_succeeded(config);
} else {
kick_log(KICK_LOG_INFO, "[output] apply %dx%d refused\n", w, hh);
wlr_output_configuration_v1_send_failed(config);
}
wlr_output_configuration_v1_destroy(config);
}
/* Gamma control: apply the client's ramp to the single output (or reset it
when the control went away: wlroots emits set_gamma with NULL then). A
failed commit fails the control so the client (wlsunset) retries later
@ -10063,6 +10222,8 @@ static void render_standalone(void) {
apply_tearing_hint(&state);
if (!wlr_output_commit_state(server.output, &state))
kick_log(KICK_LOG_DEBUG, "[render] output commit failed\n");
else
output_mgmt_sync();
frame_done:
struct timespec now;
@ -10147,6 +10308,8 @@ static void render_and_present(void) {
apply_tearing_hint(&state);
if (!wlr_output_commit_state(server.output, &state))
kick_log(KICK_LOG_DEBUG, "[render] output commit failed\n");
else
output_mgmt_sync();
frame_done:
struct timespec now;
@ -11189,6 +11352,14 @@ int main(int argc, char *argv[]) {
wl_signal_add(&server.output_power_mgr->events.set_mode,
&server.output_power_set_mode);
/* Output management: publish the single head; client resizes apply
standalone and are refused nested (see the section). */
server.output_mgr = wlr_output_manager_v1_create(server.wl_display);
server.output_mgr_apply.notify = handle_output_mgr_apply;
wl_signal_add(&server.output_mgr->events.apply, &server.output_mgr_apply);
server.output_mgr_test.notify = handle_output_mgr_test;
wl_signal_add(&server.output_mgr->events.test, &server.output_mgr_test);
/* Gamma control: night-light clients set the output ramp (see
handle_gamma_set); wlroots itself fails controls for outputs with
no LUT, so headless/nested clients learn it is unsupported. */