diff --git a/flake.lock b/flake.lock index 5a9df819..5f609479 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768564909, - "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", + "lastModified": 1783522502, + "narHash": "sha256-iffAls3iaNTyJC2faYcUXSI+Gp02cDjYl+MygxKl2GI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", + "rev": "0bb7ec54c8483066ec9d7720e780a5caa71f8612", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5ddad2ce..0e88ae7f 100644 --- a/flake.nix +++ b/flake.nix @@ -299,7 +299,6 @@ pkgs.sdl3 pkgs.vulkan-loader pkgs.vulkan-headers - pkgs.vulkan-validation-layers cimgui ]; @@ -339,8 +338,7 @@ pkgs.sdl3 pkgs.vulkan-loader pkgs.vulkan-headers - pkgs.vulkan-validation-layers - pkgs.mesa.drivers + pkgs.mesa cimgui ]; @@ -385,8 +383,7 @@ pkgs.sdl3 pkgs.vulkan-loader pkgs.vulkan-headers - pkgs.vulkan-validation-layers - pkgs.mesa.drivers + pkgs.mesa cimgui ]; diff --git a/modules/engine-core/src/window.zig b/modules/engine-core/src/window.zig index 60724534..a4f374fb 100644 --- a/modules/engine-core/src/window.zig +++ b/modules/engine-core/src/window.zig @@ -65,6 +65,16 @@ pub const WindowManager = struct { }; } + pub fn show(self: *WindowManager) void { + if (c.SDL_ShowWindow(self.window) == false) { + log.log.warn("SDL_ShowWindow failed: {s}", .{c.SDL_GetError()}); + return; + } + if (c.SDL_SyncWindow(self.window) == false) { + log.log.warn("SDL_SyncWindow failed: {s}", .{c.SDL_GetError()}); + } + } + const HyprlandMonitor = struct { width: c_int, height: c_int, diff --git a/modules/engine-graphics/src/render_graph.zig b/modules/engine-graphics/src/render_graph.zig index f2801554..3d202250 100644 --- a/modules/engine-graphics/src/render_graph.zig +++ b/modules/engine-graphics/src/render_graph.zig @@ -257,6 +257,7 @@ pub const ShadowPass = struct { const self: *ShadowPass = @ptrCast(@alignCast(ptr)); // Runtime verification to ensuring pointer safety in debug mode std.debug.assert(self.cascade_index < rhi_pkg.SHADOW_CASCADE_COUNT); + if (!self.enabled or !ctx.shadow_draw_enabled) return; const cascade_idx = self.cascade_index; const shadow_resolution = ctx.shadow_ctx.getResolution(); @@ -299,8 +300,6 @@ pub const ShadowPass = struct { }); } - if (!self.enabled or !ctx.shadow_draw_enabled) return; - // Keep cutout casters sampling the terrain atlas during the shadow pass. // Without this, shadow.frag can alpha-clip against whatever texture was last bound. self.material_system.bindTerrainMaterial(ctx.render_ctx, ctx.env_map_handle); diff --git a/modules/game-ui/src/menu_theme.zig b/modules/game-ui/src/menu_theme.zig index db890a4d..608b37b3 100644 --- a/modules/game-ui/src/menu_theme.zig +++ b/modules/game-ui/src/menu_theme.zig @@ -1,5 +1,7 @@ -//! Shared Nordic glass menu rendering primitives. +//! Shared menu design system. Screens compose these restrained, high-contrast +//! primitives instead of inventing local decoration and interaction states. +const std = @import("std"); const UISystem = @import("engine-ui").UISystem; pub const Color = @import("engine-ui").Color; pub const Rect = @import("engine-ui").Rect; @@ -31,63 +33,65 @@ pub const Shell = struct { scale: f32, }; -pub const title = Color.rgba(0.88, 0.93, 0.96, 1.0); -pub const text = Color.rgba(0.73, 0.81, 0.87, 0.96); -pub const muted = Color.rgba(0.50, 0.61, 0.69, 0.94); -pub const dim = Color.rgba(0.32, 0.41, 0.48, 0.88); -pub const copper = Color.rgba(0.59, 0.70, 0.78, 1.0); -pub const amber = Color.rgba(0.70, 0.76, 0.62, 1.0); -pub const signal = Color.rgba(0.58, 0.72, 0.82, 1.0); -pub const danger = Color.rgba(0.78, 0.30, 0.34, 1.0); -pub const panel = Color.rgba(0.060, 0.095, 0.125, 0.66); -pub const panel_soft = Color.rgba(0.095, 0.135, 0.165, 0.46); -pub const panel_header = Color.rgba(0.075, 0.115, 0.148, 0.50); -pub const outline = Color.rgba(0.54, 0.66, 0.74, 0.58); - -const backdrop_base = Color.rgba(0.018, 0.026, 0.036, 1.0); +pub const title = Color.rgba(0.96, 0.98, 1.0, 1.0); +pub const text = Color.rgba(0.80, 0.85, 0.89, 1.0); +pub const muted = Color.rgba(0.60, 0.67, 0.73, 1.0); +pub const dim = Color.rgba(0.42, 0.48, 0.53, 1.0); +pub const copper = Color.rgba(0.45, 0.70, 0.78, 1.0); +pub const amber = Color.rgba(0.94, 0.73, 0.33, 1.0); +pub const signal = Color.rgba(0.25, 0.75, 0.92, 1.0); +pub const danger = Color.rgba(0.95, 0.36, 0.40, 1.0); +pub const panel = Color.rgba(0.025, 0.040, 0.055, 0.97); +pub const panel_soft = Color.rgba(0.070, 0.100, 0.126, 0.98); +pub const panel_header = Color.rgba(0.055, 0.082, 0.105, 1.0); +pub const outline = Color.rgba(0.32, 0.42, 0.50, 1.0); + +const backdrop_base = Color.rgba(0.012, 0.020, 0.030, 1.0); const glass_top = Color.rgba(0.94, 0.98, 1.0, 0.16); const glass_edge = Color.rgba(0.78, 0.88, 0.94, 0.42); const shadow = Color.rgba(0, 0, 0, 0.32); pub fn scaleFor(screen_h: f32, user_scale: f32) f32 { - return @max(1.0, screen_h / 720.0) * user_scale; + // Small windows must reduce the whole layout; large displays stop growing + // before controls become oversized. The user preference remains effective. + return std.math.clamp(screen_h / 1080.0, 0.72, 2.0) * user_scale; } pub fn drawBackdrop(ui: *UISystem, screen_w: f32, screen_h: f32, scale: f32, tone: Tone) void { - _ = scale; - _ = tone; ui.drawRect(.{ .x = 0, .y = 0, .width = screen_w, .height = screen_h }, backdrop_base); + const accent = toneAccent(tone); + ui.drawRect(.{ .x = 0, .y = 0, .width = screen_w, .height = 3.0 * scale }, accent); + ui.drawRect(.{ .x = 0, .y = 0, .width = 5.0 * scale, .height = screen_h }, Color.rgba(accent.r, accent.g, accent.b, 0.12)); +} + +pub fn drawWorldScrim(ui: *UISystem, screen_w: f32, screen_h: f32, scale: f32) void { + ui.drawRect(.{ .x = 0, .y = 0, .width = screen_w, .height = screen_h }, Color.rgba(0.006, 0.012, 0.018, 0.36)); + ui.drawRect(.{ .x = 0, .y = 0, .width = screen_w, .height = 3.0 * scale }, signal); } pub fn drawShell(ui: *UISystem, rect: Rect, scale: f32, kicker: []const u8, heading: []const u8, subtitle: []const u8) Shell { - const header_h = @min(rect.height * 0.18, 108.0 * scale); - const footer_h = 72.0 * scale; + const padding = @min(32.0 * scale, rect.width * 0.07); + const header_h = @min(rect.height * 0.20, 112.0 * scale); + const footer_h = 68.0 * scale; const content = Rect{ - .x = rect.x + 36.0 * scale, - .y = rect.y + header_h + 28.0 * scale, - .width = rect.width - 72.0 * scale, - .height = rect.height - header_h - footer_h - 38.0 * scale, + .x = rect.x + padding, + .y = rect.y + header_h + 20.0 * scale, + .width = rect.width - padding * 2.0, + .height = rect.height - header_h - footer_h - 26.0 * scale, }; - drawSoftHalo(ui, rect, scale, 0.16); - ui.drawRect(.{ .x = rect.x + 18.0 * scale, .y = rect.y + 24.0 * scale, .width = rect.width, .height = rect.height }, shadow); + ui.drawRect(.{ .x = rect.x + 10.0 * scale, .y = rect.y + 12.0 * scale, .width = rect.width, .height = rect.height }, shadow); ui.drawRect(rect, panel); ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = rect.width, .height = header_h }, panel_header); ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = rect.width, .height = 1.0 * scale }, glass_top); ui.drawRect(.{ .x = rect.x, .y = rect.y + header_h - 2.0 * scale, .width = rect.width, .height = 2.0 * scale }, Color.rgba(signal.r, signal.g, signal.b, 0.52)); - ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = 2.0 * scale, .height = rect.height }, Color.rgba(1.0, 1.0, 1.0, 0.18)); - ui.drawRect(.{ .x = rect.x + rect.width - 2.0 * scale, .y = rect.y, .width = 2.0 * scale, .height = rect.height }, Color.rgba(signal.r, signal.g, signal.b, 0.22)); - drawFrost(ui, rect, scale, 0.045); ui.drawRectOutline(rect, outline, 1.0 * scale); - drawGlassCorners(ui, rect, scale, Color.rgba(signal.r, signal.g, signal.b, 0.54)); - Font.drawText(ui, kicker, rect.x + 36.0 * scale, rect.y + 20.0 * scale, 0.78 * scale, muted); - Font.drawText(ui, heading, rect.x + 30.0 * scale, rect.y + 40.0 * scale, 3.08 * scale, title); - Font.drawText(ui, subtitle, rect.x + 38.0 * scale, rect.y + header_h - 22.0 * scale, 0.94 * scale, muted); + Font.drawText(ui, kicker, rect.x + padding, rect.y + 17.0 * scale, 0.82 * scale, signal); + Font.drawText(ui, heading, rect.x + padding, rect.y + 39.0 * scale, 2.80 * scale, title); + Font.drawText(ui, subtitle, rect.x + padding, rect.y + header_h - 20.0 * scale, 0.98 * scale, text); - ui.drawRect(.{ .x = content.x, .y = content.y - 15.0 * scale, .width = content.width * 0.62, .height = 1.0 * scale }, Color.rgba(0.80, 0.88, 0.92, 0.30)); - ui.drawRect(.{ .x = content.x, .y = content.y - 9.0 * scale, .width = content.width * 0.20, .height = 2.0 * scale }, Color.rgba(signal.r, signal.g, signal.b, 0.44)); - ui.drawRect(.{ .x = rect.x + 24.0 * scale, .y = rect.y + rect.height - footer_h, .width = rect.width - 48.0 * scale, .height = 1.0 * scale }, Color.rgba(0.68, 0.76, 0.82, 0.22)); + ui.drawRect(.{ .x = rect.x + padding, .y = rect.y + rect.height - footer_h, .width = rect.width - padding * 2.0, .height = 1.0 * scale }, outline); return .{ .rect = rect, @@ -98,54 +102,86 @@ pub fn drawShell(ui: *UISystem, rect: Rect, scale: f32, kicker: []const u8, head } pub fn drawHeroTitle(ui: *UISystem, x: f32, y: f32, scale: f32, compact: bool) void { - const word_scale: f32 = if (compact) 3.15 * scale else 4.15 * scale; - const line_w: f32 = if (compact) 420.0 * scale else 620.0 * scale; - const word_y = y + 4.0 * scale; + const word_scale: f32 = if (compact) 3.6 * scale else 5.2 * scale; + const line_w: f32 = if (compact) 400.0 * scale else 620.0 * scale; + const word_y = y; const zig_w = Font.measureTextWidth("Zig", word_scale); - const craft_w = Font.measureTextWidth("Craft", word_scale); - const mark_w = zig_w + craft_w + 3.0 * scale; - const mark_h = word_scale * 7.0 + 22.0 * scale; - const copy_y = word_y + mark_h + 18.0 * scale; - - ui.drawRect(.{ .x = x, .y = y - 18.0 * scale, .width = line_w, .height = 1.0 * scale }, Color.rgba(0.72, 0.82, 0.88, 0.34)); - ui.drawRect(.{ .x = x, .y = y - 10.0 * scale, .width = line_w * 0.46, .height = 2.0 * scale }, Color.rgba(signal.r, signal.g, signal.b, 0.48)); - Font.drawText(ui, "VOXEL SURVIVAL ENGINE", x + 2.0 * scale, y - 48.0 * scale, 0.92 * scale, muted); - - ui.drawRect(.{ .x = x - 10.0 * scale, .y = word_y - 10.0 * scale, .width = mark_w + 28.0 * scale, .height = mark_h }, Color.rgba(0.060, 0.095, 0.125, 0.28)); - ui.drawRect(.{ .x = x - 10.0 * scale, .y = word_y - 10.0 * scale, .width = mark_w + 28.0 * scale, .height = 1.0 * scale }, Color.rgba(0.94, 0.98, 1.0, 0.16)); - ui.drawRect(.{ .x = x - 10.0 * scale, .y = word_y + mark_h - 12.0 * scale, .width = mark_w + 28.0 * scale, .height = 1.0 * scale }, Color.rgba(signal.r, signal.g, signal.b, 0.36)); - Font.drawText(ui, "Zig", x + 2.0 * scale, word_y + 2.0 * scale, word_scale, Color.rgba(0, 0, 0, 0.28)); - Font.drawText(ui, "Craft", x + zig_w + 5.0 * scale, word_y + 2.0 * scale, word_scale, Color.rgba(0, 0, 0, 0.24)); + const copy_y = word_y + word_scale * 7.0 + 18.0 * scale; + + Font.drawText(ui, "ZIGCRAFT", x, y - 38.0 * scale, 0.92 * scale, signal); Font.drawText(ui, "Zig", x, word_y, word_scale, title); Font.drawText(ui, "Craft", x + zig_w + 3.0 * scale, word_y, word_scale, Color.rgba(0.72, 0.82, 0.88, 1.0)); - - Font.drawText(ui, "BUILD WORLDS IN QUIET GLASS", x + 4.0 * scale, copy_y, 1.05 * scale, text); - Font.drawText(ui, "PROCEDURAL TERRAIN / LOCAL SAVES / VULKAN", x + 4.0 * scale, copy_y + 28.0 * scale, 0.74 * scale, muted); + Font.drawText(ui, "Explore procedural worlds, your way.", x, copy_y, 1.24 * scale, title); + ui.drawRect(.{ .x = x, .y = copy_y + 34.0 * scale, .width = line_w, .height = 1.0 * scale }, outline); } pub fn drawButton(ui: *UISystem, rect: Rect, label: []const u8, text_scale: f32, mx: f32, my: f32, clicked: bool, style: ButtonStyle, scale: f32) bool { + return drawButtonFocused(ui, rect, label, text_scale, mx, my, clicked, style, false, scale); +} + +pub fn drawButtonFocused(ui: *UISystem, rect: Rect, label: []const u8, text_scale: f32, mx: f32, my: f32, clicked: bool, style: ButtonStyle, focused: bool, scale: f32) bool { const disabled = style == .disabled; const hovered = !disabled and rect.contains(mx, my); - const colors = buttonColors(style, hovered); + const highlighted = hovered or focused; + const colors = buttonColors(style, highlighted); - ui.drawRect(.{ .x = rect.x + 8.0 * scale, .y = rect.y + 10.0 * scale, .width = rect.width, .height = rect.height }, colors.shadow_color); + ui.drawRect(.{ .x = rect.x + 5.0 * scale, .y = rect.y + 6.0 * scale, .width = rect.width, .height = rect.height }, colors.shadow_color); ui.drawRect(rect, colors.bg); - ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = rect.width, .height = 1.0 * scale }, colors.top); - ui.drawRect(.{ .x = rect.x, .y = rect.y + rect.height - 1.0 * scale, .width = rect.width, .height = 1.0 * scale }, colors.bottom); ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = 3.0 * scale, .height = rect.height }, colors.accent); - ui.drawRect(.{ .x = rect.x + 10.0 * scale, .y = rect.y + 9.0 * scale, .width = rect.width - 20.0 * scale, .height = 1.0 * scale }, Color.rgba(1.0, 1.0, 1.0, if (hovered) 0.20 else 0.11)); - ui.drawRectOutline(rect, colors.border, if (hovered) 2.0 * scale else 1.0 * scale); + ui.drawRectOutline(rect, colors.border, if (highlighted) 2.0 * scale else 1.0 * scale); + if (focused) ui.drawRectOutline(.{ .x = rect.x - 3.0 * scale, .y = rect.y - 3.0 * scale, .width = rect.width + 6.0 * scale, .height = rect.height + 6.0 * scale }, Color.rgba(signal.r, signal.g, signal.b, 0.72), 1.0 * scale); - const max_text_width = @max(1.0, rect.width - 58.0 * scale); + const max_text_width = @max(1.0, rect.width - 42.0 * scale); Font.drawTextCenteredFit(ui, label, rect.x + rect.width * 0.5 + 2.0 * scale, rect.y + (rect.height - 7.0 * text_scale) * 0.5, text_scale, max_text_width, colors.text); return hovered and clicked; } +pub fn drawActionCard(ui: *UISystem, rect: Rect, heading: []const u8, description: []const u8, meta: []const u8, mx: f32, my: f32, clicked: bool, primary: bool, focused: bool, scale: f32) bool { + const hovered = rect.contains(mx, my); + const active = hovered or focused; + const edge = if (primary) signal else copper; + const bg = if (primary) + if (active) Color.rgba(0.055, 0.225, 0.290, 0.99) else Color.rgba(0.045, 0.160, 0.210, 0.99) + else if (active) + Color.rgba(0.095, 0.145, 0.180, 0.99) + else + panel_soft; + + ui.drawRect(.{ .x = rect.x + 7.0 * scale, .y = rect.y + 8.0 * scale, .width = rect.width, .height = rect.height }, Color.rgba(0, 0, 0, 0.46)); + ui.drawRect(rect, bg); + ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = if (primary) 7.0 * scale else 4.0 * scale, .height = rect.height }, edge); + ui.drawRectOutline(rect, if (active) edge else outline, if (active) 2.0 * scale else 1.0 * scale); + if (focused) ui.drawRectOutline(.{ .x = rect.x - 3.0 * scale, .y = rect.y - 3.0 * scale, .width = rect.width + 6.0 * scale, .height = rect.height + 6.0 * scale }, Color.rgba(signal.r, signal.g, signal.b, 0.72), 1.0 * scale); + + const heading_scale: f32 = if (primary) 1.92 * scale else 1.28 * scale; + const description_offset: f32 = if (primary) 62.0 else 49.0; + Font.drawText(ui, heading, rect.x + 26.0 * scale, rect.y + 20.0 * scale, heading_scale, title); + Font.drawText(ui, description, rect.x + 26.0 * scale, rect.y + description_offset * scale, 0.90 * scale, if (primary) text else muted); + if (meta.len > 0) { + const meta_w = Font.measureTextWidth(meta, 0.68 * scale); + Font.drawText(ui, meta, rect.x + rect.width - meta_w - 20.0 * scale, rect.y + 22.0 * scale, 0.76 * scale, if (primary) title else if (active) signal else dim); + } + return hovered and clicked; +} + +pub fn drawNavItem(ui: *UISystem, rect: Rect, label: []const u8, index: usize, selected: bool, mx: f32, my: f32, clicked: bool, scale: f32) bool { + const hovered = rect.contains(mx, my); + const active = selected or hovered; + ui.drawRect(rect, if (active) Color.rgba(0.075, 0.125, 0.155, 1.0) else Color.rgba(0.040, 0.060, 0.078, 0.92)); + if (selected) ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = 5.0 * scale, .height = rect.height }, signal); + ui.drawRectOutline(rect, if (active) Color.rgba(signal.r, signal.g, signal.b, 0.54) else outline, 1.0 * scale); + Font.drawText(ui, label, rect.x + 18.0 * scale, rect.y + (rect.height - 7.0 * 0.92 * scale) * 0.5, 0.92 * scale, if (selected) title else text); + var index_buf: [8]u8 = undefined; + const index_text = std.fmt.bufPrint(&index_buf, "0{}", .{index + 1}) catch ""; + const index_w = Font.measureTextWidth(index_text, 0.66 * scale); + Font.drawText(ui, index_text, rect.x + rect.width - index_w - 16.0 * scale, rect.y + (rect.height - 7.0 * 0.66 * scale) * 0.5, 0.66 * scale, if (selected) signal else dim); + return hovered and clicked; +} + pub fn drawTextInput(ui: *UISystem, rect: Rect, value: []const u8, placeholder: []const u8, text_scale: f32, focused: bool, caret: bool, scale: f32) void { const border = if (focused) signal else Color.rgba(0.42, 0.54, 0.62, 0.50); - ui.drawRect(.{ .x = rect.x + 5.0 * scale, .y = rect.y + 7.0 * scale, .width = rect.width, .height = rect.height }, shadow); - ui.drawRect(rect, Color.rgba(0.075, 0.110, 0.140, 0.66)); - ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = rect.width, .height = rect.height * 0.30 }, Color.rgba(0.92, 0.97, 1.0, if (focused) 0.12 else 0.08)); + ui.drawRect(.{ .x = rect.x + 3.0 * scale, .y = rect.y + 4.0 * scale, .width = rect.width, .height = rect.height }, shadow); + ui.drawRect(rect, if (focused) Color.rgba(0.070, 0.135, 0.175, 1.0) else Color.rgba(0.055, 0.080, 0.105, 1.0)); ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = 3.0 * scale, .height = rect.height }, border); ui.drawRectOutline(rect, Color.rgba(border.r, border.g, border.b, if (focused) 0.78 else 0.46), if (focused) 2.0 * scale else 1.0 * scale); @@ -162,34 +198,28 @@ pub fn drawTextInput(ui: *UISystem, rect: Rect, value: []const u8, placeholder: } pub fn drawSectionLabel(ui: *UISystem, x: f32, y: f32, label: []const u8, scale: f32) void { - ui.drawRect(.{ .x = x, .y = y + 8.0 * scale, .width = 40.0 * scale, .height = 1.0 * scale }, Color.rgba(signal.r, signal.g, signal.b, 0.50)); - Font.drawText(ui, label, x + 58.0 * scale, y, 0.88 * scale, muted); + ui.drawRect(.{ .x = x, .y = y + 7.0 * scale, .width = 24.0 * scale, .height = 2.0 * scale }, signal); + Font.drawText(ui, label, x + 36.0 * scale, y, 0.78 * scale, muted); } pub fn drawOptionRow(ui: *UISystem, rect: Rect, label: []const u8, description: []const u8, label_scale: f32, selected: bool, scale: f32) void { - const bg = if (selected) Color.rgba(0.095, 0.140, 0.170, 0.70) else Color.rgba(0.060, 0.095, 0.125, 0.52); + const bg = if (selected) Color.rgba(0.065, 0.185, 0.240, 0.98) else Color.rgba(0.055, 0.082, 0.105, 0.98); const edge = if (selected) signal else Color.rgba(0.40, 0.52, 0.60, 0.40); - ui.drawRect(.{ .x = rect.x + 5.0 * scale, .y = rect.y + 7.0 * scale, .width = rect.width, .height = rect.height }, shadow); ui.drawRect(rect, bg); - ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = rect.width, .height = 1.0 * scale }, Color.rgba(1.0, 1.0, 1.0, if (selected) 0.20 else 0.10)); ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = 3.0 * scale, .height = rect.height }, edge); ui.drawRectOutline(rect, Color.rgba(edge.r, edge.g, edge.b, if (selected) 0.62 else 0.34), 1.0 * scale); - Font.drawText(ui, label, rect.x + 18.0 * scale, rect.y + 10.0 * scale, label_scale, if (selected) title else text); - if (description.len > 0) Font.drawText(ui, description, rect.x + 18.0 * scale, rect.y + 34.0 * scale, label_scale * 0.54, muted); + Font.drawText(ui, label, rect.x + 20.0 * scale, rect.y + 10.0 * scale, label_scale, if (selected) title else text); + if (description.len > 0) Font.drawText(ui, description, rect.x + 20.0 * scale, rect.y + 35.0 * scale, @max(label_scale * 0.62, 0.72 * scale), muted); } pub fn drawValueText(ui: *UISystem, rect: Rect, value: []const u8, text_scale: f32, scale: f32) void { - ui.drawRect(.{ .x = rect.x + 4.0 * scale, .y = rect.y + 5.0 * scale, .width = rect.width, .height = rect.height }, shadow); - ui.drawRect(rect, Color.rgba(0.045, 0.070, 0.092, 0.74)); - ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = rect.width, .height = 1.0 * scale }, glass_top); + ui.drawRect(rect, Color.rgba(0.050, 0.085, 0.112, 1.0)); ui.drawRectOutline(rect, Color.rgba(signal.r, signal.g, signal.b, 0.44), 1.0 * scale); Font.drawTextCenteredFit(ui, value, rect.x + rect.width * 0.5, rect.y + (rect.height - 7.0 * text_scale) * 0.5, text_scale, rect.width - 12.0 * scale, title); } pub fn drawListRail(ui: *UISystem, rect: Rect, scale: f32) void { - ui.drawRect(.{ .x = rect.x + 8.0 * scale, .y = rect.y + 10.0 * scale, .width = rect.width, .height = rect.height }, shadow); - ui.drawRect(rect, Color.rgba(0.052, 0.086, 0.112, 0.54)); - drawFrost(ui, rect, scale, 0.035); + ui.drawRect(rect, Color.rgba(0.032, 0.052, 0.070, 0.98)); ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = 3.0 * scale, .height = rect.height }, Color.rgba(signal.r, signal.g, signal.b, 0.44)); ui.drawRect(.{ .x = rect.x + rect.width - 1.0 * scale, .y = rect.y, .width = 1.0 * scale, .height = rect.height }, Color.rgba(1.0, 1.0, 1.0, 0.14)); ui.drawRectOutline(rect, Color.rgba(0.48, 0.60, 0.68, 0.42), 1.0 * scale); @@ -241,18 +271,18 @@ fn buttonColors(style: ButtonStyle, hovered: bool) struct { bg: Color, top: Colo } if (style == .primary) { return .{ - .bg = if (hovered) Color.rgba(0.105, 0.155, 0.190, 0.76) else Color.rgba(0.075, 0.120, 0.155, 0.66), - .top = Color.rgba(0.94, 0.98, 1.0, if (hovered) 0.22 else 0.14), - .bottom = Color.rgba(signal.r, signal.g, signal.b, 0.46), - .border = if (hovered) Color.rgba(0.72, 0.84, 0.90, 0.72) else Color.rgba(signal.r, signal.g, signal.b, 0.54), + .bg = if (hovered) Color.rgba(0.10, 0.48, 0.61, 1.0) else Color.rgba(0.07, 0.36, 0.48, 1.0), + .top = Color.rgba(0.94, 0.98, 1.0, if (hovered) 0.28 else 0.18), + .bottom = signal, + .border = if (hovered) title else signal, .accent = signal, .text = title, - .shadow_color = Color.rgba(0, 0.02, 0.04, 0.34), + .shadow_color = Color.rgba(0, 0.02, 0.04, 0.52), }; } if (style == .ghost) { return .{ - .bg = if (hovered) Color.rgba(0.078, 0.105, 0.130, 0.54) else Color.rgba(0.050, 0.075, 0.095, 0.42), + .bg = if (hovered) Color.rgba(0.105, 0.150, 0.185, 1.0) else Color.rgba(0.055, 0.080, 0.105, 0.96), .top = Color.rgba(1.0, 1.0, 1.0, if (hovered) 0.16 else 0.08), .bottom = Color.rgba(0.50, 0.62, 0.70, if (hovered) 0.32 else 0.18), .border = if (hovered) Color.rgba(0.60, 0.72, 0.80, 0.62) else Color.rgba(0.40, 0.52, 0.60, 0.42), @@ -262,7 +292,7 @@ fn buttonColors(style: ButtonStyle, hovered: bool) struct { bg: Color, top: Colo }; } return .{ - .bg = if (hovered) Color.rgba(0.105, 0.135, 0.160, 0.70) else Color.rgba(0.075, 0.105, 0.130, 0.58), + .bg = if (hovered) Color.rgba(0.125, 0.190, 0.225, 1.0) else Color.rgba(0.075, 0.120, 0.150, 1.0), .top = Color.rgba(0.94, 0.98, 1.0, if (hovered) 0.19 else 0.12), .bottom = Color.rgba(copper.r, copper.g, copper.b, 0.36), .border = if (hovered) Color.rgba(0.68, 0.78, 0.84, 0.66) else Color.rgba(copper.r, copper.g, copper.b, 0.48), @@ -272,6 +302,16 @@ fn buttonColors(style: ButtonStyle, hovered: bool) struct { bg: Color, top: Colo }; } +fn toneAccent(tone: Tone) Color { + return switch (tone) { + .home, .create, .library => signal, + .settings, .graphics => Color.rgba(0.46, 0.64, 0.98, 1.0), + .packs => Color.rgba(0.38, 0.80, 0.60, 1.0), + .environment => Color.rgba(0.64, 0.52, 0.95, 1.0), + .paused => amber, + }; +} + fn drawSoftHalo(ui: *UISystem, rect: Rect, scale: f32, alpha: f32) void { ui.drawRect(.{ .x = rect.x - 18.0 * scale, .y = rect.y - 18.0 * scale, .width = rect.width + 36.0 * scale, .height = rect.height + 36.0 * scale }, Color.rgba(0.58, 0.72, 0.82, alpha * 0.10)); ui.drawRect(.{ .x = rect.x - 7.0 * scale, .y = rect.y - 7.0 * scale, .width = rect.width + 14.0 * scale, .height = rect.height + 14.0 * scale }, Color.rgba(0.80, 0.88, 0.92, alpha * 0.16)); diff --git a/modules/game-ui/src/menu_theme_tests.zig b/modules/game-ui/src/menu_theme_tests.zig index 1187aac5..d16a86f7 100644 --- a/modules/game-ui/src/menu_theme_tests.zig +++ b/modules/game-ui/src/menu_theme_tests.zig @@ -2,22 +2,26 @@ const testing = @import("std").testing; const Theme = @import("menu_theme.zig"); -test "scaleFor keeps 720p baseline at user scale" { - try testing.expectEqual(@as(f32, 1.0), Theme.scaleFor(720.0, 1.0)); +test "scaleFor reduces layouts on short windows" { + try testing.expectEqual(@as(f32, 0.72), Theme.scaleFor(720.0, 1.0)); } -test "scaleFor does not shrink below baseline before user scale" { - try testing.expectEqual(@as(f32, 1.0), Theme.scaleFor(480.0, 1.0)); +test "scaleFor has a readable lower bound" { + try testing.expectEqual(@as(f32, 0.72), Theme.scaleFor(480.0, 1.0)); } -test "scaleFor grows on taller displays" { - try testing.expectEqual(@as(f32, 2.0), Theme.scaleFor(1440.0, 1.0)); +test "scaleFor caps growth on tall displays" { + try testing.expectApproxEqAbs(@as(f32, 4.0 / 3.0), Theme.scaleFor(1440.0, 1.0), 0.0001); } test "scaleFor applies manual user scale" { - try testing.expectEqual(@as(f32, 1.5), Theme.scaleFor(720.0, 1.5)); + try testing.expectEqual(@as(f32, 1.08), Theme.scaleFor(720.0, 1.5)); } test "scaleFor combines display and user scale" { - try testing.expectEqual(@as(f32, 3.0), Theme.scaleFor(1440.0, 1.5)); + try testing.expectApproxEqAbs(@as(f32, 2.0), Theme.scaleFor(1440.0, 1.5), 0.0001); +} + +test "scaleFor reaches two times scale at 4K" { + try testing.expectEqual(@as(f32, 2.0), Theme.scaleFor(2160.0, 1.0)); } diff --git a/modules/game-ui/src/screen.zig b/modules/game-ui/src/screen.zig index e056356e..e00e0610 100644 --- a/modules/game-ui/src/screen.zig +++ b/modules/game-ui/src/screen.zig @@ -191,6 +191,7 @@ pub const IScreen = struct { onEnter: ?*const fn (ptr: *anyopaque) void = null, onExit: ?*const fn (ptr: *anyopaque) void = null, getWorldStats: ?*const fn (ptr: *anyopaque) ?WorldStats = null, + isReadyForPresentation: ?*const fn (ptr: *anyopaque) bool = null, }; pub fn deinit(self: IScreen) void { @@ -228,6 +229,13 @@ pub const IScreen = struct { return null; } + pub fn isReadyForPresentation(self: IScreen) bool { + if (self.vtable.isReadyForPresentation) |ready_fn| { + return ready_fn(self.ptr); + } + return true; + } + pub fn handle(self: IScreen) core_interfaces.ScreenHandle { return .{ .ptr = self.ptr, @@ -346,6 +354,11 @@ pub const ScreenManager = struct { } } + pub fn isReadyForPresentation(self: *const ScreenManager) bool { + if (self.stack.items.len == 0) return false; + return self.stack.items[self.stack.items.len - 1].isReadyForPresentation(); + } + pub fn drawParentScreen(self: *ScreenManager, current_ptr: *anyopaque, ui: *UISystem) !void { for (self.stack.items, 0..) |screen, i| { if (screen.ptr == current_ptr) { diff --git a/modules/game-ui/src/screens/environment.zig b/modules/game-ui/src/screens/environment.zig index 7227af06..3577aebe 100644 --- a/modules/game-ui/src/screens/environment.zig +++ b/modules/game-ui/src/screens/environment.zig @@ -18,6 +18,7 @@ const PANEL_HEIGHT_MAX = 760.0; pub const EnvironmentScreen = struct { context: EnvironmentContext, environment_maps: std.ArrayListUnmanaged([]const u8), + scroll_offset: f32, pub const vtable = IScreen.VTable{ .deinit = deinit, @@ -36,6 +37,7 @@ pub const EnvironmentScreen = struct { self.* = .{ .context = context.environmentContext(), .environment_maps = .empty, + .scroll_offset = 0.0, }; try self.refreshEnvironmentMaps(); return self; @@ -81,30 +83,41 @@ pub const EnvironmentScreen = struct { const panel_y = (screen_h - panel_h) * 0.5; const shell = Theme.drawShell(ui, .{ .x = panel_x, .y = panel_y, .width = panel_w, .height = panel_h }, ui_scale, "LIGHT PROBE", "ENVIRONMENT", "Choose the sky probe that lights the scene."); - const preview_w = @min(260.0 * ui_scale, shell.content.width * 0.34); + const compact = shell.content.width < 640.0 * ui_scale; + const preview_w = if (compact) 0.0 else @min(260.0 * ui_scale, shell.content.width * 0.34); const list_x = shell.content.x + preview_w + 24.0 * ui_scale; const list_w = shell.content.width - preview_w - 24.0 * ui_scale; - drawSkyPreview(ui, shell.content.x, shell.content.y, preview_w, shell.content.height, settings.environment_map, ui_scale); + if (!compact) drawSkyPreview(ui, shell.content.x, shell.content.y, preview_w, shell.content.height, settings.environment_map, ui_scale); Theme.drawListRail(ui, .{ .x = list_x, .y = shell.content.y, .width = list_w, .height = shell.content.height }, ui_scale); - var y = shell.content.y + 18.0 * ui_scale; const row_x = list_x + 18.0 * ui_scale; const row_w = list_w - 36.0 * ui_scale; const row_h = 56.0 * ui_scale; const btn_scale = 1.12 * ui_scale; + const content_h = @as(f32, @floatFromInt(self.environment_maps.items.len + 1)) * (row_h + 10.0 * ui_scale) + 26.0 * ui_scale; + const max_scroll = @max(0.0, content_h - shell.content.height); + self.scroll_offset -= ctx.input.getScrollDelta().y * 32.0 * ui_scale; + self.scroll_offset = @max(0.0, @min(self.scroll_offset, max_scroll)); + var y = shell.content.y + 18.0 * ui_scale - self.scroll_offset; + Theme.drawScrollbar(ui, list_x + list_w - 10.0 * ui_scale, shell.content.y + 12.0 * ui_scale, shell.content.height - 24.0 * ui_scale, content_h, shell.content.height, self.scroll_offset, max_scroll, ui_scale); const is_default = std.mem.eql(u8, settings.environment_map, "default"); - if (drawEnvironmentButton(ui, row_x, y, row_w, row_h, "DEFAULT SKY", "Neutral white environment texture.", is_default, btn_scale, mouse_x, mouse_y, mouse_clicked, ui_scale)) { - if (!is_default) { - try settings_pkg.persistence.setEnvironmentMap(settings, ctx.allocator, "default"); - try self.reloadEnvMap(); + if (y + row_h >= shell.content.y and y <= shell.content.y + shell.content.height) { + if (drawEnvironmentButton(ui, row_x, y, row_w, row_h, "DEFAULT SKY", "Neutral white environment texture.", is_default, btn_scale, mouse_x, mouse_y, mouse_clicked, ui_scale)) { + if (!is_default) { + try settings_pkg.persistence.setEnvironmentMap(settings, ctx.allocator, "default"); + try self.reloadEnvMap(); + } } } y += row_h + 10.0 * ui_scale; var buffer: [160]u8 = undefined; for (self.environment_maps.items) |environment_map| { - if (y + row_h > shell.content.y + shell.content.height) break; + if (y + row_h < shell.content.y or y > shell.content.y + shell.content.height) { + y += row_h + 10.0 * ui_scale; + continue; + } const is_selected = std.mem.eql(u8, settings.environment_map, environment_map); const label = std.fmt.bufPrint(&buffer, "{s}", .{environment_map}) catch "ENVIRONMENT"; if (drawEnvironmentButton(ui, row_x, y, row_w, row_h, label, "HDR/EXR sky probe from the working directory.", is_selected, btn_scale, mouse_x, mouse_y, mouse_clicked, ui_scale)) { @@ -198,8 +211,10 @@ fn drawSkyPreview(ui: *UISystem, x: f32, y: f32, w: f32, h: f32, active_name: [] } fn drawEnvironmentButton(ui: *UISystem, x: f32, y: f32, w: f32, h: f32, label: []const u8, description: []const u8, selected: bool, btn_scale: f32, mx: f32, my: f32, clicked: bool, scale: f32) bool { - Theme.drawOptionRow(ui, .{ .x = x, .y = y, .width = w, .height = h }, label, description, 1.02 * scale, selected, scale); + const row = Theme.Rect{ .x = x, .y = y, .width = w, .height = h }; + Theme.drawOptionRow(ui, row, label, description, 1.02 * scale, selected, scale); const action_w = 150.0 * scale; const action_x = x + w - action_w - 12.0 * scale; - return Theme.drawButton(ui, .{ .x = action_x, .y = y + 9.0 * scale, .width = action_w, .height = h - 18.0 * scale }, if (selected) "ACTIVE" else "SELECT", btn_scale, mx, my, clicked, if (selected) .primary else .secondary, scale); + const action_clicked = Theme.drawButton(ui, .{ .x = action_x, .y = y + 9.0 * scale, .width = action_w, .height = h - 18.0 * scale }, if (selected) "ACTIVE" else "USE SKY", btn_scale, mx, my, clicked, if (selected) .primary else .secondary, scale); + return action_clicked or (clicked and row.contains(mx, my)); } diff --git a/modules/game-ui/src/screens/home.zig b/modules/game-ui/src/screens/home.zig index 90727be5..d8d86296 100644 --- a/modules/game-ui/src/screens/home.zig +++ b/modules/game-ui/src/screens/home.zig @@ -11,30 +11,57 @@ const SingleplayerScreen = @import("singleplayer.zig").SingleplayerScreen; const SettingsScreen = @import("settings.zig").SettingsScreen; const ResourcePacksScreen = @import("resource_packs.zig").ResourcePacksScreen; const EnvironmentScreen = @import("environment.zig").EnvironmentScreen; +const WorldScreen = @import("world.zig").WorldScreen; + +const MENU_PREVIEW_SEED: u64 = 0x5A49_4743_5241_4654; pub const HomeScreen = struct { context: EngineContext, + focused_action: usize, + preview: *WorldScreen, pub const vtable = IScreen.VTable{ .deinit = deinit, + .update = update, .draw = draw, .onEnter = onEnter, + .getWorldStats = getWorldStats, + .isReadyForPresentation = isReadyForPresentation, }; pub fn init(allocator: std.mem.Allocator, context: EngineContext) !*HomeScreen { const self = try allocator.create(HomeScreen); - self.* = .{ .context = context }; + errdefer allocator.destroy(self); + self.* = .{ + .context = context, + .focused_action = 0, + .preview = try WorldScreen.initMenuPreview(allocator, context, MENU_PREVIEW_SEED, 0), + }; return self; } pub fn deinit(ptr: *anyopaque) void { const self: *@This() = @ptrCast(@alignCast(ptr)); + WorldScreen.deinit(self.preview); self.context.allocator.destroy(self); } + pub fn update(ptr: *anyopaque, dt: f32) !void { + const self: *@This() = @ptrCast(@alignCast(ptr)); + try WorldScreen.update(self.preview, dt); + const input = self.context.input; + if (input.isKeyPressed(.down) or input.isKeyPressed(.right_arrow) or input.isKeyPressed(.tab)) { + self.focused_action = (self.focused_action + 1) % 5; + } + if (input.isKeyPressed(.up) or input.isKeyPressed(.left_arrow)) { + self.focused_action = if (self.focused_action == 0) 4 else self.focused_action - 1; + } + } + pub fn draw(ptr: *anyopaque, ui: *UISystem) !void { const self: *@This() = @ptrCast(@alignCast(ptr)); const ctx = self.context; + try WorldScreen.draw(self.preview, ui); ui.begin(); defer ui.end(); @@ -49,13 +76,13 @@ pub const HomeScreen = struct { const ui_scale = Theme.scaleFor(screen_h, ctx.settings.ui_scale); const compact = screen_w < 980.0 * ui_scale; - Theme.drawBackdrop(ui, screen_w, screen_h, ui_scale, .home); + Theme.drawWorldScrim(ui, screen_w, screen_h, ui_scale); const hero_x = if (compact) 34.0 * ui_scale else 88.0 * ui_scale; const hero_y = if (compact) 82.0 * ui_scale else 112.0 * ui_scale; Theme.drawHeroTitle(ui, hero_x, hero_y, ui_scale, compact); - try drawLaunchPanel(ui, screen_w, screen_h, ui_scale, compact, mouse_x, mouse_y, mouse_clicked, ctx); + try drawLaunchPanel(ui, self, screen_w, screen_h, ui_scale, compact, mouse_x, mouse_y, mouse_clicked, ctx); drawFooter(ui, screen_w, screen_h, ui_scale); } @@ -64,66 +91,71 @@ pub const HomeScreen = struct { self.context.input.setMouseCapture(@ptrCast(self.context.window_manager.window), false); } + fn getWorldStats(ptr: *anyopaque) ?@import("engine-ui").WorldStats { + const self: *@This() = @ptrCast(@alignCast(ptr)); + return self.preview.getWorldStats(); + } + + fn isReadyForPresentation(ptr: *anyopaque) bool { + const self: *@This() = @ptrCast(@alignCast(ptr)); + const stats = self.preview.getWorldStats() orelse return false; + return stats.chunks_rendered > 0 and !self.preview.world.telemetry().isStartupBusy(); + } + pub fn screen(self: *@This()) IScreen { return Screen.makeScreen(@This(), self); } }; -fn drawLaunchPanel(ui: *UISystem, screen_w: f32, screen_h: f32, scale: f32, compact: bool, mouse_x: f32, mouse_y: f32, mouse_clicked: bool, ctx: EngineContext) !void { - const panel_w: f32 = if (compact) @min(screen_w - 44.0 * scale, 600.0 * scale) else @min(screen_w * 0.43, 620.0 * scale); - const panel_h: f32 = if (compact) 398.0 * scale else 486.0 * scale; +fn drawLaunchPanel(ui: *UISystem, self: *HomeScreen, screen_w: f32, screen_h: f32, scale: f32, compact: bool, mouse_x: f32, mouse_y: f32, mouse_clicked: bool, ctx: EngineContext) !void { + const panel_w: f32 = if (compact) @min(screen_w - 40.0 * scale, 680.0 * scale) else @min(screen_w * 0.46, 820.0 * scale); + const panel_h: f32 = if (compact) @min(520.0 * scale, screen_h * 0.62) else 520.0 * scale; const panel_x: f32 = if (compact) (screen_w - panel_w) * 0.5 else screen_w - panel_w - 82.0 * scale; - const panel_y: f32 = if (compact) screen_h * 0.46 else screen_h * 0.24; + const panel_y: f32 = if (compact) @min(screen_h * 0.40, screen_h - panel_h - 24.0 * scale) else screen_h * 0.22; const panel = Rect{ .x = panel_x, .y = panel_y, .width = panel_w, .height = panel_h }; - const row_h: f32 = if (compact) 50.0 * scale else 58.0 * scale; const gap: f32 = 12.0 * scale; - const text_scale: f32 = if (compact) 1.22 * scale else 1.42 * scale; - drawOuterGlow(ui, panel, scale); - ui.drawRect(.{ .x = panel.x + 18.0 * scale, .y = panel.y + 22.0 * scale, .width = panel.width, .height = panel.height }, Color.rgba(0, 0, 0, 0.28)); - ui.drawRect(panel, Color.rgba(0.060, 0.095, 0.125, 0.58)); - ui.drawRect(.{ .x = panel.x, .y = panel.y, .width = panel.width, .height = 1.0 * scale }, Color.rgba(0.94, 0.98, 1.0, 0.20)); - ui.drawRect(.{ .x = panel.x, .y = panel.y + 42.0 * scale, .width = panel.width, .height = 1.0 * scale }, Color.rgba(0.58, 0.72, 0.82, 0.42)); - ui.drawRect(.{ .x = panel.x, .y = panel.y, .width = 4.0 * scale, .height = panel.height }, Theme.signal); - ui.drawRectOutline(panel, Color.rgba(0.54, 0.66, 0.74, 0.58), 1.0 * scale); - drawPanelNoise(ui, panel, scale); + ui.drawRect(.{ .x = panel.x + 14.0 * scale, .y = panel.y + 16.0 * scale, .width = panel.width, .height = panel.height }, Color.rgba(0, 0, 0, 0.48)); + ui.drawRect(panel, Theme.panel); + ui.drawRectOutline(panel, Theme.outline, 1.0 * scale); - Font.drawText(ui, "LAUNCH", panel.x + 30.0 * scale, panel.y + 17.0 * scale, 0.88 * scale, Theme.signal); - Font.drawText(ui, "READY", panel.x + panel.width - 104.0 * scale, panel.y + 17.0 * scale, 0.78 * scale, Theme.amber); + Font.drawText(ui, "START", panel.x + 30.0 * scale, panel.y + 20.0 * scale, 0.86 * scale, Theme.signal); + Font.drawText(ui, "Choose an activity", panel.x + 30.0 * scale, panel.y + 45.0 * scale, 0.98 * scale, Theme.text); - var y = panel.y + 68.0 * scale; - const x = panel.x + 28.0 * scale; - const w = panel.width - 56.0 * scale; + var y = panel.y + 84.0 * scale; + const x = panel.x + 30.0 * scale; + const w = panel.width - 60.0 * scale; + const confirm = ctx.input_mapper.isActionPressed(ctx.input, .ui_confirm); - if (Theme.drawButton(ui, .{ .x = x, .y = y, .width = w, .height = row_h + 8.0 * scale }, "PLAY WORLD", text_scale, mouse_x, mouse_y, mouse_clicked, .primary, scale)) { + const play_h: f32 = 112.0 * scale; + if (Theme.drawActionCard(ui, .{ .x = x, .y = y, .width = w, .height = play_h }, "PLAY", "Create a world or continue from a local save.", "ENTER", mouse_x, mouse_y, mouse_clicked, true, self.focused_action == 0, scale) or (confirm and self.focused_action == 0)) { const sp_screen = try SingleplayerScreen.init(ctx.allocator, ctx); errdefer sp_screen.deinit(sp_screen); ctx.screen_manager.pushScreen(sp_screen.screen()); } - y += row_h + 8.0 * scale + gap; + y += play_h + 22.0 * scale; + Theme.drawSectionLabel(ui, x, y, "CUSTOMIZE", scale); + y += 34.0 * scale; - if (Theme.drawButton(ui, .{ .x = x, .y = y, .width = w, .height = row_h }, "RESOURCE PACKS", text_scale, mouse_x, mouse_y, mouse_clicked, .secondary, scale)) { + const card_w = (w - gap) * 0.5; + const card_h: f32 = 88.0 * scale; + if (Theme.drawActionCard(ui, .{ .x = x, .y = y, .width = card_w, .height = card_h }, "RESOURCE PACKS", "Change block textures.", "", mouse_x, mouse_y, mouse_clicked, false, self.focused_action == 1, scale) or (confirm and self.focused_action == 1)) { const rp_screen = try ResourcePacksScreen.init(ctx.allocator, ctx); errdefer rp_screen.deinit(rp_screen); ctx.screen_manager.pushScreen(rp_screen.screen()); } - y += row_h + gap; - - if (Theme.drawButton(ui, .{ .x = x, .y = y, .width = w, .height = row_h }, "ENVIRONMENT", text_scale, mouse_x, mouse_y, mouse_clicked, .secondary, scale)) { + if (Theme.drawActionCard(ui, .{ .x = x + card_w + gap, .y = y, .width = card_w, .height = card_h }, "SKY & LIGHTING", "Choose environment light.", "", mouse_x, mouse_y, mouse_clicked, false, self.focused_action == 2, scale) or (confirm and self.focused_action == 2)) { const env_screen = try EnvironmentScreen.init(ctx.allocator, ctx); errdefer env_screen.deinit(env_screen); ctx.screen_manager.pushScreen(env_screen.screen()); } - y += row_h + gap; - - if (Theme.drawButton(ui, .{ .x = x, .y = y, .width = w, .height = row_h }, "SETTINGS", text_scale, mouse_x, mouse_y, mouse_clicked, .ghost, scale)) { + y += card_h + gap; + if (Theme.drawActionCard(ui, .{ .x = x, .y = y, .width = card_w, .height = card_h }, "SETTINGS", "Display, controls, and graphics.", "", mouse_x, mouse_y, mouse_clicked, false, self.focused_action == 3, scale) or (confirm and self.focused_action == 3)) { const settings_screen = try SettingsScreen.init(ctx.allocator, ctx); errdefer settings_screen.deinit(settings_screen); ctx.screen_manager.pushScreen(settings_screen.screen()); } - y += row_h + gap; - - if (Theme.drawButton(ui, .{ .x = x, .y = y, .width = w, .height = row_h }, "EXIT", text_scale, mouse_x, mouse_y, mouse_clicked, .ghost, scale)) { + if (Theme.drawActionCard(ui, .{ .x = x + card_w + gap, .y = y, .width = card_w, .height = card_h }, "EXIT", "Close ZigCraft safely.", "", mouse_x, mouse_y, mouse_clicked, false, self.focused_action == 4, scale) or (confirm and self.focused_action == 4)) { ctx.input.setShouldQuit(true); } } diff --git a/modules/game-ui/src/screens/paused.zig b/modules/game-ui/src/screens/paused.zig index 144a3170..dddd0040 100644 --- a/modules/game-ui/src/screens/paused.zig +++ b/modules/game-ui/src/screens/paused.zig @@ -10,6 +10,7 @@ const SettingsScreen = @import("settings.zig").SettingsScreen; pub const PausedScreen = struct { context: EngineContext, + focused_action: usize, pub const vtable = IScreen.VTable{ .deinit = deinit, @@ -21,7 +22,7 @@ pub const PausedScreen = struct { pub fn init(allocator: std.mem.Allocator, context: EngineContext) !*PausedScreen { const self = try allocator.create(PausedScreen); - self.* = .{ .context = context }; + self.* = .{ .context = context, .focused_action = 0 }; return self; } @@ -33,7 +34,13 @@ pub const PausedScreen = struct { pub fn update(ptr: *anyopaque, dt: f32) !void { const self: *@This() = @ptrCast(@alignCast(ptr)); _ = dt; - if (self.context.input_mapper.isActionPressed(self.context.input, .ui_back)) self.context.screen_manager.popScreen(); + if (self.context.input_mapper.isActionPressed(self.context.input, .ui_back)) { + self.context.screen_manager.popScreen(); + return; + } + const input = self.context.input; + if (input.isKeyPressed(.down) or input.isKeyPressed(.tab)) self.focused_action = (self.focused_action + 1) % 3; + if (input.isKeyPressed(.up)) self.focused_action = if (self.focused_action == 0) 2 else self.focused_action - 1; } pub fn draw(ptr: *anyopaque, ui: *UISystem) !void { @@ -57,7 +64,7 @@ pub const PausedScreen = struct { ui.drawRect(.{ .x = 0, .y = 0, .width = screen_w, .height = screen_h }, Color.rgba(0, 0, 0, 0.36)); const panel_w = @min(520.0 * ui_scale, screen_w - 80.0 * ui_scale); - const panel_h = 420.0 * ui_scale; + const panel_h = @min(420.0 * ui_scale, screen_h - 32.0 * ui_scale); const panel_x = (screen_w - panel_w) * 0.5; const panel_y = (screen_h - panel_h) * 0.5; const shell = Theme.drawShell(ui, .{ .x = panel_x, .y = panel_y, .width = panel_w, .height = panel_h }, ui_scale, "SESSION", "PAUSED", "Resume or leave the current world."); @@ -68,16 +75,17 @@ pub const PausedScreen = struct { const bh = 56.0 * ui_scale; const gap = 13.0 * ui_scale; const btn_scale = 1.42 * ui_scale; + const confirm = ctx.input_mapper.isActionPressed(ctx.input, .ui_confirm); - if (Theme.drawButton(ui, .{ .x = bx, .y = by, .width = bw, .height = bh }, "RESUME WORLD", btn_scale, mouse_x, mouse_y, mouse_clicked, .primary, ui_scale)) ctx.screen_manager.popScreen(); + if (Theme.drawButtonFocused(ui, .{ .x = bx, .y = by, .width = bw, .height = bh }, "RESUME WORLD", btn_scale, mouse_x, mouse_y, mouse_clicked, .primary, self.focused_action == 0, ui_scale) or (confirm and self.focused_action == 0)) ctx.screen_manager.popScreen(); by += bh + gap; - if (Theme.drawButton(ui, .{ .x = bx, .y = by, .width = bw, .height = bh }, "SETTINGS", btn_scale, mouse_x, mouse_y, mouse_clicked, .secondary, ui_scale)) { + if (Theme.drawButtonFocused(ui, .{ .x = bx, .y = by, .width = bw, .height = bh }, "SETTINGS", btn_scale, mouse_x, mouse_y, mouse_clicked, .secondary, self.focused_action == 1, ui_scale) or (confirm and self.focused_action == 1)) { const settings_screen = try SettingsScreen.init(ctx.allocator, ctx); errdefer settings_screen.deinit(settings_screen); ctx.screen_manager.pushScreen(settings_screen.screen()); } by += bh + gap; - if (Theme.drawButton(ui, .{ .x = bx, .y = by, .width = bw, .height = bh }, "QUIT TO TITLE", btn_scale, mouse_x, mouse_y, mouse_clicked, .ghost, ui_scale)) { + if (Theme.drawButtonFocused(ui, .{ .x = bx, .y = by, .width = bw, .height = bh }, "QUIT TO TITLE", btn_scale, mouse_x, mouse_y, mouse_clicked, .ghost, self.focused_action == 2, ui_scale) or (confirm and self.focused_action == 2)) { const home_screen = try HomeScreen.init(ctx.allocator, ctx); errdefer home_screen.deinit(home_screen); ctx.screen_manager.setScreen(home_screen.screen()); diff --git a/modules/game-ui/src/screens/resource_packs.zig b/modules/game-ui/src/screens/resource_packs.zig index b90e952a..0aa6467a 100644 --- a/modules/game-ui/src/screens/resource_packs.zig +++ b/modules/game-ui/src/screens/resource_packs.zig @@ -16,6 +16,7 @@ const PANEL_HEIGHT_MAX = 760.0; pub const ResourcePacksScreen = struct { context: ResourcePacksContext, reload_status: ?[]const u8, + scroll_offset: f32, pub const vtable = IScreen.VTable{ .deinit = deinit, @@ -29,6 +30,7 @@ pub const ResourcePacksScreen = struct { self.* = .{ .context = context.resourcePacksContext(), .reload_status = null, + .scroll_offset = 0.0, }; return self; } @@ -73,28 +75,38 @@ pub const ResourcePacksScreen = struct { const shell = Theme.drawShell(ui, .{ .x = panel_x, .y = panel_y, .width = panel_w, .height = panel_h }, ui_scale, "ASSETS", "RESOURCE PACKS", "Pick a texture pack for block materials."); Theme.drawListRail(ui, shell.content, ui_scale); - var y = shell.content.y + 18.0 * ui_scale; const row_x = shell.content.x + 18.0 * ui_scale; const row_w = shell.content.width - 36.0 * ui_scale; const row_h = 56.0 * ui_scale; const btn_scale = 1.18 * ui_scale; + const packs = manager.getPackNames(); + const content_h = @as(f32, @floatFromInt(packs.len + 1)) * (row_h + 10.0 * ui_scale) + 26.0 * ui_scale; + const max_scroll = @max(0.0, content_h - shell.content.height); + self.scroll_offset -= ctx.input.getScrollDelta().y * 32.0 * ui_scale; + self.scroll_offset = @max(0.0, @min(self.scroll_offset, max_scroll)); + var y = shell.content.y + 18.0 * ui_scale - self.scroll_offset; + Theme.drawScrollbar(ui, shell.content.x + shell.content.width - 10.0 * ui_scale, shell.content.y + 12.0 * ui_scale, shell.content.height - 24.0 * ui_scale, content_h, shell.content.height, self.scroll_offset, max_scroll, ui_scale); const is_default = std.mem.eql(u8, settings.texture_pack, "default"); - if (drawPackButton(ui, row_x, y, row_w, row_h, "DEFAULT / BUILT-IN", "Base texture atlas shipped with ZigCraft.", is_default, btn_scale, mouse_x, mouse_y, mouse_clicked, ui_scale)) { - if (!is_default) { - try settings_pkg.persistence.setTexturePack(settings, ctx.allocator, "default"); - try manager.setActivePack("default"); - self.reload_status = "Reloading texture atlas..."; - try self.reloadAtlas(); - self.reload_status = "Texture pack reloaded."; + if (y + row_h >= shell.content.y and y <= shell.content.y + shell.content.height) { + if (drawPackButton(ui, row_x, y, row_w, row_h, "DEFAULT / BUILT-IN", "Base texture atlas shipped with ZigCraft.", is_default, btn_scale, mouse_x, mouse_y, mouse_clicked, ui_scale)) { + if (!is_default) { + try settings_pkg.persistence.setTexturePack(settings, ctx.allocator, "default"); + try manager.setActivePack("default"); + self.reload_status = "Reloading texture atlas..."; + try self.reloadAtlas(); + self.reload_status = "Texture pack reloaded."; + } } } y += row_h + 10.0 * ui_scale; - const packs = manager.getPackNames(); var buffer: [160]u8 = undefined; for (packs) |pack| { - if (y + row_h > shell.content.y + shell.content.height) break; + if (y + row_h < shell.content.y or y > shell.content.y + shell.content.height) { + y += row_h + 10.0 * ui_scale; + continue; + } const is_selected = std.mem.eql(u8, settings.texture_pack, pack.name); const label = std.fmt.bufPrint(&buffer, "{s}", .{pack.name}) catch "PACK"; if (drawPackButton(ui, row_x, y, row_w, row_h, label, "External pack discovered by the resource manager.", is_selected, btn_scale, mouse_x, mouse_y, mouse_clicked, ui_scale)) { @@ -138,8 +150,10 @@ pub const ResourcePacksScreen = struct { }; fn drawPackButton(ui: *UISystem, x: f32, y: f32, w: f32, h: f32, label: []const u8, description: []const u8, selected: bool, btn_scale: f32, mx: f32, my: f32, clicked: bool, scale: f32) bool { - Theme.drawOptionRow(ui, .{ .x = x, .y = y, .width = w, .height = h }, label, description, 1.04 * scale, selected, scale); + const row = Theme.Rect{ .x = x, .y = y, .width = w, .height = h }; + Theme.drawOptionRow(ui, row, label, description, 1.04 * scale, selected, scale); const action_w = 150.0 * scale; const action_x = x + w - action_w - 12.0 * scale; - return Theme.drawButton(ui, .{ .x = action_x, .y = y + 9.0 * scale, .width = action_w, .height = h - 18.0 * scale }, if (selected) "ACTIVE" else "SELECT", btn_scale, mx, my, clicked, if (selected) .primary else .secondary, scale); + const action_clicked = Theme.drawButton(ui, .{ .x = action_x, .y = y + 9.0 * scale, .width = action_w, .height = h - 18.0 * scale }, if (selected) "ACTIVE" else "USE PACK", btn_scale, mx, my, clicked, if (selected) .primary else .secondary, scale); + return action_clicked or (clicked and row.contains(mx, my)); } diff --git a/modules/game-ui/src/screens/settings.zig b/modules/game-ui/src/screens/settings.zig index 7f7d12b6..cd429de7 100644 --- a/modules/game-ui/src/screens/settings.zig +++ b/modules/game-ui/src/screens/settings.zig @@ -60,7 +60,11 @@ pub const SettingsScreen = struct { if (self.context.input_mapper.isActionPressed(self.context.input, .ui_back)) { self.context.saveSettings(); self.context.screen_manager.popScreen(); + return; } + const input = self.context.input; + if (input.isKeyPressed(.right_arrow) or input.isKeyPressed(.down) or input.isKeyPressed(.tab)) self.active_tab = nextTab(self.active_tab); + if (input.isKeyPressed(.left_arrow) or input.isKeyPressed(.up)) self.active_tab = previousTab(self.active_tab); } pub fn draw(ptr: *anyopaque, ui: *UISystem) !void { @@ -83,36 +87,51 @@ pub const SettingsScreen = struct { Theme.drawBackdrop(ui, screen_w, screen_h, ui_scale, .settings); - const margin: f32 = 24.0 * ui_scale; - const panel_w: f32 = @min(screen_w - margin * 2.0, PANEL_WIDTH_MAX * ui_scale); - const panel_h: f32 = @min(screen_h - margin * 2.0, PANEL_HEIGHT_MAX * ui_scale); - const panel_x: f32 = (screen_w - panel_w) * 0.5; - const panel_y: f32 = (screen_h - panel_h) * 0.5; - const shell = Theme.drawShell(ui, .{ .x = panel_x, .y = panel_y, .width = panel_w, .height = panel_h }, ui_scale, "CONTROL", "OPTIONS", tabDescription(self.active_tab)); - - const tab_h: f32 = 38.0 * ui_scale; - drawTabs(ui, self, shell.content.x, shell.content.y, shell.content.width, tab_h, mouse_x, mouse_y, mouse_clicked, ui_scale); - - const body = Rect{ - .x = shell.content.x, - .y = shell.content.y + tab_h + 12.0 * ui_scale, - .width = shell.content.width, - .height = shell.content.height - tab_h - 12.0 * ui_scale, + const margin: f32 = 48.0 * ui_scale; + const page_w: f32 = @min(screen_w - margin * 2.0, 1460.0 * ui_scale); + const page_x = (screen_w - page_w) * 0.5; + const page_top = @max(38.0 * ui_scale, (screen_h - 900.0 * ui_scale) * 0.5); + const header_h: f32 = 132.0 * ui_scale; + const footer_h: f32 = 72.0 * ui_scale; + const workspace_y = page_top + header_h; + const workspace_h = @min(650.0 * ui_scale, screen_h - workspace_y - footer_h - 30.0 * ui_scale); + + Font.drawText(ui, "PREFERENCES", page_x, page_top, 0.88 * ui_scale, Theme.signal); + Font.drawText(ui, "Make ZigCraft yours.", page_x, page_top + 28.0 * ui_scale, 4.2 * ui_scale, Theme.title); + Font.drawText(ui, "Changes apply immediately and are saved when you leave.", page_x, page_top + 88.0 * ui_scale, 1.02 * ui_scale, Theme.muted); + + const sidebar_layout = page_w >= 900.0 * ui_scale; + const nav_w: f32 = if (sidebar_layout) 220.0 * ui_scale else page_w; + const nav_h: f32 = if (sidebar_layout) workspace_h else 46.0 * ui_scale; + drawTabs(ui, self, page_x, workspace_y, nav_w, nav_h, sidebar_layout, mouse_x, mouse_y, mouse_clicked, ui_scale); + + const body = if (sidebar_layout) Rect{ + .x = page_x + nav_w + 38.0 * ui_scale, + .y = workspace_y, + .width = page_w - nav_w - 38.0 * ui_scale, + .height = workspace_h, + } else Rect{ + .x = page_x, + .y = workspace_y + nav_h + 20.0 * ui_scale, + .width = page_w, + .height = workspace_h - nav_h - 20.0 * ui_scale, }; - Theme.drawListRail(ui, body, ui_scale); - const inner = Rect{ - .x = body.x + 16.0 * ui_scale, - .y = body.y + 14.0 * ui_scale, - .width = body.width - 32.0 * ui_scale, - .height = body.height - 28.0 * ui_scale, + .x = body.x + 4.0 * ui_scale, + .y = body.y + 72.0 * ui_scale, + .width = body.width - 8.0 * ui_scale, + .height = body.height - 76.0 * ui_scale, }; + Font.drawText(ui, tabLabel(self.active_tab), body.x + 4.0 * ui_scale, body.y + 2.0 * ui_scale, 1.52 * ui_scale, Theme.title); + Font.drawText(ui, tabDescription(self.active_tab), body.x + 4.0 * ui_scale, body.y + 37.0 * ui_scale, 0.88 * ui_scale, Theme.muted); + ui.drawRect(.{ .x = body.x + 4.0 * ui_scale, .y = body.y + 60.0 * ui_scale, .width = body.width - 8.0 * ui_scale, .height = 1.0 * ui_scale }, Theme.outline); + const layout = columnLayout(inner, ui_scale); - const row_h: f32 = 54.0 * ui_scale; - const label_scale: f32 = 0.98 * ui_scale; - const value_scale: f32 = 0.92 * ui_scale; - const button_scale: f32 = 0.94 * ui_scale; + const row_h: f32 = 72.0 * ui_scale; + const label_scale: f32 = 1.08 * ui_scale; + const value_scale: f32 = 1.00 * ui_scale; + const button_scale: f32 = 1.00 * ui_scale; switch (self.active_tab) { .display => drawDisplayTab(ui, ctx, settings, rs, layout, row_h, label_scale, value_scale, button_scale, mouse_x, mouse_y, mouse_clicked, ui_scale), @@ -121,7 +140,8 @@ pub const SettingsScreen = struct { .rendering => drawRenderingTab(ui, self, ctx, settings, rs, inner, layout, row_h, label_scale, value_scale, button_scale, mouse_x, mouse_y, mouse_clicked, ui_scale), } - if (Theme.drawButton(ui, .{ .x = panel_x + (panel_w - 164.0 * ui_scale) * 0.5, .y = shell.footer_y + 4.0 * ui_scale, .width = 164.0 * ui_scale, .height = 40.0 * ui_scale }, "BACK", button_scale, mouse_x, mouse_y, mouse_clicked, .ghost, ui_scale)) { + const done_w: f32 = 176.0 * ui_scale; + if (Theme.drawButton(ui, .{ .x = page_x + page_w - done_w, .y = workspace_y + workspace_h + 18.0 * ui_scale, .width = done_w, .height = 52.0 * ui_scale }, "DONE", button_scale, mouse_x, mouse_y, mouse_clicked, .primary, ui_scale)) { ctx.saveSettings(); ctx.screen_manager.popScreen(); } @@ -158,17 +178,49 @@ fn columnLayout(inner: Rect, scale: f32) ColumnLayout { }; } -fn drawTabs(ui: *UISystem, self: *SettingsScreen, x: f32, y: f32, w: f32, h: f32, mx: f32, my: f32, clicked: bool, scale: f32) void { +fn drawTabs(ui: *UISystem, self: *SettingsScreen, x: f32, y: f32, w: f32, h: f32, vertical: bool, mx: f32, my: f32, clicked: bool, scale: f32) void { const tabs = [_]SettingsTab{ .display, .camera, .world, .rendering }; const gap: f32 = 8.0 * scale; - const tab_w = (w - gap * @as(f32, @floatFromInt(tabs.len - 1))) / @as(f32, @floatFromInt(tabs.len)); + const tab_w = if (vertical) w else (w - gap * @as(f32, @floatFromInt(tabs.len - 1))) / @as(f32, @floatFromInt(tabs.len)); + const tab_h = if (vertical) 58.0 * scale else h; + if (vertical) { + Font.drawText(ui, "SETTINGS", x, y, 0.76 * scale, Theme.muted); + ui.drawRect(.{ .x = x, .y = y + 24.0 * scale, .width = w, .height = 1.0 * scale }, Theme.outline); + } for (tabs, 0..) |tab, i| { - const tx = x + @as(f32, @floatFromInt(i)) * (tab_w + gap); + const tx = if (vertical) x else x + @as(f32, @floatFromInt(i)) * (tab_w + gap); + const ty = if (vertical) y + 40.0 * scale + @as(f32, @floatFromInt(i)) * (tab_h + gap) else y; const active = self.active_tab == tab; - if (Theme.drawButton(ui, .{ .x = tx, .y = y, .width = tab_w, .height = h }, tabLabel(tab), 0.82 * scale, mx, my, clicked, if (active) .primary else .ghost, scale)) { + if (if (vertical) + Theme.drawNavItem(ui, .{ .x = tx, .y = ty, .width = tab_w, .height = tab_h }, tabLabel(tab), i, active, mx, my, clicked, scale) + else + Theme.drawButtonFocused(ui, .{ .x = tx, .y = ty, .width = tab_w, .height = tab_h }, tabLabel(tab), 0.88 * scale, mx, my, clicked, if (active) .primary else .ghost, active, scale)) + { self.active_tab = tab; } } + if (vertical) { + ui.drawRect(.{ .x = x + w + 10.0 * scale, .y = y, .width = 1.0 * scale, .height = h }, Theme.outline); + Font.drawText(ui, "ARROWS Switch section", x, y + h - 18.0 * scale, 0.68 * scale, Theme.dim); + } +} + +fn nextTab(tab: SettingsTab) SettingsTab { + return switch (tab) { + .display => .camera, + .camera => .world, + .world => .rendering, + .rendering => .display, + }; +} + +fn previousTab(tab: SettingsTab) SettingsTab { + return switch (tab) { + .display => .rendering, + .camera => .display, + .world => .camera, + .rendering => .world, + }; } fn tabLabel(tab: SettingsTab) []const u8 { diff --git a/modules/game-ui/src/screens/singleplayer.zig b/modules/game-ui/src/screens/singleplayer.zig index 3e55cb71..262d2593 100644 --- a/modules/game-ui/src/screens/singleplayer.zig +++ b/modules/game-ui/src/screens/singleplayer.zig @@ -66,6 +66,10 @@ pub const SingleplayerScreen = struct { self.context.screen_manager.popScreen(); return; } + if (self.context.input.isKeyPressed(.tab)) { + self.name_focused = !self.name_focused; + self.seed_focused = !self.name_focused; + } if (self.seed_focused) try text_input.handleTextTyping(&self.seed_input, self.context.allocator, self.context.input, 32); if (self.name_focused) try text_input.handleTextTyping(&self.name_input, self.context.allocator, self.context.input, 32); } @@ -89,40 +93,57 @@ pub const SingleplayerScreen = struct { Theme.drawBackdrop(ui, screen_w, screen_h, ui_scale, .create); - const margin: f32 = 42.0 * ui_scale; - const panel_w: f32 = @min(screen_w - margin * 2.0, PANEL_WIDTH_MAX * ui_scale); - const panel_h: f32 = @min(screen_h - margin * 2.0, PANEL_HEIGHT_MAX * ui_scale); - const panel_x: f32 = (screen_w - panel_w) * 0.5; - const panel_y: f32 = (screen_h - panel_h) * 0.5; - const shell = Theme.drawShell(ui, .{ .x = panel_x, .y = panel_y, .width = panel_w, .height = panel_h }, ui_scale, "GENERATOR", "NEW WORLD", "Name it, seed it, pick a terrain generator."); - const cursor_visible = @as(u32, @truncate(@as(u64, @intFromFloat(ctx.time.elapsed * 2.0)))) % 2 == 0; const form_scale: f32 = 1.18 * ui_scale; const btn_scale: f32 = 1.16 * ui_scale; - const input_h: f32 = 52.0 * ui_scale; + const input_h: f32 = 58.0 * ui_scale; + + const margin: f32 = 48.0 * ui_scale; + const page_w = @min(screen_w - margin * 2.0, 1420.0 * ui_scale); + const page_x = (screen_w - page_w) * 0.5; + const page_top = @max(38.0 * ui_scale, (screen_h - 860.0 * ui_scale) * 0.5); + const header_h: f32 = 128.0 * ui_scale; + const footer_h: f32 = 74.0 * ui_scale; + const body_y = page_top + header_h; + const body_h = @min(610.0 * ui_scale, screen_h - body_y - footer_h - 32.0 * ui_scale); + const column_gap: f32 = 18.0 * ui_scale; + const rail_w: f32 = if (compact) 0.0 else 190.0 * ui_scale; + const summary_visible = page_w >= 1120.0 * ui_scale; + const summary_w: f32 = if (summary_visible) 330.0 * ui_scale else 0.0; + const form_x = page_x + rail_w + (if (compact) 0.0 else column_gap); + const form_w = page_w - rail_w - summary_w - (if (compact) 0.0 else column_gap) - (if (summary_visible) column_gap else 0.0); + const summary_x = form_x + form_w + column_gap; + + Font.drawText(ui, "NEW WORLD", page_x, page_top, 0.88 * ui_scale, Theme.signal); + Font.drawText(ui, "Create something new.", page_x, page_top + 28.0 * ui_scale, 4.2 * ui_scale, Theme.title); + Font.drawText(ui, "Name your world, choose its terrain, then jump in.", page_x, page_top + 86.0 * ui_scale, 1.06 * ui_scale, Theme.muted); - const gap: f32 = 28.0 * ui_scale; - const preview_w: f32 = if (compact) 0.0 else shell.content.width * 0.40; - const form_x: f32 = if (compact) shell.content.x else shell.content.x + preview_w + gap; - const form_w: f32 = if (compact) shell.content.width else shell.content.width - preview_w - gap; + if (!compact) drawCreateSteps(ui, .{ .x = page_x, .y = body_y, .width = rail_w, .height = body_h }, ui_scale); - if (!compact) drawGeneratorPreview(ui, .{ .x = shell.content.x, .y = shell.content.y, .width = preview_w, .height = shell.content.height }, self.selected_generator_index, ui_scale); + const form_rect = Rect{ .x = form_x, .y = body_y, .width = form_w, .height = body_h }; + ui.drawRect(.{ .x = form_rect.x + 9.0 * ui_scale, .y = form_rect.y + 10.0 * ui_scale, .width = form_rect.width, .height = form_rect.height }, Color.rgba(0, 0, 0, 0.38)); + ui.drawRect(form_rect, Color.rgba(0.045, 0.070, 0.090, 0.98)); + ui.drawRect(.{ .x = form_rect.x, .y = form_rect.y, .width = 7.0 * ui_scale, .height = form_rect.height }, Theme.signal); - var y = shell.content.y + 6.0 * ui_scale; - Theme.drawSectionLabel(ui, form_x, y, "WORLD NAME", ui_scale); - y += 34.0 * ui_scale; + const content_x = form_rect.x + 34.0 * ui_scale; + const content_w = form_rect.width - 68.0 * ui_scale; + var y = form_rect.y + 28.0 * ui_scale; + Font.drawText(ui, "World details", content_x, y, 1.62 * ui_scale, Theme.title); + y += 35.0 * ui_scale; + Font.drawText(ui, "Give this save a recognizable name.", content_x, y, 0.84 * ui_scale, Theme.muted); + y += 35.0 * ui_scale; - Font.drawText(ui, "WORLD NAME", form_x, y, 0.82 * ui_scale, Theme.muted); - y += 20.0 * ui_scale; - const name_rect = Rect{ .x = form_x, .y = y, .width = form_w, .height = input_h }; - y += input_h + 18.0 * ui_scale; + Font.drawText(ui, "NAME", content_x, y, 0.76 * ui_scale, Theme.signal); + y += 21.0 * ui_scale; + const name_rect = Rect{ .x = content_x, .y = y, .width = content_w, .height = input_h }; + y += input_h + 20.0 * ui_scale; - Font.drawText(ui, "SEED", form_x, y, 0.82 * ui_scale, Theme.muted); - y += 20.0 * ui_scale; - const random_w: f32 = @min(150.0 * ui_scale, form_w * 0.34); - const seed_rect = Rect{ .x = form_x, .y = y, .width = form_w - random_w - 12.0 * ui_scale, .height = input_h }; + Font.drawText(ui, "SEED", content_x, y, 0.76 * ui_scale, Theme.signal); + y += 21.0 * ui_scale; + const random_w: f32 = @min(144.0 * ui_scale, content_w * 0.30); + const seed_rect = Rect{ .x = content_x, .y = y, .width = content_w - random_w - 12.0 * ui_scale, .height = input_h }; const random_rect = Rect{ .x = seed_rect.x + seed_rect.width + 12.0 * ui_scale, .y = y, .width = random_w, .height = input_h }; - y += input_h + 24.0 * ui_scale; + y += input_h + 30.0 * ui_scale; if (mouse_clicked) { self.name_focused = name_rect.contains(mouse_x, mouse_y); @@ -137,41 +158,44 @@ pub const SingleplayerScreen = struct { self.name_focused = false; } - Theme.drawSectionLabel(ui, form_x, y, "TERRAIN TYPE", ui_scale); - y += 38.0 * ui_scale; - const g_info = registry.getGeneratorInfo(self.selected_generator_index); + Font.drawText(ui, "CHOOSE TERRAIN", content_x, y, 0.76 * ui_scale, Theme.signal); + y += 24.0 * ui_scale; const gen_count = registry.getGeneratorCount(); - const profile_h: f32 = 76.0 * ui_scale; - Theme.drawOptionRow(ui, .{ .x = form_x, .y = y, .width = form_w, .height = profile_h }, g_info.name, g_info.description, 1.22 * ui_scale, true, ui_scale); - - const arrow_w: f32 = 48.0 * ui_scale; - const label_w: f32 = 142.0 * ui_scale; - const ctrl_y = y + 14.0 * ui_scale; - const right_x = form_x + form_w - arrow_w - 12.0 * ui_scale; - const value_x = right_x - label_w - 8.0 * ui_scale; - const left_x = value_x - arrow_w - 8.0 * ui_scale; - if (Theme.drawButton(ui, .{ .x = left_x, .y = ctrl_y, .width = arrow_w, .height = 46.0 * ui_scale }, "<", btn_scale, mouse_x, mouse_y, mouse_clicked, .ghost, ui_scale)) { - self.selected_generator_index = if (self.selected_generator_index == 0) gen_count - 1 else self.selected_generator_index - 1; - } - var gen_label_buf: [32]u8 = undefined; - const gen_label = std.fmt.bufPrint(&gen_label_buf, "{}/{}", .{ self.selected_generator_index + 1, gen_count }) catch "?"; - Theme.drawValueText(ui, .{ .x = value_x, .y = ctrl_y, .width = label_w, .height = 46.0 * ui_scale }, gen_label, btn_scale, ui_scale); - if (Theme.drawButton(ui, .{ .x = right_x, .y = ctrl_y, .width = arrow_w, .height = 46.0 * ui_scale }, ">", btn_scale, mouse_x, mouse_y, mouse_clicked, .ghost, ui_scale)) { - self.selected_generator_index = (self.selected_generator_index + 1) % gen_count; + const generator_gap: f32 = 12.0 * ui_scale; + const generator_cols: usize = if (content_w >= 480.0 * ui_scale) 2 else 1; + const generator_w = (content_w - generator_gap * @as(f32, @floatFromInt(generator_cols - 1))) / @as(f32, @floatFromInt(generator_cols)); + const generator_h: f32 = 82.0 * ui_scale; + var generator_index: usize = 0; + while (generator_index < gen_count) : (generator_index += 1) { + const col = generator_index % generator_cols; + const row = generator_index / generator_cols; + const generator_rect = Rect{ + .x = content_x + @as(f32, @floatFromInt(col)) * (generator_w + generator_gap), + .y = y + @as(f32, @floatFromInt(row)) * (generator_h + generator_gap), + .width = generator_w, + .height = generator_h, + }; + const info = registry.getGeneratorInfo(generator_index); + const selected = self.selected_generator_index == generator_index; + drawTerrainTile(ui, generator_rect, info.name, info.description, generator_index, selected, ui_scale); + if (mouse_clicked and generator_rect.contains(mouse_x, mouse_y)) self.selected_generator_index = generator_index; } - const bottom_y = shell.footer_y; - const load_w = @min(260.0 * ui_scale, form_w * 0.42); - const action_w = (form_w - load_w - 24.0 * ui_scale) * 0.5; - if (Theme.drawButton(ui, .{ .x = form_x, .y = bottom_y, .width = load_w, .height = 48.0 * ui_scale }, "LOAD WORLD", btn_scale, mouse_x, mouse_y, mouse_clicked, .ghost, ui_scale)) { + if (summary_visible) drawWorldSummary(ui, .{ .x = summary_x, .y = body_y, .width = summary_w, .height = body_h }, self.selected_generator_index, self.seed_input.items, ui_scale); + + const bottom_y = body_y + body_h + 20.0 * ui_scale; + const load_w = @min(220.0 * ui_scale, page_w * 0.30); + const action_w = 176.0 * ui_scale; + const actions_x = page_x + page_w - action_w * 2.0 - 12.0 * ui_scale; + if (Theme.drawButton(ui, .{ .x = page_x, .y = bottom_y, .width = load_w, .height = 52.0 * ui_scale }, "MY WORLDS", btn_scale, mouse_x, mouse_y, mouse_clicked, .ghost, ui_scale)) { const wl_screen = try WorldListScreen.init(ctx.allocator, ctx); errdefer wl_screen.deinit(wl_screen); ctx.screen_manager.pushScreen(wl_screen.screen()); } - if (Theme.drawButton(ui, .{ .x = form_x + load_w + 12.0 * ui_scale, .y = bottom_y, .width = action_w, .height = 48.0 * ui_scale }, "BACK", btn_scale, mouse_x, mouse_y, mouse_clicked, .ghost, ui_scale)) { + if (Theme.drawButton(ui, .{ .x = actions_x, .y = bottom_y, .width = action_w, .height = 52.0 * ui_scale }, "CANCEL", btn_scale, mouse_x, mouse_y, mouse_clicked, .ghost, ui_scale)) { ctx.screen_manager.popScreen(); } - if (Theme.drawButton(ui, .{ .x = form_x + load_w + 24.0 * ui_scale + action_w, .y = bottom_y, .width = action_w, .height = 48.0 * ui_scale }, "CREATE", btn_scale, mouse_x, mouse_y, mouse_clicked, .primary, ui_scale) or ctx.input_mapper.isActionPressed(ctx.input, .ui_confirm)) { + if (Theme.drawButton(ui, .{ .x = actions_x + action_w + 12.0 * ui_scale, .y = bottom_y, .width = action_w, .height = 52.0 * ui_scale }, "CREATE", btn_scale, mouse_x, mouse_y, mouse_clicked, .primary, ui_scale) or ctx.input_mapper.isActionPressed(ctx.input, .ui_confirm)) { const seed = try seed_gen.resolveSeed(&self.seed_input, ctx.allocator); const trimmed_name = std.mem.trim(u8, self.name_input.items, " \t\r\n"); const world_name = if (trimmed_name.len > 0) trimmed_name else "New World"; @@ -188,28 +212,52 @@ pub const SingleplayerScreen = struct { } }; -fn drawGeneratorPreview(ui: *UISystem, rect: Rect, selected_generator_index: usize, scale: f32) void { +fn drawWorldSummary(ui: *UISystem, rect: Rect, selected_generator_index: usize, seed: []const u8, scale: f32) void { const g_info = registry.getGeneratorInfo(selected_generator_index); - Theme.drawListRail(ui, rect, scale); - Font.drawText(ui, "TERRAIN PREVIEW", rect.x + 22.0 * scale, rect.y + 20.0 * scale, 0.82 * scale, Theme.muted); - Font.drawText(ui, g_info.name, rect.x + 22.0 * scale, rect.y + 50.0 * scale, 1.72 * scale, Theme.title); - Font.drawText(ui, g_info.description, rect.x + 22.0 * scale, rect.y + 86.0 * scale, 0.82 * scale, Theme.text); - - const terrain_y = rect.y + rect.height - 158.0 * scale; - ui.drawRect(.{ .x = rect.x + 22.0 * scale, .y = terrain_y, .width = rect.width - 44.0 * scale, .height = 104.0 * scale }, Color.rgba(0.034, 0.054, 0.070, 0.92)); - ui.drawRect(.{ .x = rect.x + 22.0 * scale, .y = terrain_y + 52.0 * scale, .width = rect.width - 44.0 * scale, .height = 2.0 * scale }, Color.rgba(0.58, 0.72, 0.82, 0.44)); - - var x = rect.x + 40.0 * scale; - var i: usize = 0; - while (i < 8) : (i += 1) { - const w: f32 = if (i % 3 == 0) 34.0 * scale else if (i % 3 == 1) 48.0 * scale else 28.0 * scale; - const h: f32 = if (i % 4 == 0) 78.0 * scale else if (i % 4 == 1) 46.0 * scale else if (i % 4 == 2) 92.0 * scale else 60.0 * scale; - ui.drawRect(.{ .x = x, .y = terrain_y + 104.0 * scale - h, .width = w, .height = h }, Color.rgba(0.080, 0.120, 0.150, 0.86)); - ui.drawRect(.{ .x = x, .y = terrain_y + 104.0 * scale - h, .width = w, .height = 7.0 * scale }, if (i % 2 == 0) Theme.signal else Theme.copper); - x += w + 12.0 * scale; + ui.drawRect(rect, Color.rgba(0.035, 0.145, 0.185, 0.98)); + ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = rect.width, .height = 8.0 * scale }, Theme.signal); + Font.drawText(ui, "READY TO CREATE", rect.x + 28.0 * scale, rect.y + 30.0 * scale, 0.78 * scale, Color.rgba(0.72, 0.92, 1.0, 1.0)); + Font.drawText(ui, g_info.name, rect.x + 28.0 * scale, rect.y + 68.0 * scale, 1.82 * scale, Theme.title); + Font.drawText(ui, g_info.description, rect.x + 28.0 * scale, rect.y + 110.0 * scale, 0.86 * scale, Theme.text); + + const divider_y = rect.y + 156.0 * scale; + ui.drawRect(.{ .x = rect.x + 28.0 * scale, .y = divider_y, .width = rect.width - 56.0 * scale, .height = 1.0 * scale }, Color.rgba(0.55, 0.82, 0.90, 0.42)); + drawSummaryFact(ui, rect.x + 28.0 * scale, divider_y + 28.0 * scale, "SEED", if (seed.len == 0) "Random on launch" else "Custom seed", scale); + drawSummaryFact(ui, rect.x + 28.0 * scale, divider_y + 90.0 * scale, "SAVED TO", "My Worlds", scale); + drawSummaryFact(ui, rect.x + 28.0 * scale, divider_y + 152.0 * scale, "ACCESS", "Offline / local", scale); + + const note_y = rect.y + rect.height - 62.0 * scale; + Font.drawText(ui, "PRESS ENTER TO CREATE", rect.x + 28.0 * scale, note_y, 0.74 * scale, Color.rgba(0.72, 0.92, 1.0, 1.0)); +} + +fn drawSummaryFact(ui: *UISystem, x: f32, y: f32, label: []const u8, value: []const u8, scale: f32) void { + Font.drawText(ui, label, x, y, 0.68 * scale, Color.rgba(0.58, 0.80, 0.88, 1.0)); + Font.drawText(ui, value, x, y + 22.0 * scale, 1.04 * scale, Theme.title); +} + +fn drawCreateSteps(ui: *UISystem, rect: Rect, scale: f32) void { + ui.drawRect(rect, Color.rgba(0.024, 0.038, 0.052, 0.92)); + const labels = [_][]const u8{ "DETAILS", "TERRAIN", "REVIEW" }; + for (labels, 0..) |label, i| { + const y = rect.y + 30.0 * scale + @as(f32, @floatFromInt(i)) * 82.0 * scale; + var number_buf: [4]u8 = undefined; + const number = std.fmt.bufPrint(&number_buf, "0{}", .{i + 1}) catch ""; + Font.drawText(ui, number, rect.x + 22.0 * scale, y, 1.38 * scale, if (i == 0) Theme.signal else Theme.dim); + Font.drawText(ui, label, rect.x + 68.0 * scale, y + 5.0 * scale, 0.82 * scale, if (i == 0) Theme.title else Theme.muted); + if (i == 0) ui.drawRect(.{ .x = rect.x, .y = y - 8.0 * scale, .width = 5.0 * scale, .height = 42.0 * scale }, Theme.signal); } } +fn drawTerrainTile(ui: *UISystem, rect: Rect, label: []const u8, description: []const u8, index: usize, selected: bool, scale: f32) void { + const accents = [_]Color{ Theme.signal, Color.rgba(0.42, 0.78, 0.56, 1.0), Color.rgba(0.88, 0.52, 0.38, 1.0), Color.rgba(0.62, 0.54, 0.94, 1.0) }; + const accent = accents[index % accents.len]; + ui.drawRect(rect, if (selected) Color.rgba(accent.r * 0.24, accent.g * 0.24, accent.b * 0.24, 1.0) else Color.rgba(0.025, 0.044, 0.060, 1.0)); + ui.drawRect(.{ .x = rect.x, .y = rect.y, .width = 8.0 * scale, .height = rect.height }, accent); + Font.drawText(ui, label, rect.x + 22.0 * scale, rect.y + 17.0 * scale, 1.08 * scale, Theme.title); + Font.drawText(ui, description, rect.x + 22.0 * scale, rect.y + 48.0 * scale, 0.68 * scale, Theme.muted); + if (selected) Font.drawText(ui, "SELECTED", rect.x + rect.width - 82.0 * scale, rect.y + 18.0 * scale, 0.62 * scale, accent); +} + fn saveNewWorld(allocator: std.mem.Allocator, seed: u64, generator_index: usize, world_name: []const u8) !void { const home = getenv("HOME") orelse { log.log.warn("Cannot save world: HOME not set", .{}); diff --git a/modules/game-ui/src/screens/world.zig b/modules/game-ui/src/screens/world.zig index 7af9ef56..e5521398 100644 --- a/modules/game-ui/src/screens/world.zig +++ b/modules/game-ui/src/screens/world.zig @@ -66,6 +66,8 @@ pub const WorldScreen = struct { stable_shadow_sun_dir: Vec3 = Vec3.init(0.0, 1.0, 0.0), stable_shadow_sun_initialized: bool = false, save_failure_warning_count: usize = 0, + menu_preview: bool = false, + menu_preview_center: Vec3 = Vec3.zero, pub const vtable = IScreen.VTable{ .deinit = deinit, .update = update, @@ -76,8 +78,25 @@ pub const WorldScreen = struct { }; pub fn init(allocator: std.mem.Allocator, context: EngineContext, seed: u64, generator_index: usize) !*WorldScreen { + return initWithDistance(allocator, context, seed, generator_index, context.settings.render_distance, context.settings.horizon_distance, context.settings.lod_enabled, false); + } + + pub fn initMenuPreview(allocator: std.mem.Allocator, context: EngineContext, seed: u64, generator_index: usize) !*WorldScreen { + return initWithDistance( + allocator, + context, + seed, + generator_index, + context.settings.render_distance, + context.settings.horizon_distance, + context.settings.lod_enabled, + true, + ); + } + + fn initWithDistance(allocator: std.mem.Allocator, context: EngineContext, seed: u64, generator_index: usize, render_distance: i32, horizon_distance: i32, lod_enabled: bool, menu_preview: bool) !*WorldScreen { const render_system = context.render_system; - const session = try GameSession.init(allocator, render_system.getRHI(), render_system.getAtlas(), seed, context.settings.render_distance, context.settings.horizon_distance, context.settings.lod_enabled, generator_index, context.settings.render_distance_preset, context.build_config); + const session = try GameSession.init(allocator, render_system.getRHI(), render_system.getAtlas(), seed, render_distance, horizon_distance, lod_enabled, generator_index, context.settings.render_distance_preset, context.build_config); errdefer session.deinit(); const world = session.world.interface(); @@ -93,7 +112,10 @@ pub const WorldScreen = struct { .startup_diagnostic_start_frame = context.time.frame_count, .startup_diagnostic_logged = false, .save_failure_warning_count = world.takeSaveFailureWarningCount(), + .menu_preview = menu_preview, + .menu_preview_center = session.player.position, }; + if (menu_preview) self.applyMenuCamera(); settings_data.clearTerrainDebugViews(context.settings); render_system.getRHI().options().setShadowDebugChannel(@intFromEnum(settings_data.resolveShadowDebugChannel(context.settings))); render_system.getRHI().options().setDebugShadowView(false); @@ -122,7 +144,7 @@ pub const WorldScreen = struct { self.save_failure_warning_count += save_failures; } - if (!benchmark_mode and !automated_capture) { + if (!self.menu_preview and !benchmark_mode and !automated_capture) { if (try self.processControls(now)) return; } @@ -135,20 +157,35 @@ pub const WorldScreen = struct { const cam = &self.session.player.camera; ctx.audio_system.setListener(cam.position, cam.forward, cam.up); - try self.session.update(dt, ctx.time.elapsed, ctx.input, ctx.input_mapper, render_system.getAtlas(), ctx.window_manager.window, false, ctx.skip_world_update, benchmark_mode or automated_capture); + try self.session.update(dt, ctx.time.elapsed, ctx.input, ctx.input_mapper, render_system.getAtlas(), ctx.window_manager.window, false, ctx.skip_world_update, benchmark_mode or automated_capture or self.menu_preview); + if (self.menu_preview) self.applyMenuCamera(); render_system.getCloudSystem().step(dt); const world_telemetry = self.world.telemetry(); - if (world_telemetry.getRenderDistance() != ctx.settings.render_distance) { - world_telemetry.setRenderDistance(ctx.settings.render_distance); - } - if (world_telemetry.getHorizonDistance() != ctx.settings.horizon_distance) { - world_telemetry.setHorizonDistance(ctx.settings.horizon_distance); + if (!self.menu_preview) { + if (world_telemetry.getRenderDistance() != ctx.settings.render_distance) { + world_telemetry.setRenderDistance(ctx.settings.render_distance); + } + if (world_telemetry.getHorizonDistance() != ctx.settings.horizon_distance) { + world_telemetry.setHorizonDistance(ctx.settings.horizon_distance); + } } self.maybeLogStartupDiagnostic(now); } + fn applyMenuCamera(self: *@This()) void { + const angle = self.context.time.elapsed * 0.002 + 0.72; + const radius: f32 = 32.0; + const camera = &self.session.player.camera; + camera.position = Vec3.init( + self.menu_preview_center.x + std.math.cos(angle) * radius, + self.menu_preview_center.y + 16.0, + self.menu_preview_center.z + std.math.sin(angle) * radius, + ); + camera.setYawPitch(angle + std.math.pi, -0.28); + } + fn processControls(self: *@This(), now: f32) !bool { const ctx = self.context; const render_system = ctx.render_system; @@ -458,8 +495,8 @@ pub const WorldScreen = struct { .fxaa_enabled = ctx.settings.fxaa_enabled and !ctx.settings.taa_enabled, .bloom_enabled = ctx.settings.bloom_enabled and !startup_light_render, .resolution_scale = resolution_scale, - .overlay_renderer = if (clean_capture) null else renderOverlay, - .overlay_ctx = if (clean_capture) null else self, + .overlay_renderer = if (clean_capture or self.menu_preview) null else renderOverlay, + .overlay_ctx = if (clean_capture or self.menu_preview) null else self, .shadow_caster_renderer = renderEntityShadowCasters, .shadow_caster_ctx = self, .cached_cascades = &frame_cascades, @@ -485,7 +522,7 @@ pub const WorldScreen = struct { const mouse_clicked = ctx.input.isMouseButtonPressed(.left); const hud_clicked = if (self.debug_ui.menuEnabled()) false else mouse_clicked; - if (!clean_capture) { + if (!clean_capture and !self.menu_preview) { try self.session.drawHUD(ui, render_system.getAtlas(), render_system.getResourcePackManager().active_pack, ctx.time.fps, screen_w, screen_h, mouse_x, mouse_y, hud_clicked); } diff --git a/modules/game-ui/src/screens/world_list.zig b/modules/game-ui/src/screens/world_list.zig index 17aeced1..ae278b5e 100644 --- a/modules/game-ui/src/screens/world_list.zig +++ b/modules/game-ui/src/screens/world_list.zig @@ -236,6 +236,14 @@ pub const WorldListScreen = struct { if (self.confirm_rename and self.rename_focused) { try text_input.handleTextTyping(&self.rename_buffer, self.context.allocator, self.context.input, 32); } + if (!self.confirm_delete and !self.confirm_clear_all and !self.confirm_rename and self.worlds.len > 0) { + const input = self.context.input; + if (input.isKeyPressed(.down)) self.selected = if (self.selected) |idx| (idx + 1) % self.worlds.len else 0; + if (input.isKeyPressed(.up)) self.selected = if (self.selected) |idx| if (idx == 0) self.worlds.len - 1 else idx - 1 else self.worlds.len - 1; + if (self.context.input_mapper.isActionPressed(input, .ui_confirm)) { + if (self.selected) |idx| try self.loadWorld(idx); + } + } } pub fn draw(ptr: *anyopaque, ui: *UISystem) !void { @@ -260,7 +268,7 @@ pub const WorldListScreen = struct { const ph: f32 = @min(screen_h - margin * 2.0, PANEL_HEIGHT_BASE * ui_scale); const px: f32 = (screen_w - pw) * 0.5; const py: f32 = (screen_h - ph) * 0.5; - const shell = Theme.drawShell(ui, .{ .x = px, .y = py, .width = pw, .height = ph }, ui_scale, "SAVES", "WORLDS", "Load, rename, or remove saved worlds."); + const shell = Theme.drawShell(ui, .{ .x = px, .y = py, .width = pw, .height = ph }, ui_scale, "PLAY", "YOUR WORLDS", "Select a save to see details and available actions."); if (self.error_message) |message| { const banner = Rect{ .x = shell.content.x + 12.0 * ui_scale, .y = shell.content.y + 8.0 * ui_scale, .width = shell.content.width - 24.0 * ui_scale, .height = 34.0 * ui_scale }; @@ -274,16 +282,31 @@ pub const WorldListScreen = struct { const count_w = Font.measureTextWidth(count_text, 0.94 * ui_scale); Font.drawText(ui, count_text, shell.rect.x + shell.rect.width - 34.0 * ui_scale - count_w, shell.rect.y + 86.0 * ui_scale, 0.94 * ui_scale, Theme.signal); + const detail_layout = shell.content.width >= 720.0 * ui_scale; + const detail_gap: f32 = 18.0 * ui_scale; + const list_rect = Rect{ + .x = shell.content.x, + .y = shell.content.y, + .width = if (detail_layout) shell.content.width * 0.62 else shell.content.width, + .height = shell.content.height, + }; + const detail_rect = Rect{ + .x = list_rect.x + list_rect.width + detail_gap, + .y = shell.content.y, + .width = shell.content.width - list_rect.width - detail_gap, + .height = shell.content.height, + }; + const scroll_dy = ctx.input.getScrollDelta().y; self.scroll_offset -= scroll_dy * 30.0 * ui_scale; - const list_top: f32 = shell.content.y; - const list_bottom: f32 = shell.content.y + shell.content.height; + const list_top: f32 = list_rect.y; + const list_bottom: f32 = list_rect.y + list_rect.height; const row_h: f32 = 68.0 * ui_scale; const max_scroll = @max(0.0, @as(f32, @floatFromInt(self.worlds.len)) * row_h - (list_bottom - list_top)); self.scroll_offset = @max(0.0, @min(self.scroll_offset, max_scroll)); - Theme.drawListRail(ui, shell.content, ui_scale); - Theme.drawScrollbar(ui, shell.content.x + shell.content.width - 12.0 * ui_scale, list_top + 12.0 * ui_scale, shell.content.height - 24.0 * ui_scale, @as(f32, @floatFromInt(self.worlds.len)) * row_h, shell.content.height, self.scroll_offset, max_scroll, ui_scale); + Theme.drawListRail(ui, list_rect, ui_scale); + Theme.drawScrollbar(ui, list_rect.x + list_rect.width - 12.0 * ui_scale, list_top + 12.0 * ui_scale, list_rect.height - 24.0 * ui_scale, @as(f32, @floatFromInt(self.worlds.len)) * row_h, list_rect.height, self.scroll_offset, max_scroll, ui_scale); if (self.worlds.len == 0) { Font.drawTextCentered(ui, "NO SAVED WORLDS FOUND", screen_w * 0.5, list_top + (list_bottom - list_top) * 0.40, 1.35 * ui_scale, Theme.title); @@ -295,7 +318,7 @@ pub const WorldListScreen = struct { const ry: f32 = list_top + @as(f32, @floatFromInt(i)) * row_h - self.scroll_offset; if (ry + row_h < list_top or ry > list_bottom) continue; const world = self.worlds[i]; - const row_rect = Rect{ .x = shell.content.x + 16.0 * ui_scale, .y = ry, .width = shell.content.width - 42.0 * ui_scale, .height = row_h - 8.0 * ui_scale }; + const row_rect = Rect{ .x = list_rect.x + 16.0 * ui_scale, .y = ry, .width = list_rect.width - 42.0 * ui_scale, .height = row_h - 8.0 * ui_scale }; const row_hovered = row_rect.contains(mx, my); const is_selected = self.selected == i; if (mc and row_hovered and !modal_open) { @@ -307,11 +330,13 @@ pub const WorldListScreen = struct { var desc_buf: [128]u8 = undefined; const description = std.fmt.bufPrint(&desc_buf, "{s} // LAST PLAYED: {s}", .{ seed_text, last_text }) catch seed_text; Theme.drawOptionRow(ui, row_rect, world.name, description, 1.28 * ui_scale, is_selected or row_hovered, ui_scale); - if (is_selected) { + if (is_selected and !detail_layout) { Theme.drawValueText(ui, .{ .x = row_rect.x + row_rect.width - 136.0 * ui_scale, .y = row_rect.y + 12.0 * ui_scale, .width = 118.0 * ui_scale, .height = 36.0 * ui_scale }, "SELECTED", 0.72 * ui_scale, ui_scale); } } + if (detail_layout) drawWorldDetails(ui, detail_rect, self, ui_scale); + const btn_h: f32 = 46.0 * ui_scale; const byy: f32 = shell.footer_y; const btn_gap: f32 = 10.0 * ui_scale; @@ -319,17 +344,19 @@ pub const WorldListScreen = struct { const bx_base: f32 = shell.content.x; const btn_scale: f32 = 0.98 * ui_scale; - if (Theme.drawButton(ui, .{ .x = bx_base, .y = byy, .width = btn_w, .height = btn_h }, "BACK", btn_scale, mx, my, mc, .ghost, ui_scale)) { + if (Theme.drawButton(ui, .{ .x = bx_base, .y = byy, .width = if (detail_layout) 150.0 * ui_scale else btn_w, .height = btn_h }, "BACK", btn_scale, mx, my, mc, .ghost, ui_scale)) { ctx.screen_manager.popScreen(); } const load_enabled = self.selected != null and !modal_open; - if (Theme.drawButton(ui, .{ .x = bx_base + btn_w + btn_gap, .y = byy, .width = btn_w, .height = btn_h }, "LOAD", btn_scale, mx, my, mc, if (load_enabled) .primary else .disabled, ui_scale)) { + const load_rect = if (detail_layout) Rect{ .x = detail_rect.x + 18.0 * ui_scale, .y = detail_rect.y + detail_rect.height - 168.0 * ui_scale, .width = detail_rect.width - 36.0 * ui_scale, .height = 44.0 * ui_scale } else Rect{ .x = bx_base + btn_w + btn_gap, .y = byy, .width = btn_w, .height = btn_h }; + if (Theme.drawButton(ui, load_rect, "PLAY WORLD", btn_scale, mx, my, mc, if (load_enabled) .primary else .disabled, ui_scale)) { if (self.selected) |idx| try self.loadWorld(idx); } const rename_enabled = self.selected != null and !modal_open; - if (Theme.drawButton(ui, .{ .x = bx_base + 2.0 * (btn_w + btn_gap), .y = byy, .width = btn_w, .height = btn_h }, "RENAME", btn_scale, mx, my, mc, if (rename_enabled) .secondary else .disabled, ui_scale)) { + const rename_rect = if (detail_layout) Rect{ .x = detail_rect.x + 18.0 * ui_scale, .y = detail_rect.y + detail_rect.height - 114.0 * ui_scale, .width = (detail_rect.width - 46.0 * ui_scale) * 0.5, .height = 42.0 * ui_scale } else Rect{ .x = bx_base + 2.0 * (btn_w + btn_gap), .y = byy, .width = btn_w, .height = btn_h }; + if (Theme.drawButton(ui, rename_rect, "RENAME", btn_scale, mx, my, mc, if (rename_enabled) .secondary else .disabled, ui_scale)) { if (self.selected) |idx| { self.rename_buffer.clearRetainingCapacity(); try self.rename_buffer.appendSlice(self.context.allocator, self.worlds[idx].name); @@ -339,12 +366,14 @@ pub const WorldListScreen = struct { } const del_enabled = self.selected != null and !modal_open; - if (Theme.drawButton(ui, .{ .x = bx_base + 3.0 * (btn_w + btn_gap), .y = byy, .width = btn_w, .height = btn_h }, "DELETE", btn_scale, mx, my, mc, if (del_enabled) .danger else .disabled, ui_scale)) { + const delete_rect = if (detail_layout) Rect{ .x = rename_rect.x + rename_rect.width + 10.0 * ui_scale, .y = rename_rect.y, .width = rename_rect.width, .height = rename_rect.height } else Rect{ .x = bx_base + 3.0 * (btn_w + btn_gap), .y = byy, .width = btn_w, .height = btn_h }; + if (Theme.drawButton(ui, delete_rect, "DELETE", btn_scale, mx, my, mc, if (del_enabled) .danger else .disabled, ui_scale)) { self.confirm_delete = true; } const clear_enabled = self.worlds.len > 0 and !modal_open; - if (Theme.drawButton(ui, .{ .x = bx_base + 4.0 * (btn_w + btn_gap), .y = byy, .width = btn_w, .height = btn_h }, "CLEAR ALL", btn_scale, mx, my, mc, if (clear_enabled) .danger else .disabled, ui_scale)) { + const clear_rect = if (detail_layout) Rect{ .x = shell.content.x + shell.content.width - 150.0 * ui_scale, .y = byy, .width = 150.0 * ui_scale, .height = btn_h } else Rect{ .x = bx_base + 4.0 * (btn_w + btn_gap), .y = byy, .width = btn_w, .height = btn_h }; + if (Theme.drawButton(ui, clear_rect, "CLEAR ALL", btn_scale, mx, my, mc, if (clear_enabled) .danger else .disabled, ui_scale)) { self.confirm_clear_all = true; } @@ -503,6 +532,24 @@ pub const WorldListScreen = struct { } }; +fn drawWorldDetails(ui: *UISystem, rect: Rect, self: *WorldListScreen, scale: f32) void { + Theme.drawListRail(ui, rect, scale); + Font.drawText(ui, "WORLD DETAILS", rect.x + 18.0 * scale, rect.y + 18.0 * scale, 0.72 * scale, Theme.signal); + ui.drawRect(.{ .x = rect.x + 18.0 * scale, .y = rect.y + 44.0 * scale, .width = rect.width - 36.0 * scale, .height = 1.0 * scale }, Theme.outline); + if (self.selected) |idx| { + const world = self.worlds[idx]; + Font.drawText(ui, world.name, rect.x + 18.0 * scale, rect.y + 64.0 * scale, 1.36 * scale, Theme.title); + Font.drawText(ui, registry.getGeneratorInfo(world.generator_index).name, rect.x + 18.0 * scale, rect.y + 100.0 * scale, 0.82 * scale, Theme.text); + var seed_buf: [64]u8 = undefined; + const seed_text = std.fmt.bufPrint(&seed_buf, "Seed {}", .{world.seed}) catch "Seed unknown"; + Font.drawText(ui, seed_text, rect.x + 18.0 * scale, rect.y + 132.0 * scale, 0.76 * scale, Theme.muted); + Font.drawText(ui, "Use Play World to resume this save.", rect.x + 18.0 * scale, rect.y + 176.0 * scale, 0.72 * scale, Theme.muted); + } else { + Font.drawText(ui, "Select a world", rect.x + 18.0 * scale, rect.y + 68.0 * scale, 1.10 * scale, Theme.title); + Font.drawText(ui, "Its details and actions will appear here.", rect.x + 18.0 * scale, rect.y + 102.0 * scale, 0.70 * scale, Theme.muted); + } +} + fn formatTimestamp(ts: i64) []const u8 { if (ts <= 0) return "NEVER"; const now = std.Io.Clock.real.now(std.Options.debug_io).toMilliseconds(); diff --git a/modules/game-ui/src/settings_ui.zig b/modules/game-ui/src/settings_ui.zig index 6ee460b5..9942eb22 100644 --- a/modules/game-ui/src/settings_ui.zig +++ b/modules/game-ui/src/settings_ui.zig @@ -16,9 +16,9 @@ pub fn drawStepperControl(ui: *UISystem, row: Rect, value: []const u8, value_sca const value_x = right_x - value_w - 8.0 * scale; const left_x = value_x - arrow_w - 8.0 * scale; var result: StepResult = .none; - if (Theme.drawButton(ui, .{ .x = left_x, .y = control_y, .width = arrow_w, .height = control_h }, "<", button_scale, mx, my, clicked, .ghost, scale)) result = .previous; + if (Theme.drawButton(ui, .{ .x = left_x, .y = control_y, .width = arrow_w, .height = control_h }, "-", button_scale, mx, my, clicked, .ghost, scale)) result = .previous; Theme.drawValueText(ui, .{ .x = value_x, .y = control_y, .width = value_w, .height = control_h }, value, value_scale, scale); - if (Theme.drawButton(ui, .{ .x = right_x, .y = control_y, .width = arrow_w, .height = control_h }, ">", button_scale, mx, my, clicked, .ghost, scale)) result = .next; + if (Theme.drawButton(ui, .{ .x = right_x, .y = control_y, .width = arrow_w, .height = control_h }, "+", button_scale, mx, my, clicked, .ghost, scale)) result = .next; return result; } diff --git a/src/game/app.zig b/src/game/app.zig index 770c655e..8303a862 100644 --- a/src/game/app.zig +++ b/src/game/app.zig @@ -117,6 +117,7 @@ pub const App = struct { screenshot_delay_start: ?f32 = null, screenshot_settle_frames: u32 = 0, frame_start_counter: u64 = 0, + reveal_window_when_menu_ready: bool = false, pub fn init(allocator: std.mem.Allocator) !*App { log.log.info("Initializing engine systems...", .{}); @@ -139,6 +140,7 @@ pub const App = struct { applyShadowTestPreset(settings_manager.ptr()); } + const preload_menu_preview = !build_options.skip_present and !build_options.smoke_test and build_options.screenshot_path.len == 0 and !build_options.benchmark and !build_options.shadow_test_scene and resolveAutoWorldGenerator() == null; const initial_window_width: u32 = if (build_options.benchmark) 1920 else settings_manager.settings.window_width; const initial_window_height: u32 = if (build_options.benchmark) 1080 else settings_manager.settings.window_height; log.log.info("App.init: initializing WindowManager ({}x{})", .{ initial_window_width, initial_window_height }); @@ -147,7 +149,7 @@ pub const App = struct { .monitor_name = build_options.monitor_name, .video_driver = build_options.window_video_driver, .no_focus = build_options.window_no_focus, - .hidden = build_options.skip_present, + .hidden = build_options.skip_present or preload_menu_preview, }); errdefer wm.deinit(); @@ -226,6 +228,7 @@ pub const App = struct { .screenshot_delay_start = null, .screenshot_settle_frames = 0, .frame_start_counter = 0, + .reveal_window_when_menu_ready = preload_menu_preview, }; errdefer app.screen_manager.deinit(); @@ -451,6 +454,7 @@ pub const App = struct { try self.ui_manager.draw(&self.screen_manager, self.render_system.getRHI(), world_stats, cpu_ms, self.time.fps); self.render_system.endFrame(); + self.revealMenuWindowWhenReady(); if (build_options.benchmark) { if (self.benchmark_runner) |runner| { @@ -522,6 +526,13 @@ pub const App = struct { self.limitFrameRateIfNeeded(); } + fn revealMenuWindowWhenReady(self: *App) void { + if (!self.reveal_window_when_menu_ready or !self.screen_manager.isReadyForPresentation()) return; + log.log.info("MENU PREVIEW READY: revealing preloaded window", .{}); + self.window_manager.show(); + self.reveal_window_when_menu_ready = false; + } + fn limitFrameRateIfNeeded(self: *App) void { if (build_options.benchmark or build_options.skip_present) return;