From 0cbf2bbcc51e45acb46d1e2374701f476a2f8159 Mon Sep 17 00:00:00 2001 From: rohitb Date: Mon, 2 Jun 2025 20:32:00 +0530 Subject: [PATCH] Make compatible with latest Mainline, OpenGL --- src/config.zig | 3 +- src/config/Config.zig | 496 ++++---------------------------- src/renderer/Metal.zig | 89 +++--- src/renderer/metal/shaders.zig | 1 + src/renderer/shaders/cell.metal | 59 ++-- 5 files changed, 124 insertions(+), 524 deletions(-) diff --git a/src/config.zig b/src/config.zig index 2eeeb29cf..ab5cecf1e 100644 --- a/src/config.zig +++ b/src/config.zig @@ -30,10 +30,11 @@ 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 Path = Config.Path; pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures; pub const WindowPaddingColor = Config.WindowPaddingColor; pub const BackgroundImageMode = Config.BackgroundImageMode; +pub const BackgroundImagePosition = Config.BackgroundImagePosition; // Alternate APIs pub const CAPI = @import("config/CAPI.zig"); diff --git a/src/config/Config.zig b/src/config/Config.zig index 6fa895702..f7d5c9c71 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -460,7 +460,7 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// Background image for the window. -@"background-image": SinglePath = .{}, +@"background-image": ?Path = null, /// Background image opacity @"background-image-opacity": f32 = 1.0, @@ -469,23 +469,30 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// /// Valid values are: /// -/// * `zoomed` - Image is scaled to fit the window, 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 +/// * `contain` - Image is scaled to fit the window, preserving aspect ratio. +/// * `fill` - Image is stretched to fill the window, not preserving aspect ratio. +/// * `cover` - 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. -/// * `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. +/// * `none` - Image is displayed 1-to-1 pixel scale, preserving both the aspect +/// ratio and the image size. /// -@"background-image-mode": BackgroundImageMode = .zoomed, +@"background-image-mode": BackgroundImageMode = .none, + +/// Background image position. +/// +/// Valid values are: +/// * `top-left` +/// * `top-center` +/// * `top-right` +/// * `center-left` +/// * `center` +/// * `center-right` +/// * `bottom-left` +/// * `bottom-center` +/// * `bottom-right` +/// +@"background-image-position": BackgroundImagePosition = .center, /// The foreground and background color for selection. If this is not set, then /// the selection color is just the inverted window background and foreground @@ -3003,97 +3010,29 @@ fn expandPaths(self: *Config, base: []const u8) !void { ); // Expand all of our paths - inline for (@typeInfo(Config).Struct.fields) |field| { - if (field.type == RepeatablePath) { - try @field(self, field.name).expand( - arena_alloc, - base, - &self._diagnostics, - ); + inline for (@typeInfo(Config).@"struct".fields) |field| { + switch (field.type) { + RepeatablePath, Path => { + try @field(self, field.name).expand( + arena_alloc, + base, + &self._diagnostics, + ); + }, + ?Path => { + if (@field(self, field.name)) |*path| { + try path.expand( + arena_alloc, + base, + &self._diagnostics, + ); + } + }, + else => {}, } } } -/// 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 @@ -4272,63 +4211,6 @@ pub const Palette = struct { } }; -/// 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(); - - /// The actual value that is updated as we parse. - 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 value = self.value orelse return .{}; - - const copy_path = try alloc.dupe(u8, value); - return .{ - .value = copy_path, - }; - } - - /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { - const value = self.value orelse return; - 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 { - // Try expanding path relative to the base. - const path = self.value orelse return; - 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; - } - 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 @@ -4485,272 +4367,6 @@ pub const RepeatableString = struct { } }; -/// RepeatablePath is like repeatable string but represents a path value. -/// The difference is that when loading the configuration any values for -/// this will be automatically expanded relative to the path of the config -/// file. -pub const RepeatablePath = struct { - const Self = @This(); - - const Path = union(enum) { - /// No error if the file does not exist. - optional: [:0]const u8, - - /// The file is required to exist. - required: [:0]const u8, - }; - - value: std.ArrayListUnmanaged(Path) = .{}, - - pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { - const value, const optional = if (input) |value| blk: { - if (value.len == 0) { - self.value.clearRetainingCapacity(); - return; - } - - break :blk if (value[0] == '?') - .{ value[1..], true } - else if (value.len >= 2 and value[0] == '"' and value[value.len - 1] == '"') - .{ value[1 .. value.len - 1], false } - else - .{ value, false }; - } else return error.ValueRequired; - - if (value.len == 0) { - // This handles the case of zero length paths after removing any ? - // prefixes or surrounding quotes. In this case, we don't reset the - // list. - return; - } - - const item: Path = if (optional) - .{ .optional = try alloc.dupeZ(u8, value) } - else - .{ .required = try alloc.dupeZ(u8, value) }; - - try self.value.append(alloc, item); - } - - /// Deep copy of the struct. Required by Config. - pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { - const value = try self.value.clone(alloc); - for (value.items) |*item| { - switch (item.*) { - .optional, .required => |*path| path.* = try alloc.dupeZ(u8, path.*), - } - } - - return .{ - .value = value, - }; - } - - /// Compare if two of our value are requal. Required by Config. - pub fn equal(self: Self, other: Self) bool { - if (self.value.items.len != other.value.items.len) return false; - for (self.value.items, other.value.items) |a, b| { - if (!std.meta.eql(a, b)) return false; - } - - return true; - } - - /// Used by Formatter - pub fn formatEntry(self: Self, formatter: anytype) !void { - if (self.value.items.len == 0) { - try formatter.formatEntry(void, {}); - return; - } - - var buf: [std.fs.max_path_bytes + 1]u8 = undefined; - for (self.value.items) |item| { - const value = switch (item) { - .optional => |path| std.fmt.bufPrint( - &buf, - "?{s}", - .{path}, - ) catch |err| switch (err) { - // Required for builds on Linux where NoSpaceLeft - // isn't an allowed error for fmt. - error.NoSpaceLeft => return error.OutOfMemory, - }, - .required => |path| path, - }; - - 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(); - - 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 }, - ), - }); - - // 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), - } - } - } - - test "parseCLI" { - const testing = std.testing; - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var list: Self = .{}; - try list.parseCLI(alloc, "config.1"); - try list.parseCLI(alloc, "?config.2"); - try list.parseCLI(alloc, "\"?config.3\""); - - // Zero-length values, ignored - try list.parseCLI(alloc, "?"); - try list.parseCLI(alloc, "\"\""); - - try testing.expectEqual(@as(usize, 3), list.value.items.len); - - const Tag = std.meta.Tag(Path); - try testing.expectEqual(Tag.required, @as(Tag, list.value.items[0])); - try testing.expectEqualStrings("config.1", list.value.items[0].required); - - try testing.expectEqual(Tag.optional, @as(Tag, list.value.items[1])); - try testing.expectEqualStrings("config.2", list.value.items[1].optional); - - try testing.expectEqual(Tag.required, @as(Tag, list.value.items[2])); - try testing.expectEqualStrings("?config.3", list.value.items[2].required); - - try list.parseCLI(alloc, ""); - try testing.expectEqual(@as(usize, 0), list.value.items.len); - } - - test "formatConfig empty" { - const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); - defer buf.deinit(); - - var list: Self = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = \n", buf.items); - } - - test "formatConfig single item" { - const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); - defer buf.deinit(); - - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var list: Self = .{}; - try list.parseCLI(alloc, "A"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); - } - - test "formatConfig multiple items" { - const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); - defer buf.deinit(); - - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var list: Self = .{}; - try list.parseCLI(alloc, "A"); - try list.parseCLI(alloc, "?B"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = A\na = ?B\n", buf.items); - } -}; - /// FontVariation is a repeatable configuration value that sets a single /// font variation value. Font variations are configurations for what /// are often called "variable fonts." The font files usually end in @@ -6634,15 +6250,29 @@ pub const AlphaBlending = enum { /// 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, - cropped = 2, + contain = 0, + fill = 1, + cover = 2, tiled = 3, - centered = 4, - upper_left = 5, - upper_right = 6, - lower_left = 7, - lower_right = 8, + none = 4, +}; + +/// See background-image-position +/// +/// This enum is used to set the background image position. The shader expects +/// a `uint`, so we use `u8` here. The values for each position should be kept +/// in sync with the values in the vertex shader used to render the +/// background image (`bgimage`). +pub const BackgroundImagePosition = enum(u8) { + @"top-left" = 0, + @"top-center" = 1, + @"top-right" = 2, + @"center-left" = 3, + center = 4, + @"center-right" = 5, + @"bottom-left" = 6, + @"bottom-center" = 7, + @"bottom-right" = 8, }; /// See freetype-load-flag diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 6ee3ce433..f0f5acafc 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -135,11 +135,14 @@ font_shaper: font.Shaper, font_shaper_cache: font.ShaperCache, /// The background image(s) to draw. Currently, we always draw the last image. -background_image: configpkg.SinglePath, +background_image: ?configpkg.Path, /// The background image mode to use. background_image_mode: configpkg.BackgroundImageMode, +/// The background image position to use. +background_image_position: configpkg.BackgroundImagePosition, + /// The current background image to draw. If it is null, then we will not /// draw any background image. current_background_image: ?Image = null, @@ -443,9 +446,10 @@ pub const DerivedConfig = struct { cursor_text: ?terminal.color.RGB, background: terminal.color.RGB, background_opacity: f64, - background_image: configpkg.SinglePath, + background_image: ?configpkg.Path, background_image_opacity: f32, background_image_mode: configpkg.BackgroundImageMode, + background_image_position: configpkg.BackgroundImagePosition, foreground: terminal.color.RGB, selection_background: ?terminal.color.RGB, selection_foreground: ?terminal.color.RGB, @@ -471,7 +475,7 @@ pub const DerivedConfig = struct { const custom_shaders = try config.@"custom-shader".clone(alloc); // Copy our background image - const background_image = try config.@"background-image".clone(alloc); + const background_image = if (config.@"background-image") |v| try v.clone(alloc) else null; // Copy our font features const font_features = try config.@"font-feature".clone(alloc); @@ -517,6 +521,7 @@ pub const DerivedConfig = struct { .background_image = background_image, .background_image_opacity = config.@"background-image-opacity", .background_image_mode = config.@"background-image-mode", + .background_image_position = config.@"background-image-position", .invert_selection_fg_bg = config.@"selection-invert-fg-bg", .bold_is_bright = config.@"bold-is-bright", @@ -623,7 +628,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { else => @compileError("unsupported target for Metal"), }; layer.setProperty("device", gpu_state.device.value); - layer.setProperty("opaque", options.config.background_opacity >= 1 and options.config.background_image.value == null); + layer.setProperty("opaque", options.config.background_opacity >= 1 and options.config.background_image == null); layer.setProperty("displaySyncEnabled", options.config.vsync); // Set our layer's pixel format appropriately. @@ -719,6 +724,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .default_background_color = options.config.background, .background_image = options.config.background_image, .background_image_mode = options.config.background_image_mode, + .background_image_position = options.config.background_image_position, .cursor_color = null, .default_cursor_color = options.config.cursor_color, .cursor_invert = options.config.cursor_invert, @@ -744,7 +750,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .use_display_p3 = options.config.colorspace == .@"display-p3", .use_linear_blending = options.config.blending.isLinear(), .use_linear_correction = options.config.blending == .@"linear-corrected", - .has_bg_image = (options.config.background_image.value != null), + .has_bg_image = (options.config.background_image != null), .bg_image_opacity = options.config.background_image_opacity, }, @@ -1172,15 +1178,19 @@ pub fn updateFrame( try self.prepKittyGraphics(state.terminal); } - if (self.current_background_image == null and - self.background_image.value != null) - { - self.prepBackgroundImage() catch |err| switch (err) { - error.InvalidData => { - log.warn("invalid image data, skipping", .{}); - }, - else => return err, - }; + if (self.current_background_image == null) { + if (self.background_image) |background_image| { + const img_path, const required = switch (background_image) { + .optional => |path| .{ path, false }, + .required => |path| .{ path, true }, + }; + self.prepBackgroundImage(img_path) catch |err| switch (err) { + error.InvalidData => if (required) { + log.err("error loading background image {s}: {}", .{ img_path, err }); + }, + else => return err, + }; + } } // If we have any terminal dirty flags set then we need to rebuild @@ -1326,8 +1336,8 @@ pub fn updateFrame( } // Check if we need to update our current background image - if (self.current_background_image) |current_background_image| { - switch (current_background_image) { + if (self.current_background_image) |*current_background_image| { + switch (current_background_image.*) { .ready => {}, .pending_gray, @@ -1338,7 +1348,7 @@ pub fn updateFrame( .replace_gray_alpha, .replace_rgb, .replace_rgba, - => try self.current_background_image.?.upload( + => try current_background_image.upload( self.alloc, self.gpu_state.device, self.gpu_state.default_storage_mode, @@ -1348,7 +1358,7 @@ pub fn updateFrame( .unload_replace, .unload_ready, => { - self.current_background_image.?.deinit(self.alloc); + current_background_image.deinit(self.alloc); self.current_background_image = null; }, } @@ -1723,6 +1733,7 @@ fn drawBackgroundImage( @as(f32, @floatFromInt(self.size.terminal().height)), }, .mode = self.background_image_mode, + .position_index = self.background_image_position, }}, .{ // Indicate that the CPU writes to this resource but never reads it. .cpu_cache_mode = .write_combined, @@ -2281,10 +2292,7 @@ fn prepKittyImage( } /// Prepares the current background image for upload -pub fn prepBackgroundImage(self: *Metal) !void { - // If the user doesn't have a background image, do nothing... - const path = self.background_image.value orelse return; - +pub fn prepBackgroundImage(self: *Metal, path: []const u8) !void { // Read the file content const file_content = try self.readImageContent(path); defer self.alloc.free(file_content); @@ -2293,28 +2301,24 @@ pub fn prepBackgroundImage(self: *Metal) !void { 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")) { + if (std.ascii.eqlIgnoreCase(ext, ".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")) { + } else if (std.ascii.eqlIgnoreCase(ext, ".jpg") or std.ascii.eqlIgnoreCase(ext, ".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); + errdefer self.alloc.free(decoded_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 = decoded_image.width, .height = decoded_image.height, - .data = data.ptr, + .data = @constCast(decoded_image.data).ptr, }; // Store the image @@ -2323,37 +2327,23 @@ pub fn prepBackgroundImage(self: *Metal) !void { /// Reads the content of the given image path and returns it pub fn readImageContent(self: *Metal, path: []const u8) ![]u8 { - assert(std.fs.path.isAbsolute(path)); // Open the file var file = std.fs.openFileAbsolute(path, .{}) catch |err| { - log.warn("failed to open file: {}", .{err}); + log.warn("failed to open file {s}: {}", .{ path, 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(); - reader.readAllArrayList(&managed, max_image_size) catch |err| { + const image_content = reader.readAllAlloc(self.alloc, max_image_size) catch |err| { log.warn("failed to read file: {}", .{err}); return error.InvalidData; }; - return managed.toOwnedSlice(); + return image_content; } /// Update the configuration. @@ -2392,9 +2382,10 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { // Reset current background image self.background_image = config.background_image; - self.uniforms.has_bg_image = (config.background_image.value != null); + self.uniforms.has_bg_image = (config.background_image != null); self.uniforms.bg_image_opacity = config.background_image_opacity; self.background_image_mode = config.background_image_mode; + self.background_image_position = config.background_image_position; if (self.current_background_image) |*img| { img.markForUnload(); } @@ -2408,7 +2399,7 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { CATransaction.msgSend(void, "begin", .{}); defer CATransaction.msgSend(void, "commit", .{}); - self.layer.setProperty("opaque", config.background_opacity >= 1 and config.background_image.value == null); + self.layer.setProperty("opaque", config.background_opacity >= 1 and config.background_image == null); self.layer.setProperty("displaySyncEnabled", config.vsync); } diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index f9322bfe5..2d92fae50 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -117,6 +117,7 @@ pub const Image = extern struct { pub const BgImage = extern struct { terminal_size: [2]f32, mode: configpkg.BackgroundImageMode, + position_index: configpkg.BackgroundImagePosition, }; /// The uniforms that are passed to the terminal cell shader. diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index a99e6cf28..03814aa50 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -685,20 +685,17 @@ fragment float4 image_fragment( #pragma mark - BG Image Shader enum BgImageMode : uint8_t { - MODE_ZOOMED = 0u, - MODE_STRETCHED = 1u, - MODE_CROPPED = 2u, + MODE_CONTAIN = 0u, + MODE_FILL = 1u, + MODE_COVER = 2u, MODE_TILED = 3u, - MODE_CENTERED = 4u, - MODE_UPPER_LEFT = 5u, - MODE_UPPER_RIGHT = 6u, - MODE_LOWER_LEFT = 7u, - MODE_LOWER_RIGHT = 8u, + MODE_NONE = 4u, }; struct BgImageVertexIn { float2 terminal_size [[attribute(0)]]; uint8_t mode [[attribute(1)]]; + uint8_t position_index [[attribute(2)]]; }; struct BgImageVertexOut { @@ -709,7 +706,7 @@ struct BgImageVertexOut { vertex BgImageVertexOut bg_image_vertex( uint vid [[vertex_id]], BgImageVertexIn in [[stage_in]], - texture2d image [[texture(0)]], + texture2d image [[texture(0)]], constant Uniforms& uniforms [[buffer(1)]] ) { BgImageVertexOut out; @@ -732,7 +729,7 @@ vertex BgImageVertexOut bg_image_vertex( ); switch (in.mode) { - case MODE_ZOOMED: { + case MODE_CONTAIN: { // Scale to fit the terminal size if (aspect_ratio.x > aspect_ratio.y) { scale.x = aspect_ratio.y / aspect_ratio.x; @@ -741,7 +738,7 @@ vertex BgImageVertexOut bg_image_vertex( } break; } - case MODE_CROPPED: { + case MODE_COVER: { // Scale to fit the terminal size if (aspect_ratio.x < aspect_ratio.y) { scale.x = aspect_ratio.y / aspect_ratio.x; @@ -750,17 +747,13 @@ vertex BgImageVertexOut bg_image_vertex( } break; } - case MODE_CENTERED: - case MODE_UPPER_LEFT: - case MODE_UPPER_RIGHT: - case MODE_LOWER_LEFT: - case MODE_LOWER_RIGHT: { + case MODE_NONE: { // Scale to match the actual size of the image scale.x = image_size.x / in.terminal_size.x; scale.y = image_size.y / in.terminal_size.y; break; } - case MODE_STRETCHED: + case MODE_FILL: case MODE_TILED: // No adjustments needed break; @@ -768,28 +761,9 @@ vertex BgImageVertexOut bg_image_vertex( float2 final_image_size = in.terminal_size * position * scale; - float2 offset = float2(0.0, 0.0); - switch (in.mode) { - case MODE_ZOOMED: - case MODE_CROPPED: - case MODE_STRETCHED: - case MODE_TILED: - case MODE_CENTERED: - offset = (in.terminal_size * (1.0 - scale)) / 2.0; - break; - case MODE_UPPER_LEFT: - offset = float2(0.0, 0.0); - break; - case MODE_UPPER_RIGHT: - offset = float2(in.terminal_size.x - image_size.x, 0.0); - break; - case MODE_LOWER_LEFT: - offset = float2(0.0, in.terminal_size.y - image_size.y); - break; - case MODE_LOWER_RIGHT: - offset = float2(in.terminal_size.x - image_size.x, in.terminal_size.y - image_size.y); - break; - } + uint y_pos = in.position_index / 3u; // 0 = top, 1 = center, 2 = bottom + uint x_pos = in.position_index % 3u; // 0 = left, 1 = center, 2 = right + float2 offset = float2(x_pos, y_pos) * (in.terminal_size) * (1.0 - scale) / 2.0; out.position = uniforms.projection_matrix * float4(final_image_size + offset, 0.0, 1.0); out.tex_coord = position; @@ -802,12 +776,15 @@ vertex BgImageVertexOut bg_image_vertex( fragment float4 bg_image_fragment( BgImageVertexOut in [[stage_in]], - texture2d image [[texture(0)]], + texture2d image [[texture(0)]], constant Uniforms& uniforms [[buffer(1)]] ) { constexpr sampler textureSampler(address::repeat, filter::linear); float2 norm_coord = fract(in.tex_coord); - float4 color = float4(image.sample(textureSampler, norm_coord)) / 255.0f; + float4 color = image.sample(textureSampler, norm_coord); + if (!uniforms.use_linear_blending) { + color = unlinearize(color); + } return float4(color.rgb * color.a * uniforms.bg_image_opacity, color.a * uniforms.bg_image_opacity); }