Move Wallpaper code into Wallpaper.zig
The Wallpaper used to (mostly) live inside of Output. This moves that into a new Wallpaper.zig file with a similar structure to that of Bar. The code from WallpaperImage.zig is not in Wallpaper.Image
This commit is contained in:
parent
167141ef15
commit
ce01eeefe2
4 changed files with 305 additions and 306 deletions
|
|
@ -28,7 +28,7 @@ buffer_pool: BufferPool = .{},
|
|||
|
||||
/// Holds a pixman.Image (and its raw pixels) for the wallpaper
|
||||
/// (same image on all outputs, but scaled separately)
|
||||
wallpaper_image: ?*WallpaperImage,
|
||||
wallpaper_image: ?*Wallpaper.Image,
|
||||
|
||||
// WM Configuration
|
||||
config: *Config,
|
||||
|
|
@ -238,14 +238,16 @@ pub fn manage(context: *Context) void {
|
|||
var out_it = context.wm.outputs.iterator(.forward);
|
||||
while (out_it.next()) |output| {
|
||||
if (context.wallpaper_image == null) {
|
||||
output.deinitWallpaperLayerSurface();
|
||||
} else if (output.surfaces != null) {
|
||||
output.renderWallpaper() catch |err| {
|
||||
if (output.wallpaper) |*wp| wp.deinit();
|
||||
output.wallpaper = null;
|
||||
} else if (output.wallpaper) |*wp| {
|
||||
wp.render() catch |err| {
|
||||
log.err("Wallpaper re-render failed: {}", .{err});
|
||||
};
|
||||
} else {
|
||||
output.initWallpaperLayerSurface() catch |err| {
|
||||
output.wallpaper = Wallpaper.init(context, output) catch |err| {
|
||||
log.err("Failed to init wallpaper surface: {}", .{err});
|
||||
continue;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -259,10 +261,10 @@ pub fn manage(context: *Context) void {
|
|||
}
|
||||
}
|
||||
|
||||
fn loadWallpaperImage(config: *Config) ?*WallpaperImage {
|
||||
fn loadWallpaperImage(config: *Config) ?*Wallpaper.Image {
|
||||
const image_path = config.wallpaper_image_path orelse return null;
|
||||
if (image_path.len == 0) return null;
|
||||
return WallpaperImage.create(image_path) catch |e| {
|
||||
return Wallpaper.Image.create(image_path) catch |e| {
|
||||
log.err("Failed to load wallpaper image from path \"{s}\": {s}", .{ image_path, @errorName(e) });
|
||||
return null;
|
||||
};
|
||||
|
|
@ -291,7 +293,7 @@ const Config = @import("Config.zig");
|
|||
const InputManager = @import("InputManager.zig");
|
||||
const Output = @import("Output.zig");
|
||||
const TagOverlay = @import("TagOverlay.zig");
|
||||
const WallpaperImage = @import("WallpaperImage.zig");
|
||||
const Wallpaper = @import("Wallpaper.zig");
|
||||
const WindowManager = @import("WindowManager.zig");
|
||||
const XkbBindings = @import("XkbBindings.zig");
|
||||
|
||||
|
|
|
|||
223
src/Output.zig
223
src/Output.zig
|
|
@ -26,15 +26,7 @@ geometry: Rect = .{},
|
|||
// work with beansprout.
|
||||
usable_geometry: Rect = .{},
|
||||
|
||||
// Information for this Output's wallpaper
|
||||
wallpaper_render_scale: u31 = 0,
|
||||
wallpaper_render_width: u31 = 0,
|
||||
wallpaper_render_height: u31 = 0,
|
||||
|
||||
surfaces: ?struct {
|
||||
wl_surface: *wl.Surface,
|
||||
layer_surface: *zwlr.LayerSurfaceV1,
|
||||
} = null,
|
||||
wallpaper: ?Wallpaper = null,
|
||||
|
||||
bar: ?Bar,
|
||||
tag_overlay: ?TagOverlay,
|
||||
|
|
@ -59,9 +51,6 @@ tags: u32 = 0x0001,
|
|||
/// State consumed in manage() phase, reset at end of manage().
|
||||
pending_manage: PendingManage = .{},
|
||||
|
||||
/// Used for wallpaper rendering management
|
||||
configured: bool = false,
|
||||
|
||||
windows: wl.list.Head(Window, .link),
|
||||
|
||||
link: wl.list.Link,
|
||||
|
|
@ -134,7 +123,7 @@ pub fn destroy(output: *Output) void {
|
|||
// Deinit optional surfaces
|
||||
if (output.bar) |*bar| bar.deinit();
|
||||
if (output.tag_overlay) |*tag_overlay| tag_overlay.deinit();
|
||||
output.deinitWallpaperLayerSurface();
|
||||
if (output.wallpaper) |*wp| wp.deinit();
|
||||
|
||||
// Destroy/deinit other Output fields
|
||||
output.tag_layout_overrides.deinit(utils.gpa);
|
||||
|
|
@ -270,11 +259,13 @@ fn riverOutputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.E
|
|||
fn wlOutputListener(_: *wl.Output, event: wl.Output.Event, output: *Output) void {
|
||||
switch (event) {
|
||||
.done => {
|
||||
output.initWallpaperLayerSurface() catch |err| {
|
||||
const output_name = output.name orelse "some output";
|
||||
log.err("failed to add a surface to {s}: {}", .{ output_name, err });
|
||||
return;
|
||||
};
|
||||
if (output.context.wallpaper_image != null and output.wallpaper == null) {
|
||||
output.wallpaper = Wallpaper.init(output.context, output) catch |err| {
|
||||
const output_name = output.name orelse "some output";
|
||||
log.err("failed to add a wallpaper surface to {s}: {}", .{ output_name, err });
|
||||
return;
|
||||
};
|
||||
}
|
||||
if (output.bar) |*bar| {
|
||||
// Trigger a full manage cycle if the scale changed so that
|
||||
// fonts are reloaded and bar geometry is recalculated.
|
||||
|
|
@ -283,10 +274,12 @@ fn wlOutputListener(_: *wl.Output, event: wl.Output.Event, output: *Output) void
|
|||
}
|
||||
}
|
||||
// Re-render wallpaper if scale changed
|
||||
if (output.configured and output.scale != output.wallpaper_render_scale) {
|
||||
output.renderWallpaper() catch |err| {
|
||||
log.err("Wallpaper render failed: {}", .{err});
|
||||
};
|
||||
if (output.wallpaper) |*wp| {
|
||||
if (wp.configured and output.scale != wp.render_scale) {
|
||||
wp.render() catch |err| {
|
||||
log.err("Wallpaper render failed: {}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
.scale => |ev| {
|
||||
|
|
@ -305,189 +298,6 @@ fn wlOutputListener(_: *wl.Output, event: wl.Output.Event, output: *Output) void
|
|||
}
|
||||
}
|
||||
|
||||
pub fn initWallpaperLayerSurface(output: *Output) !void {
|
||||
if (output.context.wallpaper_image == null) {
|
||||
// No wallpaper image, so we don't need any surfaces
|
||||
return;
|
||||
}
|
||||
|
||||
if (output.surfaces) |_| {
|
||||
// This output already has a surface, we can exit early
|
||||
return;
|
||||
}
|
||||
|
||||
const context = output.context;
|
||||
|
||||
const wl_surface = try context.wl_compositor.createSurface();
|
||||
errdefer wl_surface.destroy();
|
||||
|
||||
const layer_surface = try context.zwlr_layer_shell_v1.getLayerSurface(wl_surface, output.wl_output, .background, "beansprout-wallpaper");
|
||||
errdefer layer_surface.destroy();
|
||||
|
||||
// We don't want our surface to have any input region (default is infinite)
|
||||
const empty_region = try context.wl_compositor.createRegion();
|
||||
defer empty_region.destroy();
|
||||
wl_surface.setInputRegion(empty_region);
|
||||
|
||||
// Full surface should be opaque
|
||||
const opaque_region = try context.wl_compositor.createRegion();
|
||||
opaque_region.add(0, 0, output.geometry.width, output.geometry.height);
|
||||
defer opaque_region.destroy();
|
||||
wl_surface.setOpaqueRegion(opaque_region);
|
||||
|
||||
layer_surface.setExclusiveZone(-1);
|
||||
layer_surface.setAnchor(.{ .top = true, .right = true, .bottom = true, .left = true });
|
||||
|
||||
output.surfaces = .{
|
||||
.wl_surface = wl_surface,
|
||||
.layer_surface = layer_surface,
|
||||
};
|
||||
context.buffer_pool.surface_count += 1;
|
||||
|
||||
layer_surface.setListener(*Output, wallpaperLayerSurfaceListener, output);
|
||||
wl_surface.commit();
|
||||
}
|
||||
|
||||
pub fn deinitWallpaperLayerSurface(output: *Output) void {
|
||||
if (output.surfaces) |surfaces| {
|
||||
surfaces.layer_surface.destroy();
|
||||
surfaces.wl_surface.destroy();
|
||||
output.context.buffer_pool.surface_count -= 1;
|
||||
}
|
||||
|
||||
output.surfaces = null;
|
||||
output.configured = false;
|
||||
}
|
||||
|
||||
fn wallpaperLayerSurfaceListener(layer_surface: *zwlr.LayerSurfaceV1, event: zwlr.LayerSurfaceV1.Event, output: *Output) void {
|
||||
switch (event) {
|
||||
.configure => |ev| {
|
||||
layer_surface.ackConfigure(ev.serial);
|
||||
|
||||
const width: u31 = @intCast(ev.width);
|
||||
const height: u31 = @intCast(ev.height);
|
||||
|
||||
if (output.configured and
|
||||
output.wallpaper_render_width == width and
|
||||
output.wallpaper_render_height == height and
|
||||
output.scale == output.wallpaper_render_scale)
|
||||
{
|
||||
if (output.surfaces) |surfaces| {
|
||||
surfaces.wl_surface.commit();
|
||||
} else {
|
||||
log.warn("Output is marked as configured but is missing its surfaces.", .{});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("configuring wallpaper surface with width {} and height {}", .{ width, height });
|
||||
output.wallpaper_render_width = width;
|
||||
output.wallpaper_render_height = height;
|
||||
output.configured = true;
|
||||
|
||||
output.renderWallpaper() catch |err| {
|
||||
log.err("Wallpaper render failed: {}", .{err});
|
||||
};
|
||||
},
|
||||
.closed => {
|
||||
output.deinitWallpaperLayerSurface();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates image_dimension / (output_dimension * scale)
|
||||
fn calculateScale(image_dimension: c_int, output_dimension: u31, scale: u31) f64 {
|
||||
const numerator: f64 = @floatFromInt(image_dimension);
|
||||
const denominator: f64 = @floatFromInt(output_dimension * scale);
|
||||
|
||||
return numerator / denominator;
|
||||
}
|
||||
|
||||
/// Calculates (image_dimension / dimension_scale - output_dimension) / 2 / dimension_scale
|
||||
fn calculateTransform(image_dimension: c_int, output_dimension: u31, dimension_scale: f64) f64 {
|
||||
const numerator1: f64 = @floatFromInt(image_dimension);
|
||||
const denominator1: f64 = dimension_scale;
|
||||
const subtrahend: f64 = @floatFromInt(output_dimension);
|
||||
const numerator2: f64 = numerator1 / denominator1 - subtrahend;
|
||||
|
||||
return numerator2 / 2 / dimension_scale;
|
||||
}
|
||||
|
||||
/// Render the wallpaper image onto the layer surface
|
||||
pub fn renderWallpaper(output: *Output) !void {
|
||||
const context = output.context;
|
||||
const width = output.wallpaper_render_width;
|
||||
const height = output.wallpaper_render_height;
|
||||
const scale = output.scale;
|
||||
|
||||
// Don't have anything to render
|
||||
if (width == 0 or height == 0 or scale == 0) {
|
||||
return;
|
||||
}
|
||||
// Scale our loaded image and then copy it into the Buffer's pixman.Image
|
||||
const wallpaper_image = context.wallpaper_image orelse return error.MissingWallpaperImage;
|
||||
const image = wallpaper_image.pix_image;
|
||||
const image_data = image.getData();
|
||||
const image_width = image.getWidth();
|
||||
const image_height = image.getHeight();
|
||||
const image_stride = image.getStride();
|
||||
const image_format = image.getFormat();
|
||||
|
||||
const buffer = try context.buffer_pool.nextBuffer(context.wl_shm, width * scale, height * scale);
|
||||
|
||||
const pix = pixman.Image.createBitsNoClear(image_format, image_width, image_height, image_data, image_stride) orelse {
|
||||
log.err("Failed to copy the wallpaper image for rendering", .{});
|
||||
return error.FailedToCreatePixmanImage;
|
||||
};
|
||||
defer _ = pix.unref();
|
||||
|
||||
// Calculate image scale
|
||||
var sx: f64 = @as(f64, @floatFromInt(image_width)) / @as(f64, @floatFromInt(width * scale));
|
||||
var sy: f64 = calculateScale(image_height, height, scale);
|
||||
|
||||
const s = if (sx > sy) sy else sx;
|
||||
sx = s;
|
||||
sy = s;
|
||||
|
||||
// Calculate translation offsets to center the image on the output.
|
||||
// If the scaled image is larger than the output, the offset crops equally from both sides.
|
||||
const tx: f64 = calculateTransform(image_width, width * scale, sx);
|
||||
const ty: f64 = calculateTransform(image_height, height * scale, sy);
|
||||
|
||||
// Build a combined source-to-destination transform matrix.
|
||||
// Pixman transforms map destination pixels back to source pixels, so:
|
||||
// t_scale: maps a destination pixel to the corresponding source pixel (scaling)
|
||||
// t_trans: shifts the sampling point to center the image
|
||||
// t = t_trans * t_scale: first scale, then translate (in source space)
|
||||
var t_scale: pixman.FTransform = undefined;
|
||||
var t_trans: pixman.FTransform = undefined;
|
||||
var t: pixman.FTransform = undefined;
|
||||
// t2 is the fixed-point version of t, which is what pixman actually uses internally
|
||||
var t2: pixman.Transform = undefined;
|
||||
|
||||
pixman.FTransform.initScale(&t_scale, sx, sy);
|
||||
pixman.FTransform.initTranslate(&t_trans, tx, ty);
|
||||
pixman.FTransform.multiply(&t, &t_trans, &t_scale);
|
||||
_ = pixman.Transform.fromFTransform(&t2, &t);
|
||||
_ = pix.setTransform(&t2);
|
||||
_ = pix.setFilter(.best, &[_]pixman.Fixed{}, 0);
|
||||
|
||||
// Combine the transformed source image into the buffer.
|
||||
pixman.Image.composite32(.src, pix, null, buffer.pixman_image, 0, 0, 0, 0, 0, 0, width * scale, height * scale);
|
||||
|
||||
log.info("render: {}x{} (scaled from {}x{})", .{ width * scale, height * scale, image_width, image_height });
|
||||
|
||||
// Attach the buffer to the surface
|
||||
const surfaces = output.surfaces orelse return error.NoSurfaces;
|
||||
const wl_surface = surfaces.wl_surface;
|
||||
wl_surface.setBufferScale(scale);
|
||||
wl_surface.attach(buffer.wl_buffer, 0, 0);
|
||||
wl_surface.damageBuffer(0, 0, width * scale, height * scale);
|
||||
wl_surface.commit();
|
||||
|
||||
output.wallpaper_render_scale = scale;
|
||||
}
|
||||
|
||||
pub fn manage(output: *Output) void {
|
||||
defer output.pending_manage = .{};
|
||||
|
||||
|
|
@ -840,15 +650,14 @@ const wayland = @import("wayland");
|
|||
const wl = wayland.client.wl;
|
||||
const river = wayland.client.river;
|
||||
const zwlr = wayland.client.zwlr;
|
||||
const pixman = @import("pixman");
|
||||
|
||||
const utils = @import("utils.zig");
|
||||
const Rect = utils.Rect;
|
||||
const Bar = @import("Bar.zig");
|
||||
const Buffer = @import("Buffer.zig");
|
||||
const Config = @import("Config.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const TagOverlay = @import("TagOverlay.zig");
|
||||
const Wallpaper = @import("Wallpaper.zig");
|
||||
const Window = @import("Window.zig");
|
||||
|
||||
const log = std.log.scoped(.Output);
|
||||
|
|
|
|||
279
src/Wallpaper.zig
Normal file
279
src/Wallpaper.zig
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
const Wallpaper = @This();
|
||||
|
||||
context: *Context,
|
||||
output: *Output,
|
||||
|
||||
wl_surface: *wl.Surface,
|
||||
layer_surface: *zwlr.LayerSurfaceV1,
|
||||
|
||||
render_scale: u31 = 0,
|
||||
render_width: u31 = 0,
|
||||
render_height: u31 = 0,
|
||||
configured: bool = false,
|
||||
|
||||
/// Decoded image data shared across all outputs.
|
||||
/// Stored on Context; each output's Wallpaper references it for rendering.
|
||||
pub const Image = struct {
|
||||
// This is used as the backing store for the pixman image
|
||||
// It's the actual image (png, jpeg, etc.) decoded into pixels.
|
||||
zigimg_image: zigimg.Image,
|
||||
// Only used on big-endian; holds manually converted ARGB pixel data.
|
||||
// On BE: std.ArrayList(u32), on LE: void
|
||||
argb_pixels: if (native_endian == .big) std.ArrayList(u32) else void = if (native_endian == .big) .empty else {},
|
||||
|
||||
// This is the actual scaled, transformed, and rendered image
|
||||
pix_image: *pixman.Image,
|
||||
|
||||
pub fn create(image_path: []const u8) !*Image {
|
||||
var image = try utils.gpa.create(Image);
|
||||
errdefer utils.gpa.destroy(image);
|
||||
|
||||
var read_buf: [zigimg.io.DEFAULT_BUFFER_SIZE]u8 = undefined;
|
||||
image.zigimg_image = try zigimg.Image.fromFilePath(utils.gpa, image_path, &read_buf);
|
||||
errdefer image.zigimg_image.deinit(utils.gpa);
|
||||
|
||||
// We don't want to deal with all the possible formats,
|
||||
// so let's just convert to one we can use with pixman.
|
||||
if (image.zigimg_image.pixelFormat() != .rgba32) {
|
||||
try image.zigimg_image.convert(utils.gpa, .rgba32);
|
||||
}
|
||||
|
||||
log.debug("image loaded ({}x{})", .{ image.zigimg_image.width, image.zigimg_image.height });
|
||||
|
||||
const pixels = image.zigimg_image.pixels.rgba32;
|
||||
const width: c_int = @intCast(image.zigimg_image.width);
|
||||
const height: c_int = @intCast(image.zigimg_image.height);
|
||||
const stride: c_int = @intCast(image.zigimg_image.width * image.zigimg_image.pixelFormat().pixelStride());
|
||||
|
||||
// zigimg's Rgba32 is an extern struct {r, g, b, a}, which actually matches pixman's a8b8g8r8
|
||||
// (u32 with R at bits 0-7, A at bits 24-31) on little endian machines. That means we can actually
|
||||
// use zigimg's pixel data directly. On big-endian we keep the manual conversion I used to use.
|
||||
switch (native_endian) {
|
||||
.little => {
|
||||
image.pix_image = pixman.Image.createBits(
|
||||
.a8b8g8r8,
|
||||
width,
|
||||
height,
|
||||
@ptrCast(@alignCast(pixels.ptr)),
|
||||
stride,
|
||||
) orelse return error.FailedToCreatePixmanImage;
|
||||
},
|
||||
.big => {
|
||||
image.argb_pixels = try std.ArrayList(u32).initCapacity(utils.gpa, pixels.len);
|
||||
errdefer image.argb_pixels.deinit(utils.gpa);
|
||||
for (pixels) |px| {
|
||||
const a: u32 = px.a;
|
||||
const r: u32 = px.r;
|
||||
const g: u32 = px.g;
|
||||
const b: u32 = px.b;
|
||||
image.argb_pixels.appendAssumeCapacity((a << 24) | (r << 16) | (g << 8) | b);
|
||||
}
|
||||
image.pix_image = pixman.Image.createBits(
|
||||
.a8r8g8b8,
|
||||
width,
|
||||
height,
|
||||
@ptrCast(@alignCast(image.argb_pixels.items.ptr)),
|
||||
stride,
|
||||
) orelse return error.FailedToCreatePixmanImage;
|
||||
},
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
pub fn destroy(image: *Image) void {
|
||||
_ = image.pix_image.unref();
|
||||
if (native_endian == .big) image.argb_pixels.deinit(utils.gpa);
|
||||
image.zigimg_image.deinit(utils.gpa);
|
||||
|
||||
utils.gpa.destroy(image);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(context: *Context, output: *Output) !Wallpaper {
|
||||
const wl_surface = try context.wl_compositor.createSurface();
|
||||
errdefer wl_surface.destroy();
|
||||
|
||||
const layer_surface = try context.zwlr_layer_shell_v1.getLayerSurface(wl_surface, output.wl_output, .background, "beansprout-wallpaper");
|
||||
errdefer layer_surface.destroy();
|
||||
|
||||
// We don't want our surface to have any input region (default is infinite)
|
||||
const empty_region = try context.wl_compositor.createRegion();
|
||||
defer empty_region.destroy();
|
||||
wl_surface.setInputRegion(empty_region);
|
||||
|
||||
// Full surface should be opaque
|
||||
const opaque_region = try context.wl_compositor.createRegion();
|
||||
opaque_region.add(0, 0, output.geometry.width, output.geometry.height);
|
||||
defer opaque_region.destroy();
|
||||
wl_surface.setOpaqueRegion(opaque_region);
|
||||
|
||||
layer_surface.setExclusiveZone(-1);
|
||||
layer_surface.setAnchor(.{ .top = true, .right = true, .bottom = true, .left = true });
|
||||
|
||||
context.buffer_pool.surface_count += 1;
|
||||
|
||||
layer_surface.setListener(*Output, layerSurfaceListener, output);
|
||||
wl_surface.commit();
|
||||
|
||||
return .{
|
||||
.context = context,
|
||||
.output = output,
|
||||
.wl_surface = wl_surface,
|
||||
.layer_surface = layer_surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(wallpaper: *Wallpaper) void {
|
||||
wallpaper.layer_surface.destroy();
|
||||
wallpaper.wl_surface.destroy();
|
||||
wallpaper.context.buffer_pool.surface_count -= 1;
|
||||
}
|
||||
|
||||
fn layerSurfaceListener(layer_surface: *zwlr.LayerSurfaceV1, event: zwlr.LayerSurfaceV1.Event, output: *Output) void {
|
||||
switch (event) {
|
||||
.configure => |ev| {
|
||||
layer_surface.ackConfigure(ev.serial);
|
||||
|
||||
const wallpaper = &(output.wallpaper orelse return);
|
||||
|
||||
const width: u31 = @intCast(ev.width);
|
||||
const height: u31 = @intCast(ev.height);
|
||||
|
||||
if (wallpaper.configured and
|
||||
wallpaper.render_width == width and
|
||||
wallpaper.render_height == height and
|
||||
output.scale == wallpaper.render_scale)
|
||||
{
|
||||
wallpaper.wl_surface.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("configuring wallpaper surface with width {} and height {}", .{ width, height });
|
||||
wallpaper.render_width = width;
|
||||
wallpaper.render_height = height;
|
||||
wallpaper.configured = true;
|
||||
|
||||
wallpaper.render() catch |err| {
|
||||
log.err("Wallpaper render failed: {}", .{err});
|
||||
};
|
||||
},
|
||||
.closed => {
|
||||
if (output.wallpaper) |*wp| wp.deinit();
|
||||
output.wallpaper = null;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates image_dimension / (output_dimension * scale)
|
||||
fn calculateScale(image_dimension: c_int, output_dimension: u31, scale: u31) f64 {
|
||||
const numerator: f64 = @floatFromInt(image_dimension);
|
||||
const denominator: f64 = @floatFromInt(output_dimension * scale);
|
||||
|
||||
return numerator / denominator;
|
||||
}
|
||||
|
||||
/// Calculates (image_dimension / dimension_scale - output_dimension) / 2 / dimension_scale
|
||||
fn calculateTransform(image_dimension: c_int, output_dimension: u31, dimension_scale: f64) f64 {
|
||||
const numerator1: f64 = @floatFromInt(image_dimension);
|
||||
const denominator1: f64 = dimension_scale;
|
||||
const subtrahend: f64 = @floatFromInt(output_dimension);
|
||||
const numerator2: f64 = numerator1 / denominator1 - subtrahend;
|
||||
|
||||
return numerator2 / 2 / dimension_scale;
|
||||
}
|
||||
|
||||
/// Render the wallpaper image onto the layer surface
|
||||
pub fn render(wallpaper: *Wallpaper) !void {
|
||||
const context = wallpaper.context;
|
||||
const output = wallpaper.output;
|
||||
const width = wallpaper.render_width;
|
||||
const height = wallpaper.render_height;
|
||||
const scale = output.scale;
|
||||
|
||||
// Don't have anything to render
|
||||
if (width == 0 or height == 0 or scale == 0) {
|
||||
return;
|
||||
}
|
||||
// Scale our loaded image and then copy it into the Buffer's pixman.Image
|
||||
const wp_image = context.wallpaper_image orelse return error.MissingWallpaperImage;
|
||||
const image = wp_image.pix_image;
|
||||
const image_data = image.getData();
|
||||
const image_width = image.getWidth();
|
||||
const image_height = image.getHeight();
|
||||
const image_stride = image.getStride();
|
||||
const image_format = image.getFormat();
|
||||
|
||||
const buffer = try context.buffer_pool.nextBuffer(context.wl_shm, width * scale, height * scale);
|
||||
|
||||
const pix = pixman.Image.createBitsNoClear(image_format, image_width, image_height, image_data, image_stride) orelse {
|
||||
log.err("Failed to copy the wallpaper image for rendering", .{});
|
||||
return error.FailedToCreatePixmanImage;
|
||||
};
|
||||
defer _ = pix.unref();
|
||||
|
||||
// Calculate image scale
|
||||
var sx: f64 = @as(f64, @floatFromInt(image_width)) / @as(f64, @floatFromInt(width * scale));
|
||||
var sy: f64 = calculateScale(image_height, height, scale);
|
||||
|
||||
const s = if (sx > sy) sy else sx;
|
||||
sx = s;
|
||||
sy = s;
|
||||
|
||||
// Calculate translation offsets to center the image on the output.
|
||||
// If the scaled image is larger than the output, the offset crops equally from both sides.
|
||||
const tx: f64 = calculateTransform(image_width, width * scale, sx);
|
||||
const ty: f64 = calculateTransform(image_height, height * scale, sy);
|
||||
|
||||
// Build a combined source-to-destination transform matrix.
|
||||
// Pixman transforms map destination pixels back to source pixels, so:
|
||||
// t_scale: maps a destination pixel to the corresponding source pixel (scaling)
|
||||
// t_trans: shifts the sampling point to center the image
|
||||
// t = t_trans * t_scale: first scale, then translate (in source space)
|
||||
var t_scale: pixman.FTransform = undefined;
|
||||
var t_trans: pixman.FTransform = undefined;
|
||||
var t: pixman.FTransform = undefined;
|
||||
// t2 is the fixed-point version of t, which is what pixman actually uses internally
|
||||
var t2: pixman.Transform = undefined;
|
||||
|
||||
pixman.FTransform.initScale(&t_scale, sx, sy);
|
||||
pixman.FTransform.initTranslate(&t_trans, tx, ty);
|
||||
pixman.FTransform.multiply(&t, &t_trans, &t_scale);
|
||||
_ = pixman.Transform.fromFTransform(&t2, &t);
|
||||
_ = pix.setTransform(&t2);
|
||||
_ = pix.setFilter(.best, &[_]pixman.Fixed{}, 0);
|
||||
|
||||
// Combine the transformed source image into the buffer.
|
||||
pixman.Image.composite32(.src, pix, null, buffer.pixman_image, 0, 0, 0, 0, 0, 0, width * scale, height * scale);
|
||||
|
||||
log.info("render: {}x{} (scaled from {}x{})", .{ width * scale, height * scale, image_width, image_height });
|
||||
|
||||
// Attach the buffer to the surface
|
||||
const wl_surface = wallpaper.wl_surface;
|
||||
wl_surface.setBufferScale(scale);
|
||||
wl_surface.attach(buffer.wl_buffer, 0, 0);
|
||||
wl_surface.damageBuffer(0, 0, width * scale, height * scale);
|
||||
wl_surface.commit();
|
||||
|
||||
wallpaper.render_scale = scale;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const native_endian = builtin.cpu.arch.endian();
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
const zwlr = wayland.client.zwlr;
|
||||
const pixman = @import("pixman");
|
||||
const zigimg = @import("zigimg");
|
||||
|
||||
const utils = @import("utils.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const Output = @import("Output.zig");
|
||||
|
||||
const log = std.log.scoped(.Wallpaper);
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
const WallpaperImage = @This();
|
||||
|
||||
// This is used as the backing store for the pixman image
|
||||
// It's the actual image (png, jpeg, etc.) decoded into pixels.
|
||||
zigimg_image: zigimg.Image,
|
||||
// Only used on big-endian; holds manually converted ARGB pixel data.
|
||||
// On BE: std.ArrayList(u32), on LE: void
|
||||
argb_pixels: if (native_endian == .big) std.ArrayList(u32) else void = if (native_endian == .big) .empty else {},
|
||||
|
||||
// This is the actual scaled, transformed, and rendered image
|
||||
pix_image: *pixman.Image,
|
||||
|
||||
pub fn create(image_path: []const u8) !*WallpaperImage {
|
||||
var wallpaper_image = try utils.gpa.create(WallpaperImage);
|
||||
errdefer utils.gpa.destroy(wallpaper_image);
|
||||
|
||||
var read_buf: [zigimg.io.DEFAULT_BUFFER_SIZE]u8 = undefined;
|
||||
wallpaper_image.zigimg_image = try zigimg.Image.fromFilePath(utils.gpa, image_path, &read_buf);
|
||||
errdefer wallpaper_image.zigimg_image.deinit(utils.gpa);
|
||||
|
||||
// We don't want to deal with all the possible formats,
|
||||
// so let's just convert to one we can use with pixman.
|
||||
if (wallpaper_image.zigimg_image.pixelFormat() != .rgba32) {
|
||||
try wallpaper_image.zigimg_image.convert(utils.gpa, .rgba32);
|
||||
}
|
||||
|
||||
log.debug("image loaded ({}x{})", .{ wallpaper_image.zigimg_image.width, wallpaper_image.zigimg_image.height });
|
||||
|
||||
const pixels = wallpaper_image.zigimg_image.pixels.rgba32;
|
||||
const width: c_int = @intCast(wallpaper_image.zigimg_image.width);
|
||||
const height: c_int = @intCast(wallpaper_image.zigimg_image.height);
|
||||
const stride: c_int = @intCast(wallpaper_image.zigimg_image.width * wallpaper_image.zigimg_image.pixelFormat().pixelStride());
|
||||
|
||||
// zigimg's Rgba32 is an extern struct {r, g, b, a}, which actually matches pixman's a8b8g8r8
|
||||
// (u32 with R at bits 0-7, A at bits 24-31) on little endian machines. That means we can actually
|
||||
// use zigimg's pixel data directly. On big-endian we keep the manual conversion I used to use.
|
||||
switch (native_endian) {
|
||||
.little => {
|
||||
wallpaper_image.pix_image = pixman.Image.createBits(
|
||||
.a8b8g8r8,
|
||||
width,
|
||||
height,
|
||||
@ptrCast(@alignCast(pixels.ptr)),
|
||||
stride,
|
||||
) orelse return error.FailedToCreatePixmanImage;
|
||||
},
|
||||
.big => {
|
||||
wallpaper_image.argb_pixels = try std.ArrayList(u32).initCapacity(utils.gpa, pixels.len);
|
||||
errdefer wallpaper_image.argb_pixels.deinit(utils.gpa);
|
||||
for (pixels) |px| {
|
||||
const a: u32 = px.a;
|
||||
const r: u32 = px.r;
|
||||
const g: u32 = px.g;
|
||||
const b: u32 = px.b;
|
||||
wallpaper_image.argb_pixels.appendAssumeCapacity((a << 24) | (r << 16) | (g << 8) | b);
|
||||
}
|
||||
wallpaper_image.pix_image = pixman.Image.createBits(
|
||||
.a8r8g8b8,
|
||||
width,
|
||||
height,
|
||||
@ptrCast(@alignCast(wallpaper_image.argb_pixels.items.ptr)),
|
||||
stride,
|
||||
) orelse return error.FailedToCreatePixmanImage;
|
||||
},
|
||||
}
|
||||
|
||||
return wallpaper_image;
|
||||
}
|
||||
|
||||
pub fn destroy(wallpaper_image: *WallpaperImage) void {
|
||||
_ = wallpaper_image.pix_image.unref();
|
||||
if (native_endian == .big) wallpaper_image.argb_pixels.deinit(utils.gpa);
|
||||
wallpaper_image.zigimg_image.deinit(utils.gpa);
|
||||
|
||||
utils.gpa.destroy(wallpaper_image);
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const native_endian = builtin.cpu.arch.endian();
|
||||
|
||||
const pixman = @import("pixman");
|
||||
const zigimg = @import("zigimg");
|
||||
|
||||
const utils = @import("utils.zig");
|
||||
|
||||
const log = std.log.scoped(.WallpaperImage);
|
||||
Loading…
Add table
Add a link
Reference in a new issue