git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit7df3c2608bc439b0ee0a04b69756beb6d2d858e0
parenta47d34dd9c
authorIsaac Freund <[email protected]>
date2024-07-17 17:42
river: rename view -> window

This terminology was cargo-culted from sway, which in turn cargo-culted
it from weston. The terminology doesn't even make sense in weston.

 river/Config.zig                   |   2 +-
 river/Cursor.zig                   |  76 +++++-----
 river/ForeignToplevelHandle.zig    |  30 ++--
 river/IdleInhibitManager.zig       |   4 +-
 river/InputPopup.zig               |   4 +-
 river/Output.zig                   |  16 +-
 river/PointerConstraint.zig        |   2 +-
 river/Root.zig                     |  74 ++++-----
 river/SceneNodeData.zig            |   4 +-
 river/Seat.zig                     |  36 ++---
 river/Server.zig                   |   8 +-
 river/{View.zig => Window.zig}     | 286 +++++++++++++++++------------------
 river/XdgDecoration.zig            |   6 +-
 river/XdgPopup.zig                 |   4 +-
 river/XdgToplevel.zig              | 122 +++++++--------
 river/XwaylandOverrideRedirect.zig |  20 +--
 river/XwaylandView.zig             | 297 -------------------------------------
 river/XwaylandWindow.zig           | 297 +++++++++++++++++++++++++++++++++++++
 18 files changed, 644 insertions(+), 644 deletions(-)

diff --git a/river/Config.zig b/river/Config.zig
index 0e7d015..fd1b129 100644
--- a/river/Config.zig
+++ b/river/Config.zig
@@ -30,7 +30,7 @@ const Server = @import("Server.zig");
 const Output = @import("Output.zig");
 const Mapping = @import("Mapping.zig");
 const Switch = @import("Switch.zig");
