Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ pub fn build(b: *std.Build) void {
"src/input/guide_chord.zig",
"src/session/message_protocol.zig",
"src/ui/keyboard.zig",
"src/ui/control_icons.zig",
Comment thread
Producdevity marked this conversation as resolved.
"src/ui/navigation_repeat.zig",
"src/ui/persistent_settings.zig",
"src/ui/stream_dimensions.zig",
Expand Down
Binary file modified packaging/portmaster/greenovercast/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions src/app/release.zig
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ pub const Release = struct {
return c.go_sdl_platform_controller(self.platform);
}

fn drawLoading(self: *Release, heading: [*c]const u8, detail: [*c]const u8, action: [*c]const u8) void {
fn drawLoading(self: *Release, heading: [*c]const u8, detail: [*c]const u8, action: c.GoHandheldUiAction) void {
c.go_handheld_ui_draw_loading(self.ui(), heading, detail, action);
}

Expand All @@ -198,12 +198,12 @@ pub const Release = struct {
}

pub fn refreshAuth(self: *Release) Result {
self.drawLoading("SIGNING IN", "REFRESHING XBOX SESSION", null);
self.drawLoading("SIGNING IN", "REFRESHING XBOX SESSION", c.GO_HANDHELD_UI_ACTION_NONE);
debug("Refreshing auth\n", .{});
return switch (c.go_xbox_auth_refresh(self.auth)) {
c.GO_XBOX_AUTH_OK => .ok,
c.GO_XBOX_AUTH_REAUTH_REQUIRED => result: {
self.drawLoading("SIGN IN EXPIRED", "REQUESTING A NEW DEVICE CODE", null);
self.drawLoading("SIGN IN EXPIRED", "REQUESTING A NEW DEVICE CODE", c.GO_HANDHELD_UI_ACTION_NONE);
c.SDL_Delay(700);
break :result .reauth_required;
},
Expand Down Expand Up @@ -231,7 +231,7 @@ pub const Release = struct {
std.debug.print("Catalog service could not be initialized\n", .{});
return .failed;
};
self.drawLoading("LOADING LIBRARY", "CHECKING YOUR CLOUD GAMES", "B BACK");
self.drawLoading("LOADING LIBRARY", "CHECKING YOUR CLOUD GAMES", c.GO_HANDHELD_UI_ACTION_BACK);
debug("Loading cloud catalog\n", .{});
const result = self.catalog.?.load() catch |err| {
if (err == error.EmptyCatalog)
Expand Down Expand Up @@ -261,7 +261,7 @@ pub const Release = struct {
}

pub fn signOut(self: *Release) Result {
self.drawLoading("SIGNING OUT", "REMOVING XBOX CREDENTIALS", null);
self.drawLoading("SIGNING OUT", "REMOVING XBOX CREDENTIALS", c.GO_HANDHELD_UI_ACTION_NONE);
if (c.go_xbox_auth_sign_out(self.auth) != 0) {
std.debug.print("Xbox credentials could not be removed\n", .{});
return .failed;
Expand All @@ -274,7 +274,7 @@ pub const Release = struct {

pub fn createSession(self: *Release) Result {
if (self.title_id[0] == 0) return .failed;
self.drawLoading("STARTING GAME", "ALLOCATING CLOUD SESSION", "B CANCEL");
self.drawLoading("STARTING GAME", "ALLOCATING CLOUD SESSION", c.GO_HANDHELD_UI_ACTION_CANCEL);
debug("Creating session ({s})\n", .{std.mem.sliceTo(&self.title_id, 0)});
if (c.go_cloud_session_start_game(self.cloud, @ptrCast(&self.title_id)) < 0) {
std.debug.print("Session creation failed\n", .{});
Expand Down
4 changes: 2 additions & 2 deletions src/auth/xbox_auth.zig
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ fn deviceSignIn(auth: *Auth, ui: *c.GoHandheldUi) !bool {
.{ .key = "client_id", .value = cString(&auth.client_id) },
.{ .key = "scope", .value = oauth_scope },
});
c.go_handheld_ui_draw_loading(ui, "XBOX SIGN IN", "REQUESTING A DEVICE CODE", "B BACK");
c.go_handheld_ui_draw_loading(ui, "XBOX SIGN IN", "REQUESTING A DEVICE CODE", c.GO_HANDHELD_UI_ACTION_BACK);
var response = c.go_http_request(
"POST",
device_code_url,
Expand Down Expand Up @@ -248,7 +248,7 @@ fn deviceSignIn(auth: *Auth, ui: *c.GoHandheldUi) !bool {
ui,
"SIGNED IN",
"OPENING YOUR CLOUD LIBRARY",
null,
c.GO_HANDHELD_UI_ACTION_NONE,
);
c.SDL_Delay(700);
return true;
Expand Down
2 changes: 1 addition & 1 deletion src/catalog/service.zig
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ pub const Service = struct {
self.uiHandle(),
"LOADING LIBRARY",
progress.ptr,
"B BACK",
c.GO_HANDHELD_UI_ACTION_BACK,
);
if (c.go_handheld_ui_cancel_requested(self.uiHandle()) != 0) return;
}
Expand Down
294 changes: 294 additions & 0 deletions src/ui/control_icons.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
const font = @import("pixel_font.zig");
const settings = @import("persistent_settings.zig");
const style = @import("view_style.zig");

const c = @cImport({
@cInclude("SDL2/SDL.h");
});

pub const Face = enum {
a,
b,
x,
y,
};

pub const Icon = enum {
a,
b,
x,
y,
dpad,
left_stick,
right_stick,
left_bumper,
right_bumper,
left_trigger,
right_trigger,
start,
select,
guide,
};

pub const Prompt = struct {
icons: [2]Icon,
icon_count: u8,
label: [*:0]const u8,

pub fn one(icon: Icon, label: [*:0]const u8) Prompt {
return .{ .icons = .{ icon, icon }, .icon_count = 1, .label = label };
}

pub fn two(first: Icon, second: Icon, label: [*:0]const u8) Prompt {
return .{ .icons = .{ first, second }, .icon_count = 2, .label = label };
}
};

const label_scale = 2;
const icon_gap = 3;
const label_gap = 5;
const prompt_gap = 12;

pub fn face(mode: settings.FaceButtonMode, semantic: Face) Icon {
return switch (mode) {
.system => switch (semantic) {
.a => .a,
.b => .b,
.x => .x,
.y => .y,
},
.swapped => switch (semantic) {
.a => .b,
.b => .a,
.x => .y,
.y => .x,
},
};
}

pub fn rowWidth(prompts: []const Prompt) c_int {
var width: c_int = 0;
for (prompts, 0..) |prompt, index| {
width += promptWidth(prompt);
if (index + 1 < prompts.len) width += prompt_gap;
}
return width;
}

pub fn drawRow(
renderer_pointer: *anyopaque,
initial_x: c_int,
y: c_int,
prompts: []const Prompt,
color: style.Color,
) void {
var x = initial_x;
for (prompts, 0..) |prompt, index| {
drawPrompt(renderer_pointer, x, y, prompt, color);
x += promptWidth(prompt);
if (index + 1 < prompts.len) x += prompt_gap;
}
}

pub fn drawCenteredRow(
renderer_pointer: *anyopaque,
y: c_int,
prompts: []const Prompt,
color: style.Color,
) void {
drawRow(
renderer_pointer,
@divTrunc(style.display_width - rowWidth(prompts), 2),
y,
prompts,
color,
);
}

pub fn drawIcon(
renderer_pointer: *anyopaque,
x: c_int,
y: c_int,
icon: Icon,
color: style.Color,
) void {
const renderer: *c.SDL_Renderer = @ptrCast(@alignCast(renderer_pointer));
style.setColor(renderer, color);
switch (icon) {
.a, .b, .x, .y => drawFace(renderer, x, y, icon, color),
.dpad => drawDpad(renderer, x, y),
.left_stick, .right_stick => drawStick(renderer, x, y, icon, color),
.guide => drawGuide(renderer, x, y),
else => drawPill(renderer, x, y, icon, color),
}
}

pub fn drawSwapPair(
renderer_pointer: *anyopaque,
x: c_int,
y: c_int,
first: Icon,
second: Icon,
color: style.Color,
) void {
const renderer: *c.SDL_Renderer = @ptrCast(@alignCast(renderer_pointer));
drawIcon(renderer, x, y, first, color);
drawIcon(renderer, x + 56, y, second, color);
style.setColor(renderer, color);
fill(renderer, x + 25, y + 8, 24, 2);
fill(renderer, x + 25, y + 6, 2, 6);
fill(renderer, x + 47, y + 6, 2, 6);
fill(renderer, x + 27, y + 4, 2, 2);
fill(renderer, x + 27, y + 12, 2, 2);
fill(renderer, x + 45, y + 4, 2, 2);
fill(renderer, x + 45, y + 12, 2, 2);
}

fn promptWidth(prompt: Prompt) c_int {
var width: c_int = 0;
for (0..prompt.icon_count) |index| {
width += iconWidth(prompt.icons[index]);
if (index + 1 < prompt.icon_count) width += icon_gap;
}
if (prompt.label[0] != 0) width += label_gap + font.textWidth(prompt.label, label_scale);
return width;
}

fn drawPrompt(
renderer_pointer: *anyopaque,
initial_x: c_int,
y: c_int,
prompt: Prompt,
color: style.Color,
) void {
var x = initial_x;
for (0..prompt.icon_count) |index| {
const icon = prompt.icons[index];
drawIcon(renderer_pointer, x, y, icon, color);
x += iconWidth(icon);
if (index + 1 < prompt.icon_count) x += icon_gap;
}
if (prompt.label[0] == 0) return;
font.text(renderer_pointer, x + label_gap, y + 2, label_scale, prompt.label, color);
}

fn iconWidth(icon: Icon) c_int {
return switch (icon) {
.a, .b, .x, .y, .dpad, .left_stick, .right_stick, .guide => 18,
.left_bumper, .right_bumper, .left_trigger, .right_trigger => 24,
.start => 37,
.select => 43,
};
}

fn drawFace(
renderer: *c.SDL_Renderer,
x: c_int,
y: c_int,
icon: Icon,
color: style.Color,
) void {
drawCircle(renderer, x, y);
font.text(renderer, x + 6, y + 5, 1, iconLabel(icon), color);
}

fn drawCircle(renderer: *c.SDL_Renderer, x: c_int, y: c_int) void {
fill(renderer, x + 5, y, 8, 2);
fill(renderer, x + 2, y + 2, 3, 3);
fill(renderer, x + 13, y + 2, 3, 3);
fill(renderer, x, y + 5, 2, 8);
fill(renderer, x + 16, y + 5, 2, 8);
fill(renderer, x + 2, y + 13, 3, 3);
fill(renderer, x + 13, y + 13, 3, 3);
fill(renderer, x + 5, y + 16, 8, 2);
}

fn drawDpad(renderer: *c.SDL_Renderer, x: c_int, y: c_int) void {
fill(renderer, x + 6, y, 6, 6);
fill(renderer, x, y + 6, 18, 6);
fill(renderer, x + 6, y + 12, 6, 6);
}

fn drawStick(
renderer: *c.SDL_Renderer,
x: c_int,
y: c_int,
icon: Icon,
color: style.Color,
) void {
drawCircle(renderer, x, y);
font.text(renderer, x + 4, y + 5, 1, iconLabel(icon), color);
}

fn drawGuide(renderer: *c.SDL_Renderer, x: c_int, y: c_int) void {
drawCircle(renderer, x, y);
fill(renderer, x + 5, y + 5, 3, 3);
fill(renderer, x + 10, y + 5, 3, 3);
fill(renderer, x + 7, y + 8, 4, 3);
fill(renderer, x + 5, y + 11, 3, 3);
fill(renderer, x + 10, y + 11, 3, 3);
}

fn drawPill(
renderer: *c.SDL_Renderer,
x: c_int,
y: c_int,
icon: Icon,
color: style.Color,
) void {
const width = iconWidth(icon);
fill(renderer, x + 2, y, width - 4, 2);
fill(renderer, x, y + 2, 2, 14);
fill(renderer, x + width - 2, y + 2, 2, 14);
fill(renderer, x + 2, y + 16, width - 4, 2);
const label = iconLabel(icon);
const width_label = font.textWidth(label, 1);
font.text(renderer, x + @divTrunc(width - width_label, 2), y + 5, 1, label, color);
}

fn iconLabel(icon: Icon) [*:0]const u8 {
return switch (icon) {
.a => "A",
.b => "B",
.x => "X",
.y => "Y",
.left_stick => "L3",
.right_stick => "R3",
.left_bumper => "LB",
.right_bumper => "RB",
.left_trigger => "LT",
.right_trigger => "RT",
.start => "START",
.select => "SELECT",
.dpad, .guide => "",
};
}

fn fill(renderer: *c.SDL_Renderer, x: c_int, y: c_int, width: c_int, height: c_int) void {
var rect = c.SDL_Rect{ .x = x, .y = y, .w = width, .h = height };
_ = c.SDL_RenderFillRect(renderer, &rect);
}

test "swapped face buttons describe the physical button" {
try @import("std").testing.expectEqual(Icon.a, face(.system, .a));
try @import("std").testing.expectEqual(Icon.b, face(.swapped, .a));
try @import("std").testing.expectEqual(Icon.a, face(.swapped, .b));
try @import("std").testing.expectEqual(Icon.y, face(.swapped, .x));
try @import("std").testing.expectEqual(Icon.x, face(.swapped, .y));
}

test "library prompts fit the display" {
const primary = [_]Prompt{
Prompt.one(.a, "PLAY"),
Prompt.one(.y, "FAVORITE"),
Prompt.one(.x, "SEARCH"),
Prompt.one(.b, "BACK"),
};
const secondary = [_]Prompt{
Prompt.two(.left_bumper, .right_bumper, "TAB"),
Prompt.two(.left_trigger, .right_trigger, "LETTER"),
Prompt.one(.start, "SETTINGS"),
};
try @import("std").testing.expect(rowWidth(&primary) <= style.display_width - 32);
try @import("std").testing.expect(rowWidth(&secondary) <= style.display_width - 32);
}
Loading
Loading