macos: implement resize overlay

Implements the resize overlay configurations completely.
This commit is contained in:
Mitchell Hashimoto
2024-08-10 20:14:21 -07:00
parent 451cf69398
commit 9cf247bb3e
7 changed files with 185 additions and 13 deletions

View File

@ -404,6 +404,15 @@ typedef struct {
const char* command; const char* command;
} ghostty_surface_config_s; } ghostty_surface_config_s;
typedef struct {
uint16_t columns;
uint16_t rows;
uint32_t width_px;
uint32_t height_px;
uint32_t cell_width_px;
uint32_t cell_height_px;
} ghostty_surface_size_s;
typedef void (*ghostty_runtime_wakeup_cb)(void*); typedef void (*ghostty_runtime_wakeup_cb)(void*);
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void*); typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void*);
typedef void (*ghostty_runtime_open_config_cb)(void*); typedef void (*ghostty_runtime_open_config_cb)(void*);
@ -530,6 +539,7 @@ void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
void ghostty_surface_set_focus(ghostty_surface_t, bool); void ghostty_surface_set_focus(ghostty_surface_t, bool);
void ghostty_surface_set_occlusion(ghostty_surface_t, bool); void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
void ghostty_surface_set_color_scheme(ghostty_surface_t, void ghostty_surface_set_color_scheme(ghostty_surface_t,
ghostty_color_scheme_e); ghostty_color_scheme_e);
ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,

View File

@ -331,5 +331,82 @@ extension Ghostty {
let newColor = isLightBackground ? backgroundColor.darken(by: 0.08) : backgroundColor.darken(by: 0.4) let newColor = isLightBackground ? backgroundColor.darken(by: 0.08) : backgroundColor.darken(by: 0.4)
return Color(newColor) return Color(newColor)
} }
var resizeOverlay: ResizeOverlay {
guard let config = self.config else { return .after_first }
var v: UnsafePointer<Int8>? = nil
let key = "resize-overlay"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .after_first }
guard let ptr = v else { return .after_first }
let str = String(cString: ptr)
return ResizeOverlay(rawValue: str) ?? .after_first
}
var resizeOverlayPosition: ResizeOverlayPosition {
let defaultValue = ResizeOverlayPosition.center
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "resize-overlay-position"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return ResizeOverlayPosition(rawValue: str) ?? defaultValue
}
var resizeOverlayDuration: UInt {
guard let config = self.config else { return 1000 }
var v: UInt = 0
let key = "resize-overlay-duration"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v;
}
}
}
// MARK: Configuration Enums
extension Ghostty.Config {
enum ResizeOverlay : String {
case always
case never
case after_first = "after-first"
}
enum ResizeOverlayPosition : String {
case center
case top_left = "top-left"
case top_center = "top-center"
case top_right = "top-right"
case bottom_left = "bottom-left"
case bottom_center = "bottom-center"
case bottom_right = "bottom-right"
func top() -> Bool {
switch (self) {
case .top_left, .top_center, .top_right: return true;
default: return false;
}
}
func bottom() -> Bool {
switch (self) {
case .bottom_left, .bottom_center, .bottom_right: return true;
default: return false;
}
}
func left() -> Bool {
switch (self) {
case .top_left, .bottom_left: return true;
default: return false;
}
}
func right() -> Bool {
switch (self) {
case .top_right, .bottom_right: return true;
default: return false;
}
}
} }
} }

View File

@ -52,6 +52,9 @@ extension Ghostty {
// True if we're hovering over the left URL view, so we can show it on the right. // True if we're hovering over the left URL view, so we can show it on the right.
@State private var isHoveringURLLeft: Bool = false @State private var isHoveringURLLeft: Bool = false
// The last size so we can detect resizes and show our resize overlay
@State private var lastSize: CGSize? = nil
@EnvironmentObject private var ghostty: Ghostty.App @EnvironmentObject private var ghostty: Ghostty.App
var body: some View { var body: some View {
@ -145,6 +148,54 @@ extension Ghostty {
// I don't know how older macOS versions behave but Ghostty only // I don't know how older macOS versions behave but Ghostty only
// supports back to macOS 12 so its moot. // supports back to macOS 12 so its moot.
} }
// If our geo size changed then we show the resize overlay as configured.
if (lastSize != geo.size) {
let resizeOverlay = ghostty.config.resizeOverlay
if (resizeOverlay != .never) {
if let surfaceSize = surfaceView.surfaceSize {
let padding: CGFloat = 5
let resizeDuration = ghostty.config.resizeOverlayDuration
let resizePosition = ghostty.config.resizeOverlayPosition
VStack {
if (!resizePosition.top()) {
Spacer()
}
HStack {
if (!resizePosition.left()) {
Spacer()
}
Text(verbatim: "\(surfaceSize.columns)c \(surfaceSize.rows)r")
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
.background(
RoundedRectangle(cornerRadius: 4)
.fill(.background)
.shadow(radius: 3)
).lineLimit(1)
.truncationMode(.middle)
if (!resizePosition.right()) {
Spacer()
}
}
if (!resizePosition.bottom()) {
Spacer()
}
}
.allowsHitTesting(false)
.opacity(resizeOverlay == .after_first && lastSize == nil ? 0 : 1)
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(resizeDuration))) {
self.lastSize = geo.size
}
}
}
}
}
} }
.ghosttySurfaceView(surfaceView) .ghosttySurfaceView(surfaceView)

View File

