initial commit
This commit is contained in:
+239
@@ -0,0 +1,239 @@
|
||||
//! A single managed window.
|
||||
//!
|
||||
//! Event handlers here only ever mutate plain fields. Every protocol request
|
||||
//! that changes window management or rendering state is issued from Wm's
|
||||
//! manage/render sequence handlers, because river only permits those requests
|
||||
//! between manage_start/manage_finish and render_start/render_finish.
|
||||
|
||||
const Window = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const river = wayland.client.river;
|
||||
|
||||
const config = @import("config");
|
||||
|
||||
const Wm = @import("Wm.zig");
|
||||
const Output = @import("Output.zig");
|
||||
const layout = @import("layout.zig");
|
||||
const Box = layout.Box;
|
||||
|
||||
wm: *Wm,
|
||||
window: *river.WindowV1,
|
||||
/// Created lazily: the protocol allows get_node exactly once per window.
|
||||
node: ?*river.NodeV1 = null,
|
||||
|
||||
title: ?[]u8 = null,
|
||||
app_id: ?[]u8 = null,
|
||||
identifier: ?[]u8 = null,
|
||||
parent: ?*Window = null,
|
||||
|
||||
tags: u32 = 0,
|
||||
output: ?*Output = null,
|
||||
|
||||
floating: bool = false,
|
||||
/// Set by rules or by the user; distinguishes "floating because it is a
|
||||
/// dialog" from "floating because it was asked to be".
|
||||
floating_forced: bool = false,
|
||||
|
||||
fullscreen: bool = false,
|
||||
/// What we last told the window, so we only send changes.
|
||||
informed_fullscreen: bool = false,
|
||||
|
||||
/// Target rectangle including the border, computed by the layout.
|
||||
cell: Box = .{},
|
||||
/// Geometry to restore when a floating window stops being fullscreen.
|
||||
float_box: Box = .{},
|
||||
/// The content size river last reported.
|
||||
content_width: i32 = 0,
|
||||
content_height: i32 = 0,
|
||||
/// The last size we proposed, so we do not re-propose every manage sequence.
|
||||
proposed_width: i32 = -1,
|
||||
proposed_height: i32 = -1,
|
||||
|
||||
min_width: i32 = 0,
|
||||
min_height: i32 = 0,
|
||||
max_width: i32 = 0,
|
||||
max_height: i32 = 0,
|
||||
|
||||
/// True once river has sent dimensions, i.e. the window is on screen.
|
||||
mapped: bool = false,
|
||||
/// True once we have sent the one-time setup requests.
|
||||
configured: bool = false,
|
||||
/// Computed each manage sequence.
|
||||
visible: bool = false,
|
||||
/// Whether the window is currently hidden, so we only send changes.
|
||||
hidden: bool = false,
|
||||
/// river has closed this window; it must be reaped and not touched again.
|
||||
closed: bool = false,
|
||||
/// A close was requested. `close` modifies window management state, so it has
|
||||
/// to wait for the next manage sequence like everything else.
|
||||
pending_close: bool = false,
|
||||
|
||||
/// Bumped whenever the window takes focus, giving a cheap "most recently
|
||||
/// focused" ordering without maintaining dwm's second linked list.
|
||||
focus_serial: u64 = 0,
|
||||
/// Cleared once the window has had its one chance at taking focus as it maps.
|
||||
wants_initial_focus: bool = true,
|
||||
|
||||
pub fn create(wm: *Wm, window: *river.WindowV1) !*Window {
|
||||
const self = try wm.gpa.create(Window);
|
||||
self.* = .{ .wm = wm, .window = window };
|
||||
window.setListener(*Window, onEvent, self);
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Window) void {
|
||||
const gpa = self.wm.gpa;
|
||||
if (self.node) |node| node.destroy();
|
||||
self.window.destroy();
|
||||
if (self.title) |t| gpa.free(t);
|
||||
if (self.app_id) |a| gpa.free(a);
|
||||
if (self.identifier) |i| gpa.free(i);
|
||||
gpa.destroy(self);
|
||||
}
|
||||
|
||||
/// The node is needed to position and stack the window; create it on demand.
|
||||
pub fn getNode(self: *Window) ?*river.NodeV1 {
|
||||
if (self.node) |node| return node;
|
||||
self.node = self.window.getNode() catch |err| {
|
||||
std.log.err("failed to create node for window: {s}", .{@errorName(err)});
|
||||
return null;
|
||||
};
|
||||
return self.node;
|
||||
}
|
||||
|
||||
/// Clamp a proposed size to the window's advertised limits. These are hints,
|
||||
/// but respecting them avoids pointless configure round-trips with windows
|
||||
/// that will refuse the size anyway.
|
||||
pub fn clampSize(self: *const Window, width: i32, height: i32) struct { i32, i32 } {
|
||||
var w = width;
|
||||
var h = height;
|
||||
if (self.min_width > 0) w = @max(w, self.min_width);
|
||||
if (self.min_height > 0) h = @max(h, self.min_height);
|
||||
if (self.max_width > 0) w = @min(w, self.max_width);
|
||||
if (self.max_height > 0) h = @min(h, self.max_height);
|
||||
return .{ @max(1, w), @max(1, h) };
|
||||
}
|
||||
|
||||
/// Apply matching rules from config to a newly created window.
|
||||
pub fn applyRules(self: *Window) void {
|
||||
for (config.rules) |rule| {
|
||||
if (rule.app_id) |want| {
|
||||
const have = self.app_id orelse continue;
|
||||
if (!std.mem.eql(u8, want, have)) continue;
|
||||
}
|
||||
if (rule.title) |want| {
|
||||
const have = self.title orelse continue;
|
||||
if (std.mem.indexOf(u8, have, want) == null) continue;
|
||||
}
|
||||
if (rule.tags) |t| self.tags = t;
|
||||
if (rule.floating) |f| {
|
||||
self.floating = f;
|
||||
self.floating_forced = f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setString(self: *Window, field: *?[]u8, value: ?[*:0]const u8) void {
|
||||
const gpa = self.wm.gpa;
|
||||
if (field.*) |old| gpa.free(old);
|
||||
field.* = null;
|
||||
if (value) |v| {
|
||||
field.* = gpa.dupe(u8, std.mem.span(v)) catch null;
|
||||
}
|
||||
}
|
||||
|
||||
fn onEvent(_: *river.WindowV1, event: river.WindowV1.Event, self: *Window) void {
|
||||
switch (event) {
|
||||
.closed => {
|
||||
self.closed = true;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
|
||||
.dimensions => |ev| {
|
||||
self.content_width = ev.width;
|
||||
self.content_height = ev.height;
|
||||
if (!self.mapped) {
|
||||
self.mapped = true;
|
||||
// Focus and stacking for a new window are settled in the
|
||||
// manage sequence, and only once it is mapped.
|
||||
self.wm.needsManage();
|
||||
self.wm.ipcDirty();
|
||||
}
|
||||
// A window may resize itself; if it is floating its cell must
|
||||
// follow, otherwise the border is drawn around the wrong area.
|
||||
if (self.floating and !self.fullscreen) {
|
||||
const bw = config.border_width;
|
||||
self.cell.width = ev.width + 2 * bw;
|
||||
self.cell.height = ev.height + 2 * bw;
|
||||
self.float_box = self.cell;
|
||||
}
|
||||
},
|
||||
|
||||
.dimensions_hint => |ev| {
|
||||
self.min_width = ev.min_width;
|
||||
self.min_height = ev.min_height;
|
||||
self.max_width = ev.max_width;
|
||||
self.max_height = ev.max_height;
|
||||
},
|
||||
|
||||
.app_id => |ev| {
|
||||
self.setString(&self.app_id, ev.app_id);
|
||||
self.applyRules();
|
||||
self.wm.ipcDirty();
|
||||
},
|
||||
|
||||
.title => |ev| {
|
||||
self.setString(&self.title, ev.title);
|
||||
self.applyRules();
|
||||
self.wm.ipcDirty();
|
||||
},
|
||||
|
||||
.identifier => |ev| {
|
||||
self.setString(&self.identifier, ev.identifier);
|
||||
},
|
||||
|
||||
.parent => |ev| {
|
||||
self.parent = if (ev.parent) |p| Wm.windowFromProxy(p) else null;
|
||||
// Dialogs and file pickers float, as in dwm.
|
||||
if (config.float_children and self.parent != null and !self.floating_forced) {
|
||||
self.floating = true;
|
||||
}
|
||||
},
|
||||
|
||||
.fullscreen_requested => |ev| {
|
||||
self.fullscreen = true;
|
||||
if (ev.output) |o| {
|
||||
if (Wm.outputFromProxy(o)) |out| self.output = out;
|
||||
}
|
||||
self.wm.needsManage();
|
||||
},
|
||||
|
||||
.exit_fullscreen_requested => {
|
||||
self.fullscreen = false;
|
||||
self.wm.needsManage();
|
||||
},
|
||||
|
||||
.pointer_move_requested => |ev| {
|
||||
if (Wm.seatFromProxy(ev.seat)) |seat| seat.startMove(self);
|
||||
},
|
||||
|
||||
.pointer_resize_requested => |ev| {
|
||||
if (Wm.seatFromProxy(ev.seat)) |seat| seat.startResize(self, ev.edges);
|
||||
},
|
||||
|
||||
// We advertise only the fullscreen capability, so these should not
|
||||
// arrive; ignoring them is the documented option either way.
|
||||
.maximize_requested,
|
||||
.unmaximize_requested,
|
||||
.minimize_requested,
|
||||
.show_window_menu_requested,
|
||||
.decoration_hint,
|
||||
.unreliable_pid,
|
||||
.presentation_hint,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user