//! Colour conversion. //! //! Config declares colours as the familiar 0xRRGGBBAA with straight alpha. //! The two sinks want something different: //! //! * `river_window_v1.set_borders` takes one full-range u32 per channel — //! river divides each by maxInt(u32) — with premultiplied alpha. //! * wl_shm ARGB8888 wants premultiplied 8-bit channels packed into a u32. const std = @import("std"); pub const Rgba = u32; pub const Channels = struct { r: u32, g: u32, b: u32, a: u32, }; fn premul8(c: u8, a: u8) u8 { // Round to nearest rather than truncating, so 0xff at full alpha stays // 0xff instead of drifting down. return @intCast((@as(u32, c) * @as(u32, a) + 127) / 255); } /// Expand an 8-bit channel to the full u32 range: 0xff maps exactly to /// 0xffffffff, which is what river treats as 1.0. fn expand(c: u8) u32 { return @as(u32, c) * 0x01010101; } fn split(rgba: Rgba) [4]u8 { return .{ @intCast((rgba >> 24) & 0xff), @intCast((rgba >> 16) & 0xff), @intCast((rgba >> 8) & 0xff), @intCast(rgba & 0xff), }; } /// Premultiplied, full-range channels for `set_borders`. pub fn toChannels(rgba: Rgba) Channels { const c = split(rgba); const a = c[3]; return .{ .r = expand(premul8(c[0], a)), .g = expand(premul8(c[1], a)), .b = expand(premul8(c[2], a)), .a = expand(a), }; } /// Premultiplied ARGB8888 as a native-endian u32, for wl_shm buffers. pub fn toArgb8888(rgba: Rgba) u32 { const c = split(rgba); const a = c[3]; return (@as(u32, a) << 24) | (@as(u32, premul8(c[0], a)) << 16) | (@as(u32, premul8(c[1], a)) << 8) | @as(u32, premul8(c[2], a)); } test "opaque white survives both conversions intact" { const ch = toChannels(0xffffffff); try std.testing.expectEqual(@as(u32, 0xffffffff), ch.r); try std.testing.expectEqual(@as(u32, 0xffffffff), ch.a); try std.testing.expectEqual(@as(u32, 0xffffffff), toArgb8888(0xffffffff)); } test "fully transparent premultiplies to zero" { const ch = toChannels(0xffffff00); try std.testing.expectEqual(@as(u32, 0), ch.r); try std.testing.expectEqual(@as(u32, 0), ch.a); try std.testing.expectEqual(@as(u32, 0), toArgb8888(0xffffff00)); } test "opaque colour keeps its channels in argb order" { // 0xRRGGBBAA -> 0xAARRGGBB try std.testing.expectEqual(@as(u32, 0xff5294e2), toArgb8888(0x5294e2ff)); } test "half alpha premultiplies channels but not alpha" { const ch = toChannels(0xff000080); try std.testing.expectEqual(@as(u32, 0x80808080), ch.a); // 0xff * 0x80 / 0xff == 0x80 try std.testing.expectEqual(@as(u32, 0x80808080), ch.r); try std.testing.expectEqual(@as(u32, 0), ch.g); }