@ -52,6 +52,13 @@ extension Ghostty {
return v return v
} }
// Returns sizing information for the surface. This is the raw C
// structure because I'm lazy.
var surfaceSize: ghostty_surface_size_s? {
guard let surface = self.surface else { return nil }
return ghostty_surface_size(surface)
}
// Returns the inspector instance for this surface, or nil if the // Returns the inspector instance for this surface, or nil if the
// surface has been closed. // surface has been closed.
var inspector: ghostty_inspector_t? { var inspector: ghostty_inspector_t? {

View File

@ -1418,6 +1418,15 @@ pub const CAPI = struct {
offset_len: u32, offset_len: u32,
}; };
const SurfaceSize = extern struct {
columns: u16,
rows: u16,
width_px: u32,
height_px: u32,
cell_width_px: u32,
cell_height_px: u32,
};
/// Create a new app. /// Create a new app.
export fn ghostty_app_new( export fn ghostty_app_new(
opts: *const apprt.runtime.App.Options, opts: *const apprt.runtime.App.Options,
@ -1593,6 +1602,18 @@ pub const CAPI = struct {
surface.updateSize(w, h); surface.updateSize(w, h);
} }
/// Return the size information a surface has.
export fn ghostty_surface_size(surface: *Surface) SurfaceSize {
return .{
.columns = surface.core_surface.grid_size.columns,
.rows = surface.core_surface.grid_size.rows,
.width_px = surface.core_surface.screen_size.width,
.height_px = surface.core_surface.screen_size.height,
.cell_width_px = surface.core_surface.cell_size.width,
.cell_height_px = surface.core_surface.cell_size.height,
};
}
/// Update the color scheme of the surface. /// Update the color scheme of the surface.
export fn ghostty_surface_set_color_scheme(surface: *Surface, scheme_raw: c_int) void { export fn ghostty_surface_set_color_scheme(surface: *Surface, scheme_raw: c_int) void {
const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch { const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch {

View File

@ -871,10 +871,9 @@ keybind: Keybinds = .{},
/// ///
/// The default is `after-first`. /// The default is `after-first`.
/// ///
/// Changing this value at runtime and reloading the configuration will only /// Changing this value at runtime and reloading the configuration will take
/// affect new windows, tabs, and splits. /// effect immediately on macOS, but will only affect new terminals on
/// /// Linux.
/// Linux/GTK only.
@"resize-overlay": ResizeOverlay = .@"after-first", @"resize-overlay": ResizeOverlay = .@"after-first",
/// If resize overlays are enabled, this controls the position of the overlay. /// If resize overlays are enabled, this controls the position of the overlay.
@ -889,8 +888,6 @@ keybind: Keybinds = .{},
/// * `bottom-right` /// * `bottom-right`
/// ///
/// The default is `center`. /// The default is `center`.
///
/// Linux/GTK only.
@"resize-overlay-position": ResizeOverlayPosition = .center, @"resize-overlay-position": ResizeOverlayPosition = .center,
/// If resize overlays are enabled, this controls how long the overlay is /// If resize overlays are enabled, this controls how long the overlay is
@ -923,8 +920,6 @@ keybind: Keybinds = .{},
/// ///
/// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any /// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any
/// value larger than this will be clamped to the maximum value. /// value larger than this will be clamped to the maximum value.
///
/// Linux/GTK only.
@"resize-overlay-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms }, @"resize-overlay-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms },
// If true, when there are multiple split panes, the mouse selects the pane // If true, when there are multiple split panes, the mouse selects the pane
@ -4027,11 +4022,11 @@ pub const Duration = struct {
.{ .name = "ns", .factor = 1 }, .{ .name = "ns", .factor = 1 },
}; };
pub fn clone(self: *const @This(), _: Allocator) !@This() { pub fn clone(self: *const Duration, _: Allocator) !Duration {
return .{ .duration = self.duration }; return .{ .duration = self.duration };
} }
pub fn equal(self: @This(), other: @This()) bool { pub fn equal(self: Duration, other: Duration) bool {
return self.duration == other.duration; return self.duration == other.duration;
} }
@ -4099,7 +4094,7 @@ pub const Duration = struct {
return if (value) |v| .{ .duration = v } else error.ValueRequired; return if (value) |v| .{ .duration = v } else error.ValueRequired;
} }
pub fn formatEntry(self: @This(), formatter: anytype) !void { pub fn formatEntry(self: Duration, formatter: anytype) !void {
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf); var fbs = std.io.fixedBufferStream(&buf);
const writer = fbs.writer(); const writer = fbs.writer();
@ -4107,7 +4102,7 @@ pub const Duration = struct {
try formatter.formatEntry([]const u8, fbs.getWritten()); try formatter.formatEntry([]const u8, fbs.getWritten());
} }
pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { pub fn format(self: Duration, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
var value = self.duration; var value = self.duration;
var i: usize = 0; var i: usize = 0;
for (units) |unit| { for (units) |unit| {
@ -4122,9 +4117,14 @@ pub const Duration = struct {
} }
} }
pub fn c_get(self: Duration, ptr_raw: *anyopaque) void {
const ptr: *usize = @ptrCast(@alignCast(ptr_raw));
ptr.* = @intCast(self.asMilliseconds());
}
/// Convenience function to convert to milliseconds since many OS and /// Convenience function to convert to milliseconds since many OS and
/// library timing functions operate on that timescale. /// library timing functions operate on that timescale.
pub fn asMilliseconds(self: @This()) c_uint { pub fn asMilliseconds(self: Duration) c_uint {
const ms: u64 = std.math.divTrunc( const ms: u64 = std.math.divTrunc(
u64, u64,
self.duration, self.duration,

View File

@ -60,6 +60,12 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool {
}, },
.Struct => |info| { .Struct => |info| {
// If the struct implements c_get then we call that
if (@hasDecl(@TypeOf(value), "c_get")) {
value.c_get(ptr_raw);
return true;
}
// Packed structs that are less than or equal to the // Packed structs that are less than or equal to the
// size of a C int can be passed directly as their // size of a C int can be passed directly as their
// bit representation. // bit representation.