diff --git a/src/config.zig b/src/config.zig index fb7359b3e..2eeeb29cf 100644 --- a/src/config.zig +++ b/src/config.zig @@ -30,8 +30,10 @@ pub const RepeatableFontVariation = Config.RepeatableFontVariation; pub const RepeatableString = Config.RepeatableString; pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig"); 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 80c67e03e..1cb10da22 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -461,22 +461,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 @@ -4181,6 +4181,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 @@ -6177,6 +6255,14 @@ pub const AlphaBlending = 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 cf6cbfd4d..26f4a9c37 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: DrawMutex = 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, @@ -824,14 +828,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, }; @@ -1205,52 +1208,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); + // Read the file content + const file_content = try self.readImageContent(path); + defer self.alloc.free(file_content); - // Load the iamge - var loading = try terminal.kitty.graphics.LoadingImage.init(self.alloc, &command); - defer loading.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); - // 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); + // 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 ec3bf9feb..d73c2b98e 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 816882c7c..9074c9918 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 dc4bdeec7..0677d4a54 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 ee2ef7804..c710f81a1 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;