Add borders to windows; add navigation keybinds
Right now, colors are hardcoded in the Config in main.zig. This commit also adds a couple of new keybinds for navigating between windows. All keybinds are hardcoded as well right now.
This commit is contained in:
parent
42494ae5d1
commit
6fce659378
7 changed files with 321 additions and 34 deletions
13
src/Config.zig
Normal file
13
src/Config.zig
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
const Config = @This();
|
||||
|
||||
border_width: u3,
|
||||
border_color_focused: RiverColor,
|
||||
border_color_unfocused: RiverColor,
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const RiverColor = @import("utils.zig").RiverColor;
|
||||
14
src/Seat.zig
14
src/Seat.zig
|
|
@ -8,7 +8,7 @@ context: *Context,
|
|||
|
||||
seat_v1: *river.SeatV1,
|
||||
|
||||
hovered: ?*Window,
|
||||
focused: ?*Window,
|
||||
|
||||
link: wl.list.Link,
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ pub fn init(seat: *Seat, context: *Context, river_seat_v1: *river.SeatV1) void {
|
|||
seat.* = .{
|
||||
.context = context,
|
||||
.seat_v1 = river_seat_v1,
|
||||
.hovered = null,
|
||||
.focused = null,
|
||||
.link = undefined, // Handled by the wl.list
|
||||
};
|
||||
|
||||
|
|
@ -36,7 +36,12 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *
|
|||
.pointer_enter => |ev| {
|
||||
const window_v1 = ev.window orelse return;
|
||||
const window: *Window = @ptrCast(@alignCast(window_v1.getUserData()));
|
||||
seat.hovered = window;
|
||||
seat.focused = window;
|
||||
},
|
||||
.window_interaction => |ev| {
|
||||
const window_v1 = ev.window orelse return;
|
||||
const window: *Window = @ptrCast(@alignCast(window_v1.getUserData()));
|
||||
seat.focused = window;
|
||||
},
|
||||
else => |ev| {
|
||||
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
||||
|
|
@ -45,9 +50,8 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *
|
|||
}
|
||||
|
||||
pub fn manage(seat: *Seat) void {
|
||||
if (seat.hovered) |window| {
|
||||
if (seat.focused) |window| {
|
||||
seat.seat_v1.focusWindow(window.window_v1);
|
||||
seat.hovered = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
103
src/Window.zig
103
src/Window.zig
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2025-2026 Ben Buhse <me@benbuhse.email>
|
||||
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
|
@ -19,8 +19,23 @@ new_height: u31 = 0,
|
|||
new_x: i32 = 0,
|
||||
new_y: i32 = 0,
|
||||
|
||||
initialized: bool = false,
|
||||
|
||||
is_fullscreen: bool = false,
|
||||
is_maximized: bool = false,
|
||||
|
||||
// Maybe this should be separate for each type of action so it can't be overwritten ?
|
||||
pending_action: ?PendingAction = null,
|
||||
|
||||
link: wl.list.Link,
|
||||
|
||||
pub const PendingAction = enum {
|
||||
enter_fullscreen,
|
||||
exit_fullscreen,
|
||||
maximize,
|
||||
unmaximize,
|
||||
};
|
||||
|
||||
pub fn init(window: *Window, context: *Context, river_window_v1: *river.WindowV1) void {
|
||||
window.* = .{
|
||||
.context = context,
|
||||
|
|
@ -42,8 +57,8 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
|
|||
{
|
||||
var it = window.context.wm.seats.iterator(.forward);
|
||||
while (it.next()) |seat| {
|
||||
if (seat.hovered == window) {
|
||||
seat.hovered = null;
|
||||
if (seat.focused == window) {
|
||||
seat.focused = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,30 +81,90 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
|
|||
}
|
||||
|
||||
pub fn manage(window: *Window) void {
|
||||
defer window.pending_action = null;
|
||||
defer window.new_width = 0;
|
||||
defer window.new_height = 0;
|
||||
|
||||
if (!window.initialized) {
|
||||
@branchHint(.unlikely);
|
||||
window.initialized = true;
|
||||
|
||||
// Use server-side decoration (no client-drawn title bars)
|
||||
// TODO: We might want a way to handle clients requesting CSD (or switching between
|
||||
// CSD and SSD)
|
||||
window.window_v1.useSsd();
|
||||
|
||||
window.window_v1.setCapabilities(.{ .fullscreen = true });
|
||||
}
|
||||
|
||||
// Layout has been pre-computed by WindowManager.calculatePrimaryStackLayout()
|
||||
// Apply the computed dimensions
|
||||
if (window.new_width > 0 and window.new_height > 0) {
|
||||
window.window_v1.proposeDimensions(window.new_width, window.new_height);
|
||||
}
|
||||
|
||||
// Use server-side decoration (no client-drawn title bars)
|
||||
window.window_v1.useSsd();
|
||||
if (window.pending_action) |pending_action| {
|
||||
switch (pending_action) {
|
||||
.enter_fullscreen => {
|
||||
window.window_v1.setTiled(.{});
|
||||
// TODO: Figure out what output this Window is on?
|
||||
const output = window.context.wm.outputs.first() orelse return;
|
||||
window.window_v1.fullscreen(output.output_v1);
|
||||
window.window_v1.informFullscreen();
|
||||
|
||||
// Inform the window about its tiled/fullscreen state
|
||||
if (window.context.wm.window_count == 1) {
|
||||
// Single window is fullscreen
|
||||
window.window_v1.setTiled(.{ .top = false, .bottom = false, .left = false, .right = false });
|
||||
window.window_v1.informFullscreen();
|
||||
} else {
|
||||
// Tiled windows - set tiled edges
|
||||
window.window_v1.informNotFullscreen();
|
||||
window.window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
|
||||
window.is_fullscreen = true;
|
||||
},
|
||||
.exit_fullscreen => {
|
||||
window.window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
|
||||
window.window_v1.exitFullscreen();
|
||||
window.window_v1.informNotFullscreen();
|
||||
|
||||
window.is_fullscreen = false;
|
||||
},
|
||||
.maximize => {
|
||||
window.window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
|
||||
window.window_v1.informMaximized();
|
||||
|
||||
window.is_maximized = true;
|
||||
},
|
||||
.unmaximize => {
|
||||
window.window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
|
||||
window.window_v1.informUnmaximized();
|
||||
|
||||
window.is_maximized = false;
|
||||
},
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn render(window: *Window) void {
|
||||
// Set the window position using the pre-computed layout
|
||||
window.node_v1.setPosition(window.new_x, window.new_y);
|
||||
|
||||
// Set borders
|
||||
if (!window.is_fullscreen) {
|
||||
const border_width = window.context.config.border_width;
|
||||
if (window.isFocused()) {
|
||||
const border_color_focused = window.context.config.border_color_focused;
|
||||
window.window_v1.setBorders(.{ .top = true, .bottom = true, .left = true, .right = true }, border_width, border_color_focused.red, border_color_focused.green, border_color_focused.blue, border_color_focused.alpha);
|
||||
} else {
|
||||
const border_color_unfocused = window.context.config.border_color_unfocused;
|
||||
window.window_v1.setBorders(.{ .top = true, .bottom = true, .left = true, .right = true }, border_width, border_color_unfocused.red, border_color_unfocused.green, border_color_unfocused.blue, border_color_unfocused.alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn isFocused(window: *Window) bool {
|
||||
// Probably a better way to do this... keep currently focused window directly in context?
|
||||
// Regardless, we only have one Seat so this shouldn't be awful performance-wise
|
||||
var it = window.context.wm.seats.iterator(.forward);
|
||||
while (it.next()) |seat| {
|
||||
if (seat.focused == window) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2025-2026 Ben Buhse <me@benbuhse.email>
|
||||
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
|
@ -16,6 +16,30 @@ windows: wl.list.Head(Window, .link),
|
|||
|
||||
window_count: u8 = 0,
|
||||
|
||||
/// Get the next window in the list, wrapping to first if at end
|
||||
pub fn getNextWindow(wm: *WindowManager, current: *Window) ?*Window {
|
||||
var it = wm.windows.iterator(.forward);
|
||||
while (it.next()) |window| {
|
||||
if (window == current) {
|
||||
return it.next() orelse wm.windows.first();
|
||||
}
|
||||
}
|
||||
return wm.windows.first();
|
||||
}
|
||||
|
||||
/// Get the previous window in the list, wrapping to last if at beginning
|
||||
pub fn getPrevWindow(wm: *WindowManager, current: *Window) ?*Window {
|
||||
var prev: ?*Window = null;
|
||||
var it = wm.windows.iterator(.forward);
|
||||
while (it.next()) |window| {
|
||||
if (window == current) {
|
||||
return prev orelse wm.windows.last();
|
||||
}
|
||||
prev = window;
|
||||
}
|
||||
return wm.windows.last();
|
||||
}
|
||||
|
||||
pub fn init(wm: *WindowManager, context: *Context, window_manager_v1: *river.WindowManagerV1) void {
|
||||
assert(wm == &context.wm);
|
||||
wm.* = .{
|
||||
|
|
@ -36,7 +60,7 @@ pub fn init(wm: *WindowManager, context: *Context, window_manager_v1: *river.Win
|
|||
/// Calculate primary/stack layout positions for all windows.
|
||||
/// - Single window: fullscreen
|
||||
/// - Multiple windows: stack (45% left, vertically tiled), primary (55% right)
|
||||
pub fn calculatePrimaryStackLayout(wm: *WindowManager) void {
|
||||
fn calculatePrimaryStackLayout(wm: *WindowManager) void {
|
||||
const count = wm.window_count;
|
||||
if (count == 0) return;
|
||||
|
||||
|
|
@ -50,27 +74,44 @@ pub fn calculatePrimaryStackLayout(wm: *WindowManager) void {
|
|||
var index: u8 = 0;
|
||||
var it = wm.windows.iterator(.forward);
|
||||
while (it.next()) |window| {
|
||||
if (count == 1) {
|
||||
// Single window: fullscreen
|
||||
if (window.is_fullscreen) {
|
||||
// Count doesn't matter if a window is fullscreen
|
||||
window.new_x = output_x;
|
||||
window.new_y = output_y;
|
||||
window.new_width = output_width;
|
||||
window.new_height = output_height;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count == 1) {
|
||||
// Single window: maximize
|
||||
// TODO: Make maximum window aspect ratio 16:10, i.e. a single window in an ultrawide
|
||||
// monitor only takes up 16:10 area in the middle with empty space on the outsides
|
||||
window.new_x = output_x;
|
||||
window.new_y = output_y;
|
||||
window.new_width = output_width;
|
||||
window.new_height = output_height;
|
||||
|
||||
if (!window.is_maximized) {
|
||||
window.pending_action = .maximize;
|
||||
}
|
||||
} else {
|
||||
// Multiple windows: primary/stack layout
|
||||
// TODO: Support multiple primaries
|
||||
const primary_width: u31 = @intFromFloat(@as(f32, @floatFromInt(output_width)) * PRIMARY_RATIO);
|
||||
const stack_width: u31 = output_width - primary_width;
|
||||
const stack_count = count - 1;
|
||||
const stack_height: u31 = @divFloor(output_height, stack_count);
|
||||
|
||||
if (index == 0) {
|
||||
// Primary window (first window) - right side
|
||||
// TODO: Support configuring primary side
|
||||
// Primary window - right side
|
||||
window.new_x = output_x + @as(i32, stack_width);
|
||||
window.new_y = output_y;
|
||||
window.new_width = primary_width;
|
||||
window.new_height = output_height;
|
||||
} else {
|
||||
// Stack window - left side
|
||||
// Stack window(s) - left side
|
||||
const stack_index = index - 1;
|
||||
window.new_x = output_x;
|
||||
window.new_y = output_y + @as(i32, stack_index) * @as(i32, stack_height);
|
||||
|
|
@ -82,6 +123,17 @@ pub fn calculatePrimaryStackLayout(wm: *WindowManager) void {
|
|||
window.new_height = stack_height;
|
||||
}
|
||||
}
|
||||
|
||||
// Make space for borders; this is the same for all non-fullscreen windows
|
||||
const border_width = wm.context.config.border_width;
|
||||
window.new_height -= 2 * border_width;
|
||||
window.new_width -= 2 * border_width;
|
||||
window.new_x += border_width;
|
||||
window.new_y += border_width;
|
||||
|
||||
if (window.is_maximized) {
|
||||
window.pending_action = .unmaximize;
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
|
@ -97,9 +149,18 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
|
|||
},
|
||||
.manage_start => {
|
||||
if (!context.initialized) {
|
||||
// This code (should) only be run once while initializing the WM, so let's
|
||||
// mark it as cold.
|
||||
@branchHint(.cold);
|
||||
context.initialized = true;
|
||||
context.xkb_bindings.addBinding(xkbcommon.Keysym.t, .{ .mod1 = true });
|
||||
context.xkb_bindings.addBinding(xkbcommon.Keysym.t, .{ .mod4 = true }, .{ .spawn = &.{"foot"} });
|
||||
context.xkb_bindings.addBinding(xkbcommon.Keysym.j, .{ .mod4 = true }, .focus_next);
|
||||
context.xkb_bindings.addBinding(xkbcommon.Keysym.k, .{ .mod4 = true }, .focus_prev);
|
||||
context.xkb_bindings.addBinding(xkbcommon.Keysym.f, .{ .mod4 = true }, .fullscreen);
|
||||
context.xkb_bindings.addBinding(xkbcommon.Keysym.q, .{ .mod4 = true, .shift = true }, .close_window);
|
||||
context.xkb_bindings.addBinding(xkbcommon.Keysym.e, .{ .mod4 = true, .shift = true }, .exit);
|
||||
}
|
||||
|
||||
{
|
||||
var it = wm.seats.iterator(.forward);
|
||||
while (it.next()) |seat| {
|
||||
|
|
@ -161,7 +222,6 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
|
|||
window.init(context, ev.id);
|
||||
wm.windows.append(window);
|
||||
wm.window_count += 1;
|
||||
log.debug("window_count = {d}", .{wm.window_count});
|
||||
},
|
||||
else => |ev| {
|
||||
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
||||
|
|
@ -182,5 +242,6 @@ const Context = @import("main.zig").Context;
|
|||
const Output = @import("Output.zig");
|
||||
const Seat = @import("Seat.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const XkbBindings = @import("XkbBindings.zig");
|
||||
|
||||
const log = std.log.scoped(.WindowManager);
|
||||
|
|
|
|||
|
|
@ -4,13 +4,26 @@
|
|||
|
||||
const XkbBindings = @This();
|
||||
|
||||
pub const Command = union(enum) {
|
||||
spawn: []const []const u8,
|
||||
focus_next,
|
||||
focus_prev,
|
||||
close_window,
|
||||
fullscreen,
|
||||
exit,
|
||||
};
|
||||
|
||||
const XkbBinding = struct {
|
||||
xkb_binding_v1: *river.XkbBindingV1,
|
||||
command: Command,
|
||||
context: *Context,
|
||||
link: wl.list.Link,
|
||||
|
||||
fn init(xkb_binding: *XkbBinding, xkb_binding_v1: *river.XkbBindingV1) void {
|
||||
fn init(xkb_binding: *XkbBinding, xkb_binding_v1: *river.XkbBindingV1, command: Command, context: *Context) void {
|
||||
xkb_binding.* = .{
|
||||
.xkb_binding_v1 = xkb_binding_v1,
|
||||
.command = command,
|
||||
.context = context,
|
||||
.link = undefined, // Handled by the wl.list
|
||||
};
|
||||
|
||||
|
|
@ -21,10 +34,7 @@ const XkbBinding = struct {
|
|||
assert(xkb_binding.xkb_binding_v1 == xkb_binding_v1);
|
||||
switch (event) {
|
||||
.pressed => {
|
||||
var child = std.process.Child.init(&.{"foot"}, std.heap.c_allocator);
|
||||
_ = child.spawn() catch |err| {
|
||||
log.err("Failed to spawn foot: {}", .{err});
|
||||
};
|
||||
xkb_binding.executeCommand();
|
||||
},
|
||||
.released => {},
|
||||
else => |ev| {
|
||||
|
|
@ -32,6 +42,56 @@ const XkbBinding = struct {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn executeCommand(xkb_binding: *XkbBinding) void {
|
||||
const context = xkb_binding.context;
|
||||
switch (xkb_binding.command) {
|
||||
.spawn => |cmd| {
|
||||
var child = std.process.Child.init(cmd, std.heap.c_allocator);
|
||||
_ = child.spawn() catch |err| {
|
||||
log.err("Failed to spawn foot: {}", .{err});
|
||||
};
|
||||
},
|
||||
.focus_next => {
|
||||
const seat = context.wm.seats.first() orelse return;
|
||||
if (seat.focused) |current| {
|
||||
seat.focused = context.wm.getNextWindow(current);
|
||||
} else {
|
||||
// No window focused, focus the first one
|
||||
seat.focused = context.wm.windows.first();
|
||||
}
|
||||
context.wm.window_manager_v1.manageDirty();
|
||||
},
|
||||
.focus_prev => {
|
||||
const seat = context.wm.seats.first() orelse return;
|
||||
if (seat.focused) |current| {
|
||||
seat.focused = context.wm.getPrevWindow(current);
|
||||
} else {
|
||||
// No window focused, focus the last one
|
||||
seat.focused = context.wm.windows.last();
|
||||
}
|
||||
context.wm.window_manager_v1.manageDirty();
|
||||
},
|
||||
.fullscreen => {
|
||||
const seat = context.wm.seats.first() orelse return;
|
||||
const window = seat.focused orelse return;
|
||||
// Toggle between fullscreen and not
|
||||
window.pending_action = if (window.is_fullscreen) .exit_fullscreen else .enter_fullscreen;
|
||||
context.wm.window_manager_v1.manageDirty();
|
||||
log.debug("pending_action={?}", .{window.pending_action});
|
||||
},
|
||||
.close_window => {
|
||||
const seat = context.wm.seats.first() orelse return;
|
||||
if (seat.focused) |window| {
|
||||
window.window_v1.close();
|
||||
}
|
||||
},
|
||||
.exit => {
|
||||
// TODO: Disabled while I'm working with river within river :P
|
||||
// _ = std.process.Child.init(&.{"killall river"}, std.heap.c_allocator);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
context: *Context,
|
||||
|
|
@ -58,7 +118,7 @@ pub fn getSeat(xkb_bindings: *XkbBindings) *river.SeatV1 {
|
|||
return seat.seat_v1;
|
||||
}
|
||||
|
||||
pub fn addBinding(xkb_bindings: *XkbBindings, keysym: u32, modifiers: river.SeatV1.Modifiers) void {
|
||||
pub fn addBinding(xkb_bindings: *XkbBindings, keysym: u32, modifiers: river.SeatV1.Modifiers, command: Command) void {
|
||||
const seat_v1 = xkb_bindings.getSeat();
|
||||
const xkb_binding_v1 = xkb_bindings.xkb_bindings_v1.getXkbBinding(seat_v1, keysym, modifiers) catch |err| {
|
||||
log.err("Failed to get xkb binding: {}", .{err});
|
||||
|
|
@ -66,7 +126,7 @@ pub fn addBinding(xkb_bindings: *XkbBindings, keysym: u32, modifiers: river.Seat
|
|||
};
|
||||
|
||||
const xkb_binding = xkb_bindings.context.allocator.create(XkbBinding) catch @panic("out-of-memory");
|
||||
xkb_binding.init(xkb_binding_v1);
|
||||
xkb_binding.init(xkb_binding_v1, command, xkb_bindings.context);
|
||||
xkb_bindings.bindings.append(xkb_binding);
|
||||
|
||||
xkb_binding_v1.enable();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ pub const Context = struct {
|
|||
wm: WindowManager,
|
||||
xkb_bindings: XkbBindings,
|
||||
|
||||
config: Config,
|
||||
|
||||
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, context: *Context) void {
|
||||
switch (event) {
|
||||
.global => |ev| {
|
||||
|
|
@ -77,6 +79,8 @@ pub fn main() !void {
|
|||
.registry = registry,
|
||||
.wm = undefined,
|
||||
.xkb_bindings = undefined,
|
||||
// Hardcoded config for now
|
||||
.config = .{ .border_width = 2, .border_color_focused = utils.parseRgbaComptime("0x89b4fa"), .border_color_unfocused = utils.parseRgbaComptime("0x1e1e2e") },
|
||||
};
|
||||
|
||||
registry.setListener(*Context, Context.registryListener, &context);
|
||||
|
|
@ -97,6 +101,7 @@ pub fn main() !void {
|
|||
log.info("Exiting beansprout", .{});
|
||||
}
|
||||
|
||||
// TODO: Actually use this...
|
||||
fn interfaceNotAdvertised(comptime WaylandGlobal: type) noreturn {
|
||||
log.err("{s} not advertised. Exiting", .{WaylandGlobal.interface.name});
|
||||
std.posix.exit(1);
|
||||
|
|
@ -114,6 +119,8 @@ const wayland = @import("wayland");
|
|||
const river = wayland.client.river;
|
||||
const wl = wayland.client.wl;
|
||||
|
||||
const utils = @import("utils.zig");
|
||||
const Config = @import("Config.zig");
|
||||
const WindowManager = @import("WindowManager.zig");
|
||||
const XkbBindings = @import("XkbBindings.zig");
|
||||
|
||||
|
|
|
|||
67
src/utils.zig
Normal file
67
src/utils.zig
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pub const RiverColor = struct {
|
||||
red: u32,
|
||||
green: u32,
|
||||
blue: u32,
|
||||
alpha: u32,
|
||||
};
|
||||
|
||||
/// Parse a color in the format 0xRRGGBB or 0xRRGGBBAA and convert it to
|
||||
/// 32-bit color values (used by Window.set_borders in rwm).
|
||||
pub fn parseRgba(s: []const u8) !RiverColor {
|
||||
if (s.len != 8 and s.len != 10) return error.InvalidRgba;
|
||||
if (s[0] != '0' or s[1] != 'x') return error.InvalidRgba;
|
||||
|
||||
// If the color is 0xRRGGBB, add FF for the alpha channel
|
||||
var color = try fmt.parseUnsigned(u32, s[2..], 16);
|
||||
if (s.len == 8) {
|
||||
color <<= 8;
|
||||
color |= 0xff;
|
||||
}
|
||||
|
||||
const bytes: [4]u8 = @as([4]u8, @bitCast(color));
|
||||
return parseRgbaHelper(bytes);
|
||||
}
|
||||
|
||||
/// Parse a color in the format 0xRRGGBB or 0xRRGGBBAA and convert it to
|
||||
/// 32-bit color values (used by Window.set_borders in rwm) at comptime.
|
||||
pub fn parseRgbaComptime(comptime s: []const u8) RiverColor {
|
||||
if (s.len != 8 and s.len != 10) @compileError("Invalid RGBA");
|
||||
if (s[0] != '0' or s[1] != 'x') @compileError("Invalid RGBA");
|
||||
|
||||
// If the color is 0xRRGGBB, add FF for the alpha channel
|
||||
comptime var color = try fmt.parseUnsigned(u32, s[2..], 16);
|
||||
if (s.len == 8) {
|
||||
color <<= 8;
|
||||
color |= 0xff;
|
||||
}
|
||||
|
||||
const bytes = @as([4]u8, @bitCast(color));
|
||||
return parseRgbaHelper(bytes);
|
||||
}
|
||||
|
||||
fn parseRgbaHelper(bytes: [4]u8) RiverColor {
|
||||
const r: u32 = bytes[3];
|
||||
const g: u32 = bytes[2];
|
||||
const b: u32 = bytes[1];
|
||||
const a: u32 = bytes[0];
|
||||
|
||||
// To convert from an 8-bit color to 32-bit color, we need to do
|
||||
// color * 2^32 / 2^8
|
||||
// which is equivalent to
|
||||
// color * 2^24
|
||||
// or, in other words,
|
||||
// color << 24
|
||||
return .{
|
||||
.red = r << 24,
|
||||
.green = g << 24,
|
||||
.blue = b << 24,
|
||||
.alpha = a << 24,
|
||||
};
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const fmt = std.fmt;
|
||||
Loading…
Add table
Add a link
Reference in a new issue