mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Add new background image modes
This commit is contained in:
@ -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");
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user