From 92a268ad6b437353f04b5b850d30889cf051bb83 Mon Sep 17 00:00:00 2001 From: yunusey Date: Tue, 31 Dec 2024 16:50:47 -0500 Subject: [PATCH 01/11] Add background image support for OpenGL --- src/config/Config.zig | 19 ++ src/renderer/OpenGL.zig | 172 +++++++++++++++++- .../opengl/BackgroundImageProgram.zig | 121 ++++++++++++ src/renderer/shaders/bgimage.f.glsl | 13 ++ src/renderer/shaders/bgimage.v.glsl | 40 ++++ src/terminal/kitty/graphics.zig | 1 + 6 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 src/renderer/opengl/BackgroundImageProgram.zig create mode 100644 src/renderer/shaders/bgimage.f.glsl create mode 100644 src/renderer/shaders/bgimage.v.glsl diff --git a/src/config/Config.zig b/src/config/Config.zig index 4aba8ce322..2aa45d8604 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -20,6 +20,7 @@ const global_state = &@import("../global.zig").state; const fontpkg = @import("../font/main.zig"); const inputpkg = @import("../input.zig"); const terminal = @import("../terminal/main.zig"); +const BackgroundImageProgram = @import("../renderer/opengl/BackgroundImageProgram.zig"); const internal_os = @import("../os/main.zig"); const cli = @import("../cli.zig"); const Command = @import("../Command.zig"); @@ -450,6 +451,24 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, +/// Background image for the window. +@"background-image": RepeatablePath = .{}, + +/// Background image opactity +@"background-image-opacity": f32 = 0.0, + +/// Background image mode to use. +/// +/// `aspect` keeps the aspect-ratio of the background image and `scaled` scales +/// the image to fit the window. `aspect` is the default mode. +/// +/// Valid values are: +/// +/// * `aspect` +/// * `scaled` +/// +@"background-image-mode": BackgroundImageProgram.BackgroundMode = .aspect, + /// The foreground and background color for selection. If this is not set, then /// the selection color is just the inverted window background and foreground /// (note: not to be confused with the cell bg/fg). diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index e5dec6b2bf..d633301e16 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -24,6 +24,7 @@ const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); const CellProgram = @import("opengl/CellProgram.zig"); +const BackgroundImageProgram = @import("opengl/BackgroundImageProgram.zig"); const ImageProgram = @import("opengl/ImageProgram.zig"); const gl_image = @import("opengl/image.zig"); const custom = @import("opengl/custom.zig"); @@ -135,6 +136,19 @@ draw_mutex: DrawMutex = drawMutexZero, /// terminal is in reversed mode. draw_background: terminal.color.RGB, +/// The background image(s) to draw. Currentlly, we always draw the last image. +background_image: configpkg.RepeatablePath, + +/// The opacity of the background image. Not to be confused with background-opacity +background_image_opacity: f32, + +/// The background image mode to use. +background_image_mode: BackgroundImageProgram.BackgroundMode, + +/// The current background image to draw. If it is null, then we will not +/// draw any background image. +current_background_image: ?Image = null, + /// Whether we're doing padding extension for vertical sides. padding_extend_top: bool = true, padding_extend_bottom: bool = true, @@ -183,7 +197,7 @@ const SetScreenSize = struct { ); // Update the projection uniform within our shader - inline for (.{ "cell_program", "image_program" }) |name| { + inline for (.{ "cell_program", "image_program", "bgimage_program" }) |name| { const program = @field(gl_state, name); const bind = try program.program.use(); defer bind.unbind(); @@ -281,6 +295,9 @@ pub const DerivedConfig = struct { cursor_opacity: f64, background: terminal.color.RGB, background_opacity: f64, + background_image: configpkg.RepeatablePath, + background_image_opacity: f32, + background_image_mode: BackgroundImageProgram.BackgroundMode, foreground: terminal.color.RGB, selection_background: ?terminal.color.RGB, selection_foreground: ?terminal.color.RGB, @@ -302,6 +319,9 @@ pub const DerivedConfig = struct { // Copy our shaders const custom_shaders = try config.@"custom-shader".clone(alloc); + // Copy our background image + const background_image = try config.@"background-image".clone(alloc); + // Copy our font features const font_features = try config.@"font-feature".clone(alloc); @@ -342,6 +362,11 @@ pub const DerivedConfig = struct { .background = config.background.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(), + + .background_image = background_image, + .background_image_opacity = config.@"background-image-opacity", + .background_image_mode = config.@"background-image-mode", + .invert_selection_fg_bg = config.@"selection-invert-fg-bg", .bold_is_bright = config.@"bold-is-bright", .min_contrast = @floatCast(config.@"minimum-contrast"), @@ -406,6 +431,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { .default_background_color = options.config.background, .cursor_color = null, .default_cursor_color = options.config.cursor_color, + .background_image = options.config.background_image, + .background_image_opacity = options.config.background_image_opacity, + .background_image_mode = options.config.background_image_mode, .cursor_invert = options.config.cursor_invert, .surface_mailbox = options.surface_mailbox, .deferred_font_size = .{ .metrics = grid.metrics }, @@ -810,6 +838,14 @@ pub fn updateFrame( try self.prepKittyGraphics(state.terminal); } + if (self.current_background_image == null and + self.background_image.value.items.len > 0) + { + if (single_threaded_draw) self.draw_mutex.lock(); + defer if (single_threaded_draw) self.draw_mutex.unlock(); + try self.prepBackgroundImage(); + } + // If we have any terminal dirty flags set then we need to rebuild // the entire screen. This can be optimized in the future. const full_rebuild: bool = rebuild: { @@ -1201,6 +1237,57 @@ fn prepKittyImage( gop.value_ptr.transmit_time = image.transmit_time; } +/// Prepares the current background image for upload +pub fn prepBackgroundImage(self: *OpenGL) !void { + // If the user doesn't have a background image, do nothing... + const last_image = self.background_image.value.getLastOrNull() orelse return; + + // Get the last background image + const path = switch (last_image) { + .optional, .required => |path| path, + }; + const command = terminal.kitty.graphics.Command{ + .control = .{ + .transmit = .{ + .format = .png, + .medium = .file, + .width = 0, + .height = 0, + .compression = .none, + .image_id = 0, + }, + }, + .data = try self.alloc.dupe(u8, path), + }; + defer command.deinit(self.alloc); + + // Load the iamge + var loading = try terminal.kitty.graphics.LoadingImage.init(self.alloc, &command); + defer loading.deinit(self.alloc); + + // Complete the image to get the final data + var image = try loading.complete(self.alloc); + defer image.deinit(self.alloc); + + // Copy the data into the pending state. + const data = try self.alloc.dupe(u8, image.data); + errdefer self.alloc.free(data); + + const pending: Image.Pending = .{ + .width = image.width, + .height = image.height, + .data = data.ptr, + }; + + self.current_background_image = switch (image.format) { + .gray => .{ .pending_gray = pending }, + .gray_alpha => .{ .pending_gray_alpha = pending }, + .rgb => .{ .pending_rgb = pending }, + .rgba => .{ .pending_rgba = pending }, + .png => unreachable, // should be decoded by now + }; +} + /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a /// slow operation but ensures that the GPU state exactly matches the CPU state. /// In steady-state operation, we use some GPU tricks to send down stale data @@ -2174,6 +2261,14 @@ pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null; self.cursor_invert = config.cursor_invert; + // Reset current background image + self.background_image = config.background_image; + self.background_image_opacity = config.background_image_opacity; + self.background_image_mode = config.background_image_mode; + if (self.current_background_image) |*img| { + img.markForUnload(); + } + // Update our uniforms self.deferred_config = .{}; @@ -2318,6 +2413,31 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { } } + // Check if we need to update our current background image + if (self.current_background_image != null) { + switch (self.current_background_image.?) { + .ready => {}, + + .pending_gray, + .pending_gray_alpha, + .pending_rgb, + .pending_rgba, + .replace_gray, + .replace_gray_alpha, + .replace_rgb, + .replace_rgba, + => try self.current_background_image.?.upload(self.alloc), + + .unload_pending, + .unload_replace, + .unload_ready, + => { + self.current_background_image.?.deinit(self.alloc); + self.current_background_image = null; + }, + } + } + // In the "OpenGL Programming Guide for Mac" it explains that: "When you // use an NSOpenGLView object with OpenGL calls that are issued from a // thread other than the main one, you must set up mutex locking." @@ -2427,6 +2547,9 @@ fn drawCellProgram( ); } + // Draw our background image if defined + try self.drawBackgroundImage(gl_state); + // Draw background images first try self.drawImages( gl_state, @@ -2452,6 +2575,46 @@ fn drawCellProgram( ); } +fn drawBackgroundImage( + self: *OpenGL, + gl_state: *const GLState, +) !void { + // If we don't have a background image, just return + if (self.current_background_image == null) { + return; + } + // Bind our background image program + const bind = try gl_state.bgimage_program.bind(); + defer bind.unbind(); + + // Get the texture + const texture = switch (self.current_background_image.?) { + .ready => |t| t, + else => { + return; + }, + }; + + // Bind the texture + try gl.Texture.active(gl.c.GL_TEXTURE0); + var texbind = try texture.bind(.@"2D"); + defer texbind.unbind(); + + try bind.vbo.setData(BackgroundImageProgram.Input{ + .terminal_width = self.size.terminal().width, + .terminal_height = self.size.terminal().height, + .mode = self.background_image_mode, + }, .static_draw); + try gl_state.bgimage_program.program.setUniform("opacity", self.config.background_image_opacity); + + try gl.drawElementsInstanced( + gl.c.GL_TRIANGLES, + 6, + gl.c.GL_UNSIGNED_BYTE, + 1, + ); +} + /// Runs the image program to draw images. fn drawImages( self: *OpenGL, @@ -2577,6 +2740,7 @@ fn drawCells( /// easy to create/destroy these as a set in situations i.e. where the /// OpenGL context is replaced. const GLState = struct { + bgimage_program: BackgroundImageProgram, cell_program: CellProgram, image_program: ImageProgram, texture: gl.Texture, @@ -2662,6 +2826,10 @@ const GLState = struct { ); } + // Build our background image renderer + const bgimage_program = try BackgroundImageProgram.init(); + errdefer bgimage_program.deinit(); + // Build our cell renderer const cell_program = try CellProgram.init(); errdefer cell_program.deinit(); @@ -2671,6 +2839,7 @@ const GLState = struct { errdefer image_program.deinit(); return .{ + .bgimage_program = bgimage_program, .cell_program = cell_program, .image_program = image_program, .texture = tex, @@ -2683,6 +2852,7 @@ const GLState = struct { if (self.custom) |v| v.deinit(alloc); self.texture.destroy(); self.texture_color.destroy(); + self.bgimage_program.deinit(); self.image_program.deinit(); self.cell_program.deinit(); } diff --git a/src/renderer/opengl/BackgroundImageProgram.zig b/src/renderer/opengl/BackgroundImageProgram.zig new file mode 100644 index 0000000000..ec3bf9febf --- /dev/null +++ b/src/renderer/opengl/BackgroundImageProgram.zig @@ -0,0 +1,121 @@ +/// The OpenGL program for rendering terminal cells. +const BackgroundImageProgram = @This(); + +const std = @import("std"); +const gl = @import("opengl"); + +pub const Input = extern struct { + /// vec2 terminal_size + terminal_width: u32 = 0, + terminal_height: u32 = 0, + + /// uint mode + mode: BackgroundMode = .aspect, +}; + +pub const BackgroundMode = enum(u8) { + aspect = 0, + scaled = 1, + _, +}; + +program: gl.Program, +vao: gl.VertexArray, +ebo: gl.Buffer, +vbo: gl.Buffer, + +pub fn init() !BackgroundImageProgram { + // Load and compile our shaders. + const program = try gl.Program.createVF( + @embedFile("../shaders/bgimage.v.glsl"), + @embedFile("../shaders/bgimage.f.glsl"), + ); + errdefer program.destroy(); + + // Set our program uniforms + const pbind = try program.use(); + defer pbind.unbind(); + + // Set all of our texture indexes + try program.setUniform("image", 0); + + // Setup our VAO + const vao = try gl.VertexArray.create(); + errdefer vao.destroy(); + const vaobind = try vao.bind(); + defer vaobind.unbind(); + + // Element buffer (EBO) + const ebo = try gl.Buffer.create(); + errdefer ebo.destroy(); + var ebobind = try ebo.bind(.element_array); + defer ebobind.unbind(); + try ebobind.setData([6]u8{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }, .static_draw); + + // Vertex buffer (VBO) + const vbo = try gl.Buffer.create(); + errdefer vbo.destroy(); + var vbobind = try vbo.bind(.array); + defer vbobind.unbind(); + var offset: usize = 0; + try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset); + offset += 2 * @sizeOf(u32); + try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Input), offset); + offset += 1 * @sizeOf(u8); + try vbobind.enableAttribArray(0); + try vbobind.enableAttribArray(1); + try vbobind.attributeDivisor(0, 1); + try vbobind.attributeDivisor(1, 1); + + return .{ + .program = program, + .vao = vao, + .ebo = ebo, + .vbo = vbo, + }; +} + +pub fn bind(self: BackgroundImageProgram) !Binding { + const program = try self.program.use(); + errdefer program.unbind(); + + const vao = try self.vao.bind(); + errdefer vao.unbind(); + + const ebo = try self.ebo.bind(.element_array); + errdefer ebo.unbind(); + + const vbo = try self.vbo.bind(.array); + errdefer vbo.unbind(); + + return .{ + .program = program, + .vao = vao, + .ebo = ebo, + .vbo = vbo, + }; +} + +pub fn deinit(self: BackgroundImageProgram) void { + self.ebo.destroy(); + self.vao.destroy(); + self.vbo.destroy(); + self.program.destroy(); +} + +pub const Binding = struct { + program: gl.Program.Binding, + vao: gl.VertexArray.Binding, + ebo: gl.Buffer.Binding, + vbo: gl.Buffer.Binding, + + pub fn unbind(self: Binding) void { + self.ebo.unbind(); + self.vao.unbind(); + self.vbo.unbind(); + self.program.unbind(); + } +}; diff --git a/src/renderer/shaders/bgimage.f.glsl b/src/renderer/shaders/bgimage.f.glsl new file mode 100644 index 0000000000..816882c7c3 --- /dev/null +++ b/src/renderer/shaders/bgimage.f.glsl @@ -0,0 +1,13 @@ +#version 330 core + +in vec2 tex_coord; + +layout(location = 0) out vec4 out_FragColor; + +uniform sampler2D image; +uniform float opacity; + +void main() { + vec4 color = texture(image, tex_coord); + out_FragColor = vec4(color.rgb * color.a * opacity, color.a * opacity); +} diff --git a/src/renderer/shaders/bgimage.v.glsl b/src/renderer/shaders/bgimage.v.glsl new file mode 100644 index 0000000000..dc4bdeec7d --- /dev/null +++ b/src/renderer/shaders/bgimage.v.glsl @@ -0,0 +1,40 @@ +#version 330 core + +const uint MODE_ASPECT = 0u; +const uint MODE_SCALED = 1u; + +layout (location = 0) in vec2 terminal_size; +layout (location = 1) in uint mode; + +out vec2 tex_coord; +uniform sampler2D image; +uniform mat4 projection; + +void main() { + vec2 position; + position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.; + position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.; + + vec2 image_size = textureSize(image, 0); + vec2 scale = vec2(1.0, 1.0); + switch (mode) { + case MODE_ASPECT: + vec2 aspect_ratio = vec2( + terminal_size.x / terminal_size.y, + image_size.x / image_size.y + ); + if (aspect_ratio.x > aspect_ratio.y) { + scale.x = aspect_ratio.y / aspect_ratio.x; + } + else { + scale.y = aspect_ratio.x / aspect_ratio.y; + } + case MODE_SCALED: + break; + } + + vec2 image_pos = terminal_size * position * scale; + vec2 offset = (terminal_size * (1.0 - scale)) / 2.0; + gl_Position = projection * vec4(image_pos.xy + offset, 0.0, 1.0); + tex_coord = position; +} diff --git a/src/terminal/kitty/graphics.zig b/src/terminal/kitty/graphics.zig index c710f81a14..ee2ef78040 100644 --- a/src/terminal/kitty/graphics.zig +++ b/src/terminal/kitty/graphics.zig @@ -24,6 +24,7 @@ const storage = @import("graphics_storage.zig"); pub const unicode = @import("graphics_unicode.zig"); pub const Command = command.Command; pub const CommandParser = command.Parser; +pub const LoadingImage = image.LoadingImage; pub const Image = image.Image; pub const ImageStorage = storage.ImageStorage; pub const RenderPlacement = render.Placement; From 5c2b97949635797f3fd2a719ad26b21938af1275 Mon Sep 17 00:00:00 2001 From: yunusey Date: Wed, 1 Jan 2025 13:55:52 -0500 Subject: [PATCH 02/11] Address review comments --- src/renderer/OpenGL.zig | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index d633301e16..a79792fbce 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -843,7 +843,13 @@ pub fn updateFrame( { if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); - try self.prepBackgroundImage(); + self.prepBackgroundImage() catch |err| switch (err) { + error.InvalidData => { + log.warn("invalid image data", .{}); + self.current_background_image = null; + }, + else => return err, + }; } // If we have any terminal dirty flags set then we need to rebuild @@ -2414,8 +2420,8 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { } // Check if we need to update our current background image - if (self.current_background_image != null) { - switch (self.current_background_image.?) { + if (self.current_background_image) |current_background_image| { + switch (current_background_image) { .ready => {}, .pending_gray, @@ -2580,15 +2586,14 @@ fn drawBackgroundImage( gl_state: *const GLState, ) !void { // If we don't have a background image, just return - if (self.current_background_image == null) { - return; - } + const current_background_image = self.current_background_image orelse return; + // Bind our background image program const bind = try gl_state.bgimage_program.bind(); defer bind.unbind(); // Get the texture - const texture = switch (self.current_background_image.?) { + const texture = switch (current_background_image) { .ready => |t| t, else => { return; From 4372b7d61bcd97fc8ad0172ba057bdcc4468b839 Mon Sep 17 00:00:00 2001 From: yunusey Date: Thu, 2 Jan 2025 15:43:50 -0500 Subject: [PATCH 03/11] Add new background image modes --- src/config.zig | 2 + src/config/Config.zig | 102 ++++++++++++++++-- src/renderer/OpenGL.zig | 102 ++++++++++-------- .../opengl/BackgroundImageProgram.zig | 11 +- src/renderer/shaders/bgimage.f.glsl | 14 ++- src/renderer/shaders/bgimage.v.glsl | 39 +++++-- src/terminal/kitty/graphics.zig | 1 - 7 files changed, 202 insertions(+), 69 deletions(-) diff --git a/src/config.zig b/src/config.zig index 75dbaae02b..81d36abf2f 100644 --- a/src/config.zig +++ b/src/config.zig @@ -28,8 +28,10 @@ pub const RepeatableCodepointMap = Config.RepeatableCodepointMap; pub const RepeatableFontVariation = Config.RepeatableFontVariation; pub const RepeatableString = Config.RepeatableString; pub const RepeatablePath = Config.RepeatablePath; +pub const SinglePath = Config.SinglePath; pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures; pub const WindowPaddingColor = Config.WindowPaddingColor; +pub const BackgroundImageMode = Config.BackgroundImageMode; // Alternate APIs pub const CAPI = @import("config/CAPI.zig"); diff --git a/src/config/Config.zig b/src/config/Config.zig index 2aa45d8604..af9d1a1ead 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -452,22 +452,22 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// Background image for the window. -@"background-image": RepeatablePath = .{}, +@"background-image": SinglePath = .{}, /// Background image opactity -@"background-image-opacity": f32 = 0.0, +@"background-image-opacity": f32 = 1.0, /// Background image mode to use. /// -/// `aspect` keeps the aspect-ratio of the background image and `scaled` scales -/// the image to fit the window. `aspect` is the default mode. -/// /// Valid values are: /// -/// * `aspect` -/// * `scaled` +/// * `zoomed` - Image is scaled to fit the window, preserving aspect ratio. +/// * `scaled` - Image is scaled to fill the window, not preserving aspect ratio. +/// * `tiled` - Image is repeated horizontally and vertically to fill the window. +/// * `centered` - Image is centered in the window and displayed 1-to-1 pixel +/// scale, preserving both the aspect ratio and the image size. /// -@"background-image-mode": BackgroundImageProgram.BackgroundMode = .aspect, +@"background-image-mode": BackgroundImageMode = .zoomed, /// The foreground and background color for selection. If this is not set, then /// the selection color is just the inverted window background and foreground @@ -4451,6 +4451,84 @@ pub const Palette = struct { } }; +/// Path is a path to a single file. +pub const SinglePath = struct { + const Self = @This(); + + /// The actual value that is updated as we parse. + value: []const u8 = "", + + /// Parse a single path. + pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { + const value = input orelse return error.ValueRequired; + const copy = try alloc.dupe(u8, value); + self.value = copy; + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: Self, alloc: Allocator) Allocator.Error!Self { + const copy_path = try alloc.dupe(u8, self.value); + return .{ + .value = copy_path, + }; + } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: anytype) !void { + try formatter.formatEntry([]const u8, self.value); + } + + pub fn expand( + self: *Self, + alloc: Allocator, + base: []const u8, + diags: *cli.DiagnosticList, + ) !void { + assert(std.fs.path.isAbsolute(base)); + var dir = try std.fs.cwd().openDir(base, .{}); + defer dir.close(); + + const path = self.value; + + // If it is already absolute we can ignore it. + if (path.len == 0 or std.fs.path.isAbsolute(path)) return; + + // If it isn't absolute, we need to make it absolute relative + // to the base. + var buf: [std.fs.max_path_bytes]u8 = undefined; + const abs = dir.realpath(path, &buf) catch |err| abs: { + if (err == error.FileNotFound) { + // The file doesn't exist. Try to resolve the relative path + // another way. + const resolved = try std.fs.path.resolve(alloc, &.{ base, path }); + defer alloc.free(resolved); + @memcpy(buf[0..resolved.len], resolved); + break :abs buf[0..resolved.len]; + } + + try diags.append(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "error resolving file path {s}: {}", + .{ path, err }, + ), + }); + + // Blank this path so that we don't attempt to resolve it again + self.value = ""; + + return; + }; + + log.debug( + "expanding file path relative={s} abs={s}", + .{ path, abs }, + ); + + self.value = try alloc.dupeZ(u8, abs); + } +}; + /// RepeatableString is a string value that can be repeated to accumulate /// a list of strings. This isn't called "StringList" because I find that /// sometimes leads to confusion that it _accepts_ a list such as @@ -5837,6 +5915,14 @@ pub const TextBlending = enum { } }; +/// See background-image-mode +pub const BackgroundImageMode = enum(u8) { + zoomed = 0, + stretched = 1, + tiled = 2, + centered = 3, +}; + /// See freetype-load-flag pub const FreetypeLoadFlags = packed struct { // The defaults here at the time of writing this match the defaults diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index a79792fbce..045e14eb79 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -4,6 +4,7 @@ pub const OpenGL = @This(); const std = @import("std"); const builtin = @import("builtin"); const glfw = @import("glfw"); +const wuffs = @import("wuffs"); const assert = std.debug.assert; const testing = std.testing; const Allocator = std.mem.Allocator; @@ -44,6 +45,9 @@ else const DrawMutex = if (single_threaded_draw) std.Thread.Mutex else void; const drawMutexZero = if (DrawMutex == void) void{} else .{}; +/// The maximum size of a background image. +const max_image_size = 400 * 1024 * 1024; // 400MB + alloc: std.mem.Allocator, /// The configuration we need derived from the main config. @@ -137,13 +141,13 @@ draw_mutex: DrawMutex = drawMutexZero, draw_background: terminal.color.RGB, /// The background image(s) to draw. Currentlly, we always draw the last image. -background_image: configpkg.RepeatablePath, +background_image: configpkg.SinglePath, /// The opacity of the background image. Not to be confused with background-opacity background_image_opacity: f32, /// The background image mode to use. -background_image_mode: BackgroundImageProgram.BackgroundMode, +background_image_mode: configpkg.BackgroundImageMode, /// The current background image to draw. If it is null, then we will not /// draw any background image. @@ -295,9 +299,9 @@ pub const DerivedConfig = struct { cursor_opacity: f64, background: terminal.color.RGB, background_opacity: f64, - background_image: configpkg.RepeatablePath, + background_image: configpkg.SinglePath, background_image_opacity: f32, - background_image_mode: BackgroundImageProgram.BackgroundMode, + background_image_mode: configpkg.BackgroundImageMode, foreground: terminal.color.RGB, selection_background: ?terminal.color.RGB, selection_foreground: ?terminal.color.RGB, @@ -839,14 +843,13 @@ pub fn updateFrame( } if (self.current_background_image == null and - self.background_image.value.items.len > 0) + self.background_image.value.len > 0) { if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); self.prepBackgroundImage() catch |err| switch (err) { error.InvalidData => { - log.warn("invalid image data", .{}); - self.current_background_image = null; + log.warn("invalid image data, skipping", .{}); }, else => return err, }; @@ -1246,52 +1249,63 @@ fn prepKittyImage( /// Prepares the current background image for upload pub fn prepBackgroundImage(self: *OpenGL) !void { // If the user doesn't have a background image, do nothing... - const last_image = self.background_image.value.getLastOrNull() orelse return; + if (self.background_image.value.len == 0) return; + const path = self.background_image.value; - // Get the last background image - const path = switch (last_image) { - .optional, .required => |path| path, - }; - const command = terminal.kitty.graphics.Command{ - .control = .{ - .transmit = .{ - .format = .png, - .medium = .file, - .width = 0, - .height = 0, - .compression = .none, - .image_id = 0, - }, - }, - .data = try self.alloc.dupe(u8, path), - }; - defer command.deinit(self.alloc); - - // Load the iamge - var loading = try terminal.kitty.graphics.LoadingImage.init(self.alloc, &command); - defer loading.deinit(self.alloc); + // Read the file content + const file_content = try self.readImageContent(path); + defer self.alloc.free(file_content); - // Complete the image to get the final data - var image = try loading.complete(self.alloc); - defer image.deinit(self.alloc); + // Decode the png (currently, we only support png) + const decoded_image = try wuffs.png.decode(self.alloc, file_content); + defer self.alloc.free(decoded_image.data); - // Copy the data into the pending state. - const data = try self.alloc.dupe(u8, image.data); + // Copy the data into the pending state + const data = try self.alloc.dupe(u8, decoded_image.data); errdefer self.alloc.free(data); - const pending: Image.Pending = .{ - .width = image.width, - .height = image.height, + .width = decoded_image.width, + .height = decoded_image.height, .data = data.ptr, }; - self.current_background_image = switch (image.format) { - .gray => .{ .pending_gray = pending }, - .gray_alpha => .{ .pending_gray_alpha = pending }, - .rgb => .{ .pending_rgb = pending }, - .rgba => .{ .pending_rgba = pending }, - .png => unreachable, // should be decoded by now + // Store the image + self.current_background_image = .{ .pending_rgba = pending }; +} + +/// Reads the content of the given image path and returns it +pub fn readImageContent(self: *OpenGL, path: []const u8) ![]u8 { + // Open the file + var file = std.fs.cwd().openFile(path, .{}) catch |err| { + log.warn("failed to open file: {}", .{err}); + return error.InvalidData; + }; + defer file.close(); + + // File must be a regular file + if (file.stat()) |stat| { + if (stat.kind != .file) { + log.warn("file is not a regular file kind={}", .{stat.kind}); + return error.InvalidData; + } + } else |err| { + log.warn("failed to stat file: {}", .{err}); + return error.InvalidData; + } + + var buf_reader = std.io.bufferedReader(file.reader()); + const reader = buf_reader.reader(); + + // Read the file + var managed = std.ArrayList(u8).init(self.alloc); + errdefer managed.deinit(); + const size: usize = max_image_size; + reader.readAllArrayList(&managed, size) catch |err| { + log.warn("failed to read file: {}", .{err}); + return error.InvalidData; }; + + return managed.items; } /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a diff --git a/src/renderer/opengl/BackgroundImageProgram.zig b/src/renderer/opengl/BackgroundImageProgram.zig index ec3bf9febf..d73c2b98ee 100644 --- a/src/renderer/opengl/BackgroundImageProgram.zig +++ b/src/renderer/opengl/BackgroundImageProgram.zig @@ -3,6 +3,7 @@ const BackgroundImageProgram = @This(); const std = @import("std"); const gl = @import("opengl"); +const configpkg = @import("../../config.zig"); pub const Input = extern struct { /// vec2 terminal_size @@ -10,13 +11,7 @@ pub const Input = extern struct { terminal_height: u32 = 0, /// uint mode - mode: BackgroundMode = .aspect, -}; - -pub const BackgroundMode = enum(u8) { - aspect = 0, - scaled = 1, - _, + mode: configpkg.BackgroundImageMode = .zoomed, }; program: gl.Program, @@ -63,7 +58,7 @@ pub fn init() !BackgroundImageProgram { var offset: usize = 0; try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset); offset += 2 * @sizeOf(u32); - try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Input), offset); + try vbobind.attributeIAdvanced(1, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Input), offset); offset += 1 * @sizeOf(u8); try vbobind.enableAttribArray(0); try vbobind.enableAttribArray(1); diff --git a/src/renderer/shaders/bgimage.f.glsl b/src/renderer/shaders/bgimage.f.glsl index 816882c7c3..9074c99186 100644 --- a/src/renderer/shaders/bgimage.f.glsl +++ b/src/renderer/shaders/bgimage.f.glsl @@ -1,6 +1,12 @@ #version 330 core +const uint MODE_ZOOMED = 0u; +const uint MODE_STRETCHED = 1u; +const uint MODE_TILED = 2u; +const uint MODE_CENTERED = 3u; + in vec2 tex_coord; +flat in uint mode; layout(location = 0) out vec4 out_FragColor; @@ -8,6 +14,12 @@ uniform sampler2D image; uniform float opacity; void main() { - vec4 color = texture(image, tex_coord); + // Normalize the coordinate if we are tiling + vec2 norm_coord = tex_coord; + // if (mode == MODE_TILED) { + // norm_coord = fract(tex_coord); + // } + norm_coord = fract(tex_coord); + vec4 color = texture(image, norm_coord); out_FragColor = vec4(color.rgb * color.a * opacity, color.a * opacity); } diff --git a/src/renderer/shaders/bgimage.v.glsl b/src/renderer/shaders/bgimage.v.glsl index dc4bdeec7d..0677d4a54d 100644 --- a/src/renderer/shaders/bgimage.v.glsl +++ b/src/renderer/shaders/bgimage.v.glsl @@ -1,24 +1,37 @@ #version 330 core -const uint MODE_ASPECT = 0u; -const uint MODE_SCALED = 1u; +const uint MODE_ZOOMED = 0u; +const uint MODE_STRETCHED = 1u; +const uint MODE_TILED = 2u; +const uint MODE_CENTERED = 3u; layout (location = 0) in vec2 terminal_size; -layout (location = 1) in uint mode; +layout (location = 1) in uint mode_in; out vec2 tex_coord; +flat out uint mode; + uniform sampler2D image; uniform mat4 projection; void main() { + // Set mode so that we can use it in the fragment shader + mode = mode_in; + + // Calculate the position of the image vec2 position; position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.; position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.; + // Get the size of the image vec2 image_size = textureSize(image, 0); + + // Handles the scale of the image relative to the terminal size vec2 scale = vec2(1.0, 1.0); + switch (mode) { - case MODE_ASPECT: + case MODE_ZOOMED: + // If zoomed, we want to scale the image to fit the terminal vec2 aspect_ratio = vec2( terminal_size.x / terminal_size.y, image_size.x / image_size.y @@ -29,12 +42,24 @@ void main() { else { scale.y = aspect_ratio.x / aspect_ratio.y; } - case MODE_SCALED: + break; + case MODE_CENTERED: + // If centered, the final scale of the image should match the actual + // size of the image and should be centered + scale.x = image_size.x / terminal_size.x; + scale.y = image_size.y / terminal_size.y; + break; + case MODE_STRETCHED: + case MODE_TILED: + // We don't need to do anything for stretched or tiled break; } - vec2 image_pos = terminal_size * position * scale; + vec2 final_image_size = terminal_size * position * scale; vec2 offset = (terminal_size * (1.0 - scale)) / 2.0; - gl_Position = projection * vec4(image_pos.xy + offset, 0.0, 1.0); + gl_Position = projection * vec4(final_image_size.xy + offset, 0.0, 1.0); tex_coord = position; + if (mode == MODE_TILED) { + tex_coord = position * terminal_size / image_size; + } } diff --git a/src/terminal/kitty/graphics.zig b/src/terminal/kitty/graphics.zig index ee2ef78040..c710f81a14 100644 --- a/src/terminal/kitty/graphics.zig +++ b/src/terminal/kitty/graphics.zig @@ -24,7 +24,6 @@ const storage = @import("graphics_storage.zig"); pub const unicode = @import("graphics_unicode.zig"); pub const Command = command.Command; pub const CommandParser = command.Parser; -pub const LoadingImage = image.LoadingImage; pub const Image = image.Image; pub const ImageStorage = storage.ImageStorage; pub const RenderPlacement = render.Placement; From 271b2832e71847fb46c62c7c0806975422d818be Mon Sep 17 00:00:00 2001 From: yunusey Date: Thu, 2 Jan 2025 16:40:51 -0500 Subject: [PATCH 04/11] Address review comments --- src/config/Config.zig | 23 +++++++++++++++-------- src/renderer/OpenGL.zig | 13 ++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index af9d1a1ead..44a9cf9ca8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3347,7 +3347,7 @@ fn expandPaths(self: *Config, base: []const u8) !void { // Expand all of our paths inline for (@typeInfo(Config).Struct.fields) |field| { - if (field.type == RepeatablePath) { + if (field.type == RepeatablePath or field.type == SinglePath) { try @field(self, field.name).expand( arena_alloc, base, @@ -4456,18 +4456,25 @@ pub const SinglePath = struct { const Self = @This(); /// The actual value that is updated as we parse. - value: []const u8 = "", + value: ?[]const u8 = null, /// Parse a single path. pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { const value = input orelse return error.ValueRequired; + // If the value is empty, we set the value to null + if (value.len == 0) { + self.value = null; + return; + } const copy = try alloc.dupe(u8, value); self.value = copy; } /// Deep copy of the struct. Required by Config. pub fn clone(self: Self, alloc: Allocator) Allocator.Error!Self { - const copy_path = try alloc.dupe(u8, self.value); + const value = self.value orelse return .{}; + + const copy_path = try alloc.dupe(u8, value); return .{ .value = copy_path, }; @@ -4475,7 +4482,8 @@ pub const SinglePath = struct { /// Used by Formatter pub fn formatEntry(self: Self, formatter: anytype) !void { - try formatter.formatEntry([]const u8, self.value); + const value = self.value orelse return; + try formatter.formatEntry([]const u8, value); } pub fn expand( @@ -4488,10 +4496,9 @@ pub const SinglePath = struct { var dir = try std.fs.cwd().openDir(base, .{}); defer dir.close(); - const path = self.value; - // If it is already absolute we can ignore it. - if (path.len == 0 or std.fs.path.isAbsolute(path)) return; + const path = self.value orelse return; + if (std.fs.path.isAbsolute(path)) return; // If it isn't absolute, we need to make it absolute relative // to the base. @@ -4515,7 +4522,7 @@ pub const SinglePath = struct { }); // Blank this path so that we don't attempt to resolve it again - self.value = ""; + self.value = null; return; }; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 045e14eb79..e1406be81a 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -843,7 +843,7 @@ pub fn updateFrame( } if (self.current_background_image == null and - self.background_image.value.len > 0) + self.background_image.value != null) { if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); @@ -1249,8 +1249,7 @@ fn prepKittyImage( /// Prepares the current background image for upload pub fn prepBackgroundImage(self: *OpenGL) !void { // If the user doesn't have a background image, do nothing... - if (self.background_image.value.len == 0) return; - const path = self.background_image.value; + const path = self.background_image.value orelse return; // Read the file content const file_content = try self.readImageContent(path); @@ -1275,8 +1274,9 @@ pub fn prepBackgroundImage(self: *OpenGL) !void { /// Reads the content of the given image path and returns it pub fn readImageContent(self: *OpenGL, path: []const u8) ![]u8 { + assert(std.fs.path.isAbsolute(path)); // Open the file - var file = std.fs.cwd().openFile(path, .{}) catch |err| { + var file = std.fs.openFileAbsolute(path, .{}) catch |err| { log.warn("failed to open file: {}", .{err}); return error.InvalidData; }; @@ -1299,13 +1299,12 @@ pub fn readImageContent(self: *OpenGL, path: []const u8) ![]u8 { // Read the file var managed = std.ArrayList(u8).init(self.alloc); errdefer managed.deinit(); - const size: usize = max_image_size; - reader.readAllArrayList(&managed, size) catch |err| { + reader.readAllArrayList(&managed, max_image_size) catch |err| { log.warn("failed to read file: {}", .{err}); return error.InvalidData; }; - return managed.items; + return managed.toOwnedSlice(); } /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a From 4d8fed669458e06ad622020583978b351c8ca283 Mon Sep 17 00:00:00 2001 From: yunusey Date: Thu, 2 Jan 2025 17:23:01 -0500 Subject: [PATCH 05/11] Fix shaders --- src/renderer/shaders/bgimage.f.glsl | 11 +---------- src/renderer/shaders/bgimage.v.glsl | 6 +----- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/renderer/shaders/bgimage.f.glsl b/src/renderer/shaders/bgimage.f.glsl index 9074c99186..6598255380 100644 --- a/src/renderer/shaders/bgimage.f.glsl +++ b/src/renderer/shaders/bgimage.f.glsl @@ -1,12 +1,6 @@ #version 330 core -const uint MODE_ZOOMED = 0u; -const uint MODE_STRETCHED = 1u; -const uint MODE_TILED = 2u; -const uint MODE_CENTERED = 3u; - in vec2 tex_coord; -flat in uint mode; layout(location = 0) out vec4 out_FragColor; @@ -14,11 +8,8 @@ uniform sampler2D image; uniform float opacity; void main() { - // Normalize the coordinate if we are tiling + // Normalize the coordinates vec2 norm_coord = tex_coord; - // if (mode == MODE_TILED) { - // norm_coord = fract(tex_coord); - // } norm_coord = fract(tex_coord); vec4 color = texture(image, norm_coord); out_FragColor = vec4(color.rgb * color.a * opacity, color.a * opacity); diff --git a/src/renderer/shaders/bgimage.v.glsl b/src/renderer/shaders/bgimage.v.glsl index 0677d4a54d..8fe9bb28b6 100644 --- a/src/renderer/shaders/bgimage.v.glsl +++ b/src/renderer/shaders/bgimage.v.glsl @@ -6,18 +6,14 @@ const uint MODE_TILED = 2u; const uint MODE_CENTERED = 3u; layout (location = 0) in vec2 terminal_size; -layout (location = 1) in uint mode_in; +layout (location = 1) in uint mode; out vec2 tex_coord; -flat out uint mode; uniform sampler2D image; uniform mat4 projection; void main() { - // Set mode so that we can use it in the fragment shader - mode = mode_in; - // Calculate the position of the image vec2 position; position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.; From 3064de5e9e29bb19ebfd70677dda780b5f880f57 Mon Sep 17 00:00:00 2001 From: yunusey Date: Thu, 2 Jan 2025 22:30:49 -0500 Subject: [PATCH 06/11] Add jpg support --- src/config/Config.zig | 201 +++++++++++++--------------- src/renderer/OpenGL.zig | 17 ++- src/renderer/shaders/bgimage.v.glsl | 3 + 3 files changed, 113 insertions(+), 108 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 44a9cf9ca8..ecfebc857b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3357,6 +3357,86 @@ fn expandPaths(self: *Config, base: []const u8) !void { } } +/// Expand a relative path to an absolute path. This function is used by +/// the RepeatablePath and SinglePath to expand the paths they store. +fn expandPath( + alloc: Allocator, + base: []const u8, + path: []const u8, + diags: *cli.DiagnosticList, +) ![]const u8 { + assert(std.fs.path.isAbsolute(base)); + var dir = try std.fs.cwd().openDir(base, .{}); + defer dir.close(); + + // If it is already absolute we can just return it + if (path.len == 0 or std.fs.path.isAbsolute(path)) return path; + + // If it isn't absolute, we need to make it absolute relative + // to the base. + var buf: [std.fs.max_path_bytes]u8 = undefined; + + // Check if the path starts with a tilde and expand it to the + // home directory on Linux/macOS. We explicitly look for "~/" + // because we don't support alternate users such as "~alice/" + if (std.mem.startsWith(u8, path, "~/")) expand: { + // Windows isn't supported yet + if (comptime builtin.os.tag == .windows) break :expand; + + const expanded: []const u8 = internal_os.expandHome( + path, + &buf, + ) catch |err| { + try diags.append(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "error expanding home directory for path {s}: {}", + .{ path, err }, + ), + }); + + // We can't expand this path so return an empty string + return ""; + }; + + log.debug( + "expanding file path from home directory: path={s}", + .{expanded}, + ); + + return expanded; + } + + const abs = dir.realpath(path, &buf) catch |err| abs: { + if (err == error.FileNotFound) { + // The file doesn't exist. Try to resolve the relative path + // another way. + const resolved = try std.fs.path.resolve(alloc, &.{ base, path }); + defer alloc.free(resolved); + @memcpy(buf[0..resolved.len], resolved); + break :abs buf[0..resolved.len]; + } + + try diags.append(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "error resolving file path {s}: {}", + .{ path, err }, + ), + }); + + // We can't expand this path so return an empty string + return ""; + }; + + log.debug( + "expanding file path relative={s} abs={s}", + .{ path, abs }, + ); + + return abs; +} + fn loadTheme(self: *Config, theme: Theme) !void { // Load the correct theme depending on the conditional state. // Dark/light themes were programmed prior to conditional configuration @@ -4486,52 +4566,22 @@ pub const SinglePath = struct { try formatter.formatEntry([]const u8, value); } + /// Expand all the paths relative to the base directory. pub fn expand( self: *Self, alloc: Allocator, base: []const u8, diags: *cli.DiagnosticList, ) !void { - assert(std.fs.path.isAbsolute(base)); - var dir = try std.fs.cwd().openDir(base, .{}); - defer dir.close(); - - // If it is already absolute we can ignore it. + // Try expanding path relative to the base. const path = self.value orelse return; - if (std.fs.path.isAbsolute(path)) return; - - // If it isn't absolute, we need to make it absolute relative - // to the base. - var buf: [std.fs.max_path_bytes]u8 = undefined; - const abs = dir.realpath(path, &buf) catch |err| abs: { - if (err == error.FileNotFound) { - // The file doesn't exist. Try to resolve the relative path - // another way. - const resolved = try std.fs.path.resolve(alloc, &.{ base, path }); - defer alloc.free(resolved); - @memcpy(buf[0..resolved.len], resolved); - break :abs buf[0..resolved.len]; - } - - try diags.append(alloc, .{ - .message = try std.fmt.allocPrintZ( - alloc, - "error resolving file path {s}: {}", - .{ path, err }, - ), - }); + const abs = try expandPath(alloc, base, path, diags); + if (abs.len == 0) { // Blank this path so that we don't attempt to resolve it again self.value = null; - return; - }; - - log.debug( - "expanding file path relative={s} abs={s}", - .{ path, abs }, - ); - + } self.value = try alloc.dupeZ(u8, abs); } }; @@ -4767,88 +4817,20 @@ pub const RepeatablePath = struct { base: []const u8, diags: *cli.DiagnosticList, ) !void { - assert(std.fs.path.isAbsolute(base)); - var dir = try std.fs.cwd().openDir(base, .{}); - defer dir.close(); - for (0..self.value.items.len) |i| { const path = switch (self.value.items[i]) { .optional, .required => |path| path, }; - // If it is already absolute we can ignore it. - if (path.len == 0 or std.fs.path.isAbsolute(path)) continue; - - // If it isn't absolute, we need to make it absolute relative - // to the base. - var buf: [std.fs.max_path_bytes]u8 = undefined; - - // Check if the path starts with a tilde and expand it to the - // home directory on Linux/macOS. We explicitly look for "~/" - // because we don't support alternate users such as "~alice/" - if (std.mem.startsWith(u8, path, "~/")) expand: { - // Windows isn't supported yet - if (comptime builtin.os.tag == .windows) break :expand; - - const expanded: []const u8 = internal_os.expandHome( - path, - &buf, - ) catch |err| { - try diags.append(alloc, .{ - .message = try std.fmt.allocPrintZ( - alloc, - "error expanding home directory for path {s}: {}", - .{ path, err }, - ), - }); - - // Blank this path so that we don't attempt to resolve it - // again - self.value.items[i] = .{ .required = "" }; - - continue; - }; - - log.debug( - "expanding file path from home directory: path={s}", - .{expanded}, - ); - - switch (self.value.items[i]) { - .optional, .required => |*p| p.* = try alloc.dupeZ(u8, expanded), - } - - continue; - } - - const abs = dir.realpath(path, &buf) catch |err| abs: { - if (err == error.FileNotFound) { - // The file doesn't exist. Try to resolve the relative path - // another way. - const resolved = try std.fs.path.resolve(alloc, &.{ base, path }); - defer alloc.free(resolved); - @memcpy(buf[0..resolved.len], resolved); - break :abs buf[0..resolved.len]; - } - - try diags.append(alloc, .{ - .message = try std.fmt.allocPrintZ( - alloc, - "error resolving file path {s}: {}", - .{ path, err }, - ), - }); + // Try expanding path relative to the base. + const abs = try expandPath(alloc, base, path, diags); + if (abs.len == 0) { // Blank this path so that we don't attempt to resolve it again self.value.items[i] = .{ .required = "" }; continue; - }; - - log.debug( - "expanding file path relative={s} abs={s}", - .{ path, abs }, - ); + } switch (self.value.items[i]) { .optional, .required => |*p| p.* = try alloc.dupeZ(u8, abs), @@ -5923,6 +5905,11 @@ pub const TextBlending = enum { }; /// See background-image-mode +/// +/// This enum is used to set the background image mode. The shader expects +/// a `uint`, so we use `u8` here. The values for each mode should be kept +/// in sync with the values in the vertex shader used to render the +/// background image (`bgimage`). pub const BackgroundImageMode = enum(u8) { zoomed = 0, stretched = 1, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index e1406be81a..0f4216155d 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1256,7 +1256,22 @@ pub fn prepBackgroundImage(self: *OpenGL) !void { defer self.alloc.free(file_content); // Decode the png (currently, we only support png) - const decoded_image = try wuffs.png.decode(self.alloc, file_content); + const decoded_image: wuffs.ImageData = blk: { + // Extract the file extension + const ext = std.fs.path.extension(path); + const ext_lower = try std.ascii.allocLowerString(self.alloc, ext); + defer self.alloc.free(ext_lower); + + // Match based on extension + if (std.mem.eql(u8, ext_lower, ".png")) { + break :blk try wuffs.png.decode(self.alloc, file_content); + } else if (std.mem.eql(u8, ext_lower, ".jpg") or std.mem.eql(u8, ext_lower, ".jpeg")) { + break :blk try wuffs.jpeg.decode(self.alloc, file_content); + } else { + log.warn("unsupported image format: {s}", .{ext}); + return error.InvalidData; + } + }; defer self.alloc.free(decoded_image.data); // Copy the data into the pending state diff --git a/src/renderer/shaders/bgimage.v.glsl b/src/renderer/shaders/bgimage.v.glsl index 8fe9bb28b6..1d0b5fe760 100644 --- a/src/renderer/shaders/bgimage.v.glsl +++ b/src/renderer/shaders/bgimage.v.glsl @@ -1,5 +1,8 @@ #version 330 core +// These are the possible modes that "mode" can be set to. +// +// NOTE: this must be kept in sync with the BackgroundImageMode const uint MODE_ZOOMED = 0u; const uint MODE_STRETCHED = 1u; const uint MODE_TILED = 2u; From 5a35acb11fabdd5249b4c709e874310565c47156 Mon Sep 17 00:00:00 2001 From: yunusey Date: Fri, 3 Jan 2025 12:00:15 -0500 Subject: [PATCH 07/11] Enhance documentation --- src/config/Config.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index ecfebc857b..e6dca80054 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4531,7 +4531,9 @@ pub const Palette = struct { } }; -/// Path is a path to a single file. +/// SinglePath is a path to a single file. When loading the configuration +/// file, always the last one will be kept and be automatically expanded +/// relative to the path of the config file. pub const SinglePath = struct { const Self = @This(); From 82645fe6a10be1a40b8a2a7591b2941c14f299c0 Mon Sep 17 00:00:00 2001 From: yunusey Date: Mon, 6 Jan 2025 14:08:20 -0500 Subject: [PATCH 08/11] Add anchor options --- src/config/Config.zig | 12 ++++++++++++ src/renderer/shaders/bgimage.v.glsl | 30 ++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index e6dca80054..761790f2c2 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -466,6 +466,14 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// * `tiled` - Image is repeated horizontally and vertically to fill the window. /// * `centered` - Image is centered in the window and displayed 1-to-1 pixel /// scale, preserving both the aspect ratio and the image size. +/// * `upper-left` - Image is anchored to the upper left corner of the window, +/// preserving the aspect ratio. +/// * `upper-right` - Image is anchored to the upper right corner of the window, +/// preserving the aspect ratio. +/// * `lower-left` - Image is anchored to the lower left corner of the window, +/// preserving the aspect ratio. +/// * `lower-right` - Image is anchored to the lower right corner of the window, +/// preserving the aspect ratio. /// @"background-image-mode": BackgroundImageMode = .zoomed, @@ -5917,6 +5925,10 @@ pub const BackgroundImageMode = enum(u8) { stretched = 1, tiled = 2, centered = 3, + upper_left = 4, + upper_right = 5, + lower_left = 6, + lower_right = 7, }; /// See freetype-load-flag diff --git a/src/renderer/shaders/bgimage.v.glsl b/src/renderer/shaders/bgimage.v.glsl index 1d0b5fe760..0834b7c297 100644 --- a/src/renderer/shaders/bgimage.v.glsl +++ b/src/renderer/shaders/bgimage.v.glsl @@ -7,6 +7,10 @@ const uint MODE_ZOOMED = 0u; const uint MODE_STRETCHED = 1u; const uint MODE_TILED = 2u; const uint MODE_CENTERED = 3u; +const uint MODE_UPPER_LEFT = 4u; +const uint MODE_UPPER_RIGHT = 5u; +const uint MODE_LOWER_LEFT = 6u; +const uint MODE_LOWER_RIGHT = 7u; layout (location = 0) in vec2 terminal_size; layout (location = 1) in uint mode; @@ -43,6 +47,10 @@ void main() { } break; case MODE_CENTERED: + case MODE_UPPER_LEFT: + case MODE_UPPER_RIGHT: + case MODE_LOWER_LEFT: + case MODE_LOWER_RIGHT: // If centered, the final scale of the image should match the actual // size of the image and should be centered scale.x = image_size.x / terminal_size.x; @@ -55,7 +63,27 @@ void main() { } vec2 final_image_size = terminal_size * position * scale; - vec2 offset = (terminal_size * (1.0 - scale)) / 2.0; + vec2 offset = vec2(0.0, 0.0); + switch (mode) { + case MODE_ZOOMED: + case MODE_STRETCHED: + case MODE_TILED: + case MODE_CENTERED: + offset = (terminal_size * (1.0 - scale)) / 2.0; + break; + case MODE_UPPER_LEFT: + offset = vec2(0.0, 0.0); + break; + case MODE_UPPER_RIGHT: + offset = vec2(terminal_size.x - image_size.x, 0.0); + break; + case MODE_LOWER_LEFT: + offset = vec2(0.0, terminal_size.y - image_size.y); + break; + case MODE_LOWER_RIGHT: + offset = vec2(terminal_size.x - image_size.x, terminal_size.y - image_size.y); + break; + } gl_Position = projection * vec4(final_image_size.xy + offset, 0.0, 1.0); tex_coord = position; if (mode == MODE_TILED) { From 404f94779fd29ff75b249f91873bf6adf570a87d Mon Sep 17 00:00:00 2001 From: yunusey Date: Wed, 8 Jan 2025 18:29:59 -0500 Subject: [PATCH 09/11] Fix typos --- src/config/Config.zig | 2 +- src/renderer/OpenGL.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 761790f2c2..b939c0a481 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -454,7 +454,7 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// Background image for the window. @"background-image": SinglePath = .{}, -/// Background image opactity +/// Background image opacity @"background-image-opacity": f32 = 1.0, /// Background image mode to use. diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 0f4216155d..9c88d2ab4d 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -140,7 +140,7 @@ draw_mutex: DrawMutex = drawMutexZero, /// terminal is in reversed mode. draw_background: terminal.color.RGB, -/// The background image(s) to draw. Currentlly, we always draw the last image. +/// The background image(s) to draw. Currently, we always draw the last image. background_image: configpkg.SinglePath, /// The opacity of the background image. Not to be confused with background-opacity From 5d583cbd2bd4b7c51d256441e6117f3a7d74db95 Mon Sep 17 00:00:00 2001 From: yunusey Date: Sat, 18 Jan 2025 12:23:07 -0500 Subject: [PATCH 10/11] Remove unused import --- src/config/Config.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index b939c0a481..6115d21fd5 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -20,7 +20,6 @@ const global_state = &@import("../global.zig").state; const fontpkg = @import("../font/main.zig"); const inputpkg = @import("../input.zig"); const terminal = @import("../terminal/main.zig"); -const BackgroundImageProgram = @import("../renderer/opengl/BackgroundImageProgram.zig"); const internal_os = @import("../os/main.zig"); const cli = @import("../cli.zig"); const Command = @import("../Command.zig"); From abf5fbf0ef0b383b27c84251d7ba71a99844c2e0 Mon Sep 17 00:00:00 2001 From: yunusey Date: Fri, 31 Jan 2025 22:24:13 -0500 Subject: [PATCH 11/11] Add cropped background image mode --- src/config/Config.zig | 17 +++++++++------ src/renderer/shaders/bgimage.v.glsl | 33 ++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 6115d21fd5..277a181000 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -461,7 +461,9 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// Valid values are: /// /// * `zoomed` - Image is scaled to fit the window, preserving aspect ratio. -/// * `scaled` - Image is scaled to fill the window, not preserving aspect ratio. +/// * `stretched` - Image is stretched to fill the window, not preserving aspect ratio. +/// * `cropped` - Image is centered in the window, preserving the aspect ratio +/// but cropping the image to fill the window, as needed. /// * `tiled` - Image is repeated horizontally and vertically to fill the window. /// * `centered` - Image is centered in the window and displayed 1-to-1 pixel /// scale, preserving both the aspect ratio and the image size. @@ -5922,12 +5924,13 @@ pub const TextBlending = enum { pub const BackgroundImageMode = enum(u8) { zoomed = 0, stretched = 1, - tiled = 2, - centered = 3, - upper_left = 4, - upper_right = 5, - lower_left = 6, - lower_right = 7, + cropped = 2, + tiled = 3, + centered = 4, + upper_left = 5, + upper_right = 6, + lower_left = 7, + lower_right = 8, }; /// See freetype-load-flag diff --git a/src/renderer/shaders/bgimage.v.glsl b/src/renderer/shaders/bgimage.v.glsl index 0834b7c297..f83096ecd1 100644 --- a/src/renderer/shaders/bgimage.v.glsl +++ b/src/renderer/shaders/bgimage.v.glsl @@ -5,12 +5,13 @@ // NOTE: this must be kept in sync with the BackgroundImageMode const uint MODE_ZOOMED = 0u; const uint MODE_STRETCHED = 1u; -const uint MODE_TILED = 2u; -const uint MODE_CENTERED = 3u; -const uint MODE_UPPER_LEFT = 4u; -const uint MODE_UPPER_RIGHT = 5u; -const uint MODE_LOWER_LEFT = 6u; -const uint MODE_LOWER_RIGHT = 7u; +const uint MODE_CROPPED = 2u; +const uint MODE_TILED = 3u; +const uint MODE_CENTERED = 4u; +const uint MODE_UPPER_LEFT = 5u; +const uint MODE_UPPER_RIGHT = 6u; +const uint MODE_LOWER_LEFT = 7u; +const uint MODE_LOWER_RIGHT = 8u; layout (location = 0) in vec2 terminal_size; layout (location = 1) in uint mode; @@ -32,13 +33,15 @@ void main() { // Handles the scale of the image relative to the terminal size vec2 scale = vec2(1.0, 1.0); + // Calculate the aspect ratio of the terminal and the image + vec2 aspect_ratio = vec2( + terminal_size.x / terminal_size.y, + image_size.x / image_size.y + ); + switch (mode) { case MODE_ZOOMED: // If zoomed, we want to scale the image to fit the terminal - vec2 aspect_ratio = vec2( - terminal_size.x / terminal_size.y, - image_size.x / image_size.y - ); if (aspect_ratio.x > aspect_ratio.y) { scale.x = aspect_ratio.y / aspect_ratio.x; } @@ -46,6 +49,15 @@ void main() { scale.y = aspect_ratio.x / aspect_ratio.y; } break; + case MODE_CROPPED: + // If cropped, we want to scale the image to fit the terminal + if (aspect_ratio.x < aspect_ratio.y) { + scale.x = aspect_ratio.y / aspect_ratio.x; + } + else { + scale.y = aspect_ratio.x / aspect_ratio.y; + } + break; case MODE_CENTERED: case MODE_UPPER_LEFT: case MODE_UPPER_RIGHT: @@ -67,6 +79,7 @@ void main() { switch (mode) { case MODE_ZOOMED: case MODE_STRETCHED: + case MODE_CROPPED: case MODE_TILED: case MODE_CENTERED: offset = (terminal_size * (1.0 - scale)) / 2.0;