pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); raylib.SetConfigFlags(raylib.FLAG_VSYNC_HINT); // | raylib.FLAG_WINDOW_RESIZABLE); raylib.InitWindow(@intFromFloat(screen_width), @intFromFloat(screen_height), "ReX"); defer raylib.CloseWindow(); raylib.SetWindowMinSize(480, 272); raylib.SetWindowMaxSize(1920, 1080); scales.recalculate(); global_font = raylib.LoadFontEx("font/SCE-PS3-RD-R-LATIN.TTF", 32, 0, 250); raylib.SetTextureFilter(global_font.texture, raylib.TEXTURE_FILTER_BILINEAR); var background = Background.init(); var column = Column.init( allocator, raylib.LoadTextureFromImage(raylib.GenImageChecked(64, 64, 8, 8, raylib.BLACK, raylib.WHITE)), "Game", ); defer column.deinit(); var item1 = Item.init( raylib.LoadTexture("menu/game/CometCrash/ICON0.PNG"), "Comet Crash", "3/1/2025 23:11", ); var item2 = Item.init( raylib.LoadTexture("menu/game/LBP1/ICON0.PNG"), "LittleBigPlanet", "3/1/2025 23:15", ); var item3 = Item.init( raylib.LoadTexture("menu/game/LBP2/ICON0.PNG"), "LittleBigPlanet 2", "3/1/2025 23:26", ); var item4 = Item.init( raylib.LoadTexture("menu/game/LBP3/ICON0.PNG"), "LittleBigPlanet 3", "3/1/2025 23:48", ); try column.appendItem(&item1); try column.appendItem(&item2); try column.appendItem(&item3); try column.appendItem(&item4); raylib.SetTargetFPS(120); while (!raylib.WindowShouldClose()) { if (raylib.IsWindowResized()) { screen_width = @floatFromInt(raylib.GetScreenWidth()); screen_height = @floatFromInt(raylib.GetScreenHeight()); scales.recalculate(); } if (raylib.IsKeyPressed('D')) debug_draw = !debug_draw; if (raylib.IsKeyPressed('Z')) item1.setLarge(!item1.large); if (raylib.IsKeyPressed('S')) raylib.TakeScreenshot("screenshot.png"); column.updatePositions(); raylib.BeginDrawing(); defer raylib.EndDrawing(); background.draw(); column.draw(); raylib.DrawFPS(1, 1); const debug_text = try std.fmt.allocPrint(allocator, \\screen size = {d}x{d} \\selected = {d} , .{ screen_width, screen_height, column.selected, }); defer allocator.free(debug_text); raylib.DrawText(@ptrCast(debug_text), 80, 2, 8, raylib.GREEN); } } var debug_draw = true; var global_font: raylib.Font = undefined; var screen_width: f32 = 480; var screen_height: f32 = 272; var scales: Scales = undefined; /// Cached scaling and positioning values for dynamic window resizing. pub const Scales = struct { item_icon_small_width: f32, item_icon_small_height: f32, item_icon_large_width: f32, item_icon_large_height: f32, item_title_font_size: f32, item_subtitle_font_size: f32, column_icon_scale: f32, column_title_font_size: f32, column_position_center: raylib.Vector2, column_position_spacing: f32, column_item_spacing: f32, column_item_start: f32, /// Recalculate scales after screen resize. pub fn recalculate(self: *Scales) void { self.item_icon_small_width = 67; self.item_icon_small_height = 48; self.item_icon_large_width = 67; self.item_icon_large_height = 48; self.item_title_font_size = 18; self.item_subtitle_font_size = 12; self.column_icon_scale = 0.75; self.column_title_font_size = 13; self.column_position_center = .{ .x = std.math.lerp(0.0, screen_width, 0.18), .y = std.math.lerp(0.0, screen_height, 0.15), }; self.column_position_spacing = 64; self.column_item_spacing = 16; self.column_item_start = 117.8; } }; pub const Column = struct { icon: raylib.Texture2D, title: []const u8, items: std.ArrayList(*Item), selected: usize = 0, start_y: f32 = undefined, pub fn init(allocator: Allocator, icon: raylib.Texture2D, title: []const u8) Column { raylib.SetTextureFilter(icon, raylib.TEXTURE_FILTER_BILINEAR); return .{ .icon = icon, .title = title, .items = .init(allocator), }; } pub fn deinit(self: *Column) void { self.items.deinit(); } pub fn draw(self: *Column) void { var icon = Image{ .texture = self.icon, .box = .{ .x = scales.column_position_center.x, .y = scales.column_position_center.y, .w = 67, .h = 48, }, }; var title = Text{ .string = self.title, .font_size = scales.column_title_font_size, .font_spacing = 1, .box = .{ .x = icon.box.x - 8, .y = icon.box.y + icon.box.h + 6, .w = icon.box.w + 16, .h = scales.column_title_font_size, }, .align_h = .center, }; // self.start_y = scales.column_position_center.y + icon.box.h + title.box.h + scales.column_item_spacing; for (self.items.items) |item| item.draw(); icon.draw(); title.draw(); } pub fn appendItem(self: *Column, item: *Item) !void { try self.items.append(item); self.refresh(); } pub fn insertItem(self: *Column, idx: usize, item: *Item) !void { try self.items.insert(idx, item); self.refresh(); } pub fn removeItem(self: *Column, idx: usize) void { _ = try self.items.orderedRemove(idx); self.refresh(); } fn updatePositions(self: *Column) void { const up = raylib.IsKeyPressed(raylib.KEY_UP) or raylib.IsKeyPressedRepeat(raylib.KEY_UP); const down = raylib.IsKeyPressed(raylib.KEY_DOWN) or raylib.IsKeyPressedRepeat(raylib.KEY_DOWN); if (up and self.selected > 0) self.selected -= 1; if (down and self.selected < self.items.items.len - 1) self.selected += 1; if (up or down) self.refresh(); } fn refresh(self: *Column) void { var y = scales.column_item_start; for (self.items.items[self.selected..]) |item| { item.setPosition(.{ .x = scales.column_position_center.x, .y = y }); y += scales.item_icon_small_height + scales.column_item_spacing; } } }; pub const Item = struct { position: raylib.Vector2 = .{ .x = 0, .y = 0 }, // icon_scale: f32, // start_scale: f32, time: f32 = 0.0, start_position: raylib.Vector2 = .{ .x = 0, .y = 0 }, icon: raylib.Texture2D, title: []const u8, subtitle: []const u8, large: bool = false, pub fn init(texture: raylib.Texture2D, title: []const u8, subtitle: []const u8) Item { raylib.SetTextureFilter(texture, raylib.TEXTURE_FILTER_BILINEAR); return .{ .icon = texture, .title = title, .subtitle = subtitle, // .icon_scale = scales.item_icon_small_scale, // .start_scale = scales.item_icon_small_scale, }; } pub fn draw(self: *Item) void { self.time += raylib.GetFrameTime(); // self.icon_scale = std.math.lerp( // self.start_scale, // if (self.large) scales.item_icon_large_scale else scales.item_icon_small_scale, // easeOutExpo(self.time / 0.333), // ); const position = raylib.Vector2Lerp( self.start_position, self.position, easeOutExpo(self.time / 0.333), ); var icon = Image{ .texture = self.icon, .box = .{ .x = position.x - scales.item_icon_small_width / 2.0 + 67.0 / 2.0, .y = position.y, .w = scales.item_icon_small_width, .h = scales.item_icon_small_height, }, }; var title = Text{ .string = self.title, .box = .{ .x = icon.box.x + 8 + icon.box.w, .y = icon.box.y, .w = 300, .h = icon.box.h / 2.0 - 2, }, .font_size = scales.item_title_font_size, .font_spacing = 1, .align_v = .right, }; var subtitle = Text{ .string = self.subtitle, .box = .{ .x = icon.box.x + 8 + icon.box.w, .y = icon.box.y + icon.box.h / 2.0 + 1, .w = 200, .h = icon.box.h / 2.0 - 2, }, .font_size = scales.item_subtitle_font_size, .font_spacing = 1, .align_v = .left, }; icon.draw(); title.draw(); subtitle.draw(); } pub fn setLarge(self: *Item, large: bool) void { self.large = large; // self.time = 0; // self.start_scale = self.icon_scale; } pub fn setPosition(self: *Item, position: raylib.Vector2) void { self.start_position = self.position; self.position = position; self.time = 0; } fn easeOutExpo(x: f32) f32 { return 1.0 - std.math.pow(f32, 2, -10 * std.math.clamp(x, 0.0, 1.0)); } }; /// Draws the dynamic gradient background. // TODO shift based on time of day // TODO image wallpaper // TODO slideshow wallpaper // TODO animated image wallpaper pub const Background = struct { top_left: Color, top_right: Color, bottom_right: Color, bottom_left: Color, pub fn init() Background { var self: Background = undefined; self.setColors(NIGHT_08); return self; } pub fn setColors(self: *Background, colors: [4]Color) void { self.top_left = colors[0]; self.bottom_right = colors[1]; self.top_right = colors[2]; self.bottom_left = colors[3]; } pub fn draw(self: *Background) void { raylib.DrawRectangleGradientEx( .{ .x = 0, .y = 0, .width = screen_width, .height = screen_height, }, self.top_left.toRaylib(), self.bottom_left.toRaylib(), self.top_right.toRaylib(), self.bottom_right.toRaylib(), ); } pub const Color = struct { r: f32, g: f32, b: f32, fn toRaylib(self: Color) raylib.Color { return .{ .r = @intFromFloat(self.r * 255.0), .g = @intFromFloat(self.g * 255.0), .b = @intFromFloat(self.b * 255.0), .a = 255, }; } }; pub const DAY_06 = [4]Color{ .{ .r = 0.408, .g = 0.333, .b = 0.643 }, .{ .r = 0.518, .g = 0.365, .b = 0.855 }, .{ .r = 0.761, .g = 0.510, .b = 0.851 }, .{ .r = 0.569, .g = 0.325, .b = 0.620 }, }; pub const DAY_08 = [4]Color{ .{ .r = 0.243, .g = 0.608, .b = 0.831 }, .{ .r = 0.039, .g = 0.690, .b = 0.878 }, .{ .r = 0.016, .g = 0.306, .b = 0.694 }, .{ .r = 0.000, .g = 0.027, .b = 0.310 }, }; pub const NIGHT_08 = [4]Color{ .{ .r = 0.000, .g = 0.145, .b = 0.349 }, .{ .r = 0.000, .g = 0.008, .b = 0.106 }, .{ .r = 0.251, .g = 0.494, .b = 0.576 }, .{ .r = 0.008, .g = 0.537, .b = 0.612 }, }; }; pub const BoundingBox = struct { x: f32 = 0, y: f32 = 0, w: f32, h: f32, }; /// A texture within a bounding box. Positions and scales based on configurable parameters. /// Will not crop texture if using `Mode.original`. pub const Image = struct { box: BoundingBox, texture: raylib.Texture2D, mode: Mode = .fit, align_w: Align = .center, align_h: Align = .center, pub fn draw(self: *Image) void { const width: f32 = @floatFromInt(self.texture.width); const height: f32 = @floatFromInt(self.texture.height); const width_scale: f32 = if (width == 0) 0.0 else self.box.w / width; const height_scale: f32 = if (height == 0) 0.0 else self.box.h / height; const scale = switch (self.mode) { .original => 1.0, .fit => @min(width_scale, height_scale), .fill => @max(width_scale, height_scale), }; const position = raylib.Vector2{ .x = switch (self.align_w) { .left => 0, .center => self.box.x + self.box.w / 2.0 - (width * scale) / 2.0, .right => self.box.x + self.box.w - width * scale, }, .y = switch (self.align_h) { .left => 0, .center => self.box.y + self.box.h / 2.0 - (height * scale) / 2.0, .right => self.box.y + self.box.h - height * scale, }, }; if (debug_draw) raylib.DrawRectangleLines( @intFromFloat(self.box.x), @intFromFloat(self.box.y), @intFromFloat(self.box.w), @intFromFloat(self.box.h), raylib.MAROON, ) else raylib.DrawTextureEx(self.texture, position, 0, scale, raylib.WHITE); } pub const Mode = enum { original, fit, fill }; pub const Align = enum { left, center, right }; }; /// Draws a string inside a bounding box. // TODO ellipsize text // TODO clip text and scroll pub const Text = struct { box: BoundingBox, string: []const u8, font_size: f32, font_spacing: f32, align_h: Align = .left, align_v: Align = .left, pub fn draw(self: *Text) void { const size = raylib.MeasureTextEx( global_font, @ptrCast(self.string), self.font_size, self.font_spacing, ); if (debug_draw) raylib.DrawRectangleLines( @intFromFloat(self.box.x), @intFromFloat(self.box.y), @intFromFloat(self.box.w), @intFromFloat(self.box.h), raylib.GREEN, ) else raylib.DrawTextEx( global_font, @ptrCast(self.string), .{ .x = switch (self.align_h) { .left => self.box.x, .center => self.box.x + self.box.w / 2.0 - size.x / 2.0, .right => self.box.x + self.box.w - size.x, }, .y = switch (self.align_v) { .left => self.box.y, .center => self.box.y + self.box.h / 2.0 - size.y / 2.0, .right => self.box.y + self.box.h - size.y, }, }, self.font_size, self.font_spacing, raylib.WHITE, ); } pub const Align = enum { left, center, right }; }; /// Create ortho camera looking down at the XY plane, with a fovy of 1 for UV-like positioning. fn createCamera() raylib.Camera3D { var camera = raylib.Camera3D{}; camera.position.x = 0; camera.position.y = 10; camera.position.z = 0; // camera pointing down, oriented correctly // TODO this works but looks weird. do better. camera.target.x = 0; camera.target.y = 0; camera.target.z = -0.000000000000001; camera.up.x = 0; camera.up.y = 1; camera.up.z = 0; camera.fovy = 1; camera.projection = raylib.CAMERA_ORTHOGRAPHIC; return camera; } const std = @import("std"); const Allocator = std.mem.Allocator; const c = @import("c.zig"); const raylib = c.raylib;