Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,8 @@ GHOSTTY_API void ghostty_surface_set_content_scale(ghostty_surface_t, double, do
GHOSTTY_API void ghostty_surface_set_focus(ghostty_surface_t, bool);
GHOSTTY_API void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
GHOSTTY_API void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
GHOSTTY_API void ghostty_surface_set_position(ghostty_surface_t, uint32_t, uint32_t,
uint32_t, uint32_t);
GHOSTTY_API ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
GHOSTTY_API void ghostty_surface_set_color_scheme(ghostty_surface_t,
ghostty_color_scheme_e);
Expand Down
29 changes: 29 additions & 0 deletions macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,9 @@ extension Ghostty {
setSurfaceSize(width: UInt32(scaledSize.width), height: UInt32(scaledSize.height))
// Store this size so we can reuse it when backing properties change
contentSize = size

// Update position within window for custom shaders
updateSurfacePosition()
}

private func setSurfaceSize(width: UInt32, height: UInt32) {
Expand All @@ -502,6 +505,29 @@ extension Ghostty {
}
}

/// Update the position of this surface within its parent window.
/// This allows custom shaders to compute window-global coordinates
/// so effects like gradients or vignettes span across splits.
private func updateSurfacePosition() {
guard let surface = self.surface else { return }
guard let window = self.window else { return }

// Get the surface view's bounds in window coordinates.
// AppKit window coordinates have origin at bottom-left.
let boundsInWindow = self.convert(self.bounds, to: nil)
let windowContentSize = window.contentLayoutRect.size
let scale = window.backingScaleFactor

// Convert to backing pixels (Retina) and flip Y axis
// (AppKit uses bottom-left origin, shaders expect top-left)
let offsetX = UInt32(max(0, boundsInWindow.origin.x * scale))
let offsetY = UInt32(max(0, (windowContentSize.height - boundsInWindow.origin.y - boundsInWindow.size.height) * scale))
let windowWidth = UInt32(windowContentSize.width * scale)
let windowHeight = UInt32(windowContentSize.height * scale)

ghostty_surface_set_position(surface, offsetX, offsetY, windowWidth, windowHeight)
}

func setCursorShape(_ shape: ghostty_action_mouse_shape_e) {
switch shape {
case GHOSTTY_MOUSE_SHAPE_DEFAULT:
Expand Down Expand Up @@ -884,6 +910,9 @@ extension Ghostty {
// When our scale factor changes, so does our fb size so we send that too
let scaledSize = self.convertToBacking(contentSize)
setSurfaceSize(width: UInt32(scaledSize.width), height: UInt32(scaledSize.height))

// Scale change also affects surface position in backing pixels
updateSurfacePosition()
}

override func mouseDown(with event: NSEvent) {
Expand Down
24 changes: 24 additions & 0 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3277,6 +3277,30 @@ pub fn occlusionCallback(self: *Surface, visible: bool) !void {
try self.queueRender();
}

/// Called by the apprt when the surface position within the window changes.
/// This updates custom shader uniforms so effects can span the full window.
pub fn surfacePositionCallback(
self: *Surface,
offset_x: u32,
offset_y: u32,
window_width: u32,
window_height: u32,
) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;

_ = self.renderer_thread.mailbox.push(.{
.surface_position = .{
.offset_x = offset_x,
.offset_y = offset_y,
.window_width = window_width,
.window_height = window_height,
},
}, .{ .forever = {} });
try self.queueRender();
}

pub fn focusCallback(self: *Surface, focused: bool) !void {
// Crash metadata in case we crash in here
crash.sentry.thread_state = self.crashThreadState();
Expand Down
30 changes: 30 additions & 0 deletions src/apprt/embedded.zig
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,24 @@ pub const Surface = struct {
};
}

pub fn updateSurfacePosition(
self: *Surface,
offset_x: u32,
offset_y: u32,
window_width: u32,
window_height: u32,
) void {
self.core_surface.surfacePositionCallback(
offset_x,
offset_y,
window_width,
window_height,
) catch |err| {
log.err("error in surface position callback err={}", .{err});
return;
};
}

pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) void {
self.core_surface.colorSchemeCallback(scheme) catch |err| {
log.err("error setting color scheme err={}", .{err});
Expand Down Expand Up @@ -1696,6 +1714,18 @@ pub const CAPI = struct {
surface.updateSize(w, h);
}

/// Update the position of a surface within its parent window.
/// Used for custom shaders that need window-global coordinates.
export fn ghostty_surface_set_position(
surface: *Surface,
offset_x: u32,
offset_y: u32,
window_width: u32,
window_height: u32,
) void {
surface.updateSurfacePosition(offset_x, offset_y, window_width, window_height);
}

/// Return the size information a surface has.
export fn ghostty_surface_size(surface: *Surface) SurfaceSize {
const grid_size = surface.core_surface.size.grid();
Expand Down
30 changes: 30 additions & 0 deletions src/apprt/gtk/class/surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,32 @@ pub const Surface = extern struct {
return priv.size;
}

/// Notify the core surface of this widget's position within the window.
/// This enables custom shaders to compute window-global coordinates
/// so effects like gradients or vignettes span across splits.
fn updateSurfacePosition(self: *Self, surface: *CoreSurface) void {
const widget = self.private().gl_area.as(gtk.Widget);
const root = widget.getRoot() orelse return;
const root_widget = root.as(gtk.Widget);

// Compute the surface's bounds within the root window widget.
// computeBounds returns a graphene.Rect in the target's coordinate space.
var bounds: gtk.graphene.Rect = undefined;
if (!widget.computeBounds(root_widget, &bounds)) return;

// GTK4 coordinates are in CSS pixels (logical), multiply by
// scale factor to get physical pixels matching iResolution.
const scale: f64 = @floatFromInt(widget.getScaleFactor());
const offset_x: u32 = @intFromFloat(@max(0, @as(f64, bounds.origin.x) * scale));
const offset_y: u32 = @intFromFloat(@max(0, @as(f64, bounds.origin.y) * scale));
const window_w: u32 = @intFromFloat(@as(f64, @floatFromInt(root_widget.getAllocatedWidth())) * scale);
const window_h: u32 = @intFromFloat(@as(f64, @floatFromInt(root_widget.getAllocatedHeight())) * scale);

surface.surfacePositionCallback(offset_x, offset_y, window_w, window_h) catch |err| {
log.warn("error in surface position callback err={}", .{err});
};
}

pub fn getCursorPos(self: *Self) apprt.CursorPos {
return self.private().cursor_pos;
}
Expand Down Expand Up @@ -3315,6 +3341,10 @@ pub const Surface = extern struct {
surface.sizeCallback(new_size) catch |err| {
log.warn("error in size callback err={}", .{err});
};

// Update surface position within the window for custom shaders.
self.updateSurfacePosition(surface);

// Setup our resize overlay if configured
self.resizeOverlaySchedule();
}
Expand Down
7 changes: 7 additions & 0 deletions src/renderer/Thread.zig
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,13 @@ fn drainMailbox(self: *Thread) !void {

.resize => |v| self.renderer.setScreenSize(v),

.surface_position => |v| self.renderer.setSurfacePosition(
v.offset_x,
v.offset_y,
v.window_width,
v.window_height,
),

.change_config => |config| {
defer config.alloc.destroy(config.thread);
defer config.alloc.destroy(config.impl);
Expand Down
42 changes: 42 additions & 0 deletions src/renderer/generic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// The size of everything.
size: renderer.Size,

/// Position of this surface within the window, in pixels.
/// Set by the apprt when the surface is repositioned (e.g. splits).
surface_offset: [2]u32 = .{ 0, 0 },

/// Total window size in pixels. When 0, defaults to surface size.
window_size: [2]u32 = .{ 0, 0 },

/// True if the window is focused
focused: bool,

Expand Down Expand Up @@ -769,6 +776,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.cursor_text = @splat(0),
.selection_background_color = @splat(0),
.selection_foreground_color = @splat(0),
.surface_offset = .{ 0, 0, 0, 0 },
.window_resolution = .{ 0, 0, 1, 0 },
},
.bg_image_buffer = undefined,

Expand Down Expand Up @@ -1915,6 +1924,21 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
}

/// Update the position of this surface within its parent window.
/// This is used by custom shaders to compute window-global coordinates.
pub fn setSurfacePosition(
self: *Self,
offset_x: u32,
offset_y: u32,
window_width: u32,
window_height: u32,
) void {
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
self.surface_offset = .{ offset_x, offset_y };
self.window_size = .{ window_width, window_height };
}

/// Resize the screen.
pub fn setScreenSize(
self: *Self,
Expand Down Expand Up @@ -2137,6 +2161,24 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
0,
};

// Surface position within the window. Defaults to (0,0) offset
// and surface resolution as window resolution when the apprt
// does not provide position information.
uniforms.surface_offset = .{
@floatFromInt(self.surface_offset[0]),
@floatFromInt(self.surface_offset[1]),
0,
0,
};
const win_w = self.window_size[0];
const win_h = self.window_size[1];
uniforms.window_resolution = .{
if (win_w > 0) @floatFromInt(win_w) else @floatFromInt(screen.width),
if (win_h > 0) @floatFromInt(win_h) else @floatFromInt(screen.height),
1,
0,
};

if (self.cells.getCursorGlyph()) |cursor| {
const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/message.zig
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ pub const Message = union(enum) {
/// Changes the size. The screen size might change, padding, grid, etc.
resize: renderer.Size,

/// Updates the position of this surface within its parent window.
/// Used by custom shaders to compute window-global coordinates.
surface_position: struct {
offset_x: u32,
offset_y: u32,
window_width: u32,
window_height: u32,
},

/// The derived configuration to update the renderer with.
change_config: struct {
alloc: Allocator,
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/shaders/shadertoy_prefix.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ layout(binding = 1, std140) uniform Globals {
uniform vec3 iCursorText;
uniform vec3 iSelectionForegroundColor;
uniform vec3 iSelectionBackgroundColor;
uniform vec4 iSurfaceOffset;
uniform vec4 iWindowResolution;
};

#define CURSORSTYLE_BLOCK 0
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/shadertoy.zig
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub const Uniforms = extern struct {
cursor_text: [4]f32 align(16),
selection_background_color: [4]f32 align(16),
selection_foreground_color: [4]f32 align(16),
surface_offset: [4]f32 align(16),
window_resolution: [4]f32 align(16),
};

/// The target to load shaders for.
Expand Down