Skip to content

Instantly share code, notes, and snippets.

@raspberrypisig
Created April 26, 2025 21:12
Show Gist options
  • Save raspberrypisig/51ea2d28c4133520c63ccbdf411f437b to your computer and use it in GitHub Desktop.
Save raspberrypisig/51ea2d28c4133520c63ccbdf411f437b to your computer and use it in GitHub Desktop.
1 | const std = @import("std");
2 | const rl = @import("raylib");
3 | const cl = @import("zclay");
4 | const renderer = @import("raylib_render_clay.zig");
5 | const math = std.math; // Import math for ceil
6 | const fmt = std.fmt; // Import fmt for string formatting
7 |
8 | const light_grey: cl.Color = .{ 224, 215, 210, 255 };
9 | const red: cl.Color = .{ 168, 66, 28, 255 };
10 | const orange: cl.Color = .{ 225, 138, 50, 255 };
11 | const white: cl.Color = .{ 250, 250, 255, 255 };
12 | const dark_grey: cl.Color = .{ 80, 80, 80, 255 }; // Added color
13 |
14 | const sidebar_item_layout: cl.LayoutConfig = .{ .sizing = .{ .w = .grow, .h = .fixed(50) } };
15 |
16 | // Unique ID for the paginated container
17 | const PAGINATED_CONTAINER_ID = cl.ElementId.ID("PaginatedContentContainer");
18 |
19 | // Global state for pagination
20 | var g_current_page: usize = 1;
21 | var g_total_pages: usize = 1;
22 |
23 | // Re-useable components are just normal functions
24 | fn sidebarItemComponent(index: u32) void {
25 | cl.UI()(.{
26 | .id = .IDI("SidebarBlob", index),
27 | .layout = sidebar_item_layout,
28 | .background_color = orange,
29 | .corner_radius = .all(4),
30 | })({});
31 | }
32 |
33 | // An example function to begin the "root" of your layout tree
34 | fn createLayout(allocator: std.mem.Allocator, profile_picture: *const rl.Texture2D) cl.ClayArray(cl.RenderCommand) {
35 | cl.beginLayout();
36 | cl.UI()(.{
37 | .id = .ID("OuterContainer"),
38 | .layout = .{ .direction = .left_to_right, .sizing = .grow, .padding = .all(16), .child_gap = 16 },
39 | .background_color = white,
40 | })({
41 | // --- Sidebar (Unchanged) ---
42 | cl.UI()(.{
43 | .id = .ID("SideBar"),
44 | .layout = .{
45 | .direction = .top_to_bottom,
46 | .sizing = .{ .h = .grow, .w = .fixed(300) },
47 | .padding = .all(16),
48 | .child_alignment = .{ .x = .center, .y = .top },
49 | .child_gap = 16,
50 | },
51 | .background_color = light_grey,
52 | .corner_radius = .all(8),
53 | })({
54 | cl.UI()(.{
55 | .id = .ID("ProfilePictureOuter"),
56 | .layout = .{ .sizing = .{ .w = .grow }, .padding = .all(16), .child_alignment = .{ .x = .left, .y = .center }, .child_gap = 16 },
57 | .background_color = red,
58 | .corner_radius = .all(8),
59 | })({
60 | cl.UI()(.{
61 | .id = .ID("ProfilePicture"),
62 | .layout = .{ .sizing = .{ .h = .fixed(60), .w = .fixed(60) } },
63 | .image = .{ .source_dimensions = .{ .h = 60, .w = 60 }, .image_data = @ptrCast(profile_picture) },
64 | .corner_radius = .all(30), // Make it round
65 | })({});
66 | cl.text("Clay Pagination", .{ .font_size = 24, .color = light_grey });
67 | });
68 |
69 | for (0..5) |i| sidebarItemComponent(@intCast(i));
70 | });
71 | // --- End Sidebar ---
72 |
73 | // --- Main Content Area (Modified for Pagination) ---
74 | cl.UI()(.{
75 | .id = .ID("MainContentAreaWrapper"), // New wrapper for content + indicator
76 | .layout = .{ .direction = .top_to_bottom, .sizing = .grow, .child_gap = 8 }, // Add gap
77 | .background_color = light_grey, // Match content background
78 | .corner_radius = .all(8),
79 | })({
80 | // This container holds the scrollable/paginated content
81 | cl.UI()(.{
82 | .id = PAGINATED_CONTAINER_ID, // Assign the unique ID
83 | .layout = .{ .sizing = .grow, .padding = .all(16) }, // Grow to fill space, add padding
84 | .scroll = .{ .vertical = true }, // IMPORTANT: Enable vertical scroll for clipping & scroll data
85 | // No background here, let items inside define it or keep wrapper's bg
86 | })({
87 | // --- CONTENT TO BE PAGINATED ---
88 | // Add more content than fits vertically to test pagination
89 | for (0..30) |i| {
90 | cl.UI()(.{
91 | .id = .IDI("ContentItem", @intCast(i)),
92 | .layout = .{
93 | .sizing = .{ .w = .grow, .h = .fixed(50) }, // Fixed height items
94 | .padding = .all(8),
95 | .child_alignment = .center,
96 | },
97 | .background_color = if (i % 2 == 0) red else orange,
98 | .corner_radius = .all(4),
99 | })({
100 | // Use a temporary buffer for formatting text to avoid heap allocation per frame
101 | var buf: [32]u8 = undefined;
102 | const itemText = fmt.bufPrint(&buf, "Content Item {d}", .{i}) catch "Error";
103 | cl.text(itemText, .{ .font_size = 20, .color = white });
104 | });
105 | // Add a small gap between items manually if needed (or use parent's child_gap if direction was top_to_bottom)
106 | // Since the scroll container itself is the parent, we can't use child_gap easily here.
107 | // Instead, we add spacing elements or rely on padding. Let's add a spacer.
108 | if (i < 29) { // Don't add spacer after the last item
109 | cl.UI()(.{ .id = .IDI("Spacer", @intCast(i)), .layout = .{ .sizing = .{ .w = .grow, .h = .fixed(8) } } })({});
110 | }
111 | }
112 | // --- END OF CONTENT TO BE PAGINATED ---
113 | });
114 |
115 | // --- Page Indicator ---
116 | cl.UI()(.{
117 | .id = .ID("PageIndicator"),
118 | .layout = .{
119 | .sizing = .{ .w = .grow, .h = .fit }, // Fit height, grow width
120 | .padding = .{ .top = 4, .bottom = 8, .left = 16, .right = 16 }, // Padding around text
121 | .child_alignment = .center, // Center the text horizontally
122 | },
123 | // No background needed, uses the wrapper's background
124 | })({
125 | var buf: [64]u8 = undefined;
126 | // Use the global state variables
127 | const indicatorText = fmt.bufPrint(&buf, "Page {d} / {d} (PgUp/PgDn or Up/Down)", .{ g_current_page, g_total_pages }) catch "Error";
128 | cl.text(indicatorText, .{ .font_size = 16, .color = dark_grey });
129 | });
130 | // --- End Page Indicator ---
131 | });
132 | // --- End Main Content Area ---
133 | });
134 | return cl.endLayout();
135 | }
136 |
137 | fn loadFont(file_data: ?[]const u8, font_id: u16, font_size: i32) !void {
138 | // Increase resolution slightly for better rendering
139 | renderer.raylib_fonts[font_id] = try rl.loadFontFromMemory(".ttf", file_data, font_size * 2, null);
140 | rl.setTextureFilter(renderer.raylib_fonts[font_id].?.texture, .bilinear);
141 | }
142 |
143 | fn loadImage(comptime path: [:0]const u8) !rl.Texture2D {
144 | const img = try rl.loadImageFromMemory(@ptrCast(std.fs.path.extension(path)), @embedFile(path));
145 | // Optional: Resize image if needed
146 | // rl.imageResizeNN(&img, 60, 60); // Example resize if source isn't 60x60
147 | const texture = try rl.loadTextureFromImage(img);
148 | rl.unloadImage(img); // Unload image RAM copy after texture creation
149 | rl.setTextureFilter(texture, .bilinear);
150 | return texture;
151 | }
152 |
153 | pub fn main() !void {
154 | const allocator = std.heap.page_allocator; // Use a persistent allocator
155 |
156 | // init clay
157 | const min_memory_size: u32 = cl.minMemorySize();
158 | const memory = try allocator.alloc(u8, min_memory_size);
159 | defer allocator.free(memory); // Ensure memory is freed at exit
160 | const arena: cl.Arena = cl.createArenaWithCapacityAndMemory(memory);
161 | _ = cl.initialize(arena, .{ .h = 1000, .w = 1000 }, .{});
162 | cl.setMeasureTextFunction(void, {}, renderer.measureText);
163 |
164 | // init raylib
165 | rl.setConfigFlags(.{
166 | .msaa_4x_hint = true,
167 | .window_resizable = true,
168 | });
169 | rl.initWindow(1000, 800, "Raylib zig Clay - Pagination Example"); // Adjusted initial size
170 | rl.setWindowMinSize(400, 300);
171 | rl.setTargetFPS(60); // 60 FPS is fine for UI
172 |
173 | // load assets
174 | // Using embedded fonts/images from the example
175 | try loadFont(@embedFile("./resources/Roboto-Regular.ttf"), 0, 24); // Font ID 0 for size 24
176 | try loadFont(@embedFile("./resources/Roboto-Regular.ttf"), 1, 20); // Font ID 1 for size 20
177 | try loadFont(@embedFile("./resources/Roboto-Regular.ttf"), 2, 16); // Font ID 2 for size 16
178 | const profile_picture = try loadImage("./resources/profile-picture.png");
179 |
180 | var debug_mode_enabled = false;
181 | while (!rl.windowShouldClose()) {
182 | const dt = rl.getFrameTime(); // Get delta time
183 |
184 | // --- Input Handling ---
185 | if (rl.isKeyPressed(.d)) {
186 | debug_mode_enabled = !debug_mode_enabled;
187 | cl.setDebugModeEnabled(debug_mode_enabled);
188 | }
189 |
190 | // --- Pagination Keyboard Input ---
191 | var page_changed = false;
192 | if (rl.isKeyPressed(.page_down) or rl.isKeyPressed(.down)) {
193 | if (g_current_page < g_total_pages) {
194 | g_current_page += 1;
195 | page_changed = true;
196 | }
197 | }
198 | if (rl.isKeyPressed(.page_up) or rl.isKeyPressed(.up)) {
199 | if (g_current_page > 1) {
200 | g_current_page -= 1;
201 | page_changed = true;
202 | }
203 | }
204 |
205 | // --- Clay Updates ---
206 | const mouse_pos = rl.getMousePosition();
207 | // We still call setPointerState for potential hover effects elsewhere (though not used in this simplified example)
208 | cl.setPointerState(.{
209 | .x = mouse_pos.x,
210 | .y = mouse_pos.y,
211 | }, rl.isMouseButtonDown(.left));
212 |
213 | // IMPORTANT: Do NOT call cl.updateScrollContainers if you want full manual control
214 | // via keyboard, as it processes mouse wheel/drag input.
215 | // If you needed both, you'd need more complex state management.
216 | // const scroll_delta = rl.getMouseWheelMoveV().multiply(.{ .x = 6, .y = 6 });
217 | // cl.updateScrollContainers( false, .{ .x = scroll_delta.x, .y = scroll_delta.y }, dt);
218 |
219 | // Set layout dimensions (needed if window resizes)
220 | cl.setLayoutDimensions(.{
221 | .w = @floatFromInt(rl.getScreenWidth()),
222 | .h = @floatFromInt(rl.getScreenHeight()),
223 | });
224 |
225 | // --- Create Layout ---
226 | // Pass allocator for potential use inside createLayout (like fmt.allocPrint, though we switched to bufPrint)
227 | var render_commands = createLayout(allocator, &profile_picture);
228 |
229 | // --- Post-Layout Calculations & Updates ---
230 |
231 | // Get Scroll Data *after* layout is calculated
232 | const scrollData = cl.getScrollContainerData(PAGINATED_CONTAINER_ID);
233 | var container_height: f32 = 1.0; // Default to avoid division by zero
234 |
235 | if (scrollData.found) {
236 | // Store container height for later use
237 | container_height = @max(1.0, scrollData.scroll_container_dimensions.h); // Ensure positive height
238 |
239 | // Calculate total pages based on content vs container size
240 | if (container_height > 0) {
241 | g_total_pages = @intFromFloat(math.ceil(scrollData.content_dimensions.h / container_height));
242 | if (g_total_pages == 0) g_total_pages = 1; // Ensure at least one page
243 | } else {
244 | g_total_pages = 1;
245 | }
246 |
247 | // Clamp current page if total pages decreased (e.g., window resize)
248 | if (g_current_page > g_total_pages) {
249 | g_current_page = g_total_pages;
250 | page_changed = true; // Need to update scroll position if clamped
251 | }
252 |
253 | // Manually set the scroll position IF the page changed THIS frame
254 | // We do this *after* getting scrollData and *before* the next frame's beginLayout.
255 | if (page_changed) {
256 | const target_scroll_y = @as(f32, @floatFromInt(g_current_page - 1)) * container_height;
257 |
258 | // Ensure the scroll position is valid (Clay might do this internally, but good practice)
259 | const max_scroll_y = @max(0.0, scrollData.content_dimensions.h - container_height);
260 | scrollData.scroll_position.y = math.clamp(target_scroll_y, 0.0, max_scroll_y);
261 |
262 | // Note: Direct modification of scroll_position works because Clay uses this
263 | // value in the *next* layout pass to determine rendering offsets/clipping.
264 | }
265 | } else {
266 | // Paginated container wasn't found in the layout - reset state
267 | g_total_pages = 1;
268 | g_current_page = 1;
269 | }
270 |
271 | // --- Drawing ---
272 | rl.beginDrawing();
273 | rl.clearBackground(rl.Color.ray_white); // Clear background
274 |
275 | // Render the UI generated by Clay
276 | try renderer.clayRaylibRender(&render_commands, allocator);
277 |
278 | if (debug_mode_enabled) {
279 | rl.drawFPS(10, 10); // Draw FPS counter if debug mode is on
280 | }
281 |
282 | rl.endDrawing();
283 | }
284 |
285 | // Unload assets
286 | rl.unloadTexture(profile_picture);
287 | for (renderer.raylib_fonts) |font| {
288 | if (font) |f| rl.unloadFont(f);
289 | }
290 |
291 | rl.closeWindow(); // Close window and OpenGL context
292 | }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment