From 8e607f372bd248e7aa0a3ca264164d67a61cfb7e Mon Sep 17 00:00:00 2001 From: Matt Robenolt Date: Tue, 12 Dec 2023 16:34:41 -0800 Subject: [PATCH 1/8] Configurable unfocused dimming color --- macos/Sources/Ghostty/SurfaceView.swift | 34 ++++++++++++++++++++++--- src/config/Config.zig | 17 +++++++++++++ src/config/c_get.zig | 6 +++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index e83f08cc3..082406774 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -60,9 +60,32 @@ extension Ghostty { var opacity: Double = 0.85 let key = "unfocused-split-opacity" _ = ghostty_config_get(ghostty.config, &opacity, key, UInt(key.count)) + AppDelegate.logger.warning("ghostty_config_get(\(key))=\(opacity)") return 1 - opacity } + private var unfocusedFill: Color { + var rgb: UInt32 = 16777215 // white default + let key = "unfocused-split-fill" + _ = ghostty_config_get(ghostty.config, &rgb, key, UInt(key.count)) + AppDelegate.logger.warning("ghostty_config_get(\(key))=\(rgb)") + let red = Double((rgb >> 16) & 0xff) + let green = Double((rgb >> 8) & 0xff) + let blue = Double(rgb & 0xff) + AppDelegate.logger.warning("red=\(red) green=\(green) blue=\(blue)") + return Color.init( + red: 255, + green: 0, + blue: 0 + ) +// return Color.init( +// red: red, +// green: green, +// blue: blue, +// opacity: unfocusedOpacity +// ) + } + var body: some View { ZStack { // We use a GeometryReader to get the frame bounds so that our metal surface @@ -155,10 +178,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 de8274cab..778d6c70e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -314,6 +314,8 @@ palette: Palette = .{}, /// clamped to the nearest valid value. @"unfocused-split-opacity": f64 = 0.85, +@"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 /// be looked up from your system. The rules for the default lookup are: @@ -1989,6 +1991,21 @@ pub const Color = struct { return .{ .r = self.r, .g = self.g, .b = self.b }; } + // Pack into an integer + pub fn toInt(self: Color) u24 { + // u24 covers RGB, typically, an alpha would pack to a full u32 + return (@as(u24, self.r) << 16) + (@as(u24, self.g) << 8) + self.b; + } + + test "toInt" { + const testing = std.testing; + + try testing.exectEqual((Color{ .r = 0, .g = 0, .b = 0 }).toInt(), 0); + try testing.exectEqual((Color{ .r = 255, .g = 255, .b = 255 }).toInt(), 16777215); + try testing.exectEqual((Color{ .r = 100, .g = 20, .b = 12 }).toInt(), 6558732); + try testing.exectEqual((Color{ .r = 55, .g = 63, .b = 202 }).toInt(), 3620810); + } + pub fn parseCLI(input: ?[]const u8) !Color { return fromHex(input orelse return error.ValueRequired); } diff --git a/src/config/c_get.zig b/src/config/c_get.zig index 504e98a87..4a123674d 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; @@ -38,6 +39,11 @@ pub fn get(config: *const Config, k: Key, ptr_raw: *anyopaque) bool { ptr.* = @floatCast(value); }, + ?Color => { + const ptr: *?c_uint = @ptrCast(@alignCast(ptr_raw)); + ptr.* = if (value) |c| c.toInt() else null; + }, + else => |T| switch (@typeInfo(T)) { .Enum => { const ptr: *[*:0]const u8 = @ptrCast(@alignCast(ptr_raw)); From 91937c4ada7766b796c5bb4195588907e670de6e Mon Sep 17 00:00:00 2001 From: Matt Robenolt Date: Tue, 12 Dec 2023 17:01:50 -0800 Subject: [PATCH 2/8] Apply feedback --- macos/Sources/Ghostty/SurfaceView.swift | 18 +++++------------- src/config/Config.zig | 3 ++- src/config/c_get.zig | 6 +++--- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 082406774..b8ad52f34 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -60,30 +60,22 @@ extension Ghostty { var opacity: Double = 0.85 let key = "unfocused-split-opacity" _ = ghostty_config_get(ghostty.config, &opacity, key, UInt(key.count)) - AppDelegate.logger.warning("ghostty_config_get(\(key))=\(opacity)") return 1 - opacity } + // The color for the rectable overlay when unfocused. private var unfocusedFill: Color { var rgb: UInt32 = 16777215 // white default let key = "unfocused-split-fill" _ = ghostty_config_get(ghostty.config, &rgb, key, UInt(key.count)) - AppDelegate.logger.warning("ghostty_config_get(\(key))=\(rgb)") let red = Double((rgb >> 16) & 0xff) let green = Double((rgb >> 8) & 0xff) let blue = Double(rgb & 0xff) - AppDelegate.logger.warning("red=\(red) green=\(green) blue=\(blue)") - return Color.init( - red: 255, - green: 0, - blue: 0 + return Color( + red: red / 255, + green: green / 255, + blue: blue / 255 ) -// return Color.init( -// red: red, -// green: green, -// blue: blue, -// opacity: unfocusedOpacity -// ) } var body: some View { diff --git a/src/config/Config.zig b/src/config/Config.zig index 778d6c70e..d468cf0e1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -314,7 +314,8 @@ palette: Palette = .{}, /// clamped to the nearest valid value. @"unfocused-split-opacity": f64 = 0.85, -@"unfocused-split-fill": ?Color = null, +// The color to dim the unfocused split. +@"unfocused-split-fill": Color = Color{ .r = 255, .g = 255, .b = 255 }, /// 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 diff --git a/src/config/c_get.zig b/src/config/c_get.zig index 4a123674d..8032dce46 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -39,9 +39,9 @@ pub fn get(config: *const Config, k: Key, ptr_raw: *anyopaque) bool { ptr.* = @floatCast(value); }, - ?Color => { - const ptr: *?c_uint = @ptrCast(@alignCast(ptr_raw)); - ptr.* = if (value) |c| c.toInt() else null; + Color => { + const ptr: *c_uint = @ptrCast(@alignCast(ptr_raw)); + ptr.* = value.toInt(); }, else => |T| switch (@typeInfo(T)) { From 3866e09210c1001f12b7391bc5a8b5d09dbab332 Mon Sep 17 00:00:00 2001 From: Matt Robenolt Date: Tue, 12 Dec 2023 17:20:42 -0800 Subject: [PATCH 3/8] Use packed struct --- src/config/Config.zig | 17 +---------------- src/config/c_get.zig | 2 +- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index d468cf0e1..7fcae46b6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1982,7 +1982,7 @@ pub const OptionAsAlt = enum { }; /// Color represents a color using RGB. -pub const Color = struct { +pub const Color = packed struct(u24) { r: u8, g: u8, b: u8, @@ -1992,21 +1992,6 @@ pub const Color = struct { return .{ .r = self.r, .g = self.g, .b = self.b }; } - // Pack into an integer - pub fn toInt(self: Color) u24 { - // u24 covers RGB, typically, an alpha would pack to a full u32 - return (@as(u24, self.r) << 16) + (@as(u24, self.g) << 8) + self.b; - } - - test "toInt" { - const testing = std.testing; - - try testing.exectEqual((Color{ .r = 0, .g = 0, .b = 0 }).toInt(), 0); - try testing.exectEqual((Color{ .r = 255, .g = 255, .b = 255 }).toInt(), 16777215); - try testing.exectEqual((Color{ .r = 100, .g = 20, .b = 12 }).toInt(), 6558732); - try testing.exectEqual((Color{ .r = 55, .g = 63, .b = 202 }).toInt(), 3620810); - } - pub fn parseCLI(input: ?[]const u8) !Color { return fromHex(input orelse return error.ValueRequired); } diff --git a/src/config/c_get.zig b/src/config/c_get.zig index 8032dce46..0305100a6 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -41,7 +41,7 @@ pub fn get(config: *const Config, k: Key, ptr_raw: *anyopaque) bool { Color => { const ptr: *c_uint = @ptrCast(@alignCast(ptr_raw)); - ptr.* = value.toInt(); + ptr.* = @as(c_uint, @as(u24, @bitCast(value))); }, else => |T| switch (@typeInfo(T)) { From 1b039b35ac6d135233109eab78efa1993a999277 Mon Sep 17 00:00:00 2001 From: Matt Robenolt Date: Tue, 12 Dec 2023 17:22:37 -0800 Subject: [PATCH 4/8] typos --- macos/Sources/Ghostty/SurfaceView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index b8ad52f34..f013d908a 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -63,7 +63,7 @@ extension Ghostty { return 1 - opacity } - // The color for the rectable overlay when unfocused. + // The color for the rectangle overlay when unfocused. private var unfocusedFill: Color { var rgb: UInt32 = 16777215 // white default let key = "unfocused-split-fill" From ae6645586e54faa2f7f7ef6846f9012079a3c6e1 Mon Sep 17 00:00:00 2001 From: Matt Robenolt Date: Tue, 12 Dec 2023 21:56:35 -0800 Subject: [PATCH 5/8] unpack rgb correctly --- macos/Sources/Ghostty/SurfaceView.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index f013d908a..29f0dcf73 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -68,9 +68,11 @@ extension Ghostty { var rgb: UInt32 = 16777215 // white default let key = "unfocused-split-fill" _ = ghostty_config_get(ghostty.config, &rgb, key, UInt(key.count)) - let red = Double((rgb >> 16) & 0xff) + + let red = Double(rgb & 0xff) let green = Double((rgb >> 8) & 0xff) - let blue = Double(rgb & 0xff) + let blue = Double((rgb >> 16) & 0xff) + return Color( red: red / 255, green: green / 255, From 6b94252da601ca7727d474fc78b1905e714d3fd4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 13 Dec 2023 18:54:36 -0800 Subject: [PATCH 6/8] config: docs --- src/config/Config.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 7fcae46b6..1f930ed74 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -314,8 +314,11 @@ palette: Palette = .{}, /// clamped to the nearest valid value. @"unfocused-split-opacity": f64 = 0.85, -// The color to dim the unfocused split. -@"unfocused-split-fill": Color = Color{ .r = 255, .g = 255, .b = 255 }, +// 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. +@"unfocused-split-fill": Color = .{ .r = 255, .g = 255, .b = 255 }, /// 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 @@ -1982,6 +1985,9 @@ pub const OptionAsAlt = enum { }; /// Color represents a color using RGB. +/// +/// 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, From 4e0916d397d67a7dfe1ebac635825591d9858f06 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 13 Dec 2023 18:54:41 -0800 Subject: [PATCH 7/8] config: C API read allows any packed struct that fits in c int --- src/config/c_get.zig | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/config/c_get.zig b/src/config/c_get.zig index 0305100a6..2a1ca95ab 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -39,17 +39,24 @@ pub fn get(config: *const Config, k: Key, ptr_raw: *anyopaque) bool { ptr.* = @floatCast(value); }, - Color => { - const ptr: *c_uint = @ptrCast(@alignCast(ptr_raw)); - ptr.* = @as(c_uint, @as(u24, @bitCast(value))); - }, - else => |T| switch (@typeInfo(T)) { .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, }, } From 220da88a9aa89c7dbbe10f5b3d97fc39619fac3e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 13 Dec 2023 19:06:25 -0800 Subject: [PATCH 8/8] config: make unfocused-split-fill default to bg --- macos/Sources/Ghostty/SurfaceView.swift | 5 +- src/config/Config.zig | 6 +- src/config/c_get.zig | 134 ++++++++++++++++-------- 3 files changed, 98 insertions(+), 47 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 29f0dcf73..7c72b9237 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -67,7 +67,10 @@ extension Ghostty { private var unfocusedFill: Color { var rgb: UInt32 = 16777215 // white default let key = "unfocused-split-fill" - _ = ghostty_config_get(ghostty.config, &rgb, key, UInt(key.count)) + 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) diff --git a/src/config/Config.zig b/src/config/Config.zig index 1f930ed74..0700bee6a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -312,13 +312,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. -@"unfocused-split-fill": Color = .{ .r = 255, .g = 255, .b = 255 }, +// +// 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 diff --git a/src/config/c_get.zig b/src/config/c_get.zig index 2a1ca95ab..2bc2ae10e 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -18,54 +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); - }, - - .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; + 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: { @@ -109,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); + } +}