diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3dc422 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Zig build artifacts +zig-out/ +zig-cache/ +.zig-cache/ + +# Temporary files +*.o +*.a +*.so +*.dylib + +# Editor artifacts +.vscode/ +*.swp +*.swo +*~ diff --git a/README.md b/README.md index 406be35..86b4501 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,332 @@ # flair-ui -Let's see if I can finally make a really good GUI toolkit + +A complete 2D vector graphics library written in [Zig](https://ziglang.org), backed **exclusively by Vulkan**. + +## Features + +- **Surface** — offscreen RGBA render target backed by a Vulkan image +- **All major 2D shapes**: line, polyline/path, quadratic & cubic Bézier curves, circle, circular arc, oval/ellipse, elliptical arc, rectangle with independent per-corner radii +- **Fill and stroke** for every shape +- **Paint system**: solid color or linear/radial gradient +- **PNG export** — save any surface to disk using a pure-Zig PNG encoder +- **Platform-agnostic Window** abstraction (vtable pattern) with keyboard and mouse input +- **Wayland backend** — presents surfaces to the screen via `VK_KHR_wayland_surface` + +--- + +## Requirements + +| Dependency | Version | +|---|---| +| Zig | 0.13.0 or later | +| Vulkan SDK / `libvulkan` | 1.0+ | +| `libwayland-client` | any | +| `wayland-protocols` | for xdg-shell | +| `glslangValidator` | for shader compilation | + +On Ubuntu/Debian: +```sh +sudo apt install libvulkan-dev libwayland-dev wayland-protocols glslang-tools +``` + +--- + +## Building + +```sh +zig build +``` + +This will: +1. Compile GLSL shaders (`src/shaders/fill.vert.glsl`, `fill.frag.glsl`) to SPIR-V +2. Build the static library `libflair-ui.a` +3. Build the example executables + +Run examples: +```sh +zig build run-basic # draws all shapes to basic_shapes.png +zig build run-window # opens a Wayland window with mouse/keyboard input +``` + +--- + +## Quick Start + +### Offscreen surface → PNG + +```zig +const flair = @import("flair-ui"); +const std = @import("std"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + defer flair.deinit(); + + // Create an 800×600 offscreen surface + var surface = try flair.Surface.init(allocator, 800, 600); + defer surface.deinit(); + + // Clear the surface to white + surface.clear(flair.Color.white); + + // Draw a filled red circle + try surface.drawCircle(.{ .x = 400, .y = 300 }, 100, .{ + .style = .fill, + .paint = .{ .solid = flair.Color.red }, + }); + + // Draw a stroked rectangle with rounded corners and a gradient + try surface.drawRect( + .{ .x = 50, .y = 50, .width = 200, .height = 150 }, + .{ .top_left = 10, .top_right = 10, .bottom_left = 0, .bottom_right = 0 }, + .{ + .style = .{ .stroke = .{ .line_width = 3.0 } }, + .paint = .{ + .gradient = flair.Gradient.linear( + .{ .x = 50, .y = 50 }, + .{ .x = 250, .y = 200 }, + &.{ + .{ .position = 0.0, .color = flair.Color.blue }, + .{ .position = 1.0, .color = flair.Color.green }, + }, + ), + }, + }, + ); + + // Draw a cubic Bézier curve + try surface.drawCubicBezier( + .{ .x = 100, .y = 500 }, + .{ .x = 200, .y = 400 }, + .{ .x = 300, .y = 600 }, + .{ .x = 400, .y = 500 }, + .{ + .style = .{ .stroke = .{ .line_width = 2.0 } }, + .paint = .{ .solid = flair.Color.black }, + }, + ); + + // Save to PNG + try surface.savePng("output.png"); +} +``` + +### Window with event handling + +```zig +const flair = @import("flair-ui"); +const std = @import("std"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + defer flair.deinit(); + + var window = try flair.Window.init(allocator, 800, 600, "Flair App"); + defer window.deinit(); + + while (!window.shouldClose()) { + // Poll events + while (window.pollEvent()) |event| { + switch (event) { + .key_press => |ke| { + if (ke.key == .escape) return; + }, + .mouse_move => |mm| { + _ = mm; // use mm.x, mm.y + }, + .close => return, + else => {}, + } + } + + // Draw to the window's surface + const surf = window.getSurface(); + surf.clear(flair.Color.white); + try surf.drawCircle(.{ .x = 400, .y = 300 }, 50, .{ + .style = .fill, + .paint = .{ .solid = flair.Color.red }, + }); + + // Present + try window.present(); + } +} +``` + +--- + +## Architecture + +``` +flair-ui/ +├── build.zig # Build: shaders, library, examples +├── build.zig.zon # Package manifest +├── src/ +│ ├── root.zig # Public API re-exports +│ ├── color.zig # Vec2, Color, Gradient, Paint, DrawOptions +│ ├── input.zig # Event, Key, MouseButton, Modifiers +│ ├── shapes.zig # CPU tessellation for all shape types +│ ├── image.zig # Pure-Zig PNG encoder +│ ├── vulkan.zig # Vulkan loader + helpers (Buffer, one-shot cmd) +│ ├── renderer.zig # Vulkan pipeline, descriptor sets, global state +│ ├── surface.zig # Surface (offscreen Vulkan framebuffer) +│ ├── window.zig # Platform-agnostic Window vtable +│ ├── platform/ +│ │ ├── wayland.zig # Wayland backend (wl_display, xdg-shell, wl_seat) +│ │ └── generated/ # Auto-generated xdg-shell C headers +│ └── shaders/ +│ ├── fill.vert.glsl # Vertex shader (position, gradient coord, color) +│ └── fill.frag.glsl # Fragment shader (solid + gradient sampling) +└── examples/ + ├── basic_shapes.zig # All shape types → PNG + └── window_events.zig # Window + mouse/keyboard +``` + +### Shape Tessellation + +All shapes are tessellated into triangles on the CPU: + +| Shape | Method | +|---|---| +| Line | Quad (2 triangles) perpendicular to direction | +| Path | Line-segment quads, joined | +| Quadratic/Cubic Bézier | De Casteljau recursive flattening → path | +| Circle / Arc | Regular polygon fan or ring segments | +| Oval / Oval Arc | Ellipse segments | +| Rectangle | 2 triangles; rounded corners add arc segments | + +### Gradient Rendering + +- Each vertex carries a `gradient_coord` computed during tessellation +- The fragment shader samples the gradient's color stops at `gradient_coord.x` +- Push constants carry the 4×4 orthographic projection matrix +- A uniform buffer (`PaintData`) carries gradient type + up to 16 color stops + +### Platform Abstraction + +`Window` uses a vtable pattern: + +```zig +pub const WindowVTable = struct { + deinit: *const fn(*anyopaque) void, + pollEvent: *const fn(*anyopaque) ?Event, + shouldClose: *const fn(*anyopaque) bool, + presentSurface: *const fn(*anyopaque, usize, u32, u32) anyerror!void, + getWidth: *const fn(*anyopaque) u32, + getHeight: *const fn(*anyopaque) u32, + setTitle: *const fn(*anyopaque, []const u8) void, +}; +``` + +Adding a new platform (macOS, Windows) requires implementing the vtable functions in a new file under `src/platform/`. + +--- + +## API Reference + +### `flair.Color` + +```zig +// Named constants +Color.white, Color.black, Color.red, Color.green, Color.blue +Color.yellow, Color.cyan, Color.magenta, Color.gray, Color.orange +Color.transparent + +// Construction +Color.rgb(r, g, b) // f32 components +Color.rgba(r, g, b, a) +Color.fromRgba8(r, g, b, a) // u8 components (0–255) +``` + +### `flair.Gradient` + +```zig +Gradient.linear(start: Vec2, end: Vec2, stops: []const ColorStop) +Gradient.radial(center: Vec2, radius: f32, stops: []const ColorStop) +``` + +### `flair.DrawOptions` + +```zig +DrawOptions{ + .style = .fill, + // or: + .style = .{ .stroke = .{ .line_width = 2.5 } }, + .paint = .{ .solid = Color.red }, + // or: + .paint = .{ .gradient = Gradient.linear(...) }, +} +``` + +### `flair.Surface` + +```zig +// Creation / destruction +Surface.init(allocator, width, height) !Surface +surface.deinit() + +// Clear +surface.clear(color: Color) + +// Drawing +surface.drawLine(a, b, opts) !void +surface.drawPath(points, closed, opts) !void +surface.drawQuadraticBezier(p0, p1, p2, opts) !void +surface.drawCubicBezier(p0, p1, p2, p3, opts) !void +surface.drawCircle(center, radius, opts) !void +surface.drawCircularArc(center, radius, start_angle, end_angle, opts) !void +surface.drawOval(center, rx, ry, opts) !void +surface.drawOvalArc(center, rx, ry, start_angle, end_angle, opts) !void +surface.drawRect(rect, radii, opts) !void + +// Export +surface.flush() !void // render pending draw calls +surface.savePng(path) !void // flush + write PNG +surface.readPixels() ![]u8 // flush + return RGBA pixels (caller frees) +``` + +### `flair.Window` + +```zig +// Creation / destruction +Window.init(allocator, width, height, title) !Window +window.deinit() + +// Per-frame +window.pollEvent() ?Event +window.shouldClose() bool +window.getSurface() *Surface +window.present() !void // flush surface + blit to screen + +// Properties +window.getWidth() u32 +window.getHeight() u32 +window.setTitle(title) void +``` + +### `flair.Event` + +```zig +.key_press => KeyEvent { .key, .scancode, .mods } +.key_release => KeyEvent +.key_repeat => KeyEvent +.mouse_button_press => MouseButtonEvent { .button, .x, .y, .mods } +.mouse_button_release => MouseButtonEvent +.mouse_move => MouseMoveEvent { .x, .y } +.mouse_scroll => ScrollEvent { .dx, .dy } +.mouse_enter => MouseMoveEvent +.mouse_leave => MouseMoveEvent +.resize => ResizeEvent { .width, .height } +.close +``` + +--- + +## License + +MIT + diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..d03c6e7 --- /dev/null +++ b/build.zig @@ -0,0 +1,138 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // --------------------------------------------------------------------------- + // Translate C headers to Zig modules (replaces @cImport, deprecated in 0.16) + // --------------------------------------------------------------------------- + + // Vulkan bindings: VK_NO_PROTOTYPES is set so we load function pointers manually. + const translate_vulkan = b.addTranslateC(.{ + .root_source_file = b.path("src/c_headers/vulkan.h"), + .target = target, + .optimize = optimize, + }); + translate_vulkan.defineCMacro("VK_NO_PROTOTYPES", "1"); + const vulkan_c_mod = translate_vulkan.createModule(); + + // Wayland + xdg-shell bindings. + const translate_wayland = b.addTranslateC(.{ + .root_source_file = b.path("src/c_headers/wayland.h"), + .target = target, + .optimize = optimize, + }); + translate_wayland.addIncludePath(b.path("src/platform/generated")); + translate_wayland.addIncludePath(b.path("src/platform")); + const wayland_c_mod = translate_wayland.createModule(); + + // --------------------------------------------------------------------------- + // Compile GLSL shaders to SPIR-V + // --------------------------------------------------------------------------- + const compile_vert = b.addSystemCommand(&.{ + "glslangValidator", + "--target-env", "vulkan1.0", + "-V", + "-o", + "src/shaders/fill.vert.spv", + "src/shaders/fill.vert.glsl", + }); + + const compile_frag = b.addSystemCommand(&.{ + "glslangValidator", + "--target-env", "vulkan1.0", + "-V", + "-o", + "src/shaders/fill.frag.spv", + "src/shaders/fill.frag.glsl", + }); + + // --------------------------------------------------------------------------- + // Library + // --------------------------------------------------------------------------- + const lib = b.addStaticLibrary(.{ + .name = "flair-ui", + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + lib.root_module.addImport("vulkan_c", vulkan_c_mod); + lib.root_module.addImport("wayland_c", wayland_c_mod); + + lib.step.dependOn(&compile_vert.step); + lib.step.dependOn(&compile_frag.step); + + // Include generated Wayland protocol C sources + lib.addCSourceFiles(.{ + .files = &.{"src/platform/generated/xdg-shell-protocol.c"}, + .flags = &.{"-std=c99"}, + }); + lib.addIncludePath(b.path("src/platform/generated")); + lib.addIncludePath(b.path("src/platform")); + lib.linkSystemLibrary("vulkan"); + lib.linkSystemLibrary("wayland-client"); + lib.linkLibC(); + + b.installArtifact(lib); + + // --------------------------------------------------------------------------- + // Example: basic_shapes + // --------------------------------------------------------------------------- + const basic_shapes_exe = b.addExecutable(.{ + .name = "basic_shapes", + .root_source_file = b.path("examples/basic_shapes.zig"), + .target = target, + .optimize = optimize, + }); + basic_shapes_exe.root_module.addImport("flair-ui", &lib.root_module); + basic_shapes_exe.addCSourceFiles(.{ + .files = &.{"src/platform/generated/xdg-shell-protocol.c"}, + .flags = &.{"-std=c99"}, + }); + basic_shapes_exe.addIncludePath(b.path("src/platform/generated")); + basic_shapes_exe.addIncludePath(b.path("src/platform")); + basic_shapes_exe.linkSystemLibrary("vulkan"); + basic_shapes_exe.linkSystemLibrary("wayland-client"); + basic_shapes_exe.linkLibC(); + basic_shapes_exe.step.dependOn(&compile_vert.step); + basic_shapes_exe.step.dependOn(&compile_frag.step); + b.installArtifact(basic_shapes_exe); + + // --------------------------------------------------------------------------- + // Example: window_events + // --------------------------------------------------------------------------- + const window_events_exe = b.addExecutable(.{ + .name = "window_events", + .root_source_file = b.path("examples/window_events.zig"), + .target = target, + .optimize = optimize, + }); + window_events_exe.root_module.addImport("flair-ui", &lib.root_module); + window_events_exe.addCSourceFiles(.{ + .files = &.{"src/platform/generated/xdg-shell-protocol.c"}, + .flags = &.{"-std=c99"}, + }); + window_events_exe.addIncludePath(b.path("src/platform/generated")); + window_events_exe.addIncludePath(b.path("src/platform")); + window_events_exe.linkSystemLibrary("vulkan"); + window_events_exe.linkSystemLibrary("wayland-client"); + window_events_exe.linkLibC(); + window_events_exe.step.dependOn(&compile_vert.step); + window_events_exe.step.dependOn(&compile_frag.step); + b.installArtifact(window_events_exe); + + // --------------------------------------------------------------------------- + // Run steps + // --------------------------------------------------------------------------- + const run_basic = b.addRunArtifact(basic_shapes_exe); + run_basic.step.dependOn(b.getInstallStep()); + const run_basic_step = b.step("run-basic", "Run the basic_shapes example"); + run_basic_step.dependOn(&run_basic.step); + + const run_window = b.addRunArtifact(window_events_exe); + run_window.step.dependOn(b.getInstallStep()); + const run_window_step = b.step("run-window", "Run the window_events example"); + run_window_step.dependOn(&run_window.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..04f663c --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .flair_ui, + .version = "0.1.0", + .fingerprint = 0xdeadbeef12345678, + .minimum_zig_version = "0.16.0", + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "examples", + }, +} diff --git a/examples/basic_shapes.zig b/examples/basic_shapes.zig new file mode 100644 index 0000000..af1b8ba --- /dev/null +++ b/examples/basic_shapes.zig @@ -0,0 +1,201 @@ +//! basic_shapes.zig — Example: draw all shape types to a surface and save as PNG. + +const std = @import("std"); +const flair = @import("flair-ui"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + defer flair.deinit(); + + // ------------------------------------------------------------------- + // Create an 800×600 offscreen surface + // ------------------------------------------------------------------- + var surface = try flair.Surface.init(allocator, 800, 600); + defer surface.deinit(); + + // Clear to white + surface.clear(flair.Color.white); + + // ------------------------------------------------------------------- + // Line + // ------------------------------------------------------------------- + try surface.drawLine( + .{ .x = 50, .y = 50 }, + .{ .x = 200, .y = 50 }, + .{ + .style = .{ .stroke = .{ .line_width = 3.0 } }, + .paint = .{ .solid = flair.Color.black }, + }, + ); + + // ------------------------------------------------------------------- + // Path (open polyline) + // ------------------------------------------------------------------- + const path_pts = [_]flair.Vec2{ + .{ .x = 50, .y = 100 }, + .{ .x = 100, .y = 80 }, + .{ .x = 150, .y = 120 }, + .{ .x = 200, .y = 90 }, + .{ .x = 250, .y = 110 }, + }; + try surface.drawPath(&path_pts, false, .{ + .style = .{ .stroke = .{ .line_width = 2.0 } }, + .paint = .{ .solid = flair.Color.blue }, + }); + + // ------------------------------------------------------------------- + // Quadratic Bézier + // ------------------------------------------------------------------- + try surface.drawQuadraticBezier( + .{ .x = 50, .y = 200 }, + .{ .x = 150, .y = 140 }, + .{ .x = 250, .y = 200 }, + .{ + .style = .{ .stroke = .{ .line_width = 2.0 } }, + .paint = .{ .solid = flair.Color.green }, + }, + ); + + // ------------------------------------------------------------------- + // Cubic Bézier + // ------------------------------------------------------------------- + try surface.drawCubicBezier( + .{ .x = 50, .y = 260 }, + .{ .x = 100, .y = 220 }, + .{ .x = 200, .y = 300 }, + .{ .x = 250, .y = 260 }, + .{ + .style = .{ .stroke = .{ .line_width = 2.5 } }, + .paint = .{ .solid = flair.Color.purple }, + }, + ); + + // ------------------------------------------------------------------- + // Filled circle + // ------------------------------------------------------------------- + try surface.drawCircle( + .{ .x = 350, .y = 100 }, + 60, + .{ + .style = .fill, + .paint = .{ .solid = flair.Color.red }, + }, + ); + + // ------------------------------------------------------------------- + // Stroked circle + // ------------------------------------------------------------------- + try surface.drawCircle( + .{ .x = 500, .y = 100 }, + 60, + .{ + .style = .{ .stroke = .{ .line_width = 4.0 } }, + .paint = .{ .solid = flair.Color.orange }, + }, + ); + + // ------------------------------------------------------------------- + // Circular arc + // ------------------------------------------------------------------- + try surface.drawCircularArc( + .{ .x = 350, .y = 250 }, + 50, + 0.0, + std.math.pi, + .{ + .style = .{ .stroke = .{ .line_width = 3.0 } }, + .paint = .{ .solid = flair.Color.cyan }, + }, + ); + + // ------------------------------------------------------------------- + // Oval (ellipse) + // ------------------------------------------------------------------- + try surface.drawOval( + .{ .x = 500, .y = 250 }, + 80, + 40, + .{ + .style = .fill, + .paint = .{ + .gradient = flair.Gradient.linear( + .{ .x = 420, .y = 250 }, + .{ .x = 580, .y = 250 }, + &.{ + .{ .position = 0.0, .color = flair.Color.blue }, + .{ .position = 1.0, .color = flair.Color.cyan }, + }, + ), + }, + }, + ); + + // ------------------------------------------------------------------- + // Oval arc + // ------------------------------------------------------------------- + try surface.drawOvalArc( + .{ .x = 350, .y = 380 }, + 70, + 35, + std.math.pi * 0.25, + std.math.pi * 1.75, + .{ + .style = .{ .stroke = .{ .line_width = 2.0 } }, + .paint = .{ .solid = flair.Color.magenta }, + }, + ); + + // ------------------------------------------------------------------- + // Plain rectangle + // ------------------------------------------------------------------- + try surface.drawRect( + .{ .x = 50, .y = 320, .width = 200, .height = 100 }, + flair.CornerRadii.zero, + .{ + .style = .fill, + .paint = .{ .solid = flair.Color.yellow }, + }, + ); + + // ------------------------------------------------------------------- + // Rounded rectangle with independent corner radii + // ------------------------------------------------------------------- + try surface.drawRect( + .{ .x = 50, .y = 450, .width = 200, .height = 120 }, + .{ .top_left = 20, .top_right = 5, .bottom_left = 5, .bottom_right = 20 }, + .{ + .style = .{ .stroke = .{ .line_width = 3.0 } }, + .paint = .{ + .gradient = flair.Gradient.radial( + .{ .x = 150, .y = 510 }, + 100, + &.{ + .{ .position = 0.0, .color = flair.Color.white }, + .{ .position = 1.0, .color = flair.Color.blue }, + }, + ), + }, + }, + ); + + // ------------------------------------------------------------------- + // Pill-shaped rectangle (fully rounded) + // ------------------------------------------------------------------- + try surface.drawRect( + .{ .x = 500, .y = 340, .width = 200, .height = 60 }, + flair.CornerRadii.uniform(30), + .{ + .style = .fill, + .paint = .{ .solid = flair.Color.fromRgba8(100, 200, 100, 255) }, + }, + ); + + // ------------------------------------------------------------------- + // Save to PNG + // ------------------------------------------------------------------- + const output_path = "basic_shapes.png"; + try surface.savePng(output_path); + std.debug.print("Saved to {s}\n", .{output_path}); +} diff --git a/examples/window_events.zig b/examples/window_events.zig new file mode 100644 index 0000000..632918c --- /dev/null +++ b/examples/window_events.zig @@ -0,0 +1,165 @@ +//! window_events.zig — Example: open a window, draw shapes, handle mouse/keyboard events. + +const std = @import("std"); +const flair = @import("flair-ui"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + defer flair.deinit(); + + // ------------------------------------------------------------------- + // Create a window + // ------------------------------------------------------------------- + var window = try flair.Window.init(allocator, 800, 600, "Flair Demo"); + defer window.deinit(); + + var circle_x: f32 = 400.0; + var circle_y: f32 = 300.0; + var bg_color = flair.Color.white; + + std.debug.print("Window opened. Press Escape to quit.\n", .{}); + std.debug.print("Move the mouse to move the circle. Click to change background.\n", .{}); + + // ------------------------------------------------------------------- + // Main loop + // ------------------------------------------------------------------- + while (!window.shouldClose()) { + // ---------------------------------------------------------------- + // Poll events + // ---------------------------------------------------------------- + while (window.pollEvent()) |event| { + switch (event) { + .key_press => |ke| { + std.debug.print("Key pressed: {} (scancode {})\n", .{ ke.key, ke.scancode }); + if (ke.key == .escape) { + return; // Exit on Escape + } + }, + .key_release => |ke| { + std.debug.print("Key released: {}\n", .{ke.key}); + }, + .mouse_button_press => |mb| { + std.debug.print("Mouse button {} at ({d:.1}, {d:.1})\n", .{ + mb.button, mb.x, mb.y, + }); + // Change background color on click + bg_color = switch (mb.button) { + .left => flair.Color.light_gray, + .right => flair.Color.fromRgba8(220, 240, 255, 255), + else => flair.Color.white, + }; + }, + .mouse_button_release => |mb| { + std.debug.print("Mouse button {} released\n", .{mb.button}); + }, + .mouse_move => |mm| { + circle_x = mm.x; + circle_y = mm.y; + }, + .mouse_scroll => |sc| { + std.debug.print("Scroll: dx={d:.2} dy={d:.2}\n", .{ sc.dx, sc.dy }); + }, + .mouse_enter => |mm| { + std.debug.print("Mouse entered at ({d:.1}, {d:.1})\n", .{ mm.x, mm.y }); + }, + .mouse_leave => { + std.debug.print("Mouse left\n", .{}); + }, + .resize => |r| { + std.debug.print("Resized to {}×{}\n", .{ r.width, r.height }); + }, + .close => { + std.debug.print("Close requested\n", .{}); + return; + }, + } + } + + // ---------------------------------------------------------------- + // Draw frame + // ---------------------------------------------------------------- + const surf = window.getSurface(); + surf.clear(bg_color); + + // Background grid + var gx: f32 = 0; + while (gx < @as(f32, @floatFromInt(surf.width))) : (gx += 80) { + try surf.drawLine( + .{ .x = gx, .y = 0 }, + .{ .x = gx, .y = @floatFromInt(surf.height) }, + .{ + .style = .{ .stroke = .{ .line_width = 0.5 } }, + .paint = .{ .solid = flair.Color.light_gray }, + }, + ); + } + var gy: f32 = 0; + while (gy < @as(f32, @floatFromInt(surf.height))) : (gy += 80) { + try surf.drawLine( + .{ .x = 0, .y = gy }, + .{ .x = @floatFromInt(surf.width), .y = gy }, + .{ + .style = .{ .stroke = .{ .line_width = 0.5 } }, + .paint = .{ .solid = flair.Color.light_gray }, + }, + ); + } + + // Filled circle that follows the mouse + try surf.drawCircle( + .{ .x = circle_x, .y = circle_y }, + 40, + .{ + .style = .fill, + .paint = .{ + .gradient = flair.Gradient.radial( + .{ .x = circle_x, .y = circle_y }, + 40, + &.{ + .{ .position = 0.0, .color = flair.Color.white }, + .{ .position = 0.5, .color = flair.Color.red }, + .{ .position = 1.0, .color = flair.Color.fromRgba8(150, 0, 0, 255) }, + }, + ), + }, + }, + ); + + // Stroked circle (ring around the mouse) + try surf.drawCircle( + .{ .x = circle_x, .y = circle_y }, + 45, + .{ + .style = .{ .stroke = .{ .line_width = 2.0 } }, + .paint = .{ .solid = flair.Color.black }, + }, + ); + + // Rounded rectangle in the corner + try surf.drawRect( + .{ .x = 20, .y = 20, .width = 180, .height = 60 }, + flair.CornerRadii.uniform(12), + .{ + .style = .fill, + .paint = .{ .solid = flair.Color.fromRgba8(0, 0, 0, 128) }, + }, + ); + + // "Flair Demo" label as a bezier path + try surf.drawCubicBezier( + .{ .x = 30, .y = 55 }, + .{ .x = 80, .y = 35 }, + .{ .x = 130, .y = 75 }, + .{ .x = 190, .y = 55 }, + .{ + .style = .{ .stroke = .{ .line_width = 2.0 } }, + .paint = .{ .solid = flair.Color.white }, + }, + ); + + // Present the frame + try window.present(); + } +} diff --git a/src/c_headers/vulkan.h b/src/c_headers/vulkan.h new file mode 100644 index 0000000..ec64cc8 --- /dev/null +++ b/src/c_headers/vulkan.h @@ -0,0 +1,5 @@ +// Wrapper header for Vulkan C bindings. +// VK_NO_PROTOTYPES is passed via -D flag from build.zig (translate-c step), +// so function pointers are used instead of direct prototypes. +#include +#include diff --git a/src/c_headers/wayland.h b/src/c_headers/wayland.h new file mode 100644 index 0000000..c9a9452 --- /dev/null +++ b/src/c_headers/wayland.h @@ -0,0 +1,5 @@ +// Wrapper header for Wayland C bindings. +// xdg-shell-client-protocol.h is generated and lives in +// src/platform/generated/; its path is provided via -I from build.zig. +#include +#include diff --git a/src/color.zig b/src/color.zig new file mode 100644 index 0000000..34e9ea4 --- /dev/null +++ b/src/color.zig @@ -0,0 +1,242 @@ +//! Color, Gradient, Paint — the paint system for flair-ui. +//! +//! Colors are RGBA with f32 components in the range [0.0, 1.0]. + +const std = @import("std"); + +// --------------------------------------------------------------------------- +// Vec2 — a 2D point / vector used throughout the library +// --------------------------------------------------------------------------- + +pub const Vec2 = struct { + x: f32, + y: f32, + + pub fn sub(a: Vec2, b: Vec2) Vec2 { + return .{ .x = a.x - b.x, .y = a.y - b.y }; + } + + pub fn add(a: Vec2, b: Vec2) Vec2 { + return .{ .x = a.x + b.x, .y = a.y + b.y }; + } + + pub fn scale(v: Vec2, s: f32) Vec2 { + return .{ .x = v.x * s, .y = v.y * s }; + } + + pub fn length(v: Vec2) f32 { + return @sqrt(v.x * v.x + v.y * v.y); + } + + pub fn normalize(v: Vec2) Vec2 { + const len = v.length(); + if (len < 1e-10) return .{ .x = 0, .y = 0 }; + return .{ .x = v.x / len, .y = v.y / len }; + } + + pub fn perp(v: Vec2) Vec2 { + return .{ .x = -v.y, .y = v.x }; + } + + pub fn dot(a: Vec2, b: Vec2) f32 { + return a.x * b.x + a.y * b.y; + } +}; + +// --------------------------------------------------------------------------- +// Color +// --------------------------------------------------------------------------- + +/// An RGBA color with f32 components in [0.0, 1.0]. +pub const Color = struct { + r: f32 = 0.0, + g: f32 = 0.0, + b: f32 = 0.0, + a: f32 = 1.0, + + pub fn rgba(r: f32, g: f32, b: f32, a: f32) Color { + return .{ .r = r, .g = g, .b = b, .a = a }; + } + + pub fn rgb(r: f32, g: f32, b: f32) Color { + return .{ .r = r, .g = g, .b = b, .a = 1.0 }; + } + + /// Convert from 0–255 RGBA integer components. + pub fn fromRgba8(r: u8, g: u8, b: u8, a: u8) Color { + return .{ + .r = @as(f32, @floatFromInt(r)) / 255.0, + .g = @as(f32, @floatFromInt(g)) / 255.0, + .b = @as(f32, @floatFromInt(b)) / 255.0, + .a = @as(f32, @floatFromInt(a)) / 255.0, + }; + } + + /// Convert to 0–255 RGBA integer components. + pub fn toRgba8(self: Color) [4]u8 { + return .{ + @as(u8, @intFromFloat(std.math.clamp(self.r * 255.0, 0.0, 255.0))), + @as(u8, @intFromFloat(std.math.clamp(self.g * 255.0, 0.0, 255.0))), + @as(u8, @intFromFloat(std.math.clamp(self.b * 255.0, 0.0, 255.0))), + @as(u8, @intFromFloat(std.math.clamp(self.a * 255.0, 0.0, 255.0))), + }; + } + + pub fn lerp(a: Color, b: Color, t: f32) Color { + const s = 1.0 - t; + return .{ + .r = a.r * s + b.r * t, + .g = a.g * s + b.g * t, + .b = a.b * s + b.b * t, + .a = a.a * s + b.a * t, + }; + } + + // Named colors + pub const white = Color{ .r = 1, .g = 1, .b = 1, .a = 1 }; + pub const black = Color{ .r = 0, .g = 0, .b = 0, .a = 1 }; + pub const red = Color{ .r = 1, .g = 0, .b = 0, .a = 1 }; + pub const green = Color{ .r = 0, .g = 1, .b = 0, .a = 1 }; + pub const blue = Color{ .r = 0, .g = 0, .b = 1, .a = 1 }; + pub const yellow = Color{ .r = 1, .g = 1, .b = 0, .a = 1 }; + pub const cyan = Color{ .r = 0, .g = 1, .b = 1, .a = 1 }; + pub const magenta = Color{ .r = 1, .g = 0, .b = 1, .a = 1 }; + pub const transparent = Color{ .r = 0, .g = 0, .b = 0, .a = 0 }; + pub const gray = Color{ .r = 0.5, .g = 0.5, .b = 0.5, .a = 1 }; + pub const dark_gray = Color{ .r = 0.25, .g = 0.25, .b = 0.25, .a = 1 }; + pub const light_gray = Color{ .r = 0.75, .g = 0.75, .b = 0.75, .a = 1 }; + pub const orange = Color{ .r = 1, .g = 0.5, .b = 0, .a = 1 }; + pub const purple = Color{ .r = 0.5, .g = 0, .b = 0.5, .a = 1 }; +}; + +// --------------------------------------------------------------------------- +// ColorStop — a position + color for gradients +// --------------------------------------------------------------------------- + +pub const ColorStop = struct { + /// Position in [0.0, 1.0] along the gradient. + position: f32, + color: Color, +}; + +// --------------------------------------------------------------------------- +// Gradient types +// --------------------------------------------------------------------------- + +pub const LinearGradient = struct { + start: Vec2, + end: Vec2, + stops: []const ColorStop, +}; + +pub const RadialGradient = struct { + center: Vec2, + radius: f32, + stops: []const ColorStop, +}; + +pub const GradientKind = union(enum) { + linear: LinearGradient, + radial: RadialGradient, +}; + +/// A gradient: linear or radial, with color stops. +pub const Gradient = struct { + kind: GradientKind, + + pub fn linear(start: Vec2, end: Vec2, stops: []const ColorStop) Gradient { + return .{ .kind = .{ .linear = .{ .start = start, .end = end, .stops = stops } } }; + } + + pub fn radial(center: Vec2, radius: f32, stops: []const ColorStop) Gradient { + return .{ .kind = .{ .radial = .{ .center = center, .radius = radius, .stops = stops } } }; + } + + /// Sample the gradient at position t ∈ [0.0, 1.0]. + pub fn sample(self: Gradient, t: f32) Color { + const stops = switch (self.kind) { + .linear => |g| g.stops, + .radial => |g| g.stops, + }; + if (stops.len == 0) return Color.black; + if (stops.len == 1) return stops[0].color; + + const tc = std.math.clamp(t, 0.0, 1.0); + + // Find the two stops bracketing t + var i: usize = 0; + while (i + 1 < stops.len and stops[i + 1].position <= tc) : (i += 1) {} + + if (i + 1 >= stops.len) return stops[stops.len - 1].color; + if (tc <= stops[0].position) return stops[0].color; + + const a = stops[i]; + const b = stops[i + 1]; + const range = b.position - a.position; + if (range < 1e-10) return a.color; + const local_t = (tc - a.position) / range; + return Color.lerp(a.color, b.color, local_t); + } +}; + +// --------------------------------------------------------------------------- +// Paint — solid or gradient +// --------------------------------------------------------------------------- + +pub const Paint = union(enum) { + solid: Color, + gradient: Gradient, + + /// Sample the paint to get a color at gradient coordinate t. + pub fn sampleAt(self: Paint, t: f32) Color { + return switch (self) { + .solid => |c| c, + .gradient => |g| g.sample(t), + }; + } +}; + +// --------------------------------------------------------------------------- +// DrawStyle +// --------------------------------------------------------------------------- + +pub const StrokeOptions = struct { + /// Width of the stroke in surface pixels. + line_width: f32 = 1.0, +}; + +pub const DrawStyle = union(enum) { + fill: void, + stroke: StrokeOptions, +}; + +/// Options passed to every draw call. +pub const DrawOptions = struct { + style: DrawStyle = .fill, + paint: Paint = .{ .solid = Color.black }, +}; + +// --------------------------------------------------------------------------- +// Rectangle & corner radii +// --------------------------------------------------------------------------- + +pub const Rect = struct { + x: f32, + y: f32, + width: f32, + height: f32, +}; + +/// Independent corner radii for a rounded rectangle. +pub const CornerRadii = struct { + top_left: f32 = 0, + top_right: f32 = 0, + bottom_left: f32 = 0, + bottom_right: f32 = 0, + + pub const zero = CornerRadii{}; + + pub fn uniform(r: f32) CornerRadii { + return .{ .top_left = r, .top_right = r, .bottom_left = r, .bottom_right = r }; + } +}; diff --git a/src/image.zig b/src/image.zig new file mode 100644 index 0000000..3c71511 --- /dev/null +++ b/src/image.zig @@ -0,0 +1,154 @@ +//! PNG image encoder. +//! +//! Encodes raw RGBA8 pixel data as a PNG file. The IDAT payload uses a +//! hand-rolled zlib/DEFLATE-stored stream so there is no dependency on +//! std.compress (whose API changed substantially in Zig 0.15+). + +const std = @import("std"); + +/// Write a PNG file containing `width × height` RGBA8 pixels. +/// +/// `pixels` must be `width * height * 4` bytes, with rows in top-to-bottom +/// order and pixels in RGBA order. +pub fn writePng( + writer: anytype, + width: u32, + height: u32, + pixels: []const u8, +) !void { + std.debug.assert(pixels.len == @as(usize, width) * height * 4); + + // PNG signature + try writer.writeAll(&png_signature); + + // IHDR chunk + try writeIhdr(writer, width, height); + + // IDAT chunk + try writeIdat(writer, width, height, pixels); + + // IEND chunk + try writeIend(writer); +} + +/// Encode to a byte slice. Caller owns the returned memory. +pub fn encodePng(allocator: std.mem.Allocator, width: u32, height: u32, pixels: []const u8) ![]u8 { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + try writePng(buf.writer(), width, height, pixels); + return buf.toOwnedSlice(); +} + +// --------------------------------------------------------------------------- +// Internals +// --------------------------------------------------------------------------- + +const png_signature = [8]u8{ 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' }; + +fn crc32(data: []const u8) u32 { + return std.hash.Crc32.hash(data); +} + +fn writeChunk(writer: anytype, chunk_type: *const [4]u8, data: []const u8) !void { + // Length (4 bytes, big-endian) + try writer.writeInt(u32, @intCast(data.len), .big); + + // Type + data + CRC + const type_slice: []const u8 = chunk_type[0..4]; + var crc_hasher = std.hash.Crc32.init(); + crc_hasher.update(type_slice); + crc_hasher.update(data); + const crc = crc_hasher.final(); + + try writer.writeAll(type_slice); + try writer.writeAll(data); + try writer.writeInt(u32, crc, .big); +} + +fn writeIhdr(writer: anytype, width: u32, height: u32) !void { + // IHDR is exactly 13 bytes: + // 4 bytes width, 4 bytes height, 1 byte bit depth, 1 byte color type, + // 1 byte compression, 1 byte filter method, 1 byte interlace method + var ihdr: [13]u8 = undefined; + std.mem.writeInt(u32, ihdr[0..4], width, .big); + std.mem.writeInt(u32, ihdr[4..8], height, .big); + ihdr[8] = 8; // bit depth: 8 bits per channel + ihdr[9] = 6; // color type 6 = RGBA + ihdr[10] = 0; // compression method 0 (deflate) + ihdr[11] = 0; // filter method 0 + ihdr[12] = 0; // interlace method 0 (no interlace) + try writeChunk(writer, "IHDR", &ihdr); +} + +fn writeIdat(writer: anytype, width: u32, height: u32, pixels: []const u8) !void { + // We need to filter the scanlines (PNG filter type 0 = None) then compress. + // Filtered data: each row is prefixed with a 1-byte filter type. + + // Allocate filtered data buffer + const row_stride = @as(usize, width) * 4; + const filtered_size = (row_stride + 1) * height; + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const filtered = try alloc.alloc(u8, filtered_size); + var dst_off: usize = 0; + var y: u32 = 0; + while (y < height) : (y += 1) { + filtered[dst_off] = 0; // filter type: None + dst_off += 1; + const src_off = @as(usize, y) * row_stride; + @memcpy(filtered[dst_off..][0..row_stride], pixels[src_off..][0..row_stride]); + dst_off += row_stride; + } + + // Compress with zlib (DEFLATE stored blocks — no LZ compression, always valid PNG). + // std.compress.zlib was removed in Zig 0.15; we encode the zlib container manually. + const compressed = try zlibStoreCompress(alloc, filtered); + try writeChunk(writer, "IDAT", compressed); +} + +/// Encode `data` as a zlib stream (RFC 1950) using DEFLATE stored blocks (RFC 1951 §3.2.4). +/// Stored blocks carry raw data with no compression, which is always legal in PNG IDAT. +fn zlibStoreCompress(allocator: std.mem.Allocator, data: []const u8) ![]u8 { + var out = std.ArrayList(u8).init(allocator); + errdefer out.deinit(); + + // Zlib header: CMF=0x78 (CM=8 deflate, CINFO=7 → 32 KiB window), + // FLG=0x01 (FLEVEL=0 fastest, FDICT=0, FCHECK=1 so that 0x7801 % 31 == 0). + try out.appendSlice(&[_]u8{ 0x78, 0x01 }); + + // DEFLATE stored blocks: each up to 65535 bytes. + var offset: usize = 0; + while (true) { + const remaining = data.len - offset; + const block_len = @min(remaining, @as(usize, 0xFFFF)); + const is_final = (offset + block_len >= data.len); + + // Block header: BFINAL (bit 0) + BTYPE=00 (bits 1-2) stored in one byte. + var block_hdr: [5]u8 = undefined; + block_hdr[0] = if (is_final) 0x01 else 0x00; + const blen: u16 = @intCast(block_len); + std.mem.writeInt(u16, block_hdr[1..3], blen, .little); + std.mem.writeInt(u16, block_hdr[3..5], ~blen, .little); // NLEN = one's complement + try out.appendSlice(&block_hdr); + try out.appendSlice(data[offset..][0..block_len]); + + offset += block_len; + if (is_final) break; + } + + // Adler-32 checksum of the uncompressed data, big-endian (RFC 1950 §2.2). + var hasher = std.hash.Adler32.init(); + hasher.update(data); + var footer: [4]u8 = undefined; + std.mem.writeInt(u32, &footer, hasher.final(), .big); + try out.appendSlice(&footer); + + return out.toOwnedSlice(); +} + +fn writeIend(writer: anytype) !void { + try writeChunk(writer, "IEND", &.{}); +} diff --git a/src/input.zig b/src/input.zig new file mode 100644 index 0000000..8851d58 --- /dev/null +++ b/src/input.zig @@ -0,0 +1,230 @@ +//! Input event types for flair-ui windows. +//! +//! Events are delivered through a polling model: call `window.pollEvent()` +//! which returns `?Event` until all pending events are consumed. + +// --------------------------------------------------------------------------- +// Keyboard +// --------------------------------------------------------------------------- + +/// Keyboard modifier flags. +pub const Modifiers = packed struct(u8) { + shift: bool = false, + ctrl: bool = false, + alt: bool = false, + super: bool = false, + _padding: u4 = 0, +}; + +/// A key on the keyboard (symbolic). +pub const Key = enum(u32) { + unknown = 0, + + // Function keys + f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, + + // Navigation + up, down, left, right, + home, end, + page_up, page_down, + insert, delete, + + // Modifier keys + left_shift, right_shift, + left_ctrl, right_ctrl, + left_alt, right_alt, + left_super, right_super, + + // Lock keys + caps_lock, num_lock, scroll_lock, + + // Special + escape, @"return", tab, backspace, space, + print_screen, pause, + + // Numpad + kp_0, kp_1, kp_2, kp_3, kp_4, kp_5, kp_6, kp_7, kp_8, kp_9, + kp_add, kp_sub, kp_mul, kp_div, kp_enter, kp_decimal, + + // Alphabet (lowercase) + a, b, c, d, e, f, g, h, i, j, k, l, m, + n, o, p, q, r, s, t, u, v, w, x, y, z, + + // Digits + @"0", @"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", + + // Punctuation / misc + minus, equal, left_bracket, right_bracket, backslash, + semicolon, apostrophe, grave, comma, period, slash, +}; + +pub const KeyEvent = struct { + key: Key, + scancode: u32, + mods: Modifiers, +}; + +// --------------------------------------------------------------------------- +// Mouse +// --------------------------------------------------------------------------- + +pub const MouseButton = enum(u8) { + left = 1, + right = 2, + middle = 3, + button4 = 4, + button5 = 5, +}; + +pub const MouseButtonEvent = struct { + button: MouseButton, + x: f32, + y: f32, + mods: Modifiers, +}; + +pub const MouseMoveEvent = struct { + x: f32, + y: f32, +}; + +pub const ScrollEvent = struct { + dx: f32, + dy: f32, +}; + +pub const ResizeEvent = struct { + width: u32, + height: u32, +}; + +// --------------------------------------------------------------------------- +// Unified event type +// --------------------------------------------------------------------------- + +pub const Event = union(enum) { + key_press: KeyEvent, + key_release: KeyEvent, + key_repeat: KeyEvent, + mouse_button_press: MouseButtonEvent, + mouse_button_release: MouseButtonEvent, + mouse_move: MouseMoveEvent, + mouse_scroll: ScrollEvent, + mouse_enter: MouseMoveEvent, + mouse_leave: MouseMoveEvent, + resize: ResizeEvent, + close, +}; + +// --------------------------------------------------------------------------- +// Linux/Wayland key-code translation +// --------------------------------------------------------------------------- + +/// Translate a Linux evdev key code (from xkb / wl_keyboard) to a `Key`. +pub fn keyFromLinux(code: u32) Key { + return switch (code) { + 1 => .escape, + 2 => .@"1", + 3 => .@"2", + 4 => .@"3", + 5 => .@"4", + 6 => .@"5", + 7 => .@"6", + 8 => .@"7", + 9 => .@"8", + 10 => .@"9", + 11 => .@"0", + 12 => .minus, + 13 => .equal, + 14 => .backspace, + 15 => .tab, + 16 => .q, + 17 => .w, + 18 => .e, + 19 => .r, + 20 => .t, + 21 => .y, + 22 => .u, + 23 => .i, + 24 => .o, + 25 => .p, + 26 => .left_bracket, + 27 => .right_bracket, + 28 => .@"return", + 29 => .left_ctrl, + 30 => .a, + 31 => .s, + 32 => .d, + 33 => .f, + 34 => .g, + 35 => .h, + 36 => .j, + 37 => .k, + 38 => .l, + 39 => .semicolon, + 40 => .apostrophe, + 41 => .grave, + 42 => .left_shift, + 43 => .backslash, + 44 => .z, + 45 => .x, + 46 => .c, + 47 => .v, + 48 => .b, + 49 => .n, + 50 => .m, + 51 => .comma, + 52 => .period, + 53 => .slash, + 54 => .right_shift, + 55 => .kp_mul, + 56 => .left_alt, + 57 => .space, + 58 => .caps_lock, + 59 => .f1, + 60 => .f2, + 61 => .f3, + 62 => .f4, + 63 => .f5, + 64 => .f6, + 65 => .f7, + 66 => .f8, + 67 => .f9, + 68 => .f10, + 69 => .num_lock, + 70 => .scroll_lock, + 71 => .kp_7, + 72 => .kp_8, + 73 => .kp_9, + 74 => .kp_sub, + 75 => .kp_4, + 76 => .kp_5, + 77 => .kp_6, + 78 => .kp_add, + 79 => .kp_1, + 80 => .kp_2, + 81 => .kp_3, + 82 => .kp_0, + 83 => .kp_decimal, + 87 => .f11, + 88 => .f12, + 96 => .kp_enter, + 97 => .right_ctrl, + 98 => .kp_div, + 100 => .right_alt, + 102 => .home, + 103 => .up, + 104 => .page_up, + 105 => .left, + 106 => .right, + 107 => .end, + 108 => .down, + 109 => .page_down, + 110 => .insert, + 111 => .delete, + 119 => .pause, + 125 => .left_super, + 126 => .right_super, + else => .unknown, + }; +} diff --git a/src/platform/generated/xdg-shell-client-protocol.h b/src/platform/generated/xdg-shell-client-protocol.h new file mode 100644 index 0000000..aec327c --- /dev/null +++ b/src/platform/generated/xdg-shell-client-protocol.h @@ -0,0 +1,2381 @@ +/* Generated by wayland-scanner 1.22.0 */ + +#ifndef XDG_SHELL_CLIENT_PROTOCOL_H +#define XDG_SHELL_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_shell The xdg_shell protocol + * @section page_ifaces_xdg_shell Interfaces + * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces + * - @subpage page_iface_xdg_positioner - child surface positioner + * - @subpage page_iface_xdg_surface - desktop user interface surface base interface + * - @subpage page_iface_xdg_toplevel - toplevel surface + * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus + * @section page_copyright_xdg_shell Copyright + *
+ *
+ * Copyright © 2008-2013 Kristian Høgsberg
+ * Copyright © 2013      Rafael Antognolli
+ * Copyright © 2013      Jasper St. Pierre
+ * Copyright © 2010-2013 Intel Corporation
+ * Copyright © 2015-2017 Samsung Electronics Co., Ltd
+ * Copyright © 2015-2017 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * 
+ */ +struct wl_output; +struct wl_seat; +struct wl_surface; +struct xdg_popup; +struct xdg_positioner; +struct xdg_surface; +struct xdg_toplevel; +struct xdg_wm_base; + +#ifndef XDG_WM_BASE_INTERFACE +#define XDG_WM_BASE_INTERFACE +/** + * @page page_iface_xdg_wm_base xdg_wm_base + * @section page_iface_xdg_wm_base_desc Description + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + * @section page_iface_xdg_wm_base_api API + * See @ref iface_xdg_wm_base. + */ +/** + * @defgroup iface_xdg_wm_base The xdg_wm_base interface + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + */ +extern const struct wl_interface xdg_wm_base_interface; +#endif +#ifndef XDG_POSITIONER_INTERFACE +#define XDG_POSITIONER_INTERFACE +/** + * @page page_iface_xdg_positioner xdg_positioner + * @section page_iface_xdg_positioner_desc Description + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an invalid_positioner error. + * @section page_iface_xdg_positioner_api API + * See @ref iface_xdg_positioner. + */ +/** + * @defgroup iface_xdg_positioner The xdg_positioner interface + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an invalid_positioner error. + */ +extern const struct wl_interface xdg_positioner_interface; +#endif +#ifndef XDG_SURFACE_INTERFACE +#define XDG_SURFACE_INTERFACE +/** + * @page page_iface_xdg_surface xdg_surface + * @section page_iface_xdg_surface_desc Description + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * After creating a role-specific object and setting it up (e.g. by sending + * the title, app ID, size constraints, parent, etc), the client must + * perform an initial commit without any buffer attached. The compositor + * will reply with initial wl_surface state such as + * wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + * event. The client must acknowledge it and is then allowed to attach a + * buffer to map the surface. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed, i.e. the client must perform the initial commit + * again before attaching a buffer. + * @section page_iface_xdg_surface_api API + * See @ref iface_xdg_surface. + */ +/** + * @defgroup iface_xdg_surface The xdg_surface interface + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * After creating a role-specific object and setting it up (e.g. by sending + * the title, app ID, size constraints, parent, etc), the client must + * perform an initial commit without any buffer attached. The compositor + * will reply with initial wl_surface state such as + * wl_surface.preferred_buffer_scale followed by an xdg_surface.configure + * event. The client must acknowledge it and is then allowed to attach a + * buffer to map the surface. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed, i.e. the client must perform the initial commit + * again before attaching a buffer. + */ +extern const struct wl_interface xdg_surface_interface; +#endif +#ifndef XDG_TOPLEVEL_INTERFACE +#define XDG_TOPLEVEL_INTERFACE +/** + * @page page_iface_xdg_toplevel xdg_toplevel + * @section page_iface_xdg_toplevel_desc Description + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * A xdg_toplevel by default is responsible for providing the full intended + * visual representation of the toplevel, which depending on the window + * state, may mean things like a title bar, window controls and drop shadow. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + * the state it had right after xdg_surface.get_toplevel. The client + * can re-map the toplevel by performing a commit without any buffer + * attached, waiting for a configure event and handling it as usual (see + * xdg_surface description). + * + * Attaching a null buffer to a toplevel unmaps the surface. + * @section page_iface_xdg_toplevel_api API + * See @ref iface_xdg_toplevel. + */ +/** + * @defgroup iface_xdg_toplevel The xdg_toplevel interface + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * A xdg_toplevel by default is responsible for providing the full intended + * visual representation of the toplevel, which depending on the window + * state, may mean things like a title bar, window controls and drop shadow. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to + * the state it had right after xdg_surface.get_toplevel. The client + * can re-map the toplevel by performing a commit without any buffer + * attached, waiting for a configure event and handling it as usual (see + * xdg_surface description). + * + * Attaching a null buffer to a toplevel unmaps the surface. + */ +extern const struct wl_interface xdg_toplevel_interface; +#endif +#ifndef XDG_POPUP_INTERFACE +#define XDG_POPUP_INTERFACE +/** + * @page page_iface_xdg_popup xdg_popup + * @section page_iface_xdg_popup_desc Description + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + * @section page_iface_xdg_popup_api API + * See @ref iface_xdg_popup. + */ +/** + * @defgroup iface_xdg_popup The xdg_popup interface + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + */ +extern const struct wl_interface xdg_popup_interface; +#endif + +#ifndef XDG_WM_BASE_ERROR_ENUM +#define XDG_WM_BASE_ERROR_ENUM +enum xdg_wm_base_error { + /** + * given wl_surface has another role + */ + XDG_WM_BASE_ERROR_ROLE = 0, + /** + * xdg_wm_base was destroyed before children + */ + XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, + /** + * the client tried to map or destroy a non-topmost popup + */ + XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, + /** + * the client specified an invalid popup parent surface + */ + XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, + /** + * the client provided an invalid surface state + */ + XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, + /** + * the client provided an invalid positioner + */ + XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, + /** + * the client didn’t respond to a ping event in time + */ + XDG_WM_BASE_ERROR_UNRESPONSIVE = 6, +}; +#endif /* XDG_WM_BASE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_wm_base + * @struct xdg_wm_base_listener + */ +struct xdg_wm_base_listener { + /** + * check if the client is alive + * + * The ping event asks the client if it's still alive. Pass the + * serial specified in the event back to the compositor by sending + * a "pong" request back with the specified serial. See + * xdg_wm_base.pong. + * + * Compositors can use this to determine if the client is still + * alive. It's unspecified what will happen if the client doesn't + * respond to the ping request, or in what timeframe. Clients + * should try to respond in a reasonable amount of time. The + * “unresponsive” error is provided for compositors that wish + * to disconnect unresponsive clients. + * + * A compositor is free to ping in any way it wants, but a client + * must always respond to any xdg_wm_base object it created. + * @param serial pass this to the pong request + */ + void (*ping)(void *data, + struct xdg_wm_base *xdg_wm_base, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_wm_base + */ +static inline int +xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, + const struct xdg_wm_base_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_wm_base, + (void (**)(void)) listener, data); +} + +#define XDG_WM_BASE_DESTROY 0 +#define XDG_WM_BASE_CREATE_POSITIONER 1 +#define XDG_WM_BASE_GET_XDG_SURFACE 2 +#define XDG_WM_BASE_PONG 3 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PING_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PONG_SINCE_VERSION 1 + +/** @ingroup iface_xdg_wm_base */ +static inline void +xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data); +} + +/** @ingroup iface_xdg_wm_base */ +static inline void * +xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_wm_base); +} + +static inline uint32_t +xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Destroy this xdg_wm_base object. + * + * Destroying a bound xdg_wm_base object while there are surfaces + * still alive created by this xdg_wm_base object instance is illegal + * and will result in a defunct_surfaces error. + */ +static inline void +xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Create a positioner object. A positioner object is used to position + * surfaces relative to some parent surface. See the interface description + * and xdg_surface.get_popup for details. + */ +static inline struct xdg_positioner * +xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_CREATE_POSITIONER, &xdg_positioner_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL); + + return (struct xdg_positioner *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * This creates an xdg_surface for the given surface. While xdg_surface + * itself is not a role, the corresponding surface may only be assigned + * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is + * illegal to create an xdg_surface for a wl_surface which already has an + * assigned role and this will result in a role error. + * + * This creates an xdg_surface for the given surface. An xdg_surface is + * used as basis to define a role to a given surface, such as xdg_toplevel + * or xdg_popup. It also manages functionality shared between xdg_surface + * based surface roles. + * + * See the documentation of xdg_surface for more details about what an + * xdg_surface is and how it is used. + */ +static inline struct xdg_surface * +xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_GET_XDG_SURFACE, &xdg_surface_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL, surface); + + return (struct xdg_surface *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * A client must respond to a ping event with a pong request or + * the client may be deemed unresponsive. See xdg_wm_base.ping + * and xdg_wm_base.error.unresponsive. + */ +static inline void +xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_PONG, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, serial); +} + +#ifndef XDG_POSITIONER_ERROR_ENUM +#define XDG_POSITIONER_ERROR_ENUM +enum xdg_positioner_error { + /** + * invalid input provided + */ + XDG_POSITIONER_ERROR_INVALID_INPUT = 0, +}; +#endif /* XDG_POSITIONER_ERROR_ENUM */ + +#ifndef XDG_POSITIONER_ANCHOR_ENUM +#define XDG_POSITIONER_ANCHOR_ENUM +enum xdg_positioner_anchor { + XDG_POSITIONER_ANCHOR_NONE = 0, + XDG_POSITIONER_ANCHOR_TOP = 1, + XDG_POSITIONER_ANCHOR_BOTTOM = 2, + XDG_POSITIONER_ANCHOR_LEFT = 3, + XDG_POSITIONER_ANCHOR_RIGHT = 4, + XDG_POSITIONER_ANCHOR_TOP_LEFT = 5, + XDG_POSITIONER_ANCHOR_BOTTOM_LEFT = 6, + XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, + XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_ANCHOR_ENUM */ + +#ifndef XDG_POSITIONER_GRAVITY_ENUM +#define XDG_POSITIONER_GRAVITY_ENUM +enum xdg_positioner_gravity { + XDG_POSITIONER_GRAVITY_NONE = 0, + XDG_POSITIONER_GRAVITY_TOP = 1, + XDG_POSITIONER_GRAVITY_BOTTOM = 2, + XDG_POSITIONER_GRAVITY_LEFT = 3, + XDG_POSITIONER_GRAVITY_RIGHT = 4, + XDG_POSITIONER_GRAVITY_TOP_LEFT = 5, + XDG_POSITIONER_GRAVITY_BOTTOM_LEFT = 6, + XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, + XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_GRAVITY_ENUM */ + +#ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +#define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +/** + * @ingroup iface_xdg_positioner + * constraint adjustments + * + * The constraint adjustment value define ways the compositor will adjust + * the position of the surface, if the unadjusted position would result + * in the surface being partly constrained. + * + * Whether a surface is considered 'constrained' is left to the compositor + * to determine. For example, the surface may be partly outside the + * compositor's defined 'work area', thus necessitating the child surface's + * position be adjusted until it is entirely inside the work area. + * + * The adjustments can be combined, according to a defined precedence: 1) + * Flip, 2) Slide, 3) Resize. + */ +enum xdg_positioner_constraint_adjustment { + /** + * don't move the child surface when constrained + * + * Don't alter the surface position even if it is constrained on + * some axis, for example partially outside the edge of an output. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, + /** + * move along the x axis until unconstrained + * + * Slide the surface along the x axis until it is no longer + * constrained. + * + * First try to slide towards the direction of the gravity on the x + * axis until either the edge in the opposite direction of the + * gravity is unconstrained or the edge in the direction of the + * gravity is constrained. + * + * Then try to slide towards the opposite direction of the gravity + * on the x axis until either the edge in the direction of the + * gravity is unconstrained or the edge in the opposite direction + * of the gravity is constrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, + /** + * move along the y axis until unconstrained + * + * Slide the surface along the y axis until it is no longer + * constrained. + * + * First try to slide towards the direction of the gravity on the y + * axis until either the edge in the opposite direction of the + * gravity is unconstrained or the edge in the direction of the + * gravity is constrained. + * + * Then try to slide towards the opposite direction of the gravity + * on the y axis until either the edge in the direction of the + * gravity is unconstrained or the edge in the opposite direction + * of the gravity is constrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, + /** + * invert the anchor and gravity on the x axis + * + * Invert the anchor and gravity on the x axis if the surface is + * constrained on the x axis. For example, if the left edge of the + * surface is constrained, the gravity is 'left' and the anchor is + * 'left', change the gravity to 'right' and the anchor to 'right'. + * + * If the adjusted position also ends up being constrained, the + * resulting position of the flip_x adjustment will be the one + * before the adjustment. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, + /** + * invert the anchor and gravity on the y axis + * + * Invert the anchor and gravity on the y axis if the surface is + * constrained on the y axis. For example, if the bottom edge of + * the surface is constrained, the gravity is 'bottom' and the + * anchor is 'bottom', change the gravity to 'top' and the anchor + * to 'top'. + * + * The adjusted position is calculated given the original anchor + * rectangle and offset, but with the new flipped anchor and + * gravity values. + * + * If the adjusted position also ends up being constrained, the + * resulting position of the flip_y adjustment will be the one + * before the adjustment. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, + /** + * horizontally resize the surface + * + * Resize the surface horizontally so that it is completely + * unconstrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, + /** + * vertically resize the surface + * + * Resize the surface vertically so that it is completely + * unconstrained. + */ + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, +}; +#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ + +#define XDG_POSITIONER_DESTROY 0 +#define XDG_POSITIONER_SET_SIZE 1 +#define XDG_POSITIONER_SET_ANCHOR_RECT 2 +#define XDG_POSITIONER_SET_ANCHOR 3 +#define XDG_POSITIONER_SET_GRAVITY 4 +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT 5 +#define XDG_POSITIONER_SET_OFFSET 6 +#define XDG_POSITIONER_SET_REACTIVE 7 +#define XDG_POSITIONER_SET_PARENT_SIZE 8 +#define XDG_POSITIONER_SET_PARENT_CONFIGURE 9 + + +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3 + +/** @ingroup iface_xdg_positioner */ +static inline void +xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data); +} + +/** @ingroup iface_xdg_positioner */ +static inline void * +xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_positioner); +} + +static inline uint32_t +xdg_positioner_get_version(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_positioner); +} + +/** + * @ingroup iface_xdg_positioner + * + * Notify the compositor that the xdg_positioner will no longer be used. + */ +static inline void +xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the size of the surface that is to be positioned with the positioner + * object. The size is in surface-local coordinates and corresponds to the + * window geometry. See xdg_surface.set_window_geometry. + * + * If a zero or negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the anchor rectangle within the parent surface that the child + * surface will be placed relative to. The rectangle is relative to the + * window geometry as defined by xdg_surface.set_window_geometry of the + * parent surface. + * + * When the xdg_positioner object is used to position a child surface, the + * anchor rectangle may not extend outside the window geometry of the + * positioned child's parent surface. + * + * If a negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR_RECT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines the anchor point for the anchor rectangle. The specified anchor + * is used derive an anchor point that the child surface will be + * positioned relative to. If a corner anchor is set (e.g. 'top_left' or + * 'bottom_right'), the anchor point will be at the specified corner; + * otherwise, the derived anchor point will be centered on the specified + * edge, or in the center of the anchor rectangle if no edge is specified. + */ +static inline void +xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, anchor); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines in what direction a surface should be positioned, relative to + * the anchor point of the parent surface. If a corner gravity is + * specified (e.g. 'bottom_right' or 'top_left'), then the child surface + * will be placed towards the specified gravity; otherwise, the child + * surface will be centered over the anchor point on any axis that had no + * gravity specified. If the gravity is not in the ‘gravity’ enum, an + * invalid_input error is raised. + */ +static inline void +xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_GRAVITY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, gravity); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify how the window should be positioned if the originally intended + * position caused the surface to be constrained, meaning at least + * partially outside positioning boundaries set by the compositor. The + * adjustment is set by constructing a bitmask describing the adjustment to + * be made when the surface is constrained on that axis. + * + * If no bit for one axis is set, the compositor will assume that the child + * surface should not change its position on that axis when constrained. + * + * If more than one bit for one axis is set, the order of how adjustments + * are applied is specified in the corresponding adjustment descriptions. + * + * The default adjustment is none. + */ +static inline void +xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, constraint_adjustment); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the surface position offset relative to the position of the + * anchor on the anchor rectangle and the anchor on the surface. For + * example if the anchor of the anchor rectangle is at (x, y), the surface + * has the gravity bottom|right, and the offset is (ox, oy), the calculated + * surface position will be (x + ox, y + oy). The offset position of the + * surface is the one used for constraint testing. See + * set_constraint_adjustment. + * + * An example use case is placing a popup menu on top of a user interface + * element, while aligning the user interface element of the parent surface + * with some user interface element placed somewhere in the popup surface. + */ +static inline void +xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y); +} + +/** + * @ingroup iface_xdg_positioner + * + * When set reactive, the surface is reconstrained if the conditions used + * for constraining changed, e.g. the parent window moved. + * + * If the conditions changed and the popup was reconstrained, an + * xdg_popup.configure event is sent with updated geometry, followed by an + * xdg_surface.configure event. + */ +static inline void +xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_REACTIVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the parent window geometry the compositor should use when + * positioning the popup. The compositor may use this information to + * determine the future state the popup should be constrained using. If + * this doesn't match the dimension of the parent the popup is eventually + * positioned against, the behavior is undefined. + * + * The arguments are given in the surface-local coordinate space. + */ +static inline void +xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t parent_width, int32_t parent_height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_PARENT_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, parent_width, parent_height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the serial of an xdg_surface.configure event this positioner will be + * used in response to. The compositor may use this information together + * with set_parent_size to determine what future state the popup should be + * constrained using. + */ +static inline void +xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_PARENT_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, serial); +} + +#ifndef XDG_SURFACE_ERROR_ENUM +#define XDG_SURFACE_ERROR_ENUM +enum xdg_surface_error { + /** + * Surface was not fully constructed + */ + XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, + /** + * Surface was already constructed + */ + XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, + /** + * Attaching a buffer to an unconfigured surface + */ + XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, + /** + * Invalid serial number when acking a configure event + */ + XDG_SURFACE_ERROR_INVALID_SERIAL = 4, + /** + * Width or height was zero or negative + */ + XDG_SURFACE_ERROR_INVALID_SIZE = 5, + /** + * Surface was destroyed before its role object + */ + XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6, +}; +#endif /* XDG_SURFACE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_surface + * @struct xdg_surface_listener + */ +struct xdg_surface_listener { + /** + * suggest a surface change + * + * The configure event marks the end of a configure sequence. A + * configure sequence is a set of one or more events configuring + * the state of the xdg_surface, including the final + * xdg_surface.configure event. + * + * Where applicable, xdg_surface surface roles will during a + * configure sequence extend this event as a latched state sent as + * events before the xdg_surface.configure event. Such events + * should be considered to make up a set of atomically applied + * configuration states, where the xdg_surface.configure commits + * the accumulated state. + * + * Clients should arrange their surface for the new states, and + * then send an ack_configure request with the serial sent in this + * configure event at some point before committing the new surface. + * + * If the client receives multiple configure events before it can + * respond to one, it is free to discard all but the last event it + * received. + * @param serial serial of the configure event + */ + void (*configure)(void *data, + struct xdg_surface *xdg_surface, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_surface + */ +static inline int +xdg_surface_add_listener(struct xdg_surface *xdg_surface, + const struct xdg_surface_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_surface, + (void (**)(void)) listener, data); +} + +#define XDG_SURFACE_DESTROY 0 +#define XDG_SURFACE_GET_TOPLEVEL 1 +#define XDG_SURFACE_GET_POPUP 2 +#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3 +#define XDG_SURFACE_ACK_CONFIGURE 4 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 + +/** @ingroup iface_xdg_surface */ +static inline void +xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data); +} + +/** @ingroup iface_xdg_surface */ +static inline void * +xdg_surface_get_user_data(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_surface); +} + +static inline uint32_t +xdg_surface_get_version(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_surface); +} + +/** + * @ingroup iface_xdg_surface + * + * Destroy the xdg_surface object. An xdg_surface must only be destroyed + * after its role object has been destroyed, otherwise + * a defunct_role_object error is raised. + */ +static inline void +xdg_surface_destroy(struct xdg_surface *xdg_surface) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_toplevel object for the given xdg_surface and gives + * the associated wl_surface the xdg_toplevel role. + * + * See the documentation of xdg_toplevel for more details about what an + * xdg_toplevel is and how it is used. + */ +static inline struct xdg_toplevel * +xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_TOPLEVEL, &xdg_toplevel_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL); + + return (struct xdg_toplevel *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_popup object for the given xdg_surface and gives + * the associated wl_surface the xdg_popup role. + * + * If null is passed as a parent, a parent surface must be specified using + * some other protocol, before committing the initial state. + * + * See the documentation of xdg_popup for more details about what an + * xdg_popup is and how it is used. + */ +static inline struct xdg_popup * +xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_POPUP, &xdg_popup_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL, parent, positioner); + + return (struct xdg_popup *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * The window geometry of a surface is its "visible bounds" from the + * user's perspective. Client-side decorations often have invisible + * portions like drop-shadows which should be ignored for the + * purposes of aligning, placing and constraining windows. + * + * The window geometry is double-buffered state, see wl_surface.commit. + * + * When maintaining a position, the compositor should treat the (x, y) + * coordinate of the window geometry as the top left corner of the window. + * A client changing the (x, y) window geometry coordinate should in + * general not alter the position of the window. + * + * Once the window geometry of the surface is set, it is not possible to + * unset it, and it will remain the same until set_window_geometry is + * called again, even if a new subsurface or buffer is attached. + * + * If never set, the value is the full bounds of the surface, + * including any subsurfaces. This updates dynamically on every + * commit. This unset is meant for extremely simple clients. + * + * The arguments are given in the surface-local coordinate space of + * the wl_surface associated with this xdg_surface, and may extend outside + * of the wl_surface itself to mark parts of the subsurface tree as part of + * the window geometry. + * + * When applied, the effective window geometry will be the set window + * geometry clamped to the bounding rectangle of the combined + * geometry of the surface of the xdg_surface and the associated + * subsurfaces. + * + * The effective geometry will not be recalculated unless a new call to + * set_window_geometry is done and the new pending surface state is + * subsequently applied. + * + * The width and height of the effective window geometry must be + * greater than zero. Setting an invalid size will raise an + * invalid_size error. + */ +static inline void +xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_SET_WINDOW_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, x, y, width, height); +} + +/** + * @ingroup iface_xdg_surface + * + * When a configure event is received, if a client commits the + * surface in response to the configure event, then the client + * must make an ack_configure request sometime before the commit + * request, passing along the serial of the configure event. + * + * For instance, for toplevel surfaces the compositor might use this + * information to move a surface to the top left only when the client has + * drawn itself for the maximized or fullscreen state. + * + * If the client receives multiple configure events before it + * can respond to one, it only has to ack the last configure event. + * Acking a configure event that was never sent raises an invalid_serial + * error. + * + * A client is not required to commit immediately after sending + * an ack_configure request - it may even ack_configure several times + * before its next surface commit. + * + * A client may send multiple ack_configure requests before committing, but + * only the last request sent before a commit indicates which configure + * event the client really is responding to. + * + * Sending an ack_configure request consumes the serial number sent with + * the request, as well as serial numbers sent by all configure events + * sent on this xdg_surface prior to the configure event referenced by + * the committed serial. + * + * It is an error to issue multiple ack_configure requests referencing a + * serial from the same configure event, or to issue an ack_configure + * request referencing a serial from a configure event issued before the + * event identified by the last ack_configure request for the same + * xdg_surface. Doing so will raise an invalid_serial error. + */ +static inline void +xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface, + XDG_SURFACE_ACK_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, serial); +} + +#ifndef XDG_TOPLEVEL_ERROR_ENUM +#define XDG_TOPLEVEL_ERROR_ENUM +enum xdg_toplevel_error { + /** + * provided value is not a valid variant of the resize_edge enum + */ + XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0, + /** + * invalid parent toplevel + */ + XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1, + /** + * client provided an invalid min or max size + */ + XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2, +}; +#endif /* XDG_TOPLEVEL_ERROR_ENUM */ + +#ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM +#define XDG_TOPLEVEL_RESIZE_EDGE_ENUM +/** + * @ingroup iface_xdg_toplevel + * edge values for resizing + * + * These values are used to indicate which edge of a surface + * is being dragged in a resize operation. + */ +enum xdg_toplevel_resize_edge { + XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, + XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM = 2, + XDG_TOPLEVEL_RESIZE_EDGE_LEFT = 4, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT = 5, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT = 6, + XDG_TOPLEVEL_RESIZE_EDGE_RIGHT = 8, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, +}; +#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ + +#ifndef XDG_TOPLEVEL_STATE_ENUM +#define XDG_TOPLEVEL_STATE_ENUM +/** + * @ingroup iface_xdg_toplevel + * types of state on the surface + * + * The different state values used on the surface. This is designed for + * state values like maximized, fullscreen. It is paired with the + * configure event to ensure that both the client and the compositor + * setting the state can be synchronized. + * + * States set in this way are double-buffered, see wl_surface.commit. + */ +enum xdg_toplevel_state { + /** + * the surface is maximized + * the surface is maximized + * + * The surface is maximized. The window geometry specified in the + * configure event must be obeyed by the client, or the + * xdg_wm_base.invalid_surface_state error is raised. + * + * The client should draw without shadow or other decoration + * outside of the window geometry. + */ + XDG_TOPLEVEL_STATE_MAXIMIZED = 1, + /** + * the surface is fullscreen + * the surface is fullscreen + * + * The surface is fullscreen. The window geometry specified in + * the configure event is a maximum; the client cannot resize + * beyond it. For a surface to cover the whole fullscreened area, + * the geometry dimensions must be obeyed by the client. For more + * details, see xdg_toplevel.set_fullscreen. + */ + XDG_TOPLEVEL_STATE_FULLSCREEN = 2, + /** + * the surface is being resized + * the surface is being resized + * + * The surface is being resized. The window geometry specified in + * the configure event is a maximum; the client cannot resize + * beyond it. Clients that have aspect ratio or cell sizing + * configuration can use a smaller size, however. + */ + XDG_TOPLEVEL_STATE_RESIZING = 3, + /** + * the surface is now activated + * the surface is now activated + * + * Client window decorations should be painted as if the window + * is active. Do not assume this means that the window actually has + * keyboard or pointer focus. + */ + XDG_TOPLEVEL_STATE_ACTIVATED = 4, + /** + * the surface’s left edge is tiled + * + * The window is currently in a tiled layout and the left edge is + * considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the left edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_LEFT = 5, + /** + * the surface’s right edge is tiled + * + * The window is currently in a tiled layout and the right edge + * is considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the right edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, + /** + * the surface’s top edge is tiled + * + * The window is currently in a tiled layout and the top edge is + * considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the top edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_TOP = 7, + /** + * the surface’s bottom edge is tiled + * + * The window is currently in a tiled layout and the bottom edge + * is considered to be adjacent to another part of the tiling grid. + * + * The client should draw without shadow or other decoration + * outside of the window geometry on the bottom edge. + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, + /** + * surface repaint is suspended + * + * The surface is currently not ordinarily being repainted; for + * example because its content is occluded by another window, or + * its outputs are switched off due to screen locking. + * @since 6 + */ + XDG_TOPLEVEL_STATE_SUSPENDED = 9, + /** + * the surface’s left edge is constrained + * + * The left edge of the window is currently constrained, meaning + * it shouldn't attempt to resize from that edge. It can for + * example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT = 10, + /** + * the surface’s right edge is constrained + * + * The right edge of the window is currently constrained, meaning + * it shouldn't attempt to resize from that edge. It can for + * example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT = 11, + /** + * the surface’s top edge is constrained + * + * The top edge of the window is currently constrained, meaning + * it shouldn't attempt to resize from that edge. It can for + * example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_TOP = 12, + /** + * the surface’s bottom edge is tiled + * + * The bottom edge of the window is currently constrained, + * meaning it shouldn't attempt to resize from that edge. It can + * for example mean it's tiled next to a monitor edge on the + * constrained side of the window. + * @since 7 + */ + XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM = 13, +}; +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION 6 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION 7 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT_SINCE_VERSION 7 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_TOP_SINCE_VERSION 7 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM_SINCE_VERSION 7 +#endif /* XDG_TOPLEVEL_STATE_ENUM */ + +#ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM +#define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM +enum xdg_toplevel_wm_capabilities { + /** + * show_window_menu is available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1, + /** + * set_maximized and unset_maximized are available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2, + /** + * set_fullscreen and unset_fullscreen are available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3, + /** + * set_minimized is available + */ + XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4, +}; +#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */ + +/** + * @ingroup iface_xdg_toplevel + * @struct xdg_toplevel_listener + */ +struct xdg_toplevel_listener { + /** + * suggest a surface change + * + * This configure event asks the client to resize its toplevel + * surface or to change its state. The configured state should not + * be applied immediately. See xdg_surface.configure for details. + * + * The width and height arguments specify a hint to the window + * about how its surface should be resized in window geometry + * coordinates. See set_window_geometry. + * + * If the width or height arguments are zero, it means the client + * should decide its own window dimension. This may happen when the + * compositor needs to configure the state of the surface but + * doesn't have any information about any previous or expected + * dimension. + * + * The states listed in the event specify how the width/height + * arguments should be interpreted, and possibly how it should be + * drawn. + * + * Clients must send an ack_configure in response to this event. + * See xdg_surface.configure and xdg_surface.ack_configure for + * details. + */ + void (*configure)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height, + struct wl_array *states); + /** + * surface wants to be closed + * + * The close event is sent by the compositor when the user wants + * the surface to be closed. This should be equivalent to the user + * clicking the close button in client-side decorations, if your + * application has any. + * + * This is only a request that the user intends to close the + * window. The client may choose to ignore this request, or show a + * dialog to ask the user to save their data, etc. + */ + void (*close)(void *data, + struct xdg_toplevel *xdg_toplevel); + /** + * recommended window geometry bounds + * + * The configure_bounds event may be sent prior to a + * xdg_toplevel.configure event to communicate the bounds a window + * geometry size is recommended to constrain to. + * + * The passed width and height are in surface coordinate space. If + * width and height are 0, it means bounds is unknown and + * equivalent to as if no configure_bounds event was ever sent for + * this surface. + * + * The bounds can for example correspond to the size of a monitor + * excluding any panels or other shell components, so that a + * surface isn't created in a way that it cannot fit. + * + * The bounds may change at any point, and in such a case, a new + * xdg_toplevel.configure_bounds will be sent, followed by + * xdg_toplevel.configure and xdg_surface.configure. + * @since 4 + */ + void (*configure_bounds)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height); + /** + * compositor capabilities + * + * This event advertises the capabilities supported by the + * compositor. If a capability isn't supported, clients should hide + * or disable the UI elements that expose this functionality. For + * instance, if the compositor doesn't advertise support for + * minimized toplevels, a button triggering the set_minimized + * request should not be displayed. + * + * The compositor will ignore requests it doesn't support. For + * instance, a compositor which doesn't advertise support for + * minimized will ignore set_minimized requests. + * + * Compositors must send this event once before the first + * xdg_surface.configure event. When the capabilities change, + * compositors must send this event again and then send an + * xdg_surface.configure event. + * + * The configured state should not be applied immediately. See + * xdg_surface.configure for details. + * + * The capabilities are sent as an array of 32-bit unsigned + * integers in native endianness. + * @param capabilities array of 32-bit capabilities + * @since 5 + */ + void (*wm_capabilities)(void *data, + struct xdg_toplevel *xdg_toplevel, + struct wl_array *capabilities); +}; + +/** + * @ingroup iface_xdg_toplevel + */ +static inline int +xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, + const struct xdg_toplevel_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_toplevel, + (void (**)(void)) listener, data); +} + +#define XDG_TOPLEVEL_DESTROY 0 +#define XDG_TOPLEVEL_SET_PARENT 1 +#define XDG_TOPLEVEL_SET_TITLE 2 +#define XDG_TOPLEVEL_SET_APP_ID 3 +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU 4 +#define XDG_TOPLEVEL_MOVE 5 +#define XDG_TOPLEVEL_RESIZE 6 +#define XDG_TOPLEVEL_SET_MAX_SIZE 7 +#define XDG_TOPLEVEL_SET_MIN_SIZE 8 +#define XDG_TOPLEVEL_SET_MAXIMIZED 9 +#define XDG_TOPLEVEL_UNSET_MAXIMIZED 10 +#define XDG_TOPLEVEL_SET_FULLSCREEN 11 +#define XDG_TOPLEVEL_UNSET_FULLSCREEN 12 +#define XDG_TOPLEVEL_SET_MINIMIZED 13 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 + +/** @ingroup iface_xdg_toplevel */ +static inline void +xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data); +} + +/** @ingroup iface_xdg_toplevel */ +static inline void * +xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_toplevel); +} + +static inline uint32_t +xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel); +} + +/** + * @ingroup iface_xdg_toplevel + * + * This request destroys the role surface and unmaps the surface; + * see "Unmapping" behavior in interface section for details. + */ +static inline void +xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set the "parent" of this surface. This surface should be stacked + * above the parent surface and all other ancestor surfaces. + * + * Parent surfaces should be set on dialogs, toolboxes, or other + * "auxiliary" surfaces, so that the parent is raised when the dialog + * is raised. + * + * Setting a null parent for a child surface unsets its parent. Setting + * a null parent for a surface which currently has no parent is a no-op. + * + * Only mapped surfaces can have child surfaces. Setting a parent which + * is not mapped is equivalent to setting a null parent. If a surface + * becomes unmapped, its children's parent is set to the parent of + * the now-unmapped surface. If the now-unmapped surface has no parent, + * its children's parent is unset. If the now-unmapped surface becomes + * mapped again, its parent-child relationship is not restored. + * + * The parent toplevel must not be one of the child toplevel's + * descendants, and the parent must be different from the child toplevel, + * otherwise the invalid_parent protocol error is raised. + */ +static inline void +xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_PARENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, parent); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a short title for the surface. + * + * This string may be used to identify the surface in a task bar, + * window list, or other user interface elements provided by the + * compositor. + * + * The string must be encoded in UTF-8. + */ +static inline void +xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, title); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set an application identifier for the surface. + * + * The app ID identifies the general class of applications to which + * the surface belongs. The compositor can use this to group multiple + * surfaces together, or to determine how to launch a new application. + * + * For D-Bus activatable applications, the app ID is used as the D-Bus + * service name. + * + * The compositor shell will try to group application surfaces together + * by their app ID. As a best practice, it is suggested to select app + * ID's that match the basename of the application's .desktop file. + * For example, "org.freedesktop.FooViewer" where the .desktop file is + * "org.freedesktop.FooViewer.desktop". + * + * Like other properties, a set_app_id request can be sent after the + * xdg_toplevel has been mapped to update the property. + * + * See the desktop-entry specification [0] for more details on + * application identifiers and how they relate to well-known D-Bus + * names and .desktop files. + * + * [0] https://standards.freedesktop.org/desktop-entry-spec/ + */ +static inline void +xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, app_id); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Clients implementing client-side decorations might want to show + * a context menu when right-clicking on the decorations, giving the + * user a menu that they can use to maximize or minimize the window. + * + * This request asks the compositor to pop up such a window menu at + * the given position, relative to the local surface coordinates of + * the parent surface. There are no guarantees as to what menu items + * the window menu contains, or even if a window menu will be drawn + * at all. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. + */ +static inline void +xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SHOW_WINDOW_MENU, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, x, y); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start an interactive, user-driven move of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive move (touch, + * pointer, etc). + * + * The server may ignore move requests depending on the state of + * the surface (e.g. fullscreen or maximized), or if the passed serial + * is no longer valid. + * + * If triggered, the surface will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the move. It is up to the + * compositor to visually indicate that the move is taking place, such as + * updating a pointer cursor, during the move. There is no guarantee + * that the device focus will return when the move is completed. + */ +static inline void +xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start a user-driven, interactive resize of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive resize (touch, + * pointer, etc). + * + * The server may ignore resize requests depending on the state of + * the surface (e.g. fullscreen or maximized). + * + * If triggered, the client will receive configure events with the + * "resize" state enum value and the expected sizes. See the "resize" + * enum value for more details about what is required. The client + * must also acknowledge configure events using "ack_configure". After + * the resize is completed, the client will receive another "configure" + * event without the resize state. + * + * If triggered, the surface also will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the resize. It is up to the + * compositor to visually indicate that the resize is taking place, + * such as updating a pointer cursor, during the resize. There is no + * guarantee that the device focus will return when the resize is + * completed. + * + * The edges parameter specifies how the surface should be resized, and + * is one of the values of the resize_edge enum. Values not matching + * a variant of the enum will cause the invalid_resize_edge protocol error. + * The compositor may use this information to update the surface position + * for example when dragging the top left corner. The compositor may also + * use this information to adapt its behavior, e.g. choose an appropriate + * cursor image. + */ +static inline void +xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, edges); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a maximum size for the window. + * + * The client can specify a maximum size so that the compositor does + * not try to configure the window beyond this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered, see wl_surface.commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the maximum + * size. The compositor may decide to ignore the values set by the + * client and request a larger size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected maximum size in the given dimension. + * As a result, a client wishing to reset the maximum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a maximum size to be smaller than the minimum size of + * a surface is illegal and will result in an invalid_size error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width or height will result in a + * invalid_size error. + */ +static inline void +xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAX_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a minimum size for the window. + * + * The client can specify a minimum size so that the compositor does + * not try to configure the window below this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered, see wl_surface.commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the minimum + * size. The compositor may decide to ignore the values set by the + * client and request a smaller size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected minimum size in the given dimension. + * As a result, a client wishing to reset the minimum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a minimum size to be larger than the maximum size of + * a surface is illegal and will result in an invalid_size error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width and height will result in a + * invalid_size error. + */ +static inline void +xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MIN_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Maximize the surface. + * + * After requesting that the surface should be maximized, the compositor + * will respond by emitting a configure event. Whether this configure + * actually sets the window maximized is subject to compositor policies. + * The client must then update its content, drawing in the configured + * state. The client must also acknowledge the configure when committing + * the new content (see ack_configure). + * + * It is up to the compositor to decide how and where to maximize the + * surface, for example which output and what region of the screen should + * be used. + * + * If the surface was already maximized, the compositor will still emit + * a configure event with the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Unmaximize the surface. + * + * After requesting that the surface should be unmaximized, the compositor + * will respond by emitting a configure event. Whether this actually + * un-maximizes the window is subject to compositor policies. + * If available and applicable, the compositor will include the window + * geometry dimensions the window had prior to being maximized in the + * configure event. The client must then update its content, drawing it in + * the configured state. The client must also acknowledge the configure + * when committing the new content (see ack_configure). + * + * It is up to the compositor to position the surface after it was + * unmaximized; usually the position the surface had before maximizing, if + * applicable. + * + * If the surface was already not maximized, the compositor will still + * emit a configure event without the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface fullscreen. + * + * After requesting that the surface should be fullscreened, the + * compositor will respond by emitting a configure event. Whether the + * client is actually put into a fullscreen state is subject to compositor + * policies. The client must also acknowledge the configure when + * committing the new content (see ack_configure). + * + * The output passed by the request indicates the client's preference as + * to which display it should be set fullscreen on. If this value is NULL, + * it's up to the compositor to choose which display will be used to map + * this surface. + * + * If the surface doesn't cover the whole output, the compositor will + * position the surface in the center of the output and compensate with + * with border fill covering the rest of the output. The content of the + * border fill is undefined, but should be assumed to be in some way that + * attempts to blend into the surrounding area (e.g. solid black). + * + * If the fullscreened surface is not opaque, the compositor must make + * sure that other screen content not part of the same surface tree (made + * up of subsurfaces, popups or similarly coupled surfaces) are not + * visible below the fullscreened surface. + */ +static inline void +xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, output); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface no longer fullscreen. + * + * After requesting that the surface should be unfullscreened, the + * compositor will respond by emitting a configure event. + * Whether this actually removes the fullscreen state of the client is + * subject to compositor policies. + * + * Making a surface unfullscreen sets states for the surface based on the following: + * * the state(s) it may have had before becoming fullscreen + * * any state(s) decided by the compositor + * * any state(s) requested by the client while the surface was fullscreen + * + * The compositor may include the previous window geometry dimensions in + * the configure event, if applicable. + * + * The client must also acknowledge the configure when committing the new + * content (see ack_configure). + */ +static inline void +xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Request that the compositor minimize your surface. There is no + * way to know if the surface is currently minimized, nor is there + * any way to unset minimization on this surface. + * + * If you are looking to throttle redrawing when minimized, please + * instead use the wl_surface.frame event for this, as this will + * also work with live previews on windows in Alt-Tab, Expose or + * similar compositor features. + */ +static inline void +xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0); +} + +#ifndef XDG_POPUP_ERROR_ENUM +#define XDG_POPUP_ERROR_ENUM +enum xdg_popup_error { + /** + * tried to grab after being mapped + */ + XDG_POPUP_ERROR_INVALID_GRAB = 0, +}; +#endif /* XDG_POPUP_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_popup + * @struct xdg_popup_listener + */ +struct xdg_popup_listener { + /** + * configure the popup surface + * + * This event asks the popup surface to configure itself given + * the configuration. The configured state should not be applied + * immediately. See xdg_surface.configure for details. + * + * The x and y arguments represent the position the popup was + * placed at given the xdg_positioner rule, relative to the upper + * left corner of the window geometry of the parent surface. + * + * For version 2 or older, the configure event for an xdg_popup is + * only ever sent once for the initial configuration. Starting with + * version 3, it may be sent again if the popup is setup with an + * xdg_positioner with set_reactive requested, or in response to + * xdg_popup.reposition requests. + * @param x x position relative to parent surface window geometry + * @param y y position relative to parent surface window geometry + * @param width window geometry width + * @param height window geometry height + */ + void (*configure)(void *data, + struct xdg_popup *xdg_popup, + int32_t x, + int32_t y, + int32_t width, + int32_t height); + /** + * popup interaction is done + * + * The popup_done event is sent out when a popup is dismissed by + * the compositor. The client should destroy the xdg_popup object + * at this point. + */ + void (*popup_done)(void *data, + struct xdg_popup *xdg_popup); + /** + * signal the completion of a repositioned request + * + * The repositioned event is sent as part of a popup + * configuration sequence, together with xdg_popup.configure and + * lastly xdg_surface.configure to notify the completion of a + * reposition request. + * + * The repositioned event is to notify about the completion of a + * xdg_popup.reposition request. The token argument is the token + * passed in the xdg_popup.reposition request. + * + * Immediately after this event is emitted, xdg_popup.configure and + * xdg_surface.configure will be sent with the updated size and + * position, as well as a new configure serial. + * + * The client should optionally update the content of the popup, + * but must acknowledge the new popup configuration for the new + * position to take effect. See xdg_surface.ack_configure for + * details. + * @param token reposition request token + * @since 3 + */ + void (*repositioned)(void *data, + struct xdg_popup *xdg_popup, + uint32_t token); +}; + +/** + * @ingroup iface_xdg_popup + */ +static inline int +xdg_popup_add_listener(struct xdg_popup *xdg_popup, + const struct xdg_popup_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_popup, + (void (**)(void)) listener, data); +} + +#define XDG_POPUP_DESTROY 0 +#define XDG_POPUP_GRAB 1 +#define XDG_POPUP_REPOSITION 2 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_GRAB_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_REPOSITION_SINCE_VERSION 3 + +/** @ingroup iface_xdg_popup */ +static inline void +xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data); +} + +/** @ingroup iface_xdg_popup */ +static inline void * +xdg_popup_get_user_data(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_popup); +} + +static inline uint32_t +xdg_popup_get_version(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_popup); +} + +/** + * @ingroup iface_xdg_popup + * + * This destroys the popup. Explicitly destroying the xdg_popup + * object will also dismiss the popup, and unmap the surface. + * + * If this xdg_popup is not the "topmost" popup, the + * xdg_wm_base.not_the_topmost_popup protocol error will be sent. + */ +static inline void +xdg_popup_destroy(struct xdg_popup *xdg_popup) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_xdg_popup + * + * This request makes the created popup take an explicit grab. An explicit + * grab will be dismissed when the user dismisses the popup, or when the + * client destroys the xdg_popup. This can be done by the user clicking + * outside the surface, using the keyboard, or even locking the screen + * through closing the lid or a timeout. + * + * If the compositor denies the grab, the popup will be immediately + * dismissed. + * + * This request must be used in response to some sort of user action like a + * button press, key press, or touch down event. The serial number of the + * event should be passed as 'serial'. + * + * The parent of a grabbing popup must either be an xdg_toplevel surface or + * another xdg_popup with an explicit grab. If the parent is another + * xdg_popup it means that the popups are nested, with this popup now being + * the topmost popup. + * + * Nested popups must be destroyed in the reverse order they were created + * in, e.g. the only popup you are allowed to destroy at all times is the + * topmost one. + * + * When compositors choose to dismiss a popup, they may dismiss every + * nested grabbing popup as well. When a compositor dismisses popups, it + * will follow the same dismissing order as required from the client. + * + * If the topmost grabbing popup is destroyed, the grab will be returned to + * the parent of the popup, if that parent previously had an explicit grab. + * + * If the parent is a grabbing popup which has already been dismissed, this + * popup will be immediately dismissed. If the parent is a popup that did + * not take an explicit grab, an error will be raised. + * + * During a popup grab, the client owning the grab will receive pointer + * and touch events for all their surfaces as normal (similar to an + * "owner-events" grab in X11 parlance), while the top most grabbing popup + * will always have keyboard focus. + */ +static inline void +xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_GRAB, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, seat, serial); +} + +/** + * @ingroup iface_xdg_popup + * + * Reposition an already-mapped popup. The popup will be placed given the + * details in the passed xdg_positioner object, and a + * xdg_popup.repositioned followed by xdg_popup.configure and + * xdg_surface.configure will be emitted in response. Any parameters set + * by the previous positioner will be discarded. + * + * The passed token will be sent in the corresponding + * xdg_popup.repositioned event. The new popup position will not take + * effect until the corresponding configure event is acknowledged by the + * client. See xdg_popup.repositioned for details. The token itself is + * opaque, and has no other special meaning. + * + * If multiple reposition requests are sent, the compositor may skip all + * but the last one. + * + * If the popup is repositioned in response to a configure event for its + * parent, the client should send an xdg_positioner.set_parent_configure + * and possibly an xdg_positioner.set_parent_size request to allow the + * compositor to properly constrain the popup. + * + * If the popup is repositioned together with a parent that is being + * resized, but not in response to a configure event, the client should + * send an xdg_positioner.set_parent_size request. + */ +static inline void +xdg_popup_reposition(struct xdg_popup *xdg_popup, struct xdg_positioner *positioner, uint32_t token) +{ + wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup, + XDG_POPUP_REPOSITION, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, positioner, token); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/platform/generated/xdg-shell-protocol.c b/src/platform/generated/xdg-shell-protocol.c new file mode 100644 index 0000000..260a89e --- /dev/null +++ b/src/platform/generated/xdg-shell-protocol.c @@ -0,0 +1,183 @@ +/* Generated by wayland-scanner 1.22.0 */ + +/* + * Copyright © 2008-2013 Kristian Høgsberg + * Copyright © 2013 Rafael Antognolli + * Copyright © 2013 Jasper St. Pierre + * Copyright © 2010-2013 Intel Corporation + * Copyright © 2015-2017 Samsung Electronics Co., Ltd + * Copyright © 2015-2017 Red Hat Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface xdg_popup_interface; +extern const struct wl_interface xdg_positioner_interface; +extern const struct wl_interface xdg_surface_interface; +extern const struct wl_interface xdg_toplevel_interface; + +static const struct wl_interface *xdg_shell_types[] = { + NULL, + NULL, + NULL, + NULL, + &xdg_positioner_interface, + &xdg_surface_interface, + &wl_surface_interface, + &xdg_toplevel_interface, + &xdg_popup_interface, + &xdg_surface_interface, + &xdg_positioner_interface, + &xdg_toplevel_interface, + &wl_seat_interface, + NULL, + NULL, + NULL, + &wl_seat_interface, + NULL, + &wl_seat_interface, + NULL, + NULL, + &wl_output_interface, + &wl_seat_interface, + NULL, + &xdg_positioner_interface, + NULL, +}; + +static const struct wl_message xdg_wm_base_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "create_positioner", "n", xdg_shell_types + 4 }, + { "get_xdg_surface", "no", xdg_shell_types + 5 }, + { "pong", "u", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_wm_base_events[] = { + { "ping", "u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_wm_base_interface = { + "xdg_wm_base", 7, + 4, xdg_wm_base_requests, + 1, xdg_wm_base_events, +}; + +static const struct wl_message xdg_positioner_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "set_size", "ii", xdg_shell_types + 0 }, + { "set_anchor_rect", "iiii", xdg_shell_types + 0 }, + { "set_anchor", "u", xdg_shell_types + 0 }, + { "set_gravity", "u", xdg_shell_types + 0 }, + { "set_constraint_adjustment", "u", xdg_shell_types + 0 }, + { "set_offset", "ii", xdg_shell_types + 0 }, + { "set_reactive", "3", xdg_shell_types + 0 }, + { "set_parent_size", "3ii", xdg_shell_types + 0 }, + { "set_parent_configure", "3u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_positioner_interface = { + "xdg_positioner", 7, + 10, xdg_positioner_requests, + 0, NULL, +}; + +static const struct wl_message xdg_surface_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "get_toplevel", "n", xdg_shell_types + 7 }, + { "get_popup", "n?oo", xdg_shell_types + 8 }, + { "set_window_geometry", "iiii", xdg_shell_types + 0 }, + { "ack_configure", "u", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_surface_events[] = { + { "configure", "u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_surface_interface = { + "xdg_surface", 7, + 5, xdg_surface_requests, + 1, xdg_surface_events, +}; + +static const struct wl_message xdg_toplevel_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "set_parent", "?o", xdg_shell_types + 11 }, + { "set_title", "s", xdg_shell_types + 0 }, + { "set_app_id", "s", xdg_shell_types + 0 }, + { "show_window_menu", "ouii", xdg_shell_types + 12 }, + { "move", "ou", xdg_shell_types + 16 }, + { "resize", "ouu", xdg_shell_types + 18 }, + { "set_max_size", "ii", xdg_shell_types + 0 }, + { "set_min_size", "ii", xdg_shell_types + 0 }, + { "set_maximized", "", xdg_shell_types + 0 }, + { "unset_maximized", "", xdg_shell_types + 0 }, + { "set_fullscreen", "?o", xdg_shell_types + 21 }, + { "unset_fullscreen", "", xdg_shell_types + 0 }, + { "set_minimized", "", xdg_shell_types + 0 }, +}; + +static const struct wl_message xdg_toplevel_events[] = { + { "configure", "iia", xdg_shell_types + 0 }, + { "close", "", xdg_shell_types + 0 }, + { "configure_bounds", "4ii", xdg_shell_types + 0 }, + { "wm_capabilities", "5a", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_toplevel_interface = { + "xdg_toplevel", 7, + 14, xdg_toplevel_requests, + 4, xdg_toplevel_events, +}; + +static const struct wl_message xdg_popup_requests[] = { + { "destroy", "", xdg_shell_types + 0 }, + { "grab", "ou", xdg_shell_types + 22 }, + { "reposition", "3ou", xdg_shell_types + 24 }, +}; + +static const struct wl_message xdg_popup_events[] = { + { "configure", "iiii", xdg_shell_types + 0 }, + { "popup_done", "", xdg_shell_types + 0 }, + { "repositioned", "3u", xdg_shell_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_popup_interface = { + "xdg_popup", 7, + 3, xdg_popup_requests, + 3, xdg_popup_events, +}; + diff --git a/src/platform/wayland.zig b/src/platform/wayland.zig new file mode 100644 index 0000000..ece047d --- /dev/null +++ b/src/platform/wayland.zig @@ -0,0 +1,896 @@ +//! Wayland platform backend for flair-ui windows. +//! +//! Uses libwayland-client and xdg-shell to create a Wayland surface, +//! then creates a VkSurfaceKHR from it for Vulkan presentation. +//! +//! Input is handled via wl_seat → wl_keyboard + wl_pointer. + +const std = @import("std"); +const input = @import("../input.zig"); +const Event = input.Event; +const Key = input.Key; +const Modifiers = input.Modifiers; +const MouseButton = input.MouseButton; +const window_mod = @import("../window.zig"); +const Window = window_mod.Window; +const WindowVTable = window_mod.WindowVTable; +const surface_mod = @import("../surface.zig"); +const Surface = surface_mod.Surface; +const color_mod = @import("../color.zig"); +const vk_mod = @import("../vulkan.zig"); +const vk = vk_mod.vk; +const c_vk = vk_mod.c; +const renderer_mod = @import("../renderer.zig"); + +/// C bindings generated from src/c_headers/wayland.h by `zig translate-c`. +const c = @import("wayland_c"); + +// Maximum events buffered between polls +const MAX_EVENTS = 256; +// Maximum swapchain images +const MAX_SWAPCHAIN_IMAGES = 8; + +// --------------------------------------------------------------------------- +// WaylandWindow +// --------------------------------------------------------------------------- + +pub const WaylandWindow = struct { + allocator: std.mem.Allocator, + width: u32, + height: u32, + should_close: bool = false, + + // Wayland core objects + display: ?*c.wl_display = null, + registry: ?*c.wl_registry = null, + compositor: ?*c.wl_compositor = null, + wl_surface: ?*c.wl_surface = null, + xdg_wm_base: ?*c.xdg_wm_base = null, + xdg_surface: ?*c.xdg_surface = null, + xdg_toplevel: ?*c.xdg_toplevel = null, + seat: ?*c.wl_seat = null, + keyboard: ?*c.wl_keyboard = null, + pointer: ?*c.wl_pointer = null, + + // Input state + events: [MAX_EVENTS]Event = undefined, + event_head: usize = 0, + event_tail: usize = 0, + mouse_x: f32 = 0, + mouse_y: f32 = 0, + mods: Modifiers = .{}, + + // Vulkan surface and swapchain + vk_surface: c_vk.VkSurfaceKHR = null, + swapchain: c_vk.VkSwapchainKHR = null, + swapchain_images: [MAX_SWAPCHAIN_IMAGES]c_vk.VkImage = [_]c_vk.VkImage{null} ** MAX_SWAPCHAIN_IMAGES, + swapchain_image_count: u32 = 0, + swapchain_format: c_vk.VkFormat = c_vk.VK_FORMAT_B8G8R8A8_UNORM, + + // Per-frame sync objects + image_available: c_vk.VkSemaphore = null, + render_finished: c_vk.VkSemaphore = null, + in_flight: c_vk.VkFence = null, + + // Command buffer for window rendering + cmd: c_vk.VkCommandBuffer = null, + + // Title (owned) + title: [:0]u8 = undefined, + + renderer: *renderer_mod.Renderer = undefined, + + // --------------------------------------------------------------------------- + // VTable + // --------------------------------------------------------------------------- + + const vtable = WindowVTable{ + .deinit = vtDeinit, + .pollEvent = vtPollEvent, + .shouldClose = vtShouldClose, + .presentSurface = vtPresentSurface, + .getWidth = vtGetWidth, + .getHeight = vtGetHeight, + .setTitle = vtSetTitle, + }; + + fn vtDeinit(impl: *anyopaque) void { + const self: *WaylandWindow = @ptrCast(@alignCast(impl)); + if (self.renderer.device != null) _ = vk.DeviceWaitIdle.?(self.renderer.device); + self.destroySwapchain(); + if (self.in_flight != null) vk.DestroyFence.?(self.renderer.device, self.in_flight, null); + if (self.render_finished != null) vk.DestroySemaphore.?(self.renderer.device, self.render_finished, null); + if (self.image_available != null) vk.DestroySemaphore.?(self.renderer.device, self.image_available, null); + if (self.vk_surface != null) vk.DestroySurfaceKHR.?(self.renderer.instance, self.vk_surface, null); + if (self.pointer != null) _ = c.wl_pointer_destroy(self.pointer); + if (self.keyboard != null) _ = c.wl_keyboard_destroy(self.keyboard); + if (self.xdg_toplevel != null) c.xdg_toplevel_destroy(self.xdg_toplevel); + if (self.xdg_surface != null) c.xdg_surface_destroy(self.xdg_surface); + if (self.wl_surface != null) c.wl_surface_destroy(self.wl_surface); + if (self.xdg_wm_base != null) c.xdg_wm_base_destroy(self.xdg_wm_base); + if (self.compositor != null) c.wl_compositor_destroy(self.compositor); + if (self.seat != null) c.wl_seat_destroy(self.seat); + if (self.display != null) _ = c.wl_display_disconnect(self.display); + self.allocator.free(self.title); + self.allocator.destroy(self); + } + + fn vtPollEvent(impl: *anyopaque) ?Event { + const self: *WaylandWindow = @ptrCast(@alignCast(impl)); + // Dispatch pending Wayland events (non-blocking) + _ = c.wl_display_dispatch_pending(self.display); + _ = c.wl_display_flush(self.display); + return self.dequeueEvent(); + } + + fn vtShouldClose(impl: *anyopaque) bool { + const self: *WaylandWindow = @ptrCast(@alignCast(impl)); + return self.should_close; + } + + fn vtPresentSurface(impl: *anyopaque, surf_image: usize, sw: u32, sh: u32) anyerror!void { + const self: *WaylandWindow = @ptrCast(@alignCast(impl)); + return self.presentFrame(@ptrFromInt(surf_image), sw, sh); + } + + fn vtGetWidth(impl: *anyopaque) u32 { + const self: *WaylandWindow = @ptrCast(@alignCast(impl)); + return self.width; + } + + fn vtGetHeight(impl: *anyopaque) u32 { + const self: *WaylandWindow = @ptrCast(@alignCast(impl)); + return self.height; + } + + fn vtSetTitle(impl: *anyopaque, title: []const u8) void { + const self: *WaylandWindow = @ptrCast(@alignCast(impl)); + if (self.xdg_toplevel != null) { + const z_title = self.allocator.dupeZ(u8, title) catch return; + defer self.allocator.free(z_title); + c.xdg_toplevel_set_title(self.xdg_toplevel, z_title.ptr); + _ = c.wl_display_flush(self.display); + } + } + + // --------------------------------------------------------------------------- + // Initialization + // --------------------------------------------------------------------------- + + pub fn initWindow( + allocator: std.mem.Allocator, + width: u32, + height: u32, + title: []const u8, + ) !Window { + const self = try allocator.create(WaylandWindow); + errdefer allocator.destroy(self); + + self.* = WaylandWindow{ + .allocator = allocator, + .width = width, + .height = height, + .title = try allocator.dupeZ(u8, title), + }; + + // Connect to Wayland display and create the wl_surface + try self.connectDisplay(); + + // Acquire/init the Vulkan renderer (creates instance, device, etc.) + self.renderer = try renderer_mod.acquire(allocator); + + // Create the Vulkan surface from the Wayland surface + try self.createVkSurface(); + + try self.createSwapchain(); + try self.createSyncObjects(); + try self.allocateCommandBuffer(); + + // Create the offscreen surface used for drawing + const surf = try Surface.init(allocator, width, height); + + return Window{ + .impl = self, + .vtable = &vtable, + .surface = surf, + }; + } + + // --------------------------------------------------------------------------- + // Wayland connection + // --------------------------------------------------------------------------- + + fn connectDisplay(self: *WaylandWindow) !void { + self.display = c.wl_display_connect(null) orelse return error.WaylandConnectFailed; + + self.registry = c.wl_display_get_registry(self.display) orelse return error.WaylandRegistryFailed; + + const registry_listener = c.wl_registry_listener{ + .global = registryGlobal, + .global_remove = registryGlobalRemove, + }; + _ = c.wl_registry_add_listener(self.registry, ®istry_listener, self); + _ = c.wl_display_roundtrip(self.display); + + if (self.compositor == null) return error.NoWaylandCompositor; + if (self.xdg_wm_base == null) return error.NoXdgWmBase; + + // Create surface + self.wl_surface = c.wl_compositor_create_surface(self.compositor) orelse return error.NoWlSurface; + + // Set up xdg_wm_base ping/pong + const wm_base_listener = c.xdg_wm_base_listener{ + .ping = xdgWmBasePing, + }; + _ = c.xdg_wm_base_add_listener(self.xdg_wm_base, &wm_base_listener, self); + + // Create xdg_surface and xdg_toplevel + self.xdg_surface = c.xdg_wm_base_get_xdg_surface(self.xdg_wm_base, self.wl_surface) orelse + return error.NoXdgSurface; + + const xdg_surface_listener = c.xdg_surface_listener{ + .configure = xdgSurfaceConfigure, + }; + _ = c.xdg_surface_add_listener(self.xdg_surface, &xdg_surface_listener, self); + + self.xdg_toplevel = c.xdg_surface_get_toplevel(self.xdg_surface) orelse + return error.NoXdgToplevel; + + const xdg_toplevel_listener = c.xdg_toplevel_listener{ + .configure = xdgToplevelConfigure, + .close = xdgToplevelClose, + }; + _ = c.xdg_toplevel_add_listener(self.xdg_toplevel, &xdg_toplevel_listener, self); + + c.xdg_toplevel_set_title(self.xdg_toplevel, self.title.ptr); + c.wl_surface_commit(self.wl_surface); + _ = c.wl_display_roundtrip(self.display); + } + + // --------------------------------------------------------------------------- + // Vulkan surface creation (VkSurfaceKHR from wl_surface) + // --------------------------------------------------------------------------- + + fn createVkSurface(self: *WaylandWindow) !void { + const create_info = c_vk.VkWaylandSurfaceCreateInfoKHR{ + .sType = c_vk.VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR, + .pNext = null, + .flags = 0, + .display = self.display, + .surface = self.wl_surface, + }; + try vk_mod.check(vk.CreateWaylandSurfaceKHR.?( + self.renderer.instance, + &create_info, + null, + &self.vk_surface, + )); + } + + // --------------------------------------------------------------------------- + // Swapchain + // --------------------------------------------------------------------------- + + fn createSwapchain(self: *WaylandWindow) !void { + const dev = self.renderer.device; + const phys = self.renderer.physical_device; + + // Query surface capabilities + var caps: c_vk.VkSurfaceCapabilitiesKHR = undefined; + try vk_mod.check(vk.GetPhysicalDeviceSurfaceCapabilitiesKHR.?(phys, self.vk_surface, &caps)); + + // Select format + var format_count: u32 = 0; + try vk_mod.check(vk.GetPhysicalDeviceSurfaceFormatsKHR.?(phys, self.vk_surface, &format_count, null)); + const formats = try self.allocator.alloc(c_vk.VkSurfaceFormatKHR, format_count); + defer self.allocator.free(formats); + try vk_mod.check(vk.GetPhysicalDeviceSurfaceFormatsKHR.?(phys, self.vk_surface, &format_count, formats.ptr)); + + // Prefer BGRA8 SRGB + var chosen_format = formats[0]; + for (formats) |fmt| { + if (fmt.format == c_vk.VK_FORMAT_B8G8R8A8_SRGB and + fmt.colorSpace == c_vk.VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + { + chosen_format = fmt; + break; + } + } + self.swapchain_format = chosen_format.format; + + // Extent + var extent = caps.currentExtent; + if (extent.width == 0xFFFFFFFF) { + extent.width = self.width; + extent.height = self.height; + } + self.width = extent.width; + self.height = extent.height; + + var image_count: u32 = caps.minImageCount + 1; + if (caps.maxImageCount > 0 and image_count > caps.maxImageCount) { + image_count = caps.maxImageCount; + } + if (image_count > MAX_SWAPCHAIN_IMAGES) image_count = MAX_SWAPCHAIN_IMAGES; + + const sc_info = c_vk.VkSwapchainCreateInfoKHR{ + .sType = c_vk.VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, + .pNext = null, + .flags = 0, + .surface = self.vk_surface, + .minImageCount = image_count, + .imageFormat = chosen_format.format, + .imageColorSpace = chosen_format.colorSpace, + .imageExtent = extent, + .imageArrayLayers = 1, + .imageUsage = c_vk.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | c_vk.VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .imageSharingMode = c_vk.VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = null, + .preTransform = caps.currentTransform, + .compositeAlpha = c_vk.VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, + .presentMode = c_vk.VK_PRESENT_MODE_FIFO_KHR, + .clipped = c_vk.VK_TRUE, + .oldSwapchain = null, + }; + try vk_mod.check(vk.CreateSwapchainKHR.?(dev, &sc_info, null, &self.swapchain)); + + // Get swapchain images (no views/framebuffers needed for the blit approach) + var actual_count: u32 = 0; + try vk_mod.check(vk.GetSwapchainImagesKHR.?(dev, self.swapchain, &actual_count, null)); + if (actual_count > MAX_SWAPCHAIN_IMAGES) actual_count = MAX_SWAPCHAIN_IMAGES; + self.swapchain_image_count = actual_count; + try vk_mod.check(vk.GetSwapchainImagesKHR.?(dev, self.swapchain, &actual_count, &self.swapchain_images[0])); + } + + fn destroySwapchain(self: *WaylandWindow) void { + if (self.swapchain != null) vk.DestroySwapchainKHR.?(self.renderer.device, self.swapchain, null); + self.swapchain = null; + self.swapchain_image_count = 0; + } + + // --------------------------------------------------------------------------- + // Sync objects + // --------------------------------------------------------------------------- + + fn createSyncObjects(self: *WaylandWindow) !void { + const dev = self.renderer.device; + const sem_info = c_vk.VkSemaphoreCreateInfo{ + .sType = c_vk.VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + .pNext = null, + .flags = 0, + }; + try vk_mod.check(vk.CreateSemaphore.?(dev, &sem_info, null, &self.image_available)); + try vk_mod.check(vk.CreateSemaphore.?(dev, &sem_info, null, &self.render_finished)); + + const fence_info = c_vk.VkFenceCreateInfo{ + .sType = c_vk.VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, + .pNext = null, + .flags = c_vk.VK_FENCE_CREATE_SIGNALED_BIT, + }; + try vk_mod.check(vk.CreateFence.?(dev, &fence_info, null, &self.in_flight)); + } + + fn allocateCommandBuffer(self: *WaylandWindow) !void { + const alloc_info = c_vk.VkCommandBufferAllocateInfo{ + .sType = c_vk.VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .pNext = null, + .commandPool = self.renderer.command_pool, + .level = c_vk.VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = 1, + }; + try vk_mod.check(vk.AllocateCommandBuffers.?( + self.renderer.device, + &alloc_info, + &self.cmd, + )); + } + + // --------------------------------------------------------------------------- + // Present frame + // --------------------------------------------------------------------------- + + fn presentFrame(self: *WaylandWindow, src_image: c_vk.VkImage, src_width: u32, src_height: u32) !void { + const dev = self.renderer.device; + + // Wait for previous frame + try vk_mod.check(vk.WaitForFences.?(dev, 1, &self.in_flight, c_vk.VK_TRUE, std.math.maxInt(u64))); + try vk_mod.check(vk.ResetFences.?(dev, 1, &self.in_flight)); + + // Acquire next image + var image_index: u32 = 0; + const acquire_result = vk.AcquireNextImageKHR.?( + dev, + self.swapchain, + std.math.maxInt(u64), + self.image_available, + null, + &image_index, + ); + if (acquire_result == c_vk.VK_ERROR_OUT_OF_DATE_KHR) { + try self.recreateSwapchain(); + return; + } + try vk_mod.check(acquire_result); + + // Flush the surface draw calls to the offscreen image, then blit to swapchain + try self.blitSurfaceToSwapchain(src_image, src_width, src_height, image_index); + + // Present + const wait_stages = [_]c_vk.VkPipelineStageFlags{c_vk.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; + const submit_info = c_vk.VkSubmitInfo{ + .sType = c_vk.VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = null, + .waitSemaphoreCount = 1, + .pWaitSemaphores = &self.image_available, + .pWaitDstStageMask = &wait_stages[0], + .commandBufferCount = 1, + .pCommandBuffers = &self.cmd, + .signalSemaphoreCount = 1, + .pSignalSemaphores = &self.render_finished, + }; + try vk_mod.check(vk.QueueSubmit.?(self.renderer.graphics_queue, 1, &submit_info, self.in_flight)); + + const present_info = c_vk.VkPresentInfoKHR{ + .sType = c_vk.VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + .pNext = null, + .waitSemaphoreCount = 1, + .pWaitSemaphores = &self.render_finished, + .swapchainCount = 1, + .pSwapchains = &self.swapchain, + .pImageIndices = &image_index, + .pResults = null, + }; + const present_result = vk.QueuePresentKHR.?(self.renderer.graphics_queue, &present_info); + if (present_result == c_vk.VK_ERROR_OUT_OF_DATE_KHR or present_result == c_vk.VK_SUBOPTIMAL_KHR) { + try self.recreateSwapchain(); + } else { + try vk_mod.check(present_result); + } + + // Dispatch Wayland events + _ = c.wl_display_dispatch_pending(self.display); + _ = c.wl_display_flush(self.display); + } + + fn blitSurfaceToSwapchain( + self: *WaylandWindow, + src_image: c_vk.VkImage, + src_width: u32, + src_height: u32, + image_index: u32, + ) !void { + try vk_mod.check(vk.ResetCommandBuffer.?(self.cmd, 0)); + + const begin_info = c_vk.VkCommandBufferBeginInfo{ + .sType = c_vk.VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = null, + .flags = c_vk.VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, + .pInheritanceInfo = null, + }; + try vk_mod.check(vk.BeginCommandBuffer.?(self.cmd, &begin_info)); + + // Transition swapchain image to TRANSFER_DST_OPTIMAL + const barrier_to_dst = c_vk.VkImageMemoryBarrier{ + .sType = c_vk.VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = null, + .srcAccessMask = 0, + .dstAccessMask = c_vk.VK_ACCESS_TRANSFER_WRITE_BIT, + .oldLayout = c_vk.VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = c_vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .srcQueueFamilyIndex = c_vk.VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = c_vk.VK_QUEUE_FAMILY_IGNORED, + .image = self.swapchain_images[image_index], + .subresourceRange = .{ + .aspectMask = c_vk.VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + vk.CmdPipelineBarrier.?( + self.cmd, + c_vk.VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + c_vk.VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, 0, null, 0, null, 1, &barrier_to_dst, + ); + + // Blit offscreen surface image to swapchain image + const src_offsets = [2]c_vk.VkOffset3D{ + .{ .x = 0, .y = 0, .z = 0 }, + .{ .x = @intCast(src_width), .y = @intCast(src_height), .z = 1 }, + }; + const dst_offsets = [2]c_vk.VkOffset3D{ + .{ .x = 0, .y = 0, .z = 0 }, + .{ .x = @intCast(self.width), .y = @intCast(self.height), .z = 1 }, + }; + const blit_region = c_vk.VkImageBlit{ + .srcSubresource = .{ + .aspectMask = c_vk.VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .srcOffsets = src_offsets, + .dstSubresource = .{ + .aspectMask = c_vk.VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .dstOffsets = dst_offsets, + }; + vk.CmdBlitImage.?( + self.cmd, + src_image, + c_vk.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + self.swapchain_images[image_index], + c_vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, + &blit_region, + c_vk.VK_FILTER_LINEAR, + ); + + // Transition swapchain image to PRESENT_SRC_KHR + const barrier_to_present = c_vk.VkImageMemoryBarrier{ + .sType = c_vk.VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = null, + .srcAccessMask = c_vk.VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = 0, + .oldLayout = c_vk.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .newLayout = c_vk.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + .srcQueueFamilyIndex = c_vk.VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = c_vk.VK_QUEUE_FAMILY_IGNORED, + .image = self.swapchain_images[image_index], + .subresourceRange = .{ + .aspectMask = c_vk.VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + vk.CmdPipelineBarrier.?( + self.cmd, + c_vk.VK_PIPELINE_STAGE_TRANSFER_BIT, + c_vk.VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, + 0, 0, null, 0, null, 1, &barrier_to_present, + ); + + try vk_mod.check(vk.EndCommandBuffer.?(self.cmd)); + } + fn recreateSwapchain(self: *WaylandWindow) !void { + _ = vk.DeviceWaitIdle.?(self.renderer.device); + self.destroySwapchain(); + try self.createSwapchain(); + } + + // --------------------------------------------------------------------------- + // Event queue + // --------------------------------------------------------------------------- + + fn enqueueEvent(self: *WaylandWindow, event: Event) void { + const next = (self.event_tail + 1) % MAX_EVENTS; + if (next == self.event_head) return; // queue full, drop event + self.events[self.event_tail] = event; + self.event_tail = next; + } + + fn dequeueEvent(self: *WaylandWindow) ?Event { + if (self.event_head == self.event_tail) return null; + const ev = self.events[self.event_head]; + self.event_head = (self.event_head + 1) % MAX_EVENTS; + return ev; + } + + // --------------------------------------------------------------------------- + // Wayland registry callbacks + // --------------------------------------------------------------------------- + + fn registryGlobal( + data: ?*anyopaque, + registry: ?*c.wl_registry, + name: u32, + interface: [*c]const u8, + version: u32, + ) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + const iface = std.mem.sliceTo(interface, 0); + + if (std.mem.eql(u8, iface, "wl_compositor")) { + self.compositor = @ptrCast(c.wl_registry_bind( + registry, + name, + &c.wl_compositor_interface, + @min(version, 4), + )); + } else if (std.mem.eql(u8, iface, "xdg_wm_base")) { + self.xdg_wm_base = @ptrCast(c.wl_registry_bind( + registry, + name, + &c.xdg_wm_base_interface, + @min(version, 2), + )); + } else if (std.mem.eql(u8, iface, "wl_seat")) { + self.seat = @ptrCast(c.wl_registry_bind( + registry, + name, + &c.wl_seat_interface, + @min(version, 4), + )); + const seat_listener = c.wl_seat_listener{ + .capabilities = seatCapabilities, + .name = seatName, + }; + _ = c.wl_seat_add_listener(self.seat, &seat_listener, self); + } + } + + fn registryGlobalRemove( + _: ?*anyopaque, + _: ?*c.wl_registry, + _: u32, + ) callconv(.C) void {} + + // --------------------------------------------------------------------------- + // xdg_wm_base ping + // --------------------------------------------------------------------------- + + fn xdgWmBasePing( + _: ?*anyopaque, + wm_base: ?*c.xdg_wm_base, + serial: u32, + ) callconv(.C) void { + c.xdg_wm_base_pong(wm_base, serial); + } + + // --------------------------------------------------------------------------- + // xdg_surface configure + // --------------------------------------------------------------------------- + + fn xdgSurfaceConfigure( + _: ?*anyopaque, + xdg_surface: ?*c.xdg_surface, + serial: u32, + ) callconv(.C) void { + c.xdg_surface_ack_configure(xdg_surface, serial); + } + + // --------------------------------------------------------------------------- + // xdg_toplevel configure / close + // --------------------------------------------------------------------------- + + fn xdgToplevelConfigure( + data: ?*anyopaque, + _: ?*c.xdg_toplevel, + new_width: i32, + new_height: i32, + _: ?*c.wl_array, + ) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + if (new_width > 0 and new_height > 0) { + self.width = @intCast(new_width); + self.height = @intCast(new_height); + self.enqueueEvent(.{ .resize = .{ + .width = self.width, + .height = self.height, + } }); + } + } + + fn xdgToplevelClose(data: ?*anyopaque, _: ?*c.xdg_toplevel) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + self.should_close = true; + self.enqueueEvent(.close); + } + + // --------------------------------------------------------------------------- + // wl_seat + // --------------------------------------------------------------------------- + + fn seatCapabilities(data: ?*anyopaque, seat: ?*c.wl_seat, caps: u32) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + + if (caps & c.WL_SEAT_CAPABILITY_KEYBOARD != 0 and self.keyboard == null) { + self.keyboard = c.wl_seat_get_keyboard(seat); + const listener = c.wl_keyboard_listener{ + .keymap = keyboardKeymap, + .enter = keyboardEnter, + .leave = keyboardLeave, + .key = keyboardKey, + .modifiers = keyboardModifiers, + .repeat_info = keyboardRepeatInfo, + }; + _ = c.wl_keyboard_add_listener(self.keyboard, &listener, self); + } + + if (caps & c.WL_SEAT_CAPABILITY_POINTER != 0 and self.pointer == null) { + self.pointer = c.wl_seat_get_pointer(seat); + const listener = c.wl_pointer_listener{ + .enter = pointerEnter, + .leave = pointerLeave, + .motion = pointerMotion, + .button = pointerButton, + .axis = pointerAxis, + .frame = pointerFrame, + .axis_source = pointerAxisSource, + .axis_stop = pointerAxisStop, + .axis_discrete = pointerAxisDiscrete, + }; + _ = c.wl_pointer_add_listener(self.pointer, &listener, self); + } + } + + fn seatName(_: ?*anyopaque, _: ?*c.wl_seat, _: [*c]const u8) callconv(.C) void {} + + // --------------------------------------------------------------------------- + // Keyboard listeners + // --------------------------------------------------------------------------- + + fn keyboardKeymap( + _: ?*anyopaque, + _: ?*c.wl_keyboard, + _: u32, + _: i32, + _: u32, + ) callconv(.C) void {} + + fn keyboardEnter( + _: ?*anyopaque, + _: ?*c.wl_keyboard, + _: u32, + _: ?*c.wl_surface, + _: ?*c.wl_array, + ) callconv(.C) void {} + + fn keyboardLeave( + _: ?*anyopaque, + _: ?*c.wl_keyboard, + _: u32, + _: ?*c.wl_surface, + ) callconv(.C) void {} + + fn keyboardKey( + data: ?*anyopaque, + _: ?*c.wl_keyboard, + _: u32, + _: u32, + scancode: u32, + state: u32, + ) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + const key = input.keyFromLinux(scancode); + const ev_key = input.KeyEvent{ + .key = key, + .scancode = scancode, + .mods = self.mods, + }; + if (state == c.WL_KEYBOARD_KEY_STATE_PRESSED) { + self.enqueueEvent(.{ .key_press = ev_key }); + } else { + self.enqueueEvent(.{ .key_release = ev_key }); + } + } + + fn keyboardModifiers( + data: ?*anyopaque, + _: ?*c.wl_keyboard, + _: u32, + mods_depressed: u32, + _: u32, + _: u32, + _: u32, + ) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + // xkb modifier indices: Shift=1, Ctrl=4, Alt=8, Super=64 + self.mods = .{ + .shift = (mods_depressed & 0x01) != 0, + .ctrl = (mods_depressed & 0x04) != 0, + .alt = (mods_depressed & 0x08) != 0, + .super = (mods_depressed & 0x40) != 0, + }; + } + + fn keyboardRepeatInfo( + _: ?*anyopaque, + _: ?*c.wl_keyboard, + _: i32, + _: i32, + ) callconv(.C) void {} + + // --------------------------------------------------------------------------- + // Pointer listeners + // --------------------------------------------------------------------------- + + fn pointerEnter( + data: ?*anyopaque, + _: ?*c.wl_pointer, + _: u32, + _: ?*c.wl_surface, + x_fixed: i32, + y_fixed: i32, + ) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + self.mouse_x = @as(f32, @floatFromInt(x_fixed)) / 256.0; + self.mouse_y = @as(f32, @floatFromInt(y_fixed)) / 256.0; + self.enqueueEvent(.{ .mouse_enter = .{ .x = self.mouse_x, .y = self.mouse_y } }); + } + + fn pointerLeave( + data: ?*anyopaque, + _: ?*c.wl_pointer, + _: u32, + _: ?*c.wl_surface, + ) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + self.enqueueEvent(.{ .mouse_leave = .{ .x = self.mouse_x, .y = self.mouse_y } }); + } + + fn pointerMotion( + data: ?*anyopaque, + _: ?*c.wl_pointer, + _: u32, + x_fixed: i32, + y_fixed: i32, + ) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + // Wayland uses wl_fixed_t: 1 unit = 1/256 pixel + self.mouse_x = @as(f32, @floatFromInt(x_fixed)) / 256.0; + self.mouse_y = @as(f32, @floatFromInt(y_fixed)) / 256.0; + self.enqueueEvent(.{ .mouse_move = .{ .x = self.mouse_x, .y = self.mouse_y } }); + } + + fn pointerButton( + data: ?*anyopaque, + _: ?*c.wl_pointer, + _: u32, + _: u32, + linux_button: u32, + state: u32, + ) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + // Linux button codes: BTN_LEFT=0x110, BTN_RIGHT=0x111, BTN_MIDDLE=0x112 + const btn: MouseButton = switch (linux_button) { + 0x110 => .left, + 0x111 => .right, + 0x112 => .middle, + 0x113 => .button4, + 0x114 => .button5, + else => return, + }; + const ev = input.MouseButtonEvent{ + .button = btn, + .x = self.mouse_x, + .y = self.mouse_y, + .mods = self.mods, + }; + if (state == c.WL_POINTER_BUTTON_STATE_PRESSED) { + self.enqueueEvent(.{ .mouse_button_press = ev }); + } else { + self.enqueueEvent(.{ .mouse_button_release = ev }); + } + } + + fn pointerAxis( + data: ?*anyopaque, + _: ?*c.wl_pointer, + _: u32, + axis: u32, + value_fixed: i32, + ) callconv(.C) void { + const self: *WaylandWindow = @ptrCast(@alignCast(data.?)); + const val = @as(f32, @floatFromInt(value_fixed)) / 256.0; + const scroll = switch (axis) { + c.WL_POINTER_AXIS_VERTICAL_SCROLL => input.ScrollEvent{ .dx = 0, .dy = val }, + c.WL_POINTER_AXIS_HORIZONTAL_SCROLL => input.ScrollEvent{ .dx = val, .dy = 0 }, + else => return, + }; + self.enqueueEvent(.{ .mouse_scroll = scroll }); + } + + fn pointerFrame(_: ?*anyopaque, _: ?*c.wl_pointer) callconv(.C) void {} + fn pointerAxisSource(_: ?*anyopaque, _: ?*c.wl_pointer, _: u32) callconv(.C) void {} + fn pointerAxisStop(_: ?*anyopaque, _: ?*c.wl_pointer, _: u32, _: u32) callconv(.C) void {} + fn pointerAxisDiscrete(_: ?*anyopaque, _: ?*c.wl_pointer, _: u32, _: i32) callconv(.C) void {} +}; diff --git a/src/renderer.zig b/src/renderer.zig new file mode 100644 index 0000000..909b282 --- /dev/null +++ b/src/renderer.zig @@ -0,0 +1,691 @@ +//! Renderer — Vulkan pipeline management and draw command submission. +//! +//! The Renderer is a global singleton that manages: +//! - Vulkan instance, physical device, logical device +//! - Graphics command pool and queue +//! - Shared render pass and graphics pipeline +//! +//! Surfaces and Windows reference the global Renderer. + +const std = @import("std"); +const vk_mod = @import("vulkan.zig"); +const vk = vk_mod.vk; +const c = vk_mod.c; +const shapes = @import("shapes.zig"); +const color_mod = @import("color.zig"); +const Paint = color_mod.Paint; + +// --------------------------------------------------------------------------- +// Paint UBO layout (must match fill.frag.glsl) +// --------------------------------------------------------------------------- + +pub const MAX_COLOR_STOPS: usize = 16; + +pub const GpuColorStop = extern struct { + position: f32, + _pad0: [3]f32 = .{ 0, 0, 0 }, + color: [4]f32, +}; + +pub const GpuPaintData = extern struct { + gradient_type: i32, // 0 = solid, 1 = linear, 2 = radial + num_stops: i32, + _pad: [2]i32 = .{ 0, 0 }, + gradient_p0: [2]f32, // linear: start, radial: center + gradient_p1: [2]f32, // linear: end (unused for radial) + gradient_radius: f32, // radial only + _pad2: [3]f32 = .{ 0, 0, 0 }, + stops: [MAX_COLOR_STOPS]GpuColorStop, +}; + +// --------------------------------------------------------------------------- +// Push constants layout (must match fill.vert.glsl) +// --------------------------------------------------------------------------- + +pub const PushConstants = extern struct { + /// Orthographic projection matrix (column-major 4×4). + proj: [16]f32, +}; + +// --------------------------------------------------------------------------- +// Global renderer state +// --------------------------------------------------------------------------- + +pub const Renderer = struct { + allocator: std.mem.Allocator, + instance: c.VkInstance = null, + physical_device: c.VkPhysicalDevice = null, + device: c.VkDevice = null, + graphics_queue: c.VkQueue = null, + graphics_family: u32 = 0, + mem_props: c.VkPhysicalDeviceMemoryProperties = undefined, + + command_pool: c.VkCommandPool = null, + + // Offscreen render pass (color-only, no depth) + offscreen_render_pass: c.VkRenderPass = null, + + // Pipeline shared for both offscreen and window + pipeline_layout: c.VkPipelineLayout = null, + pipeline: c.VkPipeline = null, + + // Descriptor set layout for the paint UBO + desc_set_layout: c.VkDescriptorSetLayout = null, + desc_pool: c.VkDescriptorPool = null, + + pub fn init(allocator: std.mem.Allocator) !Renderer { + try vk_mod.load(); + + var r = Renderer{ .allocator = allocator }; + try r.createInstance(); + try r.selectPhysicalDevice(); + try r.createDevice(); + try r.createCommandPool(); + try r.createOffscreenRenderPass(); + try r.createDescriptorSetLayout(); + try r.createDescriptorPool(); + try r.createPipeline(); + return r; + } + + pub fn deinit(self: *Renderer) void { + if (self.device == null) return; + _ = vk.DeviceWaitIdle.?(self.device); + if (self.pipeline != null) vk.DestroyPipeline.?(self.device, self.pipeline, null); + if (self.pipeline_layout != null) vk.DestroyPipelineLayout.?(self.device, self.pipeline_layout, null); + if (self.desc_pool != null) vk.DestroyDescriptorPool.?(self.device, self.desc_pool, null); + if (self.desc_set_layout != null) vk.DestroyDescriptorSetLayout.?(self.device, self.desc_set_layout, null); + if (self.offscreen_render_pass != null) vk.DestroyRenderPass.?(self.device, self.offscreen_render_pass, null); + if (self.command_pool != null) vk.DestroyCommandPool.?(self.device, self.command_pool, null); + if (self.device != null) vk.DestroyDevice.?(self.device, null); + if (self.instance != null) vk.DestroyInstance.?(self.instance, null); + } + + // ----------------------------------------------------------------------- + // Instance + // ----------------------------------------------------------------------- + + fn createInstance(self: *Renderer) !void { + const app_info = c.VkApplicationInfo{ + .sType = c.VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pNext = null, + .pApplicationName = "flair-ui", + .applicationVersion = c.VK_MAKE_VERSION(0, 1, 0), + .pEngineName = "flair-ui", + .engineVersion = c.VK_MAKE_VERSION(0, 1, 0), + .apiVersion = c.VK_API_VERSION_1_0, + }; + + const extensions = [_][*:0]const u8{ + c.VK_KHR_SURFACE_EXTENSION_NAME, + c.VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME, + }; + + const create_info = c.VkInstanceCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pNext = null, + .flags = 0, + .pApplicationInfo = &app_info, + .enabledLayerCount = 0, + .ppEnabledLayerNames = null, + .enabledExtensionCount = extensions.len, + .ppEnabledExtensionNames = @ptrCast(&extensions[0]), + }; + + try vk_mod.check(vk.CreateInstance.?(&create_info, null, &self.instance)); + vk_mod.loadInstance(self.instance); + } + + // ----------------------------------------------------------------------- + // Physical device + // ----------------------------------------------------------------------- + + fn selectPhysicalDevice(self: *Renderer) !void { + var count: u32 = 0; + try vk_mod.check(vk.EnumeratePhysicalDevices.?(self.instance, &count, null)); + if (count == 0) return error.NoVulkanDevice; + + const devices = try self.allocator.alloc(c.VkPhysicalDevice, count); + defer self.allocator.free(devices); + try vk_mod.check(vk.EnumeratePhysicalDevices.?(self.instance, &count, devices.ptr)); + + // Prefer discrete GPU + var best: c.VkPhysicalDevice = null; + for (devices) |dev| { + var props: c.VkPhysicalDeviceProperties = undefined; + vk.GetPhysicalDeviceProperties.?(dev, &props); + if (props.deviceType == c.VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + best = dev; + break; + } + if (best == null) best = dev; + } + self.physical_device = best; + vk.GetPhysicalDeviceMemoryProperties.?(self.physical_device, &self.mem_props); + } + + // ----------------------------------------------------------------------- + // Logical device + // ----------------------------------------------------------------------- + + fn createDevice(self: *Renderer) !void { + // Find graphics queue family + var family_count: u32 = 0; + vk.GetPhysicalDeviceQueueFamilyProperties.?(self.physical_device, &family_count, null); + const families = try self.allocator.alloc(c.VkQueueFamilyProperties, family_count); + defer self.allocator.free(families); + vk.GetPhysicalDeviceQueueFamilyProperties.?(self.physical_device, &family_count, families.ptr); + + var found = false; + for (families, 0..) |fam, i| { + if (fam.queueFlags & c.VK_QUEUE_GRAPHICS_BIT != 0) { + self.graphics_family = @intCast(i); + found = true; + break; + } + } + if (!found) return error.NoGraphicsQueue; + + const queue_priority: f32 = 1.0; + const queue_info = c.VkDeviceQueueCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, + .pNext = null, + .flags = 0, + .queueFamilyIndex = self.graphics_family, + .queueCount = 1, + .pQueuePriorities = &queue_priority, + }; + + const device_extensions = [_][*:0]const u8{ + c.VK_KHR_SWAPCHAIN_EXTENSION_NAME, + }; + + const device_info = c.VkDeviceCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, + .pNext = null, + .flags = 0, + .queueCreateInfoCount = 1, + .pQueueCreateInfos = &queue_info, + .enabledLayerCount = 0, + .ppEnabledLayerNames = null, + .enabledExtensionCount = device_extensions.len, + .ppEnabledExtensionNames = @ptrCast(&device_extensions[0]), + .pEnabledFeatures = null, + }; + + try vk_mod.check(vk.CreateDevice.?(self.physical_device, &device_info, null, &self.device)); + vk_mod.loadDevice(self.instance, self.device); + vk.GetDeviceQueue.?(self.device, self.graphics_family, 0, &self.graphics_queue); + } + + // ----------------------------------------------------------------------- + // Command pool + // ----------------------------------------------------------------------- + + fn createCommandPool(self: *Renderer) !void { + const info = c.VkCommandPoolCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .pNext = null, + .flags = c.VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, + .queueFamilyIndex = self.graphics_family, + }; + try vk_mod.check(vk.CreateCommandPool.?(self.device, &info, null, &self.command_pool)); + } + + // ----------------------------------------------------------------------- + // Offscreen render pass + // ----------------------------------------------------------------------- + + fn createOffscreenRenderPass(self: *Renderer) !void { + const color_attachment = c.VkAttachmentDescription{ + .flags = 0, + .format = c.VK_FORMAT_R8G8B8A8_UNORM, + .samples = c.VK_SAMPLE_COUNT_1_BIT, + .loadOp = c.VK_ATTACHMENT_LOAD_OP_CLEAR, + .storeOp = c.VK_ATTACHMENT_STORE_OP_STORE, + .stencilLoadOp = c.VK_ATTACHMENT_LOAD_OP_DONT_CARE, + .stencilStoreOp = c.VK_ATTACHMENT_STORE_OP_DONT_CARE, + .initialLayout = c.VK_IMAGE_LAYOUT_UNDEFINED, + .finalLayout = c.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + }; + + const color_ref = c.VkAttachmentReference{ + .attachment = 0, + .layout = c.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + }; + + const subpass = c.VkSubpassDescription{ + .flags = 0, + .pipelineBindPoint = c.VK_PIPELINE_BIND_POINT_GRAPHICS, + .inputAttachmentCount = 0, + .pInputAttachments = null, + .colorAttachmentCount = 1, + .pColorAttachments = &color_ref, + .pResolveAttachments = null, + .pDepthStencilAttachment = null, + .preserveAttachmentCount = 0, + .pPreserveAttachments = null, + }; + + const dependency = c.VkSubpassDependency{ + .srcSubpass = c.VK_SUBPASS_EXTERNAL, + .dstSubpass = 0, + .srcStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + .dstStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + .srcAccessMask = 0, + .dstAccessMask = c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .dependencyFlags = 0, + }; + + const rp_info = c.VkRenderPassCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, + .pNext = null, + .flags = 0, + .attachmentCount = 1, + .pAttachments = &color_attachment, + .subpassCount = 1, + .pSubpasses = &subpass, + .dependencyCount = 1, + .pDependencies = &dependency, + }; + + try vk_mod.check(vk.CreateRenderPass.?(self.device, &rp_info, null, &self.offscreen_render_pass)); + } + + // ----------------------------------------------------------------------- + // Descriptor set layout + // ----------------------------------------------------------------------- + + fn createDescriptorSetLayout(self: *Renderer) !void { + const binding = c.VkDescriptorSetLayoutBinding{ + .binding = 0, + .descriptorType = c.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .descriptorCount = 1, + .stageFlags = c.VK_SHADER_STAGE_FRAGMENT_BIT, + .pImmutableSamplers = null, + }; + + const layout_info = c.VkDescriptorSetLayoutCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + .pNext = null, + .flags = 0, + .bindingCount = 1, + .pBindings = &binding, + }; + + try vk_mod.check(vk.CreateDescriptorSetLayout.?(self.device, &layout_info, null, &self.desc_set_layout)); + } + + fn createDescriptorPool(self: *Renderer) !void { + const pool_size = c.VkDescriptorPoolSize{ + .type = c.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .descriptorCount = 256, + }; + + const pool_info = c.VkDescriptorPoolCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + .pNext = null, + .flags = c.VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, + .maxSets = 256, + .poolSizeCount = 1, + .pPoolSizes = &pool_size, + }; + + try vk_mod.check(vk.CreateDescriptorPool.?(self.device, &pool_info, null, &self.desc_pool)); + } + + // ----------------------------------------------------------------------- + // Graphics pipeline + // ----------------------------------------------------------------------- + + fn createPipeline(self: *Renderer) !void { + // Load compiled SPIR-V shaders (embedded at compile time) + const vert_spv = @embedFile("shaders/fill.vert.spv"); + const frag_spv = @embedFile("shaders/fill.frag.spv"); + + const vert_module = try self.createShaderModule(vert_spv); + defer vk.DestroyShaderModule.?(self.device, vert_module, null); + + const frag_module = try self.createShaderModule(frag_spv); + defer vk.DestroyShaderModule.?(self.device, frag_module, null); + + // Shader stages + const shader_stages = [_]c.VkPipelineShaderStageCreateInfo{ + .{ + .sType = c.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = null, + .flags = 0, + .stage = c.VK_SHADER_STAGE_VERTEX_BIT, + .module = vert_module, + .pName = "main", + .pSpecializationInfo = null, + }, + .{ + .sType = c.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = null, + .flags = 0, + .stage = c.VK_SHADER_STAGE_FRAGMENT_BIT, + .module = frag_module, + .pName = "main", + .pSpecializationInfo = null, + }, + }; + + // Vertex input: Vertex struct layout + // binding 0: per-vertex data + const vertex_binding = c.VkVertexInputBindingDescription{ + .binding = 0, + .stride = @sizeOf(shapes.Vertex), + .inputRate = c.VK_VERTEX_INPUT_RATE_VERTEX, + }; + + const vertex_attribs = [_]c.VkVertexInputAttributeDescription{ + // location 0: position (vec2) + .{ + .location = 0, + .binding = 0, + .format = c.VK_FORMAT_R32G32_SFLOAT, + .offset = @offsetOf(shapes.Vertex, "position"), + }, + // location 1: gradient_coord (vec2) + .{ + .location = 1, + .binding = 0, + .format = c.VK_FORMAT_R32G32_SFLOAT, + .offset = @offsetOf(shapes.Vertex, "gradient_coord"), + }, + // location 2: color (vec4) + .{ + .location = 2, + .binding = 0, + .format = c.VK_FORMAT_R32G32B32A32_SFLOAT, + .offset = @offsetOf(shapes.Vertex, "color"), + }, + }; + + const vertex_input = c.VkPipelineVertexInputStateCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + .pNext = null, + .flags = 0, + .vertexBindingDescriptionCount = 1, + .pVertexBindingDescriptions = &vertex_binding, + .vertexAttributeDescriptionCount = vertex_attribs.len, + .pVertexAttributeDescriptions = &vertex_attribs[0], + }; + + const input_assembly = c.VkPipelineInputAssemblyStateCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .pNext = null, + .flags = 0, + .topology = c.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, + .primitiveRestartEnable = c.VK_FALSE, + }; + + const viewport_state = c.VkPipelineViewportStateCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .pNext = null, + .flags = 0, + .viewportCount = 1, + .pViewports = null, // dynamic + .scissorCount = 1, + .pScissors = null, // dynamic + }; + + const rasterizer = c.VkPipelineRasterizationStateCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .pNext = null, + .flags = 0, + .depthClampEnable = c.VK_FALSE, + .rasterizerDiscardEnable = c.VK_FALSE, + .polygonMode = c.VK_POLYGON_MODE_FILL, + .cullMode = c.VK_CULL_MODE_NONE, + .frontFace = c.VK_FRONT_FACE_COUNTER_CLOCKWISE, + .depthBiasEnable = c.VK_FALSE, + .depthBiasConstantFactor = 0, + .depthBiasClamp = 0, + .depthBiasSlopeFactor = 0, + .lineWidth = 1.0, + }; + + const multisampling = c.VkPipelineMultisampleStateCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .pNext = null, + .flags = 0, + .rasterizationSamples = c.VK_SAMPLE_COUNT_1_BIT, + .sampleShadingEnable = c.VK_FALSE, + .minSampleShading = 1.0, + .pSampleMask = null, + .alphaToCoverageEnable = c.VK_FALSE, + .alphaToOneEnable = c.VK_FALSE, + }; + + // Blending: standard alpha blending + const blend_attachment = c.VkPipelineColorBlendAttachmentState{ + .blendEnable = c.VK_TRUE, + .srcColorBlendFactor = c.VK_BLEND_FACTOR_SRC_ALPHA, + .dstColorBlendFactor = c.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, + .colorBlendOp = c.VK_BLEND_OP_ADD, + .srcAlphaBlendFactor = c.VK_BLEND_FACTOR_ONE, + .dstAlphaBlendFactor = c.VK_BLEND_FACTOR_ZERO, + .alphaBlendOp = c.VK_BLEND_OP_ADD, + .colorWriteMask = c.VK_COLOR_COMPONENT_R_BIT | c.VK_COLOR_COMPONENT_G_BIT | + c.VK_COLOR_COMPONENT_B_BIT | c.VK_COLOR_COMPONENT_A_BIT, + }; + + const blend_state = c.VkPipelineColorBlendStateCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + .pNext = null, + .flags = 0, + .logicOpEnable = c.VK_FALSE, + .logicOp = c.VK_LOGIC_OP_COPY, + .attachmentCount = 1, + .pAttachments = &blend_attachment, + .blendConstants = .{ 0, 0, 0, 0 }, + }; + + // Dynamic state: viewport and scissor + const dynamic_states = [_]c.VkDynamicState{ + c.VK_DYNAMIC_STATE_VIEWPORT, + c.VK_DYNAMIC_STATE_SCISSOR, + }; + const dynamic_state = c.VkPipelineDynamicStateCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .pNext = null, + .flags = 0, + .dynamicStateCount = dynamic_states.len, + .pDynamicStates = &dynamic_states[0], + }; + + // Pipeline layout: push constants (projection) + descriptor set (paint UBO) + const push_range = c.VkPushConstantRange{ + .stageFlags = c.VK_SHADER_STAGE_VERTEX_BIT, + .offset = 0, + .size = @sizeOf(PushConstants), + }; + + const layout_info = c.VkPipelineLayoutCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .pNext = null, + .flags = 0, + .setLayoutCount = 1, + .pSetLayouts = &self.desc_set_layout, + .pushConstantRangeCount = 1, + .pPushConstantRanges = &push_range, + }; + try vk_mod.check(vk.CreatePipelineLayout.?(self.device, &layout_info, null, &self.pipeline_layout)); + + // Create pipeline (using offscreen render pass for compatibility) + const pipeline_info = c.VkGraphicsPipelineCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + .pNext = null, + .flags = 0, + .stageCount = shader_stages.len, + .pStages = &shader_stages[0], + .pVertexInputState = &vertex_input, + .pInputAssemblyState = &input_assembly, + .pTessellationState = null, + .pViewportState = &viewport_state, + .pRasterizationState = &rasterizer, + .pMultisampleState = &multisampling, + .pDepthStencilState = null, + .pColorBlendState = &blend_state, + .pDynamicState = &dynamic_state, + .layout = self.pipeline_layout, + .renderPass = self.offscreen_render_pass, + .subpass = 0, + .basePipelineHandle = null, + .basePipelineIndex = -1, + }; + + try vk_mod.check(vk.CreateGraphicsPipelines.?(self.device, null, 1, &pipeline_info, null, &self.pipeline)); + } + + fn createShaderModule(self: *Renderer, spv: []const u8) !c.VkShaderModule { + const info = c.VkShaderModuleCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + .pNext = null, + .flags = 0, + .codeSize = spv.len, + .pCode = @ptrCast(@alignCast(spv.ptr)), + }; + var module: c.VkShaderModule = undefined; + try vk_mod.check(vk.CreateShaderModule.?(self.device, &info, null, &module)); + return module; + } + + // ----------------------------------------------------------------------- + // Paint descriptor set management + // ----------------------------------------------------------------------- + + /// Allocate and populate a descriptor set for the given paint. + pub fn allocatePaintDescriptorSet( + self: *Renderer, + paint: Paint, + paint_buf: *vk_mod.Buffer, + ) !c.VkDescriptorSet { + // Allocate descriptor set + var desc_set: c.VkDescriptorSet = undefined; + const alloc_info = c.VkDescriptorSetAllocateInfo{ + .sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + .pNext = null, + .descriptorPool = self.desc_pool, + .descriptorSetCount = 1, + .pSetLayouts = &self.desc_set_layout, + }; + try vk_mod.check(vk.AllocateDescriptorSets.?(self.device, &alloc_info, &desc_set)); + + // Fill UBO + const gpu_paint = buildGpuPaintData(paint); + try paint_buf.upload(std.mem.asBytes(&gpu_paint)); + + // Update descriptor set + const buf_info = c.VkDescriptorBufferInfo{ + .buffer = paint_buf.buffer, + .offset = 0, + .range = @sizeOf(GpuPaintData), + }; + const write = c.VkWriteDescriptorSet{ + .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = null, + .dstSet = desc_set, + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = c.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .pImageInfo = null, + .pBufferInfo = &buf_info, + .pTexelBufferView = null, + }; + vk.UpdateDescriptorSets.?(self.device, 1, &write, 0, null); + + return desc_set; + } +}; + +// --------------------------------------------------------------------------- +// Global singleton renderer +// --------------------------------------------------------------------------- + +var g_renderer: ?Renderer = null; +var g_renderer_init = false; +var g_allocator: std.mem.Allocator = undefined; + +/// Get or initialize the global Renderer instance. +pub fn acquire(allocator: std.mem.Allocator) !*Renderer { + if (!g_renderer_init) { + g_allocator = allocator; + g_renderer = try Renderer.init(allocator); + g_renderer_init = true; + } + return &g_renderer.?; +} + +/// Release the global Renderer. Call this when the application exits. +pub fn release() void { + if (g_renderer_init) { + g_renderer.?.deinit(); + g_renderer = null; + g_renderer_init = false; + } +} + +// --------------------------------------------------------------------------- +// Helper: build GPU paint data from a Paint value +// --------------------------------------------------------------------------- + +fn buildGpuPaintData(paint: Paint) GpuPaintData { + var data = GpuPaintData{ + .gradient_type = 0, + .num_stops = 0, + .gradient_p0 = .{ 0, 0 }, + .gradient_p1 = .{ 0, 0 }, + .gradient_radius = 0, + .stops = undefined, + }; + // Zero-init stops + for (&data.stops) |*s| { + s.* = .{ .position = 0, .color = .{ 0, 0, 0, 1 } }; + } + + switch (paint) { + .solid => |col| { + data.gradient_type = 0; + data.num_stops = 1; + data.stops[0] = .{ .position = 0, .color = .{ col.r, col.g, col.b, col.a } }; + }, + .gradient => |g| { + switch (g.kind) { + .linear => |lg| { + data.gradient_type = 1; + data.gradient_p0 = .{ lg.start.x, lg.start.y }; + data.gradient_p1 = .{ lg.end.x, lg.end.y }; + const n = @min(lg.stops.len, MAX_COLOR_STOPS); + data.num_stops = @intCast(n); + for (0..n) |i| { + const s = lg.stops[i]; + data.stops[i] = .{ + .position = s.position, + .color = .{ s.color.r, s.color.g, s.color.b, s.color.a }, + }; + } + }, + .radial => |rg| { + data.gradient_type = 2; + data.gradient_p0 = .{ rg.center.x, rg.center.y }; + data.gradient_radius = rg.radius; + const n = @min(rg.stops.len, MAX_COLOR_STOPS); + data.num_stops = @intCast(n); + for (0..n) |i| { + const s = rg.stops[i]; + data.stops[i] = .{ + .position = s.position, + .color = .{ s.color.r, s.color.g, s.color.b, s.color.a }, + }; + } + }, + } + }, + } + + return data; +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..b8df915 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,99 @@ +//! flair-ui: a 2D vector graphics library backed by Vulkan. +//! +//! ## Quick Start +//! +//! ```zig +//! const flair = @import("flair-ui"); +//! const std = @import("std"); +//! +//! pub fn main() !void { +//! var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +//! defer _ = gpa.deinit(); +//! const allocator = gpa.allocator(); +//! +//! // Off-screen surface +//! var surface = try flair.Surface.init(allocator, 800, 600); +//! defer surface.deinit(); +//! +//! surface.clear(flair.Color.white); +//! try surface.drawCircle(.{ .x = 400, .y = 300 }, 100, .{ +//! .style = .fill, +//! .paint = .{ .solid = flair.Color.red }, +//! }); +//! try surface.savePng("output.png"); +//! } +//! ``` + +// --------------------------------------------------------------------------- +// Re-export the public surface of the library +// --------------------------------------------------------------------------- + +/// 2D point / vector +pub const Vec2 = @import("color.zig").Vec2; + +/// RGBA color with f32 components in [0, 1] +pub const Color = @import("color.zig").Color; + +/// A position + color pair used in gradients +pub const ColorStop = @import("color.zig").ColorStop; + +/// A linear or radial gradient +pub const Gradient = @import("color.zig").Gradient; + +/// How a shape is painted: solid color or gradient +pub const Paint = @import("color.zig").Paint; + +/// Options for a draw call: style (fill/stroke) + paint +pub const DrawOptions = @import("color.zig").DrawOptions; + +/// `.fill` or `.stroke` (with configurable width) +pub const DrawStyle = @import("color.zig").DrawStyle; + +/// Stroke parameters +pub const StrokeOptions = @import("color.zig").StrokeOptions; + +/// A 2D rectangle (position + size) +pub const Rect = @import("color.zig").Rect; + +/// Per-corner radii for a rounded rectangle +pub const CornerRadii = @import("color.zig").CornerRadii; + +/// Input events (keyboard, mouse, resize, close) +pub const Event = @import("input.zig").Event; +pub const Key = @import("input.zig").Key; +pub const Modifiers = @import("input.zig").Modifiers; +pub const MouseButton = @import("input.zig").MouseButton; +pub const KeyEvent = @import("input.zig").KeyEvent; +pub const MouseButtonEvent = @import("input.zig").MouseButtonEvent; +pub const MouseMoveEvent = @import("input.zig").MouseMoveEvent; +pub const ScrollEvent = @import("input.zig").ScrollEvent; + +/// The primary drawing primitive: an offscreen Vulkan render target +pub const Surface = @import("surface.zig").Surface; + +/// Platform-agnostic windowing interface +pub const Window = @import("window.zig").Window; + +// --------------------------------------------------------------------------- +// Convenience constructors +// --------------------------------------------------------------------------- + +/// Initialize a surface. Equivalent to `Surface.init(allocator, width, height)`. +pub fn createSurface(allocator: @import("std").mem.Allocator, width: u32, height: u32) !Surface { + return Surface.init(allocator, width, height); +} + +/// Initialize a window. Equivalent to `Window.init(allocator, width, height, title)`. +pub fn createWindow( + allocator: @import("std").mem.Allocator, + width: u32, + height: u32, + title: []const u8, +) !Window { + return Window.init(allocator, width, height, title); +} + +/// Release the global Vulkan renderer. Call this when the application exits. +pub fn deinit() void { + @import("renderer.zig").release(); +} diff --git a/src/shaders/fill.frag.glsl b/src/shaders/fill.frag.glsl new file mode 100644 index 0000000..d1644b4 --- /dev/null +++ b/src/shaders/fill.frag.glsl @@ -0,0 +1,77 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable + +// --------------------------------------------------------------------------- +// Input from vertex shader +// --------------------------------------------------------------------------- +layout(location = 0) in vec2 frag_gradient_coord; +layout(location = 1) in vec4 frag_color; + +// --------------------------------------------------------------------------- +// Paint uniform buffer +// --------------------------------------------------------------------------- +struct ColorStop { + float position; + float _pad0; + float _pad1; + float _pad2; + vec4 color; +}; + +#define MAX_STOPS 16 + +layout(set = 0, binding = 0) uniform PaintData { + int gradient_type; // 0 = solid, 1 = linear, 2 = radial + int num_stops; + int _pad0; + int _pad1; + vec2 gradient_p0; // linear: start point; radial: center (in pixel coords) + vec2 gradient_p1; // linear: end point + float gradient_radius; // radial only + float _pad2; + float _pad3; + float _pad4; + ColorStop stops[MAX_STOPS]; +} paint; + +// --------------------------------------------------------------------------- +// Output +// --------------------------------------------------------------------------- +layout(location = 0) out vec4 out_color; + +// --------------------------------------------------------------------------- +// Gradient sampling +// --------------------------------------------------------------------------- +vec4 sampleStops(float t) { + t = clamp(t, 0.0, 1.0); + if (paint.num_stops <= 0) return frag_color; + if (paint.num_stops == 1) return paint.stops[0].color; + + // Find bracketing stops + int i = 0; + for (i = 0; i < paint.num_stops - 1; i++) { + if (t <= paint.stops[i + 1].position) break; + } + if (i >= paint.num_stops - 1) return paint.stops[paint.num_stops - 1].color; + + float a_pos = paint.stops[i].position; + float b_pos = paint.stops[i + 1].position; + float range = b_pos - a_pos; + if (range < 1e-6) return paint.stops[i].color; + + float local_t = (t - a_pos) / range; + return mix(paint.stops[i].color, paint.stops[i + 1].color, local_t); +} + +void main() { + if (paint.gradient_type == 0) { + // Solid color — use the per-vertex color + out_color = frag_color; + } else if (paint.gradient_type == 1) { + // Linear gradient — gradient_coord.x is already the normalized t + out_color = sampleStops(frag_gradient_coord.x); + } else { + // Radial gradient — gradient_coord.x is the normalized distance + out_color = sampleStops(frag_gradient_coord.x); + } +} diff --git a/src/shaders/fill.frag.spv b/src/shaders/fill.frag.spv new file mode 100644 index 0000000..6b6b281 Binary files /dev/null and b/src/shaders/fill.frag.spv differ diff --git a/src/shaders/fill.vert.glsl b/src/shaders/fill.vert.glsl new file mode 100644 index 0000000..10a6f65 --- /dev/null +++ b/src/shaders/fill.vert.glsl @@ -0,0 +1,28 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable + +// --------------------------------------------------------------------------- +// Input: per-vertex attributes +// --------------------------------------------------------------------------- +layout(location = 0) in vec2 in_position; +layout(location = 1) in vec2 in_gradient_coord; +layout(location = 2) in vec4 in_color; + +// --------------------------------------------------------------------------- +// Push constants: orthographic projection matrix +// --------------------------------------------------------------------------- +layout(push_constant) uniform PushConstants { + mat4 proj; +} pc; + +// --------------------------------------------------------------------------- +// Output to fragment shader +// --------------------------------------------------------------------------- +layout(location = 0) out vec2 frag_gradient_coord; +layout(location = 1) out vec4 frag_color; + +void main() { + gl_Position = pc.proj * vec4(in_position, 0.0, 1.0); + frag_gradient_coord = in_gradient_coord; + frag_color = in_color; +} diff --git a/src/shaders/fill.vert.spv b/src/shaders/fill.vert.spv new file mode 100644 index 0000000..c670a2a Binary files /dev/null and b/src/shaders/fill.vert.spv differ diff --git a/src/shapes.zig b/src/shapes.zig new file mode 100644 index 0000000..6aeb18b --- /dev/null +++ b/src/shapes.zig @@ -0,0 +1,496 @@ +//! Shape tessellation: converts drawing primitives into triangle vertex lists. +//! +//! All shapes are tessellated into triangles on the CPU. The resulting vertices +//! are uploaded to a Vulkan vertex buffer for rendering. + +const std = @import("std"); +const color = @import("color.zig"); +const Vec2 = color.Vec2; +const Color = color.Color; +const Paint = color.Paint; +const DrawOptions = color.DrawOptions; +const DrawStyle = color.DrawStyle; +const Rect = color.Rect; +const CornerRadii = color.CornerRadii; + +// --------------------------------------------------------------------------- +// Vertex format (matches the GLSL vertex shader input layout) +// --------------------------------------------------------------------------- + +pub const Vertex = extern struct { + /// 2D position in surface pixel coordinates. + position: [2]f32, + /// Gradient coordinate (0.0–1.0). Used by the fragment shader to sample gradients. + gradient_coord: [2]f32, + /// RGBA color. For solid paints, this is the fill color. + /// For gradients, the fragment shader samples from the gradient using gradient_coord. + color: [4]f32, +}; + +// --------------------------------------------------------------------------- +// Gradient coordinate helpers +// --------------------------------------------------------------------------- + +/// Compute gradient_coord.x for a position along a linear gradient. +fn linearGradCoord(pos: Vec2, g_start: Vec2, g_end: Vec2) f32 { + const dir = Vec2.sub(g_end, g_start); + const len_sq = dir.x * dir.x + dir.y * dir.y; + if (len_sq < 1e-10) return 0.0; + const dp = Vec2.sub(pos, g_start); + return std.math.clamp(Vec2.dot(dp, dir) / len_sq, 0.0, 1.0); +} + +/// Compute gradient_coord.x for a position in a radial gradient. +fn radialGradCoord(pos: Vec2, center: Vec2, radius: f32) f32 { + if (radius < 1e-10) return 0.0; + const d = Vec2.sub(pos, center); + return std.math.clamp(@sqrt(d.x * d.x + d.y * d.y) / radius, 0.0, 1.0); +} + +fn gradCoord(pos: Vec2, opts: DrawOptions) [2]f32 { + return switch (opts.paint) { + .solid => .{ 0.0, 0.0 }, + .gradient => |g| switch (g.kind) { + .linear => |lg| .{ linearGradCoord(pos, lg.start, lg.end), 0.0 }, + .radial => |rg| .{ radialGradCoord(pos, rg.center, rg.radius), 0.0 }, + }, + }; +} + +fn solidColor(opts: DrawOptions) [4]f32 { + return switch (opts.paint) { + .solid => |c| .{ c.r, c.g, c.b, c.a }, + // For gradients we encode the color at t=0 as a fallback; the fragment + // shader will actually sample from the gradient using gradient_coord. + .gradient => |g| blk: { + const c = g.sample(0.0); + break :blk .{ c.r, c.g, c.b, c.a }; + }, + }; +} + +fn makeVertex(pos: Vec2, opts: DrawOptions) Vertex { + return .{ + .position = .{ pos.x, pos.y }, + .gradient_coord = gradCoord(pos, opts), + .color = solidColor(opts), + }; +} + +// --------------------------------------------------------------------------- +// Tessellator — accumulates vertices and indices +// --------------------------------------------------------------------------- + +pub const Tessellator = struct { + vertices: std.ArrayList(Vertex), + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator) Tessellator { + return .{ + .vertices = std.ArrayList(Vertex).init(allocator), + .allocator = allocator, + }; + } + + pub fn deinit(self: *Tessellator) void { + self.vertices.deinit(); + } + + pub fn reset(self: *Tessellator) void { + self.vertices.clearRetainingCapacity(); + } + + fn addTri(self: *Tessellator, a: Vertex, b: Vertex, c: Vertex) !void { + try self.vertices.appendSlice(&.{ a, b, c }); + } + + // ----------------------------------------------------------------------- + // Line + // ----------------------------------------------------------------------- + + /// Draw a line from `a` to `b` as a filled quad (two triangles). + pub fn addLine(self: *Tessellator, a: Vec2, b: Vec2, opts: DrawOptions) !void { + const width = switch (opts.style) { + .fill => 1.0, + .stroke => |s| s.line_width, + }; + try self.addLineSegment(a, b, width, opts); + } + + fn addLineSegment(self: *Tessellator, a: Vec2, b: Vec2, width: f32, opts: DrawOptions) !void { + const dir = Vec2.normalize(Vec2.sub(b, a)); + const perp = Vec2.scale(Vec2.perp(dir), width * 0.5); + + const v0 = Vec2.sub(a, perp); + const v1 = Vec2.add(a, perp); + const v2 = Vec2.sub(b, perp); + const v3 = Vec2.add(b, perp); + + try self.addTri(makeVertex(v0, opts), makeVertex(v1, opts), makeVertex(v2, opts)); + try self.addTri(makeVertex(v1, opts), makeVertex(v3, opts), makeVertex(v2, opts)); + } + + // ----------------------------------------------------------------------- + // Path (polyline) + // ----------------------------------------------------------------------- + + /// Draw a path (sequence of connected line segments). + pub fn addPath(self: *Tessellator, points: []const Vec2, closed: bool, opts: DrawOptions) !void { + if (points.len < 2) return; + + const width = switch (opts.style) { + .fill => 1.0, + .stroke => |s| s.line_width, + }; + + var i: usize = 0; + while (i + 1 < points.len) : (i += 1) { + try self.addLineSegment(points[i], points[i + 1], width, opts); + } + if (closed and points.len >= 2) { + try self.addLineSegment(points[points.len - 1], points[0], width, opts); + } + } + + // ----------------------------------------------------------------------- + // Bézier curves + // ----------------------------------------------------------------------- + + /// Flatten a quadratic Bézier curve into line segments using De Casteljau, + /// then tessellate as a path. + pub fn addQuadraticBezier( + self: *Tessellator, + p0: Vec2, + p1: Vec2, + p2: Vec2, + opts: DrawOptions, + ) !void { + var pts = std.ArrayList(Vec2).init(self.allocator); + defer pts.deinit(); + try pts.append(p0); + try flattenQuadratic(&pts, p0, p1, p2, 0); + try pts.append(p2); + try self.addPath(pts.items, false, opts); + } + + /// Flatten a cubic Bézier curve into line segments using De Casteljau, + /// then tessellate as a path. + pub fn addCubicBezier( + self: *Tessellator, + p0: Vec2, + p1: Vec2, + p2: Vec2, + p3: Vec2, + opts: DrawOptions, + ) !void { + var pts = std.ArrayList(Vec2).init(self.allocator); + defer pts.deinit(); + try pts.append(p0); + try flattenCubic(&pts, p0, p1, p2, p3, 0); + try pts.append(p3); + try self.addPath(pts.items, false, opts); + } + + // ----------------------------------------------------------------------- + // Circle + // ----------------------------------------------------------------------- + + pub fn addCircle(self: *Tessellator, center: Vec2, radius: f32, opts: DrawOptions) !void { + try self.addEllipse(center, radius, radius, opts); + } + + // ----------------------------------------------------------------------- + // Circular arc + // ----------------------------------------------------------------------- + + pub fn addCircularArc( + self: *Tessellator, + center: Vec2, + radius: f32, + start_angle: f32, + end_angle: f32, + opts: DrawOptions, + ) !void { + try self.addEllipticalArc(center, radius, radius, start_angle, end_angle, opts); + } + + // ----------------------------------------------------------------------- + // Oval (Ellipse) + // ----------------------------------------------------------------------- + + pub fn addEllipse( + self: *Tessellator, + center: Vec2, + rx: f32, + ry: f32, + opts: DrawOptions, + ) !void { + try self.addEllipticalArc(center, rx, ry, 0.0, std.math.tau, opts); + } + + // ----------------------------------------------------------------------- + // Oval arc (Elliptical arc) + // ----------------------------------------------------------------------- + + pub fn addEllipticalArc( + self: *Tessellator, + center: Vec2, + rx: f32, + ry: f32, + start_angle: f32, + end_angle: f32, + opts: DrawOptions, + ) !void { + const segments = computeArcSegments(rx, ry, start_angle, end_angle); + + switch (opts.style) { + .fill => try self.ellipticalArcFill(center, rx, ry, start_angle, end_angle, segments, opts), + .stroke => |s| try self.ellipticalArcStroke(center, rx, ry, start_angle, end_angle, segments, s.line_width, opts), + } + } + + fn ellipticalArcFill( + self: *Tessellator, + center: Vec2, + rx: f32, + ry: f32, + start_angle: f32, + end_angle: f32, + segments: usize, + opts: DrawOptions, + ) !void { + const vc = makeVertex(center, opts); + const step = (end_angle - start_angle) / @as(f32, @floatFromInt(segments)); + + var i: usize = 0; + while (i < segments) : (i += 1) { + const a0 = start_angle + @as(f32, @floatFromInt(i)) * step; + const a1 = a0 + step; + const p0 = Vec2{ + .x = center.x + rx * @cos(a0), + .y = center.y + ry * @sin(a0), + }; + const p1 = Vec2{ + .x = center.x + rx * @cos(a1), + .y = center.y + ry * @sin(a1), + }; + try self.addTri(vc, makeVertex(p0, opts), makeVertex(p1, opts)); + } + } + + fn ellipticalArcStroke( + self: *Tessellator, + center: Vec2, + rx: f32, + ry: f32, + start_angle: f32, + end_angle: f32, + segments: usize, + line_width: f32, + opts: DrawOptions, + ) !void { + const step = (end_angle - start_angle) / @as(f32, @floatFromInt(segments)); + var i: usize = 0; + while (i < segments) : (i += 1) { + const a0 = start_angle + @as(f32, @floatFromInt(i)) * step; + const a1 = a0 + step; + const p0 = Vec2{ + .x = center.x + rx * @cos(a0), + .y = center.y + ry * @sin(a0), + }; + const p1 = Vec2{ + .x = center.x + rx * @cos(a1), + .y = center.y + ry * @sin(a1), + }; + try self.addLineSegment(p0, p1, line_width, opts); + } + } + + // ----------------------------------------------------------------------- + // Rectangle + // ----------------------------------------------------------------------- + + pub fn addRect( + self: *Tessellator, + rect: Rect, + radii: CornerRadii, + opts: DrawOptions, + ) !void { + const has_radius = + radii.top_left > 0 or radii.top_right > 0 or + radii.bottom_left > 0 or radii.bottom_right > 0; + + if (has_radius) { + try self.addRoundedRect(rect, radii, opts); + } else { + try self.addPlainRect(rect, opts); + } + } + + fn addPlainRect(self: *Tessellator, rect: Rect, opts: DrawOptions) !void { + const tl = Vec2{ .x = rect.x, .y = rect.y }; + const tr = Vec2{ .x = rect.x + rect.width, .y = rect.y }; + const bl = Vec2{ .x = rect.x, .y = rect.y + rect.height }; + const br = Vec2{ .x = rect.x + rect.width, .y = rect.y + rect.height }; + + switch (opts.style) { + .fill => { + try self.addTri(makeVertex(tl, opts), makeVertex(tr, opts), makeVertex(bl, opts)); + try self.addTri(makeVertex(tr, opts), makeVertex(br, opts), makeVertex(bl, opts)); + }, + .stroke => |s| { + const corners = [_]Vec2{ tl, tr, br, bl }; + try self.addPath(&corners, true, DrawOptions{ + .style = .{ .stroke = s }, + .paint = opts.paint, + }); + }, + } + } + + fn addRoundedRect(self: *Tessellator, rect: Rect, radii: CornerRadii, opts: DrawOptions) !void { + // Clamp corner radii to half the smaller dimension + const max_r = @min(rect.width, rect.height) * 0.5; + const tl = @min(radii.top_left, max_r); + const tr = @min(radii.top_right, max_r); + const bl = @min(radii.bottom_left, max_r); + const br_r = @min(radii.bottom_right, max_r); + + const x0 = rect.x; + const y0 = rect.y; + const x1 = rect.x + rect.width; + const y1 = rect.y + rect.height; + + // Corner arc centers + const c_tl = Vec2{ .x = x0 + tl, .y = y0 + tl }; + const c_tr = Vec2{ .x = x1 - tr, .y = y0 + tr }; + const c_bl = Vec2{ .x = x0 + bl, .y = y1 - bl }; + const c_br = Vec2{ .x = x1 - br_r, .y = y1 - br_r }; + + // Build outline as a polyline + const corner_segs: usize = 8; // segments per corner + var pts = std.ArrayList(Vec2).init(self.allocator); + defer pts.deinit(); + + const pi = std.math.pi; + + // Top-left corner: π to 3π/2 (i.e., 180° to 270°) + if (tl > 0) try appendArcPts(&pts, c_tl, tl, pi, pi * 1.5, corner_segs) else try pts.append(.{ .x = x0, .y = y0 }); + // Top-right corner: -π/2 to 0 (i.e., 270° to 360°) + if (tr > 0) try appendArcPts(&pts, c_tr, tr, -pi * 0.5, 0.0, corner_segs) else try pts.append(.{ .x = x1, .y = y0 }); + // Bottom-right corner: 0 to π/2 + if (br_r > 0) try appendArcPts(&pts, c_br, br_r, 0.0, pi * 0.5, corner_segs) else try pts.append(.{ .x = x1, .y = y1 }); + // Bottom-left corner: π/2 to π + if (bl > 0) try appendArcPts(&pts, c_bl, bl, pi * 0.5, pi, corner_segs) else try pts.append(.{ .x = x0, .y = y1 }); + + switch (opts.style) { + .fill => { + // Fan triangulation from center + const cx = rect.x + rect.width * 0.5; + const cy = rect.y + rect.height * 0.5; + const center_v = makeVertex(.{ .x = cx, .y = cy }, opts); + var i: usize = 0; + while (i < pts.items.len) : (i += 1) { + const p0 = pts.items[i]; + const p1 = pts.items[(i + 1) % pts.items.len]; + try self.addTri(center_v, makeVertex(p0, opts), makeVertex(p1, opts)); + } + }, + .stroke => |s| { + try self.addPath(pts.items, true, DrawOptions{ + .style = .{ .stroke = s }, + .paint = opts.paint, + }); + }, + } + } +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn computeArcSegments(rx: f32, ry: f32, start_angle: f32, end_angle: f32) usize { + const r = @max(rx, ry); + const arc_len = r * @abs(end_angle - start_angle); + const segs = @max(8, @as(usize, @intFromFloat(arc_len / 2.0))); + return @min(segs, 512); +} + +fn appendArcPts( + pts: *std.ArrayList(Vec2), + center: Vec2, + radius: f32, + start_angle: f32, + end_angle: f32, + segments: usize, +) !void { + const step = (end_angle - start_angle) / @as(f32, @floatFromInt(segments)); + var i: usize = 0; + while (i <= segments) : (i += 1) { + const angle = start_angle + @as(f32, @floatFromInt(i)) * step; + try pts.append(.{ + .x = center.x + radius * @cos(angle), + .y = center.y + radius * @sin(angle), + }); + } +} + +const max_bezier_depth: u32 = 8; + +fn flattenQuadratic(pts: *std.ArrayList(Vec2), p0: Vec2, p1: Vec2, p2: Vec2, depth: u32) !void { + // Check flatness + const mx = (p0.x + 2.0 * p1.x + p2.x) * 0.25; + const my = (p0.y + 2.0 * p1.y + p2.y) * 0.25; + const mid = Vec2{ .x = (p0.x + p2.x) * 0.5, .y = (p0.y + p2.y) * 0.5 }; + const dx = mx - mid.x; + const dy = my - mid.y; + const flatness_sq = dx * dx + dy * dy; + + if (depth >= max_bezier_depth or flatness_sq < 0.25) { + try pts.append(Vec2{ .x = (p0.x + p2.x) * 0.5, .y = (p0.y + p2.y) * 0.5 }); + return; + } + + // De Casteljau split + const m01 = Vec2{ .x = (p0.x + p1.x) * 0.5, .y = (p0.y + p1.y) * 0.5 }; + const m12 = Vec2{ .x = (p1.x + p2.x) * 0.5, .y = (p1.y + p2.y) * 0.5 }; + const m012 = Vec2{ .x = (m01.x + m12.x) * 0.5, .y = (m01.y + m12.y) * 0.5 }; + + try flattenQuadratic(pts, p0, m01, m012, depth + 1); + try pts.append(m012); + try flattenQuadratic(pts, m012, m12, p2, depth + 1); +} + +fn flattenCubic( + pts: *std.ArrayList(Vec2), + p0: Vec2, + p1: Vec2, + p2: Vec2, + p3: Vec2, + depth: u32, +) !void { + // Check flatness using control-point deviation from the chord + const dx1 = p1.x - p0.x; + const dy1 = p1.y - p0.y; + const dx2 = p2.x - p3.x; + const dy2 = p2.y - p3.y; + const flatness_sq = (dx1 * dx1 + dy1 * dy1) + (dx2 * dx2 + dy2 * dy2); + + if (depth >= max_bezier_depth or flatness_sq < 0.5) { + try pts.append(Vec2{ .x = (p0.x + p3.x) * 0.5, .y = (p0.y + p3.y) * 0.5 }); + return; + } + + // De Casteljau split + const m01 = Vec2{ .x = (p0.x + p1.x) * 0.5, .y = (p0.y + p1.y) * 0.5 }; + const m12 = Vec2{ .x = (p1.x + p2.x) * 0.5, .y = (p1.y + p2.y) * 0.5 }; + const m23 = Vec2{ .x = (p2.x + p3.x) * 0.5, .y = (p2.y + p3.y) * 0.5 }; + const m012 = Vec2{ .x = (m01.x + m12.x) * 0.5, .y = (m01.y + m12.y) * 0.5 }; + const m123 = Vec2{ .x = (m12.x + m23.x) * 0.5, .y = (m12.y + m23.y) * 0.5 }; + const m0123 = Vec2{ .x = (m012.x + m123.x) * 0.5, .y = (m012.y + m123.y) * 0.5 }; + + try flattenCubic(pts, p0, m01, m012, m0123, depth + 1); + try pts.append(m0123); + try flattenCubic(pts, m0123, m123, m23, p3, depth + 1); +} diff --git a/src/surface.zig b/src/surface.zig new file mode 100644 index 0000000..67504ca --- /dev/null +++ b/src/surface.zig @@ -0,0 +1,689 @@ +//! Surface — the central drawing primitive. +//! +//! A Surface is an offscreen Vulkan render target backed by a VkImage. +//! All drawing operations target a Surface, which can then be: +//! - Saved to disk as a PNG image (`savePng`). +//! - Presented to a window via a Window (see window.zig). + +const std = @import("std"); +const vk_mod = @import("vulkan.zig"); +const vk = vk_mod.vk; +const c = vk_mod.c; +const renderer_mod = @import("renderer.zig"); +const Renderer = renderer_mod.Renderer; +const shapes = @import("shapes.zig"); +const color_mod = @import("color.zig"); +const Color = color_mod.Color; +const Vec2 = color_mod.Vec2; +const Rect = color_mod.Rect; +const CornerRadii = color_mod.CornerRadii; +const DrawOptions = color_mod.DrawOptions; +const DrawStyle = color_mod.DrawStyle; +const Paint = color_mod.Paint; +const Gradient = color_mod.Gradient; +const image = @import("image.zig"); + +// --------------------------------------------------------------------------- +// Surface +// --------------------------------------------------------------------------- + +pub const Surface = struct { + allocator: std.mem.Allocator, + renderer: *Renderer, + width: u32, + height: u32, + + // Vulkan resources + image: c.VkImage = null, + image_mem: c.VkDeviceMemory = null, + image_view: c.VkImageView = null, + framebuffer: c.VkFramebuffer = null, + + // Readback staging buffer (host-visible, used for PNG export) + staging_buf: vk_mod.Buffer = .{}, + + // Command buffer for this surface + cmd: c.VkCommandBuffer = null, + + // Accumulated vertex data for the current frame + tess: shapes.Tessellator, + + // Per-draw paint buffer and descriptor set lists + paint_bufs: std.ArrayList(vk_mod.Buffer), + desc_sets: std.ArrayList(c.VkDescriptorSet), + + // Clear color for this frame + clear_color: Color = Color.white, + + /// Create an offscreen surface of the given size. + pub fn init(allocator: std.mem.Allocator, width: u32, height: u32) !Surface { + const r = try renderer_mod.acquire(allocator); + var s = Surface{ + .allocator = allocator, + .renderer = r, + .width = width, + .height = height, + .tess = shapes.Tessellator.init(allocator), + .paint_bufs = std.ArrayList(vk_mod.Buffer).init(allocator), + .desc_sets = std.ArrayList(c.VkDescriptorSet).init(allocator), + }; + try s.createVulkanResources(); + try s.allocateCommandBuffer(); + return s; + } + + pub fn deinit(self: *Surface) void { + _ = vk.DeviceWaitIdle.?(self.renderer.device); + self.freePaintResources(); + self.paint_bufs.deinit(); + self.desc_sets.deinit(); + self.tess.deinit(); + self.staging_buf.deinit(); + if (self.framebuffer != null) vk.DestroyFramebuffer.?(self.renderer.device, self.framebuffer, null); + if (self.image_view != null) vk.DestroyImageView.?(self.renderer.device, self.image_view, null); + if (self.image != null) vk.DestroyImage.?(self.renderer.device, self.image, null); + if (self.image_mem != null) vk.FreeMemory.?(self.renderer.device, self.image_mem, null); + } + + // ----------------------------------------------------------------------- + // Clear + // ----------------------------------------------------------------------- + + /// Set the background clear color. Called before drawing to set up the + /// color that will be used when `present()` or `flush()` is called. + pub fn clear(self: *Surface, col: Color) void { + self.clear_color = col; + } + + // ----------------------------------------------------------------------- + // Drawing primitives + // ----------------------------------------------------------------------- + + /// Draw a line from `a` to `b`. + pub fn drawLine(self: *Surface, a: Vec2, b: Vec2, opts: DrawOptions) !void { + try self.tess.addLine(a, b, opts); + } + + /// Draw a path (polyline) through the given points. + pub fn drawPath(self: *Surface, points: []const Vec2, closed: bool, opts: DrawOptions) !void { + try self.tess.addPath(points, closed, opts); + } + + /// Draw a quadratic Bézier curve. + pub fn drawQuadraticBezier(self: *Surface, p0: Vec2, p1: Vec2, p2: Vec2, opts: DrawOptions) !void { + try self.tess.addQuadraticBezier(p0, p1, p2, opts); + } + + /// Draw a cubic Bézier curve. + pub fn drawCubicBezier(self: *Surface, p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, opts: DrawOptions) !void { + try self.tess.addCubicBezier(p0, p1, p2, p3, opts); + } + + /// Draw a circle. + pub fn drawCircle(self: *Surface, center: Vec2, radius: f32, opts: DrawOptions) !void { + try self.tess.addCircle(center, radius, opts); + } + + /// Draw a circular arc. + pub fn drawCircularArc( + self: *Surface, + center: Vec2, + radius: f32, + start_angle: f32, + end_angle: f32, + opts: DrawOptions, + ) !void { + try self.tess.addCircularArc(center, radius, start_angle, end_angle, opts); + } + + /// Draw an oval (ellipse). + pub fn drawOval(self: *Surface, center: Vec2, rx: f32, ry: f32, opts: DrawOptions) !void { + try self.tess.addEllipse(center, rx, ry, opts); + } + + /// Draw an elliptical arc. + pub fn drawOvalArc( + self: *Surface, + center: Vec2, + rx: f32, + ry: f32, + start_angle: f32, + end_angle: f32, + opts: DrawOptions, + ) !void { + try self.tess.addEllipticalArc(center, rx, ry, start_angle, end_angle, opts); + } + + /// Draw a rectangle with optional per-corner radii. + pub fn drawRect(self: *Surface, rect: Rect, radii: CornerRadii, opts: DrawOptions) !void { + try self.tess.addRect(rect, radii, opts); + } + + // ----------------------------------------------------------------------- + // Flush: record and submit the draw commands + // ----------------------------------------------------------------------- + + /// Render all accumulated draw calls to the surface image. + /// This must be called before `readPixels` or `savePng`. + pub fn flush(self: *Surface) !void { + const dev = self.renderer.device; + const vertices = self.tess.vertices.items; + + // Upload vertices to a staging + device buffer + if (vertices.len == 0) { + // Still do a clear pass + try self.recordClearOnly(); + return; + } + + const vtx_size = vertices.len * @sizeOf(shapes.Vertex); + var vtx_buf = try vk_mod.Buffer.init( + dev, + self.renderer.mem_props, + vtx_size, + c.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, + c.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | c.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + ); + defer vtx_buf.deinit(); + try vtx_buf.upload(std.mem.sliceAsBytes(vertices)); + + // Allocate a paint UBO buffer (one per flush — used for the current draw) + // For simplicity, we use a single solid-white paint for the whole flush; + // per-draw paint is encoded per-vertex via the color field. + var paint_buf = try vk_mod.Buffer.init( + dev, + self.renderer.mem_props, + @sizeOf(renderer_mod.GpuPaintData), + c.VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, + c.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | c.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + ); + // We pass .solid white — the actual color comes from vertex attributes + const desc_set = try self.renderer.allocatePaintDescriptorSet( + .{ .solid = Color.white }, + &paint_buf, + ); + try self.paint_bufs.append(paint_buf); + try self.desc_sets.append(desc_set); + + // Record command buffer + try self.recordDrawCommands(vtx_buf.buffer, @intCast(vertices.len), desc_set); + + // Submit + const submit_info = c.VkSubmitInfo{ + .sType = c.VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = null, + .waitSemaphoreCount = 0, + .pWaitSemaphores = null, + .pWaitDstStageMask = null, + .commandBufferCount = 1, + .pCommandBuffers = &self.cmd, + .signalSemaphoreCount = 0, + .pSignalSemaphores = null, + }; + try vk_mod.check(vk.QueueSubmit.?(self.renderer.graphics_queue, 1, &submit_info, null)); + try vk_mod.check(vk.QueueWaitIdle.?(self.renderer.graphics_queue)); + + // Reset tessellator for next frame + self.tess.reset(); + self.freePaintResources(); + } + + // ----------------------------------------------------------------------- + // PNG export + // ----------------------------------------------------------------------- + + /// Save the surface to a PNG file at `path`. Calls `flush()` first. + pub fn savePng(self: *Surface, path: []const u8) !void { + try self.flush(); + try self.transitionImageForReadback(); + + const pixel_count = @as(usize, self.width) * self.height * 4; + if (self.staging_buf.size < pixel_count) { + self.staging_buf.deinit(); + self.staging_buf = try vk_mod.Buffer.init( + self.renderer.device, + self.renderer.mem_props, + pixel_count, + c.VK_BUFFER_USAGE_TRANSFER_DST_BIT, + c.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | c.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + ); + } + + // Copy image to staging buffer + try self.copyImageToBuffer(); + + // Read back pixels + var mapped: ?*anyopaque = null; + try vk_mod.check(vk.MapMemory.?( + self.renderer.device, + self.staging_buf.memory, + 0, + pixel_count, + 0, + &mapped, + )); + const pixels: []const u8 = @as([*]const u8, @ptrCast(mapped.?))[0..pixel_count]; + + // Encode PNG + const file = try std.fs.cwd().createFile(path, .{}); + defer file.close(); + try image.writePng(file.writer(), self.width, self.height, pixels); + + vk.UnmapMemory.?(self.renderer.device, self.staging_buf.memory); + + // Transition back for rendering + try self.transitionImageForRendering(); + } + + /// Return the raw RGBA pixels of the surface. Calls `flush()` first. + /// Caller must free the returned slice. + pub fn readPixels(self: *Surface) ![]u8 { + try self.flush(); + try self.transitionImageForReadback(); + + const pixel_count = @as(usize, self.width) * self.height * 4; + if (self.staging_buf.size < pixel_count) { + self.staging_buf.deinit(); + self.staging_buf = try vk_mod.Buffer.init( + self.renderer.device, + self.renderer.mem_props, + pixel_count, + c.VK_BUFFER_USAGE_TRANSFER_DST_BIT, + c.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | c.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + ); + } + + try self.copyImageToBuffer(); + + var mapped: ?*anyopaque = null; + try vk_mod.check(vk.MapMemory.?( + self.renderer.device, + self.staging_buf.memory, + 0, + pixel_count, + 0, + &mapped, + )); + const pixels = try self.allocator.dupe(u8, @as([*]const u8, @ptrCast(mapped.?))[0..pixel_count]); + vk.UnmapMemory.?(self.renderer.device, self.staging_buf.memory); + + try self.transitionImageForRendering(); + return pixels; + } + + // ----------------------------------------------------------------------- + // Vulkan resource creation + // ----------------------------------------------------------------------- + + fn createVulkanResources(self: *Surface) !void { + const dev = self.renderer.device; + + // Create color image + const image_info = c.VkImageCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = null, + .flags = 0, + .imageType = c.VK_IMAGE_TYPE_2D, + .format = c.VK_FORMAT_R8G8B8A8_UNORM, + .extent = .{ .width = self.width, .height = self.height, .depth = 1 }, + .mipLevels = 1, + .arrayLayers = 1, + .samples = c.VK_SAMPLE_COUNT_1_BIT, + .tiling = c.VK_IMAGE_TILING_OPTIMAL, + .usage = c.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | c.VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + .sharingMode = c.VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = null, + .initialLayout = c.VK_IMAGE_LAYOUT_UNDEFINED, + }; + try vk_mod.check(vk.CreateImage.?(dev, &image_info, null, &self.image)); + + // Allocate device memory + var mem_reqs: c.VkMemoryRequirements = undefined; + vk.GetImageMemoryRequirements.?(dev, self.image, &mem_reqs); + + const alloc_info = c.VkMemoryAllocateInfo{ + .sType = c.VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .pNext = null, + .allocationSize = mem_reqs.size, + .memoryTypeIndex = try vk_mod.findMemoryType( + self.renderer.mem_props, + mem_reqs.memoryTypeBits, + c.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, + ), + }; + try vk_mod.check(vk.AllocateMemory.?(dev, &alloc_info, null, &self.image_mem)); + try vk_mod.check(vk.BindImageMemory.?(dev, self.image, self.image_mem, 0)); + + // Image view + const view_info = c.VkImageViewCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = null, + .flags = 0, + .image = self.image, + .viewType = c.VK_IMAGE_VIEW_TYPE_2D, + .format = c.VK_FORMAT_R8G8B8A8_UNORM, + .components = .{ + .r = c.VK_COMPONENT_SWIZZLE_IDENTITY, + .g = c.VK_COMPONENT_SWIZZLE_IDENTITY, + .b = c.VK_COMPONENT_SWIZZLE_IDENTITY, + .a = c.VK_COMPONENT_SWIZZLE_IDENTITY, + }, + .subresourceRange = .{ + .aspectMask = c.VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + try vk_mod.check(vk.CreateImageView.?(dev, &view_info, null, &self.image_view)); + + // Framebuffer + const fb_info = c.VkFramebufferCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, + .pNext = null, + .flags = 0, + .renderPass = self.renderer.offscreen_render_pass, + .attachmentCount = 1, + .pAttachments = &self.image_view, + .width = self.width, + .height = self.height, + .layers = 1, + }; + try vk_mod.check(vk.CreateFramebuffer.?(dev, &fb_info, null, &self.framebuffer)); + } + + fn allocateCommandBuffer(self: *Surface) !void { + const alloc_info = c.VkCommandBufferAllocateInfo{ + .sType = c.VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .pNext = null, + .commandPool = self.renderer.command_pool, + .level = c.VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = 1, + }; + try vk_mod.check(vk.AllocateCommandBuffers.?( + self.renderer.device, + &alloc_info, + &self.cmd, + )); + } + + // ----------------------------------------------------------------------- + // Command recording + // ----------------------------------------------------------------------- + + fn recordClearOnly(self: *Surface) !void { + try vk_mod.check(vk.ResetCommandBuffer.?(self.cmd, 0)); + + const begin_info = c.VkCommandBufferBeginInfo{ + .sType = c.VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = null, + .flags = c.VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, + .pInheritanceInfo = null, + }; + try vk_mod.check(vk.BeginCommandBuffer.?(self.cmd, &begin_info)); + + const clear_val = c.VkClearValue{ + .color = .{ + .float32 = .{ + self.clear_color.r, + self.clear_color.g, + self.clear_color.b, + self.clear_color.a, + }, + }, + }; + const rp_begin = c.VkRenderPassBeginInfo{ + .sType = c.VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, + .pNext = null, + .renderPass = self.renderer.offscreen_render_pass, + .framebuffer = self.framebuffer, + .renderArea = .{ .offset = .{ .x = 0, .y = 0 }, .extent = .{ .width = self.width, .height = self.height } }, + .clearValueCount = 1, + .pClearValues = &clear_val, + }; + vk.CmdBeginRenderPass.?(self.cmd, &rp_begin, c.VK_SUBPASS_CONTENTS_INLINE); + vk.CmdEndRenderPass.?(self.cmd); + + try vk_mod.check(vk.EndCommandBuffer.?(self.cmd)); + + const submit_info = c.VkSubmitInfo{ + .sType = c.VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = null, + .waitSemaphoreCount = 0, + .pWaitSemaphores = null, + .pWaitDstStageMask = null, + .commandBufferCount = 1, + .pCommandBuffers = &self.cmd, + .signalSemaphoreCount = 0, + .pSignalSemaphores = null, + }; + try vk_mod.check(vk.QueueSubmit.?(self.renderer.graphics_queue, 1, &submit_info, null)); + try vk_mod.check(vk.QueueWaitIdle.?(self.renderer.graphics_queue)); + } + + fn recordDrawCommands( + self: *Surface, + vtx_buf: c.VkBuffer, + vertex_count: u32, + desc_set: c.VkDescriptorSet, + ) !void { + try vk_mod.check(vk.ResetCommandBuffer.?(self.cmd, 0)); + + const begin_info = c.VkCommandBufferBeginInfo{ + .sType = c.VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = null, + .flags = c.VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, + .pInheritanceInfo = null, + }; + try vk_mod.check(vk.BeginCommandBuffer.?(self.cmd, &begin_info)); + + // Orthographic projection: map (0,0)–(W,H) to NDC (-1,1)–(1,-1) + const proj = orthoProjection( + @floatFromInt(self.width), + @floatFromInt(self.height), + ); + + const clear_val = c.VkClearValue{ + .color = .{ + .float32 = .{ + self.clear_color.r, + self.clear_color.g, + self.clear_color.b, + self.clear_color.a, + }, + }, + }; + const rp_begin = c.VkRenderPassBeginInfo{ + .sType = c.VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, + .pNext = null, + .renderPass = self.renderer.offscreen_render_pass, + .framebuffer = self.framebuffer, + .renderArea = .{ + .offset = .{ .x = 0, .y = 0 }, + .extent = .{ .width = self.width, .height = self.height }, + }, + .clearValueCount = 1, + .pClearValues = &clear_val, + }; + vk.CmdBeginRenderPass.?(self.cmd, &rp_begin, c.VK_SUBPASS_CONTENTS_INLINE); + + // Viewport and scissor + const viewport = c.VkViewport{ + .x = 0, + .y = 0, + .width = @floatFromInt(self.width), + .height = @floatFromInt(self.height), + .minDepth = 0.0, + .maxDepth = 1.0, + }; + vk.CmdSetViewport.?(self.cmd, 0, 1, &viewport); + + const scissor = c.VkRect2D{ + .offset = .{ .x = 0, .y = 0 }, + .extent = .{ .width = self.width, .height = self.height }, + }; + vk.CmdSetScissor.?(self.cmd, 0, 1, &scissor); + + // Bind pipeline + vk.CmdBindPipeline.?(self.cmd, c.VK_PIPELINE_BIND_POINT_GRAPHICS, self.renderer.pipeline); + + // Push constants + const pc = renderer_mod.PushConstants{ .proj = proj }; + vk.CmdPushConstants.?( + self.cmd, + self.renderer.pipeline_layout, + c.VK_SHADER_STAGE_VERTEX_BIT, + 0, + @sizeOf(renderer_mod.PushConstants), + &pc, + ); + + // Bind descriptor set (paint UBO) + vk.CmdBindDescriptorSets.?( + self.cmd, + c.VK_PIPELINE_BIND_POINT_GRAPHICS, + self.renderer.pipeline_layout, + 0, + 1, + &desc_set, + 0, + null, + ); + + // Bind vertex buffer and draw + const offset: c.VkDeviceSize = 0; + vk.CmdBindVertexBuffers.?(self.cmd, 0, 1, &vtx_buf, &offset); + vk.CmdDraw.?(self.cmd, vertex_count, 1, 0, 0); + + vk.CmdEndRenderPass.?(self.cmd); + try vk_mod.check(vk.EndCommandBuffer.?(self.cmd)); + } + + // ----------------------------------------------------------------------- + // Readback helpers + // ----------------------------------------------------------------------- + + fn transitionImageForReadback(self: *Surface) !void { + const cmd = try vk_mod.beginOneShot( + self.renderer.device, + self.renderer.command_pool, + ); + + const barrier = c.VkImageMemoryBarrier{ + .sType = c.VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = null, + .srcAccessMask = c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .dstAccessMask = c.VK_ACCESS_TRANSFER_READ_BIT, + .oldLayout = c.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .newLayout = c.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + .srcQueueFamilyIndex = c.VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = c.VK_QUEUE_FAMILY_IGNORED, + .image = self.image, + .subresourceRange = .{ + .aspectMask = c.VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + vk.CmdPipelineBarrier.?( + cmd, + c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + c.VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, + 0, + null, + 0, + null, + 1, + &barrier, + ); + + try vk_mod.submitOneShot( + self.renderer.device, + self.renderer.command_pool, + self.renderer.graphics_queue, + cmd, + ); + } + + fn transitionImageForRendering(self: *Surface) !void { + // No-op: the render pass handles layout transitions for the next frame. + _ = self; + } + + fn copyImageToBuffer(self: *Surface) !void { + const cmd = try vk_mod.beginOneShot( + self.renderer.device, + self.renderer.command_pool, + ); + + const region = c.VkBufferImageCopy{ + .bufferOffset = 0, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource = .{ + .aspectMask = c.VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .imageOffset = .{ .x = 0, .y = 0, .z = 0 }, + .imageExtent = .{ .width = self.width, .height = self.height, .depth = 1 }, + }; + vk.CmdCopyImageToBuffer.?( + cmd, + self.image, + c.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + self.staging_buf.buffer, + 1, + ®ion, + ); + + try vk_mod.submitOneShot( + self.renderer.device, + self.renderer.command_pool, + self.renderer.graphics_queue, + cmd, + ); + } + + fn freePaintResources(self: *Surface) void { + // The descriptor pool was created with VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, + // so we can free individual descriptor sets back to the pool. + // For simplicity, we just let them accumulate until we destroy the pool. + // The renderer owns the pool and will clean it up on deinit. + self.desc_sets.clearRetainingCapacity(); + + // Free paint UBO buffers + for (self.paint_bufs.items) |*pb| { + pb.deinit(); + } + self.paint_bufs.clearRetainingCapacity(); + } +}; + +// --------------------------------------------------------------------------- +// Orthographic projection +// --------------------------------------------------------------------------- + +/// Build a column-major 4×4 orthographic projection matrix mapping +/// (0,0)–(width,height) to NDC (-1,1)–(1,-1) with Y pointing down. +fn orthoProjection(width: f32, height: f32) [16]f32 { + const r: f32 = width; + const t: f32 = height; + // Column-major: + // [ 2/r, 0, 0, 0 ] + // [ 0, 2/t, 0, 0 ] (note: flipped Y for screen space) + // [ 0, 0, -1, 0 ] + // [-1, -1, 0, 1 ] + return .{ + 2.0 / r, 0.0, 0.0, 0.0, + 0.0, 2.0 / t, 0.0, 0.0, + 0.0, 0.0, -1.0, 0.0, + -1.0, -1.0, 0.0, 1.0, + }; +} diff --git a/src/vulkan.zig b/src/vulkan.zig new file mode 100644 index 0000000..0770803 --- /dev/null +++ b/src/vulkan.zig @@ -0,0 +1,407 @@ +//! Vulkan helpers: instance, physical device, logical device, memory, utilities. +//! +//! This module wraps the C Vulkan API and provides ergonomic Zig wrappers for +//! the most common operations. The C bindings are generated at build time via +//! `b.addTranslateC` (the Zig 0.16 replacement for the deprecated @cImport). + +const std = @import("std"); +const builtin = @import("builtin"); + +/// C bindings generated from src/c_headers/vulkan.h by `zig translate-c`. +pub const c = @import("vulkan_c"); + +// Re-export commonly used Vulkan types for convenience +pub const VkResult = c.VkResult; +pub const VkInstance = c.VkInstance; +pub const VkPhysicalDevice = c.VkPhysicalDevice; +pub const VkDevice = c.VkDevice; +pub const VkQueue = c.VkQueue; +pub const VkCommandPool = c.VkCommandPool; +pub const VkCommandBuffer = c.VkCommandBuffer; +pub const VkRenderPass = c.VkRenderPass; +pub const VkFramebuffer = c.VkFramebuffer; +pub const VkPipeline = c.VkPipeline; +pub const VkPipelineLayout = c.VkPipelineLayout; +pub const VkDescriptorSetLayout = c.VkDescriptorSetLayout; +pub const VkDescriptorPool = c.VkDescriptorPool; +pub const VkDescriptorSet = c.VkDescriptorSet; +pub const VkBuffer = c.VkBuffer; +pub const VkDeviceMemory = c.VkDeviceMemory; +pub const VkImage = c.VkImage; +pub const VkImageView = c.VkImageView; +pub const VkSampler = c.VkSampler; +pub const VkSemaphore = c.VkSemaphore; +pub const VkFence = c.VkFence; +pub const VkSurfaceKHR = c.VkSurfaceKHR; +pub const VkSwapchainKHR = c.VkSwapchainKHR; +pub const VkShaderModule = c.VkShaderModule; + +// --------------------------------------------------------------------------- +// Function pointer loader (we use vkGetInstanceProcAddr / vkGetDeviceProcAddr) +// --------------------------------------------------------------------------- + +/// Loaded global Vulkan function pointers. +pub var vk: VkFunctions = undefined; +var vk_loaded = false; + +pub const VkFunctions = struct { + // Global + GetInstanceProcAddr: c.PFN_vkGetInstanceProcAddr, + CreateInstance: c.PFN_vkCreateInstance, + EnumerateInstanceExtensionProperties: c.PFN_vkEnumerateInstanceExtensionProperties, + EnumerateInstanceLayerProperties: c.PFN_vkEnumerateInstanceLayerProperties, + + // Instance + DestroyInstance: c.PFN_vkDestroyInstance, + EnumeratePhysicalDevices: c.PFN_vkEnumeratePhysicalDevices, + GetPhysicalDeviceProperties: c.PFN_vkGetPhysicalDeviceProperties, + GetPhysicalDeviceFeatures: c.PFN_vkGetPhysicalDeviceFeatures, + GetPhysicalDeviceQueueFamilyProperties: c.PFN_vkGetPhysicalDeviceQueueFamilyProperties, + GetPhysicalDeviceMemoryProperties: c.PFN_vkGetPhysicalDeviceMemoryProperties, + GetPhysicalDeviceFormatProperties: c.PFN_vkGetPhysicalDeviceFormatProperties, + CreateDevice: c.PFN_vkCreateDevice, + DestroySurfaceKHR: c.PFN_vkDestroySurfaceKHR, + GetPhysicalDeviceSurfaceSupportKHR: c.PFN_vkGetPhysicalDeviceSurfaceSupportKHR, + GetPhysicalDeviceSurfaceCapabilitiesKHR: c.PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR, + GetPhysicalDeviceSurfaceFormatsKHR: c.PFN_vkGetPhysicalDeviceSurfaceFormatsKHR, + GetPhysicalDeviceSurfacePresentModesKHR: c.PFN_vkGetPhysicalDeviceSurfacePresentModesKHR, + CreateWaylandSurfaceKHR: c.PFN_vkCreateWaylandSurfaceKHR, + + // Device + DestroyDevice: c.PFN_vkDestroyDevice, + GetDeviceQueue: c.PFN_vkGetDeviceQueue, + DeviceWaitIdle: c.PFN_vkDeviceWaitIdle, + CreateCommandPool: c.PFN_vkCreateCommandPool, + DestroyCommandPool: c.PFN_vkDestroyCommandPool, + AllocateCommandBuffers: c.PFN_vkAllocateCommandBuffers, + FreeCommandBuffers: c.PFN_vkFreeCommandBuffers, + BeginCommandBuffer: c.PFN_vkBeginCommandBuffer, + EndCommandBuffer: c.PFN_vkEndCommandBuffer, + ResetCommandBuffer: c.PFN_vkResetCommandBuffer, + QueueSubmit: c.PFN_vkQueueSubmit, + QueueWaitIdle: c.PFN_vkQueueWaitIdle, + QueuePresentKHR: c.PFN_vkQueuePresentKHR, + CreateRenderPass: c.PFN_vkCreateRenderPass, + DestroyRenderPass: c.PFN_vkDestroyRenderPass, + CreateFramebuffer: c.PFN_vkCreateFramebuffer, + DestroyFramebuffer: c.PFN_vkDestroyFramebuffer, + CreateImageView: c.PFN_vkCreateImageView, + DestroyImageView: c.PFN_vkDestroyImageView, + CreateImage: c.PFN_vkCreateImage, + DestroyImage: c.PFN_vkDestroyImage, + GetImageMemoryRequirements: c.PFN_vkGetImageMemoryRequirements, + BindImageMemory: c.PFN_vkBindImageMemory, + AllocateMemory: c.PFN_vkAllocateMemory, + FreeMemory: c.PFN_vkFreeMemory, + MapMemory: c.PFN_vkMapMemory, + UnmapMemory: c.PFN_vkUnmapMemory, + CreateBuffer: c.PFN_vkCreateBuffer, + DestroyBuffer: c.PFN_vkDestroyBuffer, + GetBufferMemoryRequirements: c.PFN_vkGetBufferMemoryRequirements, + BindBufferMemory: c.PFN_vkBindBufferMemory, + CreateShaderModule: c.PFN_vkCreateShaderModule, + DestroyShaderModule: c.PFN_vkDestroyShaderModule, + CreateGraphicsPipelines: c.PFN_vkCreateGraphicsPipelines, + DestroyPipeline: c.PFN_vkDestroyPipeline, + CreatePipelineLayout: c.PFN_vkCreatePipelineLayout, + DestroyPipelineLayout: c.PFN_vkDestroyPipelineLayout, + CreateDescriptorSetLayout: c.PFN_vkCreateDescriptorSetLayout, + DestroyDescriptorSetLayout: c.PFN_vkDestroyDescriptorSetLayout, + CreateDescriptorPool: c.PFN_vkCreateDescriptorPool, + DestroyDescriptorPool: c.PFN_vkDestroyDescriptorPool, + AllocateDescriptorSets: c.PFN_vkAllocateDescriptorSets, + UpdateDescriptorSets: c.PFN_vkUpdateDescriptorSets, + CreateSemaphore: c.PFN_vkCreateSemaphore, + DestroySemaphore: c.PFN_vkDestroySemaphore, + CreateFence: c.PFN_vkCreateFence, + DestroyFence: c.PFN_vkDestroyFence, + WaitForFences: c.PFN_vkWaitForFences, + ResetFences: c.PFN_vkResetFences, + CmdBeginRenderPass: c.PFN_vkCmdBeginRenderPass, + CmdEndRenderPass: c.PFN_vkCmdEndRenderPass, + CmdBindPipeline: c.PFN_vkCmdBindPipeline, + CmdBindVertexBuffers: c.PFN_vkCmdBindVertexBuffers, + CmdDraw: c.PFN_vkCmdDraw, + CmdSetViewport: c.PFN_vkCmdSetViewport, + CmdSetScissor: c.PFN_vkCmdSetScissor, + CmdPushConstants: c.PFN_vkCmdPushConstants, + CmdBindDescriptorSets: c.PFN_vkCmdBindDescriptorSets, + CmdCopyImageToBuffer: c.PFN_vkCmdCopyImageToBuffer, + CmdPipelineBarrier: c.PFN_vkCmdPipelineBarrier, + CmdBlitImage: c.PFN_vkCmdBlitImage, + CreateSwapchainKHR: c.PFN_vkCreateSwapchainKHR, + DestroySwapchainKHR: c.PFN_vkDestroySwapchainKHR, + GetSwapchainImagesKHR: c.PFN_vkGetSwapchainImagesKHR, + AcquireNextImageKHR: c.PFN_vkAcquireNextImageKHR, + FlushMappedMemoryRanges: c.PFN_vkFlushMappedMemoryRanges, + InvalidateMappedMemoryRanges: c.PFN_vkInvalidateMappedMemoryRanges, +}; + +/// The loaded Vulkan shared library handle. Kept alive to prevent unloading. +var g_vulkan_lib: ?std.DynLib = null; + +/// Load all Vulkan function pointers. Must be called before any Vulkan API use. +pub fn load() !void { + if (vk_loaded) return; + + // Open libvulkan dynamically; keep the handle alive in g_vulkan_lib + g_vulkan_lib = std.DynLib.open("libvulkan.so.1") catch + try std.DynLib.open("libvulkan.so"); + + const get_proc_addr = g_vulkan_lib.?.lookup( + c.PFN_vkGetInstanceProcAddr, + "vkGetInstanceProcAddr", + ) orelse return error.VkGetInstanceProcAddrNotFound; + + vk.GetInstanceProcAddr = get_proc_addr; + + // Load pre-instance functions via null instance + vk.CreateInstance = @ptrCast(get_proc_addr(null, "vkCreateInstance")); + vk.EnumerateInstanceExtensionProperties = @ptrCast(get_proc_addr(null, "vkEnumerateInstanceExtensionProperties")); + vk.EnumerateInstanceLayerProperties = @ptrCast(get_proc_addr(null, "vkEnumerateInstanceLayerProperties")); + + vk_loaded = true; +} + +/// Load instance-level function pointers. +pub fn loadInstance(instance: VkInstance) void { + const g = vk.GetInstanceProcAddr; + vk.DestroyInstance = @ptrCast(g(instance, "vkDestroyInstance")); + vk.EnumeratePhysicalDevices = @ptrCast(g(instance, "vkEnumeratePhysicalDevices")); + vk.GetPhysicalDeviceProperties = @ptrCast(g(instance, "vkGetPhysicalDeviceProperties")); + vk.GetPhysicalDeviceFeatures = @ptrCast(g(instance, "vkGetPhysicalDeviceFeatures")); + vk.GetPhysicalDeviceQueueFamilyProperties = @ptrCast(g(instance, "vkGetPhysicalDeviceQueueFamilyProperties")); + vk.GetPhysicalDeviceMemoryProperties = @ptrCast(g(instance, "vkGetPhysicalDeviceMemoryProperties")); + vk.GetPhysicalDeviceFormatProperties = @ptrCast(g(instance, "vkGetPhysicalDeviceFormatProperties")); + vk.CreateDevice = @ptrCast(g(instance, "vkCreateDevice")); + vk.DestroySurfaceKHR = @ptrCast(g(instance, "vkDestroySurfaceKHR")); + vk.GetPhysicalDeviceSurfaceSupportKHR = @ptrCast(g(instance, "vkGetPhysicalDeviceSurfaceSupportKHR")); + vk.GetPhysicalDeviceSurfaceCapabilitiesKHR = @ptrCast(g(instance, "vkGetPhysicalDeviceSurfaceCapabilitiesKHR")); + vk.GetPhysicalDeviceSurfaceFormatsKHR = @ptrCast(g(instance, "vkGetPhysicalDeviceSurfaceFormatsKHR")); + vk.GetPhysicalDeviceSurfacePresentModesKHR = @ptrCast(g(instance, "vkGetPhysicalDeviceSurfacePresentModesKHR")); + vk.CreateWaylandSurfaceKHR = @ptrCast(g(instance, "vkCreateWaylandSurfaceKHR")); +} + +/// Load device-level function pointers. +pub fn loadDevice(instance: VkInstance, device: VkDevice) void { + const g = vk.GetInstanceProcAddr; + // Use vkGetDeviceProcAddr for device-level functions + const gd: c.PFN_vkGetDeviceProcAddr = @ptrCast(g(instance, "vkGetDeviceProcAddr")); + + // Load device-level functions + vk.DestroyDevice = @ptrCast(gd.?(device, "vkDestroyDevice")); + vk.GetDeviceQueue = @ptrCast(gd.?(device, "vkGetDeviceQueue")); + vk.DeviceWaitIdle = @ptrCast(gd.?(device, "vkDeviceWaitIdle")); + vk.CreateCommandPool = @ptrCast(gd.?(device, "vkCreateCommandPool")); + vk.DestroyCommandPool = @ptrCast(gd.?(device, "vkDestroyCommandPool")); + vk.AllocateCommandBuffers = @ptrCast(gd.?(device, "vkAllocateCommandBuffers")); + vk.FreeCommandBuffers = @ptrCast(gd.?(device, "vkFreeCommandBuffers")); + vk.BeginCommandBuffer = @ptrCast(gd.?(device, "vkBeginCommandBuffer")); + vk.EndCommandBuffer = @ptrCast(gd.?(device, "vkEndCommandBuffer")); + vk.ResetCommandBuffer = @ptrCast(gd.?(device, "vkResetCommandBuffer")); + vk.QueueSubmit = @ptrCast(gd.?(device, "vkQueueSubmit")); + vk.QueueWaitIdle = @ptrCast(gd.?(device, "vkQueueWaitIdle")); + vk.QueuePresentKHR = @ptrCast(gd.?(device, "vkQueuePresentKHR")); + vk.CreateRenderPass = @ptrCast(gd.?(device, "vkCreateRenderPass")); + vk.DestroyRenderPass = @ptrCast(gd.?(device, "vkDestroyRenderPass")); + vk.CreateFramebuffer = @ptrCast(gd.?(device, "vkCreateFramebuffer")); + vk.DestroyFramebuffer = @ptrCast(gd.?(device, "vkDestroyFramebuffer")); + vk.CreateImageView = @ptrCast(gd.?(device, "vkCreateImageView")); + vk.DestroyImageView = @ptrCast(gd.?(device, "vkDestroyImageView")); + vk.CreateImage = @ptrCast(gd.?(device, "vkCreateImage")); + vk.DestroyImage = @ptrCast(gd.?(device, "vkDestroyImage")); + vk.GetImageMemoryRequirements = @ptrCast(gd.?(device, "vkGetImageMemoryRequirements")); + vk.BindImageMemory = @ptrCast(gd.?(device, "vkBindImageMemory")); + vk.AllocateMemory = @ptrCast(gd.?(device, "vkAllocateMemory")); + vk.FreeMemory = @ptrCast(gd.?(device, "vkFreeMemory")); + vk.MapMemory = @ptrCast(gd.?(device, "vkMapMemory")); + vk.UnmapMemory = @ptrCast(gd.?(device, "vkUnmapMemory")); + vk.CreateBuffer = @ptrCast(gd.?(device, "vkCreateBuffer")); + vk.DestroyBuffer = @ptrCast(gd.?(device, "vkDestroyBuffer")); + vk.GetBufferMemoryRequirements = @ptrCast(gd.?(device, "vkGetBufferMemoryRequirements")); + vk.BindBufferMemory = @ptrCast(gd.?(device, "vkBindBufferMemory")); + vk.CreateShaderModule = @ptrCast(gd.?(device, "vkCreateShaderModule")); + vk.DestroyShaderModule = @ptrCast(gd.?(device, "vkDestroyShaderModule")); + vk.CreateGraphicsPipelines = @ptrCast(gd.?(device, "vkCreateGraphicsPipelines")); + vk.DestroyPipeline = @ptrCast(gd.?(device, "vkDestroyPipeline")); + vk.CreatePipelineLayout = @ptrCast(gd.?(device, "vkCreatePipelineLayout")); + vk.DestroyPipelineLayout = @ptrCast(gd.?(device, "vkDestroyPipelineLayout")); + vk.CreateDescriptorSetLayout = @ptrCast(gd.?(device, "vkCreateDescriptorSetLayout")); + vk.DestroyDescriptorSetLayout = @ptrCast(gd.?(device, "vkDestroyDescriptorSetLayout")); + vk.CreateDescriptorPool = @ptrCast(gd.?(device, "vkCreateDescriptorPool")); + vk.DestroyDescriptorPool = @ptrCast(gd.?(device, "vkDestroyDescriptorPool")); + vk.AllocateDescriptorSets = @ptrCast(gd.?(device, "vkAllocateDescriptorSets")); + vk.UpdateDescriptorSets = @ptrCast(gd.?(device, "vkUpdateDescriptorSets")); + vk.CreateSemaphore = @ptrCast(gd.?(device, "vkCreateSemaphore")); + vk.DestroySemaphore = @ptrCast(gd.?(device, "vkDestroySemaphore")); + vk.CreateFence = @ptrCast(gd.?(device, "vkCreateFence")); + vk.DestroyFence = @ptrCast(gd.?(device, "vkDestroyFence")); + vk.WaitForFences = @ptrCast(gd.?(device, "vkWaitForFences")); + vk.ResetFences = @ptrCast(gd.?(device, "vkResetFences")); + vk.CmdBeginRenderPass = @ptrCast(gd.?(device, "vkCmdBeginRenderPass")); + vk.CmdEndRenderPass = @ptrCast(gd.?(device, "vkCmdEndRenderPass")); + vk.CmdBindPipeline = @ptrCast(gd.?(device, "vkCmdBindPipeline")); + vk.CmdBindVertexBuffers = @ptrCast(gd.?(device, "vkCmdBindVertexBuffers")); + vk.CmdDraw = @ptrCast(gd.?(device, "vkCmdDraw")); + vk.CmdSetViewport = @ptrCast(gd.?(device, "vkCmdSetViewport")); + vk.CmdSetScissor = @ptrCast(gd.?(device, "vkCmdSetScissor")); + vk.CmdPushConstants = @ptrCast(gd.?(device, "vkCmdPushConstants")); + vk.CmdBindDescriptorSets = @ptrCast(gd.?(device, "vkCmdBindDescriptorSets")); + vk.CmdCopyImageToBuffer = @ptrCast(gd.?(device, "vkCmdCopyImageToBuffer")); + vk.CmdPipelineBarrier = @ptrCast(gd.?(device, "vkCmdPipelineBarrier")); + vk.CmdBlitImage = @ptrCast(gd.?(device, "vkCmdBlitImage")); + vk.CreateSwapchainKHR = @ptrCast(gd.?(device, "vkCreateSwapchainKHR")); + vk.DestroySwapchainKHR = @ptrCast(gd.?(device, "vkDestroySwapchainKHR")); + vk.GetSwapchainImagesKHR = @ptrCast(gd.?(device, "vkGetSwapchainImagesKHR")); + vk.AcquireNextImageKHR = @ptrCast(gd.?(device, "vkAcquireNextImageKHR")); + vk.FlushMappedMemoryRanges = @ptrCast(gd.?(device, "vkFlushMappedMemoryRanges")); + vk.InvalidateMappedMemoryRanges = @ptrCast(gd.?(device, "vkInvalidateMappedMemoryRanges")); +} + +// --------------------------------------------------------------------------- +// Utility: check VkResult +// --------------------------------------------------------------------------- + +pub fn check(result: c.VkResult) !void { + if (result == c.VK_SUCCESS) return; + switch (result) { + c.VK_ERROR_OUT_OF_HOST_MEMORY => return error.VkOutOfHostMemory, + c.VK_ERROR_OUT_OF_DEVICE_MEMORY => return error.VkOutOfDeviceMemory, + c.VK_ERROR_INITIALIZATION_FAILED => return error.VkInitializationFailed, + c.VK_ERROR_DEVICE_LOST => return error.VkDeviceLost, + c.VK_ERROR_MEMORY_MAP_FAILED => return error.VkMemoryMapFailed, + c.VK_ERROR_LAYER_NOT_PRESENT => return error.VkLayerNotPresent, + c.VK_ERROR_EXTENSION_NOT_PRESENT => return error.VkExtensionNotPresent, + c.VK_ERROR_FEATURE_NOT_PRESENT => return error.VkFeatureNotPresent, + c.VK_ERROR_INCOMPATIBLE_DRIVER => return error.VkIncompatibleDriver, + c.VK_ERROR_TOO_MANY_OBJECTS => return error.VkTooManyObjects, + c.VK_ERROR_FORMAT_NOT_SUPPORTED => return error.VkFormatNotSupported, + c.VK_ERROR_SURFACE_LOST_KHR => return error.VkSurfaceLost, + c.VK_ERROR_OUT_OF_DATE_KHR => return error.VkOutOfDate, + c.VK_SUBOPTIMAL_KHR => return, // treat as success + else => return error.VkUnknownError, + } +} + +// --------------------------------------------------------------------------- +// Memory type selection +// --------------------------------------------------------------------------- + +pub fn findMemoryType( + mem_props: c.VkPhysicalDeviceMemoryProperties, + type_bits: u32, + required_flags: c.VkMemoryPropertyFlags, +) !u32 { + var i: u32 = 0; + while (i < mem_props.memoryTypeCount) : (i += 1) { + const has_bit = (type_bits & (@as(u32, 1) << @intCast(i))) != 0; + const has_flags = (mem_props.memoryTypes[i].propertyFlags & required_flags) == required_flags; + if (has_bit and has_flags) return i; + } + return error.NoSuitableMemoryType; +} + +// --------------------------------------------------------------------------- +// Buffer creation helper +// --------------------------------------------------------------------------- + +pub const Buffer = struct { + buffer: c.VkBuffer = null, + memory: c.VkDeviceMemory = null, + size: c.VkDeviceSize = 0, + device: c.VkDevice = null, + + pub fn init( + device: c.VkDevice, + mem_props: c.VkPhysicalDeviceMemoryProperties, + size: c.VkDeviceSize, + usage: c.VkBufferUsageFlags, + mem_flags: c.VkMemoryPropertyFlags, + ) !Buffer { + var buf = Buffer{ .size = size, .device = device }; + + const create_info = c.VkBufferCreateInfo{ + .sType = c.VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = null, + .flags = 0, + .size = size, + .usage = usage, + .sharingMode = c.VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = null, + }; + try check(vk.CreateBuffer.?(device, &create_info, null, &buf.buffer)); + + var mem_reqs: c.VkMemoryRequirements = undefined; + vk.GetBufferMemoryRequirements.?(device, buf.buffer, &mem_reqs); + + const alloc_info = c.VkMemoryAllocateInfo{ + .sType = c.VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .pNext = null, + .allocationSize = mem_reqs.size, + .memoryTypeIndex = try findMemoryType(mem_props, mem_reqs.memoryTypeBits, mem_flags), + }; + try check(vk.AllocateMemory.?(device, &alloc_info, null, &buf.memory)); + try check(vk.BindBufferMemory.?(device, buf.buffer, buf.memory, 0)); + + return buf; + } + + pub fn deinit(self: *Buffer) void { + if (self.buffer != null) vk.DestroyBuffer.?(self.device, self.buffer, null); + if (self.memory != null) vk.FreeMemory.?(self.device, self.memory, null); + self.* = .{}; + } + + /// Upload data to this (host-visible) buffer. + pub fn upload(self: Buffer, data: []const u8) !void { + var mapped: ?*anyopaque = null; + try check(vk.MapMemory.?(self.device, self.memory, 0, self.size, 0, &mapped)); + @memcpy(@as([*]u8, @ptrCast(mapped.?))[0..data.len], data); + vk.UnmapMemory.?(self.device, self.memory); + } +}; + +// --------------------------------------------------------------------------- +// Single-shot command buffer (for one-time submissions) +// --------------------------------------------------------------------------- + +pub fn beginOneShot(device: c.VkDevice, pool: c.VkCommandPool) !c.VkCommandBuffer { + var cmd: c.VkCommandBuffer = undefined; + const alloc_info = c.VkCommandBufferAllocateInfo{ + .sType = c.VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .pNext = null, + .commandPool = pool, + .level = c.VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = 1, + }; + try check(vk.AllocateCommandBuffers.?(device, &alloc_info, &cmd)); + + const begin_info = c.VkCommandBufferBeginInfo{ + .sType = c.VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = null, + .flags = c.VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, + .pInheritanceInfo = null, + }; + try check(vk.BeginCommandBuffer.?(cmd, &begin_info)); + return cmd; +} + +pub fn submitOneShot(device: c.VkDevice, pool: c.VkCommandPool, queue: c.VkQueue, cmd: c.VkCommandBuffer) !void { + try check(vk.EndCommandBuffer.?(cmd)); + + const submit_info = c.VkSubmitInfo{ + .sType = c.VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = null, + .waitSemaphoreCount = 0, + .pWaitSemaphores = null, + .pWaitDstStageMask = null, + .commandBufferCount = 1, + .pCommandBuffers = &cmd, + .signalSemaphoreCount = 0, + .pSignalSemaphores = null, + }; + try check(vk.QueueSubmit.?(queue, 1, &submit_info, null)); + try check(vk.QueueWaitIdle.?(queue)); + vk.FreeCommandBuffers.?(device, pool, 1, &cmd); +} diff --git a/src/window.zig b/src/window.zig new file mode 100644 index 0000000..9574451 --- /dev/null +++ b/src/window.zig @@ -0,0 +1,97 @@ +//! Platform-agnostic Window interface. +//! +//! The Window type uses a vtable pattern to allow different platform backends +//! (Wayland, macOS/Cocoa, Win32) to implement the same interface. +//! +//! Currently only the Wayland backend is implemented. + +const std = @import("std"); +const input = @import("input.zig"); +const Event = input.Event; +const surface_mod = @import("surface.zig"); +const Surface = surface_mod.Surface; +const color_mod = @import("color.zig"); + +// --------------------------------------------------------------------------- +// Window vtable — platform backends implement these function pointers +// --------------------------------------------------------------------------- + +pub const WindowVTable = struct { + deinit: *const fn (self: *anyopaque) void, + pollEvent: *const fn (self: *anyopaque) ?Event, + shouldClose: *const fn (self: *anyopaque) bool, + /// present is called after the surface has been flushed. + /// `surf_image` is the VkImage handle of the surface's offscreen image (as usize). + presentSurface: *const fn (self: *anyopaque, surf_image: usize, sw: u32, sh: u32) anyerror!void, + getWidth: *const fn (self: *anyopaque) u32, + getHeight: *const fn (self: *anyopaque) u32, + setTitle: *const fn (self: *anyopaque, title: []const u8) void, +}; + +// --------------------------------------------------------------------------- +// Window — the public-facing type +// --------------------------------------------------------------------------- + +pub const Window = struct { + impl: *anyopaque, + vtable: *const WindowVTable, + /// The surface associated with this window. Created when the window is initialized. + surface: Surface, + + /// Create a new window with the specified size and title. + /// Uses the Wayland backend on Linux. + pub fn init(allocator: std.mem.Allocator, width: u32, height: u32, title: []const u8) !Window { + // Platform selection: on Linux we always use Wayland for now. + // Future: detect platform and dispatch to the right backend. + const wayland = @import("platform/wayland.zig"); + return wayland.WaylandWindow.initWindow(allocator, width, height, title); + } + + pub fn deinit(self: *Window) void { + self.surface.deinit(); + self.vtable.deinit(self.impl); + } + + /// Poll for the next pending event. Returns `null` when no more events. + pub fn pollEvent(self: *Window) ?Event { + return self.vtable.pollEvent(self.impl); + } + + /// Returns `true` if the window has been requested to close. + pub fn shouldClose(self: *Window) bool { + return self.vtable.shouldClose(self.impl); + } + + /// Present the window's surface to the screen. + /// Calls `surface.flush()` then blits/presents to the screen. + pub fn present(self: *Window) !void { + try self.surface.flush(); + const img_handle: usize = @intFromPtr(self.surface.image); + return self.vtable.presentSurface( + self.impl, + img_handle, + self.surface.width, + self.surface.height, + ); + } + + /// Get the window's current width. + pub fn getWidth(self: *Window) u32 { + return self.vtable.getWidth(self.impl); + } + + /// Get the window's current height. + pub fn getHeight(self: *Window) u32 { + return self.vtable.getHeight(self.impl); + } + + /// Set the window title. + pub fn setTitle(self: *Window, title: []const u8) void { + self.vtable.setTitle(self.impl, title); + } + + /// Access the window's drawing surface. + pub fn getSurface(self: *Window) *Surface { + return &self.surface; + } +};