@@ -7,6 +7,7 @@ pub const Metal = @This();
77const std = @import ("std" );
88const builtin = @import ("builtin" );
99const glfw = @import ("glfw" );
10+ const wuffs = @import ("wuffs" );
1011const objc = @import ("objc" );
1112const macos = @import ("macos" );
1213const imgui = @import ("imgui" );
@@ -59,6 +60,9 @@ const glfwNative = glfw.Native(.{
5960
6061const log = std .log .scoped (.metal );
6162
63+ /// The maximum size of a background image.
64+ const max_image_size = 400 * 1024 * 1024 ; // 400MB
65+
6266/// Allocator that can be used
6367alloc : std.mem.Allocator ,
6468
@@ -130,6 +134,16 @@ font_grid: *font.SharedGrid,
130134font_shaper : font.Shaper ,
131135font_shaper_cache : font.ShaperCache ,
132136
137+ /// The background image(s) to draw. Currently, we always draw the last image.
138+ background_image : configpkg.SinglePath ,
139+
140+ /// The background image mode to use.
141+ background_image_mode : configpkg.BackgroundImageMode ,
142+
143+ /// The current background image to draw. If it is null, then we will not
144+ /// draw any background image.
145+ current_background_image : ? Image = null ,
146+
133147/// The images that we may render.
134148images : ImageMap = .{},
135149image_placements : ImagePlacementList = .{},
@@ -380,6 +394,9 @@ pub const DerivedConfig = struct {
380394 cursor_text : ? terminal.color.RGB ,
381395 background : terminal.color.RGB ,
382396 background_opacity : f64 ,
397+ background_image : configpkg.SinglePath ,
398+ background_image_opacity : f32 ,
399+ background_image_mode : configpkg.BackgroundImageMode ,
383400 foreground : terminal.color.RGB ,
384401 selection_background : ? terminal.color.RGB ,
385402 selection_foreground : ? terminal.color.RGB ,
@@ -404,6 +421,9 @@ pub const DerivedConfig = struct {
404421 // Copy our shaders
405422 const custom_shaders = try config .@"custom-shader" .clone (alloc );
406423
424+ // Copy our background image
425+ const background_image = try config .@"background-image" .clone (alloc );
426+
407427 // Copy our font features
408428 const font_features = try config .@"font-feature" .clone (alloc );
409429
@@ -444,6 +464,11 @@ pub const DerivedConfig = struct {
444464
445465 .background = config .background .toTerminalRGB (),
446466 .foreground = config .foreground .toTerminalRGB (),
467+
468+ .background_image = background_image ,
469+ .background_image_opacity = config .@"background-image-opacity" ,
470+ .background_image_mode = config .@"background-image-mode" ,
471+
447472 .invert_selection_fg_bg = config .@"selection-invert-fg-bg" ,
448473 .bold_is_bright = config .@"bold-is-bright" ,
449474 .min_contrast = @floatCast (config .@"minimum-contrast" ),
@@ -643,6 +668,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
643668 .default_foreground_color = options .config .foreground ,
644669 .background_color = null ,
645670 .default_background_color = options .config .background ,
671+ .background_image = options .config .background_image ,
672+ .background_image_mode = options .config .background_image_mode ,
646673 .cursor_color = null ,
647674 .default_cursor_color = options .config .cursor_color ,
648675 .cursor_invert = options .config .cursor_invert ,
@@ -668,6 +695,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
668695 .use_display_p3 = options .config .colorspace == .@"display-p3" ,
669696 .use_linear_blending = options .config .blending .isLinear (),
670697 .use_experimental_linear_correction = options .config .blending == .@"linear-corrected" ,
698+ .has_bg_image = (options .config .background_image .value != null ),
699+ .bg_image_opacity = options .config .background_image_opacity ,
671700 },
672701
673702 // Fonts
@@ -1103,6 +1132,17 @@ pub fn updateFrame(
11031132 try self .prepKittyGraphics (state .terminal );
11041133 }
11051134
1135+ if (self .current_background_image == null and
1136+ self .background_image .value != null )
1137+ {
1138+ self .prepBackgroundImage () catch | err | switch (err ) {
1139+ error .InvalidData = > {
1140+ log .warn ("invalid image data, skipping" , .{});
1141+ },
1142+ else = > return err ,
1143+ };
1144+ }
1145+
11061146 // If we have any terminal dirty flags set then we need to rebuild
11071147 // the entire screen. This can be optimized in the future.
11081148 const full_rebuild : bool = rebuild : {
@@ -1192,6 +1232,7 @@ pub fn updateFrame(
11921232 // TODO: Is this expensive? Should we be checking if our
11931233 // bg color has changed first before doing this work?
11941234 {
1235+ std .log .info ("Updating background color to {}" , .{critical .bg });
11951236 const color = graphics .c .CGColorCreate (
11961237 @ptrCast (self .terminal_colorspace ),
11971238 &[4 ]f64 {
@@ -1240,6 +1281,31 @@ pub fn updateFrame(
12401281 }
12411282 }
12421283 }
1284+
1285+ // Check if we need to update our current background image
1286+ if (self .current_background_image ) | current_background_image | {
1287+ switch (current_background_image ) {
1288+ .ready = > {},
1289+
1290+ .pending_gray ,
1291+ .pending_gray_alpha ,
1292+ .pending_rgb ,
1293+ .pending_rgba ,
1294+ .replace_gray ,
1295+ .replace_gray_alpha ,
1296+ .replace_rgb ,
1297+ .replace_rgba ,
1298+ = > try self .current_background_image .? .upload (self .alloc , self .gpu_state .device ),
1299+
1300+ .unload_pending ,
1301+ .unload_replace ,
1302+ .unload_ready ,
1303+ = > {
1304+ self .current_background_image .? .deinit (self .alloc );
1305+ self .current_background_image = null ;
1306+ },
1307+ }
1308+ }
12431309}
12441310
12451311/// Draw the frame to the screen.
@@ -1351,7 +1417,10 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
13511417 );
13521418 defer encoder .msgSend (void , objc .sel ("endEncoding" ), .{});
13531419
1354- // Draw background images first
1420+ // Draw background image set by the user first
1421+ try self .drawBackgroundImage (encoder , frame );
1422+
1423+ // Then draw background images
13551424 try self .drawImagePlacements (encoder , frame , self .image_placements .items [0.. self .image_bg_end ]);
13561425
13571426 // Then draw background cells
@@ -1554,6 +1623,92 @@ fn drawPostShader(
15541623 );
15551624}
15561625
1626+ fn drawBackgroundImage (
1627+ self : * Metal ,
1628+ encoder : objc.Object ,
1629+ frame : * const FrameState ,
1630+ ) ! void {
1631+ // If we don't have a background image, just return
1632+ const current_background_image = self .current_background_image orelse return ;
1633+
1634+ // Use our background image shader pipeline
1635+ encoder .msgSend (
1636+ void ,
1637+ objc .sel ("setRenderPipelineState:" ),
1638+ .{self .shaders .bg_image_pipeline .value },
1639+ );
1640+
1641+ // Set our uniforms
1642+ encoder .msgSend (
1643+ void ,
1644+ objc .sel ("setVertexBuffer:offset:atIndex:" ),
1645+ .{ frame .uniforms .buffer .value , @as (c_ulong , 0 ), @as (c_ulong , 1 ) },
1646+ );
1647+ encoder .msgSend (
1648+ void ,
1649+ objc .sel ("setFragmentBuffer:offset:atIndex:" ),
1650+ .{ frame .uniforms .buffer .value , @as (c_ulong , 0 ), @as (c_ulong , 1 ) },
1651+ );
1652+
1653+ // Get the texture
1654+ const texture = switch (current_background_image ) {
1655+ .ready = > | t | t ,
1656+ else = > {
1657+ return ;
1658+ },
1659+ };
1660+
1661+ // Create our vertex buffer, which is always exactly one item.
1662+ const Buffer = mtl_buffer .Buffer (mtl_shaders .BgImage );
1663+ var buf = try Buffer .initFill (self .gpu_state .device , &.{.{
1664+ .terminal_size = .{
1665+ @as (f32 , @floatFromInt (self .size .terminal ().width )),
1666+ @as (f32 , @floatFromInt (self .size .terminal ().height )),
1667+ },
1668+ .mode = self .background_image_mode ,
1669+ }});
1670+ defer buf .deinit ();
1671+
1672+ // Set our buffer
1673+ encoder .msgSend (
1674+ void ,
1675+ objc .sel ("setVertexBuffer:offset:atIndex:" ),
1676+ .{ buf .buffer .value , @as (c_ulong , 0 ), @as (c_ulong , 0 ) },
1677+ );
1678+
1679+ // Set our texture
1680+ encoder .msgSend (
1681+ void ,
1682+ objc .sel ("setVertexTexture:atIndex:" ),
1683+ .{
1684+ texture .value ,
1685+ @as (c_ulong , 0 ),
1686+ },
1687+ );
1688+ encoder .msgSend (
1689+ void ,
1690+ objc .sel ("setFragmentTexture:atIndex:" ),
1691+ .{
1692+ texture .value ,
1693+ @as (c_ulong , 0 ),
1694+ },
1695+ );
1696+
1697+ // Draw!
1698+ encoder .msgSend (
1699+ void ,
1700+ objc .sel ("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:" ),
1701+ .{
1702+ @intFromEnum (mtl .MTLPrimitiveType .triangle ),
1703+ @as (c_ulong , 6 ),
1704+ @intFromEnum (mtl .MTLIndexType .uint16 ),
1705+ self .gpu_state .instance .buffer .value ,
1706+ @as (c_ulong , 0 ),
1707+ @as (c_ulong , 1 ),
1708+ },
1709+ );
1710+ }
1711+
15571712fn drawImagePlacements (
15581713 self : * Metal ,
15591714 encoder : objc.Object ,
@@ -2086,6 +2241,82 @@ fn prepKittyImage(
20862241 gop .value_ptr .transmit_time = image .transmit_time ;
20872242}
20882243
2244+ /// Prepares the current background image for upload
2245+ pub fn prepBackgroundImage (self : * Metal ) ! void {
2246+ // If the user doesn't have a background image, do nothing...
2247+ const path = self .background_image .value orelse return ;
2248+
2249+ // Read the file content
2250+ const file_content = try self .readImageContent (path );
2251+ defer self .alloc .free (file_content );
2252+
2253+ // Decode the image
2254+ const decoded_image : wuffs.ImageData = blk : {
2255+ // Extract the file extension
2256+ const ext = std .fs .path .extension (path );
2257+ const ext_lower = try std .ascii .allocLowerString (self .alloc , ext );
2258+ defer self .alloc .free (ext_lower );
2259+
2260+ // Match based on extension
2261+ if (std .mem .eql (u8 , ext_lower , ".png" )) {
2262+ break :blk try wuffs .png .decode (self .alloc , file_content );
2263+ } else if (std .mem .eql (u8 , ext_lower , ".jpg" ) or std .mem .eql (u8 , ext_lower , ".jpeg" )) {
2264+ break :blk try wuffs .jpeg .decode (self .alloc , file_content );
2265+ } else {
2266+ log .warn ("unsupported image format: {s}" , .{ext });
2267+ return error .InvalidData ;
2268+ }
2269+ };
2270+ defer self .alloc .free (decoded_image .data );
2271+
2272+ // Copy the data into the pending state
2273+ const data = try self .alloc .dupe (u8 , decoded_image .data );
2274+ errdefer self .alloc .free (data );
2275+ const pending : Image.Pending = .{
2276+ .width = decoded_image .width ,
2277+ .height = decoded_image .height ,
2278+ .data = data .ptr ,
2279+ };
2280+
2281+ // Store the image
2282+ self .current_background_image = .{ .pending_rgba = pending };
2283+ }
2284+
2285+ /// Reads the content of the given image path and returns it
2286+ pub fn readImageContent (self : * Metal , path : []const u8 ) ! []u8 {
2287+ assert (std .fs .path .isAbsolute (path ));
2288+ // Open the file
2289+ var file = std .fs .openFileAbsolute (path , .{}) catch | err | {
2290+ log .warn ("failed to open file: {}" , .{err });
2291+ return error .InvalidData ;
2292+ };
2293+ defer file .close ();
2294+
2295+ // File must be a regular file
2296+ if (file .stat ()) | stat | {
2297+ if (stat .kind != .file ) {
2298+ log .warn ("file is not a regular file kind={}" , .{stat .kind });
2299+ return error .InvalidData ;
2300+ }
2301+ } else | err | {
2302+ log .warn ("failed to stat file: {}" , .{err });
2303+ return error .InvalidData ;
2304+ }
2305+
2306+ var buf_reader = std .io .bufferedReader (file .reader ());
2307+ const reader = buf_reader .reader ();
2308+
2309+ // Read the file
2310+ var managed = std .ArrayList (u8 ).init (self .alloc );
2311+ errdefer managed .deinit ();
2312+ reader .readAllArrayList (& managed , max_image_size ) catch | err | {
2313+ log .warn ("failed to read file: {}" , .{err });
2314+ return error .InvalidData ;
2315+ };
2316+
2317+ return managed .toOwnedSlice ();
2318+ }
2319+
20892320/// Update the configuration.
20902321pub fn changeConfig (self : * Metal , config : * DerivedConfig ) ! void {
20912322 // We always redo the font shaper in case font features changed. We
@@ -2120,6 +2351,15 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
21202351 self .default_cursor_color = if (! config .cursor_invert ) config .cursor_color else null ;
21212352 self .cursor_invert = config .cursor_invert ;
21222353
2354+ // Reset current background image
2355+ self .background_image = config .background_image ;
2356+ self .uniforms .has_bg_image = (config .background_image .value != null );
2357+ self .uniforms .bg_image_opacity = config .background_image_opacity ;
2358+ self .background_image_mode = config .background_image_mode ;
2359+ if (self .current_background_image ) | * img | {
2360+ img .markForUnload ();
2361+ }
2362+
21232363 // Update our layer's opaqueness and display sync in case they changed.
21242364 {
21252365 // We use a CATransaction so that Core Animation knows that we
@@ -2256,6 +2496,8 @@ pub fn setScreenSize(
22562496 .use_display_p3 = old .use_display_p3 ,
22572497 .use_linear_blending = old .use_linear_blending ,
22582498 .use_experimental_linear_correction = old .use_experimental_linear_correction ,
2499+ .has_bg_image = old .has_bg_image ,
2500+ .bg_image_opacity = old .bg_image_opacity ,
22592501 };
22602502
22612503 // Reset our cell contents if our grid size has changed.
@@ -2663,7 +2905,7 @@ fn rebuildCells(
26632905 const bg_alpha : u8 = bg_alpha : {
26642906 const default : u8 = 255 ;
26652907
2666- if (self .config .background_opacity >= 1 ) break :bg_alpha default ;
2908+ if (self .current_background_image == null and self . config .background_opacity >= 1 ) break :bg_alpha default ;
26672909
26682910 // Cells that are selected should be fully opaque.
26692911 if (selected ) break :bg_alpha default ;
@@ -2677,6 +2919,11 @@ fn rebuildCells(
26772919 break :bg_alpha default ;
26782920 }
26792921
2922+ // If we have a background image, use the configured background image opacity.
2923+ if (self .current_background_image != null ) {
2924+ break :bg_alpha @intFromFloat (@round ((1 - self .config .background_image_opacity ) * 255.0 ));
2925+ }
2926+
26802927 // Otherwise, we use the configured background opacity.
26812928 break :bg_alpha @intFromFloat (@round (self .config .background_opacity * 255.0 ));
26822929 };
0 commit comments