introduce split-preserve-zoom config to maintain zoomed splits during navigation (#9089)
Closes #8458. - Adds the split-preserve-zoom config option with a navigation flag. - GTK and macOS runtimes now respect the flag so zoomed splits stay zoomed when using split navigation. I've tested this on macOS (video below), but have not tested on GTK. This PR was written primarily with Codex CLI, using the [gh-issue](https://github.com/ghostty-org/ghostty/blob/main/.agents/commands/gh-issue) command. Here is a short video of the debug build: https://github.com/user-attachments/assets/3abea255-98e1-4a4f-9196-7c1b2663b9d2pull/9633/merge
commit
a1ffac3c58
|
|
@ -621,9 +621,14 @@ class BaseTerminalController: NSWindowController,
|
|||
return
|
||||
}
|
||||
|
||||
// Remove the zoomed state for this surface tree.
|
||||
if surfaceTree.zoomed != nil {
|
||||
surfaceTree = .init(root: surfaceTree.root, zoomed: nil)
|
||||
if derivedConfig.splitPreserveZoom.contains(.navigation) {
|
||||
surfaceTree = SplitTree(
|
||||
root: surfaceTree.root,
|
||||
zoomed: surfaceTree.root?.node(view: nextSurface))
|
||||
} else {
|
||||
surfaceTree = SplitTree(root: surfaceTree.root, zoomed: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Move focus to the next surface
|
||||
|
|
@ -1188,17 +1193,20 @@ class BaseTerminalController: NSWindowController,
|
|||
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
|
||||
let windowStepResize: Bool
|
||||
let focusFollowsMouse: Bool
|
||||
let splitPreserveZoom: Ghostty.Config.SplitPreserveZoom
|
||||
|
||||
init() {
|
||||
self.macosTitlebarProxyIcon = .visible
|
||||
self.windowStepResize = false
|
||||
self.focusFollowsMouse = false
|
||||
self.splitPreserveZoom = .init()
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon
|
||||
self.windowStepResize = config.windowStepResize
|
||||
self.focusFollowsMouse = config.focusFollowsMouse
|
||||
self.splitPreserveZoom = config.splitPreserveZoom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,14 @@ extension Ghostty {
|
|||
return .init(rawValue: v)
|
||||
}
|
||||
|
||||
var splitPreserveZoom: SplitPreserveZoom {
|
||||
guard let config = self.config else { return .init() }
|
||||
var v: CUnsignedInt = 0
|
||||
let key = "split-preserve-zoom"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .init() }
|
||||
return .init(rawValue: v)
|
||||
}
|
||||
|
||||
var initialWindow: Bool {
|
||||
guard let config = self.config else { return true }
|
||||
var v = true;
|
||||
|
|
@ -690,6 +698,12 @@ extension Ghostty.Config {
|
|||
static let border = BellFeatures(rawValue: 1 << 4)
|
||||
}
|
||||
|
||||
struct SplitPreserveZoom: OptionSet {
|
||||
let rawValue: CUnsignedInt
|
||||
|
||||
static let navigation = SplitPreserveZoom(rawValue: 1 << 0)
|
||||
}
|
||||
|
||||
enum MacDockDropBehavior: String {
|
||||
case new_tab = "new-tab"
|
||||
case new_window = "new-window"
|
||||
|
|
|
|||
|
|
@ -340,6 +340,35 @@ pub const SplitTree = extern struct {
|
|||
const surface = tree.nodes[target.idx()].leaf;
|
||||
surface.grabFocus();
|
||||
|
||||
// We also need to setup our last_focused to this because if we
|
||||
// trigger a tree change like below, the grab focus above never
|
||||
// actually triggers in time to set this and this ensures we
|
||||
// grab focus to the right thing.
|
||||
const old_last_focused = self.private().last_focused.get();
|
||||
defer if (old_last_focused) |v| v.unref(); // unref strong ref from get
|
||||
self.private().last_focused.set(surface);
|
||||
errdefer self.private().last_focused.set(old_last_focused);
|
||||
|
||||
if (tree.zoomed != null) {
|
||||
const app = Application.default();
|
||||
const config_obj = app.getConfig();
|
||||
defer config_obj.unref();
|
||||
const config = config_obj.get();
|
||||
|
||||
if (!config.@"split-preserve-zoom".navigation) {
|
||||
tree.zoomed = null;
|
||||
} else {
|
||||
tree.zoom(target);
|
||||
}
|
||||
|
||||
// When the zoom state changes our tree state changes and
|
||||
// we need to send the proper notifications to trigger
|
||||
// relayout.
|
||||
const object = self.as(gobject.Object);
|
||||
object.notifyByPspec(properties.tree.impl.param_spec);
|
||||
object.notifyByPspec(properties.@"is-zoomed".impl.param_spec);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -985,6 +985,14 @@ palette: Palette = .{},
|
|||
/// Available since: 1.1.0
|
||||
@"split-divider-color": ?Color = null,
|
||||
|
||||
/// Control when Ghostty preserves the zoomed state of a split. This is a packed
|
||||
/// struct so more options can be added in the future. The `navigation` option
|
||||
/// keeps the current split zoomed when split navigation (`goto_split`) changes
|
||||
/// the focused split.
|
||||
///
|
||||
/// Example: `split-preserve-zoom = navigation`
|
||||
@"split-preserve-zoom": SplitPreserveZoom = .{},
|
||||
|
||||
/// The foreground and background color for search matches. This only applies
|
||||
/// to non-focused search matches, also known as candidate matches.
|
||||
///
|
||||
|
|
@ -7423,6 +7431,10 @@ pub const ShellIntegrationFeatures = packed struct {
|
|||
path: bool = true,
|
||||
};
|
||||
|
||||
pub const SplitPreserveZoom = packed struct {
|
||||
navigation: bool = false,
|
||||
};
|
||||
|
||||
pub const RepeatableCommand = struct {
|
||||
value: std.ArrayListUnmanaged(inputpkg.Command) = .empty,
|
||||
|
||||
|
|
|
|||
|
|
@ -222,3 +222,19 @@ test "c_get: background-blur" {
|
|||
try testing.expectEqual(-2, cval);
|
||||
}
|
||||
}
|
||||
|
||||
test "c_get: split-preserve-zoom" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var c = try Config.default(alloc);
|
||||
defer c.deinit();
|
||||
|
||||
var bits: c_uint = undefined;
|
||||
try testing.expect(get(&c, .@"split-preserve-zoom", @ptrCast(&bits)));
|
||||
try testing.expectEqual(@as(c_uint, 0), bits);
|
||||
|
||||
c.@"split-preserve-zoom".navigation = true;
|
||||
try testing.expect(get(&c, .@"split-preserve-zoom", @ptrCast(&bits)));
|
||||
try testing.expectEqual(@as(c_uint, 1), bits);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue