From 9c0b4cc94f4fb5fcced3c3dd2fc85db19d441b95 Mon Sep 17 00:00:00 2001 From: irisz64 Date: Sun, 5 Apr 2026 23:30:45 +0200 Subject: [PATCH] just copied tinyrwm for now --- .gitignore | 1 + meson.build | 39 + protocol/river-window-management-v1.xml | 1854 +++++++++++++++++++++++ protocol/river-xkb-bindings-v1.xml | 268 ++++ src/main.c | 702 +++++++++ 5 files changed, 2864 insertions(+) create mode 100644 .gitignore create mode 100644 meson.build create mode 100644 protocol/river-window-management-v1.xml create mode 100644 protocol/river-xkb-bindings-v1.xml create mode 100644 src/main.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..468d90c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*build*/ diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..71cde2f --- /dev/null +++ b/meson.build @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: © 2026 Isaac Freund +# SPDX-License-Identifier: 0BSD + +project( + 'srwwm', + 'c', + meson_version: '>= 1.8.0', + version: '0.1', + default_options: [ + 'c_std=c99', + 'warning_level=3', + ], +) + +cc = meson.get_compiler('c') +add_project_arguments(cc.get_supported_arguments([ + '-D_POSIX_C_SOURCE=200809L', + '-Wno-unused-parameter', +]), language: 'c') + +wayland_client = dependency('wayland-client') +xkbcommon = dependency('xkbcommon') + +wl_mod = import('wayland') +generated = wl_mod.scan_xml( + 'protocol/river-window-management-v1.xml', + 'protocol/river-xkb-bindings-v1.xml', +) + +exe = executable( + 'srwwm', + 'src/main.c', + generated, + dependencies: [ + wayland_client, + xkbcommon, + ], + install: true, +) diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml new file mode 100644 index 0000000..64bfa58 --- /dev/null +++ b/protocol/river-window-management-v1.xml @@ -0,0 +1,1854 @@ + + + + SPDX-FileCopyrightText: © 2024 Isaac Freund + SPDX-License-Identifier: MIT + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + + + + This protocol allows a single "window manager" client to determine the + window management policy of the compositor. State is globally + double-buffered allowing for frame perfect state changes involving multiple + windows. + + The key words "must", "must not", "required", "shall", "shall not", + "should", "should not", "recommended", "may", and "optional" in this + document are to be interpreted as described in IETF RFC 2119. + + + + + This global interface should only be advertised to the window manager + process. Only one window management client may be active at a time. The + compositor should use the unavailable event if necessary to enforce this. + + There are two disjoint categories of state managed by this protocol: + + Window management state influences the communication between the + compositor and individual windows (e.g. xdg_toplevels). Window management + state includes window dimensions, fullscreen state, keyboard focus, + keyboard bindings, and more. + + Rendering state only affects the rendered output of the compositor and + does not influence communication between the compositor and individual + windows. Rendering state includes the position and rendering order of + windows, shell surfaces, decoration surfaces, borders, and more. + + Window management state may only be modified by the window manager as part + of a manage sequence. A manage sequence is started with the manage_start + event and ended with the manage_finish request. It is a protocol error to + modify window management state outside of a manage sequence. + + A manage sequence is always followed by at least one render sequence. A + render sequence is started with the render_start event and ended with the + render_finish request. + + Rendering state may be modified by the window manager during a manage + sequence or a render sequence. Regardless of when the rendering state is + modified, it is applied with the next render_finish request. It is a + protocol error to modify rendering state outside of a manage or render + sequence. + + The server will start a manage sequence by sending new state and the + manage_start event as soon as possible whenever there is a change in state + that must be communicated with the window manager. + + If the window manager client needs to ensure a manage sequence is started + due to a state change the compositor is not aware of, it may send the + manage_dirty request. + + The server will start a render sequence by sending new state and the + render_start event as soon as possible whenever there is a change in + window dimensions that must be communicated with the window manager. + Multiple render sequences may be made consecutively without a manage + sequence in between, for example if a window independently changes its own + dimensions. + + To summarize, the main loop of this protocol is as follows: + + 1. The server sends events indicating all changes since the last + manage sequence followed by the manage_start event. + + 2. The client sends requests modifying window management state or + rendering state (as defined above) followed by the manage_finish + request. + + 3. The server sends new state to windows and waits for responses. + + 4. The server sends new window dimensions to the client followed by the + render_start event. + + 5. The client sends requests modifying rendering state (as defined above) + followed by the render_finish request. + + 6. If window dimensions change, loop back to step 4. + If state that requires a manage sequence changes or if the client makes + a manage_dirty request, loop back to step 1. + + For the purposes of frame perfection, the server may delay rendering new + state committed by the windows in step 3 until after step 5 is finished. + + It is a protocol error for the client to make a manage_finish or + render_finish request that violates this ordering. + + + + + + + + + + + This event indicates that window management is not available to the + client, perhaps due to another window management client already running. + The circumstances causing this event to be sent are compositor policy. + + If sent, this event is guaranteed to be the first and only event sent by + the server. + + The server will send no further events on this object. The client should + destroy this object and all objects created through this interface. + + + + + + This request indicates that the client no longer wishes to receive + events on this object. + + The Wayland protocol is asynchronous, which means the server may send + further events until the stop request is processed. The client must wait + for a river_window_manager_v1.finished event before destroying this + object. + + + + + + This event indicates that the server will send no further events on this + object. The client should destroy the object. See + river_window_manager_v1.destroy for more information. + + + + + + This request should be called after the finished event has been received + to complete destruction of the object. + + If a client wishes to destroy this object it should send a + river_window_manager_v1.stop request and wait for a + river_window_manager_v1.finished event. Once the finished event is + received it is safe to destroy this object and any other objects created + through this interface. + + + + + + This event indicates that the server has sent events indicating all + state changes since the last manage sequence. + + In response to this event, the client should make requests modifying + window management state as it chooses. Then, the client must make the + manage_finish request. + + See the description of the river_window_manager_v1 interface for a + complete overview of the manage/render sequence loop. + + + + + + This request indicates that the client has made all changes to window + management state it wishes to include in the current manage sequence and + that the server should atomically send these state changes to the + windows and continue with the manage sequence. + + After sending this request, it is a protocol error for the client to + make further changes to window management state until the next + manage_start event is received. + + See the description of the river_window_manager_v1 interface for a + complete overview of the manage/render sequence loop. + + + + + + This request ensures a manage sequence is started and that a + manage_start event is sent by the server. If this request is made during + an ongoing manage sequence, a new manage sequence will be started as + soon as the current one is completed. + + The client may want to use this request due to an internal state change + that the compositor is not aware of (e.g. a dbus event) which should + affect window management or rendering state. + + + + + + This event indicates that the server has sent all river_node_v1.position + and river_window_v1.dimensions events necessary. + + In response to this event, the client should make requests modifying + rendering state as it chooses. Then, the client must make the + render_finish request. + + See the description of the river_window_manager_v1 interface for a + complete overview of the manage/render sequence loop. + + + + + + This request indicates that the client has made all changes to rendering + state it wishes to include in the current manage sequence and that the + server should atomically apply and display these state changes to the + user. + + After sending this request, it is a protocol error for the client to + make further changes to rendering state until the next manage_start or + render_start event is received, whichever comes first. + + See the description of the river_window_manager_v1 interface for a + complete overview of the manage/render sequence loop. + + + + + + This event indicates that the session has been locked. + + The window manager may wish to restrict which key bindings are available + while locked or otherwise use this information. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + This event indicates that the session has been unlocked. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + A new window has been created. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + A new logical output has been created, perhaps due to a new physical + monitor being plugged in or perhaps due to a change in configuration. + + This event will be followed by river_output_v1.position and dimensions + events as well as a manage_start event after all other new state has + been sent by the server. + + + + + + + A new seat has been created. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + Create a new shell surface for window manager UI and assign the + river_shell_surface_v1 role to the surface. + + Providing a wl_surface which already has a role or already has a buffer + attached or committed is a protocol error. + + + + + + + + End the current Wayland session and exit the compositor. + All Wayland clients running in the current session, including + the window manager, will be disconnected. + + Window managers should only make this request if the user explicitly + asks to exit the Wayland session, not for example on normal window + manager termination. + + + + + + + This represents a logical window. For example, a window may correspond to + an xdg_toplevel or Xwayland window. + + A newly created window will not be displayed until the window manager + makes a propose_dimensions or fullscreen request as part of a manage + sequence, the server replies with a dimensions event as part of a render + sequence, and that render sequence is finished. + + + + + + + + + + + + This request indicates that the client will no longer use the window + object and that it may be safely destroyed. + + This request should be made after the river_window_v1.closed event or + river_window_manager_v1.finished is received to complete destruction of + the window. + + + + + + The window has been closed by the server, perhaps due to an + xdg_toplevel.close request or similar. + + The server will send no further events on this object and ignore any + request other than river_window_v1.destroy made after this event is + sent. The client should destroy this object with the + river_window_v1.destroy request to free up resources. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + Request that the window be closed. The window may ignore this request or + only close after some delay, perhaps opening a dialog asking the user to + save their work or similar. + + The server will send a river_window_v1.closed event if/when the window + has been closed. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + Get the node in the render list corresponding to the window. + + It is a protocol error to make this request more than once for a single + window. + + + + + + + This event informs the window manager of the window's preferred min/max + dimensions. These preferences are a hint, and the window manager is free + to propose dimensions outside of these bounds. + + All min/max width/height values must be strictly greater than or equal + to 0. A value of 0 indicates that the window has no preference for that + value. + + The min_width/min_height must be strictly less than or equal to the + max_width/max_height. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + + + + This event indicates the dimensions of the window in the compositor's + logical coordinate space. The width and height must be strictly greater + than zero. + + Note that the dimensions of a river_window_v1 refer to the dimensions of + the window content and are unaffected by the presence of borders or + decoration surfaces. + + This event is sent as part of a render sequence before the render_start + event. + + It may be sent due to a propose_dimensions or fullscreen request in a + previous manage sequence or because a window independently decides to + change its dimensions. + + The window will not be displayed until the first dimensions event is + received and the render sequence is finished. + + + + + + + + This request proposes dimensions for the window in the compositor's + logical coordinate space. + + The width and height must be greater than or equal to zero. If the width + or height is zero the window will be allowed to decide its own + dimensions. + + The window may not take the exact dimensions proposed. The actual + dimensions taken by the window will be sent in a subsequent + river_window_v1.dimensions event. For example, a terminal emulator may + only allow dimensions that are multiple of the cell size. + + When a propose_dimensions request is made, the server must send a + dimensions event in response as soon as possible. It may not be possible + to send a dimensions event in the very next render sequence if, for + example, the window takes too long to respond to the proposed + dimensions. In this case, the server will send the dimensions event in a + future render sequence. + + Note that the dimensions of a river_window_v1 refer to the dimensions of + the window content and are unaffected by the presence of borders or + decoration surfaces. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + + Request that the window be hidden. Has no effect if the window is + already hidden. Hides any window borders and decorations as well. + + Newly created windows are considered shown unless explicitly hidden with + the hide request. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + Request that the window be shown. Has no effect if the window is not + hidden. Does not guarantee that the window is visible as it may be + completely obscured by other windows placed above it for example. + + Newly created windows are considered shown unless explicitly hidden with + the hide request. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + The window set an application ID. + + The app_id argument will be null if the window has never set an + application ID or if the window cleared its application ID. (Xwayland + windows may do this for example, though xdg-toplevels may not.) + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + The window set a title. + + The title argument will be null if the window has never set a title or + if the window cleared its title. (Xwayland windows may do this for + example, though xdg-toplevels may not.) + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + The window set a parent window. If this event is never received or if + the parent argument is null then the window has no parent. + + A surface with a parent set might be a dialog, file picker, or similar + for the parent window. + + Child windows should generally be rendered directly above their parent. + + The compositor must guarantee that there are no loops in the window + tree: a parent must not be the descendant of one of its children. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + + + + + + + + Information from the window about the supported and preferred client + side/server side decoration options. + + This event may be sent multiple times over the lifetime of the window if + the window changes its preferences. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + Tell the client to use client side decoration and draw its own title + bar, borders, etc. + + This is the default if neither this request nor the use_ssd request is + ever made. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + Tell the client to use server side decoration and not draw any client + side decorations. + + This request will have no effect if the client only supports client side + decoration, see the decoration_hint event. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + + + + + + + + This request decorates the window with borders drawn by the compositor + on the specified edges of the window. Borders are drawn above the window + content. + + Corners are drawn only between borders on adjacent edges. If e.g. the + left edge has a border and the top edge does not, the border drawn on + the left edge will not extend vertically beyond the top edge of the + window. + + Borders are not drawn while the window is fullscreen. + + The color is defined by four 32-bit RGBA values. Unless specified in + another protocol extension, the RGBA values use pre-multiplied alpha. + + Setting the edges to none or the width to 0 disables the borders. + Setting a negative width is a protocol error. + + This request completely overrides all previous set_borders requests. + Only the most recent set_borders request has an effect. + + Note that the position/dimensions of a river_window_v1 refer to the + position/dimensions of the window content and are unaffected by the + presence of borders or decoration surfaces. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + + + + + + + Inform the window that it is part of a tiled layout and adjacent to + other elements in the tiled layout on the given edges. + + The window should use this information to change the style of its client + side decorations and avoid drawing e.g. drop shadows outside of the + window dimensions on the tiled edges. + + Setting the edges argument to none informs the window that it is not + part of a tiled layout. If this request is never made, the window is + informed that it is not part of a tiled layout. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + Create a decoration surface and assign the river_decoration_v1 role to + the surface. The created decoration is placed above the window in + rendering order, see the description of river_decoration_v1. + + Providing a wl_surface which already has a role or already has a buffer + attached or committed is a protocol error. + + + + + + + + Create a decoration surface and assign the river_decoration_v1 role to + the surface. The created decoration is placed below the window in + rendering order, see the description of river_decoration_v1. + + Providing a wl_surface which already has a role or already has a buffer + attached or committed is a protocol error. + + + + + + + + This event informs the window manager that the window has requested to + be interactively moved using the pointer. The seat argument indicates the + seat for the move. + + The xdg-shell protocol for example allows windows to request that an + interactive move be started, perhaps when a client-side rendered + titlebar is dragged. + + The window manager may use the river_seat_v1.op_start_pointer request to + interactively move the window or ignore this event entirely. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + This event informs the window manager that the window has requested to + be interactively resized using the pointer. The seat argument indicates + the seat for the resize. + + The edges argument indicates which edges the window has requested to be + resized from. The edges argument will never be none and will never have + both top and bottom or both left and right edges set. + + The xdg-shell protocol for example allows windows to request that an + interactive resize be started, perhaps when the corner of client-side + rendered decorations is dragged. + + The window manager may use the river_seat_v1.op_start_pointer request to + interactively resize the window or ignore this event entirely. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + + Inform the window that it is being resized. The window manager should + use this request to inform windows that are the target of an interactive + resize for example. + + The window manager remains responsible for handling the position and + dimensions of the window while it is resizing. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + Inform the window that it is no longer being resized. The window manager + should use this request to inform windows that are the target of an + interactive resize that the interactive resize has ended for example. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + + + + + + + This request informs the window of the capabilities supported by the + window manager. If the window manager, for example, ignores requests to + be maximized from the window it should not tell the window that it + supports the maximize capability. + + The window might use this information to, for example, only show a + maximize button if the window manager supports the maximize capability. + + The window manager client should use this request to set capabilities + for all new windows. If this request is never made, the compositor will + inform windows that all capabilities are supported. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + The xdg-shell protocol for example allows windows to request that a + window menu be shown, for example when the user right clicks on client + side window decorations. + + A window menu might include options to maximize or minimize the window. + + The window manager is free to ignore this request and decide what the + window menu contains if it does choose to show one. + + The x and y arguments indicate where the window requested that the + window menu be shown. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + + The xdg-shell protocol for example allows windows to request to be + maximized. + + The window manager is free to honor this request using + river_window_v1.inform_maximize or ignore it. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + The xdg-shell protocol for example allows windows to request to be + unmaximized. + + The window manager is free to honor this request using + river_window_v1.inform_unmaximized or ignore it. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + Inform the window that it is maximized. The window might use this + information to adapt the style of its client-side window decorations for + example. + + The window manager remains responsible for handling the position and + dimensions of the window while it is maximized. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + Inform the window that it is unmaximized. The window might use this + information to adapt the style of its client-side window decorations for + example. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + The xdg-shell protocol for example allows windows to request that they + be made fullscreen and allows them to provide an optional output hint. + + If the output argument is null, the window has no preference and the + window manager should choose an output. + + The window manager is free to honor this request using + river_window_v1.fullscreen or ignore it. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + The xdg-shell protocol for example allows windows to request to exit + fullscreen. + + The window manager is free to honor this request using + river_window_v1.exit_fullscreen or ignore it. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + Inform the window that it is fullscreen. The window might use this + information to adapt the style of its client-side window decorations for + example. + + This request does not affect the size/position of the window or cause it + to become the only window rendered, see the river_window_v1.fullscreen + and exit_fullscreen requests for that. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + Inform the window that it is not fullscreen. The window might use this + information to adapt the style of its client-side window decorations for + example. + + This request does not affect the size/position of the window or cause it + to become the only window rendered, see the river_window_v1.fullscreen + and exit_fullscreen requests for that. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + Make the window fullscreen on the given output. If multiple windows are + fullscreen on the same output at the same time only the "top" window in + rendering order shall be displayed. + + All river_shell_surface_v1 objects above the top fullscreen window in + the rendering order will continue to be rendered. + + The compositor will handle the position and dimensions of the window + while it is fullscreen. The set_position and propose_dimensions requests + shall not affect the current position and dimensions of a fullscreen + window. + + When a fullscreen request is made, the server must send a dimensions + event in response as soon as possible. It may not be possible to send a + dimensions event in the very next render sequence if, for example, the + window takes too long to respond. In this case, the server will send the + dimensions event in a future render sequence. + + The compositor will clip window content, decoration surfaces, and + borders to the given output's dimensions while the window is fullscreen. + The effects of set_clip_box and set_content_clip_box are ignored while + the window is fullscreen. + + If the output on which a window is currently fullscreen is removed, the + windowing state is modified as if there were an exit_fullscreen request + made in the same manage sequence as the river_output_v1.removed event. + + This request does not inform the window that it is fullscreen, see the + river_window_v1.inform_fullscreen and inform_not_fullscreen requests. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + Make the window not fullscreen. + + The position and dimensions are undefined after this request is made + until a manage sequence in which the window manager makes the + propose_dimensions and set_position requests is completed. + + The window manager should make propose_dimensions and set_position + requests in the same manage sequence as the exit_fullscreen request for + frame perfection. + + This request does not inform the window that it is fullscreen, see the + river_window_v1.inform_fullscreen and inform_not_fullscreen requests. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + The xdg-shell protocol for example allows windows to request to be + minimized. + + The window manager is free to ignore this request, hide the window, or + do whatever else it chooses. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + Clip the window, including borders and decoration surfaces, to the box + specified by the x, y, width, and height arguments. The x/y position of + the box is relative to the top left corner of the window. + + The width and height arguments must be greater than or equal to 0. + + Setting a clip box with 0 width or height disables clipping. + + The clip box is ignored while the window is fullscreen. + + Both set_clip_box and set_content_clip_box may be enabled simultaneously. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + + + + + This event gives an unreliable PID of the process that created the + window. Obtaining this information is inherently racy due to PID reuse. + Therefore, this PID must not be used for anything security sensitive. + + Note also that a single process may create multiple windows, so there is + not necessarily a 1-to-1 mapping from PID to window. Multiple windows + may have the same PID. + + This event is sent once when the river_window_v1 is created and never + sent again. + + + + + + + Clip the content of the window, excluding borders and decoration + surfaces, to the box specified by the x, y, width, and height arguments. + The x/y position of the box is relative to the top left corner of the + window. + + Borders drawn by the compositor (see set_borders) are placed around the + intersection of the window content (as defined by the dimensions event) + and the content clip box when content clipping is enabled. + + The width and height arguments must be greater than or equal to 0. + + Setting a box with 0 width or height disables content clipping. + + The content clip box is ignored while the window is fullscreen. + + Both set_clip_box and set_content_clip_box may be enabled simultaneously. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + + + + + This event communicates the window's preferred presentation mode. + + This event will be followed by a render_start event after all other new + state has been sent by the server. + + + + + + + The identifier is a string that contains up to 32 printable ASCII bytes. + The identifier must not be an empty string. + + It is compositor policy how the identifier is generated, but the following + properties must be upheld: + + 1. The identifier must uniquely identify the window. Two windows must not + share the same identifier. + + 2. The identifier must not be reused. This avoids races around window + creation/destruction when identifiers are used in out-of-band IPC. + + If the compositor implements the ext-foreign-toplevel-list-v1 protocol, + the river_window_v1.identifier event must match the corresponding + ext_foreign_toplevel_handle_v1.identifier event. + + This event is sent once when the river_window_v1 is created and never + sent again. + + + + + + + Recommend that the window keep its dimensions within a given + maximum width/height. This recommendation is only a hint and the window + may ignore it. + + Setting the width and height to 0 indicates that there are no bounds + and is equivalent to having never made this request. + + Setting width or height to a negative value is a protocol error. + + The server should communicate this hint to an xdg_toplevel window with + the xdg_toplevel.configure_bounds event for example. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + + + The rendering order of windows with decorations is follows: + + 1. Decorations created with get_decoration_below at the bottom + 2. Window content + 3. Borders configured with river_window_v1.set_borders + 4. Decorations created with get_decoration_above at the top + + The relative ordering of decoration surfaces above/below a window is + undefined by this protocol and left up to the compositor. + + + + + + + + + This request indicates that the client will no longer use the decoration + object and that it may be safely destroyed. + + + + + + This request sets the offset of the decoration surface from the top left + corner of the window. + + If this request is never sent, the x and y offsets are undefined by this + protocol and left up to the compositor. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + + + Synchronize application of the next wl_surface.commit request on the + decoration surface with rest of the state atomically applied with the + next river_window_manager_v1.render_finish request. + + The client must make a wl_surface.commit request on the decoration + surface after this request and before the render_finish request, failure + to do so is a protocol error. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + + The window manager might use a shell surface to display a status bar, + background image, desktop notifications, launcher, desktop menu, or + whatever else it wants. + + + + + + + + + + This request indicates that the client will no longer use the shell + surface object and that it may be safely destroyed. + + + + + + Get the node in the render list corresponding to the shell surface. + + It is a protocol error to make this request more than once for a single + shell surface. + + + + + + + Synchronize application of the next wl_surface.commit request on the + shell surface with rest of the rendering state atomically applied with + the next river_window_manager_v1.render_finish request. + + The client must make a wl_surface.commit request on the shell surface + after this request and before the render_finish request, failure to do + so is a protocol error. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + + The render list is a list of nodes that determines the rendering order of + the compositor. Nodes may correspond to windows or shell surfaces. The + relative ordering of nodes may be changed with the place_above and + place_below requests, changing the rendering order. + + The initial position of a node in the render list is undefined, the window + manager client must use the place_above or place_below request to + guarantee a specific rendering order. + + + + + This request indicates that the client will no longer use the node + object and that it may be safely destroyed. + + + + + + Set the absolute position of the node in the compositor's logical + coordinate space. The x and y coordinates may be positive or negative. + + Note that the position of a river_window_v1 refers to the position of + the window content and is unaffected by the presence of borders or + decoration surfaces. + + If this request is never sent, the position of the node is undefined by + this protocol and left up to the compositor. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + + + This request places the node above all other nodes in the compositor's + render list. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + This request places the node below all other nodes in the compositor's + render list. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + This request places the node directly above another node in the + compositor's render list. + + Attempting to place a node above itself has no effect. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + + This request places the node directly below another node in the + compositor's render list. + + Attempting to place a node below itself has no effect. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + + + An area in the compositor's logical coordinate space that should be + treated as a single output for window management purposes. This area may + correspond to a single physical output or multiple physical outputs in the + case of mirroring or tiled monitors depending on the hardware and + compositor configuration. + + + + + This request indicates that the client will no longer use the output + object and that it may be safely destroyed. + + This request should be made after the river_output_v1.removed event is + received to complete destruction of the output. + + + + + + This event indicates that the logical output is no longer conceptually + part of window management space. + + The server will send no further events on this object and ignore any + request (other than river_output_v1.destroy) made after this event is + sent. The client should destroy this object with the + river_output_v1.destroy request to free up resources. + + This event may be sent because a corresponding physical output has been + physically unplugged or because some output configuration has changed. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + The wl_output object corresponding to the river_output_v1. The argument + is the global name of the wl_output advertised with wl_registry.global. + + It is guaranteed that the corresponding wl_output is advertised before + this event is sent. + + This event is sent exactly once. The wl_output associated with a + river_output_v1 cannot change. It is guaranteed that there is a 1-to-1 + mapping between wl_output and river_output_v1 objects. + + The global_remove event for the corresponding wl_output may be sent + before the river_output_v1.remove event. This is due to the fact that + river_output_v1 state changes are synced to the river window management + manage sequence while changes to globals are not. + + Rationale: The window manager may need information provided by the + wl_output interface such as the name/description. It also may need the + wl_output object to start screencopy for example. + + + + + + + This event indicates the position of the output in the compositor's + logical coordinate space. The x and y coordinates may be positive or + negative. + + This event is sent once when the river_output_v1 is created and again + whenever the position changes. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + The server must guarantee that the position and dimensions events do not + cause the areas of multiple logical outputs to overlap when the + corresponding manage_start event is received. + + + + + + + + This event indicates the dimensions of the output in the compositor's + logical coordinate space. The width and height will always be strictly + greater than zero. + + This event is sent once when the river_output_v1 is created and again + whenever the dimensions change. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + The server must guarantee that the position and dimensions events do not + cause the areas of multiple logical outputs to overlap when the + corresponding manage_start event is received. + + + + + + + + + + + + + Output page-flips should be synchronized to the vertical blanking + period, eliminating tearing. This is the default presentation mode. + + + + + Output page-flips should not be synchronized to the vertical blanking + period, visual screen tearing may occur. + + + + + + + Set the preferred presentation mode of the output. The compositor should + always respect the preference of the window manager if possible. If this + request is never made, the preferred presentation mode is vsync. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + + + + + + This object represents a single user's collection of input devices. It + allows the window manager to route keyboard input to windows, get + high-level information about pointer input, define pointer bindings, etc. + + For keyboard bindings, see the river-xkb-bindings-v1 protocol. + + Since version 4: The cursor surface/shape set by the window manager on the + wl_pointer of this seat is used when no client has pointer focus, for + example during a pointer operation. Since the window manager is allowed to + set cursor surface/shape even when it does not have pointer focus, the + compositor must ignore the serial argument of wl_pointer.set_cursor and + wp_cursor_shape_device_v1.set_shape requests made by the window manager. + + The most recent cursor surface/shape set by the window manager is + remembered by the compositor and restored whenever no client has pointer + focus. If the window manager never sets a cursor surface/shape, the + "default" shape is used. + + + + + This request indicates that the client will no longer use the seat + object and that it may be safely destroyed. + + This request should be made after the river_seat_v1.removed event is + received to complete destruction of the seat. + + + + + + This event indicates that seat is no longer in use and should be + destroyed. + + The server will send no further events on this object and ignore any + request (other than river_seat_v1.destroy) made after this event is + sent. The client should destroy this object with the + river_seat_v1.destroy request to free up resources. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + The wl_seat object corresponding to the river_seat_v1. The argument is + the global name of the wl_seat advertised with wl_registry.global. + + It is guaranteed that the corresponding wl_seat is advertised before + this event is sent. + + This event is sent exactly once. The wl_seat associated with a + river_seat_v1 cannot change. It is guaranteed that there is a 1-to-1 + mapping between wl_seat and river_seat_v1 objects. + + The global_remove event for the corresponding wl_seat may be sent before + the river_seat_v1.remove event. This is due to the fact that + river_seat_v1 state changes are synced to the river window management + manage sequence while changes to globals are not. + + Rationale: The window manager may want to trigger window management + state changes based on normal input events received by its shell + surfaces for example. + + + + + + + Request that the compositor send keyboard input to the given window. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + Request that the compositor send keyboard input to the given shell + surface. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + Request that the compositor not send keyboard input to any client. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + The seat's pointer entered the given window's area. + + The area of a window is defined to include the area defined by the + window dimensions, borders configured using river_window_v1.set_borders, + and the input regions of decoration surfaces. In particular, it does not + include input regions of surfaces belonging to the window that extend + outside the window dimensions. + + The pointer of a seat may only enter a single window at a time. When the + pointer moves between windows, the pointer_leave event for the old + window must be sent before the pointer_enter event for the new window. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + The seat's pointer left the window for which pointer_enter was most + recently sent. See pointer_enter for details. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + A window has been interacted with beyond the pointer merely passing over + it. This event might be sent due to a pointer button press or due to a + touch/tablet tool interaction with the window. + + There are no guarantees regarding how this event is sent in relation to + the pointer_enter and pointer_leave events as the interaction may use + touch or tablet tool input. + + Rationale: this event gives window managers necessary information to + determine when to send keyboard focus, raise a window that already has + keyboard focus, etc. Rather than expose all pointer, touch, and tablet + events to window managers, a policy over mechanism approach is taken. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + A shell surface has been interacted with beyond the pointer merely + passing over it. This event might be sent due to a pointer button press + or due to a touch/tablet tool interaction with the shell_surface. + + There are no guarantees regarding how this event is sent in relation to + the pointer_enter and pointer_leave events as the interaction may use + touch or tablet tool input. + + Rationale: While the shell surface does receive all wl_pointer, + wl_touch, etc. input events for the surface directly, these events do + not necessarily trigger a manage sequence and therefore do not allow the + window manager to update focus or perform other actions in response to + the input in a race-free way. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + Start an interactive pointer operation. During the operation, op_delta + events will be sent based on pointer input. + + When all pointer buttons are released, the op_release event is sent. + + The pointer operation continues until the op_end request is made during + a manage sequence and that manage sequence is finished. + + The window manager may use this operation to implement interactive + move/resize of windows by setting the position of windows and proposing + dimensions based off of the op_delta events. + + This request is ignored if an operation is already in progress. + + The compositor must ensure that no client has pointer focus from this + seat during the pointer operation. This means that the window manager + has control over the pointer's cursor surface/shape during the pointer + operation. See the river_seat_v1 description. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + This event indicates the total change in position since the start of the + operation of the pointer/touch point/etc. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + + The input driving the current interactive operation has been released. + For a pointer op for example, all pointer buttons have been released. + + Depending on the op type, op_delta events may continue to be sent until + the op is ended with the op_end request. + + This event is sent at most once during an interactive operation. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + End an interactive operation. + + This request is ignored if there is no operation in progress. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + This enum is used to describe the keyboard modifiers that must be held + down to trigger a key binding or pointer binding. + + Note that river and wlroots use the values 2 and 16 for capslock and + numlock internally. It doesn't make sense to use locked modifiers for + bindings however so these values are not included in this enum. + + + + + + + + + + + + + Define a pointer binding in terms of a pointer button, keyboard + modifiers, and other configurable properties. + + The button argument is a Linux input event code defined in the + linux/input-event-codes.h header file (e.g. BTN_RIGHT). + + The new pointer binding is not enabled until initial configuration is + completed and the enable request is made during a manage sequence. + + + + + + + + + Set the XCursor theme for the seat. This theme is used for cursors + rendered by the compositor, but not necessarily for cursors rendered by + clients. + + Note: The window manager may also wish to set the XCURSOR_THEME and + XCURSOR_SIZE environment variable for programs it starts. + + + + + + + + The current position of the pointer in the compositor's logical + coordinate space. + + This state is special in that a change in pointer position alone must + not cause the compositor to start a manage sequence. + + Assuming the seat has a pointer, this event must be sent in every manage + sequence unless there is no change in x/y position since the last time this + event was sent. + + + + + + + + Warp the pointer to the given position in the compositor's logical + coordinate space. + + If the given position is outside the bounds of all outputs, the pointer + will be warped to the closest point inside an output instead. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + + + This object allows the window manager to configure a pointer binding and + receive events when the binding is triggered. + + The new pointer binding is not enabled until the enable request is made + during a manage sequence. + + Normally, all pointer button events are sent to the surface with pointer + focus by the compositor. Pointer button events that trigger a pointer + binding are not sent to the surface with pointer focus. + + If multiple pointer bindings would be triggered by a single physical + pointer event on the compositor side, it is compositor policy which + pointer binding(s) will receive press/release events or if all of the + matched pointer bindings receive press/release events. + + + + + This request indicates that the client will no longer use the pointer + binding object and that it may be safely destroyed. + + + + + + This request should be made after all initial configuration has been + completed and the window manager wishes the pointer binding to be able + to be triggered. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + This request may be used to temporarily disable the pointer binding. It + may be later re-enabled with the enable request. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + This event indicates that the pointer button triggering the binding has + been pressed. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + The compositor should wait for the manage sequence to complete before + processing further input events. This allows the window manager client + to, for example, modify key bindings and keyboard focus without racing + against future input events. The window manager should of course respond + as soon as possible as the capacity of the compositor to buffer incoming + input events is finite. + + + + + + This event indicates that the pointer button triggering the binding has + been released. + + Releasing the modifiers for the binding without releasing the pointer + button does not trigger the release event. This event is sent when the + pointer button is released, even if the modifiers have changed since the + pressed event. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + The compositor should wait for the manage sequence to complete before + processing further input events. This allows the window manager client + to, for example, modify key bindings and keyboard focus without racing + against future input events. The window manager should of course respond + as soon as possible as the capacity of the compositor to buffer incoming + input events is finite. + + + + diff --git a/protocol/river-xkb-bindings-v1.xml b/protocol/river-xkb-bindings-v1.xml new file mode 100644 index 0000000..de4f100 --- /dev/null +++ b/protocol/river-xkb-bindings-v1.xml @@ -0,0 +1,268 @@ + + + + SPDX-FileCopyrightText: © 2025 Isaac Freund + SPDX-License-Identifier: MIT + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + + + + This protocol allows the river-window-management-v1 window manager to + define key bindings in terms of xkbcommon keysyms and other configurable + properties. + + The key words "must", "must not", "required", "shall", "shall not", + "should", "should not", "recommended", "may", and "optional" in this + document are to be interpreted as described in IETF RFC 2119. + + + + + This global interface should only be advertised to the client if the + river_window_manager_v1 global is also advertised. + + + + + + + + + This request indicates that the client will no longer use the + river_xkb_bindings_v1 object. + + + + + + Define a key binding for the given seat in terms of an xkbcommon keysym + and other configurable properties. + + The new key binding is not enabled until initial configuration is + completed and the enable request is made during a manage sequence. + + + + + + + + + + Create an object to manage seat-specific xkb bindings state. + + It is a protocol error to make this request more than once for a given + river_seat_v1 object. + + + + + + + + + This object allows the window manager to configure a xkbcommon key binding + and receive events when the key binding is triggered. + + The new key binding is not enabled until the enable request is made during + a manage sequence. + + Normally, all key events are sent to the surface with keyboard focus by + the compositor. Key events that trigger a key binding are not sent to the + surface with keyboard focus. + + If multiple key bindings would be triggered by a single physical key event + on the compositor side, it is compositor policy which key binding(s) will + receive press/release events or if all of the matched key bindings receive + press/release events. + + Key bindings might be matched by the same physical key event due to shared + keysym and modifiers. The layout override feature may also cause the same + physical key event to trigger two key bindings with different keysyms and + different layout overrides configured. + + + + + This request indicates that the client will no longer use the xkb key + binding object and that it may be safely destroyed. + + + + + + Specify an xkb layout that should be used to translate key events for + the purpose of triggering this key binding irrespective of the currently + active xkb layout. + + The layout argument is a 0-indexed xkbcommon layout number for the + keyboard that generated the key event. + + If this request is never made, the currently active xkb layout of the + keyboard that generated the key event will be used. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + + This request should be made after all initial configuration has been + completed and the window manager wishes the key binding to be able to be + triggered. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + This request may be used to temporarily disable the key binding. It may + be later re-enabled with the enable request. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + This event indicates that the physical key triggering the binding has + been pressed. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + The compositor should wait for the manage sequence to complete before + processing further input events. This allows the window manager client + to, for example, modify key bindings and keyboard focus without racing + against future input events. The window manager should of course respond + as soon as possible as the capacity of the compositor to buffer incoming + input events is finite. + + + + + + This event indicates that the physical key triggering the binding has + been released. + + Releasing the modifiers for the binding without releasing the "main" + physical key that produces the bound keysym does not trigger the release + event. This event is sent when the "main" key is released, even if the + modifiers have changed since the pressed event. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + The compositor should wait for the manage sequence to complete before + processing further input events. This allows the window manager client + to, for example, modify key bindings and keyboard focus without racing + against future input events. The window manager should of course respond + as soon as possible as the capacity of the compositor to buffer incoming + input events is finite. + + + + + + This event indicates that repeating should be stopped for the binding if + the window manager has been repeating some action since the pressed + event. + + This event is generally sent when some other (possible unbound) key is + pressed after the pressed event is sent and before the released event + is sent for this binding. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + + + + This object manages xkb bindings state associated with a specific seat. + + + + + This request indicates that the client will no longer use the object and + that it may be safely destroyed. + + + + + + Ensure that the next non-modifier key press and corresponding release + events for this seat are not sent to the currently focused surface. + + If the next non-modifier key press triggers a binding, the + pressed/released events are sent to the river_xkb_binding_v1 object as + usual. + + If the next non-modifier key press does not trigger a binding, the + ate_unbound_key event is sent instead. + + Rationale: the window manager may wish to implement "chorded" + keybindings where triggering a binding activates a "submap" with a + different set of keybindings. Without a way to eat the next key + press event, there is no good way for the window manager to know that it + should error out and exit the submap when a key not bound in the submap + is pressed. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + This requests cancels the effect of the latest ensure_next_key_eaten + request if no key has been eaten due to the request yet. This request + has no effect if a key has already been eaten or no + ensure_next_key_eaten was made. + + Rationale: the window manager may wish cancel an uncompleted "chorded" + keybinding after a timeout of a few seconds. Note that since this + timeout use-case requires the window manager to trigger a manage sequence + with the river_window_manager_v1.manage_dirty request it is possible that + the ate_unbound_key key event may be sent before the window manager has + a chance to make the cancel_ensure_next_key_eaten request. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + + + + An unbound key press event was eaten due to the ensure_next_key_eaten + request. + + This event will be followed by a manage_start event after all other new + state has been sent by the server. + + + + diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..0c5ce7c --- /dev/null +++ b/src/main.c @@ -0,0 +1,702 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +struct Output { + struct river_output_v1 *obj; + bool removed; + struct wl_list link; // WindowManager.outputs +}; + +struct Window { + struct river_window_v1 *obj; + struct river_node_v1 *node; + + bool new; + bool closed; + + int32_t x; + int32_t y; + int32_t width; + int32_t height; + + struct Seat *pointer_move_requested; + struct Seat *pointer_resize_requested; + uint32_t pointer_resize_requested_edges; + + struct wl_list link; // WindowManager.windows +}; + +enum Action { + ACTION_NONE, + ACTION_SPAWN_TERMINAL, + ACTION_CLOSE, + ACTION_FOCUS_NEXT, + ACTION_MOVE, + ACTION_RESIZE, + ACTION_EXIT, +}; + +struct XkbBinding { + struct river_xkb_binding_v1 *obj; + struct Seat *seat; + enum Action action; + struct wl_list link; +}; + +struct PointerBinding { + struct river_pointer_binding_v1 *obj; + struct Seat *seat; + enum Action action; + struct wl_list link; +}; + +enum SeatOp { + SEAT_OP_NONE, + SEAT_OP_MOVE, + SEAT_OP_RESIZE, +}; + +struct Seat { + struct river_seat_v1 *obj; + bool new; + bool removed; + + struct Window *focused; + struct Window *hovered; + struct Window *interacted; + + struct wl_list xkb_bindings; // XkbBinding + struct wl_list pointer_bindings; // PointerBinding + enum Action pending_action; + + enum SeatOp op; + // For SEAT_OP_MOVE and SEAT_OP_RESIZE + struct Window *op_window; + int32_t op_start_x, op_start_y; + int32_t op_dx, op_dy; + bool op_release; + // For SEAT_OP_RESIZE only + int32_t op_start_width, op_start_height; + uint32_t op_edges; + + struct wl_list link; // WindowManager.seats +}; + +struct WindowManager { + struct wl_list outputs; // Output + struct wl_list windows; // Window + struct wl_list seats; // Seat +}; + +struct WindowManager wm; + +struct river_window_manager_v1 *window_manager_v1; +struct river_xkb_bindings_v1 *xkb_bindings_v1; + +static void output_handle_removed(void *data, struct river_output_v1 *obj) { + struct Output *output = data; + output->removed = true; +} + +// Ignored events +static void output_handle_wl_output(void *data, struct river_output_v1 *obj, uint32_t name) {} +static void output_handle_position(void *data, struct river_output_v1 *obj, int32_t x, int32_t y) {} +static void output_handle_dimensions(void *data, struct river_output_v1 *obj, int32_t width, int32_t height) {} + +const struct river_output_v1_listener river_output_listener = { + .removed = output_handle_removed, + .wl_output = output_handle_wl_output, + .position = output_handle_position, + .dimensions = output_handle_dimensions, +}; + +static void output_maybe_destroy(struct Output *output) { + if (!output->removed) { + return; + } + river_output_v1_destroy(output->obj); + wl_list_remove(&output->link); + free(output); +} + +static void window_handle_closed(void *data, struct river_window_v1 *obj) { + struct Window *window = data; + window->closed = true; +} + +static void window_handle_dimensions( + void *data, struct river_window_v1 *obj, int32_t width, int32_t height) { + struct Window *window = data; + window->width = width; + window->height = height; +} + +static void window_handle_pointer_move_requested( + void *data, struct river_window_v1 *obj, struct river_seat_v1 *river_seat) { + struct Window *window = data; + window->pointer_move_requested = river_seat_v1_get_user_data(river_seat); +} + +static void window_handle_pointer_resize_requested( + void *data, struct river_window_v1 *obj, struct river_seat_v1 *river_seat, uint32_t edges) { + struct Window *window = data; + window->pointer_resize_requested = river_seat_v1_get_user_data(river_seat); + window->pointer_resize_requested_edges = edges; +} + +// Ignored events +static void window_handle_dimensions_hint(void *data, struct river_window_v1 *obj, int32_t min_width, int32_t min_height, int32_t max_width, int32_t max_height) {} +static void window_handle_app_id(void *data, struct river_window_v1 *obj, const char *app_id) {} +static void window_handle_title(void *data, struct river_window_v1 *obj, const char *title) {} +static void window_handle_parent(void *data, struct river_window_v1 *obj, struct river_window_v1 *parent) {} +static void window_handle_decoration_hint(void *data, struct river_window_v1 *obj, uint32_t hint) {} +static void window_handle_show_window_menu_requested(void *data, struct river_window_v1 *obj, int32_t x, int32_t y) {} +static void window_handle_maximize_requested(void *data, struct river_window_v1 *obj) {} +static void window_handle_unmaximize_requested(void *data, struct river_window_v1 *obj) {} +static void window_handle_fullscreen_requested(void *data, struct river_window_v1 *obj, struct river_output_v1 *river_output) {} +static void window_handle_exit_fullscreen_requested(void *data, struct river_window_v1 *obj) {} +static void window_handle_minimize_requested(void *data, struct river_window_v1 *obj) {} +static void window_handle_unreliable_pid(void *data, struct river_window_v1 *obj, int32_t unreliable_pid) {} +static void window_handle_presentation_hint(void *data, struct river_window_v1 *obj, uint32_t hint) {} +static void window_handle_identifier(void *data, struct river_window_v1 *obj, const char *indentifier) {} + +const struct river_window_v1_listener river_window_listener = { + .closed = window_handle_closed, + .dimensions_hint = window_handle_dimensions_hint, + .dimensions = window_handle_dimensions, + .app_id = window_handle_app_id, + .title = window_handle_title, + .parent = window_handle_parent, + .decoration_hint = window_handle_decoration_hint, + .pointer_move_requested = window_handle_pointer_move_requested, + .pointer_resize_requested = window_handle_pointer_resize_requested, + .show_window_menu_requested = window_handle_show_window_menu_requested, + .maximize_requested = window_handle_maximize_requested, + .unmaximize_requested = window_handle_unmaximize_requested, + .fullscreen_requested = window_handle_fullscreen_requested, + .exit_fullscreen_requested = window_handle_exit_fullscreen_requested, + .minimize_requested = window_handle_minimize_requested, + .unreliable_pid = window_handle_unreliable_pid, + .presentation_hint = window_handle_presentation_hint, + .identifier = window_handle_identifier, +}; + +static void window_maybe_destroy(struct Window *window) { + if (!window->closed) { + return; + } + + struct Seat *seat; + wl_list_for_each(seat, &wm.seats, link) { + if (seat->focused == window) { + seat->focused = NULL; + } + if (seat->op_window == window) { + river_seat_v1_op_end(seat->obj); + seat->op = SEAT_OP_NONE; + seat->op_window = NULL; + } + } + + river_window_v1_destroy(window->obj); + wl_list_remove(&window->link); + free(window); +} + +static void window_set_position(struct Window *window, int32_t x, int32_t y) { + river_node_v1_set_position(window->node, x, y); + window->x = x; + window->y = y; +} + +static void seat_pointer_move(struct Seat *seat, struct Window *window); +static void seat_pointer_resize(struct Seat *seat, struct Window *window, uint32_t edges); + +static void window_manage(struct Window *window) { + if (window->new) { + window->new = false; + window_set_position(window, 0, 0); + river_window_v1_propose_dimensions(window->obj, 0, 0); + } + if (window->pointer_move_requested != NULL) { + seat_pointer_move(window->pointer_move_requested, window); + window->pointer_move_requested = NULL; + } + if (window->pointer_resize_requested != NULL) { + seat_pointer_resize(window->pointer_resize_requested, window, + window->pointer_resize_requested_edges); + window->pointer_resize_requested = NULL; + } +} + +static void xkb_binding_handle_pressed(void *data, struct river_xkb_binding_v1 *obj) { + struct XkbBinding *binding = data; + binding->seat->pending_action = binding->action; +} + +static void xkb_binding_handle_released(void *data, struct river_xkb_binding_v1 *obj) {} + +const struct river_xkb_binding_v1_listener river_xkb_binding_listener = { + .pressed = xkb_binding_handle_pressed, + .released = xkb_binding_handle_released, +}; + +static void xkb_binding_destroy(struct XkbBinding *binding) { + river_xkb_binding_v1_destroy(binding->obj); + wl_list_remove(&binding->link); + free(binding); +} + +static void xkb_binding_create( + struct Seat *seat, uint32_t mods, xkb_keysym_t keysym, enum Action action) { + struct XkbBinding *binding = calloc(1, sizeof(struct XkbBinding)); + binding->obj = river_xkb_bindings_v1_get_xkb_binding(xkb_bindings_v1, seat->obj, keysym, mods); + binding->seat = seat; + binding->action = action; + + river_xkb_binding_v1_add_listener(binding->obj, &river_xkb_binding_listener, binding); + river_xkb_binding_v1_enable(binding->obj); + + wl_list_insert(seat->xkb_bindings.prev, &binding->link); +} + +static void pointer_binding_handle_pressed(void *data, struct river_pointer_binding_v1 *obj) { + struct PointerBinding *binding = data; + binding->seat->pending_action = binding->action; +} + +static void pointer_binding_handle_released(void *data, struct river_pointer_binding_v1 *obj) {} + +const struct river_pointer_binding_v1_listener river_pointer_binding_listener = { + .pressed = pointer_binding_handle_pressed, + .released = pointer_binding_handle_released, +}; + +static void pointer_binding_destroy(struct PointerBinding *binding) { + river_pointer_binding_v1_destroy(binding->obj); + wl_list_remove(&binding->link); + free(binding); +} + +static void pointer_binding_create( + struct Seat *seat, uint32_t mods, uint32_t button, enum Action action) { + struct PointerBinding *binding = calloc(1, sizeof(struct PointerBinding)); + binding->obj = river_seat_v1_get_pointer_binding(seat->obj, button, mods); + binding->seat = seat; + binding->action = action; + + river_pointer_binding_v1_add_listener(binding->obj, &river_pointer_binding_listener, binding); + river_pointer_binding_v1_enable(binding->obj); + + wl_list_insert(seat->pointer_bindings.prev, &binding->link); +} + +static void seat_handle_removed(void *data, struct river_seat_v1 *obj) { + struct Seat *seat = data; + seat->removed = true; +} + +static void seat_handle_pointer_enter( + void *data, struct river_seat_v1 *obj, struct river_window_v1 *river_window) { + struct Seat *seat = data; + seat->hovered = river_window_v1_get_user_data(river_window); +} + +static void seat_handle_pointer_leave(void *data, struct river_seat_v1 *obj) { + struct Seat *seat = data; + seat->hovered = NULL; +} + +static void seat_handle_window_interaction( + void *data, struct river_seat_v1 *obj, struct river_window_v1 *river_window) { + struct Seat *seat = data; + seat->interacted = river_window_v1_get_user_data(river_window); +} + +static void seat_handle_op_delta(void *data, struct river_seat_v1 *obj, int32_t dx, int32_t dy) { + struct Seat *seat = data; + seat->op_dx = dx; + seat->op_dy = dy; +} + +static void seat_handle_op_release(void *data, struct river_seat_v1 *obj) { + struct Seat *seat = data; + seat->op_release = true; +} + +// Ignored events +static void seat_handle_wl_seat(void *data, struct river_seat_v1 *obj, uint32_t id) {} +static void seat_handle_shell_surface_interaction(void *data, struct river_seat_v1 *obj, struct river_shell_surface_v1 *river_shell_surface) {} +static void seat_handle_pointer_position(void *data, struct river_seat_v1 *obj, int32_t x, int32_t y) {} + +const struct river_seat_v1_listener river_seat_listener = { + .removed = seat_handle_removed, + .wl_seat = seat_handle_wl_seat, + .pointer_enter = seat_handle_pointer_enter, + .pointer_leave = seat_handle_pointer_leave, + .window_interaction = seat_handle_window_interaction, + .shell_surface_interaction = seat_handle_shell_surface_interaction, + .op_delta = seat_handle_op_delta, + .op_release = seat_handle_op_release, + .pointer_position = seat_handle_pointer_position, +}; + +static void seat_maybe_destroy(struct Seat *seat) { + if (!seat->removed) { + return; + } + + struct XkbBinding *xkb_binding, *xkb_binding_tmp; + wl_list_for_each_safe(xkb_binding, xkb_binding_tmp, &seat->xkb_bindings, link) { + xkb_binding_destroy(xkb_binding); + } + + struct PointerBinding *pointer_binding, *pointer_binding_tmp; + wl_list_for_each_safe(pointer_binding, pointer_binding_tmp, &seat->pointer_bindings, link) { + pointer_binding_destroy(pointer_binding); + } + + river_seat_v1_destroy(seat->obj); + wl_list_remove(&seat->link); + free(seat); +} + +static void seat_focus(struct Seat *seat, struct Window *window) { + // Focus the top window (if any) when there is no explicit target. + if (window == NULL && !wl_list_empty(&wm.windows)) { + window = wl_container_of(wm.windows.prev, window, link); + } + + if (seat->focused == window) { + return; + } + + if (window != NULL) { + river_seat_v1_focus_window(seat->obj, window->obj); + river_node_v1_place_top(window->node); + wl_list_remove(&window->link); + wl_list_insert(wm.windows.prev, &window->link); + } else { + river_seat_v1_clear_focus(seat->obj); + } + + seat->focused = window; +} + +static void seat_pointer_move(struct Seat *seat, struct Window *window) { + seat_focus(seat, window); + river_seat_v1_op_start_pointer(seat->obj); + seat->op = SEAT_OP_MOVE; + seat->op_window = window; + seat->op_start_x = window->x; + seat->op_start_y = window->y; + seat->op_dx = 0; + seat->op_dy = 0; +} + +static void seat_pointer_resize(struct Seat *seat, struct Window *window, uint32_t edges) { + seat_focus(seat, window); + river_window_v1_inform_resize_start(window->obj); + river_seat_v1_op_start_pointer(seat->obj); + seat->op = SEAT_OP_RESIZE; + seat->op_window = window; + seat->op_edges = edges; + seat->op_start_x = window->x; + seat->op_start_y = window->y; + seat->op_start_width = window->width; + seat->op_start_height = window->height; + seat->op_dx = 0; + seat->op_dy = 0; +} + +static void seat_action(struct Seat *seat, enum Action action) { + switch (action) { + case ACTION_NONE: + break; + case ACTION_SPAWN_TERMINAL: + if (fork() == 0) { + execlp("kitty", "kitty", (char *)0); + } + break; + case ACTION_CLOSE: + if (seat->focused != NULL) { + river_window_v1_close(seat->focused->obj); + } + break; + case ACTION_FOCUS_NEXT: + if (!wl_list_empty(&wm.windows)) { + // Focus the bottom window + struct Window *window = wl_container_of(wm.windows.next, window, link); + seat_focus(seat, window); + } + break; + case ACTION_MOVE: + if (seat->op == SEAT_OP_NONE && seat->hovered != NULL) { + seat_pointer_move(seat, seat->hovered); + } + break; + case ACTION_RESIZE: + if (seat->op == SEAT_OP_NONE && seat->hovered != NULL) { + seat_pointer_resize(seat, seat->hovered, + RIVER_WINDOW_V1_EDGES_BOTTOM | RIVER_WINDOW_V1_EDGES_RIGHT); + } + break; + case ACTION_EXIT: + river_window_manager_v1_exit_session(window_manager_v1); + break; + } +} + +static void seat_manage(struct Seat *seat) { + if (seat->new) { + seat->new = false; + const uint32_t super = RIVER_SEAT_V1_MODIFIERS_MOD4; + xkb_binding_create(seat, super, XKB_KEY_space, ACTION_SPAWN_TERMINAL); + xkb_binding_create(seat, super, XKB_KEY_q, ACTION_CLOSE); + xkb_binding_create(seat, super, XKB_KEY_n, ACTION_FOCUS_NEXT); + xkb_binding_create(seat, super, XKB_KEY_Escape, ACTION_EXIT); + pointer_binding_create(seat, super, BTN_LEFT, ACTION_MOVE); + pointer_binding_create(seat, super, BTN_RIGHT, ACTION_RESIZE); + } + + // If no window was interacted with in the current manage sequence, + // intentionally pass NULL to ensure the window on top has focus. + // This is necessary to handle new windows for example. + seat_focus(seat, seat->interacted); + seat->interacted = NULL; + + seat_action(seat, seat->pending_action); + seat->pending_action = ACTION_NONE; + + switch (seat->op) { + case SEAT_OP_NONE: + break; + case SEAT_OP_MOVE: + if (seat->op_release) { + river_seat_v1_op_end(seat->obj); + seat->op = SEAT_OP_NONE; + seat->op_window = NULL; + break; + } + break; + case SEAT_OP_RESIZE: + if (seat->op_release) { + river_window_v1_inform_resize_end(seat->op_window->obj); + river_seat_v1_op_end(seat->obj); + seat->op = SEAT_OP_NONE; + seat->op_window = NULL; + break; + } + int32_t width = seat->op_start_width; + int32_t height = seat->op_start_height; + if ((seat->op_edges & RIVER_WINDOW_V1_EDGES_LEFT) != 0) { + width -= seat->op_dx; + } + if ((seat->op_edges & RIVER_WINDOW_V1_EDGES_RIGHT) != 0) { + width += seat->op_dx; + } + if ((seat->op_edges & RIVER_WINDOW_V1_EDGES_TOP) != 0) { + height -= seat->op_dy; + } + if ((seat->op_edges & RIVER_WINDOW_V1_EDGES_BOTTOM) != 0) { + height += seat->op_dy; + } + river_window_v1_propose_dimensions( + seat->op_window->obj, width > 1 ? width : 1, height > 1 ? height : 1); + break; + } + seat->op_release = false; +} + +static void seat_render(struct Seat *seat) { + switch (seat->op) { + case SEAT_OP_NONE: + break; + case SEAT_OP_MOVE: + window_set_position( + seat->op_window, seat->op_start_x + seat->op_dx, seat->op_start_y + seat->op_dy); + break; + case SEAT_OP_RESIZE:; + int32_t x = seat->op_start_x; + int32_t y = seat->op_start_y; + if ((seat->op_edges & RIVER_WINDOW_V1_EDGES_LEFT) != 0) { + x += seat->op_start_width - seat->op_window->width; + } + if ((seat->op_edges & RIVER_WINDOW_V1_EDGES_TOP) != 0) { + y += seat->op_start_height - seat->op_window->height; + } + window_set_position(seat->op_window, x, y); + break; + } +} + +static void wm_handle_unavailable(void *data, struct river_window_manager_v1 *obj) { + fprintf(stderr, "error: another window manager is already running\n"); + exit(1); +} + +static void wm_handle_finished(void *data, struct river_window_manager_v1 *obj) { + exit(0); +} + +static void wm_handle_manage_start(void *data, struct river_window_manager_v1 *obj) { + // Destroy closed windows and removed outputs/seats + struct Output *output, *output_tmp; + wl_list_for_each_safe(output, output_tmp, &wm.outputs, link) { + output_maybe_destroy(output); + } + struct Window *window, *window_tmp; + wl_list_for_each_safe(window, window_tmp, &wm.windows, link) { + window_maybe_destroy(window); + } + struct Seat *seat, *seat_tmp; + wl_list_for_each_safe(seat, seat_tmp, &wm.seats, link) { + seat_maybe_destroy(seat); + } + + // Carry out window management policy + wl_list_for_each(window, &wm.windows, link) { + window_manage(window); + } + wl_list_for_each(seat, &wm.seats, link) { + seat_manage(seat); + } + + river_window_manager_v1_manage_finish(window_manager_v1); +} + +static void wm_handle_render_start(void *data, struct river_window_manager_v1 *obj) { + struct Seat *seat; + wl_list_for_each(seat, &wm.seats, link) { + seat_render(seat); + } + + river_window_manager_v1_render_finish(window_manager_v1); +} + +static void wm_handle_window( + void *data, struct river_window_manager_v1 *obj, struct river_window_v1 *river_window) { + struct Window *window = calloc(1, sizeof(struct Window)); + window->obj = river_window; + window->node = river_window_v1_get_node(window->obj); + window->new = true; + + river_window_v1_add_listener(window->obj, &river_window_listener, window); + + wl_list_insert(wm.windows.prev, &window->link); +} + +static void wm_handle_output( + void *data, struct river_window_manager_v1 *obj, struct river_output_v1 *river_output) { + struct Output *output = calloc(1, sizeof(struct Output)); + output->obj = river_output; + + river_output_v1_add_listener(output->obj, &river_output_listener, output); + + wl_list_insert(wm.outputs.prev, &output->link); +} + +static void wm_handle_seat( + void *data, struct river_window_manager_v1 *obj, struct river_seat_v1 *river_seat) { + struct Seat *seat = calloc(1, sizeof(struct Seat)); + seat->obj = river_seat; + seat->new = true; + wl_list_init(&seat->xkb_bindings); + wl_list_init(&seat->pointer_bindings); + + river_seat_v1_add_listener(seat->obj, &river_seat_listener, seat); + + wl_list_insert(wm.seats.prev, &seat->link); +} + +// Ignored events +static void wm_handle_session_locked(void *data, struct river_window_manager_v1 *obj) {} +static void wm_handle_session_unlocked(void *data, struct river_window_manager_v1 *obj) {} + +static const struct river_window_manager_v1_listener wm_listener = { + .unavailable = wm_handle_unavailable, + .finished = wm_handle_finished, + .manage_start = wm_handle_manage_start, + .render_start = wm_handle_render_start, + .session_locked = wm_handle_session_locked, + .session_unlocked = wm_handle_session_unlocked, + .window = wm_handle_window, + .output = wm_handle_output, + .seat = wm_handle_seat, +}; + +static void wm_init(void) { + wl_list_init(&wm.outputs); + wl_list_init(&wm.windows); + wl_list_init(&wm.seats); +} + +static void handle_global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) { + if (strcmp(interface, river_window_manager_v1_interface.name) == 0) { + if (version >= 4) { + window_manager_v1 = wl_registry_bind(registry, name, &river_window_manager_v1_interface, 4); + } + } else if (strcmp(interface, river_xkb_bindings_v1_interface.name) == 0) { + xkb_bindings_v1 = wl_registry_bind(registry, name, &river_xkb_bindings_v1_interface, 1); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) {} + +static const struct wl_registry_listener registry_listener = { + .global = handle_global, + .global_remove = handle_global_remove, +}; + +int main(void) { + struct wl_display *display = wl_display_connect(NULL); + if (display == NULL) { + fprintf(stderr, "failed to connect to Wayland server\n"); + return 1; + } + + // Avoid passing WAYLAND_DEBUG on to our children. + // It only matters if it's set when the display is created. + unsetenv("WAYLAND_DEBUG"); + + // Ensure children are automatically reaped. + signal(SIGCHLD, SIG_IGN); + + struct wl_registry *registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, NULL); + if (wl_display_roundtrip(display) < 0) { + fprintf(stderr, "roundtrip failed\n"); + return 1; + } + + if (window_manager_v1 == NULL || xkb_bindings_v1 == NULL) { + fprintf(stderr, + "river_window_manager_v1 or river_xkb_bindings_v1 " + "not supported by the Wayland server\n"); + return 1; + } + + wm_init(); + + river_window_manager_v1_add_listener(window_manager_v1, &wm_listener, NULL); + + while (true) { + if (wl_display_dispatch(display) < 0) { + fprintf(stderr, "dispatch failed\n"); + return 1; + } + } + + return 0; +}