Add new background image modes

This commit is contained in:
yunusey
2025-01-02 15:43:50 -05:00
parent cf52578067
commit ad5b90a2ea
7 changed files with 202 additions and 69 deletions

View File

@ -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");

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;