diff --git a/include/ghostty.h b/include/ghostty.h index afc20bb3f5b..bcb8c1c12dc 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -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); diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index f9448cd0d8e..00eb587cbf9 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -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) { @@ -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: @@ -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) { diff --git a/src/Surface.zig b/src/Surface.zig index dfc3a50ea13..a6870519a94 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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(); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 519a35f2bd1..6b1dc299570 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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}); @@ -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(); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 179c779d7be..7e18bce6479 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -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; } @@ -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(); } diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 5087213797d..24a1645c6c6 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -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); diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 0f4a294bc71..015ee91b518 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -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, @@ -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, @@ -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, @@ -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]); diff --git a/src/renderer/message.zig b/src/renderer/message.zig index a47b9608051..361e04cb0fc 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -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, diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index 4b6d091b853..3fafd0c72d2 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -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 diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 556c282938c..b2823aa05ea 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -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.