-const View = @import("View.zig");
+const Window = @import("Window.zig");
 
 pub const Position = struct {
     x: u31,
diff --git a/river/Cursor.zig b/river/Cursor.zig
index 10a3327..786fb94 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -41,7 +41,7 @@ const Root = @import("Root.zig");
 const Seat = @import("Seat.zig");
 const Tablet = @import("Tablet.zig");
 const TabletTool = @import("TabletTool.zig");
-const View = @import("View.zig");
+const Window = @import("Window.zig");
 const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
 
 const Mode = union(enum) {
@@ -60,9 +60,9 @@ const Mode = union(enum) {
         sy: f64,
     },
     move: struct {
-        view: *View,
+        window: *Window,
 
-        /// View coordinates are stored as i32s as they are in logical pixels.
+        /// Window coordinates are stored as i32s as they are in logical pixels.
         /// However, it is possible to move the cursor by a fraction of a
         /// logical pixel and this happens in practice with low dpi, high
         /// polling rate mice. Therefore we must accumulate the current
@@ -77,13 +77,13 @@ const Mode = union(enum) {
         offset_y: i32,
     },
     resize: struct {
-        view: *View,
+        window: *Window,
 
         delta_x: f64 = 0,
         delta_y: f64 = 0,
 
         /// Total x/y movement of the pointer device since the start of the resize,
-        /// clamped to the bounds of the resize as defined by the view min/max
+        /// clamped to the bounds of the resize as defined by the window min/max
         /// dimensions and output dimensions.
         /// This is not directly tied to the rendered cursor position.
         x: i32 = 0,
@@ -116,7 +116,7 @@ mode: Mode = .passthrough,
 /// Set to whatever the current mode is when a transaction is started.
 /// This is necessary to handle termination of move/resize modes properly
 /// since the termination is not complete until a transaction completes and
-/// View.resizeUpdatePosition() is called.
+/// Window.resizeUpdatePosition() is called.
 inflight_mode: Mode = .passthrough,
 
 seat: *Seat,
@@ -332,7 +332,7 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
                     _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
                 },
                 .move => {},
-                .resize => |data| data.view.pending.resizing = false,
+                .resize => |data| data.window.pending.resizing = false,
             }
 
             cursor.mode = .passthrough;
@@ -354,7 +354,7 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
     }
 
     if (server.root.at(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) |result| {
-        if (result.data == .view and cursor.handlePointerMapping(event, result.data.view)) {
+        if (result.data == .window and cursor.handlePointerMapping(event, result.data.window)) {
             // If a mapping is triggered don't send events to clients.
             return;
         }
@@ -381,8 +381,8 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
 /// Requires a call to Root.applyPending()
 fn updateKeyboardFocus(cursor: Cursor, result: Root.AtResult) void {
     switch (result.data) {
-        .view => |view| {
-            cursor.seat.focus(view);
+        .window => |window| {
+            cursor.seat.focus(window);
         },
         .layer_surface => |layer_surface| {
             // If a keyboard inteactive layer surface has been clicked on,
@@ -626,7 +626,7 @@ fn handleTabletToolButton(
 
 /// Handle the mapping for the passed button if any. Returns true if there
 /// was a mapping and the button was handled.
-fn handlePointerMapping(cursor: *Cursor, event: *wlr.Pointer.event.Button, _: *View) bool {
+fn handlePointerMapping(cursor: *Cursor, event: *wlr.Pointer.event.Button, _: *Window) bool {
     const wlr_keyboard = cursor.seat.wlr_seat.getKeyboard() orelse return false;
     const modifiers = wlr_keyboard.getModifiers();
 
@@ -703,20 +703,20 @@ fn handleRequestSetCursor(
     }
 }
 
-pub fn startMove(cursor: *Cursor, view: *View) void {
+pub fn startMove(cursor: *Cursor, window: *Window) void {
     if (cursor.constraint) |constraint| {
         if (constraint.state == .active) constraint.deactivate();
     }
 
     const new_mode: Mode = .{ .move = .{
-        .view = view,
-        .offset_x = @as(i32, @intFromFloat(cursor.wlr_cursor.x)) - view.current.box.x,
-        .offset_y = @as(i32, @intFromFloat(cursor.wlr_cursor.y)) - view.current.box.y,
+        .window = window,
+        .offset_x = @as(i32, @intFromFloat(cursor.wlr_cursor.x)) - window.current.box.x,
+        .offset_y = @as(i32, @intFromFloat(cursor.wlr_cursor.y)) - window.current.box.y,
     } };
-    cursor.enterMode(new_mode, view, "move");
+    cursor.enterMode(new_mode, window, "move");
 }
 
-pub fn startResize(cursor: *Cursor, view: *View, proposed_edges: ?wlr.Edges) void {
+pub fn startResize(cursor: *Cursor, window: *Window, proposed_edges: ?wlr.Edges) void {
     if (cursor.constraint) |constraint| {
         if (constraint.state == .active) constraint.deactivate();
     }
@@ -727,31 +727,31 @@ pub fn startResize(cursor: *Cursor, view: *View, proposed_edges: ?wlr.Edges) voi
                 break :blk edges;
             }
         }
-        break :blk cursor.computeEdges(view);
+        break :blk cursor.computeEdges(window);
     };
 
-    const box = &view.current.box;
+    const box = &window.current.box;
     const lx: i32 = @intFromFloat(cursor.wlr_cursor.x);
     const ly: i32 = @intFromFloat(cursor.wlr_cursor.y);
     const offset_x = if (edges.left) lx - box.x else box.x + box.width - lx;
     const offset_y = if (edges.top) ly - box.y else box.y + box.height - ly;
 
-    view.pending.resizing = true;
+    window.pending.resizing = true;
 
     const new_mode: Mode = .{ .resize = .{
-        .view = view,
+        .window = window,
         .edges = edges,
         .offset_x = offset_x,
         .offset_y = offset_y,
         .initial_width = @intCast(box.width),
         .initial_height = @intCast(box.height),
     } };
-    cursor.enterMode(new_mode, view, wlr.Xcursor.getResizeName(edges));
+    cursor.enterMode(new_mode, window, wlr.Xcursor.getResizeName(edges));
 }
 
-fn computeEdges(cursor: *const Cursor, view: *const View) wlr.Edges {
+fn computeEdges(cursor: *const Cursor, window: *const Window) wlr.Edges {
     const min_handle_size = 20;
-    const box = &view.current.box;
+    const box = &window.current.box;
 
     const sx = @as(i32, @intFromFloat(cursor.wlr_cursor.x)) - box.x;
     const sy = @as(i32, @intFromFloat(cursor.wlr_cursor.y)) - box.y;
@@ -783,7 +783,7 @@ fn computeEdges(cursor: *const Cursor, view: *const View) wlr.Edges {
     }
 }
 
-fn enterMode(cursor: *Cursor, mode: Mode, view: *View, xcursor_name: [*:0]const u8) void {
+fn enterMode(cursor: *Cursor, mode: Mode, window: *Window, xcursor_name: [*:0]const u8) void {
     assert(cursor.mode == .passthrough or cursor.mode == .down);
     assert(mode == .move or mode == .resize);
 
@@ -791,7 +791,7 @@ fn enterMode(cursor: *Cursor, mode: Mode, view: *View, xcursor_name: [*:0]const
 
     cursor.mode = mode;
 
-    cursor.seat.focus(view);
+    cursor.seat.focus(window);
 
     cursor.seat.wlr_seat.pointerNotifyClearFocus();
     cursor.setXcursor(xcursor_name);
@@ -851,7 +851,7 @@ fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x:
             data.delta_x = dx - @trunc(dx);
             data.delta_y = dy - @trunc(dy);
 
-            data.view.pending.move(@intFromFloat(dx), @intFromFloat(dy));
+            data.window.pending.move(@intFromFloat(dx), @intFromFloat(dy));
 
             server.root.applyPending();
         },
@@ -865,16 +865,16 @@ fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x:
             data.y += @intFromFloat(dy);
 
             // Modify width/height of the pending box, taking constraints into account
-            // The x/y coordinates of the view will be adjusted as needed in View.resizeCommit()
+            // The x/y coordinates of the window will be adjusted as needed in Window.resizeCommit()
             // based on the dimensions actually committed by the client.
-            const border_width = if (data.view.pending.ssd) server.config.border_width else 0;
+            const border_width = if (data.window.pending.ssd) server.config.border_width else 0;
 
             // TODO
             const output_width: i32 = 1920;
             const output_height: i32 = 1080;
 
-            const constraints = &data.view.constraints;
-            const box = &data.view.pending.box;
+            const constraints = &data.window.constraints;
+            const box = &data.window.pending.box;
 
             if (data.edges.left) {
                 const x2 = box.x + box.width;
@@ -911,8 +911,8 @@ fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x:
     }
 }
 
-/// Handle potential change in location of views on the output, as well as
-/// the target view of a cursor operation potentially being moved to a non-visible tag,
+/// Handle potential change in location of windows on the output, as well as
+/// the target window of a cursor operation potentially being moved to a non-visible tag,
 /// becoming fullscreen, etc.
 pub fn updateState(cursor: *Cursor) void {
     if (cursor.constraint) |constraint| {
@@ -930,7 +930,7 @@ pub fn updateState(cursor: *Cursor) void {
         // TODO: Leave down mode if the target surface is no longer visible.
         .down => {},
         .move, .resize => {
-            // Moving and resizing of views is handled through the transaction system. Therefore,
+            // Moving and resizing of windows is handled through the transaction system. Therefore,
             // we must inspect the inflight_mode instead if a move or a resize is in progress.
             //
             // The cases when a move/resize is being started or ended and e.g. mode is resize
@@ -942,17 +942,17 @@ pub fn updateState(cursor: *Cursor) void {
             //
             // In the second case, a move/resize has been terminated by the user but the
             // transaction carrying out the final size/position change is still inflight.
-            // Therefore, the user already expects the cursor to be free from the view and
+            // Therefore, the user already expects the cursor to be free from the window and
             // we should not warp it back to the fixed offset of the move/resize.
             switch (cursor.inflight_mode) {
                 .passthrough, .down => {},
                 inline .move, .resize => |data, mode| {
 
                     // These conditions are checked in Root.applyPending()
-                    assert(!data.view.current.fullscreen);
+                    assert(!data.window.current.fullscreen);
 
-                    // Keep the cursor locked to the original offset from the edges of the view.
-                    const box = &data.view.current.box;
+                    // Keep the cursor locked to the original offset from the edges of the window.
+                    const box = &data.window.current.box;
                     const new_x: f64 = blk: {
                         if (mode == .move or data.edges.left) {
                             break :blk @floatFromInt(data.offset_x + box.x);
diff --git a/river/ForeignToplevelHandle.zig b/river/ForeignToplevelHandle.zig
index 97a1c90..f6e0555 100644
--- a/river/ForeignToplevelHandle.zig
+++ b/river/ForeignToplevelHandle.zig
@@ -23,7 +23,7 @@ const wl = @import("wayland").server.wl;
 
 const server = &@import("main.zig").server;
 
-const View = @import("View.zig");
+const Window = @import("Window.zig");
 const Seat = @import("Seat.zig");
 
 wlr_handle: ?*wlr.ForeignToplevelHandleV1 = null,
@@ -36,7 +36,7 @@ foreign_close: wl.Listener(*wlr.ForeignToplevelHandleV1) =
     wl.Listener(*wlr.ForeignToplevelHandleV1).init(handleForeignClose),
 
 pub fn map(handle: *ForeignToplevelHandle) void {
-    const view: *View = @fieldParentPtr("foreign_toplevel_handle", handle);
+    const window: *Window = @fieldParentPtr("foreign_toplevel_handle", handle);
 
     assert(handle.wlr_handle == null);
 
@@ -49,8 +49,8 @@ pub fn map(handle: *ForeignToplevelHandle) void {
     handle.wlr_handle.?.events.request_fullscreen.add(&handle.foreign_fullscreen);
     handle.wlr_handle.?.events.request_close.add(&handle.foreign_close);
 
-    if (view.getTitle()) |title| handle.wlr_handle.?.setTitle(title);
-    if (view.getAppId()) |app_id| handle.wlr_handle.?.setAppId(app_id);
+    if (window.getTitle()) |title| handle.wlr_handle.?.setTitle(title);
+    if (window.getAppId()) |app_id| handle.wlr_handle.?.setAppId(app_id);
 }
 
 pub fn unmap(handle: *ForeignToplevelHandle) void {
@@ -65,27 +65,27 @@ pub fn unmap(handle: *ForeignToplevelHandle) void {
     handle.wlr_handle = null;
 }
 
-/// Must be called just before the view's inflight state is made current.
+/// Must be called just before the window's inflight state is made current.
 pub fn update(handle: *ForeignToplevelHandle) void {
-    const view: *View = @fieldParentPtr("foreign_toplevel_handle", handle);
+    const window: *Window = @fieldParentPtr("foreign_toplevel_handle", handle);
 
     const wlr_handle = handle.wlr_handle orelse return;
 
-    wlr_handle.setActivated(view.inflight.focus != 0);
-    wlr_handle.setFullscreen(view.inflight.fullscreen);
+    wlr_handle.setActivated(window.inflight.focus != 0);
+    wlr_handle.setFullscreen(window.inflight.fullscreen);
 }
 
-/// Only honors the request if the view is already visible on the seat's
+/// Only honors the request if the window is already visible on the seat's
 /// currently focused output.
 fn handleForeignActivate(
     listener: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated),
     event: *wlr.ForeignToplevelHandleV1.event.Activated,
 ) void {
     const handle: *ForeignToplevelHandle = @fieldParentPtr("foreign_activate", listener);
-    const view: *View = @fieldParentPtr("foreign_toplevel_handle", handle);
+    const window: *Window = @fieldParentPtr("foreign_toplevel_handle", handle);
     const seat: *Seat = @ptrFromInt(event.seat.data);
 
-    seat.focus(view);
+    seat.focus(window);
     server.root.applyPending();
 }
 
@@ -94,9 +94,9 @@ fn handleForeignFullscreen(
     event: *wlr.ForeignToplevelHandleV1.event.Fullscreen,
 ) void {
     const handle: *ForeignToplevelHandle = @fieldParentPtr("foreign_fullscreen", listener);
-    const view: *View = @fieldParentPtr("foreign_toplevel_handle", handle);
+    const window: *Window = @fieldParentPtr("foreign_toplevel_handle", handle);
 
-    view.pending.fullscreen = event.fullscreen;
+    window.pending.fullscreen = event.fullscreen;
     server.root.applyPending();
 }
 
@@ -105,7 +105,7 @@ fn handleForeignClose(
     _: *wlr.ForeignToplevelHandleV1,
 ) void {
     const handle: *ForeignToplevelHandle = @fieldParentPtr("foreign_close", listener);
-    const view: *View = @fieldParentPtr("foreign_toplevel_handle", handle);
+    const window: *Window = @fieldParentPtr("foreign_toplevel_handle", handle);
 
-    view.close();
+    window.close();
 }
diff --git a/river/IdleInhibitManager.zig b/river/IdleInhibitManager.zig
index 73a0141..dfda3ab 100644
--- a/river/IdleInhibitManager.zig
+++ b/river/IdleInhibitManager.zig
@@ -25,7 +25,7 @@ const util = @import("util.zig");
 
 const IdleInhibitor = @import("IdleInhibitor.zig");
 const SceneNodeData = @import("SceneNodeData.zig");
-const View = @import("View.zig");
+const Window = @import("Window.zig");
 
 wlr_manager: *wlr.IdleInhibitManagerV1,
 new_idle_inhibitor: wl.Listener(*wlr.IdleInhibitorV1) =
@@ -53,7 +53,7 @@ pub fn checkActive(inhibit_manager: *IdleInhibitManager) void {
     while (it) |node| : (it = node.next) {
         const node_data = SceneNodeData.fromSurface(node.data.wlr_inhibitor.surface) orelse continue;
         switch (node_data.data) {
-            .view => {
+            .window => {
                 inhibited = true;
                 break;
             },
diff --git a/river/InputPopup.zig b/river/InputPopup.zig
index 6503ddf..19abbf8 100644
--- a/river/InputPopup.zig
+++ b/river/InputPopup.zig
@@ -108,7 +108,7 @@ pub fn update(input_popup: *InputPopup) void {
     const focused = SceneNodeData.fromSurface(focused_surface) orelse return;
 
     const output = switch (focused.data) {
-        .view => @panic("TODO"),
+        .window => @panic("TODO"),
         .layer_surface => |layer_surface| layer_surface.output,
         .lock_surface => |lock_surface| lock_surface.getOutput(),
         // Xwayland doesn't use the text-input protocol
@@ -116,7 +116,7 @@ pub fn update(input_popup: *InputPopup) void {
     };
 
     const popup_tree = switch (focused.data) {
-        .view => |view| view.popup_tree,
+        .window => |window| window.popup_tree,
         .layer_surface => |layer_surface| layer_surface.popup_tree,
         .lock_surface => |lock_surface| lock_surface.getOutput().layers.popups,
         // Xwayland doesn't use the text-input protocol
diff --git a/river/Output.zig b/river/Output.zig
index d8994bd..7a07f6d 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -33,7 +33,7 @@ const util = @import("util.zig");
 const LayerSurface = @import("LayerSurface.zig");
 const LockSurface = @import("LockSurface.zig");
 const SceneNodeData = @import("SceneNodeData.zig");
-const View = @import("View.zig");
+const Window = @import("Window.zig");
 const Config = @import("Config.zig");
 
 const log = std.log.scoped(.output);
@@ -47,7 +47,7 @@ all_link: wl.list.Link,
 /// For Root.active_outputs
 active_link: wl.list.Link,
 
-/// The area left for views and other layer surfaces after applying the
+/// The area left for windows and other layer surfaces after applying the
 /// exclusive zones of exclusive layer surfaces.
 /// TODO: this should be part of the output's State
 usable_box: wlr.Box,
@@ -69,7 +69,7 @@ layers: struct {
     wm: *wlr.SceneTree,
     /// Top layer shell layer
     top: *wlr.SceneTree,
-    /// Fullscreen views
+    /// Fullscreen windows
     fullscreen: *wlr.SceneTree,
     /// Overlay layer shell layer
     overlay: *wlr.SceneTree,
@@ -106,14 +106,14 @@ gamma_dirty: bool = false,
 /// This state is immutable until all clients have replied and the transaction
 /// is completed, at which point this inflight state is copied to current.
 inflight: struct {
-    /// The view to be made fullscreen, if any.
-    fullscreen: ?*View = null,
+    /// The window to be made fullscreen, if any.
+    fullscreen: ?*Window = null,
 } = .{},
 
 /// The current state represented by the scene graph.
 current: struct {
-    /// The currently fullscreen view, if any.
-    fullscreen: ?*View = null,
+    /// The currently fullscreen window, if any.
+    fullscreen: ?*Window = null,
 } = .{},
 
 destroy: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleDestroy),
@@ -236,7 +236,7 @@ pub fn layerSurfaceTree(output: Output, layer: zwlr.LayerShellV1.Layer) *wlr.Sce
 }
 
 /// Arrange all layer surfaces of this output and adjust the usable area.
-/// Will arrange views as well if the usable area changes.
+/// Will arrange windows as well if the usable area changes.
 /// Requires a call to Root.applyPending()
 pub fn arrangeLayers(output: *Output) void {
     var full_box: wlr.Box = .{
diff --git a/river/PointerConstraint.zig b/river/PointerConstraint.zig
index a2c4d74..7caeb54 100644
--- a/river/PointerConstraint.zig
+++ b/river/PointerConstraint.zig
@@ -127,7 +127,7 @@ pub fn updateState(constraint: *PointerConstraint) void {
     }
 
     // It is possible for the cursor to end up outside of the constraint region despite the warp
-    // if, for example, the a keybinding is used to resize the view.
+    // if, for example, the a keybinding is used to resize the window.
     if (!constraint.wlr_constraint.region.containsPoint(@intFromFloat(sx), @intFromFloat(sy), null)) {
         log.info("deactivating pointer constraint, cursor outside region despite warp", .{});
         constraint.deactivate();
diff --git a/river/Root.zig b/river/Root.zig
index 22bd4f3..262233a 100644
--- a/river/Root.zig
+++ b/river/Root.zig
@@ -32,7 +32,7 @@ const LayerSurface = @import("LayerSurface.zig");
 const LockSurface = @import("LockSurface.zig");
 const Output = @import("Output.zig");
 const SceneNodeData = @import("SceneNodeData.zig");
-const View = @import("View.zig");
+const Window = @import("Window.zig");
 const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
 
 scene: *wlr.Scene,
@@ -54,29 +54,29 @@ layers: struct {
 
 wm: struct {
     pending: struct {
-        render_list: wl.list.Head(View, .pending_render_list_link),
+        render_list: wl.list.Head(Window, .pending_render_list_link),
     },
 
     inflight: struct {
-        render_list: wl.list.Head(View, .inflight_render_list_link),
+        render_list: wl.list.Head(Window, .inflight_render_list_link),
     },
 },
 
-/// This is kind of like an imaginary output where views start and end their life.
+/// This is kind of like an imaginary output where windows start and end their life.
 hidden: struct {
     /// This tree is always disabled.
     tree: *wlr.SceneTree,
 
     pending: struct {
-        render_list: wl.list.Head(View, .pending_render_list_link),
+        render_list: wl.list.Head(Window, .pending_render_list_link),
     },
 
     inflight: struct {
-        render_list: wl.list.Head(View, .inflight_render_list_link),
+        render_list: wl.list.Head(Window, .inflight_render_list_link),
     },
 },
 
-views: wl.list.Head(View, .link),
+windows: wl.list.Head(Window, .link),
 
 new_output: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleNewOutput),
 
@@ -158,7 +158,7 @@ pub fn init(root: *Root) !void {
                 .render_list = undefined,
             },
         },
-        .views = undefined,
+        .windows = undefined,
         .output_layout = output_layout,
         .all_outputs = undefined,
         .active_outputs = undefined,
@@ -175,7 +175,7 @@ pub fn init(root: *Root) !void {
     root.hidden.pending.render_list.init();
     root.hidden.inflight.render_list.init();
 
-    root.views.init();
+    root.windows.init();
     root.all_outputs.init();
     root.active_outputs.init();
 
@@ -250,7 +250,7 @@ fn handleNewOutput(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
 }
 
 /// Remove the output from root.active_outputs and the output layout.
-/// Evacuate views if necessary.
+/// Evacuate windows if necessary.
 pub fn deactivateOutput(root: *Root, output: *Output) void {
     {
         // If the output has already been removed, do nothing
@@ -307,7 +307,7 @@ pub fn activateOutput(root: *Root, output: *Output) void {
     };
 }
 
-/// Trigger asynchronous application of pending state for all outputs and views.
+/// Trigger asynchronous application of pending state for all outputs and windows.
 /// Changes will not be applied to the scene graph until the layout generator
 /// generates a new layout for all outputs and all affected clients ack a
 /// configure and commit a new buffer.
@@ -330,9 +330,9 @@ pub fn applyPending(root: *Root) void {
 
     {
         var it = root.hidden.pending.render_list.iterator(.forward);
-        while (it.next()) |view| {
-            view.inflight_render_list_link.remove();
-            root.hidden.inflight.render_list.append(view);
+        while (it.next()) |window| {
+            window.inflight_render_list_link.remove();
+            root.hidden.inflight.render_list.append(window);
         }
     }
 
@@ -344,10 +344,10 @@ pub fn applyPending(root: *Root) void {
             switch (cursor.mode) {
                 .passthrough, .down => {},
                 inline .move, .resize => |data| {
-                    if (data.view.inflight.fullscreen) {
+                    if (data.window.inflight.fullscreen) {
                         cursor.mode = .passthrough;
-                        data.view.pending.resizing = false;
-                        data.view.inflight.resizing = false;
+                        data.window.pending.resizing = false;
+                        data.window.inflight.resizing = false;
                     }
                 },
             }
@@ -364,19 +364,19 @@ fn sendConfigures(root: *Root) void {
 
     {
         var it = root.wm.inflight.render_list.iterator(.forward);
-        while (it.next()) |view| {
-            assert(!view.inflight_transaction);
-            view.inflight_transaction = true;
+        while (it.next()) |window| {
+            assert(!window.inflight_transaction);
+            window.inflight_transaction = true;
 
-            // This can happen if a view is unmapped while a layout demand including it is inflight
-            // If a view has been unmapped, don't send it a configure.
-            if (!view.mapped) continue;
+            // This can happen if a window is unmapped while a layout demand including it is inflight
+            // If a window has been unmapped, don't send it a configure.
+            if (!window.mapped) continue;
 
-            if (view.configure()) {
+            if (window.configure()) {
                 root.inflight_configures += 1;
 
-                view.saveSurfaceTree();
-                view.sendFrameDone();
+                window.saveSurfaceTree();
+                window.sendFrameDone();
             }
         }
     }
@@ -424,19 +424,19 @@ fn commitTransaction(root: *Root) void {
 
     {
         var it = root.hidden.inflight.render_list.safeIterator(.forward);
-        while (it.next()) |view| {
-            view.tree.node.reparent(root.hidden.tree);
-            view.popup_tree.node.reparent(root.hidden.tree);
+        while (it.next()) |window| {
+            window.tree.node.reparent(root.hidden.tree);
+            window.popup_tree.node.reparent(root.hidden.tree);
         }
     }
 
     {
         var it = root.wm.inflight.render_list.iterator(.forward);
-        while (it.next()) |view| {
-            view.commitTransaction();
+        while (it.next()) |window| {
+            window.commitTransaction();
 
-            view.tree.node.setEnabled(true);
-            view.popup_tree.node.setEnabled(true);
+            window.tree.node.setEnabled(true);
+            window.popup_tree.node.setEnabled(true);
         }
     }
 
@@ -446,11 +446,11 @@ fn commitTransaction(root: *Root) void {
     }
 
     {
-        // This must be done after updating cursor state in case the view was the target of move/resize.
+        // This must be done after updating cursor state in case the window was the target of move/resize.
         var it = root.hidden.inflight.render_list.safeIterator(.forward);
-        while (it.next()) |view| {
-            view.dropSavedSurfaceTree();
-            if (view.destroying) view.destroy(.assert);
+        while (it.next()) |window| {
+            window.dropSavedSurfaceTree();
+            if (window.destroying) window.destroy(.assert);
         }
     }
 
diff --git a/river/SceneNodeData.zig b/river/SceneNodeData.zig
index 68d5923..ec28f45 100644
--- a/river/SceneNodeData.zig
+++ b/river/SceneNodeData.zig
@@ -25,11 +25,11 @@ const util = @import("util.zig");
 const LayerSurface = @import("LayerSurface.zig");
 const LockSurface = @import("LockSurface.zig");
 const InputPopup = @import("InputPopup.zig");
-const View = @import("View.zig");
+const Window = @import("Window.zig");
 const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
 
 pub const Data = union(enum) {
-    view: *View,
+    window: *Window,
     lock_surface: *LockSurface,
     layer_surface: *LayerSurface,
     override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else noreturn,
diff --git a/river/Seat.zig b/river/Seat.zig
index 7289278..77864e5 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -40,13 +40,13 @@ const Output = @import("Output.zig");
 const PointerConstraint = @import("PointerConstraint.zig");
 const Switch = @import("Switch.zig");
 const Tablet = @import("Tablet.zig");
-const View = @import("View.zig");
+const Window = @import("Window.zig");
 const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
 
 const log = std.log.scoped(.seat);
 
 pub const FocusTarget = union(enum) {
-    view: *View,
+    window: *Window,
     override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else noreturn,
     layer: *LayerSurface,
     lock_surface: *LockSurface,
@@ -54,7 +54,7 @@ pub const FocusTarget = union(enum) {
 
     pub fn surface(target: FocusTarget) ?*wlr.Surface {
         return switch (target) {
-            .view => |view| view.rootSurface(),
+            .window => |window| window.rootSurface(),
             .override_redirect => |override_redirect| override_redirect.xwayland_surface.surface,
             .layer => |layer| layer.wlr_layer_surface.surface,
             .lock_surface => |lock_surface| lock_surface.wlr_lock_surface.surface,
@@ -127,16 +127,16 @@ pub fn deinit(seat: *Seat) void {
     seat.request_set_primary_selection.link.remove();
 }
 
-/// Set the current focus. If a visible view is passed it will be focused.
-/// If null is passed, the top view in the stack of the focused output will be focused.
+/// Set the current focus. If a visible window is passed it will be focused.
+/// If null is passed, the top window in the stack of the focused output will be focused.
 /// Requires a call to Root.applyPending()
-pub fn focus(seat: *Seat, target: ?*View) void {
+pub fn focus(seat: *Seat, target: ?*Window) void {
     // Views may not receive focus while locked.
     if (server.lock_manager.state != .unlocked) return;
 
-    // A layer surface with exclusive focus will prevent any view from gaining
+    // A layer surface with exclusive focus will prevent any window from gaining
     // focus if it is on the top or overlay layer. Otherwise, only steal focus
-    // from a focused layer surface if there is an explicit target view.
+    // from a focused layer surface if there is an explicit target window.
     if (seat.focused == .layer) {
         const wlr_layer_surface = seat.focused.layer.wlr_layer_surface;
         assert(wlr_layer_surface.surface.mapped);
@@ -152,9 +152,9 @@ pub fn focus(seat: *Seat, target: ?*View) void {
         }
     }
 
-    // Focus the target view or clear the focus if target is null
-    if (target) |view| {
-        seat.setFocusRaw(.{ .view = view });
+    // Focus the target window or clear the focus if target is null
+    if (target) |window| {
+        seat.setFocusRaw(.{ .window = window });
     } else {
         seat.setFocusRaw(.{ .none = {} });
     }
@@ -162,7 +162,7 @@ pub fn focus(seat: *Seat, target: ?*View) void {
 
 /// Switch focus to the target, handling unfocus and input inhibition
 /// properly. This should only be called directly if dealing with layers or
-/// override redirect xwayland views.
+/// override redirect xwayland windows.
 pub fn setFocusRaw(seat: *Seat, new_focus: FocusTarget) void {
     // If the target is already focused, do nothing
     if (std.meta.eql(new_focus, seat.focused)) return;
@@ -171,9 +171,9 @@ pub fn setFocusRaw(seat: *Seat, new_focus: FocusTarget) void {
 
     // First clear the current focus
     switch (seat.focused) {
-        .view => |view| {
-            view.pending.focus -= 1;
-            view.destroyPopups();
+        .window => |window| {
+            window.pending.focus -= 1;
+            window.destroyPopups();
         },
         .layer => |layer_surface| {
             layer_surface.destroyPopups();
@@ -183,10 +183,10 @@ pub fn setFocusRaw(seat: *Seat, new_focus: FocusTarget) void {
 
     // Set the new focus
     switch (new_focus) {
-        .view => |target_view| {
+        .window => |target_window| {
             assert(server.lock_manager.state != .locked);
-            target_view.pending.focus += 1;
-            target_view.pending.urgent = false;
+            target_window.pending.focus += 1;
+            target_window.pending.urgent = false;
         },
         .layer => assert(server.lock_manager.state != .locked),
         .lock_surface => assert(server.lock_manager.state != .unlocked),
diff --git a/river/Server.zig b/river/Server.zig
index d46cb4f..459beae 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -39,7 +39,7 @@ const TabletTool = @import("TabletTool.zig");
 const XdgDecoration = @import("XdgDecoration.zig");
 const XdgToplevel = @import("XdgToplevel.zig");
 const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
-const XwaylandView = @import("XwaylandView.zig");
+const XwaylandWindow = @import("XwaylandWindow.zig");
 
 const log = std.log.scoped(.server);
 
@@ -394,7 +394,7 @@ fn handleNewXwaylandSurface(_: *wl.Listener(*wlr.XwaylandSurface), xwayland_surf
             return;
         };
     } else {
-        _ = XwaylandView.create(xwayland_surface) catch {
+        _ = XwaylandWindow.create(xwayland_surface) catch {
             log.err("out of memory", .{});
             return;
         };
@@ -409,8 +409,8 @@ fn handleRequestActivate(
 
     const node_data = SceneNodeData.fromSurface(event.surface) orelse return;
     switch (node_data.data) {
-        .view => |view| if (view.pending.focus == 0) {
-            view.pending.urgent = true;
+        .window => |window| if (window.pending.focus == 0) {
+            window.pending.urgent = true;
             server.root.applyPending();
         },
         else => |tag| {
diff --git a/river/View.zig b/river/Window.zig
similarity index 59%
rename from river/View.zig
rename to river/Window.zig
index 27a6301..779c251 100644
--- a/river/View.zig
+++ b/river/Window.zig
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with this program. If not, see <https://www.gnu.org/licenses/>.
 
-const View = @This();
+const Window = @This();
 
 const build_options = @import("build_options");
 const std = @import("std");
@@ -32,9 +32,9 @@ const Output = @import("Output.zig");
 const SceneNodeData = @import("SceneNodeData.zig");
 const Seat = @import("Seat.zig");
 const XdgToplevel = @import("XdgToplevel.zig");
-const XwaylandView = @import("XwaylandView.zig");
+const XwaylandWindow = @import("XwaylandWindow.zig");
 
-const log = std.log.scoped(.view);
+const log = std.log.scoped(.window);
 
 pub const Constraints = struct {
     min_width: u31 = 1,
@@ -45,20 +45,20 @@ pub const Constraints = struct {
 
 const Impl = union(enum) {
     toplevel: XdgToplevel,
-    xwayland_view: if (build_options.xwayland) XwaylandView else noreturn,
+    xwayland_window: if (build_options.xwayland) XwaylandWindow else noreturn,
     /// This state is assigned during destruction after the xdg toplevel
     /// has been destroyed but while the transaction system is still rendering
-    /// saved surfaces of the view.
+    /// saved surfaces of the window.
     /// The toplevel could simply be set to undefined instead, but using a
     /// tag like this gives us better safety checks.
     none,
 };
 
 pub const State = struct {
-    /// The output-relative coordinates of the view and dimensions requested by river.
+    /// The output-relative coordinates of the window and dimensions requested by river.
     box: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
 
-    /// Number of seats currently focusing the view
+    /// Number of seats currently focusing the window
     focus: u32 = 0,
 
     fullscreen: bool = false,
@@ -102,10 +102,10 @@ pub const State = struct {
     }
 };
 
-/// The implementation of this view
+/// The implementation of this window
 impl: Impl,
 
-/// Link for Root.views
+/// Link for Root.windows
 link: wl.list.Link,
 
 tree: *wlr.SceneTree,
@@ -115,17 +115,17 @@ saved_surface_tree: *wlr.SceneTree,
 borders: [4]*wlr.SceneRect,
 popup_tree: *wlr.SceneTree,
 
-/// Bounds on the width/height of the view, set by the toplevel/xwayland_view implementation.
+/// Bounds on the width/height of the window, set by the toplevel/xwayland_window implementation.
 constraints: Constraints = .{},
 
 mapped: bool = false,
-/// This is true if the View is involved in the currently inflight transaction.
+/// This is true if the Window is involved in the currently inflight transaction.
 inflight_transaction: bool = false,
-/// This indicates that the view should be destroyed when the current
-/// transaction completes. See View.destroy()
+/// This indicates that the window should be destroyed when the current
+/// transaction completes. See Window.destroy()
 destroying: bool = false,
 
-/// The state of the view that is directly acted upon/modified through user input.
+/// The state of the window that is directly acted upon/modified through user input.
 ///
 /// Pending state will be copied to the inflight state and communicated to clients
 /// to be applied as a single atomic transaction across all clients as soon as any
@@ -147,11 +147,11 @@ current: State = .{},
 
 foreign_toplevel_handle: ForeignToplevelHandle = .{},
 
-pub fn create(impl: Impl) error{OutOfMemory}!*View {
+pub fn create(impl: Impl) error{OutOfMemory}!*Window {
     assert(impl != .none);
 
-    const view = try util.gpa.create(View);
-    errdefer util.gpa.destroy(view);
+    const window = try util.gpa.create(Window);
+    errdefer util.gpa.destroy(window);
 
     const tree = try server.root.hidden.tree.createSceneTree();
     errdefer tree.node.destroy();
@@ -159,7 +159,7 @@ pub fn create(impl: Impl) error{OutOfMemory}!*View {
     const popup_tree = try server.root.hidden.tree.createSceneTree();
     errdefer popup_tree.node.destroy();
 
-    view.* = .{
+    window.* = .{
         .impl = impl,
         .link = undefined,
         .tree = tree,
@@ -177,41 +177,41 @@ pub fn create(impl: Impl) error{OutOfMemory}!*View {
         .inflight_render_list_link = undefined,
     };
 
-    server.root.views.prepend(view);
-    server.root.hidden.pending.render_list.prepend(view);
-    server.root.hidden.inflight.render_list.prepend(view);
+    server.root.windows.prepend(window);
+    server.root.hidden.pending.render_list.prepend(window);
+    server.root.hidden.inflight.render_list.prepend(window);
 
-    view.tree.node.setEnabled(false);
-    view.popup_tree.node.setEnabled(false);
-    view.saved_surface_tree.node.setEnabled(false);
+    window.tree.node.setEnabled(false);
+    window.popup_tree.node.setEnabled(false);
+    window.saved_surface_tree.node.setEnabled(false);
 
-    try SceneNodeData.attach(&view.tree.node, .{ .view = view });
-    try SceneNodeData.attach(&view.popup_tree.node, .{ .view = view });
+    try SceneNodeData.attach(&window.tree.node, .{ .window = window });
+    try SceneNodeData.attach(&window.popup_tree.node, .{ .window = window });
 
-    return view;
+    return window;
 }
 
-/// If saved buffers of the view are currently in use by a transaction,
-/// mark this view for destruction when the transaction completes. Otherwise
+/// If saved buffers of the window are currently in use by a transaction,
+/// mark this window for destruction when the transaction completes. Otherwise
 /// destroy immediately.
-pub fn destroy(view: *View, when: enum { lazy, assert }) void {
-    assert(view.impl == .none);
-    assert(!view.mapped);
+pub fn destroy(window: *Window, when: enum { lazy, assert }) void {
+    assert(window.impl == .none);
+    assert(!window.mapped);
 
-    view.destroying = true;
+    window.destroying = true;
 
-    // If there are still saved buffers, then this view needs to be kept
+    // If there are still saved buffers, then this window needs to be kept
     // around until the current transaction completes. This function will be
     // called again in Root.commitTransaction()
-    if (!view.saved_surface_tree.node.enabled) {
-        view.tree.node.destroy();
-        view.popup_tree.node.destroy();
+    if (!window.saved_surface_tree.node.enabled) {
+        window.tree.node.destroy();
+        window.popup_tree.node.destroy();
 
-        view.link.remove();
-        view.pending_render_list_link.remove();
-        view.inflight_render_list_link.remove();
+        window.link.remove();
+        window.pending_render_list_link.remove();
+        window.inflight_render_list_link.remove();
 
-        util.gpa.destroy(view);
+        util.gpa.destroy(window);
     } else {
         switch (when) {
             .lazy => {},
@@ -220,47 +220,47 @@ pub fn destroy(view: *View, when: enum { lazy, assert }) void {
     }
 }
 
-/// The change in x/y position of the view during resize cannot be determined
+/// The change in x/y position of the window during resize cannot be determined
 /// until the size of the buffer actually committed is known. Clients are permitted
 /// by the protocol to take a size smaller than that requested by the compositor in
 /// order to maintain an aspect ratio or similar (mpv does this for example).
-pub fn resizeUpdatePosition(view: *View, width: i32, height: i32) void {
-    assert(view.inflight.resizing);
+pub fn resizeUpdatePosition(window: *Window, width: i32, height: i32) void {
+    assert(window.inflight.resizing);
 
     const data = blk: {
         var it = server.input_manager.seats.first;
         while (it) |node| : (it = node.next) {
             const cursor = &node.data.cursor;
-            if (cursor.inflight_mode == .resize and cursor.inflight_mode.resize.view == view) {
+            if (cursor.inflight_mode == .resize and cursor.inflight_mode.resize.window == window) {
                 break :blk cursor.inflight_mode.resize;
             }
         } else {
-            // The view resizing state should never be set when the view is
+            // The window resizing state should never be set when the window is
             // not the target of an interactive resize.
             unreachable;
         }
     };
 
     if (data.edges.left) {
-        view.inflight.box.x += view.current.box.width - width;
-        view.pending.box.x = view.inflight.box.x;
+        window.inflight.box.x += window.current.box.width - width;
+        window.pending.box.x = window.inflight.box.x;
     }
 
     if (data.edges.top) {
-        view.inflight.box.y += view.current.box.height - height;
-        view.pending.box.y = view.inflight.box.y;
+        window.inflight.box.y += window.current.box.height - height;
+        window.pending.box.y = window.inflight.box.y;
     }
 }
 
-pub fn commitTransaction(view: *View) void {
-    assert(view.inflight_transaction);
-    view.inflight_transaction = false;
+pub fn commitTransaction(window: *Window) void {
+    assert(window.inflight_transaction);
+    window.inflight_transaction = false;
 
-    view.foreign_toplevel_handle.update();
+    window.foreign_toplevel_handle.update();
 
-    view.dropSavedSurfaceTree();
+    window.dropSavedSurfaceTree();
 
-    switch (view.impl) {
+    switch (window.impl) {
         .toplevel => |*toplevel| {
             switch (toplevel.configure_state) {
                 .inflight, .acked => {
@@ -288,50 +288,50 @@ pub fn commitTransaction(view: *View) void {
                     // If we did not use the current geometry of the toplevel at this point
                     // we would be rendering the SSD border at initial size X but the surface
                     // would be rendered at size Y.
-                    if (view.inflight.resizing) {
-                        view.resizeUpdatePosition(toplevel.geometry.width, toplevel.geometry.height);
+                    if (window.inflight.resizing) {
+                        window.resizeUpdatePosition(toplevel.geometry.width, toplevel.geometry.height);
                     }
 
-                    view.current = view.inflight;
-                    view.current.box.width = toplevel.geometry.width;
-                    view.current.box.height = toplevel.geometry.height;
+                    window.current = window.inflight;
+                    window.current.box.width = toplevel.geometry.width;
+                    window.current.box.height = toplevel.geometry.height;
                 },
                 .idle, .committed => {
                     toplevel.configure_state = .idle;
-                    view.current = view.inflight;
+                    window.current = window.inflight;
                 },
                 .timed_out, .timed_out_acked => unreachable,
             }
         },
-        .xwayland_view => |xwayland_view| {
-            if (view.inflight.resizing) {
-                view.resizeUpdatePosition(
-                    xwayland_view.xwayland_surface.width,
-                    xwayland_view.xwayland_surface.height,
+        .xwayland_window => |xwayland_window| {
+            if (window.inflight.resizing) {
+                window.resizeUpdatePosition(
+                    xwayland_window.xwayland_surface.width,
+                    xwayland_window.xwayland_surface.height,
                 );
             }
 
-            view.inflight.box.width = xwayland_view.xwayland_surface.width;
-            view.inflight.box.height = xwayland_view.xwayland_surface.height;
-            view.pending.box.width = xwayland_view.xwayland_surface.width;
-            view.pending.box.height = xwayland_view.xwayland_surface.height;
+            window.inflight.box.width = xwayland_window.xwayland_surface.width;
+            window.inflight.box.height = xwayland_window.xwayland_surface.height;
+            window.pending.box.width = xwayland_window.xwayland_surface.width;
+            window.pending.box.height = xwayland_window.xwayland_surface.height;
 
-            view.current = view.inflight;
+            window.current = window.inflight;
         },
         // This may seem pointless at first glance, but is in fact necessary
         // to prevent an assertion failure in Root.commitTransaction() as that
         // function assumes that the inflight tags/output will be applied by
-        // View.commitTransaction() even for views being destroyed.
-        .none => view.current = view.inflight,
+        // Window.commitTransaction() even for windows being destroyed.
+        .none => window.current = window.inflight,
     }
 
-    view.updateSceneState();
+    window.updateSceneState();
 }
 
-pub fn updateSceneState(view: *View) void {
-    const box = &view.current.box;
-    view.tree.node.setPosition(box.x, box.y);
-    view.popup_tree.node.setPosition(box.x, box.y);
+pub fn updateSceneState(window: *Window) void {
+    const box = &window.current.box;
+    window.tree.node.setPosition(box.x, box.y);
+    window.popup_tree.node.setPosition(box.x, box.y);
 
     {
         const config = &server.config;
@@ -367,8 +367,8 @@ pub fn updateSceneState(view: *View) void {
             },
         };
 
-        for (&view.borders, &border_boxes) |border, *border_box| {
-            border.node.setEnabled(view.current.ssd and !view.current.fullscreen);
+        for (&window.borders, &border_boxes) |border, *border_box| {
+            border.node.setEnabled(window.current.ssd and !window.current.fullscreen);
             border.node.setPosition(border_box.x, border_box.y);
             border.setSize(border_box.width, border_box.height);
             border.setColor(border_color);
@@ -377,52 +377,52 @@ pub fn updateSceneState(view: *View) void {
 }
 
 /// Returns true if the configure should be waited for by the transaction system.
-pub fn configure(view: *View) bool {
-    assert(view.mapped and !view.destroying);
-    switch (view.impl) {
+pub fn configure(window: *Window) bool {
+    assert(window.mapped and !window.destroying);
+    switch (window.impl) {
         .toplevel => |*toplevel| return toplevel.configure(),
-        .xwayland_view => |*xwayland_view| return xwayland_view.configure(),
+        .xwayland_window => |*xwayland_window| return xwayland_window.configure(),
         .none => unreachable,
     }
 }
 
-/// Returns null if the view is currently being destroyed and no longer has
+/// Returns null if the window is currently being destroyed and no longer has
 /// an associated surface.
-/// May also return null for Xwayland views that are not currently mapped.
-pub fn rootSurface(view: View) ?*wlr.Surface {
-    return switch (view.impl) {
+/// May also return null for Xwayland windows that are not currently mapped.
+pub fn rootSurface(window: Window) ?*wlr.Surface {
+    return switch (window.impl) {
         .toplevel => |toplevel| toplevel.wlr_toplevel.base.surface,
-        .xwayland_view => |xwayland_view| xwayland_view.xwayland_surface.surface,
+        .xwayland_window => |xwayland_window| xwayland_window.xwayland_surface.surface,
         .none => null,
     };
 }
 
-pub fn sendFrameDone(view: View) void {
-    assert(view.mapped and !view.destroying);
+pub fn sendFrameDone(window: Window) void {
+    assert(window.mapped and !window.destroying);
 
     var now: posix.timespec = undefined;
     posix.clock_gettime(posix.CLOCK.MONOTONIC, &now) catch @panic("CLOCK_MONOTONIC not supported");
-    view.rootSurface().?.sendFrameDone(&now);
+    window.rootSurface().?.sendFrameDone(&now);
 }
 
-pub fn dropSavedSurfaceTree(view: *View) void {
-    if (!view.saved_surface_tree.node.enabled) return;
+pub fn dropSavedSurfaceTree(window: *Window) void {
+    if (!window.saved_surface_tree.node.enabled) return;
 
-    var it = view.saved_surface_tree.children.safeIterator(.forward);
+    var it = window.saved_surface_tree.children.safeIterator(.forward);
     while (it.next()) |node| node.destroy();
 
-    view.saved_surface_tree.node.setEnabled(false);
-    view.surface_tree.node.setEnabled(true);
+    window.saved_surface_tree.node.setEnabled(false);
+    window.surface_tree.node.setEnabled(true);
 }
 
-pub fn saveSurfaceTree(view: *View) void {
-    assert(!view.saved_surface_tree.node.enabled);
-    assert(view.saved_surface_tree.children.empty());
+pub fn saveSurfaceTree(window: *Window) void {
+    assert(!window.saved_surface_tree.node.enabled);
+    assert(window.saved_surface_tree.children.empty());
 
-    view.surface_tree.node.forEachBuffer(*wlr.SceneTree, saveSurfaceTreeIter, view.saved_surface_tree);
+    window.surface_tree.node.forEachBuffer(*wlr.SceneTree, saveSurfaceTreeIter, window.saved_surface_tree);
 
-    view.surface_tree.node.setEnabled(false);
-    view.saved_surface_tree.node.setEnabled(true);
+    window.surface_tree.node.setEnabled(false);
+    window.saved_surface_tree.node.setEnabled(true);
 }
 
 fn saveSurfaceTreeIter(
@@ -441,87 +441,87 @@ fn saveSurfaceTreeIter(
     saved.setTransform(buffer.transform);
 }
 
-pub fn close(view: View) void {
-    switch (view.impl) {
+pub fn close(window: Window) void {
+    switch (window.impl) {
         .toplevel => |toplevel| toplevel.wlr_toplevel.sendClose(),
-        .xwayland_view => |xwayland_view| xwayland_view.xwayland_surface.close(),
+        .xwayland_window => |xwayland_window| xwayland_window.xwayland_surface.close(),
         .none => {},
     }
 }
 
-pub fn destroyPopups(view: View) void {
-    switch (view.impl) {
+pub fn destroyPopups(window: Window) void {
+    switch (window.impl) {
         .toplevel => |toplevel| toplevel.destroyPopups(),
-        .xwayland_view, .none => {},
+        .xwayland_window, .none => {},
     }
 }
 
-/// Return the current title of the view if any.
-pub fn getTitle(view: View) ?[*:0]const u8 {
-    assert(!view.destroying);
-    return switch (view.impl) {
+/// Return the current title of the window if any.
+pub fn getTitle(window: Window) ?[*:0]const u8 {
+    assert(!window.destroying);
+    return switch (window.impl) {
         .toplevel => |toplevel| toplevel.wlr_toplevel.title,
-        .xwayland_view => |xwayland_view| xwayland_view.xwayland_surface.title,
+        .xwayland_window => |xwayland_window| xwayland_window.xwayland_surface.title,
         .none => unreachable,
     };
 }
 
-/// Return the current app_id of the view if any.
-pub fn getAppId(view: View) ?[*:0]const u8 {
-    assert(!view.destroying);
-    return switch (view.impl) {
+/// Return the current app_id of the window if any.
+pub fn getAppId(window: Window) ?[*:0]const u8 {
+    assert(!window.destroying);
+    return switch (window.impl) {
         .toplevel => |toplevel| toplevel.wlr_toplevel.app_id,
         // X11 clients don't have an app_id but the class serves a similar role.
-        .xwayland_view => |xwayland_view| xwayland_view.xwayland_surface.class,
+        .xwayland_window => |xwayland_window| xwayland_window.xwayland_surface.class,
         .none => unreachable,
     };
 }
 
-/// Clamp the width/height of the box to the constraints of the view
-pub fn applyConstraints(view: *View, box: *wlr.Box) void {
-    box.width = math.clamp(box.width, view.constraints.min_width, view.constraints.max_width);
-    box.height = math.clamp(box.height, view.constraints.min_height, view.constraints.max_height);
+/// Clamp the width/height of the box to the constraints of the window
+pub fn applyConstraints(window: *Window, box: *wlr.Box) void {
+    box.width = math.clamp(box.width, window.constraints.min_width, window.constraints.max_width);
+    box.height = math.clamp(box.height, window.constraints.min_height, window.constraints.max_height);
 }
 
 /// Called by the impl when the surface is ready to be displayed
-pub fn map(view: *View) !void {
-    log.debug("view '{?s}' mapped", .{view.getTitle()});
+pub fn map(window: *Window) !void {
+    log.debug("window '{?s}' mapped", .{window.getTitle()});
 
-    assert(!view.mapped and !view.destroying);
-    view.mapped = true;
+    assert(!window.mapped and !window.destroying);
+    window.mapped = true;
 
-    view.foreign_toplevel_handle.map();
+    window.foreign_toplevel_handle.map();
 
     server.root.applyPending();
 }
 
 /// Called by the impl when the surface will no longer be displayed
-pub fn unmap(view: *View) void {
-    log.debug("view '{?s}' unmapped", .{view.getTitle()});
+pub fn unmap(window: *Window) void {
+    log.debug("window '{?s}' unmapped", .{window.getTitle()});
 
-    if (!view.saved_surface_tree.node.enabled) view.saveSurfaceTree();
+    if (!window.saved_surface_tree.node.enabled) window.saveSurfaceTree();
 
     {
-        view.pending_render_list_link.remove();
-        server.root.hidden.pending.render_list.prepend(view);
+        window.pending_render_list_link.remove();
+        server.root.hidden.pending.render_list.prepend(window);
     }
 
-    assert(view.mapped and !view.destroying);
-    view.mapped = false;
+    assert(window.mapped and !window.destroying);
+    window.mapped = false;
 
-    view.foreign_toplevel_handle.unmap();
+    window.foreign_toplevel_handle.unmap();
 
     server.root.applyPending();
 }
 
-pub fn notifyTitle(view: *const View) void {
-    if (view.foreign_toplevel_handle.wlr_handle) |wlr_handle| {
-        if (view.getTitle()) |title| wlr_handle.setTitle(title);
+pub fn notifyTitle(window: *const Window) void {
+    if (window.foreign_toplevel_handle.wlr_handle) |wlr_handle| {
+        if (window.getTitle()) |title| wlr_handle.setTitle(title);
     }
 }
 
-pub fn notifyAppId(view: View) void {
-    if (view.foreign_toplevel_handle.wlr_handle) |wlr_handle| {
-        if (view.getAppId()) |app_id| wlr_handle.setAppId(app_id);
+pub fn notifyAppId(window: Window) void {
+    if (window.foreign_toplevel_handle.wlr_handle) |wlr_handle| {
+        if (window.getAppId()) |app_id| wlr_handle.setAppId(app_id);
     }
 }
diff --git a/river/XdgDecoration.zig b/river/XdgDecoration.zig
index eda0664..6545eb9 100644
--- a/river/XdgDecoration.zig
+++ b/river/XdgDecoration.zig
@@ -73,12 +73,12 @@ fn handleRequestMode(
     const decoration: *XdgDecoration = @fieldParentPtr("request_mode", listener);
 
     const toplevel: *XdgToplevel = @ptrFromInt(decoration.wlr_decoration.toplevel.base.data);
-    const view = toplevel.view;
+    const window = toplevel.window;
 
     const ssd = true;
 
-    if (view.pending.ssd != ssd) {
-        view.pending.ssd = ssd;
+    if (window.pending.ssd != ssd) {
+        window.pending.ssd = ssd;
         server.root.applyPending();
     }
 }
diff --git a/river/XdgPopup.zig b/river/XdgPopup.zig
index 911f8d4..b14e1e5 100644
--- a/river/XdgPopup.zig
+++ b/river/XdgPopup.zig
@@ -29,7 +29,7 @@ const SceneNodeData = @import("SceneNodeData.zig");
 const log = std.log.scoped(.xdg_popup);
 
 wlr_xdg_popup: *wlr.XdgPopup,
-/// The root of the surface tree, i.e. the View or LayerSurface popup_tree.
+/// The root of the surface tree, i.e. the Window or LayerSurface popup_tree.
 root: *wlr.SceneTree,
 
 tree: *wlr.SceneTree,
@@ -96,7 +96,7 @@ fn handleReposition(listener: *wl.Listener(void)) void {
     const xdg_popup: *XdgPopup = @fieldParentPtr("reposition", listener);
 
     const output = switch (SceneNodeData.fromNode(&xdg_popup.root.node).?.data) {
-        .view => |_| @panic("TODO"),
+        .window => |_| @panic("TODO"),
         .layer_surface => |layer_surface| layer_surface.output,
         else => unreachable,
     };
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 481659a..2c40642 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -28,13 +28,13 @@ const util = @import("util.zig");
 const Output = @import("Output.zig");
 const Seat = @import("Seat.zig");
 const XdgPopup = @import("XdgPopup.zig");
-const View = @import("View.zig");
+const Window = @import("Window.zig");
 const XdgDecoration = @import("XdgDecoration.zig");
 
 const log = std.log.scoped(.xdg_shell);
 
 /// TODO(zig): get rid of this and use @fieldParentPtr(), https://github.com/ziglang/zig/issues/6611
-view: *View,
+window: *Window,
 
 wlr_toplevel: *wlr.XdgToplevel,
 
@@ -58,14 +58,14 @@ configure_state: union(enum) {
     timed_out_acked,
 } = .idle,
 
-// Listeners that are always active over the view's lifetime
+// Listeners that are always active over the window's lifetime
 destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
 map: wl.Listener(void) = wl.Listener(void).init(handleMap),
 unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
 commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
 new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup),
 
-// Listeners that are only active while the view is mapped
+// Listeners that are only active while the window is mapped
 ack_configure: wl.Listener(*wlr.XdgSurface.Configure) =
     wl.Listener(*wlr.XdgSurface.Configure).init(handleAckConfigure),
 request_fullscreen: wl.Listener(void) = wl.Listener(void).init(handleRequestFullscreen),
@@ -77,13 +77,13 @@ set_title: wl.Listener(void) = wl.Listener(void).init(handleSetTitle),
 set_app_id: wl.Listener(void) = wl.Listener(void).init(handleSetAppId),
 
 pub fn create(wlr_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void {
-    const view = try View.create(.{ .toplevel = .{
-        .view = undefined,
+    const window = try Window.create(.{ .toplevel = .{
+        .window = undefined,
         .wlr_toplevel = wlr_toplevel,
     } });
-    errdefer view.destroy(.assert);
+    errdefer window.destroy(.assert);
 
-    const toplevel = &view.impl.toplevel;
+    const toplevel = &window.impl.toplevel;
 
     // This listener must be added before the scene xdg surface is created.
     // Otherwise, the scene surface nodes will already be disabled by the unmap
@@ -96,12 +96,12 @@ pub fn create(wlr_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void {
     wlr_toplevel.base.surface.events.unmap.add(&toplevel.unmap);
     errdefer toplevel.unmap.link.remove();
 
-    _ = try view.surface_tree.createSceneXdgSurface(wlr_toplevel.base);
+    _ = try window.surface_tree.createSceneXdgSurface(wlr_toplevel.base);
 
-    toplevel.view = view;
+    toplevel.window = window;
 
     wlr_toplevel.base.data = @intFromPtr(toplevel);
-    wlr_toplevel.base.surface.data = @intFromPtr(&view.tree.node);
+    wlr_toplevel.base.surface.data = @intFromPtr(&window.tree.node);
 
     // Add listeners that are active over the toplevel's entire lifetime
     wlr_toplevel.events.destroy.add(&toplevel.destroy);
@@ -110,7 +110,7 @@ pub fn create(wlr_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void {
     wlr_toplevel.base.events.new_popup.add(&toplevel.new_popup);
 }
 
-/// Send a configure event, applying the inflight state of the view.
+/// Send a configure event, applying the inflight state of the window.
 pub fn configure(toplevel: *XdgToplevel) bool {
     switch (toplevel.configure_state) {
         .idle, .timed_out, .timed_out_acked => {},
@@ -122,12 +122,12 @@ pub fn configure(toplevel: *XdgToplevel) bool {
         .timed_out, .timed_out_acked, .committed => unreachable,
     };
 
-    const inflight = &toplevel.view.inflight;
-    const current = &toplevel.view.current;
+    const inflight = &toplevel.window.inflight;
+    const current = &toplevel.window.current;
 
-    // We avoid a special case for newly mapped views which we have not yet
+    // We avoid a special case for newly mapped windows which we have not yet
     // configured by setting the current width/height to the initial width/height
-    // of the view in handleMap().
+    // of the window in handleMap().
     if (inflight.box.width == current.box.width and
         inflight.box.height == current.box.height and
         (inflight.focus != 0) == (current.focus != 0) and
@@ -173,7 +173,7 @@ pub fn configure(toplevel: *XdgToplevel) bool {
     // from getting out of sync in the case where a client has resized ittoplevel.
     const configure_serial = wlr_toplevel.setSize(inflight.box.width, inflight.box.height);
 
-    // Only track configures with the transaction system if they affect the dimensions of the view.
+    // Only track configures with the transaction system if they affect the dimensions of the window.
     // If the configure state is not idle this means we are currently tracking a timed out
     // configure from a previous transaction and should instead track the newly sent configure.
     if (inflight.box.width == current.box.width and
@@ -205,7 +205,7 @@ fn handleDestroy(listener: *wl.Listener(void)) void {
     }
     assert(toplevel.decoration == null);
 
-    // Remove listeners that are active for the entire lifetime of the view
+    // Remove listeners that are active for the entire lifetime of the window
     toplevel.destroy.link.remove();
     toplevel.map.link.remove();
     toplevel.unmap.link.remove();
@@ -215,14 +215,14 @@ fn handleDestroy(listener: *wl.Listener(void)) void {
     // The wlr_surface may outlive the wlr_xdg_toplevel so we must clean up the user data.
     toplevel.wlr_toplevel.base.surface.data = 0;
 
-    const view = toplevel.view;
-    view.impl = .none;
-    view.destroy(.lazy);
+    const window = toplevel.window;
+    window.impl = .none;
+    window.destroy(.lazy);
 }
 
 fn handleMap(listener: *wl.Listener(void)) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("map", listener);
-    const view = toplevel.view;
+    const window = toplevel.window;
 
     // Add listeners that are only active while mapped
     toplevel.wlr_toplevel.base.events.ack_configure.add(&toplevel.ack_configure);
@@ -234,18 +234,18 @@ fn handleMap(listener: *wl.Listener(void)) void {
 
     toplevel.wlr_toplevel.base.getGeometry(&toplevel.geometry);
 
-    view.pending.box = .{
+    window.pending.box = .{
         .x = 0,
         .y = 0,
         .width = toplevel.geometry.width,
         .height = toplevel.geometry.height,
     };
-    view.inflight.box = view.pending.box;
-    view.current.box = view.pending.box;
+    window.inflight.box = window.pending.box;
+    window.current.box = window.pending.box;
 
-    toplevel.view.pending.fullscreen = toplevel.wlr_toplevel.requested.fullscreen;
+    toplevel.window.pending.fullscreen = toplevel.wlr_toplevel.requested.fullscreen;
 
-    view.map() catch {
+    window.map() catch {
         log.err("out of memory", .{});
         toplevel.wlr_toplevel.resource.getClient().postNoMemory();
     };
@@ -263,13 +263,13 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
     toplevel.set_title.link.remove();
     toplevel.set_app_id.link.remove();
 
-    toplevel.view.unmap();
+    toplevel.window.unmap();
 }
 
 fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("new_popup", listener);
 
-    XdgPopup.create(wlr_xdg_popup, toplevel.view.popup_tree, toplevel.view.popup_tree) catch {
+    XdgPopup.create(wlr_xdg_popup, toplevel.window.popup_tree, toplevel.window.popup_tree) catch {
         wlr_xdg_popup.resource.postNoMemory();
         return;
     };
@@ -293,7 +293,7 @@ fn handleAckConfigure(
 
 fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("commit", listener);
-    const view = toplevel.view;
+    const window = toplevel.window;
 
     if (toplevel.wlr_toplevel.base.initial_commit) {
         _ = toplevel.wlr_toplevel.setWmCapabilities(.{ .fullscreen = true });
@@ -301,19 +301,19 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
         if (toplevel.decoration) |decoration| {
             const ssd = true;
             _ = decoration.wlr_decoration.setMode(if (ssd) .server_side else .client_side);
-            toplevel.view.pending.ssd = ssd;
+            toplevel.window.pending.ssd = ssd;
         }
 
         return;
     }
 
-    if (!view.mapped) {
+    if (!window.mapped) {
         return;
     }
 
     {
         const state = &toplevel.wlr_toplevel.current;
-        view.constraints = .{
+        window.constraints = .{
             .min_width = @max(state.min_width, 1),
             .max_width = if (state.max_width > 0) @intCast(state.max_width) else math.maxInt(u31),
             .min_height = @max(state.min_height, 1),
@@ -335,7 +335,7 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
                     .{ old_geometry.width, old_geometry.height, toplevel.geometry.width, toplevel.geometry.height },
                 );
                 // TODO check tiled state
-                if (!view.current.fullscreen) {
+                if (!window.current.fullscreen) {
                     // It seems that a disappointingly high number of clients have a buggy
                     // response to configure events. They ack the configure immediately but then
                     // proceed to make one or more wl_surface.commit requests with the old size
@@ -344,39 +344,39 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
                     // interest of best serving river's users we will fix up their size here after
                     // logging a shame message.
                     log.err("client with app-id '{s}' is buggy and initiated size change while tiled or fullscreen, shame on it", .{
-                        view.getAppId() orelse "",
+                        window.getAppId() orelse "",
                     });
                 }
 
-                view.inflight.box.width = toplevel.geometry.width;
-                view.inflight.box.height = toplevel.geometry.height;
-                view.pending.box.width = toplevel.geometry.width;
-                view.pending.box.height = toplevel.geometry.height;
-                view.current = view.inflight;
-                view.updateSceneState();
+                window.inflight.box.width = toplevel.geometry.width;
+                window.inflight.box.height = toplevel.geometry.height;
+                window.pending.box.width = toplevel.geometry.width;
+                window.pending.box.height = toplevel.geometry.height;
+                window.current = window.inflight;
+                window.updateSceneState();
             } else if (old_geometry.x != toplevel.geometry.x or
                 old_geometry.y != toplevel.geometry.y)
             {
                 // We need to update the surface clip box to reflect the geometry change.
-                view.updateSceneState();
+                window.updateSceneState();
             }
         },
         // If the client has not yet acked our configure, we need to send a
         // frame done event so that it commits another buffer. These
         // buffers won't be rendered since we are still rendering our
         // stashed buffer from when the transaction started.
-        .inflight => view.sendFrameDone(),
+        .inflight => window.sendFrameDone(),
         .acked, .timed_out_acked => {
             toplevel.wlr_toplevel.base.getGeometry(&toplevel.geometry);
 
-            if (view.inflight.resizing) {
-                view.resizeUpdatePosition(toplevel.geometry.width, toplevel.geometry.height);
+            if (window.inflight.resizing) {
+                window.resizeUpdatePosition(toplevel.geometry.width, toplevel.geometry.height);
             }
 
-            view.inflight.box.width = toplevel.geometry.width;
-            view.inflight.box.height = toplevel.geometry.height;
-            view.pending.box.width = toplevel.geometry.width;
-            view.pending.box.height = toplevel.geometry.height;
+            window.inflight.box.width = toplevel.geometry.width;
+            window.inflight.box.height = toplevel.geometry.height;
+            window.pending.box.width = toplevel.geometry.width;
+            window.pending.box.height = toplevel.geometry.height;
 
             switch (toplevel.configure_state) {
                 .acked => {
@@ -385,8 +385,8 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
                 },
                 .timed_out_acked => {
                     toplevel.configure_state = .idle;
-                    view.current = view.inflight;
-                    view.updateSceneState();
+                    window.current = window.inflight;
+                    window.updateSceneState();
                 },
                 else => unreachable,
             }
@@ -398,8 +398,8 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
 /// for now, perhaps it should be denied in some cases in the future.
 fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("request_fullscreen", listener);
-    if (toplevel.view.pending.fullscreen != toplevel.wlr_toplevel.requested.fullscreen) {
-        toplevel.view.pending.fullscreen = toplevel.wlr_toplevel.requested.fullscreen;
+    if (toplevel.window.pending.fullscreen != toplevel.wlr_toplevel.requested.fullscreen) {
+        toplevel.window.pending.fullscreen = toplevel.wlr_toplevel.requested.fullscreen;
         server.root.applyPending();
     }
 }
@@ -410,14 +410,14 @@ fn handleRequestMove(
 ) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("request_move", listener);
     const seat: *Seat = @ptrFromInt(event.seat.seat.data);
-    const view = toplevel.view;
+    const window = toplevel.window;
 
-    if (view.pending.fullscreen) return;
+    if (window.pending.fullscreen) return;
 
     // Moving windows with touch or tablet tool is not yet supported.
     if (seat.wlr_seat.validatePointerGrabSerial(null, event.serial)) {
         switch (seat.cursor.mode) {
-            .passthrough, .down => seat.cursor.startMove(view),
+            .passthrough, .down => seat.cursor.startMove(window),
             .move, .resize => {},
         }
     }
@@ -426,14 +426,14 @@ fn handleRequestMove(
 fn handleRequestResize(listener: *wl.Listener(*wlr.XdgToplevel.event.Resize), event: *wlr.XdgToplevel.event.Resize) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("request_resize", listener);
     const seat: *Seat = @ptrFromInt(event.seat.seat.data);
-    const view = toplevel.view;
+    const window = toplevel.window;
 
-    if (view.pending.fullscreen) return;
+    if (window.pending.fullscreen) return;
 
     // Resizing windows with touch or tablet tool is not yet supported.
     if (seat.wlr_seat.validatePointerGrabSerial(null, event.serial)) {
         switch (seat.cursor.mode) {
-            .passthrough, .down => seat.cursor.startResize(view, event.edges),
+            .passthrough, .down => seat.cursor.startResize(window, event.edges),
             .move, .resize => {},
         }
     }
@@ -442,11 +442,11 @@ fn handleRequestResize(listener: *wl.Listener(*wlr.XdgToplevel.event.Resize), ev
 /// Called when the client sets / updates its title
 fn handleSetTitle(listener: *wl.Listener(void)) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("set_title", listener);
-    toplevel.view.notifyTitle();
+    toplevel.window.notifyTitle();
 }
 
 /// Called when the client sets / updates its app_id
 fn handleSetAppId(listener: *wl.Listener(void)) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("set_app_id", listener);
-    toplevel.view.notifyAppId();
+    toplevel.window.notifyAppId();
 }
diff --git a/river/XwaylandOverrideRedirect.zig b/river/XwaylandOverrideRedirect.zig
index ff638a3..28f3480 100644
--- a/river/XwaylandOverrideRedirect.zig
+++ b/river/XwaylandOverrideRedirect.zig
@@ -26,8 +26,8 @@ const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
 const SceneNodeData = @import("SceneNodeData.zig");
-const View = @import("View.zig");
-const XwaylandView = @import("XwaylandView.zig");
+const Window = @import("Window.zig");
+const XwaylandWindow = @import("XwaylandWindow.zig");
 
 const log = std.log.scoped(.xwayland);
 
@@ -139,13 +139,13 @@ pub fn focusIfDesired(override_redirect: *XwaylandOverrideRedirect) void {
         override_redirect.xwayland_surface.icccmInputModel() != .none)
     {
         const seat = server.input_manager.defaultSeat();
-        // Keep the parent top-level Xwayland view of any override redirect surface
+        // Keep the parent top-level Xwayland window of any override redirect surface
         // activated while that override redirect surface is focused. This ensures
         // override redirect menus do not disappear as a result of deactivating
         // their parent window.
-        if (seat.focused == .view and
-            seat.focused.view.impl == .xwayland_view and
-            seat.focused.view.impl.xwayland_view.xwayland_surface.pid == override_redirect.xwayland_surface.pid)
+        if (seat.focused == .window and
+            seat.focused.window.impl == .xwayland_window and
+            seat.focused.window.impl.xwayland_window.xwayland_surface.pid == override_redirect.xwayland_surface.pid)
         {
             seat.keyboardEnterOrLeave(override_redirect.xwayland_surface.surface);
         } else {
@@ -168,11 +168,11 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
     var seat_it = server.input_manager.seats.first;
     while (seat_it) |seat_node| : (seat_it = seat_node.next) {
         const seat = &seat_node.data;
-        if (seat.focused == .view and seat.focused.view.impl == .xwayland_view and
-            seat.focused.view.impl.xwayland_view.xwayland_surface.pid == override_redirect.xwayland_surface.pid and
+        if (seat.focused == .window and seat.focused.window.impl == .xwayland_window and
+            seat.focused.window.impl.xwayland_window.xwayland_surface.pid == override_redirect.xwayland_surface.pid and
             seat.wlr_seat.keyboard_state.focused_surface == override_redirect.xwayland_surface.surface)
         {
-            seat.keyboardEnterOrLeave(seat.focused.view.rootSurface());
+            seat.keyboardEnterOrLeave(seat.focused.window.rootSurface());
         }
     }
 
@@ -204,7 +204,7 @@ fn handleSetOverrideRedirect(listener: *wl.Listener(void)) void {
     }
     handleDestroy(&override_redirect.destroy);
 
-    XwaylandView.create(xwayland_surface) catch {
+    XwaylandWindow.create(xwayland_surface) catch {
         log.err("out of memory", .{});
         return;
     };
diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig
deleted file mode 100644
index c4e6662..0000000
--- a/river/XwaylandView.zig
+++ /dev/null
@@ -1,297 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const XwaylandView = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const math = std.math;
-
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Output = @import("Output.zig");
-const View = @import("View.zig");
-const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
-
-const log = std.log.scoped(.xwayland);
-
-/// TODO(zig): get rid of this and use @fieldParentPtr(), https://github.com/ziglang/zig/issues/6611
-view: *View,
-
-xwayland_surface: *wlr.XwaylandSurface,
-/// Created on map and destroyed on unmap
-surface_tree: ?*wlr.SceneTree = null,
-
-// Active over entire lifetime
-destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-request_configure: wl.Listener(*wlr.XwaylandSurface.event.Configure) =
-    wl.Listener(*wlr.XwaylandSurface.event.Configure).init(handleRequestConfigure),
-set_override_redirect: wl.Listener(void) = wl.Listener(void).init(handleSetOverrideRedirect),
-associate: wl.Listener(void) = wl.Listener(void).init(handleAssociate),
-dissociate: wl.Listener(void) = wl.Listener(void).init(handleDissociate),
-
-// Active while the xwayland_surface is associated with a wlr_surface
-map: wl.Listener(void) = wl.Listener(void).init(handleMap),
-unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
-
-// Active while mapped
-set_title: wl.Listener(void) = wl.Listener(void).init(handleSetTitle),
-set_class: wl.Listener(void) = wl.Listener(void).init(handleSetClass),
-set_decorations: wl.Listener(void) = wl.Listener(void).init(handleSetDecorations),
-request_fullscreen: wl.Listener(void) = wl.Listener(void).init(handleRequestFullscreen),
-request_minimize: wl.Listener(*wlr.XwaylandSurface.event.Minimize) =
-    wl.Listener(*wlr.XwaylandSurface.event.Minimize).init(handleRequestMinimize),
-
-pub fn create(xwayland_surface: *wlr.XwaylandSurface) error{OutOfMemory}!void {
-    const view = try View.create(.{ .xwayland_view = .{
-        .view = undefined,
-        .xwayland_surface = xwayland_surface,
-    } });
-    errdefer view.destroy(.assert);
-
-    const xwayland_view = &view.impl.xwayland_view;
-    xwayland_view.view = view;
-
-    // Add listeners that are active over the view's entire lifetime
-    xwayland_surface.events.destroy.add(&xwayland_view.destroy);
-    xwayland_surface.events.associate.add(&xwayland_view.associate);
-    xwayland_surface.events.dissociate.add(&xwayland_view.dissociate);
-    xwayland_surface.events.request_configure.add(&xwayland_view.request_configure);
-    xwayland_surface.events.set_override_redirect.add(&xwayland_view.set_override_redirect);
-
-    if (xwayland_surface.surface) |surface| {
-        handleAssociate(&xwayland_view.associate);
-        if (surface.mapped) {
-            handleMap(&xwayland_view.map);
-        }
-    }
-}
-
-/// Always returns false as we do not care about frame perfection for Xwayland views.
-pub fn configure(xwayland_view: XwaylandView) bool {
-    const inflight = &xwayland_view.view.inflight;
-    const current = &xwayland_view.view.current;
-
-    if (xwayland_view.xwayland_surface.x == inflight.box.x and
-        xwayland_view.xwayland_surface.y == inflight.box.y and
-        xwayland_view.xwayland_surface.width == inflight.box.width and
-        xwayland_view.xwayland_surface.height == inflight.box.height and
-        (inflight.focus != 0) == (current.focus != 0))
-        // TODO fullscreen
-    {
-        return false;
-    }
-
-    xwayland_view.xwayland_surface.configure(
-        math.lossyCast(i16, inflight.box.x),
-        math.lossyCast(i16, inflight.box.y),
-        math.lossyCast(u16, inflight.box.width),
-        math.lossyCast(u16, inflight.box.height),
-    );
-
-    xwayland_view.setActivated(inflight.focus != 0);
-
-    if (false) xwayland_view.xwayland_surface.setFullscreen();
-
-    return false;
-}
-
-fn setActivated(xwayland_view: XwaylandView, activated: bool) void {
-    // See comment on handleRequestMinimize() for details
-    if (activated and xwayland_view.xwayland_surface.minimized) {
-        xwayland_view.xwayland_surface.setMinimized(false);
-    }
-    xwayland_view.xwayland_surface.activate(activated);
-    if (activated) {
-        xwayland_view.xwayland_surface.restack(null, .above);
-    }
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("destroy", listener);
-
-    // Remove listeners that are active for the entire lifetime of the view
-    xwayland_view.destroy.link.remove();
-    xwayland_view.associate.link.remove();
-    xwayland_view.dissociate.link.remove();
-    xwayland_view.request_configure.link.remove();
-    xwayland_view.set_override_redirect.link.remove();
-
-    const view = xwayland_view.view;
-    view.impl = .none;
-    view.destroy(.lazy);
-}
-
-fn handleAssociate(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("associate", listener);
-
-    xwayland_view.xwayland_surface.surface.?.events.map.add(&xwayland_view.map);
-    xwayland_view.xwayland_surface.surface.?.events.unmap.add(&xwayland_view.unmap);
-}
-
-fn handleDissociate(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("dissociate", listener);
-    xwayland_view.map.link.remove();
-    xwayland_view.unmap.link.remove();
-}
-
-pub fn handleMap(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("map", listener);
-    const view = xwayland_view.view;
-
-    const xwayland_surface = xwayland_view.xwayland_surface;
-    const surface = xwayland_surface.surface.?;
-    surface.data = @intFromPtr(&view.tree.node);
-
-    // Add listeners that are only active while mapped
-    xwayland_surface.events.set_title.add(&xwayland_view.set_title);
-    xwayland_surface.events.set_class.add(&xwayland_view.set_class);
-    xwayland_surface.events.set_decorations.add(&xwayland_view.set_decorations);
-    xwayland_surface.events.request_fullscreen.add(&xwayland_view.request_fullscreen);
-    xwayland_surface.events.request_minimize.add(&xwayland_view.request_minimize);
-
-    xwayland_view.surface_tree = view.surface_tree.createSceneSubsurfaceTree(surface) catch {
-        log.err("out of memory", .{});
-        surface.resource.getClient().postNoMemory();
-        return;
-    };
-
-    view.pending.box = .{
-        .x = 0,
-        .y = 0,
-        .width = xwayland_view.xwayland_surface.width,
-        .height = xwayland_view.xwayland_surface.height,
-    };
-    view.inflight.box = view.pending.box;
-    view.current.box = view.pending.box;
-
-    // This will be overwritten in View.map() if the view is matched by a rule.
-    view.pending.ssd = !xwayland_surface.decorations.no_border;
-
-    view.pending.fullscreen = xwayland_surface.fullscreen;
-
-    view.map() catch {
-        log.err("out of memory", .{});
-        surface.resource.getClient().postNoMemory();
-    };
-}
-
-fn handleUnmap(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("unmap", listener);
-
-    xwayland_view.xwayland_surface.surface.?.data = 0;
-
-    // Remove listeners that are only active while mapped
-    xwayland_view.set_title.link.remove();
-    xwayland_view.set_class.link.remove();
-    xwayland_view.request_fullscreen.link.remove();
-    xwayland_view.request_minimize.link.remove();
-
-    xwayland_view.view.unmap();
-
-    // Don't destroy the surface tree until after View.unmap() has a chance
-    // to save buffers for frame perfection.
-    xwayland_view.surface_tree.?.node.destroy();
-    xwayland_view.surface_tree = null;
-}
-
-fn handleRequestConfigure(
-    listener: *wl.Listener(*wlr.XwaylandSurface.event.Configure),
-    event: *wlr.XwaylandSurface.event.Configure,
-) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("request_configure", listener);
-
-    // If unmapped, let the client do whatever it wants
-    if (xwayland_view.xwayland_surface.surface == null or
-        !xwayland_view.xwayland_surface.surface.?.mapped)
-    {
-        xwayland_view.xwayland_surface.configure(event.x, event.y, event.width, event.height);
-        return;
-    }
-
-    // Allow xwayland views to set their own dimensions (but not position) if floating
-    xwayland_view.view.pending.box.width = event.width;
-    xwayland_view.view.pending.box.height = event.height;
-    server.root.applyPending();
-}
-
-fn handleSetOverrideRedirect(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("set_override_redirect", listener);
-    const xwayland_surface = xwayland_view.xwayland_surface;
-
-    log.debug("xwayland surface set override redirect", .{});
-
-    assert(xwayland_surface.override_redirect);
-
-    if (xwayland_surface.surface) |surface| {
-        if (surface.mapped) {
-            handleUnmap(&xwayland_view.unmap);
-        }
-        handleDissociate(&xwayland_view.dissociate);
-    }
-    handleDestroy(&xwayland_view.destroy);
-
-    XwaylandOverrideRedirect.create(xwayland_surface) catch {
-        log.err("out of memory", .{});
-        return;
-    };
-}
-
-fn handleSetTitle(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("set_title", listener);
-    xwayland_view.view.notifyTitle();
-}
-
-fn handleSetClass(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("set_class", listener);
-    xwayland_view.view.notifyAppId();
-}
-
-fn handleSetDecorations(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("set_decorations", listener);
-    const view = xwayland_view.view;
-
-    const ssd = !xwayland_view.xwayland_surface.decorations.no_border;
-
-    if (view.pending.ssd != ssd) {
-        view.pending.ssd = ssd;
-        server.root.applyPending();
-    }
-}
-
-fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("request_fullscreen", listener);
-    if (xwayland_view.view.pending.fullscreen != xwayland_view.xwayland_surface.fullscreen) {
-        xwayland_view.view.pending.fullscreen = xwayland_view.xwayland_surface.fullscreen;
-        server.root.applyPending();
-    }
-}
-
-/// Some X11 clients will minimize themselves regardless of how we respond.
-/// Therefore to ensure they don't get stuck in this minimized state we tell
-/// them their request has been honored without actually doing anything and
-/// unminimize them if they gain focus while minimized.
-fn handleRequestMinimize(
-    listener: *wl.Listener(*wlr.XwaylandSurface.event.Minimize),
-    event: *wlr.XwaylandSurface.event.Minimize,
-) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("request_minimize", listener);
-    xwayland_view.xwayland_surface.setMinimized(event.minimize);
-}
diff --git a/river/XwaylandWindow.zig b/river/XwaylandWindow.zig
new file mode 100644
index 0000000..85751d2
--- /dev/null
+++ b/river/XwaylandWindow.zig
@@ -0,0 +1,297 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2020 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const XwaylandWindow = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const math = std.math;
+
+const wlr = @import("wlroots");
+const wl = @import("wayland").server.wl;
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Output = @import("Output.zig");
+const Window = @import("Window.zig");
+const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
+
+const log = std.log.scoped(.xwayland);
+
+/// TODO(zig): get rid of this and use @fieldParentPtr(), https://github.com/ziglang/zig/issues/6611
+window: *Window,
+
+xwayland_surface: *wlr.XwaylandSurface,
+/// Created on map and destroyed on unmap
+surface_tree: ?*wlr.SceneTree = null,
+
+// Active over entire lifetime
+destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
+request_configure: wl.Listener(*wlr.XwaylandSurface.event.Configure) =
+    wl.Listener(*wlr.XwaylandSurface.event.Configure).init(handleRequestConfigure),
+set_override_redirect: wl.Listener(void) = wl.Listener(void).init(handleSetOverrideRedirect),
+associate: wl.Listener(void) = wl.Listener(void).init(handleAssociate),
+dissociate: wl.Listener(void) = wl.Listener(void).init(handleDissociate),
+
+// Active while the xwayland_surface is associated with a wlr_surface
+map: wl.Listener(void) = wl.Listener(void).init(handleMap),
+unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
+
+// Active while mapped
+set_title: wl.Listener(void) = wl.Listener(void).init(handleSetTitle),
+set_class: wl.Listener(void) = wl.Listener(void).init(handleSetClass),
+set_decorations: wl.Listener(void) = wl.Listener(void).init(handleSetDecorations),
+request_fullscreen: wl.Listener(void) = wl.Listener(void).init(handleRequestFullscreen),
+request_minimize: wl.Listener(*wlr.XwaylandSurface.event.Minimize) =
+    wl.Listener(*wlr.XwaylandSurface.event.Minimize).init(handleRequestMinimize),
+
+pub fn create(xwayland_surface: *wlr.XwaylandSurface) error{OutOfMemory}!void {
+    const window = try Window.create(.{ .xwayland_window = .{
+        .window = undefined,
+        .xwayland_surface = xwayland_surface,
+    } });
+    errdefer window.destroy(.assert);
+
+    const xwayland_window = &window.impl.xwayland_window;
+    xwayland_window.window = window;
+
+    // Add listeners that are active over the window's entire lifetime
+    xwayland_surface.events.destroy.add(&xwayland_window.destroy);
+    xwayland_surface.events.associate.add(&xwayland_window.associate);
+    xwayland_surface.events.dissociate.add(&xwayland_window.dissociate);
+    xwayland_surface.events.request_configure.add(&xwayland_window.request_configure);
+    xwayland_surface.events.set_override_redirect.add(&xwayland_window.set_override_redirect);
+
+    if (xwayland_surface.surface) |surface| {
+        handleAssociate(&xwayland_window.associate);
+        if (surface.mapped) {
+            handleMap(&xwayland_window.map);
+        }
+    }
+}
+
+/// Always returns false as we do not care about frame perfection for Xwayland windows.
+pub fn configure(xwayland_window: XwaylandWindow) bool {
+    const inflight = &xwayland_window.window.inflight;
+    const current = &xwayland_window.window.current;
+
+    if (xwayland_window.xwayland_surface.x == inflight.box.x and
+        xwayland_window.xwayland_surface.y == inflight.box.y and
+        xwayland_window.xwayland_surface.width == inflight.box.width and
+        xwayland_window.xwayland_surface.height == inflight.box.height and
+        (inflight.focus != 0) == (current.focus != 0))
+        // TODO fullscreen
+    {
+        return false;
+    }
+
+    xwayland_window.xwayland_surface.configure(
+        math.lossyCast(i16, inflight.box.x),
+        math.lossyCast(i16, inflight.box.y),
+        math.lossyCast(u16, inflight.box.width),
+        math.lossyCast(u16, inflight.box.height),
+    );
+
+    xwayland_window.setActivated(inflight.focus != 0);
+
+    if (false) xwayland_window.xwayland_surface.setFullscreen();
+
+    return false;
+}
+
+fn setActivated(xwayland_window: XwaylandWindow, activated: bool) void {
+    // See comment on handleRequestMinimize() for details
+    if (activated and xwayland_window.xwayland_surface.minimized) {
+        xwayland_window.xwayland_surface.setMinimized(false);
+    }
+    xwayland_window.xwayland_surface.activate(activated);
+    if (activated) {
+        xwayland_window.xwayland_surface.restack(null, .above);
+    }
+}
+
+fn handleDestroy(listener: *wl.Listener(void)) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("destroy", listener);
+
+    // Remove listeners that are active for the entire lifetime of the window
+    xwayland_window.destroy.link.remove();
+    xwayland_window.associate.link.remove();
+    xwayland_window.dissociate.link.remove();
+    xwayland_window.request_configure.link.remove();
+    xwayland_window.set_override_redirect.link.remove();
+
+    const window = xwayland_window.window;
+    window.impl = .none;
+    window.destroy(.lazy);
+}
+
+fn handleAssociate(listener: *wl.Listener(void)) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("associate", listener);
+
+    xwayland_window.xwayland_surface.surface.?.events.map.add(&xwayland_window.map);
+    xwayland_window.xwayland_surface.surface.?.events.unmap.add(&xwayland_window.unmap);
+}
+
+fn handleDissociate(listener: *wl.Listener(void)) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("dissociate", listener);
+    xwayland_window.map.link.remove();
+    xwayland_window.unmap.link.remove();
+}
+
+pub fn handleMap(listener: *wl.Listener(void)) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("map", listener);
+    const window = xwayland_window.window;
+
+    const xwayland_surface = xwayland_window.xwayland_surface;
+    const surface = xwayland_surface.surface.?;
+    surface.data = @intFromPtr(&window.tree.node);
+
+    // Add listeners that are only active while mapped
+    xwayland_surface.events.set_title.add(&xwayland_window.set_title);
+    xwayland_surface.events.set_class.add(&xwayland_window.set_class);
+    xwayland_surface.events.set_decorations.add(&xwayland_window.set_decorations);
+    xwayland_surface.events.request_fullscreen.add(&xwayland_window.request_fullscreen);
+    xwayland_surface.events.request_minimize.add(&xwayland_window.request_minimize);
+
+    xwayland_window.surface_tree = window.surface_tree.createSceneSubsurfaceTree(surface) catch {
+        log.err("out of memory", .{});
+        surface.resource.getClient().postNoMemory();
+        return;
+    };
+
+    window.pending.box = .{
+        .x = 0,
+        .y = 0,
+        .width = xwayland_window.xwayland_surface.width,
+        .height = xwayland_window.xwayland_surface.height,
+    };
+    window.inflight.box = window.pending.box;
+    window.current.box = window.pending.box;
+
+    // This will be overwritten in Window.map() if the window is matched by a rule.
+    window.pending.ssd = !xwayland_surface.decorations.no_border;
+
+    window.pending.fullscreen = xwayland_surface.fullscreen;
+
+    window.map() catch {
+        log.err("out of memory", .{});
+        surface.resource.getClient().postNoMemory();
+    };
+}
+
+fn handleUnmap(listener: *wl.Listener(void)) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("unmap", listener);
+
+    xwayland_window.xwayland_surface.surface.?.data = 0;
+
+    // Remove listeners that are only active while mapped
+    xwayland_window.set_title.link.remove();
+    xwayland_window.set_class.link.remove();
+    xwayland_window.request_fullscreen.link.remove();
+    xwayland_window.request_minimize.link.remove();
+
+    xwayland_window.window.unmap();
+
+    // Don't destroy the surface tree until after Window.unmap() has a chance
+    // to save buffers for frame perfection.
+    xwayland_window.surface_tree.?.node.destroy();
+    xwayland_window.surface_tree = null;
+}
+
+fn handleRequestConfigure(
+    listener: *wl.Listener(*wlr.XwaylandSurface.event.Configure),
+    event: *wlr.XwaylandSurface.event.Configure,
+) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("request_configure", listener);
+
+    // If unmapped, let the client do whatever it wants
+    if (xwayland_window.xwayland_surface.surface == null or
+        !xwayland_window.xwayland_surface.surface.?.mapped)
+    {
+        xwayland_window.xwayland_surface.configure(event.x, event.y, event.width, event.height);
+        return;
+    }
+
+    // Allow xwayland windows to set their own dimensions (but not position) if floating
+    xwayland_window.window.pending.box.width = event.width;
+    xwayland_window.window.pending.box.height = event.height;
+    server.root.applyPending();
+}
+
+fn handleSetOverrideRedirect(listener: *wl.Listener(void)) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("set_override_redirect", listener);
+    const xwayland_surface = xwayland_window.xwayland_surface;
+
+    log.debug("xwayland surface set override redirect", .{});
+
+    assert(xwayland_surface.override_redirect);
+
+    if (xwayland_surface.surface) |surface| {
+        if (surface.mapped) {
+            handleUnmap(&xwayland_window.unmap);
+        }
+        handleDissociate(&xwayland_window.dissociate);
+    }
+    handleDestroy(&xwayland_window.destroy);
+
+    XwaylandOverrideRedirect.create(xwayland_surface) catch {
+        log.err("out of memory", .{});
+        return;
+    };
+}
+
+fn handleSetTitle(listener: *wl.Listener(void)) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("set_title", listener);
+    xwayland_window.window.notifyTitle();
+}
+
+fn handleSetClass(listener: *wl.Listener(void)) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("set_class", listener);
+    xwayland_window.window.notifyAppId();
+}
+
+fn handleSetDecorations(listener: *wl.Listener(void)) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("set_decorations", listener);
+    const window = xwayland_window.window;
+
+    const ssd = !xwayland_window.xwayland_surface.decorations.no_border;
+
+    if (window.pending.ssd != ssd) {
+        window.pending.ssd = ssd;
+        server.root.applyPending();
+    }
+}
+
+fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("request_fullscreen", listener);
+    if (xwayland_window.window.pending.fullscreen != xwayland_window.xwayland_surface.fullscreen) {
+        xwayland_window.window.pending.fullscreen = xwayland_window.xwayland_surface.fullscreen;
+        server.root.applyPending();
+    }
+}
+
+/// Some X11 clients will minimize themselves regardless of how we respond.
+/// Therefore to ensure they don't get stuck in this minimized state we tell
+/// them their request has been honored without actually doing anything and
+/// unminimize them if they gain focus while minimized.
+fn handleRequestMinimize(
+    listener: *wl.Listener(*wlr.XwaylandSurface.event.Minimize),
+    event: *wlr.XwaylandSurface.event.Minimize,
+) void {
+    const xwayland_window: *XwaylandWindow = @fieldParentPtr("request_minimize", listener);
+    xwayland_window.xwayland_surface.setMinimized(event.minimize);
+}