Created
April 26, 2025 21:12
-
-
Save raspberrypisig/51ea2d28c4133520c63ccbdf411f437b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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