diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index f0ec0f656..deb0a6987 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -63,6 +63,26 @@ extension Ghostty { return 1 - opacity } + // The color for the rectangle overlay when unfocused. + private var unfocusedFill: Color { + var rgb: UInt32 = 16777215 // white default + let key = "unfocused-split-fill" + if (!ghostty_config_get(ghostty.config, &rgb, key, UInt(key.count))) { + let bg_key = "background" + _ = ghostty_config_get(ghostty.config, &rgb, bg_key, UInt(bg_key.count)); + } + + let red = Double(rgb & 0xff) + let green = Double((rgb >> 8) & 0xff) + let blue = Double((rgb >> 16) & 0xff) + + return Color( + red: red / 255, + green: green / 255, + blue: blue / 255 + ) + } + var body: some View { ZStack { // We use a GeometryReader to get the frame bounds so that our metal surface @@ -155,10 +175,13 @@ extension Ghostty { // because we want to keep our focused surface dark even if we don't have window // focus. if (isSplit && !surfaceFocus) { - Rectangle() - .fill(.white) - .allowsHitTesting(false) - .opacity(unfocusedOpacity) + let overlayOpacity = unfocusedOpacity; + if (overlayOpacity > 0) { + Rectangle() + .fill(unfocusedFill) + .allowsHitTesting(false) + .opacity(overlayOpacity) + } } } } diff --git a/src/config/Config.zig b/src/config/Config.zig index e741083be..49eb55df7 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -319,7 +319,15 @@ palette: Palette = .{}, /// minimum value is 0.15. This value still looks weird but you can at least /// see what's going on. A value outside of the range 0.15 to 1 will be /// clamped to the nearest valid value. -@"unfocused-split-opacity": f64 = 0.85, +@"unfocused-split-opacity": f64 = 0.7, + +// The color to dim the unfocused split. Unfocused splits are dimmed by +// rendering a semi-transparent rectangle over the split. This sets +// the color of that rectangle and can be used to carefully control +// the dimming effect. +// +// This will default to the background color. +@"unfocused-split-fill": ?Color = null, /// The command to run, usually a shell. If this is not an absolute path, /// it'll be looked up in the PATH. If this is not set, a default will @@ -1991,7 +1999,10 @@ pub const OptionAsAlt = enum { }; /// Color represents a color using RGB. -pub const Color = struct { +/// +/// This is a packed struct so that the C API to read color values just +/// works by setting it to a C integer. +pub const Color = packed struct(u24) { r: u8, g: u8, b: u8, diff --git a/src/config/c_get.zig b/src/config/c_get.zig index 504e98a87..2bc2ae10e 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -2,6 +2,7 @@ const std = @import("std"); const key = @import("key.zig"); const Config = @import("Config.zig"); +const Color = Config.Color; const Key = key.Key; const Value = key.Value; @@ -17,42 +18,66 @@ pub fn get(config: *const Config, k: Key, ptr_raw: *anyopaque) bool { switch (k) { inline else => |tag| { const value = fieldByKey(config, tag); - switch (@TypeOf(value)) { - ?[:0]const u8 => { - const ptr: *?[*:0]const u8 = @ptrCast(@alignCast(ptr_raw)); - ptr.* = if (value) |slice| @ptrCast(slice.ptr) else null; - }, - - bool => { - const ptr: *bool = @ptrCast(@alignCast(ptr_raw)); - ptr.* = value; - }, - - u8, u32 => { - const ptr: *c_uint = @ptrCast(@alignCast(ptr_raw)); - ptr.* = @intCast(value); - }, - - f32, f64 => { - const ptr: *f64 = @ptrCast(@alignCast(ptr_raw)); - ptr.* = @floatCast(value); - }, - - else => |T| switch (@typeInfo(T)) { - .Enum => { - const ptr: *[*:0]const u8 = @ptrCast(@alignCast(ptr_raw)); - ptr.* = @tagName(value); - }, - - else => return false, - }, - } - - return true; + return getValue(ptr_raw, value); }, } } +/// Get the value anytype and put it into the pointer. Returns false if +/// the type is not supported by the C API yet or the value is null. +fn getValue(ptr_raw: *anyopaque, value: anytype) bool { + switch (@TypeOf(value)) { + ?[:0]const u8 => { + const ptr: *?[*:0]const u8 = @ptrCast(@alignCast(ptr_raw)); + ptr.* = if (value) |slice| @ptrCast(slice.ptr) else null; + }, + + bool => { + const ptr: *bool = @ptrCast(@alignCast(ptr_raw)); + ptr.* = value; + }, + + u8, u32 => { + const ptr: *c_uint = @ptrCast(@alignCast(ptr_raw)); + ptr.* = @intCast(value); + }, + + f32, f64 => { + const ptr: *f64 = @ptrCast(@alignCast(ptr_raw)); + ptr.* = @floatCast(value); + }, + + else => |T| switch (@typeInfo(T)) { + .Optional => { + // If an optional has no value we return false. + const unwrapped = value orelse return false; + return getValue(ptr_raw, unwrapped); + }, + + .Enum => { + const ptr: *[*:0]const u8 = @ptrCast(@alignCast(ptr_raw)); + ptr.* = @tagName(value); + }, + + .Struct => |info| { + // Packed structs that are less than or equal to the + // size of a C int can be passed directly as their + // bit representation. + if (info.layout != .Packed) return false; + const Backing = info.backing_integer orelse return false; + if (@bitSizeOf(Backing) > @bitSizeOf(c_uint)) return false; + + const ptr: *c_uint = @ptrCast(@alignCast(ptr_raw)); + ptr.* = @intCast(@as(Backing, @bitCast(value))); + }, + + else => return false, + }, + } + + return true; +} + /// Get a value from the config by key. fn fieldByKey(self: *const Config, comptime k: Key) Value(k) { const field = comptime field: { @@ -96,3 +121,37 @@ test "enum" { const str = std.mem.sliceTo(cval, 0); try testing.expectEqualStrings("dark", str); } + +test "color" { + const testing = std.testing; + const alloc = testing.allocator; + + var c = try Config.default(alloc); + defer c.deinit(); + c.background = .{ .r = 255, .g = 0, .b = 0 }; + + var cval: c_uint = undefined; + try testing.expect(get(&c, .background, @ptrCast(&cval))); + try testing.expectEqual(@as(c_uint, 255), cval); +} + +test "optional" { + const testing = std.testing; + const alloc = testing.allocator; + + var c = try Config.default(alloc); + defer c.deinit(); + + { + c.@"unfocused-split-fill" = null; + var cval: c_uint = undefined; + try testing.expect(!get(&c, .@"unfocused-split-fill", @ptrCast(&cval))); + } + + { + c.@"unfocused-split-fill" = .{ .r = 255, .g = 0, .b = 0 }; + var cval: c_uint = undefined; + try testing.expect(get(&c, .@"unfocused-split-fill", @ptrCast(&cval))); + try testing.expectEqual(@as(c_uint, 255), cval); + } +}