mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #2077 from ghostty-org/resize-mac
macos: implement resize overlay
This commit is contained in:
@ -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,
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,6 +145,17 @@ 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 let surfaceSize = surfaceView.surfaceSize {
|
||||||
|
SurfaceResizeOverlay(
|
||||||
|
geoSize: geo.size,
|
||||||
|
size: surfaceSize,
|
||||||
|
overlay: ghostty.config.resizeOverlay,
|
||||||
|
position: ghostty.config.resizeOverlayPosition,
|
||||||
|
duration: ghostty.config.resizeOverlayDuration)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.ghosttySurfaceView(surfaceView)
|
.ghosttySurfaceView(surfaceView)
|
||||||
|
|
||||||
@ -255,6 +266,77 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is the resize overlay that shows on top of a surface to show the current
|
||||||
|
// size during a resize operation.
|
||||||
|
struct SurfaceResizeOverlay: View {
|
||||||
|
let geoSize: CGSize
|
||||||
|
let size: ghostty_surface_size_s
|
||||||
|
let overlay: Ghostty.Config.ResizeOverlay
|
||||||
|
let position: Ghostty.Config.ResizeOverlayPosition
|
||||||
|
let duration: UInt
|
||||||
|
|
||||||
|
// This is the last size that we processed. This is how we handle our
|
||||||
|
// timer state.
|
||||||
|
@State var lastSize: CGSize? = nil
|
||||||
|
|
||||||
|
// Fixed value set based on personal taste.
|
||||||
|
private let padding: CGFloat = 5
|
||||||
|
|
||||||
|
// This computed boolean is set to true when the overlay should be hidden.
|
||||||
|
private var hidden: Bool {
|
||||||
|
// Hidden if we already processed this size.
|
||||||
|
if (lastSize == geoSize) { return true; }
|
||||||
|
|
||||||
|
// Hidden depending on overlay config
|
||||||
|
switch (overlay) {
|
||||||
|
case .never: return true;
|
||||||
|
case .always: return false;
|
||||||
|
case .after_first: return lastSize == nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
if (!position.top()) {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
if (!position.left()) {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(verbatim: "\(size.columns)c ⨯ \(size.rows)r")
|
||||||
|
.padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding))
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.fill(.background)
|
||||||
|
.shadow(radius: 3)
|
||||||
|
)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
|
||||||
|
if (!position.right()) {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!position.bottom()) {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.opacity(hidden ? 0 : 1)
|
||||||
|
.task(id: geoSize) {
|
||||||
|
// By ID-ing the task on the geoSize, we get the task to restart if our
|
||||||
|
// geoSize changes. This also ensures that future resize overlays are shown
|
||||||
|
// properly.
|
||||||
|
try? await Task.sleep(nanoseconds: UInt64(duration) * 1_000_000)
|
||||||
|
lastSize = geoSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn
|
/// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn
|
||||||
/// and interacted with. The word "surface" is used because a surface may represent a window, a tab,
|
/// and interacted with. The word "surface" is used because a surface may represent a window, a tab,
|
||||||
/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it.
|
/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it.
|
||||||
|
@ -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? {
|
||||||
|
@ -28,6 +28,13 @@ extension Ghostty {
|
|||||||
// The hovered URL
|
// The hovered URL
|
||||||
@Published var hoverUrl: String? = nil
|
@Published var hoverUrl: String? = nil
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
private(set) var surface: ghostty_surface_t?
|
private(set) var surface: ghostty_surface_t?
|
||||||
|
|
||||||
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -43,8 +43,14 @@ pub const ScreenSize = struct {
|
|||||||
const grid_height = grid.rows * cell.height;
|
const grid_height = grid.rows * cell.height;
|
||||||
const padded_width = grid_width + (padding.left + padding.right);
|
const padded_width = grid_width + (padding.left + padding.right);
|
||||||
const padded_height = grid_height + (padding.top + padding.bottom);
|
const padded_height = grid_height + (padding.top + padding.bottom);
|
||||||
const leftover_width = self.width - padded_width;
|
|
||||||
const leftover_height = self.height - padded_height;
|
// Note these have to use a saturating subtraction to avoid underflow
|
||||||
|
// because our padding can cause the padded sizes to be larger than
|
||||||
|
// our real screen if the screen is shrunk to a minimal size such
|
||||||
|
// as 1x1.
|
||||||
|
const leftover_width = self.width -| padded_width;
|
||||||
|
const leftover_height = self.height -| padded_height;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.top = 0,
|
.top = 0,
|
||||||
.bottom = leftover_height,
|
.bottom = leftover_height,
|
||||||
|
Reference in New Issue
Block a user