diff --git a/src/config/Config.zig b/src/config/Config.zig index 92caa5744..88f3d5375 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2783,6 +2783,22 @@ keybind: Keybinds = .{}, /// the same time as the `iTime` uniform, allowing you to compute the /// time since the change by subtracting this from `iTime`. /// +/// * `float iTimeFocus` - Timestamp when the surface last gained iFocus. +/// +/// When the surface gains focus, this is set to the current value of +/// `iTime`, similar to how `iTimeCursorChange` works. This allows you +/// to compute the time since focus was gained or lost by calculating +/// `iTime - iTimeFocus`. Use this to create animations that restart +/// when the terminal regains focus. +/// +/// * `int iFocus` - Current focus state of the surface. +/// +/// Set to 1.0 when the surface is focused, 0.0 when unfocused. This +/// allows shaders to detect unfocused state and avoid animation artifacts +/// from large time deltas caused by infrequent "deceptive frames" +/// (e.g., modifier key presses, link hover events in unfocused split panes). +/// Check `iFocus > 0` to determine if the surface is currently focused. +/// /// If the shader fails to compile, the shader will be ignored. Any errors /// related to shader compilation will not show up as configuration errors /// and only show up in the log, since shader compilation happens after diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 4ebe501f7..5fa3432a6 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -116,6 +116,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// True if the window is focused focused: bool, + /// Flag to indicate that our focus state changed for custom + /// shaders to update their state. + custom_shader_focused_changed: bool = false, + /// The most recent scrollbar state. We use this as a cache to /// determine if we need to notify the apprt that there was a /// scrollbar change. @@ -746,6 +750,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .current_cursor_color = @splat(0), .previous_cursor_color = @splat(0), .cursor_change_time = 0, + .time_focus = 0, + .focus = 1, // assume focused initially }, .bg_image_buffer = undefined, @@ -1008,8 +1014,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// /// Must be called on the render thread. pub fn setFocus(self: *Self, focus: bool) !void { + assert(self.focused != focus); + self.focused = focus; + // Flag that we need to update our custom shaders + self.custom_shader_focused_changed = true; + // If we're not focused, then we want to stop the display link // because it is a waste of resources and we can move to pure // change-driven updates. @@ -2255,6 +2266,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // We only need to do this if we have custom shaders. if (!self.has_custom_shaders) return; + const uniforms = &self.custom_shader_uniforms; + const now = try std.time.Instant.now(); defer self.last_frame_time = now; const first_frame_time = self.first_frame_time orelse t: { @@ -2264,23 +2277,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const last_frame_time = self.last_frame_time orelse now; const since_ns: f32 = @floatFromInt(now.since(first_frame_time)); - self.custom_shader_uniforms.time = since_ns / std.time.ns_per_s; + uniforms.time = since_ns / std.time.ns_per_s; const delta_ns: f32 = @floatFromInt(now.since(last_frame_time)); - self.custom_shader_uniforms.time_delta = delta_ns / std.time.ns_per_s; + uniforms.time_delta = delta_ns / std.time.ns_per_s; - self.custom_shader_uniforms.frame += 1; + uniforms.frame += 1; const screen = self.size.screen; const padding = self.size.padding; const cell = self.size.cell; - self.custom_shader_uniforms.resolution = .{ + uniforms.resolution = .{ @floatFromInt(screen.width), @floatFromInt(screen.height), 1, }; - self.custom_shader_uniforms.channel_resolution[0] = .{ + uniforms.channel_resolution[0] = .{ @floatFromInt(screen.width), @floatFromInt(screen.height), 1, @@ -2345,8 +2358,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { @as(f32, @floatFromInt(cursor.color[3])) / 255.0, }; - const uniforms = &self.custom_shader_uniforms; - const cursor_changed: bool = !std.meta.eql(new_cursor, uniforms.current_cursor) or !std.meta.eql(cursor_color, uniforms.current_cursor_color); @@ -2359,6 +2370,19 @@ pub fn Renderer(comptime GraphicsAPI: type) type { uniforms.cursor_change_time = uniforms.time; } } + + // Update focus uniforms + uniforms.focus = @intFromBool(self.focused); + + // If we need to update the time our focus state changed + // then update it to our current frame time. This may not be + // exactly correct since it is frame time, not exact focus + // time, but focus time on its own isn't exactly correct anyways + // since it comes async from a message. + if (self.custom_shader_focused_changed and self.focused) { + uniforms.time_focus = uniforms.time; + self.custom_shader_focused_changed = false; + } } /// Convert the terminal state to GPU cells stored in CPU memory. These diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index 6d9cf0f68..c984334f4 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -16,6 +16,8 @@ layout(binding = 1, std140) uniform Globals { uniform vec4 iCurrentCursorColor; uniform vec4 iPreviousCursorColor; uniform float iTimeCursorChange; + uniform float iTimeFocus; + uniform int iFocus; }; layout(binding = 0) uniform sampler2D iChannel0; diff --git a/src/renderer/shaders/test_shadertoy_focus.glsl b/src/renderer/shaders/test_shadertoy_focus.glsl new file mode 100644 index 000000000..9fc2304df --- /dev/null +++ b/src/renderer/shaders/test_shadertoy_focus.glsl @@ -0,0 +1,41 @@ +// Test shader for iTimeFocus and iFocus +// Shows border when focused, green fade that restarts on each focus gain +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + + // Sample the terminal content + vec4 terminal = texture2D(iChannel0, uv); + vec3 color = terminal.rgb; + + if (iFocus > 0) { + // FOCUSED: Add border and fading green overlay + + // Calculate time since focus was gained + float timeSinceFocus = iTime - iTimeFocus; + + // Green fade: starts at 1.0 (full green), fades to 0.0 over 3 seconds + float fadeOut = max(0.0, 1.0 - (timeSinceFocus / 3.0)); + + // Add green overlay that fades out + color = mix(color, vec3(0.0, 1.0, 0.0), fadeOut * 0.4); + + // Add border (5 pixels) + float borderSize = 5.0; + vec2 pixelCoord = fragCoord; + bool isBorder = pixelCoord.x < borderSize || + pixelCoord.x > iResolution.x - borderSize || + pixelCoord.y < borderSize || + pixelCoord.y > iResolution.y - borderSize; + + if (isBorder) { + // Bright cyan border that pulses subtly + float pulse = sin(timeSinceFocus * 2.0) * 0.1 + 0.9; + color = vec3(0.0, 1.0, 1.0) * pulse; + } + } else { + // UNFOCUSED: Solid red overlay (no border) + color = mix(color, vec3(1.0, 0.0, 0.0), 0.3); + } + + fragColor = vec4(color, 1.0); +} diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 0d096c0fc..f71200610 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -25,6 +25,8 @@ pub const Uniforms = extern struct { current_cursor_color: [4]f32 align(16), previous_cursor_color: [4]f32 align(16), cursor_change_time: f32 align(4), + time_focus: f32 align(4), + focus: i32 align(4), }; /// The target to load shaders for. @@ -412,3 +414,4 @@ test "shadertoy to glsl" { const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl"); const test_invalid = @embedFile("shaders/test_shadertoy_invalid.glsl"); +const test_focus = @embedFile("shaders/test_shadertoy_focus.glsl");