diff --git a/examples/raylib-resizable-sidebar/build.zig b/examples/raylib-resizable-sidebar/build.zig new file mode 100644 index 0000000..f84b6dd --- /dev/null +++ b/examples/raylib-resizable-sidebar/build.zig @@ -0,0 +1,93 @@ +const std = @import("std"); +const B = std.Build; + +pub fn build(b: *B) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + { + const exe = b.addExecutable(.{ + .name = "zclay-example", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + addDependencies(exe, b, target, optimize); + + b.installArtifact(exe); + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + } + + { + const exe_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + addDependencies(exe_unit_tests, b, target, optimize); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); + } + + { + const exe_check = b.addExecutable(.{ + .name = "check", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + addDependencies(exe_check, b, target, optimize); + + const tests_check = b.addTest(.{ + .name = "check", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + addDependencies(tests_check, b, target, optimize); + + const check = b.step("check", "Check if exe and tests compile"); + check.dependOn(&exe_check.step); + check.dependOn(&tests_check.step); + } +} + +fn addDependencies( + compile_step: *B.Step.Compile, + b: *B, + target: B.ResolvedTarget, + optimize: std.builtin.OptimizeMode, +) void { + const raylib_dep = b.dependency("raylib-zig", .{ + .target = target, + .optimize = optimize, + }); + + const raylib = raylib_dep.module("raylib"); + const raylib_artifact = raylib_dep.artifact("raylib"); + + compile_step.linkLibrary(raylib_artifact); + compile_step.root_module.addImport("raylib", raylib); + + const zclay_dep = b.dependency("zclay", .{ + .target = target, + .optimize = optimize, + }); + + const zclay = zclay_dep.module("zclay"); + compile_step.root_module.addImport("zclay", zclay); +} diff --git a/examples/raylib-resizable-sidebar/build.zig.zon b/examples/raylib-resizable-sidebar/build.zig.zon new file mode 100644 index 0000000..daa499a --- /dev/null +++ b/examples/raylib-resizable-sidebar/build.zig.zon @@ -0,0 +1,20 @@ +.{ + .name = "zig-exe-template", + .version = "0.0.0", + .dependencies = .{ + .zclay = .{ + .path = "../../", + }, + .@"raylib-zig" = .{ + .url = "https://github.com/johan0A/raylib-zig/archive/db141613a6d5fc7c3c94a52965669c0e86444c50.tar.gz", + .hash = "1220a100a2f60cf4c970110666b9f3607fa388c5544de0311df2c36898b516e424b4", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "LICENSE", + "README.md", + }, +} diff --git a/examples/raylib-resizable-sidebar/src/main.zig b/examples/raylib-resizable-sidebar/src/main.zig new file mode 100644 index 0000000..64a1ec1 --- /dev/null +++ b/examples/raylib-resizable-sidebar/src/main.zig @@ -0,0 +1,184 @@ +const std = @import("std"); +const rl = @import("raylib"); +const cl = @import("zclay"); +const renderer = @import("raylib_render_clay.zig"); + +const light_grey: cl.Color = .{ 224, 215, 210, 255 }; +const red: cl.Color = .{ 168, 66, 28, 255 }; +const orange: cl.Color = .{ 225, 138, 50, 255 }; + +const sidebarItemLayout: cl.LayoutConfig = .{ + .sizing = .{ + .width = cl.sizingGrow(.{}), + .height = cl.sizingFixed(50), + }, +}; + +var side_bar_handle: struct { + position: f32, + dragging: bool, + start_drag_pos: f32, + start_drag_mouse_pos: f32, + + fn tick(self: *@This()) void { + const mouse_pos = rl.getMousePosition(); + if (!rl.isMouseButtonDown(.mouse_button_left)) + self.dragging = false; + + if (self.dragging == true) { + if (!rl.isMouseButtonDown(.mouse_button_left)) + self.dragging = false; + self.position = self.start_drag_pos + mouse_pos.x - self.start_drag_mouse_pos; + self.position = @min(@as(f32, @floatFromInt(rl.getScreenWidth() - 100)), @max(270, self.position)); + } else { + if (rl.isMouseButtonPressed(.mouse_button_left) and cl.pointerOver(cl.ID("ResizeHandle"))) { + self.dragging = true; + self.start_drag_mouse_pos = mouse_pos.x; + self.start_drag_pos = self.position; + } + } + if (cl.pointerOver(cl.ID("ResizeHandle")) or self.dragging) { + rl.setMouseCursor(.mouse_cursor_pointing_hand); + } else { + rl.setMouseCursor(.mouse_cursor_arrow); + } + } +} = .{ + .position = 300, + .dragging = false, + .start_drag_pos = 0, + .start_drag_mouse_pos = 0, +}; + +fn sidebarItemCompoment(index: usize) void { + cl.rectangle(cl.IDI("SidebarBlob", @intCast(index)), cl.layout(sidebarItemLayout), cl.rectangleConfig(.{ .color = orange })); + defer cl.closeParent(); +} + +fn createLayout(profile_picture: *const rl.Texture2D) cl.ClayArray(cl.RenderCommand) { + cl.beginLayout(); + { + cl.rectangle( + cl.ID("OuterContainer"), + cl.layout(.{ + .layoutDirection = .LEFT_TO_RIGHT, + .sizing = .{ .height = cl.sizingGrow(.{}), .width = cl.sizingGrow(.{}) }, + .padding = .{ .x = 16, .y = 16 }, + }), + cl.rectangleConfig(.{ .color = .{ 250, 250, 255, 255 } }), + ); + defer cl.closeParent(); + + { + cl.rectangle( + cl.ID("SideBar"), + cl.layout(.{ + .layoutDirection = .TOP_TO_BOTTOM, + .sizing = .{ .height = cl.sizingGrow(.{}), .width = cl.sizingFixed(side_bar_handle.position) }, + .padding = .{ .x = 16, .y = 16 }, + .childAlignment = .{ .x = .CENTER, .y = .TOP }, + .childGap = 16, + }), + cl.rectangleConfig(.{ .color = light_grey }), + ); + defer cl.closeParent(); + + { + cl.rectangle( + cl.ID("ProfilePictureOuter"), + cl.layout(.{ + .sizing = .{ .width = cl.sizingGrow(.{}) }, + .padding = .{ .x = 16, .y = 16 }, + .childAlignment = .{ .y = .CENTER }, + .childGap = 16, + }), + cl.rectangleConfig(.{ .color = red }), + ); + defer cl.closeParent(); + + cl.image( + cl.ID("ProfilePicture"), + cl.layout(.{ .sizing = .{ .height = cl.sizingFixed(60), .width = cl.sizingFixed(60) } }), + cl.imageConfig(.{ .sourceDimensions = .{ .height = 60, .width = 60 }, .imageData = @ptrCast(@constCast(profile_picture)) }), + ); + cl.closeParent(); + cl.text(cl.ID("profileTitle"), "Clay - UI Library", cl.textConfig(.{ .fontSize = 24, .textColor = light_grey })); + } + + for (0..5) |i| { + sidebarItemCompoment(i); + } + } + { + cl.rectangle(cl.ID("ResizeHandle"), cl.layout(.{ .padding = .{ .x = 7, .y = 7 }, .sizing = .{ .height = cl.sizingGrow(.{}), .width = cl.sizingFit(.{}) } }), cl.rectangleConfig(.{ .color = .{ 0, 0, 0, 0 } })); + defer cl.closeParent(); + cl.rectangle(cl.ID("ResizeHandleInner"), cl.layout(.{ .sizing = .{ .height = cl.sizingGrow(.{}), .width = cl.sizingFixed(2) } }), cl.rectangleConfig(.{ .color = red })); + defer cl.closeParent(); + } + { + cl.rectangle( + cl.ID("MainContent"), + cl.layout(.{ .sizing = .{ .height = cl.sizingGrow(.{}), .width = cl.sizingGrow(.{}) } }), + cl.rectangleConfig(.{ .color = light_grey }), + ); + defer cl.closeParent(); + } + } + return cl.endLayout(); +} + +fn loadFont(file_data: ?[]const u8, fontId: u16, fontSize: i32) void { + renderer.raylib_fonts[fontId] = rl.loadFontFromMemory(".ttf", file_data, fontSize * 2, null); + rl.setTextureFilter(renderer.raylib_fonts[fontId].?.texture, .texture_filter_bilinear); +} + +pub fn main() anyerror!void { + const allocator = std.heap.page_allocator; + + const min_memory_size: u32 = cl.minMemorySize(); + const memory = try allocator.alloc(u8, min_memory_size); + defer allocator.free(memory); + const arena: cl.Arena = cl.createArenaWithCapacityAndMemory(min_memory_size, @ptrCast(memory)); + + cl.initialize(arena, .{ .height = 1000, .width = 1000 }); + cl.setMeasureTextFunction(renderer.measureText); + + rl.setConfigFlags(.{ + .msaa_4x_hint = true, + .vsync_hint = true, + .window_highdpi = true, + .window_resizable = true, + }); + rl.initWindow(1000, 1000, "Raylib zig Example"); + rl.setTargetFPS(60); + + loadFont(@embedFile("./resources/Roboto-Regular.ttf"), 0, 100); + const profile_picture = rl.loadTextureFromImage(rl.loadImageFromMemory(".png", @embedFile("./resources/profile-picture.png"))); + + var debug_mode_enabled = false; + + while (!rl.windowShouldClose()) { + if (rl.isKeyPressed(.key_d)) { + debug_mode_enabled = !debug_mode_enabled; + cl.setDebugModeEnabled(debug_mode_enabled); + } + + const mouse_pos = rl.getMousePosition(); + cl.setPointerState(.{ + .x = mouse_pos.x, + .y = mouse_pos.y, + }, rl.isMouseButtonDown(.mouse_button_left)); + + cl.setLayoutDimensions(.{ + .width = @floatFromInt(rl.getScreenWidth()), + .height = @floatFromInt(rl.getScreenHeight()), + }); + var renderCommands = createLayout(&profile_picture); + + rl.beginDrawing(); + renderer.clayRaylibRender(&renderCommands, allocator); + rl.endDrawing(); + + side_bar_handle.tick(); + } +} diff --git a/examples/raylib-resizable-sidebar/src/raylib_render_clay.zig b/examples/raylib-resizable-sidebar/src/raylib_render_clay.zig new file mode 100644 index 0000000..418f1fa --- /dev/null +++ b/examples/raylib-resizable-sidebar/src/raylib_render_clay.zig @@ -0,0 +1,232 @@ +const std = @import("std"); +const rl = @import("raylib"); +const cl = @import("zclay"); +const math = std.math; + +pub fn clayColorToRaylibColor(color: cl.Color) rl.Color { + return rl.Color{ + .r = @intFromFloat(color[0]), + .g = @intFromFloat(color[1]), + .b = @intFromFloat(color[2]), + .a = @intFromFloat(color[3]), + }; +} + +pub var raylib_fonts: [10]?rl.Font = .{null} ** 10; + +pub fn clayRaylibRender(renderCommands: *cl.ClayArray(cl.RenderCommand), allocator: std.mem.Allocator) void { + var i: usize = 0; + while (i < renderCommands.length) : (i += 1) { + const renderCommand = cl.renderCommandArrayGet(renderCommands, @intCast(i)); + const boundingBox = renderCommand.boundingBox; + switch (renderCommand.commandType) { + .None => {}, + .Text => { + const text = renderCommand.text.chars[0..@intCast(renderCommand.text.length)]; + const cloned = allocator.dupeZ(c_char, text) catch unreachable; + defer allocator.free(cloned); + const fontToUse: rl.Font = raylib_fonts[renderCommand.config.textElementConfig.fontId].?; + rl.setTextLineSpacing(renderCommand.config.textElementConfig.lineSpacing); + rl.drawTextEx( + fontToUse, + @ptrCast(@alignCast(cloned.ptr)), + rl.Vector2{ .x = boundingBox.x, .y = boundingBox.y }, + @floatFromInt(renderCommand.config.textElementConfig.fontSize), + @floatFromInt(renderCommand.config.textElementConfig.letterSpacing), + clayColorToRaylibColor(renderCommand.config.textElementConfig.textColor), + ); + }, + .Image => { + const imageTexture: *rl.Texture2D = @ptrCast( + @alignCast(renderCommand.config.imageElementConfig.imageData), + ); + rl.drawTextureEx( + imageTexture.*, + rl.Vector2{ .x = boundingBox.x, .y = boundingBox.y }, + 0, + boundingBox.width / @as(f32, @floatFromInt(imageTexture.width)), + rl.Color.white, + ); + }, + .ScissorStart => { + rl.beginScissorMode( + @intFromFloat(math.round(boundingBox.x)), + @intFromFloat(math.round(boundingBox.y)), + @intFromFloat(math.round(boundingBox.width)), + @intFromFloat(math.round(boundingBox.height)), + ); + }, + .ScissorEnd => rl.endScissorMode(), + .Rectangle => { + const config = renderCommand.config.rectangleElementConfig; + if (config.cornerRadius.topLeft > 0) { + const radius: f32 = (config.cornerRadius.topLeft * 2) / @min(boundingBox.width, boundingBox.height); + rl.drawRectangleRounded( + rl.Rectangle{ + .x = boundingBox.x, + .y = boundingBox.y, + .width = boundingBox.width, + .height = boundingBox.height, + }, + radius, + 8, + clayColorToRaylibColor(config.color), + ); + } else { + rl.drawRectangle( + @intFromFloat(boundingBox.x), + @intFromFloat(boundingBox.y), + @intFromFloat(boundingBox.width), + @intFromFloat(boundingBox.height), + clayColorToRaylibColor(config.color), + ); + } + }, + .Border => { + const config = renderCommand.config.borderElementConfig; + if (config.left.width > 0) { + rl.drawRectangle( + @intFromFloat(math.round(boundingBox.x)), + @intFromFloat(math.round(boundingBox.y + config.cornerRadius.topLeft)), + @intCast(config.left.width), + @intFromFloat(math.round(boundingBox.height - config.cornerRadius.topLeft - config.cornerRadius.bottomLeft)), + clayColorToRaylibColor(config.left.color), + ); + } + if (config.right.width > 0) { + rl.drawRectangle( + @intFromFloat(math.round(boundingBox.x + boundingBox.width - @as(f32, @floatFromInt(config.right.width)))), + @intFromFloat(math.round(boundingBox.y + config.cornerRadius.topRight)), + @intCast(config.right.width), + @intFromFloat(math.round(boundingBox.height - config.cornerRadius.topRight - config.cornerRadius.bottomRight)), + clayColorToRaylibColor(config.right.color), + ); + } + if (config.top.width > 0) { + rl.drawRectangle( + @intFromFloat(math.round(boundingBox.x + config.cornerRadius.topLeft)), + @intFromFloat(math.round(boundingBox.y)), + @intFromFloat(math.round(boundingBox.width - config.cornerRadius.topLeft - config.cornerRadius.topRight)), + @intCast(config.top.width), + clayColorToRaylibColor(config.top.color), + ); + } + if (config.bottom.width > 0) { + rl.drawRectangle( + @intFromFloat(math.round(boundingBox.x + config.cornerRadius.bottomLeft)), + @intFromFloat(math.round(boundingBox.y + boundingBox.height - @as(f32, @floatFromInt(config.bottom.width)))), + @intFromFloat(math.round(boundingBox.width - config.cornerRadius.bottomLeft - config.cornerRadius.bottomRight)), + @intCast(config.bottom.width), + clayColorToRaylibColor(config.bottom.color), + ); + } + + if (config.cornerRadius.topLeft > 0) { + rl.drawRing( + rl.Vector2{ + .x = math.round(boundingBox.x + config.cornerRadius.topLeft), + .y = math.round(boundingBox.y + config.cornerRadius.topLeft), + }, + math.round(config.cornerRadius.topLeft - @as(f32, @floatFromInt(config.top.width))), + config.cornerRadius.topLeft, + 180, + 270, + 10, + clayColorToRaylibColor(config.top.color), + ); + } + if (config.cornerRadius.topRight > 0) { + rl.drawRing( + rl.Vector2{ + .x = math.round(boundingBox.x + boundingBox.width - config.cornerRadius.topRight), + .y = math.round(boundingBox.y + config.cornerRadius.topRight), + }, + math.round(config.cornerRadius.topRight - @as(f32, @floatFromInt(config.top.width))), + config.cornerRadius.topRight, + 270, + 360, + 10, + clayColorToRaylibColor(config.top.color), + ); + } + if (config.cornerRadius.bottomLeft > 0) { + rl.drawRing( + rl.Vector2{ + .x = math.round(boundingBox.x + config.cornerRadius.bottomLeft), + .y = math.round(boundingBox.y + boundingBox.height - config.cornerRadius.bottomLeft), + }, + math.round(config.cornerRadius.bottomLeft - @as(f32, @floatFromInt(config.top.width))), + config.cornerRadius.bottomLeft, + 90, + 180, + 10, + clayColorToRaylibColor(config.bottom.color), + ); + } + if (config.cornerRadius.bottomRight > 0) { + rl.drawRing( + rl.Vector2{ + .x = math.round(boundingBox.x + boundingBox.width - config.cornerRadius.bottomRight), + .y = math.round(boundingBox.y + boundingBox.height - config.cornerRadius.bottomRight), + }, + math.round(config.cornerRadius.bottomRight - @as(f32, @floatFromInt(config.top.width))), + config.cornerRadius.bottomRight, + 0.1, + 90, + 10, + clayColorToRaylibColor(config.bottom.color), + ); + } + }, + .Custom => { + // Implement custom element rendering here + }, + } + } +} + +pub fn measureText(clay_text: []const u8, config: *cl.TextElementConfig) cl.Dimensions { + const font = raylib_fonts[config.fontId].?; + const text: []const u8 = clay_text; + const font_size: f32 = @floatFromInt(config.fontSize); + const letter_spacing: f32 = @floatFromInt(config.letterSpacing); + const line_spacing = config.lineSpacing; + + var temp_byte_counter: usize = 0; + var byte_counter: usize = 0; + var text_width: f32 = 0.0; + var temp_text_width: f32 = 0.0; + var text_height: f32 = font_size; + const scale_factor: f32 = font_size / @as(f32, @floatFromInt(font.baseSize)); + + var utf8 = std.unicode.Utf8View.initUnchecked(text).iterator(); + + while (utf8.nextCodepoint()) |codepoint| { + byte_counter += std.unicode.utf8CodepointSequenceLength(codepoint) catch 1; + const index: usize = @intCast( + rl.getGlyphIndex(font, @as(i32, @intCast(codepoint))), + ); + + if (codepoint != '\n') { + if (font.glyphs[index].advanceX != 0) { + text_width += @floatFromInt(font.glyphs[index].advanceX); + } else { + text_width += font.recs[index].width + @as(f32, @floatFromInt(font.glyphs[index].offsetX)); + } + } else { + if (temp_text_width < text_width) temp_text_width = text_width; + byte_counter = 0; + text_width = 0; + text_height += font_size + @as(f32, @floatFromInt(line_spacing)); + } + + if (temp_byte_counter < byte_counter) temp_byte_counter = byte_counter; + } + + if (temp_text_width < text_width) temp_text_width = text_width; + + return cl.Dimensions{ + .height = text_height, + .width = temp_text_width * scale_factor + @as(f32, @floatFromInt(temp_byte_counter - 1)) * letter_spacing, + }; +} diff --git a/examples/raylib-resizable-sidebar/src/resources/Roboto-Regular.ttf b/examples/raylib-resizable-sidebar/src/resources/Roboto-Regular.ttf new file mode 100644 index 0000000..ddf4bfa Binary files /dev/null and b/examples/raylib-resizable-sidebar/src/resources/Roboto-Regular.ttf differ diff --git a/examples/raylib-resizable-sidebar/src/resources/RobotoMono-Medium.ttf b/examples/raylib-resizable-sidebar/src/resources/RobotoMono-Medium.ttf new file mode 100644 index 0000000..f6c149a Binary files /dev/null and b/examples/raylib-resizable-sidebar/src/resources/RobotoMono-Medium.ttf differ diff --git a/examples/raylib-resizable-sidebar/src/resources/profile-picture.png b/examples/raylib-resizable-sidebar/src/resources/profile-picture.png new file mode 100644 index 0000000..8c4ea3e Binary files /dev/null and b/examples/raylib-resizable-sidebar/src/resources/profile-picture.png differ