+6
.prettierrc
+6
.prettierrc
+55
.vscode/settings.json
+55
.vscode/settings.json
···
1
+
{
2
+
"zig.zls.enabled": "on",
3
+
// You can add more Zig and ZLS options here
4
+
5
+
// Whether to enable build-on-save diagnostics
6
+
//
7
+
// Further information about build-on save:
8
+
// https://zigtools.org/zls/guides/build-on-save/
9
+
// "zig.zls.enableBuildOnSave": true,
10
+
11
+
// All nested settings will only affect Zig files.
12
+
"[zig]": {
13
+
// The Zig FAQ answers some common questions about Zig's formatter (`zig fmt`)
14
+
// https://github.com/ziglang/zig/wiki/FAQ
15
+
//
16
+
"editor.formatOnSave": true,
17
+
18
+
"editor.inlayHints.enabled": "on",
19
+
20
+
// overwrite words when accepting completions
21
+
22
+
"editor.suggest.insertMode": "replace",
23
+
"editor.codeActionsOnSave": {
24
+
// Run code actions that currently supports adding and removing discards.
25
+
// "source.fixAll": "explicit",
26
+
// Run code actions that sorts @import declarations.
27
+
// "source.organizeImports": "explicit",
28
+
}
29
+
},
30
+
"editor.semanticTokenColorCustomizations": {
31
+
"rules": {
32
+
"*.deprecated": {
33
+
// highlight semantic tokens that are marked as "deprecated"
34
+
"strikethrough": true
35
+
}
36
+
}
37
+
},
38
+
"cSpell.words": [
39
+
"anyopaque",
40
+
"callconv",
41
+
"comptime",
42
+
"linenum",
43
+
"memcpy",
44
+
"orelse",
45
+
"pdapi",
46
+
"pdtools",
47
+
"playdate",
48
+
"repost",
49
+
"reposted",
50
+
"stringval",
51
+
"Sublist",
52
+
"Trunc",
53
+
"usize"
54
+
]
55
+
}
+7
-1
README.md
+7
-1
README.md
assets/fonts/Roobert-10-Bold.pft
assets/fonts/Roobert-10-Bold.pft
This is a binary file and will not be displayed.
assets/fonts/Roobert-9-Mono-Condensed-table-8-14.png
assets/fonts/Roobert-9-Mono-Condensed-table-8-14.png
This is a binary file and will not be displayed.
+95
assets/fonts/Roobert-9-Mono-Condensed.fnt
+95
assets/fonts/Roobert-9-Mono-Condensed.fnt
···
1
+
space 8
2
+
! 8
3
+
" 8
4
+
# 8
5
+
$ 8
6
+
% 8
7
+
& 8
8
+
' 8
9
+
( 8
10
+
) 8
11
+
* 8
12
+
+ 8
13
+
, 8
14
+
- 8
15
+
. 8
16
+
/ 8
17
+
0 8
18
+
1 8
19
+
2 8
20
+
3 8
21
+
4 8
22
+
5 8
23
+
6 8
24
+
7 8
25
+
8 8
26
+
9 8
27
+
: 8
28
+
; 8
29
+
< 8
30
+
= 8
31
+
> 8
32
+
? 8
33
+
@ 8
34
+
A 8
35
+
B 8
36
+
C 8
37
+
D 8
38
+
E 8
39
+
F 8
40
+
G 8
41
+
H 8
42
+
I 8
43
+
J 8
44
+
K 8
45
+
L 8
46
+
M 8
47
+
N 8
48
+
O 8
49
+
P 8
50
+
Q 8
51
+
R 8
52
+
S 8
53
+
T 8
54
+
U 8
55
+
V 8
56
+
W 8
57
+
X 8
58
+
Y 8
59
+
Z 8
60
+
[ 8
61
+
\ 8
62
+
] 8
63
+
^ 8
64
+
_ 8
65
+
` 8
66
+
a 8
67
+
b 8
68
+
c 8
69
+
d 8
70
+
e 8
71
+
f 8
72
+
g 8
73
+
h 8
74
+
i 8
75
+
j 8
76
+
k 8
77
+
l 8
78
+
m 8
79
+
n 8
80
+
o 8
81
+
p 8
82
+
q 8
83
+
r 8
84
+
s 8
85
+
t 8
86
+
u 8
87
+
v 8
88
+
w 8
89
+
x 8
90
+
y 8
91
+
z 8
92
+
{ 8
93
+
| 8
94
+
} 8
95
+
~ 8
+217
build.zig
+217
build.zig
···
1
+
const std = @import("std");
2
+
const builtin = @import("builtin");
3
+
4
+
const name = "bluesky";
5
+
pub fn build(b: *std.Build) !void {
6
+
const pdx_file_name = name ++ ".pdx";
7
+
const optimize = b.standardOptimizeOption(.{});
8
+
9
+
const writer = b.addWriteFiles();
10
+
const source_dir = writer.getDirectory();
11
+
writer.step.name = "write source directory";
12
+
13
+
const FORCE_COMPILE_M1_MAC = false;
14
+
const supported_targets = [_]std.Build.ResolvedTarget{
15
+
host_or_cross_target(
16
+
b,
17
+
.{
18
+
.abi = .msvc,
19
+
.os_tag = .windows,
20
+
.cpu_arch = .x86_64,
21
+
},
22
+
false,
23
+
),
24
+
host_or_cross_target(
25
+
b,
26
+
.{
27
+
.abi = .none,
28
+
.os_tag = .macos,
29
+
.cpu_arch = .aarch64,
30
+
},
31
+
FORCE_COMPILE_M1_MAC,
32
+
),
33
+
host_or_cross_target(
34
+
b,
35
+
.{
36
+
.abi = .gnu,
37
+
.os_tag = .linux,
38
+
.cpu_arch = .x86_64,
39
+
},
40
+
false,
41
+
),
42
+
};
43
+
for (supported_targets) |target| {
44
+
try compile_simulator_binary(b, optimize, target, writer);
45
+
}
46
+
47
+
const playdate_target = b.resolveTargetQuery(try std.Target.Query.parse(.{
48
+
.arch_os_abi = "thumb-freestanding-eabihf",
49
+
.cpu_features = "cortex_m7+vfp4d16sp",
50
+
}));
51
+
const elf = b.addExecutable(.{
52
+
.name = "pdex.elf",
53
+
.root_source_file = b.path("src/main.zig"),
54
+
.target = playdate_target,
55
+
.optimize = optimize,
56
+
.pic = true,
57
+
.single_threaded = true,
58
+
});
59
+
elf.link_emit_relocs = true;
60
+
elf.entry = .{ .symbol_name = "eventHandler" };
61
+
62
+
elf.setLinkerScript(b.path("link_map.ld"));
63
+
if (optimize == .ReleaseFast) {
64
+
elf.root_module.omit_frame_pointer = true;
65
+
}
66
+
_ = writer.addCopyFile(elf.getEmittedBin(), "pdex.elf");
67
+
_ = writer.addCopyFile(b.path("pdxinfo"), "pdxinfo");
68
+
69
+
try addCopyDirectory(writer, "assets", "./assets");
70
+
71
+
const playdate_sdk_path = try std.process.getEnvVarOwned(b.allocator, "PLAYDATE_SDK_PATH");
72
+
const pdc_path = b.pathJoin(&.{ playdate_sdk_path, "bin", if (builtin.os.tag == .windows) "pdc.exe" else "pdc" });
73
+
const pd_simulator_path = switch (builtin.os.tag) {
74
+
.linux => b.pathJoin(&.{ playdate_sdk_path, "bin", "PlaydateSimulator" }),
75
+
.macos => "open", // `open` focuses the window, while running the simulator directry doesn't.
76
+
.windows => b.pathJoin(&.{ playdate_sdk_path, "bin", "PlaydateSimulator.exe" }),
77
+
else => @panic("Unsupported OS"),
78
+
};
79
+
80
+
const pdc = b.addSystemCommand(&.{pdc_path});
81
+
pdc.addDirectoryArg(source_dir);
82
+
pdc.setName("pdc");
83
+
const pdx = pdc.addOutputFileArg(pdx_file_name);
84
+
85
+
b.installDirectory(.{
86
+
.source_dir = pdx,
87
+
.install_dir = .prefix,
88
+
.install_subdir = pdx_file_name,
89
+
});
90
+
b.installDirectory(.{
91
+
.source_dir = source_dir,
92
+
.install_dir = .prefix,
93
+
.install_subdir = "pdx_source_dir",
94
+
});
95
+
96
+
const run_cmd = b.addSystemCommand(&.{pd_simulator_path});
97
+
run_cmd.addDirectoryArg(pdx);
98
+
run_cmd.setName("PlaydateSimulator");
99
+
const run_step = b.step("run", "Run the app");
100
+
run_step.dependOn(&run_cmd.step);
101
+
run_step.dependOn(b.getInstallStep());
102
+
103
+
const clean_step = b.step("clean", "Clean all artifacts");
104
+
clean_step.dependOn(&b.addRemoveDirTree(b.path("zig-out")).step);
105
+
if (builtin.os.tag != .windows) {
106
+
//Removing zig-cache from the Zig build script does not work on Windows: https://github.com/ziglang/zig/issues/9216
107
+
clean_step.dependOn(&b.addRemoveDirTree(b.path("zig-cache")).step);
108
+
clean_step.dependOn(&b.addRemoveDirTree(b.path(".zig-cache")).step);
109
+
}
110
+
111
+
// Add test step
112
+
const test_step = b.step("test", "Run unit tests");
113
+
114
+
// Create test executables for each test file
115
+
const test_files = [_][]const u8{
116
+
"src/test_memory.zig",
117
+
"src/test_json_parser.zig",
118
+
"src/test_network.zig",
119
+
};
120
+
121
+
for (test_files) |test_file| {
122
+
const test_exe = b.addTest(.{
123
+
.root_source_file = b.path(test_file),
124
+
.optimize = optimize,
125
+
});
126
+
127
+
const run_test = b.addRunArtifact(test_exe);
128
+
test_step.dependOn(&run_test.step);
129
+
}
130
+
}
131
+
132
+
//The purpose of this function is a result of:
133
+
// 1) This script supports cross-compiling PDX's that work on Mac, Windows or Linux without having
134
+
// to compile on those OS's.
135
+
//
136
+
// 2) Inside of a PDX, there can only be 1 pdex executable per OS regardless of the CPU architecture.
137
+
// This has unexpected consequences where, say, a given PDX file can only work on M1 Macs,
138
+
// but not Intel ones. Or, vice versa.
139
+
//
140
+
// So, in the build() function above, I hardcoded ".cpu_arch = .aarch64", which is for M1 Macs.
141
+
// What this means is that if you compiling your game on, say, Windows, it will generate a .pdx
142
+
// that will only work on M1 Macs, but not Intel Macs.
143
+
// BUT, cruicially, if you compiling your game on an Intel Mac, the resulting PDX will work
144
+
// on Intel Macs, but not M1 Macs. Without this function, the game would fail
145
+
// to run on the machine your compiling the code on (Intel Mac), which I'd like to avoid.
146
+
fn host_or_cross_target(
147
+
b: *std.Build,
148
+
cross_target: std.Target.Query,
149
+
force_use_cross_target: bool,
150
+
) std.Build.ResolvedTarget {
151
+
const result =
152
+
if (!force_use_cross_target and b.graph.host.result.os.tag == cross_target.os_tag.?)
153
+
b.graph.host
154
+
else
155
+
b.resolveTargetQuery(cross_target);
156
+
return result;
157
+
}
158
+
159
+
fn compile_simulator_binary(
160
+
b: *std.Build,
161
+
optimize: std.builtin.OptimizeMode,
162
+
target: std.Build.ResolvedTarget,
163
+
writer: *std.Build.Step.WriteFile,
164
+
) !void {
165
+
const os_tag = target.result.os.tag;
166
+
const lib = b.addSharedLibrary(.{
167
+
.name = "pdex",
168
+
.root_source_file = b.path("src/main.zig"),
169
+
.optimize = optimize,
170
+
.target = target,
171
+
});
172
+
const pdex_extension = switch (os_tag) {
173
+
.windows => "dll",
174
+
.macos => "dylib",
175
+
.linux => "so",
176
+
else => @panic("Unsupported OS"),
177
+
};
178
+
const pdex_filename = try std.fmt.allocPrint(b.allocator, "pdex.{s}", .{pdex_extension});
179
+
_ = writer.addCopyFile(lib.getEmittedBin(), pdex_filename);
180
+
181
+
if (os_tag == .windows) {
182
+
_ = writer.addCopyFile(lib.getEmittedPdb(), "pdex.pdb");
183
+
}
184
+
}
185
+
186
+
fn addCopyDirectory(
187
+
wf: *std.Build.Step.WriteFile,
188
+
src_path: []const u8,
189
+
dest_path: []const u8,
190
+
) !void {
191
+
const b = wf.step.owner;
192
+
var dir = try b.build_root.handle.openDir(
193
+
src_path,
194
+
.{ .iterate = true },
195
+
);
196
+
defer dir.close();
197
+
var it = dir.iterate();
198
+
while (try it.next()) |entry| {
199
+
const new_src_path = b.pathJoin(&.{ src_path, entry.name });
200
+
const new_dest_path = b.pathJoin(&.{ dest_path, entry.name });
201
+
const new_src = b.path(new_src_path);
202
+
switch (entry.kind) {
203
+
.file => {
204
+
_ = wf.addCopyFile(new_src, new_dest_path);
205
+
},
206
+
.directory => {
207
+
try addCopyDirectory(
208
+
wf,
209
+
new_src_path,
210
+
new_dest_path,
211
+
);
212
+
},
213
+
//TODO: possible support for sym links?
214
+
else => {},
215
+
}
216
+
}
217
+
}
+110
link_map.ld
+110
link_map.ld
···
1
+
/*
2
+
NOTE(Daniel Bokser): It seems that due to the way the Zig now invokes the linker,
3
+
the entry point name must be specified in build.zig. Thus, the ENTRY() call here is no longer needed.
4
+
5
+
ENTRY(eventHandler)
6
+
*/
7
+
8
+
/*
9
+
NOTE(Daniel Bokser): This GROUP line is from the PlaydateSDK. This doesn't seem to be required for Zig.
10
+
But, leaving it here just in case...
11
+
12
+
GROUP(libgcc.a libc.a libm.a)
13
+
*/
14
+
15
+
/*
16
+
NOTE(Daniel Bokser): Had to add this PHDRS contstruct in to force however Zig invokes the linker
17
+
to make sure there is only one combined ELF segment, which is what PDC seems to expect.
18
+
*/
19
+
PHDRS
20
+
{
21
+
global_segment PT_LOAD;
22
+
}
23
+
SECTIONS
24
+
{
25
+
/*
26
+
NOTE(Daniel Bokser): Had to add this in to force however Zig now invokes the linker
27
+
to make sure everything starts at address 0, which is what PDC seems to expect.
28
+
*/
29
+
. = 0;
30
+
31
+
.text :
32
+
{
33
+
*(.text)
34
+
*(.text.*)
35
+
36
+
KEEP(*(.init))
37
+
KEEP(*(.fini))
38
+
39
+
/* .ctors */
40
+
*crtbegin.o(.ctors)
41
+
*crtbegin?.o(.ctors)
42
+
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)
43
+
*(SORT(.ctors.*))
44
+
*(.ctors)
45
+
46
+
/* .dtors */
47
+
*crtbegin.o(.dtors)
48
+
*crtbegin?.o(.dtors)
49
+
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)
50
+
*(SORT(.dtors.*))
51
+
*(.dtors)
52
+
53
+
*(.rodata*)
54
+
55
+
KEEP(*(.eh_frame*))
56
+
57
+
}: global_segment
58
+
59
+
.data :
60
+
{
61
+
__etext = .;
62
+
63
+
__data_start__ = .;
64
+
*(vtable)
65
+
*(.data*)
66
+
67
+
. = ALIGN(4);
68
+
/* preinit data */
69
+
PROVIDE_HIDDEN (__preinit_array_start = .);
70
+
KEEP(*(.preinit_array))
71
+
PROVIDE_HIDDEN (__preinit_array_end = .);
72
+
73
+
. = ALIGN(4);
74
+
/* init data */
75
+
PROVIDE_HIDDEN (__init_array_start = .);
76
+
KEEP(*(SORT(.init_array.*)))
77
+
KEEP(*(.init_array))
78
+
PROVIDE_HIDDEN (__init_array_end = .);
79
+
80
+
. = ALIGN(4);
81
+
/* finit data */
82
+
PROVIDE_HIDDEN (__fini_array_start = .);
83
+
KEEP(*(SORT(.fini_array.*)))
84
+
KEEP(*(.fini_array))
85
+
PROVIDE_HIDDEN (__fini_array_end = .);
86
+
87
+
. = ALIGN(4);
88
+
/* All data end */
89
+
__data_end__ = .;
90
+
91
+
}: global_segment
92
+
93
+
.bss :
94
+
{
95
+
. = ALIGN(4);
96
+
__bss_start__ = .;
97
+
*(.bss*)
98
+
*(COMMON)
99
+
*(COM)
100
+
. = ALIGN(4);
101
+
__bss_end__ = .;
102
+
103
+
}: global_segment
104
+
105
+
/DISCARD/ :
106
+
{
107
+
*(.ARM.exidx)
108
+
}
109
+
110
+
}
+5
pdxinfo
+5
pdxinfo
+626
src/bsky_post.zig
+626
src/bsky_post.zig
···
1
+
const pdapi = @import("playdate_api_definitions.zig");
2
+
const std = @import("std");
3
+
const pdtools = @import("pdtools/index.zig");
4
+
const DrawableText = @import("pdtools/DrawableText.zig").DrawableText;
5
+
const fonts = @import("fonts.zig");
6
+
7
+
// Memory size constants
8
+
pub const AUTHOR_BUFFER_SIZE = 32;
9
+
pub const CONTENT_BUFFER_SIZE = 512;
10
+
pub const ALT_TEXT_BUFFER_SIZE = 256;
11
+
pub const MAX_POSTS = 50;
12
+
13
+
pub const CURSOR_BUFFER_SIZE = 256;
14
+
15
+
pub const PostType = enum {
16
+
normal,
17
+
repost,
18
+
reply,
19
+
};
20
+
21
+
pub const BskyPost = struct {
22
+
author: [AUTHOR_BUFFER_SIZE]u8,
23
+
author_len: usize,
24
+
content: [CONTENT_BUFFER_SIZE]u8,
25
+
content_len: usize,
26
+
alt_text: [ALT_TEXT_BUFFER_SIZE]u8,
27
+
alt_text_len: usize,
28
+
29
+
// Repost/Reply metadata
30
+
post_type: PostType,
31
+
repost_author: [AUTHOR_BUFFER_SIZE]u8,
32
+
repost_author_len: usize,
33
+
reply_author: [AUTHOR_BUFFER_SIZE]u8,
34
+
reply_author_len: usize,
35
+
};
36
+
37
+
/// Clean up text content by removing multiple consecutive newlines
38
+
fn cleanupTextContent(buffer: []u8, content_len: usize) usize {
39
+
if (content_len == 0) return 0;
40
+
41
+
var write_pos: usize = 0;
42
+
var read_pos: usize = 0;
43
+
var consecutive_newlines: usize = 0;
44
+
45
+
while (read_pos < content_len) {
46
+
const char = buffer[read_pos];
47
+
48
+
if (char == '\n' or char == '\r') {
49
+
consecutive_newlines += 1;
50
+
// Allow max 2 consecutive newlines (one blank line)
51
+
if (consecutive_newlines <= 2) {
52
+
buffer[write_pos] = '\n'; // Normalize to \n
53
+
write_pos += 1;
54
+
}
55
+
} else {
56
+
consecutive_newlines = 0;
57
+
buffer[write_pos] = char;
58
+
write_pos += 1;
59
+
}
60
+
61
+
read_pos += 1;
62
+
}
63
+
64
+
return write_pos;
65
+
}
66
+
67
+
pub var g_playdate: *pdapi.PlaydateAPI = undefined;
68
+
69
+
var g_posts_out: []BskyPost = undefined;
70
+
var g_post_count: *usize = undefined;
71
+
var g_post_max: usize = undefined;
72
+
var g_parsing_feed: bool = false;
73
+
var g_current_feed_index: i32 = -1;
74
+
75
+
fn onDecodeError(decoder: ?*pdapi.JSONDecoder, err: ?[*:0]const u8, linenum: c_int) callconv(.C) void {
76
+
_ = decoder; // Unused in this context
77
+
78
+
const pd = g_playdate;
79
+
pd.system.logToConsole("JSON decoding error at line %d: %s", linenum, err);
80
+
81
+
// Log parsing context
82
+
pd.system.logToConsole("Parsing context: posts found so far = %d", @as(c_int, @intCast(g_post_count.*)));
83
+
pd.system.logToConsole("Current feed index: %d", g_current_feed_index);
84
+
}
85
+
86
+
fn debugDidDecodeSublist(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) ?*anyopaque {
87
+
_ = decoder;
88
+
_ = name;
89
+
_ = json_type;
90
+
return null;
91
+
}
92
+
93
+
fn author_didDecodeSublist(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) ?*anyopaque {
94
+
_ = name;
95
+
_ = json_type;
96
+
97
+
// Reset to post-level parsing
98
+
decoder.?.didDecodeTableValue = post_didDecodeTableValue;
99
+
decoder.?.willDecodeSublist = null;
100
+
decoder.?.didDecodeSublist = null;
101
+
102
+
return null;
103
+
}
104
+
105
+
fn record_didDecodeSublist(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) ?*anyopaque {
106
+
_ = name;
107
+
_ = json_type;
108
+
109
+
// Reset to post-level parsing
110
+
decoder.?.didDecodeTableValue = post_didDecodeTableValue;
111
+
decoder.?.willDecodeSublist = null;
112
+
decoder.?.didDecodeSublist = null;
113
+
114
+
return null;
115
+
}
116
+
117
+
fn author_didDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void {
118
+
if (key == null) return;
119
+
120
+
const key_str = std.mem.span(key.?);
121
+
const json_type: pdapi.JSONValueType = @enumFromInt(value.type);
122
+
123
+
// Extract handle from author object
124
+
if (std.mem.eql(u8, key_str, "handle") and json_type == pdapi.JSONValueType.JSONString) {
125
+
if (g_post_count.* < g_post_max) {
126
+
const author_str = std.mem.span(value.data.stringval);
127
+
const copy_len = @min(author_str.len, g_posts_out[g_post_count.*].author.len - 1);
128
+
@memcpy(g_posts_out[g_post_count.*].author[0..copy_len], author_str[0..copy_len]);
129
+
g_posts_out[g_post_count.*].author_len = copy_len;
130
+
}
131
+
}
132
+
133
+
if (false) {
134
+
debugDidDecodeTableValue(decoder, key, value);
135
+
}
136
+
}
137
+
138
+
fn record_didDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void {
139
+
if (key == null) return;
140
+
141
+
const key_str = std.mem.span(key.?);
142
+
const json_type: pdapi.JSONValueType = @enumFromInt(value.type);
143
+
144
+
// Extract text from record object
145
+
if (std.mem.eql(u8, key_str, "text") and json_type == pdapi.JSONValueType.JSONString) {
146
+
if (g_post_count.* < g_post_max) {
147
+
const content_str = std.mem.span(value.data.stringval);
148
+
const copy_len = @min(content_str.len, g_posts_out[g_post_count.*].content.len - 1);
149
+
@memcpy(g_posts_out[g_post_count.*].content[0..copy_len], content_str[0..copy_len]);
150
+
151
+
// Clean up multiple consecutive newlines
152
+
const cleaned_len = cleanupTextContent(g_posts_out[g_post_count.*].content[0..copy_len], copy_len);
153
+
g_posts_out[g_post_count.*].content_len = cleaned_len;
154
+
}
155
+
}
156
+
157
+
if (false) {
158
+
debugDidDecodeTableValue(decoder, key, value);
159
+
}
160
+
}
161
+
162
+
fn post_didDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void {
163
+
if (key == null) return;
164
+
165
+
const key_str = std.mem.span(key.?);
166
+
const json_type: pdapi.JSONValueType = @enumFromInt(value.type);
167
+
168
+
// Set up nested object parsing
169
+
if (std.mem.eql(u8, key_str, "author") and json_type == pdapi.JSONValueType.JSONTable) {
170
+
decoder.?.willDecodeSublist = null;
171
+
decoder.?.didDecodeSublist = author_didDecodeSublist;
172
+
decoder.?.didDecodeTableValue = author_didDecodeTableValue;
173
+
return;
174
+
}
175
+
176
+
if (std.mem.eql(u8, key_str, "record") and json_type == pdapi.JSONValueType.JSONTable) {
177
+
decoder.?.willDecodeSublist = null;
178
+
decoder.?.didDecodeSublist = record_didDecodeSublist;
179
+
decoder.?.didDecodeTableValue = record_didDecodeTableValue;
180
+
return;
181
+
}
182
+
183
+
debugDidDecodeTableValue(decoder, key, value);
184
+
}
185
+
186
+
fn feed_item_didDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void {
187
+
if (key == null) return;
188
+
189
+
const key_str = std.mem.span(key.?);
190
+
const json_type: pdapi.JSONValueType = @enumFromInt(value.type);
191
+
192
+
// Set up post object parsing
193
+
if (std.mem.eql(u8, key_str, "post") and json_type == pdapi.JSONValueType.JSONTable) {
194
+
decoder.?.willDecodeSublist = null;
195
+
decoder.?.didDecodeSublist = post_didDecodeSublist;
196
+
decoder.?.didDecodeTableValue = post_didDecodeTableValue;
197
+
return;
198
+
}
199
+
200
+
if (false) {
201
+
debugDidDecodeTableValue(decoder, key, value);
202
+
}
203
+
}
204
+
205
+
fn post_didDecodeSublist(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) ?*anyopaque {
206
+
_ = name;
207
+
_ = json_type;
208
+
209
+
// Reset to feed item level parsing
210
+
decoder.?.didDecodeTableValue = feed_item_didDecodeTableValue;
211
+
decoder.?.willDecodeSublist = null;
212
+
decoder.?.didDecodeSublist = null;
213
+
214
+
return null;
215
+
}
216
+
217
+
fn willDecodeFeedList(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) void {
218
+
_ = name;
219
+
220
+
// This is called when entering each element in the feed array
221
+
if (json_type == pdapi.JSONValueType.JSONTable) {
222
+
// Initialize new post with empty data at current index
223
+
if (g_post_count.* < g_post_max) {
224
+
g_posts_out[g_post_count.*] = BskyPost{
225
+
.author = [_]u8{0} ** AUTHOR_BUFFER_SIZE,
226
+
.author_len = 0,
227
+
.content = [_]u8{0} ** CONTENT_BUFFER_SIZE,
228
+
.content_len = 0,
229
+
.alt_text = [_]u8{0} ** ALT_TEXT_BUFFER_SIZE,
230
+
.alt_text_len = 0,
231
+
.post_type = .normal,
232
+
.repost_author = [_]u8{0} ** AUTHOR_BUFFER_SIZE,
233
+
.repost_author_len = 0,
234
+
.reply_author = [_]u8{0} ** AUTHOR_BUFFER_SIZE,
235
+
.reply_author_len = 0,
236
+
};
237
+
}
238
+
239
+
// Set up feed item parsing (looking for "post" object)
240
+
decoder.?.didDecodeTableValue = feed_item_didDecodeTableValue;
241
+
}
242
+
}
243
+
244
+
fn didDecodeFeedList(decoder: ?*pdapi.JSONDecoder, name: ?[*:0]const u8, json_type: pdapi.JSONValueType) callconv(.C) ?*anyopaque {
245
+
_ = name;
246
+
247
+
if (json_type != pdapi.JSONValueType.JSONTable) {
248
+
return null;
249
+
}
250
+
251
+
// Post parsing complete - increment count if we have valid data
252
+
if (g_post_count.* < g_post_max) {
253
+
const has_author = g_posts_out[g_post_count.*].author_len > 0;
254
+
const has_content = g_posts_out[g_post_count.*].content_len > 0;
255
+
256
+
if (has_author or has_content) {
257
+
g_post_count.* += 1;
258
+
}
259
+
}
260
+
261
+
// Reset decoder for root-level parsing (back to root after this feed item)
262
+
decoder.?.didDecodeTableValue = root_didDecodeTableValue;
263
+
decoder.?.willDecodeSublist = willDecodeFeedList;
264
+
decoder.?.didDecodeSublist = didDecodeFeedList;
265
+
266
+
return null;
267
+
}
268
+
269
+
fn root_didDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void {
270
+
const json_type: pdapi.JSONValueType = @enumFromInt(value.type);
271
+
const path_ptr = decoder.?.path orelse "_root";
272
+
const path = std.mem.span(path_ptr);
273
+
274
+
// Check if we're inside a feed item by looking at the path
275
+
if (key != null) {
276
+
const key_str = std.mem.span(key.?);
277
+
278
+
// Extract author handle
279
+
if (std.mem.eql(u8, key_str, "handle") and json_type == pdapi.JSONValueType.JSONString) {
280
+
// Check if we're in an author context (path should contain something like feed[n].post.author)
281
+
if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".post.author") != null) {
282
+
if (g_post_count.* < g_post_max) {
283
+
const author_str = std.mem.span(value.data.stringval);
284
+
const copy_len = @min(author_str.len, g_posts_out[g_post_count.*].author.len - 1);
285
+
@memcpy(g_posts_out[g_post_count.*].author[0..copy_len], author_str[0..copy_len]);
286
+
g_posts_out[g_post_count.*].author_len = copy_len;
287
+
g_playdate.system.logToConsole("Found author: %.*s", @as(c_int, @intCast(copy_len)), @as([*:0]const u8, @ptrCast(&g_posts_out[g_post_count.*].author)));
288
+
}
289
+
return;
290
+
}
291
+
}
292
+
293
+
// Extract post text content
294
+
if (std.mem.eql(u8, key_str, "text") and json_type == pdapi.JSONValueType.JSONString) {
295
+
// Check if we're in a record context (path should contain something like feed[n].post.record)
296
+
if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".post.record") != null) {
297
+
if (g_post_count.* < g_post_max) {
298
+
const content_str = std.mem.span(value.data.stringval);
299
+
const copy_len = @min(content_str.len, g_posts_out[g_post_count.*].content.len - 1);
300
+
@memcpy(g_posts_out[g_post_count.*].content[0..copy_len], content_str[0..copy_len]);
301
+
g_posts_out[g_post_count.*].content_len = copy_len;
302
+
const preview_len = @min(50, copy_len);
303
+
g_playdate.system.logToConsole("Found text content (%d chars): %.*s...", @as(c_int, @intCast(copy_len)), @as(c_int, @intCast(preview_len)), @as([*:0]const u8, @ptrCast(&g_posts_out[g_post_count.*].content)));
304
+
}
305
+
return;
306
+
}
307
+
}
308
+
309
+
// Extract alt text from image embeds
310
+
if (std.mem.eql(u8, key_str, "alt") and json_type == pdapi.JSONValueType.JSONString) {
311
+
// Check if we're in an image embed context (path should contain something like feed[n].post.embed.images[])
312
+
if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".post.embed.images[") != null) {
313
+
if (g_post_count.* < g_post_max) {
314
+
const alt_str = std.mem.span(value.data.stringval);
315
+
316
+
// Create alt text with [IMG]: prefix
317
+
const prefix = "[IMG]: ";
318
+
const prefix_len = prefix.len;
319
+
const max_alt_len = g_posts_out[g_post_count.*].alt_text.len - prefix_len - 1;
320
+
const alt_copy_len = @min(alt_str.len, max_alt_len);
321
+
322
+
// Copy prefix first
323
+
@memcpy(g_posts_out[g_post_count.*].alt_text[0..prefix_len], prefix);
324
+
325
+
// Copy alt text after prefix
326
+
@memcpy(g_posts_out[g_post_count.*].alt_text[prefix_len .. prefix_len + alt_copy_len], alt_str[0..alt_copy_len]);
327
+
328
+
g_posts_out[g_post_count.*].alt_text_len = prefix_len + alt_copy_len;
329
+
}
330
+
return;
331
+
}
332
+
}
333
+
334
+
// Handle repost detection
335
+
if (std.mem.eql(u8, key_str, "$type") and json_type == pdapi.JSONValueType.JSONString) {
336
+
const type_str = std.mem.span(value.data.stringval);
337
+
// Check if we're in a reason context (indicating a repost)
338
+
if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".reason") != null) {
339
+
if (std.mem.eql(u8, type_str, "app.bsky.feed.defs#reasonRepost")) {
340
+
if (g_post_count.* < g_post_max) {
341
+
g_posts_out[g_post_count.*].post_type = .repost;
342
+
g_playdate.system.logToConsole("Detected repost");
343
+
}
344
+
}
345
+
return;
346
+
}
347
+
}
348
+
349
+
// Handle repost author (the person who reposted)
350
+
if (std.mem.eql(u8, key_str, "handle") and json_type == pdapi.JSONValueType.JSONString) {
351
+
// Check if we're in a reason.by context (repost author)
352
+
if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".reason.by") != null) {
353
+
if (g_post_count.* < g_post_max) {
354
+
const repost_author_str = std.mem.span(value.data.stringval);
355
+
const copy_len = @min(repost_author_str.len, g_posts_out[g_post_count.*].repost_author.len - 1);
356
+
@memcpy(g_posts_out[g_post_count.*].repost_author[0..copy_len], repost_author_str[0..copy_len]);
357
+
g_posts_out[g_post_count.*].repost_author_len = copy_len;
358
+
g_playdate.system.logToConsole("Found repost author: %.*s", @as(c_int, @intCast(copy_len)), @as([*:0]const u8, @ptrCast(&g_posts_out[g_post_count.*].repost_author)));
359
+
}
360
+
return;
361
+
}
362
+
}
363
+
364
+
// Handle reply detection - look for reply field in record
365
+
if (std.mem.eql(u8, key_str, "reply") and json_type == pdapi.JSONValueType.JSONTable) {
366
+
// Check if we're in a post.record context
367
+
if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.indexOf(u8, path, ".post.record") != null) {
368
+
if (g_post_count.* < g_post_max) {
369
+
g_posts_out[g_post_count.*].post_type = .reply;
370
+
g_playdate.system.logToConsole("Detected reply");
371
+
}
372
+
return;
373
+
}
374
+
}
375
+
376
+
// Handle feed item initialization and completion
377
+
if (std.mem.eql(u8, key_str, "post") and json_type == pdapi.JSONValueType.JSONTable) {
378
+
// Check if we're at feed[n].post level (not deeper)
379
+
if (std.mem.indexOf(u8, path, "feed[") != null and std.mem.count(u8, path, ".") == 0) {
380
+
// Extract the feed index from the path like "feed[5]"
381
+
const feed_start = std.mem.indexOf(u8, path, "feed[").? + 5;
382
+
const feed_end = std.mem.indexOf(u8, path[feed_start..], "]").? + feed_start;
383
+
const index_str = path[feed_start..feed_end];
384
+
const feed_index = std.fmt.parseInt(i32, index_str, 10) catch -1;
385
+
386
+
// If this is a new feed item, complete the previous post
387
+
if (g_current_feed_index != feed_index) {
388
+
if (g_current_feed_index >= 0 and g_post_count.* < g_post_max) {
389
+
// Complete previous post if it has data
390
+
const has_author = g_posts_out[g_post_count.*].author_len > 0;
391
+
const has_content = g_posts_out[g_post_count.*].content_len > 0;
392
+
const has_alt_text = g_posts_out[g_post_count.*].alt_text_len > 0;
393
+
if (has_author or has_content or has_alt_text) {
394
+
g_post_count.* += 1;
395
+
}
396
+
}
397
+
398
+
// Initialize new post
399
+
g_current_feed_index = feed_index;
400
+
if (g_post_count.* < g_post_max) {
401
+
g_posts_out[g_post_count.*] = BskyPost{
402
+
.author = [_]u8{0} ** AUTHOR_BUFFER_SIZE,
403
+
.author_len = 0,
404
+
.content = [_]u8{0} ** CONTENT_BUFFER_SIZE,
405
+
.content_len = 0,
406
+
.alt_text = [_]u8{0} ** ALT_TEXT_BUFFER_SIZE,
407
+
.alt_text_len = 0,
408
+
.post_type = .normal,
409
+
.repost_author = [_]u8{0} ** AUTHOR_BUFFER_SIZE,
410
+
.repost_author_len = 0,
411
+
.reply_author = [_]u8{0} ** AUTHOR_BUFFER_SIZE,
412
+
.reply_author_len = 0,
413
+
};
414
+
g_playdate.system.logToConsole("Initialized new post at feed index %d", feed_index);
415
+
}
416
+
}
417
+
return;
418
+
}
419
+
}
420
+
}
421
+
422
+
if (false) {
423
+
debugDidDecodeTableValue(decoder, key, value);
424
+
}
425
+
}
426
+
427
+
fn debugDidDecodeArrayValue(decoder: ?*pdapi.JSONDecoder, pos: c_int, value: pdapi.JSONValue) callconv(.C) void {
428
+
_ = decoder;
429
+
_ = pos;
430
+
_ = value;
431
+
}
432
+
433
+
fn debugDidDecodeTableValue(decoder: ?*pdapi.JSONDecoder, key: ?[*:0]const u8, value: pdapi.JSONValue) callconv(.C) void {
434
+
_ = decoder;
435
+
_ = key;
436
+
_ = value;
437
+
}
438
+
439
+
pub fn decodePostsJson(
440
+
playdate: *pdapi.PlaydateAPI,
441
+
buffer: []const u8,
442
+
buffer_size: usize,
443
+
post_max: usize,
444
+
posts: []BskyPost,
445
+
post_count: *usize,
446
+
cursor_buffer: []u8,
447
+
cursor_len: *usize,
448
+
) void {
449
+
// Initialize globals for parsing
450
+
g_playdate = playdate;
451
+
g_posts_out = posts;
452
+
g_post_count = post_count;
453
+
g_post_max = post_max;
454
+
_ = cursor_buffer;
455
+
_ = cursor_len;
456
+
457
+
const pd = playdate;
458
+
459
+
if (buffer_size == 0) {
460
+
pd.system.logToConsole("JSON decode called with empty buffer");
461
+
return;
462
+
}
463
+
464
+
// Reset parsing state
465
+
post_count.* = 0;
466
+
g_parsing_feed = false;
467
+
g_current_feed_index = -1;
468
+
469
+
// Log the first and last few characters of the JSON for debugging
470
+
pd.system.logToConsole("JSON buffer size: %d bytes", @as(c_int, @intCast(buffer_size)));
471
+
472
+
// Log first 100 characters
473
+
const preview_len = @min(100, buffer_size);
474
+
var preview_buffer: [101]u8 = undefined;
475
+
@memcpy(preview_buffer[0..preview_len], buffer[0..preview_len]);
476
+
preview_buffer[preview_len] = 0;
477
+
pd.system.logToConsole("JSON start: %s", @as([*:0]const u8, @ptrCast(&preview_buffer)));
478
+
479
+
// Log last 50 characters if buffer is large enough
480
+
if (buffer_size > 50) {
481
+
const tail_start = buffer_size - 50;
482
+
var tail_buffer: [51]u8 = undefined;
483
+
@memcpy(tail_buffer[0..50], buffer[tail_start..buffer_size]);
484
+
tail_buffer[50] = 0;
485
+
pd.system.logToConsole("JSON end: %s", @as([*:0]const u8, @ptrCast(&tail_buffer)));
486
+
}
487
+
488
+
if (false) {
489
+
pdtools.logLargeMessage(pd, buffer, buffer_size);
490
+
}
491
+
492
+
var decoder = pdapi.JSONDecoder{
493
+
.willDecodeSublist = null,
494
+
.shouldDecodeTableValueForKey = null,
495
+
.didDecodeTableValue = root_didDecodeTableValue,
496
+
.shouldDecodeArrayValueAtIndex = null,
497
+
.didDecodeArrayValue = null,
498
+
.didDecodeSublist = null,
499
+
.decodeError = onDecodeError,
500
+
.userdata = null,
501
+
.returnString = 0,
502
+
.path = null,
503
+
};
504
+
505
+
var jsonValue: pdapi.JSONValue = undefined;
506
+
507
+
pd.system.logToConsole("Starting JSON decoding...");
508
+
const decode_result = pd.json.decodeString(&decoder, @as(?[*:0]const u8, @ptrCast(buffer)), &jsonValue);
509
+
pd.system.logToConsole("JSON decoding finished with result: %d", decode_result);
510
+
511
+
// Complete the final post if we have one
512
+
if (g_current_feed_index >= 0 and g_post_count.* < g_post_max) {
513
+
const has_author = g_posts_out[g_post_count.*].author_len > 0;
514
+
const has_content = g_posts_out[g_post_count.*].content_len > 0;
515
+
const has_alt_text = g_posts_out[g_post_count.*].alt_text_len > 0;
516
+
pd.system.logToConsole("Final post check: author=%d content=%d alt=%d", @as(c_int, if (has_author) 1 else 0), @as(c_int, if (has_content) 1 else 0), @as(c_int, if (has_alt_text) 1 else 0));
517
+
if (has_author or has_content or has_alt_text) {
518
+
g_post_count.* += 1;
519
+
pd.system.logToConsole("Added final post, total count: %d", @as(c_int, @intCast(g_post_count.*)));
520
+
}
521
+
}
522
+
523
+
pd.system.logToConsole("JSON parsing complete - final post count: %d", @as(c_int, @intCast(g_post_count.*)));
524
+
}
525
+
526
+
pub fn renderPost(
527
+
pd: *pdapi.PlaydateAPI,
528
+
post: *const BskyPost,
529
+
x: c_int,
530
+
y: c_int,
531
+
width: c_int,
532
+
dry_run: bool,
533
+
) usize {
534
+
const font = fonts.g_font;
535
+
const padding = 4;
536
+
const corner_radius = 4;
537
+
538
+
// Account for post padding
539
+
const content_width = @as(?c_int, @intCast(width - (padding * 2)));
540
+
541
+
// Author height
542
+
const author_len = std.mem.len(@as([*:0]const u8, @ptrCast(&post.author)));
543
+
const author_drawable = DrawableText{
544
+
.playdate = pd,
545
+
.text = post.author[0..author_len],
546
+
.max_width = content_width,
547
+
.font = font,
548
+
.wrapping_mode = .WrapWord,
549
+
.alignment = .AlignTextLeft,
550
+
};
551
+
552
+
// Content height
553
+
const content_len = std.mem.len(@as([*:0]const u8, @ptrCast(&post.content)));
554
+
const content_drawable = DrawableText{
555
+
.playdate = pd,
556
+
.text = post.content[0..content_len],
557
+
.max_width = content_width,
558
+
.font = font,
559
+
.wrapping_mode = .WrapWord,
560
+
.alignment = .AlignTextLeft,
561
+
};
562
+
563
+
// Alt text height
564
+
const alt_text_len = std.mem.len(@as([*:0]const u8, @ptrCast(&post.alt_text)));
565
+
var alt_drawable = DrawableText{
566
+
.playdate = pd,
567
+
.text = post.alt_text[0..alt_text_len],
568
+
.max_width = content_width,
569
+
.font = font,
570
+
.wrapping_mode = .WrapWord,
571
+
.alignment = .AlignTextLeft,
572
+
};
573
+
574
+
// Render the post (it's at least partially visible)
575
+
576
+
// Create DrawableText for @ symbol and author text
577
+
const at_symbol = "@";
578
+
579
+
const at_drawable = DrawableText{
580
+
.playdate = pd,
581
+
.text = at_symbol,
582
+
.max_width = content_width,
583
+
.font = font,
584
+
.wrapping_mode = .WrapWord,
585
+
.alignment = .AlignTextLeft,
586
+
};
587
+
588
+
var current_y = y + padding;
589
+
const current_x = x + padding;
590
+
591
+
// Draw @ symbol and author text
592
+
if (author_len > 0) {
593
+
at_drawable.render(current_x, current_y, dry_run);
594
+
const at_width = at_drawable.getWidth();
595
+
author_drawable.render(current_x + at_width, current_y, dry_run);
596
+
current_y += author_drawable.getHeight();
597
+
}
598
+
599
+
// Draw post content with text wrapping
600
+
content_drawable.render(current_x, current_y, dry_run);
601
+
current_y += content_drawable.getHeight();
602
+
current_y += padding;
603
+
604
+
// Draw alt text if available
605
+
if (alt_text_len > 0) {
606
+
alt_drawable.render(current_x, current_y, dry_run);
607
+
current_y += alt_drawable.getHeight();
608
+
current_y += padding;
609
+
}
610
+
611
+
const height = current_y - y;
612
+
613
+
if (!dry_run) {
614
+
pd.graphics.drawRoundRect(
615
+
x,
616
+
y,
617
+
width,
618
+
height,
619
+
corner_radius,
620
+
1,
621
+
@intFromEnum(pdapi.LCDSolidColor.ColorBlack),
622
+
);
623
+
}
624
+
625
+
return @as(usize, @intCast(height)); // Return total height of the rendered post
626
+
}
+14
src/definitions.zig
+14
src/definitions.zig
···
1
+
pub const BSKY_USERNAME_SIZE = 64;
2
+
pub const BSKY_PASSWORD_SIZE = 64;
3
+
4
+
pub const EDITOR_BUFFER_SIZE = 256;
5
+
pub const RESPONSE_BUFFER_SIZE = 1024 * 1024;
6
+
7
+
pub const MARGIN = 4;
8
+
9
+
pub const DEBUG_DONT_POST = true;
10
+
11
+
// Credential storage constants
12
+
pub const USERNAME_FILENAME = "bsky-username.txt";
13
+
pub const PASSWORD_FILENAME = "bsky-password.txt";
14
+
pub const MAX_CREDENTIAL_LENGTH = 128;
+19
src/fonts.zig
+19
src/fonts.zig
···
1
+
const pdapi = @import("playdate_api_definitions.zig");
2
+
3
+
const g_font_path = "assets/fonts/Roobert-10-Bold.pft";
4
+
5
+
pub var g_font: *pdapi.LCDFont = undefined;
6
+
7
+
pub fn initializeFonts(pd: *pdapi.PlaydateAPI) void {
8
+
9
+
// Load the font
10
+
const font = pd.graphics.loadFont(g_font_path, null);
11
+
12
+
if (font) |f| {
13
+
pd.graphics.setFont(f);
14
+
g_font = f;
15
+
pd.system.logToConsole("Custom Roobert font loaded successfully");
16
+
} else {
17
+
pd.system.logToConsole("Failed to load custom font, using system default");
18
+
}
19
+
}
+1243
src/main.zig
+1243
src/main.zig
···
1
+
const std = @import("std");
2
+
const pdapi = @import("playdate_api_definitions.zig");
3
+
const network = @import("network.zig");
4
+
const bsky_post = @import("bsky_post.zig");
5
+
const pdtools = @import("pdtools/index.zig");
6
+
const ScrollingValue = @import("pdtools/ScrollingValue.zig").SlidingValue;
7
+
const panic_handler = @import("panic_handler.zig");
8
+
const defs = @import("definitions.zig");
9
+
const keyboard_mod = @import("pdtools/keyboard.zig");
10
+
const Keyboard = keyboard_mod.Keyboard(256);
11
+
12
+
const fonts = @import("fonts.zig");
13
+
14
+
// Global buffers to avoid large stack allocations
15
+
var g_headers_buffer: [512]u8 = undefined;
16
+
17
+
// Login input state
18
+
const LoginInputState = enum {
19
+
username,
20
+
password,
21
+
ready,
22
+
};
23
+
24
+
// Login field selection
25
+
const LoginFieldSelection = enum {
26
+
username,
27
+
password,
28
+
login_button,
29
+
};
30
+
31
+
// Post field selection
32
+
const PostFieldSelection = enum {
33
+
edit_text,
34
+
confirm_post,
35
+
};
36
+
37
+
// Simple app state for timeline fetching
38
+
const AppState = struct {
39
+
playdate: *pdapi.PlaydateAPI,
40
+
message: []const u8,
41
+
response_buffer: [defs.RESPONSE_BUFFER_SIZE]u8 = undefined,
42
+
response_length: usize,
43
+
44
+
posts: [bsky_post.MAX_POSTS]bsky_post.BskyPost,
45
+
post_count: usize,
46
+
cursor_buffer: [bsky_post.CURSOR_BUFFER_SIZE]u8,
47
+
cursor_len: usize,
48
+
49
+
network_access_requested: bool,
50
+
body_scroll: ScrollingValue,
51
+
52
+
// Login state
53
+
is_logged_in: bool,
54
+
access_token: [512]u8,
55
+
access_token_len: usize,
56
+
user_did: [256]u8,
57
+
user_did_len: usize,
58
+
59
+
keyboard: keyboard_mod.Keyboard(256),
60
+
61
+
// Login input state
62
+
login_input_state: LoginInputState,
63
+
login_field_selection: LoginFieldSelection,
64
+
username: [defs.MAX_CREDENTIAL_LENGTH]u8,
65
+
password: [defs.MAX_CREDENTIAL_LENGTH]u8,
66
+
67
+
previous_buttons: pdapi.PDButtons,
68
+
69
+
// Post composition state
70
+
post_field_selection: PostFieldSelection,
71
+
post_text: [defs.EDITOR_BUFFER_SIZE]u8,
72
+
post_text_len: usize,
73
+
};
74
+
75
+
var g_app: AppState = undefined;
76
+
77
+
const Page = enum {
78
+
login,
79
+
home,
80
+
post,
81
+
};
82
+
83
+
var g_selected_page: Page = .login;
84
+
85
+
const LCD_WIDTH = pdapi.LCD_COLUMNS;
86
+
const LCD_HEIGHT = pdapi.LCD_ROWS;
87
+
const MARGIN = 4;
88
+
const CHUNK_SIZE = 512; // Read 512 bytes at a time
89
+
90
+
// Credential storage functions
91
+
fn saveCredentials(pd: *pdapi.PlaydateAPI) void {
92
+
pdtools.saveStringFile(pd, defs.USERNAME_FILENAME, &g_app.username);
93
+
pdtools.saveStringFile(pd, defs.PASSWORD_FILENAME, &g_app.password);
94
+
}
95
+
96
+
fn loadCredentials(pd: *pdapi.PlaydateAPI) void {
97
+
pd.system.logToConsole("Loading username from: %s", defs.USERNAME_FILENAME.ptr);
98
+
pdtools.loadStringFile(pd, defs.USERNAME_FILENAME, &g_app.username);
99
+
pd.system.logToConsole("Loading password from: %s", defs.PASSWORD_FILENAME.ptr);
100
+
pdtools.loadStringFile(pd, defs.PASSWORD_FILENAME, &g_app.password);
101
+
}
102
+
103
+
fn clearCredentials(pd: *pdapi.PlaydateAPI) void {
104
+
// Clear in-memory credentials
105
+
@memset(&g_app.username, 0);
106
+
@memset(&g_app.password, 0);
107
+
g_app.login_input_state = .username;
108
+
g_app.login_field_selection = .username;
109
+
110
+
pdtools.delStringFile(pd, defs.USERNAME_FILENAME);
111
+
pdtools.delStringFile(pd, defs.PASSWORD_FILENAME);
112
+
113
+
pd.system.logToConsole("Credentials cleared");
114
+
g_app.message = "Credentials cleared - enter new ones";
115
+
}
116
+
117
+
// Network callback functions
118
+
fn onLoginSuccess(status_code: network.HttpStatusCode, response_data: []const u8) void {
119
+
const pd = g_app.playdate;
120
+
pd.system.logToConsole("Login successful with status: %d", status_code);
121
+
122
+
// Parse the JSON to extract access token
123
+
if (std.mem.indexOf(u8, response_data, "\"accessJwt\":\"")) |start_pos| {
124
+
const token_start = start_pos + 13; // Length of "accessJwt":"
125
+
if (std.mem.indexOfPos(u8, response_data, token_start, "\"")) |end_pos| {
126
+
const token_len = end_pos - token_start;
127
+
if (token_len < g_app.access_token.len) {
128
+
@memcpy(g_app.access_token[0..token_len], response_data[token_start..end_pos]);
129
+
g_app.access_token_len = token_len;
130
+
131
+
// Also parse the DID from the response
132
+
if (std.mem.indexOf(u8, response_data, "\"did\":\"")) |did_start_pos| {
133
+
const did_token_start = did_start_pos + 7; // Length of "did":"
134
+
if (std.mem.indexOfPos(u8, response_data, did_token_start, "\"")) |did_end_pos| {
135
+
const did_len = did_end_pos - did_token_start;
136
+
if (did_len < g_app.user_did.len) {
137
+
@memcpy(g_app.user_did[0..did_len], response_data[did_token_start..did_end_pos]);
138
+
g_app.user_did[did_len] = 0;
139
+
g_app.user_did_len = did_len;
140
+
pd.system.logToConsole("User DID extracted: %.*s", @as(c_int, @intCast(did_len)), @as([*c]const u8, @ptrCast(&g_app.user_did)));
141
+
}
142
+
}
143
+
}
144
+
145
+
g_app.is_logged_in = true;
146
+
g_app.message = "Login successful - fetching posts...";
147
+
g_selected_page = .home;
148
+
pd.system.logToConsole("Access token extracted successfully (length: %d)", @as(c_int, @intCast(token_len)));
149
+
150
+
// Automatically fetch posts after successful login
151
+
fetchBlueskyFeed();
152
+
} else {
153
+
g_app.message = "Access token too long";
154
+
pd.system.logToConsole("Access token too long: %d", @as(c_int, @intCast(token_len)));
155
+
}
156
+
} else {
157
+
g_app.message = "Failed to parse access token";
158
+
pd.system.logToConsole("Failed to find end of access token");
159
+
}
160
+
} else {
161
+
g_app.message = "Access token not found";
162
+
pd.system.logToConsole("Access token not found in response");
163
+
}
164
+
}
165
+
166
+
fn onLoginFailure(status_code: network.HttpStatusCode, error_message: []const u8) void {
167
+
const pd = g_app.playdate;
168
+
pd.system.logToConsole("Login failed with status: %d", status_code);
169
+
pd.system.logToConsole("Error: %.*s", @as(c_int, @intCast(error_message.len)), @as([*c]const u8, @ptrCast(error_message.ptr)));
170
+
g_app.message = if (status_code == 0) "Login connection error" else "Login failed";
171
+
}
172
+
173
+
fn onFeedSuccess(status_code: network.HttpStatusCode, response_data: []const u8) void {
174
+
const pd = g_app.playdate;
175
+
pd.system.logToConsole("Feed fetch successful with status: %d, %d bytes", status_code, response_data.len);
176
+
177
+
// The response_data is already pointing to our g_app.response_buffer
178
+
// So we just need to set the length - no copying needed
179
+
g_app.response_length = response_data.len;
180
+
181
+
// Ensure null termination
182
+
if (g_app.response_length < g_app.response_buffer.len) {
183
+
g_app.response_buffer[g_app.response_length] = 0;
184
+
}
185
+
186
+
g_app.message = "Feed data received";
187
+
188
+
// Parse the JSON response into posts
189
+
parseFeedResponseIntoPosts();
190
+
}
191
+
192
+
fn onFeedFailure(status_code: network.HttpStatusCode, error_message: []const u8) void {
193
+
const pd = g_app.playdate;
194
+
pd.system.logToConsole("Feed fetch failed with status: %d", status_code);
195
+
196
+
if (error_message.len > 0) {
197
+
pd.system.logToConsole("Error message: %.*s", @as(c_int, @intCast(error_message.len)), @as([*c]const u8, @ptrCast(error_message.ptr)));
198
+
} else {
199
+
pd.system.logToConsole("No error message provided");
200
+
}
201
+
202
+
// Clear any previous response data
203
+
g_app.response_length = 0;
204
+
g_app.post_count = 0;
205
+
206
+
if (status_code == 0) {
207
+
g_app.message = "Network connection failed";
208
+
} else if (status_code == 401) {
209
+
g_app.message = "Login expired - please re-login";
210
+
} else if (status_code >= 400 and status_code < 500) {
211
+
g_app.message = "Request error - check credentials";
212
+
} else if (status_code >= 500) {
213
+
g_app.message = "Server error - try again later";
214
+
} else {
215
+
g_app.message = "Feed fetch failed";
216
+
}
217
+
}
218
+
219
+
fn onPostSuccess(status_code: network.HttpStatusCode, response_data: []const u8) void {
220
+
const pd = g_app.playdate;
221
+
pd.system.logToConsole("Post successful with status: %d", status_code);
222
+
223
+
// Clear the post text since it was successfully posted
224
+
@memset(&g_app.post_text, 0);
225
+
g_app.post_text_len = 0;
226
+
227
+
// Reset to default text for next post
228
+
const default_post_text = "Hello World!\n\nPosted from my #playdate";
229
+
const default_len = @min(default_post_text.len, g_app.post_text.len - 1);
230
+
@memcpy(g_app.post_text[0..default_len], default_post_text[0..default_len]);
231
+
g_app.post_text[default_len] = 0;
232
+
g_app.post_text_len = default_len;
233
+
234
+
g_app.message = "Post published successfully!";
235
+
236
+
// Log response for debugging
237
+
if (response_data.len > 0) {
238
+
const preview_len = @min(100, response_data.len);
239
+
pd.system.logToConsole("Post response: %.*s", @as(c_int, @intCast(preview_len)), @as([*c]const u8, @ptrCast(response_data.ptr)));
240
+
}
241
+
}
242
+
243
+
fn onPostFailure(status_code: network.HttpStatusCode, error_message: []const u8) void {
244
+
const pd = g_app.playdate;
245
+
pd.system.logToConsole("Post failed with status: %d", status_code);
246
+
247
+
if (error_message.len > 0) {
248
+
pd.system.logToConsole("Post error: %.*s", @as(c_int, @intCast(error_message.len)), @as([*c]const u8, @ptrCast(error_message.ptr)));
249
+
}
250
+
251
+
if (status_code == 0) {
252
+
g_app.message = "Post failed - network error";
253
+
} else if (status_code == 401) {
254
+
g_app.message = "Post failed - please re-login";
255
+
} else if (status_code == 413) {
256
+
g_app.message = "Post too long - please shorten";
257
+
} else if (status_code >= 400 and status_code < 500) {
258
+
g_app.message = "Post failed - invalid request";
259
+
} else if (status_code >= 500) {
260
+
g_app.message = "Post failed - server error";
261
+
} else {
262
+
g_app.message = "Post failed - unknown error";
263
+
}
264
+
}
265
+
266
+
fn parseFeedResponseIntoPosts() void {
267
+
// Check if we have a valid response
268
+
if (g_app.response_length == 0) {
269
+
g_app.playdate.system.logToConsole("No response data to parse");
270
+
g_app.message = "No response data received";
271
+
return;
272
+
}
273
+
274
+
// Validate response length isn't too large
275
+
if (g_app.response_length >= g_app.response_buffer.len) {
276
+
g_app.playdate.system.logToConsole("WARNING: Response length %d exceeds buffer size %d", @as(c_int, @intCast(g_app.response_length)), @as(c_int, @intCast(g_app.response_buffer.len)));
277
+
g_app.response_length = g_app.response_buffer.len - 1;
278
+
}
279
+
280
+
g_app.playdate.system.logToConsole("Starting JSON parsing of %d bytes", @as(c_int, @intCast(g_app.response_length)));
281
+
282
+
// Log first few characters for debugging
283
+
const preview_len = @min(100, g_app.response_length);
284
+
g_app.playdate.system.logToConsole("Response preview (first %d chars): %.*s", @as(c_int, @intCast(preview_len)), @as(c_int, @intCast(preview_len)), @as([*]const u8, @ptrCast(&g_app.response_buffer)));
285
+
286
+
// Reset post count before parsing
287
+
g_app.post_count = 0;
288
+
289
+
// Add error handling around JSON parsing
290
+
bsky_post.decodePostsJson(
291
+
g_app.playdate,
292
+
&g_app.response_buffer,
293
+
g_app.response_length,
294
+
g_app.posts.len,
295
+
&g_app.posts,
296
+
&g_app.post_count,
297
+
&g_app.cursor_buffer,
298
+
&g_app.cursor_len,
299
+
);
300
+
301
+
g_app.playdate.system.logToConsole("JSON parsing completed, found %d posts", @as(c_int, @intCast(g_app.post_count)));
302
+
303
+
if (g_app.post_count == 0) {
304
+
g_app.message = "No posts found in response";
305
+
} else {
306
+
g_app.message = "Feed loaded successfully";
307
+
}
308
+
}
309
+
310
+
fn onScrollMaxThreshold() void {
311
+
// Handle scroll max threshold being reached
312
+
g_app.playdate.system.logToConsole("Scroll max threshold reached");
313
+
}
314
+
315
+
fn onScrollMinThreshold() void {
316
+
// Handle scroll min threshold being reached
317
+
g_app.playdate.system.logToConsole("Scroll min threshold reached");
318
+
319
+
// Refresh posts.
320
+
g_app.playdate.system.logToConsole("Refreshing posts...");
321
+
fetchBlueskyFeed();
322
+
}
323
+
324
+
pub export fn eventHandler(playdate: *pdapi.PlaydateAPI, event: pdapi.PDSystemEvent, arg: u32) callconv(.C) c_int {
325
+
_ = arg;
326
+
switch (event) {
327
+
.EventInit => {
328
+
playdate.system.logToConsole("Initializing timeline fetch app...");
329
+
330
+
// Initialize panic handler for better error reporting
331
+
panic_handler.init(playdate);
332
+
333
+
// Test basic memory allocation to catch early issues
334
+
playdate.system.logToConsole("Testing basic memory allocation...");
335
+
336
+
playdate.system.logToConsole("Memory test passed - AppState size: %d bytes", @as(c_int, @intCast(@sizeOf(AppState))));
337
+
338
+
playdate.system.logToConsole("Initializing full application");
339
+
340
+
// Initialize full app state
341
+
g_app.playdate = playdate;
342
+
343
+
fonts.initializeFonts(playdate);
344
+
345
+
// Initialize keyboard
346
+
g_app.keyboard = keyboard_mod.Keyboard(256){
347
+
.playdate = playdate,
348
+
.font = fonts.g_font,
349
+
.title = undefined,
350
+
.editor_buffer = undefined,
351
+
.output_buffer = undefined,
352
+
};
353
+
354
+
g_app.message = "Use ← → to navigate, B to interact";
355
+
g_app.response_length = 0;
356
+
g_app.post_count = 0;
357
+
g_app.cursor_len = 0;
358
+
g_app.network_access_requested = false;
359
+
360
+
// Initialize scrolling value for posts with reasonable bounds
361
+
g_app.body_scroll = ScrollingValue{
362
+
.playdate = playdate,
363
+
.soft_min_runout = 100, // Static runout distance from min_value
364
+
.soft_max_runout = 100, // Static runout distance from max_value
365
+
.runout_margin = 5.0, // Margin before callback detection
366
+
.min_value = -200, // Allow scrolling above top with hard limit
367
+
.max_value = 2000, // Hard limit - will be updated based on content
368
+
.current_value = 0,
369
+
.current_height = 0,
370
+
.crank_position_offset = 0,
371
+
.onScrollMaxThreshold = &onScrollMaxThreshold,
372
+
.onScrollMinThreshold = &onScrollMinThreshold,
373
+
};
374
+
g_app.is_logged_in = false;
375
+
g_app.access_token_len = 0;
376
+
g_app.user_did_len = 0;
377
+
// Initialize login state with default credentials for testing
378
+
g_app.login_input_state = .ready; // Start ready since we have default credentials
379
+
g_app.login_field_selection = .username; // Start with username selected
380
+
381
+
// Initialize with empty credentials first
382
+
@memset(&g_app.username, 0);
383
+
@memset(&g_app.password, 0);
384
+
385
+
g_app.previous_buttons = 0;
386
+
387
+
// Initialize post composition state with default text
388
+
g_app.post_field_selection = .edit_text;
389
+
const default_post_text = "Hello World!\n\nPosted from my #playdate";
390
+
const default_len = @min(default_post_text.len, g_app.post_text.len - 1);
391
+
@memcpy(g_app.post_text[0..default_len], default_post_text[0..default_len]);
392
+
g_app.post_text[default_len] = 0;
393
+
g_app.post_text_len = default_len;
394
+
395
+
// Set up real update callback with full functionality
396
+
playdate.system.setUpdateCallback(update_and_render, null);
397
+
playdate.system.logToConsole("Update callback set");
398
+
399
+
playdate.system.logToConsole("App initialized successfully");
400
+
401
+
playdate.graphics.clear(@intFromEnum(pdapi.LCDSolidColor.ColorWhite));
402
+
403
+
// Try to load saved credentials
404
+
playdate.system.logToConsole("Attempting to load saved credentials...");
405
+
loadCredentials(playdate);
406
+
g_app.login_input_state = .ready;
407
+
playdate.system.logToConsole("Credentials loaded - Username: %s", &g_app.username);
408
+
g_app.message = "Saved credentials loaded";
409
+
},
410
+
411
+
else => {},
412
+
}
413
+
return 0;
414
+
}
415
+
416
+
fn loginToBluesky() void {
417
+
const pd = g_app.playdate;
418
+
419
+
// Check if we're already busy with a network operation
420
+
if (!network.isNetworkIdle()) {
421
+
return;
422
+
}
423
+
424
+
// Request network access
425
+
if (!g_app.network_access_requested) {
426
+
pd.system.logToConsole("Requesting network access for https://bsky.social...");
427
+
const response = pd.network.playdate_http.requestAccess("https://bsky.social", 443, true, "login", null, null);
428
+
g_app.network_access_requested = true;
429
+
430
+
switch (response) {
431
+
.AccessAllow => {
432
+
pd.system.logToConsole("Network access GRANTED");
433
+
},
434
+
.AccessDeny => {
435
+
pd.system.logToConsole("Network access DENIED by user");
436
+
g_app.message = "Network access denied";
437
+
return;
438
+
},
439
+
else => {
440
+
pd.system.logToConsole("Network access request FAILED");
441
+
g_app.message = "Access request failed";
442
+
return;
443
+
},
444
+
}
445
+
} else {
446
+
pd.system.logToConsole("Network access already requested");
447
+
}
448
+
449
+
// Check if we have credentials
450
+
if (g_app.username[0] == 0 or g_app.password[0] == 0) {
451
+
g_app.message = "Please enter username and password";
452
+
g_app.login_input_state = if (g_app.username[0] == 0) .username else .password;
453
+
return;
454
+
}
455
+
456
+
// Prepare login request body using entered credentials
457
+
var login_body: [512]u8 = undefined;
458
+
const username = std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.username)));
459
+
const password = std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.password)));
460
+
461
+
// Debug logging for credentials
462
+
pd.system.logToConsole("Login credentials - Username: %s", @as([*:0]const u8, @ptrCast(&g_app.username)));
463
+
pd.system.logToConsole("Login credentials - Password: %s", @as([*:0]const u8, @ptrCast(&g_app.password)));
464
+
const login_json = std.fmt.bufPrint(&login_body, "{{\"identifier\":\"{s}\",\"password\":\"{s}\"}}", .{ username, password }) catch {
465
+
g_app.message = "Failed to format login request";
466
+
return;
467
+
};
468
+
469
+
// Create HTTP request
470
+
const request = network.HttpRequest{
471
+
.method = .POST,
472
+
.url = "https://bsky.social/xrpc/com.atproto.server.createSession",
473
+
.server = "bsky.social",
474
+
.port = 443,
475
+
.path = "/xrpc/com.atproto.server.createSession",
476
+
.use_https = true,
477
+
.bearer_token = null,
478
+
.body = login_json,
479
+
.response_buffer = &g_app.response_buffer,
480
+
.success_callback = onLoginSuccess,
481
+
.failure_callback = onLoginFailure,
482
+
};
483
+
484
+
if (network.makeHttpRequest(pd, request)) {
485
+
g_app.message = "Logging in...";
486
+
} else {
487
+
g_app.message = "Network connection failed - check internet";
488
+
pd.system.logToConsole("Troubleshooting suggestions:");
489
+
pd.system.logToConsole("- Check internet connection");
490
+
pd.system.logToConsole("- Try in Playdate simulator vs device");
491
+
pd.system.logToConsole("- Check firewall settings");
492
+
pd.system.logToConsole("- Verify DNS resolution works");
493
+
}
494
+
}
495
+
496
+
fn fetchBlueskyFeed() void {
497
+
const pd = g_app.playdate;
498
+
499
+
// Check if logged in
500
+
if (!g_app.is_logged_in) {
501
+
g_app.message = "Please log in first";
502
+
return;
503
+
}
504
+
505
+
// Check if we're already busy with a network operation
506
+
if (!network.isNetworkIdle()) {
507
+
return;
508
+
}
509
+
510
+
// Reset state for refetch
511
+
g_app.post_count = 0;
512
+
g_app.body_scroll.setValue(0);
513
+
514
+
// Request network access only once
515
+
if (!g_app.network_access_requested) {
516
+
pd.system.logToConsole("Requesting network access...");
517
+
const response = pd.network.playdate_http.requestAccess("https://bsky.social", 443, true, "timeline", null, null);
518
+
g_app.network_access_requested = true;
519
+
520
+
switch (response) {
521
+
.AccessAllow => {},
522
+
.AccessDeny => {
523
+
g_app.message = "Network access denied";
524
+
return;
525
+
},
526
+
else => {
527
+
g_app.message = "Access request failed";
528
+
return;
529
+
},
530
+
}
531
+
}
532
+
533
+
// Create HTTP request with bearer token
534
+
const bearer_token = g_app.access_token[0..g_app.access_token_len];
535
+
const request = network.HttpRequest{
536
+
.method = .GET,
537
+
.url = "https://bsky.social/xrpc/app.bsky.feed.getTimeline?limit=50",
538
+
.server = "bsky.social",
539
+
.port = 443,
540
+
.path = "/xrpc/app.bsky.feed.getTimeline?limit=50",
541
+
.use_https = true,
542
+
.bearer_token = bearer_token,
543
+
.body = null,
544
+
.response_buffer = &g_app.response_buffer,
545
+
.success_callback = onFeedSuccess,
546
+
.failure_callback = onFeedFailure,
547
+
};
548
+
549
+
if (network.makeHttpRequest(pd, request)) {
550
+
g_app.message = "Fetching feed data...";
551
+
} else {
552
+
g_app.message = "Failed to start feed request";
553
+
}
554
+
}
555
+
556
+
// Login keyboard callbacks
557
+
fn usernameKeyboardCancelled() void {
558
+
g_app.playdate.system.logToConsole("Username input cancelled");
559
+
g_app.login_input_state = .username; // Stay on username input
560
+
}
561
+
562
+
fn usernameKeyboardConfirmed(text: []const u8) void {
563
+
g_app.playdate.system.logToConsole("Username entered: %.*s", @as(c_int, @intCast(text.len)), @as([*c]const u8, @ptrCast(text.ptr)));
564
+
565
+
// Copy username
566
+
const copy_len = @min(text.len, g_app.username.len - 1);
567
+
@memcpy(g_app.username[0..copy_len], text[0..copy_len]);
568
+
g_app.username[copy_len] = 0;
569
+
570
+
saveCredentials(g_app.playdate);
571
+
572
+
// Move to password input
573
+
g_app.login_input_state = .password;
574
+
}
575
+
576
+
fn passwordKeyboardCancelled() void {
577
+
g_app.playdate.system.logToConsole("Password input cancelled");
578
+
g_app.login_input_state = .username; // Go back to username input
579
+
}
580
+
581
+
fn passwordKeyboardConfirmed(text: []const u8) void {
582
+
g_app.playdate.system.logToConsole("Password entered (length: %d)", @as(c_int, @intCast(text.len)));
583
+
584
+
// Copy password
585
+
const copy_len = @min(text.len, g_app.password.len - 1);
586
+
@memcpy(g_app.password[0..copy_len], text[0..copy_len]);
587
+
g_app.password[copy_len] = 0;
588
+
589
+
saveCredentials(g_app.playdate);
590
+
591
+
g_app.login_input_state = .ready;
592
+
}
593
+
594
+
// JSON writer context for building post JSON
595
+
const JsonWriteContext = struct {
596
+
buffer: []u8,
597
+
pos: usize,
598
+
};
599
+
600
+
// JSON write callback for Playdate JSON encoder
601
+
fn jsonWriteCallback(userdata: ?*anyopaque, str: [*c]const u8, len: c_int) callconv(.C) void {
602
+
if (userdata == null) return;
603
+
const context: *JsonWriteContext = @ptrCast(@alignCast(userdata.?));
604
+
const string_len = @as(usize, @intCast(len));
605
+
606
+
if (context.pos + string_len > context.buffer.len) return; // Buffer overflow protection
607
+
608
+
const src_slice = @as([*]const u8, @ptrCast(str))[0..string_len];
609
+
@memcpy(context.buffer[context.pos .. context.pos + string_len], src_slice);
610
+
context.pos += string_len;
611
+
}
612
+
613
+
fn submitBlueskyPost() void {
614
+
const pd = g_app.playdate;
615
+
616
+
// Check if logged in
617
+
if (!g_app.is_logged_in) {
618
+
g_app.message = "Please log in first";
619
+
return;
620
+
}
621
+
622
+
// Check if we have post content
623
+
if (g_app.post_text_len == 0) {
624
+
g_app.message = "Please enter post content first";
625
+
return;
626
+
}
627
+
628
+
// Check if we're already busy with a network operation
629
+
if (!network.isNetworkIdle()) {
630
+
return;
631
+
}
632
+
633
+
// Request network access if not already done
634
+
if (!g_app.network_access_requested) {
635
+
const response = pd.network.playdate_http.requestAccess("https://bsky.social", 443, true, "post", null, null);
636
+
g_app.network_access_requested = true;
637
+
638
+
switch (response) {
639
+
.AccessAllow => {},
640
+
.AccessDeny => {
641
+
g_app.message = "Network access denied";
642
+
return;
643
+
},
644
+
else => {
645
+
g_app.message = "Access request failed";
646
+
return;
647
+
},
648
+
}
649
+
}
650
+
651
+
// Get current timestamp in ISO 8601 format using accurate Playdate API
652
+
const seconds_since_2000 = pd.system.getSecondsSinceEpoch(null);
653
+
654
+
// Convert to PDDateTime for formatting
655
+
var datetime: pdapi.PDDateTime = undefined;
656
+
pd.system.convertEpochToDateTime(seconds_since_2000, &datetime);
657
+
658
+
// Format as ISO 8601 using the utility function
659
+
const timestamp = pdtools.datetimeToISO(datetime);
660
+
661
+
// Check if we have user DID
662
+
if (g_app.user_did_len == 0) {
663
+
g_app.message = "Missing user DID - please re-login";
664
+
return;
665
+
}
666
+
667
+
// Prepare post request body using Playdate JSON encoder
668
+
var post_body: [1024]u8 = undefined;
669
+
var json_context = JsonWriteContext{ .buffer = &post_body, .pos = 0 };
670
+
671
+
var encoder: pdapi.JSONEncoder = undefined;
672
+
pd.json.initEncoder(&encoder, jsonWriteCallback, &json_context, 0);
673
+
674
+
const post_text = g_app.post_text[0..g_app.post_text_len];
675
+
const user_did = g_app.user_did[0..g_app.user_did_len];
676
+
677
+
// Build JSON: {"repo":"...", "collection":"...", "record":{"text":"...", "createdAt":"..."}}
678
+
encoder.startTable(&encoder);
679
+
680
+
// Add repo field
681
+
encoder.addTableMember(&encoder, "repo", 4);
682
+
encoder.writeString(&encoder, @as([*c]const u8, @ptrCast(user_did.ptr)), @as(c_int, @intCast(user_did.len)));
683
+
684
+
// Add collection field
685
+
encoder.addTableMember(&encoder, "collection", 10);
686
+
encoder.writeString(&encoder, "app.bsky.feed.post", 18);
687
+
688
+
// Add record field
689
+
encoder.addTableMember(&encoder, "record", 6);
690
+
encoder.startTable(&encoder);
691
+
692
+
// Add text field to record
693
+
encoder.addTableMember(&encoder, "text", 4);
694
+
encoder.writeString(&encoder, @as([*c]const u8, @ptrCast(post_text.ptr)), @as(c_int, @intCast(post_text.len)));
695
+
696
+
// Add createdAt field to record
697
+
encoder.addTableMember(&encoder, "createdAt", 9);
698
+
encoder.writeString(&encoder, @as([*c]const u8, @ptrCast(timestamp.ptr)), @as(c_int, @intCast(timestamp.len)));
699
+
700
+
encoder.endTable(&encoder); // End record
701
+
encoder.endTable(&encoder); // End root
702
+
703
+
const post_json = post_body[0..json_context.pos];
704
+
705
+
// Log json
706
+
pdtools.logLargeMessage(pd, post_json, post_json.len);
707
+
708
+
// Create HTTP request
709
+
if (defs.DEBUG_DONT_POST) {
710
+
g_app.message = "Debug mode - not posting";
711
+
return;
712
+
}
713
+
714
+
const bearer_token = g_app.access_token[0..g_app.access_token_len];
715
+
const request = network.HttpRequest{
716
+
.method = .POST,
717
+
.url = "https://bsky.social/xrpc/com.atproto.repo.createRecord",
718
+
.server = "bsky.social",
719
+
.port = 443,
720
+
.path = "/xrpc/com.atproto.repo.createRecord",
721
+
.use_https = true,
722
+
.bearer_token = bearer_token,
723
+
.body = post_json,
724
+
.response_buffer = &g_app.response_buffer,
725
+
.success_callback = onPostSuccess,
726
+
.failure_callback = onPostFailure,
727
+
};
728
+
729
+
if (network.makeHttpRequest(pd, request)) {
730
+
g_app.message = "Posting...";
731
+
} else {
732
+
g_app.message = "Failed to start post request";
733
+
}
734
+
}
735
+
736
+
// Post keyboard callbacks
737
+
fn postKeyboardCancelled() void {
738
+
g_app.playdate.system.logToConsole("Post input cancelled");
739
+
// Stay on post page, don't change anything
740
+
}
741
+
742
+
// New callback for text editing that doesn't auto-submit
743
+
fn postTextEditConfirmed(text: []const u8) void {
744
+
g_app.playdate.system.logToConsole("Post text edited (length: %d)", @as(c_int, @intCast(text.len)));
745
+
746
+
// Copy post text but don't submit automatically
747
+
const copy_len = @min(text.len, g_app.post_text.len - 1);
748
+
@memcpy(g_app.post_text[0..copy_len], text[0..copy_len]);
749
+
g_app.post_text[copy_len] = 0;
750
+
g_app.post_text_len = copy_len;
751
+
752
+
g_app.message = "Post text updated - use confirm button to post";
753
+
}
754
+
755
+
fn update_and_render(userdata: ?*anyopaque) callconv(.C) c_int {
756
+
_ = userdata; // Ignore userdata, use global state
757
+
758
+
// Safety check - ensure app is initialized
759
+
const pd = g_app.playdate;
760
+
761
+
// Update network processing
762
+
network.updateNetwork(pd);
763
+
764
+
if (g_app.keyboard.updateAndRender()) {
765
+
return 1;
766
+
}
767
+
768
+
// Handle button input
769
+
var current: pdapi.PDButtons = undefined;
770
+
var pushed: pdapi.PDButtons = undefined;
771
+
var released: pdapi.PDButtons = undefined;
772
+
pd.system.getButtonState(¤t, &pushed, &released);
773
+
774
+
// Handle keyboard input first if keyboard is active
775
+
776
+
// Handle up/down navigation for login field selection
777
+
if (g_selected_page == .login and !g_app.is_logged_in) {
778
+
if (pushed & pdapi.BUTTON_UP != 0) {
779
+
g_app.login_field_selection = switch (g_app.login_field_selection) {
780
+
.username => .login_button,
781
+
.password => .username,
782
+
.login_button => .password,
783
+
};
784
+
} else if (pushed & pdapi.BUTTON_DOWN != 0) {
785
+
g_app.login_field_selection = switch (g_app.login_field_selection) {
786
+
.username => .password,
787
+
.password => .login_button,
788
+
.login_button => .username,
789
+
};
790
+
}
791
+
}
792
+
793
+
// Handle up/down navigation for post field selection
794
+
if (g_selected_page == .post and g_app.is_logged_in) {
795
+
if (pushed & pdapi.BUTTON_UP != 0) {
796
+
g_app.post_field_selection = switch (g_app.post_field_selection) {
797
+
.edit_text => .confirm_post,
798
+
.confirm_post => .edit_text,
799
+
};
800
+
} else if (pushed & pdapi.BUTTON_DOWN != 0) {
801
+
g_app.post_field_selection = switch (g_app.post_field_selection) {
802
+
.edit_text => .confirm_post,
803
+
.confirm_post => .edit_text,
804
+
};
805
+
}
806
+
}
807
+
808
+
// Check if left/right was pressed to change page selection
809
+
if (pushed & pdapi.BUTTON_LEFT != 0) {
810
+
g_selected_page = switch (g_selected_page) {
811
+
.login => if (g_app.is_logged_in) .post else .login,
812
+
.home => .login,
813
+
.post => .home,
814
+
};
815
+
} else if (pushed & pdapi.BUTTON_RIGHT != 0) {
816
+
g_selected_page = switch (g_selected_page) {
817
+
.login => if (g_app.is_logged_in) .home else .login,
818
+
.home => if (g_app.is_logged_in) .post else .login,
819
+
.post => .login,
820
+
};
821
+
}
822
+
823
+
// Check if B button was pushed (only if keyboard not active)
824
+
if (pushed & pdapi.BUTTON_B != 0) {
825
+
if (g_selected_page == .login and !g_app.is_logged_in) {
826
+
// Handle login input based on selected field
827
+
switch (g_app.login_field_selection) {
828
+
.username => {
829
+
// Start username input
830
+
const initial_text = if (g_app.username[0] != 0) std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.username))) else null;
831
+
g_app.keyboard.start(
832
+
"Enter username:",
833
+
g_app.username.len,
834
+
initial_text,
835
+
false,
836
+
1,
837
+
usernameKeyboardCancelled,
838
+
usernameKeyboardConfirmed,
839
+
);
840
+
},
841
+
.password => {
842
+
// Start password input
843
+
const initial_text = if (g_app.password[0] != 0) std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.password))) else null;
844
+
g_app.keyboard.start(
845
+
"Enter password:",
846
+
g_app.password.len,
847
+
initial_text,
848
+
false,
849
+
1,
850
+
passwordKeyboardCancelled,
851
+
passwordKeyboardConfirmed,
852
+
);
853
+
},
854
+
.login_button => {
855
+
// Check if we can login
856
+
if (g_app.username[0] > 0 and g_app.password[0] > 0) {
857
+
loginToBluesky();
858
+
} else {
859
+
g_app.message = "Please enter username and password";
860
+
}
861
+
},
862
+
}
863
+
} else if (g_selected_page == .home) {
864
+
fetchBlueskyFeed();
865
+
} else if (g_selected_page == .post) {
866
+
// Check if user is logged in before allowing post creation
867
+
if (!g_app.is_logged_in) {
868
+
g_app.message = "Please log in first to create posts";
869
+
g_selected_page = .login;
870
+
} else {
871
+
// Handle post field selection
872
+
switch (g_app.post_field_selection) {
873
+
.edit_text => {
874
+
// Start post composition with keyboard (multiline enabled)
875
+
const initial_text = if (g_app.post_text_len > 0) g_app.post_text[0..g_app.post_text_len] else null;
876
+
g_app.keyboard.start("Enter your post:", 300, initial_text, true, 5, postKeyboardCancelled, postTextEditConfirmed);
877
+
},
878
+
.confirm_post => {
879
+
// Submit the post directly
880
+
if (g_app.post_text_len > 0) {
881
+
submitBlueskyPost();
882
+
} else {
883
+
g_app.message = "Please enter some text first";
884
+
}
885
+
},
886
+
}
887
+
}
888
+
}
889
+
}
890
+
891
+
// Render keyboard if active, otherwise render the selected page
892
+
switch (g_selected_page) {
893
+
.login => render_login(0, 20, LCD_WIDTH, LCD_HEIGHT - 20),
894
+
.home => render_home(0, 20, LCD_WIDTH, LCD_HEIGHT - 20),
895
+
.post => render_create_post(0, 20, LCD_WIDTH, LCD_HEIGHT - 20),
896
+
}
897
+
render_header(0, 0, LCD_WIDTH);
898
+
899
+
return 1;
900
+
}
901
+
902
+
fn render_header(x: c_int, y: c_int, w: c_int) void {
903
+
const pd = g_app.playdate;
904
+
905
+
const header_height = pd.graphics.getFontHeight(fonts.g_font) + MARGIN * 2;
906
+
907
+
// Draw header background
908
+
pd.graphics.fillRect(
909
+
x,
910
+
y,
911
+
w + MARGIN * 2,
912
+
header_height,
913
+
@intFromEnum(pdapi.LCDSolidColor.ColorBlack),
914
+
);
915
+
916
+
// Set draw mode for white text on black background
917
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillWhite);
918
+
919
+
// Draw header text
920
+
const header_text = "Bluesky";
921
+
_ = pd.graphics.drawText(
922
+
header_text.ptr,
923
+
header_text.len,
924
+
pdapi.PDStringEncoding.UTF8Encoding,
925
+
x + MARGIN,
926
+
y + MARGIN,
927
+
);
928
+
929
+
const text_width = pd.graphics.getTextWidth(
930
+
fonts.g_font,
931
+
header_text.ptr,
932
+
header_text.len,
933
+
pdapi.PDStringEncoding.UTF8Encoding,
934
+
0,
935
+
);
936
+
937
+
// spinning loading indicator.
938
+
// Spinning loading indicator based on network state
939
+
const current_network_state = network.getNetworkState();
940
+
if (current_network_state == .connecting or current_network_state == .requesting) {
941
+
pdtools.renderSpinner(pd, x + w - 20, y + 2, pdapi.LCDSolidColor.ColorWhite);
942
+
}
943
+
944
+
// Reset draw mode to normal
945
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
946
+
947
+
// Show all available pages
948
+
var temp_w: c_int = 0;
949
+
var curr_x = text_width + x + MARGIN * 2;
950
+
951
+
// Login button (always available)
952
+
pdtools.renderButton(
953
+
pd,
954
+
curr_x,
955
+
y,
956
+
"Login",
957
+
g_selected_page == .login,
958
+
&temp_w,
959
+
null,
960
+
);
961
+
962
+
if (g_app.is_logged_in) {
963
+
curr_x += temp_w;
964
+
pdtools.renderButton(
965
+
pd,
966
+
curr_x,
967
+
y,
968
+
"Home",
969
+
g_selected_page == .home,
970
+
&temp_w,
971
+
null,
972
+
);
973
+
974
+
curr_x += temp_w;
975
+
pdtools.renderButton(
976
+
pd,
977
+
curr_x,
978
+
y,
979
+
"Post",
980
+
g_selected_page == .post,
981
+
null,
982
+
null,
983
+
);
984
+
}
985
+
}
986
+
987
+
fn render_post_list(x: c_int, y: c_int, w: c_int, h: c_int) void {
988
+
const scrollbar_width = 10;
989
+
const post_gap = 4;
990
+
const padding = 4;
991
+
const pd = g_app.playdate;
992
+
993
+
// Clip the whole thing so we don't go out of bounds
994
+
pd.graphics.setClipRect(x, y, w, h);
995
+
996
+
const posts_width = w - scrollbar_width - (padding * 2);
997
+
998
+
const start_y = @as(c_int, @intFromFloat(@as(f32, @floatFromInt(y)) - g_app.body_scroll.getValue()));
999
+
var current_y = start_y + padding;
1000
+
const current_x = x + padding;
1001
+
1002
+
// Now render each post with actual positions
1003
+
for (0..g_app.post_count) |i| {
1004
+
const post = &g_app.posts[i];
1005
+
const post_height = bsky_post.renderPost(
1006
+
pd,
1007
+
post,
1008
+
current_x,
1009
+
current_y,
1010
+
posts_width,
1011
+
false,
1012
+
);
1013
+
current_y += @as(c_int, @intCast(post_height));
1014
+
current_y += post_gap;
1015
+
}
1016
+
current_y += padding;
1017
+
1018
+
// current_y is the scrolled position, so we need to add back the scroll offset to get true content height
1019
+
const content_height = current_y - start_y;
1020
+
1021
+
g_app.body_scroll.current_height = @as(f32, @floatFromInt(h));
1022
+
g_app.body_scroll.min_value = 0;
1023
+
g_app.body_scroll.max_value = @floatFromInt(content_height);
1024
+
1025
+
// Render scrollbar on the right side (below header, taking remaining height)
1026
+
const scrollbar_x = pdapi.LCD_COLUMNS - scrollbar_width;
1027
+
pdtools.renderScrollbar(
1028
+
pd,
1029
+
scrollbar_x,
1030
+
y,
1031
+
scrollbar_width,
1032
+
h,
1033
+
g_app.body_scroll.getValue(),
1034
+
@as(f32, @floatFromInt(content_height)),
1035
+
@as(f32, @floatFromInt(h)),
1036
+
);
1037
+
1038
+
pd.graphics.clearClipRect();
1039
+
}
1040
+
1041
+
fn render_home(x: i32, y: i32, w: usize, h: usize) void {
1042
+
const pd = g_app.playdate;
1043
+
1044
+
// #region Body
1045
+
1046
+
// Update scrolling value with crank input
1047
+
g_app.body_scroll.update();
1048
+
1049
+
pd.graphics.fillRect(
1050
+
@intCast(x),
1051
+
@intCast(y),
1052
+
@intCast(w),
1053
+
@intCast(h),
1054
+
@intFromEnum(pdapi.LCDSolidColor.ColorWhite),
1055
+
);
1056
+
1057
+
// Set draw mode for black text on white background
1058
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillBlack);
1059
+
1060
+
// Show login prompt if not logged in
1061
+
if (!g_app.is_logged_in) {
1062
+
_ = pd.graphics.drawText("Please log in first", 19, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, y + MARGIN);
1063
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
1064
+
return;
1065
+
}
1066
+
1067
+
// Get current network state
1068
+
const current_network_state = network.getNetworkState();
1069
+
1070
+
// Show loading progress if network is active
1071
+
if (current_network_state == .connecting or current_network_state == .requesting) {
1072
+
const progress_msg = "Loading feed...";
1073
+
_ = pd.graphics.drawText(progress_msg.ptr, progress_msg.len, pdapi.PDStringEncoding.UTF8Encoding, x, y);
1074
+
1075
+
// Reset draw mode to normal
1076
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
1077
+
return;
1078
+
}
1079
+
1080
+
// If we have parsed posts, display them formatted
1081
+
if (g_app.post_count > 0) {
1082
+
render_post_list(@intCast(x), @intCast(y), @intCast(w), @intCast(h));
1083
+
} else if (current_network_state == .success or current_network_state == .idle) {
1084
+
// Show message when no posts are available
1085
+
const no_posts_msg = "No posts found - Press B to refresh";
1086
+
_ = pd.graphics.drawText(no_posts_msg.ptr, no_posts_msg.len, pdapi.PDStringEncoding.UTF8Encoding, x, y);
1087
+
} else {
1088
+
// Show current status message
1089
+
_ = pd.graphics.drawText(g_app.message.ptr, g_app.message.len, pdapi.PDStringEncoding.UTF8Encoding, x, y);
1090
+
}
1091
+
1092
+
// Reset draw mode to normal
1093
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
1094
+
}
1095
+
1096
+
fn render_login(x: c_int, y: c_int, w: c_int, h: c_int) void {
1097
+
const pd = g_app.playdate;
1098
+
1099
+
pd.graphics.fillRect(x, y, w, h, @intFromEnum(pdapi.LCDSolidColor.ColorWhite));
1100
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillBlack);
1101
+
1102
+
const line_height = pd.graphics.getFontHeight(fonts.g_font) + 4;
1103
+
var current_y = y + MARGIN;
1104
+
1105
+
const current_network_state = network.getNetworkState();
1106
+
1107
+
if (current_network_state == .requesting or current_network_state == .connecting) {
1108
+
_ = pd.graphics.drawText("Logging in...", 12, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1109
+
} else if (current_network_state == .network_error) {
1110
+
_ = pd.graphics.drawText("Login failed!", 13, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1111
+
current_y += line_height;
1112
+
_ = pd.graphics.drawText("Press B to retry", 16, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1113
+
} else {
1114
+
_ = pd.graphics.drawText("Welcome to Bluesky!", 20, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1115
+
current_y += line_height * 2;
1116
+
1117
+
// Show current username with selection indicator
1118
+
var username_text: [200]u8 = undefined;
1119
+
const username_prefix = if (g_app.login_field_selection == .username) pdtools.SELECTION_ARROW ++ " Username: " else " Username: ";
1120
+
const username_value = if (g_app.username[0] != 0) std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.username))) else "(not entered)";
1121
+
const username_display = std.fmt.bufPrint(&username_text, "{s}{s}", .{ username_prefix, username_value }) catch "Username: (error)";
1122
+
_ = pd.graphics.drawText(username_display.ptr, username_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1123
+
current_y += line_height;
1124
+
1125
+
// Show password status with selection indicator
1126
+
const password_prefix = if (g_app.login_field_selection == .password) pdtools.SELECTION_ARROW ++ " Password: " else " Password: ";
1127
+
var password_text: [200]u8 = undefined;
1128
+
const password_display = if (g_app.password[0] != 0) blk: {
1129
+
// Generate dots based on actual password length using safe ASCII dots
1130
+
var dots_buffer: [64]u8 = undefined;
1131
+
const password_span = std.mem.span(@as([*:0]const u8, @ptrCast(&g_app.password)));
1132
+
const dots_len = @min(password_span.len, dots_buffer.len);
1133
+
@memset(dots_buffer[0..dots_len], '*');
1134
+
const dots_str = dots_buffer[0..dots_len];
1135
+
break :blk std.fmt.bufPrint(&password_text, "{s}{s}", .{ password_prefix, dots_str }) catch "Password: (error)";
1136
+
} else std.fmt.bufPrint(&password_text, "{s}(not entered)", .{password_prefix}) catch "Password: (error)";
1137
+
_ = pd.graphics.drawText(password_display.ptr, password_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1138
+
current_y += line_height;
1139
+
1140
+
// Show login button with selection indicator
1141
+
const login_prefix = if (g_app.login_field_selection == .login_button) pdtools.SELECTION_ARROW ++ " " else " ";
1142
+
const login_text = if (g_app.username[0] != 0 and g_app.password[0] != 0) "Login" else "Login (enter credentials first)";
1143
+
var login_button_text: [200]u8 = undefined;
1144
+
const login_display = std.fmt.bufPrint(&login_button_text, "{s}{s}", .{ login_prefix, login_text }) catch "Login";
1145
+
_ = pd.graphics.drawText(login_display.ptr, login_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1146
+
current_y += line_height * 2;
1147
+
1148
+
// Show navigation instructions
1149
+
const instruction = pdtools.UP_ARROW_EMOJI ++ pdtools.DOWN_ARROW_EMOJI ++ ": Select field B: Edit/Login";
1150
+
_ = pd.graphics.drawText(instruction.ptr, instruction.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1151
+
current_y += line_height;
1152
+
}
1153
+
1154
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
1155
+
}
1156
+
1157
+
fn render_create_post(x: c_int, y: c_int, w: c_int, h: c_int) void {
1158
+
const pd = g_app.playdate;
1159
+
1160
+
pd.graphics.fillRect(x, y, w, h, @intFromEnum(pdapi.LCDSolidColor.ColorWhite));
1161
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillBlack);
1162
+
1163
+
const line_height = pd.graphics.getFontHeight(fonts.g_font) + 4;
1164
+
var current_y = y + MARGIN;
1165
+
1166
+
// Check if user is logged in
1167
+
if (!g_app.is_logged_in) {
1168
+
_ = pd.graphics.drawText("Please log in first to create posts", 34, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1169
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
1170
+
return;
1171
+
}
1172
+
1173
+
// Post composition page
1174
+
_ = pd.graphics.drawText("Create a Post", 13, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1175
+
current_y += line_height;
1176
+
1177
+
// Show character count
1178
+
var char_count_text: [50]u8 = undefined;
1179
+
const char_count_display = std.fmt.bufPrint(&char_count_text, "Characters: {d}/300", .{g_app.post_text_len}) catch "Characters: 0/300";
1180
+
_ = pd.graphics.drawText(char_count_display.ptr, char_count_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1181
+
current_y += line_height;
1182
+
1183
+
// Show current post text if any
1184
+
if (g_app.post_text_len > 0) {
1185
+
_ = pd.graphics.drawText("Current post:", 13, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1186
+
current_y += line_height;
1187
+
1188
+
// Draw a border around the post text area
1189
+
const text_area_height = line_height * 6; // Space for multiple lines
1190
+
pd.graphics.drawRect(x + MARGIN, current_y, w - MARGIN * 2, text_area_height, @intFromEnum(pdapi.LCDSolidColor.ColorBlack));
1191
+
1192
+
// Render multiline post text
1193
+
const post_text = g_app.post_text[0..g_app.post_text_len];
1194
+
var text_y = current_y + 4;
1195
+
var line_start: usize = 0;
1196
+
var displayed_lines: usize = 0;
1197
+
const max_lines = 5;
1198
+
1199
+
for (post_text, 0..) |char, i| {
1200
+
if (char == '\n' or i == post_text.len - 1) {
1201
+
if (displayed_lines >= max_lines) break;
1202
+
1203
+
const line_end = if (char == '\n') i else i + 1;
1204
+
const line_text = post_text[line_start..line_end];
1205
+
1206
+
_ = pd.graphics.drawText(line_text.ptr, line_text.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN + 4, text_y);
1207
+
text_y += line_height;
1208
+
displayed_lines += 1;
1209
+
line_start = i + 1;
1210
+
}
1211
+
}
1212
+
1213
+
// If no newlines, display as single line
1214
+
if (displayed_lines == 0) {
1215
+
_ = pd.graphics.drawText(post_text.ptr, post_text.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN + 4, current_y + 4);
1216
+
}
1217
+
1218
+
current_y += text_area_height + line_height;
1219
+
} else {
1220
+
current_y += line_height * 2;
1221
+
_ = pd.graphics.drawText("No post drafted yet", 19, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1222
+
current_y += line_height * 2;
1223
+
}
1224
+
1225
+
// Show field selection options
1226
+
const edit_prefix = if (g_app.post_field_selection == .edit_text) pdtools.SELECTION_ARROW ++ " " else " ";
1227
+
var edit_text: [100]u8 = undefined;
1228
+
const edit_display = std.fmt.bufPrint(&edit_text, "{s}Edit Post Text", .{edit_prefix}) catch "Edit Post Text";
1229
+
_ = pd.graphics.drawText(edit_display.ptr, edit_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1230
+
current_y += line_height;
1231
+
1232
+
const confirm_prefix = if (g_app.post_field_selection == .confirm_post) pdtools.SELECTION_ARROW ++ " " else " ";
1233
+
var confirm_text: [100]u8 = undefined;
1234
+
const confirm_display = std.fmt.bufPrint(&confirm_text, "{s}Confirm & Post", .{confirm_prefix}) catch "Confirm & Post";
1235
+
_ = pd.graphics.drawText(confirm_display.ptr, confirm_display.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1236
+
current_y += line_height * 2;
1237
+
1238
+
// Show navigation instructions
1239
+
const instruction = pdtools.UP_ARROW_EMOJI ++ pdtools.DOWN_ARROW_EMOJI ++ ": Select action B: Execute " ++ pdtools.LEFT_ARROW_EMOJI ++ pdtools.RIGHT_ARROW_EMOJI ++ ": Navigate tabs";
1240
+
_ = pd.graphics.drawText(instruction.ptr, instruction.len, pdapi.PDStringEncoding.UTF8Encoding, x + MARGIN, current_y);
1241
+
1242
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
1243
+
}
+316
src/network.zig
+316
src/network.zig
···
1
+
const std = @import("std");
2
+
const pdapi = @import("playdate_api_definitions.zig");
3
+
4
+
pub const NetworkState = enum {
5
+
idle,
6
+
connecting,
7
+
authenticating,
8
+
requesting,
9
+
fetching_posts,
10
+
success,
11
+
network_error,
12
+
};
13
+
14
+
// HTTP method types
15
+
pub const HttpMethod = enum {
16
+
GET,
17
+
POST,
18
+
PUT,
19
+
DELETE,
20
+
PATCH,
21
+
};
22
+
23
+
// HTTP status code ranges
24
+
pub const HttpStatusCode = u16;
25
+
26
+
// Callback function types
27
+
pub const SuccessCallback = *const fn (status_code: HttpStatusCode, response_data: []const u8) void;
28
+
pub const FailureCallback = *const fn (status_code: HttpStatusCode, error_message: []const u8) void;
29
+
30
+
// Request configuration
31
+
pub const HttpRequest = struct {
32
+
method: HttpMethod,
33
+
url: []const u8,
34
+
server: []const u8,
35
+
port: u16,
36
+
path: []const u8,
37
+
use_https: bool,
38
+
bearer_token: ?[]const u8,
39
+
body: ?[]const u8,
40
+
response_buffer: []u8,
41
+
success_callback: ?SuccessCallback,
42
+
failure_callback: ?FailureCallback,
43
+
};
44
+
45
+
// Network request state
46
+
pub const NetworkRequestState = struct {
47
+
state: NetworkState,
48
+
connection: ?*pdapi.HTTPConnection,
49
+
request: ?HttpRequest,
50
+
bytes_read: usize,
51
+
is_reading_chunks: bool,
52
+
total_bytes_read: usize,
53
+
expected_content_length: usize,
54
+
network_access_requested: bool,
55
+
};
56
+
57
+
// Global network state
58
+
var g_network_state = NetworkRequestState{
59
+
.state = .idle,
60
+
.connection = null,
61
+
.request = null,
62
+
.bytes_read = 0,
63
+
.is_reading_chunks = false,
64
+
.total_bytes_read = 0,
65
+
.expected_content_length = 0,
66
+
.network_access_requested = false,
67
+
};
68
+
69
+
// Global buffers to avoid large stack allocations
70
+
var g_headers_buffer: [512]u8 = undefined;
71
+
72
+
/// Convert HttpMethod enum to string
73
+
fn httpMethodToString(method: HttpMethod) []const u8 {
74
+
return switch (method) {
75
+
.GET => "GET",
76
+
.POST => "POST",
77
+
.PUT => "PUT",
78
+
.DELETE => "DELETE",
79
+
.PATCH => "PATCH",
80
+
};
81
+
}
82
+
83
+
/// Create HTTP connection with proper configuration
84
+
pub fn createConnection(playdate: *pdapi.PlaydateAPI, server: []const u8, port: u16, use_https: bool) ?*pdapi.HTTPConnection {
85
+
// Create a null-terminated buffer for the server name
86
+
var server_buffer: [256]u8 = undefined;
87
+
if (server.len >= server_buffer.len) {
88
+
playdate.system.logToConsole("ERROR: Server name too long: %d", @as(c_int, @intCast(server.len)));
89
+
return null;
90
+
}
91
+
@memcpy(server_buffer[0..server.len], server);
92
+
server_buffer[server.len] = 0;
93
+
94
+
playdate.system.logToConsole("Attempting to create connection to %s:%d", @as([*:0]const u8, @ptrCast(&server_buffer)), port);
95
+
if (use_https) {
96
+
playdate.system.logToConsole("Using HTTPS connection");
97
+
} else {
98
+
playdate.system.logToConsole("Using HTTP connection");
99
+
}
100
+
const connection = playdate.network.playdate_http.newConnection(@as([*:0]const u8, @ptrCast(&server_buffer)), port, use_https);
101
+
102
+
// Set connection timeout - increase to 30 seconds for more reliability
103
+
playdate.network.playdate_http.setConnectTimeout(connection, 30000); // 30 seconds
104
+
playdate.network.playdate_http.setReadTimeout(connection, 30000); // 30 seconds
105
+
playdate.system.logToConsole("HTTP connection created with 30s timeouts");
106
+
107
+
return connection;
108
+
}
109
+
110
+
/// Start an HTTP request with optional bearer token
111
+
pub fn makeHttpRequest(playdate: *pdapi.PlaydateAPI, request: HttpRequest) bool {
112
+
// Check if already processing a request
113
+
if (g_network_state.state != .idle) {
114
+
playdate.system.logToConsole("ERROR: Network request already in progress");
115
+
return false;
116
+
}
117
+
118
+
// Create connection
119
+
const connection = createConnection(playdate, request.server, request.port, request.use_https);
120
+
if (connection == null) {
121
+
return false;
122
+
}
123
+
124
+
// Store request info
125
+
g_network_state.connection = connection;
126
+
g_network_state.request = request;
127
+
g_network_state.state = .connecting;
128
+
g_network_state.bytes_read = 0;
129
+
g_network_state.total_bytes_read = 0;
130
+
g_network_state.is_reading_chunks = false;
131
+
132
+
// Build headers
133
+
var headers_len: usize = 0;
134
+
135
+
// Add Authorization header if bearer token provided
136
+
if (request.bearer_token) |token| {
137
+
const auth_header = std.fmt.bufPrint(g_headers_buffer[headers_len..], "Authorization: Bearer {s}\r\n", .{token}) catch {
138
+
playdate.system.logToConsole("ERROR: Failed to format authorization header");
139
+
return false;
140
+
};
141
+
headers_len += auth_header.len;
142
+
}
143
+
144
+
// Add Content-Type header for POST/PUT/PATCH requests
145
+
if (request.method == .POST or request.method == .PUT or request.method == .PATCH) {
146
+
const content_type = std.fmt.bufPrint(g_headers_buffer[headers_len..], "Content-Type: application/json\r\n", .{}) catch {
147
+
playdate.system.logToConsole("ERROR: Failed to format content-type header");
148
+
return false;
149
+
};
150
+
headers_len += content_type.len;
151
+
}
152
+
153
+
// Null terminate headers
154
+
if (headers_len < g_headers_buffer.len) {
155
+
g_headers_buffer[headers_len] = 0;
156
+
}
157
+
158
+
// Send request using query method
159
+
const method_str = httpMethodToString(request.method);
160
+
const headers_ptr = if (headers_len > 0) @as([*:0]const u8, @ptrCast(&g_headers_buffer)) else null;
161
+
const body_ptr = if (request.body) |body| @as([*:0]const u8, @ptrCast(body.ptr)) else null;
162
+
const body_len = if (request.body) |body| body.len else 0;
163
+
164
+
const result = playdate.network.playdate_http.query(connection, @as([*:0]const u8, @ptrCast(method_str.ptr)), @as([*:0]const u8, @ptrCast(request.path.ptr)), headers_ptr, if (headers_len > 0) headers_len else 0, body_ptr, body_len);
165
+
166
+
if (result != .NET_OK) {
167
+
playdate.system.logToConsole("ERROR: Failed to send HTTP request");
168
+
g_network_state.state = .network_error;
169
+
return false;
170
+
}
171
+
172
+
g_network_state.state = .requesting;
173
+
playdate.system.logToConsole("HTTP %s request sent to %s%s", @as([*:0]const u8, @ptrCast(method_str.ptr)), @as([*:0]const u8, @ptrCast(request.server.ptr)), @as([*:0]const u8, @ptrCast(request.path.ptr)));
174
+
175
+
return true;
176
+
}
177
+
178
+
/// Process network responses and handle callbacks
179
+
pub fn updateNetwork(playdate: *pdapi.PlaydateAPI) void {
180
+
if (g_network_state.state == .idle or g_network_state.connection == null) {
181
+
return;
182
+
}
183
+
184
+
const connection = g_network_state.connection.?;
185
+
_ = g_network_state.request.?;
186
+
187
+
// Check for errors
188
+
const net_error = playdate.network.playdate_http.getError(connection);
189
+
if (net_error != .NET_OK) {
190
+
playdate.system.logToConsole("ERROR: HTTP network error: %d", @intFromEnum(net_error));
191
+
handleNetworkError(playdate, "Network error");
192
+
return;
193
+
}
194
+
195
+
// Read available data if we're in requesting state
196
+
if (g_network_state.state == .requesting) {
197
+
const bytes_available = playdate.network.playdate_http.getBytesAvailable(connection);
198
+
if (bytes_available > 0) {
199
+
readResponseData(playdate);
200
+
} else {
201
+
// Check if request is complete by trying to get response status
202
+
const status_code = playdate.network.playdate_http.getResponseStatus(connection);
203
+
if (status_code > 0) {
204
+
// We have a response, process it
205
+
processResponse(playdate);
206
+
}
207
+
}
208
+
}
209
+
}
210
+
211
+
/// Read response data into the provided buffer
212
+
fn readResponseData(playdate: *pdapi.PlaydateAPI) void {
213
+
const connection = g_network_state.connection.?;
214
+
const request = g_network_state.request.?;
215
+
const available_space = request.response_buffer.len - g_network_state.bytes_read;
216
+
217
+
if (available_space == 0) {
218
+
playdate.system.logToConsole("WARNING: Response buffer full, stopping read");
219
+
processResponse(playdate);
220
+
return;
221
+
}
222
+
223
+
const bytes_to_read = @min(available_space, 1024); // Read in chunks of 1KB or less
224
+
const buffer_ptr = request.response_buffer[g_network_state.bytes_read .. g_network_state.bytes_read + bytes_to_read];
225
+
226
+
const bytes_read = playdate.network.playdate_http.read(connection, @as([*]u8, @ptrCast(buffer_ptr.ptr)), @intCast(bytes_to_read));
227
+
228
+
if (bytes_read > 0) {
229
+
g_network_state.bytes_read += @intCast(bytes_read);
230
+
g_network_state.total_bytes_read += @intCast(bytes_read);
231
+
playdate.system.logToConsole("Read %d bytes, total: %d", bytes_read, g_network_state.total_bytes_read);
232
+
}
233
+
}
234
+
235
+
/// Process the completed response and call appropriate callback
236
+
fn processResponse(playdate: *pdapi.PlaydateAPI) void {
237
+
const connection = g_network_state.connection.?;
238
+
const request = g_network_state.request.?;
239
+
240
+
// Get HTTP status code
241
+
const status_code = playdate.network.playdate_http.getResponseStatus(connection);
242
+
playdate.system.logToConsole("HTTP response status: %d", status_code);
243
+
244
+
// Null-terminate response data
245
+
if (g_network_state.bytes_read < request.response_buffer.len) {
246
+
request.response_buffer[g_network_state.bytes_read] = 0;
247
+
}
248
+
249
+
const response_data = request.response_buffer[0..g_network_state.bytes_read];
250
+
251
+
// Determine if this is a success (2xx) or failure (4xx/5xx)
252
+
if (status_code >= 200 and status_code < 300) {
253
+
// Success (2xx)
254
+
g_network_state.state = .success;
255
+
if (request.success_callback) |callback| {
256
+
callback(@intCast(status_code), response_data);
257
+
}
258
+
} else if (status_code >= 400) {
259
+
// Client/Server error (4xx/5xx)
260
+
g_network_state.state = .network_error;
261
+
if (request.failure_callback) |callback| {
262
+
callback(@intCast(status_code), response_data);
263
+
}
264
+
} else {
265
+
// Other status codes (1xx, 3xx) - treat as success for now
266
+
g_network_state.state = .success;
267
+
if (request.success_callback) |callback| {
268
+
callback(@intCast(status_code), response_data);
269
+
}
270
+
}
271
+
272
+
// Cleanup
273
+
cleanup(playdate);
274
+
}
275
+
276
+
/// Handle network errors
277
+
fn handleNetworkError(playdate: *pdapi.PlaydateAPI, error_message: []const u8) void {
278
+
g_network_state.state = .network_error;
279
+
280
+
if (g_network_state.request) |request| {
281
+
if (request.failure_callback) |callback| {
282
+
callback(0, error_message); // Status code 0 indicates network error
283
+
}
284
+
}
285
+
286
+
cleanup(playdate);
287
+
}
288
+
289
+
/// Clean up network resources
290
+
fn cleanup(playdate: *pdapi.PlaydateAPI) void {
291
+
if (g_network_state.connection) |connection| {
292
+
playdate.network.playdate_http.close(connection);
293
+
g_network_state.connection = null;
294
+
}
295
+
296
+
g_network_state.request = null;
297
+
g_network_state.bytes_read = 0;
298
+
g_network_state.total_bytes_read = 0;
299
+
g_network_state.is_reading_chunks = false;
300
+
g_network_state.state = .idle;
301
+
}
302
+
303
+
/// Get current network state
304
+
pub fn getNetworkState() NetworkState {
305
+
return g_network_state.state;
306
+
}
307
+
308
+
/// Check if network is idle
309
+
pub fn isNetworkIdle() bool {
310
+
return g_network_state.state == .idle;
311
+
}
312
+
313
+
/// Force cleanup network resources
314
+
pub fn forceCleanup(playdate: *pdapi.PlaydateAPI) void {
315
+
cleanup(playdate);
316
+
}
+74
src/panic_handler.zig
+74
src/panic_handler.zig
···
1
+
const std = @import("std");
2
+
const pdapi = @import("playdate_api_definitions.zig");
3
+
const builtin = @import("builtin");
4
+
5
+
var global_playdate: *pdapi.PlaydateAPI = undefined;
6
+
pub fn init(playdate: *pdapi.PlaydateAPI) void {
7
+
global_playdate = playdate;
8
+
}
9
+
10
+
pub fn panic(
11
+
msg: []const u8,
12
+
error_return_trace: ?*std.builtin.StackTrace,
13
+
return_address: ?usize,
14
+
) noreturn {
15
+
_ = error_return_trace;
16
+
_ = return_address;
17
+
18
+
switch (comptime builtin.os.tag) {
19
+
.freestanding => {
20
+
//Playdate hardware
21
+
22
+
//TODO: The Zig std library does not yet support stacktraces on Playdate hardware.
23
+
//We will need to do this manually. Some notes on trying to get it working:
24
+
//Frame pointer is R7
25
+
//Next Frame pointer is *R7
26
+
//Return address is *(R7+4)
27
+
//To print out the trace corrently,
28
+
//We need to know the load address and it doesn't seem to be exactly
29
+
//0x6000_0000 as originally thought
30
+
31
+
global_playdate.system.logToConsole("PANIC: %s", msg.ptr);
32
+
global_playdate.system.@"error"("PANIC: %s", msg.ptr);
33
+
},
34
+
else => {
35
+
//playdate simulator
36
+
var stack_trace_buffer = [_]u8{0} ** 4096;
37
+
var buffer = [_]u8{0} ** 4096;
38
+
var stream = std.io.fixedBufferStream(&stack_trace_buffer);
39
+
40
+
const stack_trace_string = b: {
41
+
if (builtin.strip_debug_info) {
42
+
break :b "Unable to dump stack trace: Debug info stripped";
43
+
}
44
+
const debug_info = std.debug.getSelfDebugInfo() catch |err| {
45
+
const to_print = std.fmt.bufPrintZ(
46
+
&buffer,
47
+
"Unable to dump stack trace: Unable to open debug info: {s}\n",
48
+
.{@errorName(err)},
49
+
) catch break :b "Unable to dump stack trace: Unable to open debug info due unknown error";
50
+
break :b to_print;
51
+
};
52
+
std.debug.writeCurrentStackTrace(
53
+
stream.writer(),
54
+
debug_info,
55
+
.no_color,
56
+
null,
57
+
) catch break :b "Unable to dump stack trace: Unknown error writng stack trace";
58
+
59
+
//NOTE: playdate.system.error (and all Playdate APIs that deal with strings) require a null termination
60
+
const null_char_index = @min(stream.pos, stack_trace_buffer.len - 1);
61
+
stack_trace_buffer[null_char_index] = 0;
62
+
63
+
break :b &stack_trace_buffer;
64
+
};
65
+
global_playdate.system.@"error"(
66
+
"PANIC: %s\n\n%s",
67
+
msg.ptr,
68
+
stack_trace_string.ptr,
69
+
);
70
+
},
71
+
}
72
+
73
+
while (true) {}
74
+
}
+81
src/pdtools/DrawableText.zig
+81
src/pdtools/DrawableText.zig
···
1
+
const pdapi = @import("../playdate_api_definitions.zig");
2
+
3
+
pub const DrawableText = struct {
4
+
playdate: *pdapi.PlaydateAPI,
5
+
text: []const u8,
6
+
max_width: ?c_int = null,
7
+
max_height: ?c_int = null,
8
+
font: ?*pdapi.LCDFont = null,
9
+
wrapping_mode: pdapi.PDTextWrappingMode = .WrapWord,
10
+
alignment: pdapi.PDTextAlignment = .AlignTextLeft,
11
+
12
+
pub fn getWidth(self: *const DrawableText) c_int {
13
+
const natural_width = self.playdate.graphics.getTextWidth(
14
+
self.font,
15
+
@as(?[*:0]const u8, @ptrCast(self.text.ptr)),
16
+
self.text.len,
17
+
pdapi.PDStringEncoding.UTF8Encoding,
18
+
0,
19
+
);
20
+
21
+
// If a max width is set, return the minimum of the two
22
+
if (self.max_width) |max_w| {
23
+
return @min(max_w, natural_width);
24
+
}
25
+
26
+
return natural_width;
27
+
}
28
+
29
+
pub fn getHeight(self: *const DrawableText) c_int {
30
+
const width_constraint = self.max_width orelse 0;
31
+
32
+
const natural_height = self.playdate.graphics.getTextHeightForMaxWidth(
33
+
self.font,
34
+
@as(?[*:0]const u8, @ptrCast(self.text.ptr)),
35
+
self.text.len,
36
+
width_constraint,
37
+
pdapi.PDStringEncoding.UTF8Encoding,
38
+
self.wrapping_mode,
39
+
0,
40
+
0,
41
+
);
42
+
43
+
// If a max height is set, return the minimum of the two
44
+
if (self.max_height) |max_h| {
45
+
return @min(max_h, natural_height);
46
+
}
47
+
48
+
return natural_height;
49
+
}
50
+
51
+
pub fn render(self: *const DrawableText, x: c_int, y: c_int, dry_run: bool) void {
52
+
if (dry_run) return;
53
+
54
+
const width = self.getWidth();
55
+
const height = self.getHeight();
56
+
57
+
if (self.max_width != null) {
58
+
// Use drawTextInRect for constrained rendering
59
+
_ = self.playdate.graphics.drawTextInRect(
60
+
@as(?[*:0]const u8, @ptrCast(self.text.ptr)),
61
+
self.text.len,
62
+
pdapi.PDStringEncoding.UTF8Encoding,
63
+
x,
64
+
y,
65
+
width,
66
+
height,
67
+
self.wrapping_mode,
68
+
self.alignment,
69
+
);
70
+
} else {
71
+
// Use simple drawText for unconstrained rendering
72
+
_ = self.playdate.graphics.drawText(
73
+
@as(?[*:0]const u8, @ptrCast(self.text.ptr)),
74
+
self.text.len,
75
+
pdapi.PDStringEncoding.UTF8Encoding,
76
+
x,
77
+
y,
78
+
);
79
+
}
80
+
}
81
+
};
+160
src/pdtools/ScrollingValue.zig
+160
src/pdtools/ScrollingValue.zig
···
1
+
const pdapi = @import("../playdate_api_definitions.zig");
2
+
3
+
// A representation of a scrolling or sliding value with soft limits and springback behavior
4
+
pub const SlidingValue = struct {
5
+
playdate: *pdapi.PlaydateAPI,
6
+
7
+
// Soft runout distances for the scrolling value
8
+
soft_min_runout: f32,
9
+
soft_max_runout: f32,
10
+
11
+
// Margin before runout callback detection happens
12
+
runout_margin: f32 = 5.0,
13
+
14
+
min_value: f32,
15
+
max_value: f32,
16
+
17
+
current_value: f32,
18
+
current_height: f32,
19
+
20
+
// Rate at which the value springs back to the min/max
21
+
spring_rate: f32 = 0.5,
22
+
23
+
// ms until lack of movement causes spring back to min/max or ticks
24
+
spring_back_ms: u32 = 250,
25
+
26
+
// Called when the scroll max threshold is reached
27
+
scroll_max_threshold_ms: ?u32 = 1000,
28
+
ms_since_scroll_max: u32 = 0,
29
+
onScrollMaxThreshold: ?*const fn () void,
30
+
called_scroll_max: bool = false,
31
+
32
+
// Called when the scroll min threshold is reached
33
+
scroll_min_threshold_ms: ?u32 = 1000,
34
+
ms_since_scroll_min: u32 = 0,
35
+
called_scroll_min: bool = false,
36
+
onScrollMinThreshold: ?*const fn () void,
37
+
38
+
crank_position_offset: f32,
39
+
crank_multiplier: f32 = 1.0,
40
+
41
+
last_update_time: u32 = 0,
42
+
last_movement_time: u32 = 0,
43
+
44
+
/// Updates the scrolling value based on crank input and applies constraints
45
+
pub fn update(self: *SlidingValue) void {
46
+
const current_time = self.playdate.system.getCurrentTimeMilliseconds();
47
+
48
+
const crank_change = self.playdate.system.getCrankChange();
49
+
const adjusted_change = crank_change * self.crank_multiplier;
50
+
51
+
// Update current value
52
+
self.current_value += adjusted_change;
53
+
54
+
// Calculate hard limits based on soft limits + runout distances
55
+
// For max limit, account for viewport height
56
+
const hard_min_limit = self.min_value - self.soft_min_runout;
57
+
const effective_max_soft_limit = self.max_value - self.current_height;
58
+
const hard_max_limit = effective_max_soft_limit + self.soft_max_runout;
59
+
60
+
// Hard clamp to absolute hard limits
61
+
self.current_value = @max(hard_min_limit, @min(hard_max_limit, self.current_value));
62
+
63
+
// Track if we moved
64
+
const moved = @abs(adjusted_change) > 0.01;
65
+
if (moved) {
66
+
self.last_movement_time = current_time;
67
+
}
68
+
69
+
// Handle spring back behavior when beyond soft limits (min_value and max_value)
70
+
// For max value, account for viewport height
71
+
const effective_max_for_spring = self.max_value - self.current_height;
72
+
const time_since_last_movement = current_time - self.last_movement_time;
73
+
if (time_since_last_movement >= self.spring_back_ms) {
74
+
if (self.current_value > effective_max_for_spring) {
75
+
// Spring back towards effective max (soft limit accounting for viewport)
76
+
const spring_force = (self.current_value - effective_max_for_spring) * self.spring_rate;
77
+
self.current_value -= spring_force;
78
+
if (self.current_value < effective_max_for_spring) {
79
+
self.current_value = effective_max_for_spring;
80
+
}
81
+
} else if (self.current_value < self.min_value) {
82
+
// Spring back towards min_value (soft limit)
83
+
const spring_force = (self.min_value - self.current_value) * self.spring_rate;
84
+
self.current_value += spring_force;
85
+
if (self.current_value > self.min_value) {
86
+
self.current_value = self.min_value;
87
+
}
88
+
}
89
+
}
90
+
91
+
// Handle scroll thresholds with margin and debouncing
92
+
// In the new paradigm, min_value and max_value ARE the soft limits
93
+
// Thresholds trigger when beyond these soft limits
94
+
// For max threshold, account for viewport height - the effective max is when content bottom is visible
95
+
const effective_max_value = self.max_value - self.current_height;
96
+
97
+
if (self.current_value >= effective_max_value + self.runout_margin) {
98
+
self.ms_since_scroll_max += current_time - self.last_update_time;
99
+
if (self.scroll_max_threshold_ms) |threshold| {
100
+
if (self.ms_since_scroll_max >= threshold and self.onScrollMaxThreshold != null and !self.called_scroll_max) {
101
+
self.onScrollMaxThreshold.?();
102
+
self.called_scroll_max = true;
103
+
self.ms_since_scroll_max = 0; // Reset after triggering
104
+
}
105
+
}
106
+
} else {
107
+
self.ms_since_scroll_max = 0;
108
+
// Reset debounce flag when we're back within normal range
109
+
if (self.current_value <= effective_max_value - self.runout_margin) {
110
+
self.called_scroll_max = false;
111
+
}
112
+
}
113
+
114
+
if (self.current_value <= self.min_value - self.runout_margin) {
115
+
self.ms_since_scroll_min += current_time - self.last_update_time;
116
+
if (self.scroll_min_threshold_ms) |threshold| {
117
+
if (self.ms_since_scroll_min >= threshold and self.onScrollMinThreshold != null and !self.called_scroll_min) {
118
+
self.onScrollMinThreshold.?();
119
+
self.called_scroll_min = true;
120
+
self.ms_since_scroll_min = 0; // Reset after triggering
121
+
}
122
+
}
123
+
} else {
124
+
self.ms_since_scroll_min = 0;
125
+
// Reset debounce flag when we're back within normal range
126
+
if (self.current_value >= self.min_value + self.runout_margin) {
127
+
self.called_scroll_min = false;
128
+
}
129
+
}
130
+
131
+
self.last_update_time = current_time;
132
+
}
133
+
134
+
/// Sets the current value and resets movement tracking
135
+
pub fn setValue(self: *SlidingValue, value: f32) void {
136
+
// Calculate hard limits and clamp to them, accounting for viewport height
137
+
const hard_min_limit = self.min_value - self.soft_min_runout;
138
+
const effective_max_soft_limit = self.max_value - self.current_height;
139
+
const hard_max_limit = effective_max_soft_limit + self.soft_max_runout;
140
+
self.current_value = @max(hard_min_limit, @min(hard_max_limit, value));
141
+
self.last_movement_time = self.playdate.system.getCurrentTimeMilliseconds();
142
+
143
+
// Reset debounce flags when value is explicitly set
144
+
self.called_scroll_max = false;
145
+
self.called_scroll_min = false;
146
+
self.ms_since_scroll_max = 0;
147
+
self.ms_since_scroll_min = 0;
148
+
}
149
+
150
+
/// Gets the current constrained value
151
+
pub fn getValue(self: *SlidingValue) f32 {
152
+
return self.current_value;
153
+
}
154
+
155
+
/// Returns true if the value is beyond soft limits (min_value and effective max_value)
156
+
pub fn isBeyondSoftLimits(self: *SlidingValue) bool {
157
+
const effective_max_value = self.max_value - self.current_height;
158
+
return self.current_value < self.min_value or self.current_value > effective_max_value;
159
+
}
160
+
};
+198
src/pdtools/index.zig
+198
src/pdtools/index.zig
···
1
+
const pdapi = @import("../playdate_api_definitions.zig");
2
+
const fonts = @import("../fonts.zig");
3
+
const std = @import("std");
4
+
5
+
pub const CRANK_EMOJI = "🎣";
6
+
pub const UP_ARROW_EMOJI = "⬆️";
7
+
pub const DOWN_ARROW_EMOJI = "⬇️";
8
+
pub const LEFT_ARROW_EMOJI = "⬅️";
9
+
pub const RIGHT_ARROW_EMOJI = "➡️";
10
+
pub const SELECTION_ARROW = ">";
11
+
12
+
pub fn drawText(
13
+
pd: *pdapi.PlaydateAPI,
14
+
font: ?*pdapi.LCDFont,
15
+
text: []const u8,
16
+
x: c_int,
17
+
y: c_int,
18
+
out_width: *c_int,
19
+
out_height: *c_int,
20
+
) void {
21
+
out_width.* = pd.graphics.getTextWidth(font, text.ptr, text.len, pdapi.PDStringEncoding.UTF8Encoding, 0);
22
+
out_height.* = pd.graphics.getFontHeight(font);
23
+
24
+
_ = pd.graphics.drawText(
25
+
text.ptr,
26
+
text.len,
27
+
pdapi.PDStringEncoding.UTF8Encoding,
28
+
x,
29
+
y,
30
+
);
31
+
}
32
+
33
+
pub fn logLargeMessage(pd: *pdapi.PlaydateAPI, buffer: []const u8, buffer_size: usize) void {
34
+
35
+
// Log the complete response for debugging
36
+
pd.system.logToConsole("========== COMPLETE JSON RESPONSE START ==========");
37
+
38
+
// Log in chunks to avoid truncation
39
+
const chunk_size = 512; // Log 512 chars at a time
40
+
var offset: usize = 0;
41
+
var chunk_num: usize = 1;
42
+
43
+
while (offset < buffer_size) {
44
+
const remaining = buffer_size - offset;
45
+
const current_chunk_size = @min(chunk_size, remaining);
46
+
47
+
pd.system.logToConsole("%.*s", @as(c_int, @intCast(current_chunk_size)), &buffer[offset]);
48
+
49
+
offset += current_chunk_size;
50
+
chunk_num += 1;
51
+
}
52
+
53
+
pd.system.logToConsole("========== COMPLETE JSON RESPONSE END ==========");
54
+
}
55
+
56
+
pub fn renderScrollbar(
57
+
pd: *pdapi.PlaydateAPI,
58
+
x: c_int,
59
+
y: c_int,
60
+
width: c_int,
61
+
height: c_int,
62
+
scroll_offset: f32,
63
+
content_height: f32,
64
+
viewport_height: f32,
65
+
) void {
66
+
// Calculate scrollbar thumb position and size relative to the given bounds
67
+
const thumb_y = y + @as(c_int, @intFromFloat(scroll_offset / content_height * @as(f32, @floatFromInt(height))));
68
+
const thumb_height = @as(c_int, @intFromFloat(viewport_height / content_height * @as(f32, @floatFromInt(height))));
69
+
70
+
// Draw scrollbar background (white background)
71
+
pd.graphics.fillRect(x, y, width, height, @as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorWhite))));
72
+
73
+
// Draw scrollbar thumb (black thumb)
74
+
pd.graphics.fillRect(x, thumb_y, width, thumb_height, @as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorBlack))));
75
+
}
76
+
77
+
pub fn renderButton(
78
+
pd: *pdapi.PlaydateAPI,
79
+
x: c_int,
80
+
y: c_int,
81
+
label: []const u8,
82
+
active: bool,
83
+
out_width: ?*c_int,
84
+
out_height: ?*c_int,
85
+
) void {
86
+
const MARGIN: c_int = 4;
87
+
const BG_COLOR = if (active) pdapi.LCDSolidColor.ColorWhite else pdapi.LCDSolidColor.ColorBlack;
88
+
89
+
const text_width = pd.graphics.getTextWidth(
90
+
fonts.g_font,
91
+
label.ptr,
92
+
label.len,
93
+
pdapi.PDStringEncoding.UTF8Encoding,
94
+
0,
95
+
);
96
+
const text_height = pd.graphics.getFontHeight(fonts.g_font);
97
+
const btn_width = text_width + MARGIN * 2;
98
+
const btn_height = text_height + MARGIN * 2;
99
+
100
+
if (out_width) |w| w.* = btn_width;
101
+
if (out_height) |h| h.* = btn_height;
102
+
103
+
// Draw button background
104
+
pd.graphics.fillRect(
105
+
x,
106
+
y,
107
+
text_width + MARGIN * 2,
108
+
text_height + MARGIN * 2,
109
+
@as(usize, @intCast(@intFromEnum(BG_COLOR))),
110
+
);
111
+
112
+
pd.graphics.drawRect(
113
+
x,
114
+
y,
115
+
text_width + MARGIN * 2,
116
+
text_height + MARGIN * 2,
117
+
@as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorBlack))),
118
+
);
119
+
120
+
// Draw button label with appropriate draw mode for text color
121
+
if (active) {
122
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillBlack);
123
+
} else {
124
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeFillWhite);
125
+
}
126
+
_ = pd.graphics.drawText(
127
+
label.ptr,
128
+
label.len,
129
+
pdapi.PDStringEncoding.UTF8Encoding,
130
+
x + MARGIN,
131
+
y + MARGIN,
132
+
);
133
+
134
+
// Reset draw mode to normal
135
+
pd.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
136
+
}
137
+
138
+
pub fn renderSpinner(pd: *pdapi.PlaydateAPI, x: c_int, y: c_int, color: ?pdapi.LCDSolidColor) void {
139
+
const time = pd.system.getCurrentTimeMilliseconds();
140
+
const c = color orelse pdapi.LCDSolidColor.ColorBlack;
141
+
const rotation = @as(f32, @floatFromInt(time % 2000)) / 2000.0 * 360.0;
142
+
pd.graphics.drawEllipse(
143
+
x,
144
+
y,
145
+
16,
146
+
16,
147
+
2,
148
+
rotation,
149
+
rotation + 90,
150
+
@as(usize, @intCast(@intFromEnum(c))),
151
+
);
152
+
}
153
+
154
+
pub fn datetimeToISO(datetime: pdapi.PDDateTime) []const u8 {
155
+
var buffer: [32]u8 = undefined;
156
+
const result = std.fmt.bufPrint(
157
+
&buffer,
158
+
"{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.000Z",
159
+
.{ datetime.year, datetime.month, datetime.day, datetime.hour, datetime.minute, datetime.second },
160
+
) catch return "2025-08-27T00:00:00.000Z"; // Fallback
161
+
162
+
return buffer[0..result.len];
163
+
}
164
+
165
+
pub fn saveStringFile(playdate: *pdapi.PlaydateAPI, path: [:0]const u8, data: []const u8) void {
166
+
const file = playdate.file.open(path.ptr, pdapi.FILE_WRITE) orelse {
167
+
playdate.system.logToConsole("Failed to open file for writing: %s", path.ptr);
168
+
return;
169
+
};
170
+
defer _ = playdate.file.close(file);
171
+
172
+
const bytes_written = playdate.file.write(file, data.ptr, @intCast(data.len));
173
+
if (bytes_written != @as(c_int, @intCast(data.len))) {
174
+
playdate.system.logToConsole("Failed to write all data to file");
175
+
}
176
+
}
177
+
178
+
pub fn loadStringFile(playdate: *pdapi.PlaydateAPI, path: [:0]const u8, output: []u8) void {
179
+
const file = playdate.file.open(path.ptr, pdapi.FILE_READ_DATA) orelse {
180
+
const err = playdate.file.geterr();
181
+
playdate.system.logToConsole("Failed to open file for reading: %s, Error: %s", path.ptr, err);
182
+
return;
183
+
};
184
+
defer _ = playdate.file.close(file);
185
+
186
+
const bytes_read = playdate.file.read(file, output.ptr, @intCast(output.len - 1));
187
+
if (bytes_read <= 0) {
188
+
playdate.system.logToConsole("Failed to read from file");
189
+
return;
190
+
}
191
+
192
+
const read_len = @as(usize, @intCast(bytes_read));
193
+
output[read_len] = 0; // Null terminate
194
+
}
195
+
196
+
pub fn delStringFile(playdate: *pdapi.PlaydateAPI, path: [:0]const u8) void {
197
+
_ = playdate.file.unlink(path.ptr, 0);
198
+
}
+633
src/pdtools/keyboard.zig
+633
src/pdtools/keyboard.zig
···
1
+
const pdapi = @import("../playdate_api_definitions.zig");
2
+
const std = @import("std");
3
+
const pdtools = @import("index.zig");
4
+
const defs = @import("../definitions.zig");
5
+
6
+
// Keyboard configuration constants
7
+
const VISIBLE_CHARS = 13; // More characters for better context
8
+
const ROW_HEIGHT = 26;
9
+
const CHAR_WIDTH = 25;
10
+
const CHAR_SPACING = 2;
11
+
const START_Y = 80;
12
+
const SCREEN_WIDTH = 400;
13
+
const CENTER_X = SCREEN_WIDTH / 2;
14
+
const BUTTON_WIDTH = 80;
15
+
const BUTTON_HEIGHT = 25;
16
+
const BUTTON_Y_OFFSET = 160;
17
+
18
+
const KeyboardState = enum {
19
+
inactive,
20
+
active,
21
+
completed,
22
+
cancelled,
23
+
};
24
+
25
+
const Selected = enum {
26
+
uppercase,
27
+
lowercase,
28
+
special,
29
+
cancel,
30
+
confirm,
31
+
};
32
+
33
+
// Callback function types
34
+
pub const KeyboardCancelCallback = *const fn () void;
35
+
pub const KeyboardConfirmCallback = *const fn (text: []const u8) void;
36
+
37
+
// Character sets - each row is a complete character set that can be scrolled through
38
+
const UPPERCASE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
39
+
const LOWERCASE_CHARS = "abcdefghijklmnopqrstuvwxyz ";
40
+
const SPECIAL_CHARS = "0123456789!@#$%^&*()_+-=[]{}\\|;':\",./<>?~` ";
41
+
const SPECIAL_CHARS_WITH_NEWLINE = "0123456789!@#$%^&*()_+-=[]{}\\|;':\",./<>?~` \n";
42
+
43
+
const SelectionMode = enum {
44
+
characters,
45
+
buttons,
46
+
};
47
+
48
+
// Keyboard struct definition
49
+
pub fn Keyboard(comptime buffer_size: usize) type {
50
+
return struct {
51
+
playdate: *pdapi.PlaydateAPI,
52
+
font: *pdapi.LCDFont,
53
+
54
+
state: KeyboardState = .inactive,
55
+
selected: Selected = .uppercase,
56
+
selection_mode: SelectionMode = .characters,
57
+
title: [*:0]const u8,
58
+
editor_buffer: [buffer_size]u8,
59
+
output_buffer: [buffer_size]u8 = undefined,
60
+
input_len: usize = 0,
61
+
62
+
// Absolute crank position for smooth scrolling
63
+
crank_position: f32 = 0.0,
64
+
initial_crank_position: f32 = 0.0,
65
+
66
+
// Text cursor position within input_buffer
67
+
cursor_pos: usize = 0,
68
+
69
+
// Callback functions
70
+
cancel_callback: ?KeyboardCancelCallback = null,
71
+
confirm_callback: ?KeyboardConfirmCallback = null,
72
+
73
+
editor_lines: usize = 1,
74
+
allow_newlines: bool = false,
75
+
76
+
max_length: usize = 0,
77
+
78
+
// Sound effects for UI feedback
79
+
click_synth: ?*pdapi.PDSynth = null,
80
+
previous_selected_char_index: usize = 0,
81
+
82
+
pub fn setup(self: *Keyboard(buffer_size), pd: *pdapi.PlaydateAPI, font: *pdapi.LCDFont) void {
83
+
self.playdate = pd;
84
+
self.font = font;
85
+
86
+
// Initialize click sound synth
87
+
self.click_synth = pd.sound.synth.newSynth();
88
+
if (self.click_synth) |synth| {
89
+
// Set up a short pulse wave for clicking
90
+
pd.sound.synth.setWaveform(synth, pdapi.SoundWaveform.kWaveformSquare);
91
+
pd.sound.synth.setVolume(synth, 0.1, 0.1); // Low volume
92
+
}
93
+
}
94
+
95
+
// Initialize and show the keyboard
96
+
pub fn start(
97
+
self: *Keyboard(buffer_size),
98
+
title: []const u8,
99
+
max_length: usize,
100
+
initial_value: ?[]const u8,
101
+
allow_newlines: bool,
102
+
editor_lines: usize,
103
+
cancel_callback: ?KeyboardCancelCallback,
104
+
confirm_callback: ?KeyboardConfirmCallback,
105
+
) void {
106
+
// Clear the screen when entering keyboard mode
107
+
self.playdate.graphics.clear(@as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorWhite))));
108
+
109
+
// Reset keyboard state
110
+
self.state = .active;
111
+
self.selected = .uppercase;
112
+
self.selection_mode = .characters;
113
+
self.input_len = 0;
114
+
self.cursor_pos = 0;
115
+
self.allow_newlines = allow_newlines;
116
+
self.editor_lines = editor_lines;
117
+
self.cancel_callback = cancel_callback;
118
+
self.confirm_callback = confirm_callback;
119
+
120
+
// Set title (assuming title is null-terminated string literal)
121
+
self.title = @as([*:0]const u8, @ptrCast(title.ptr));
122
+
123
+
// Initialize with initial value
124
+
if (initial_value) |value| {
125
+
const copy_len = @min(value.len, self.editor_buffer.len - 1);
126
+
@memcpy(self.editor_buffer[0..copy_len], value[0..copy_len]);
127
+
self.input_len = copy_len;
128
+
self.cursor_pos = copy_len;
129
+
} else {
130
+
self.input_len = 0;
131
+
self.cursor_pos = 0;
132
+
}
133
+
134
+
// Record initial crank position for absolute positioning
135
+
self.initial_crank_position = self.playdate.system.getCrankAngle();
136
+
self.crank_position = self.initial_crank_position;
137
+
self.max_length = @min(max_length, self.editor_buffer.len - 1); // Leave room for null terminator
138
+
139
+
// Reset character tracking for sound
140
+
self.previous_selected_char_index = 0;
141
+
}
142
+
143
+
// Render the keyboard interface
144
+
pub fn updateAndRender(self: *Keyboard(buffer_size)) bool {
145
+
if (self.state != .active) return false;
146
+
147
+
self.handleInput();
148
+
149
+
// Clear screen
150
+
self.playdate.graphics.clear(@as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorWhite))));
151
+
152
+
const font_height = self.playdate.graphics.getFontHeight(self.font);
153
+
154
+
// Draw title
155
+
var temp_width: c_int = 0;
156
+
var temp_height: c_int = 0;
157
+
var curr_y: c_int = defs.MARGIN;
158
+
const prompt_y = curr_y;
159
+
const title_slice = std.mem.span(self.title);
160
+
_ = pdtools.drawText(
161
+
self.playdate,
162
+
self.font,
163
+
title_slice,
164
+
defs.MARGIN,
165
+
defs.MARGIN,
166
+
&temp_width,
167
+
&temp_height,
168
+
);
169
+
170
+
curr_y += temp_height + defs.MARGIN;
171
+
172
+
// Draw current input
173
+
const line_count = if (self.allow_newlines) self.editor_lines else 1;
174
+
const rectHeight = font_height * @as(c_int, @intCast(line_count)) + defs.MARGIN * 2;
175
+
const rectWidth = pdapi.LCD_COLUMNS - defs.MARGIN * 2;
176
+
var input_x: c_int = defs.MARGIN;
177
+
var input_y: c_int = curr_y;
178
+
self.playdate.graphics.drawRect(input_x, input_y, rectWidth, rectHeight, @as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorBlack))));
179
+
input_y += defs.MARGIN;
180
+
input_x += defs.MARGIN;
181
+
curr_y += rectHeight + defs.MARGIN;
182
+
183
+
// Render multiline text if newlines are allowed
184
+
if (self.allow_newlines) {
185
+
const input_text = self.editor_buffer[0..self.input_len];
186
+
var line_y = input_y;
187
+
var line_start: usize = 0;
188
+
var current_line: usize = 0;
189
+
190
+
// Split text by newlines and draw each line
191
+
for (input_text, 0..) |char, i| {
192
+
if (char == '\n' or i == input_text.len - 1) {
193
+
const line_end = if (char == '\n') i else i + 1;
194
+
const line_text = input_text[line_start..line_end];
195
+
196
+
if (current_line < line_count) {
197
+
_ = pdtools.drawText(
198
+
self.playdate,
199
+
self.font,
200
+
line_text,
201
+
input_x,
202
+
line_y,
203
+
&temp_width,
204
+
&temp_height,
205
+
);
206
+
line_y += font_height;
207
+
current_line += 1;
208
+
}
209
+
line_start = i + 1;
210
+
}
211
+
}
212
+
213
+
// If no newlines in text, draw as single line
214
+
if (current_line == 0) {
215
+
_ = pdtools.drawText(
216
+
self.playdate,
217
+
self.font,
218
+
input_text,
219
+
input_x,
220
+
input_y,
221
+
&temp_width,
222
+
&temp_height,
223
+
);
224
+
}
225
+
} else {
226
+
const input_text = self.editor_buffer[0..self.input_len];
227
+
_ = pdtools.drawText(
228
+
self.playdate,
229
+
self.font,
230
+
input_text,
231
+
input_x,
232
+
input_y,
233
+
&temp_width,
234
+
&temp_height,
235
+
);
236
+
}
237
+
238
+
curr_y += font_height * @as(c_int, @intCast(line_count)) + defs.MARGIN;
239
+
240
+
// Draw character counter above input area on the right
241
+
var counter_buffer: [32]u8 = undefined;
242
+
const counter_text = std.fmt.bufPrint(counter_buffer[0..], "{d}/{d}", .{ self.input_len, self.max_length }) catch "0/0";
243
+
const counter_width = self.playdate.graphics.getTextWidth(self.font, counter_text.ptr, counter_text.len, pdapi.PDStringEncoding.UTF8Encoding, 0);
244
+
const counter_x = rectWidth - counter_width; // Right-aligned with some margin
245
+
const counter_y = prompt_y; // Above the input box
246
+
_ = self.playdate.graphics.drawText(counter_text.ptr, counter_text.len, pdapi.PDStringEncoding.UTF8Encoding, counter_x, counter_y);
247
+
248
+
// Draw cursor in input field at cursor position
249
+
var cursor_x: c_int = 0;
250
+
var cursor_y: c_int = input_y;
251
+
252
+
if (self.allow_newlines) {
253
+
// Calculate cursor position accounting for newlines
254
+
const text_before_cursor = self.editor_buffer[0..self.cursor_pos];
255
+
var current_line: usize = 0;
256
+
var line_start: usize = 0;
257
+
258
+
// Find which line the cursor is on
259
+
for (text_before_cursor, 0..) |char, i| {
260
+
if (char == '\n') {
261
+
current_line += 1;
262
+
line_start = i + 1;
263
+
}
264
+
}
265
+
266
+
// Calculate cursor position within the current line
267
+
const line_text = text_before_cursor[line_start..];
268
+
cursor_x = self.playdate.graphics.getTextWidth(self.font, @ptrCast(line_text), line_text.len, pdapi.PDStringEncoding.UTF8Encoding, 0);
269
+
cursor_y = input_y + @as(c_int, @intCast(current_line)) * font_height;
270
+
} else {
271
+
cursor_x = self.playdate.graphics.getTextWidth(self.font, @ptrCast(&self.editor_buffer), self.cursor_pos, pdapi.PDStringEncoding.UTF8Encoding, 0);
272
+
}
273
+
274
+
self.playdate.graphics.fillRect(cursor_x + defs.MARGIN + 2, cursor_y, 2, 14, @as(usize, @intCast(@intFromEnum(pdapi.LCDSolidColor.ColorBlack))));
275
+
276
+
// Draw the 3 character set rows
277
+
const row_configs = [3]struct {
278
+
row: Selected,
279
+
y_pos: c_int,
280
+
}{
281
+
.{ .row = .uppercase, .y_pos = START_Y },
282
+
.{ .row = .lowercase, .y_pos = START_Y + ROW_HEIGHT },
283
+
.{ .row = .special, .y_pos = START_Y + ROW_HEIGHT * 2 },
284
+
};
285
+
286
+
for (row_configs) |config| {
287
+
const is_selected_row = (self.selected == config.row and self.selection_mode == .characters);
288
+
const chars = self.getCharacterSet(config.row);
289
+
290
+
// Draw visible characters centered around the current crank position
291
+
const center_pos = VISIBLE_CHARS / 2; // Position 6 for 13 chars (0-12)
292
+
293
+
// Calculate selected character index from crank position
294
+
const relative_position = self.crank_position - self.initial_crank_position;
295
+
const chars_per_360 = @as(f32, @floatFromInt(chars.len));
296
+
const position_factor = relative_position / 360.0;
297
+
const selected_char_index = @as(usize, @intCast(@mod(@as(i32, @intFromFloat(position_factor * chars_per_360)), @as(i32, @intCast(chars.len)))));
298
+
299
+
// Play click sound when character changes (only for the currently selected row)
300
+
if (is_selected_row and selected_char_index != self.previous_selected_char_index) {
301
+
self.playClickSound();
302
+
self.previous_selected_char_index = selected_char_index;
303
+
}
304
+
305
+
for (0..VISIBLE_CHARS) |i| {
306
+
// Calculate character index relative to center
307
+
const offset = @as(i32, @intCast(i)) - @as(i32, @intCast(center_pos));
308
+
var char_index = @as(i32, @intCast(selected_char_index)) + offset;
309
+
310
+
// Wrap around character set bounds
311
+
if (char_index < 0) {
312
+
char_index += @as(i32, @intCast(chars.len));
313
+
}
314
+
char_index = @rem(char_index, @as(i32, @intCast(chars.len)));
315
+
316
+
const char = chars[@as(usize, @intCast(char_index))];
317
+
318
+
// Center the entire character row on screen
319
+
const total_width = VISIBLE_CHARS * CHAR_WIDTH + (VISIBLE_CHARS - 1) * CHAR_SPACING;
320
+
const start_x = CENTER_X - @divTrunc(total_width, 2);
321
+
const x = start_x + @as(c_int, @intCast(i * (CHAR_WIDTH + CHAR_SPACING)));
322
+
323
+
// The center character is selected when this row is active
324
+
const is_selected_char = (is_selected_row and i == center_pos);
325
+
326
+
// Draw character box with better sizing
327
+
const box_height = 24;
328
+
const char_color = if (is_selected_char) pdapi.LCDSolidColor.ColorBlack else pdapi.LCDSolidColor.ColorWhite;
329
+
const border_color = if (is_selected_row) pdapi.LCDSolidColor.ColorBlack else pdapi.LCDSolidColor.ColorXOR;
330
+
331
+
self.playdate.graphics.fillRect(x, config.y_pos, CHAR_WIDTH, box_height, @as(usize, @intCast(@intFromEnum(char_color))));
332
+
self.playdate.graphics.drawRect(x, config.y_pos, CHAR_WIDTH, box_height, @as(usize, @intCast(@intFromEnum(border_color))));
333
+
334
+
// Draw character with improved centering
335
+
if (char == ' ') {
336
+
const space_text = "SPC";
337
+
const text_width = self.playdate.graphics.getTextWidth(self.font, space_text.ptr, space_text.len, pdapi.PDStringEncoding.UTF8Encoding, 0);
338
+
const text_height = 12; // Approximate font height
339
+
const text_x = x + @divTrunc(CHAR_WIDTH - text_width, 2);
340
+
const text_y = config.y_pos + @divTrunc(box_height - text_height, 2);
341
+
342
+
if (is_selected_char) {
343
+
self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeInverted);
344
+
}
345
+
_ = self.playdate.graphics.drawText(space_text.ptr, space_text.len, pdapi.PDStringEncoding.UTF8Encoding, text_x, text_y);
346
+
if (is_selected_char) {
347
+
self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
348
+
}
349
+
} else if (char == '\n') {
350
+
const newline_text = "NL";
351
+
const text_width = self.playdate.graphics.getTextWidth(self.font, newline_text.ptr, newline_text.len, pdapi.PDStringEncoding.UTF8Encoding, 0);
352
+
const text_height = 12; // Approximate font height
353
+
const text_x = x + @divTrunc(CHAR_WIDTH - text_width, 2);
354
+
const text_y = config.y_pos + @divTrunc(box_height - text_height, 2);
355
+
356
+
if (is_selected_char) {
357
+
self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeInverted);
358
+
}
359
+
_ = self.playdate.graphics.drawText(newline_text.ptr, newline_text.len, pdapi.PDStringEncoding.UTF8Encoding, text_x, text_y);
360
+
if (is_selected_char) {
361
+
self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
362
+
}
363
+
} else {
364
+
var char_str = [2]u8{ char, 0 };
365
+
const text_width = self.playdate.graphics.getTextWidth(self.font, @ptrCast(&char_str), 1, pdapi.PDStringEncoding.UTF8Encoding, 0);
366
+
const text_height = 12; // Approximate font height
367
+
const text_x = x + @divTrunc(CHAR_WIDTH - text_width, 2);
368
+
const text_y = config.y_pos + @divTrunc(box_height - text_height, 2);
369
+
370
+
if (is_selected_char) {
371
+
self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeInverted);
372
+
}
373
+
_ = self.playdate.graphics.drawText(@ptrCast(&char_str), 1, pdapi.PDStringEncoding.UTF8Encoding, text_x, text_y);
374
+
if (is_selected_char) {
375
+
self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
376
+
}
377
+
}
378
+
}
379
+
}
380
+
381
+
// Draw Cancel and Confirm buttons
382
+
const button_names = [2][]const u8{ "Cancel", "Confirm" };
383
+
const button_spacing = 20;
384
+
const total_button_width = 2 * BUTTON_WIDTH + button_spacing;
385
+
const buttons_start_x = CENTER_X - @divTrunc(total_button_width, 2);
386
+
387
+
for (0..2) |i| {
388
+
const x = buttons_start_x + @as(c_int, @intCast(i * (BUTTON_WIDTH + button_spacing)));
389
+
const y = BUTTON_Y_OFFSET;
390
+
391
+
const is_selected = (self.selection_mode == .buttons and
392
+
((i == 0 and self.selected == .cancel) or (i == 1 and self.selected == .confirm)));
393
+
const button_color = if (is_selected) pdapi.LCDSolidColor.ColorBlack else pdapi.LCDSolidColor.ColorWhite;
394
+
const border_color = pdapi.LCDSolidColor.ColorBlack;
395
+
396
+
self.playdate.graphics.fillRect(x, y, BUTTON_WIDTH, BUTTON_HEIGHT, @as(usize, @intCast(@intFromEnum(button_color))));
397
+
self.playdate.graphics.drawRect(x, y, BUTTON_WIDTH, BUTTON_HEIGHT, @as(usize, @intCast(@intFromEnum(border_color))));
398
+
399
+
// Draw button text
400
+
const text = button_names[i];
401
+
const text_width = self.playdate.graphics.getTextWidth(self.font, text.ptr, text.len, pdapi.PDStringEncoding.UTF8Encoding, 0);
402
+
const text_x = x + @divTrunc(BUTTON_WIDTH - text_width, 2);
403
+
const text_y = y + @divTrunc(BUTTON_HEIGHT - 12, 2);
404
+
405
+
if (is_selected) {
406
+
self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeInverted);
407
+
}
408
+
_ = self.playdate.graphics.drawText(text.ptr, text.len, pdapi.PDStringEncoding.UTF8Encoding, text_x, text_y);
409
+
if (is_selected) {
410
+
self.playdate.graphics.setDrawMode(pdapi.LCDBitmapDrawMode.DrawModeCopy);
411
+
}
412
+
}
413
+
414
+
// Draw instructions below the buttons
415
+
const instruction_y = BUTTON_Y_OFFSET + BUTTON_HEIGHT + 5; // 5 pixels below buttons
416
+
const instr1 = "A: Select B: Backspace " ++ pdtools.LEFT_ARROW_EMOJI ++ pdtools.RIGHT_ARROW_EMOJI ++ ": Move Cursor";
417
+
const instr2 = pdtools.UP_ARROW_EMOJI ++ pdtools.DOWN_ARROW_EMOJI ++ ": Switch Rows " ++ pdtools.CRANK_EMOJI ++ ": Scroll Characters";
418
+
_ = self.playdate.graphics.drawText(instr1.ptr, instr1.len, pdapi.PDStringEncoding.UTF8Encoding, 10, instruction_y);
419
+
_ = self.playdate.graphics.drawText(instr2.ptr, instr2.len, pdapi.PDStringEncoding.UTF8Encoding, 10, instruction_y + 15);
420
+
421
+
return true;
422
+
}
423
+
424
+
// Check if keyboard is currently active
425
+
pub fn isKeyboardActive(self: *Keyboard(buffer_size)) bool {
426
+
return self.state == .active;
427
+
}
428
+
429
+
// Get the current keyboard state
430
+
pub fn getKeyboardState(self: *Keyboard(buffer_size)) KeyboardState {
431
+
return self.state;
432
+
}
433
+
434
+
// Get the entered text (call after keyboard is completed)
435
+
pub fn getEnteredText(self: *Keyboard(buffer_size)) []const u8 {
436
+
if (self.state == .completed) {
437
+
return self.output_buffer[0..std.mem.len(@as([*:0]const u8, @ptrCast(self.output_buffer)))];
438
+
}
439
+
return "";
440
+
}
441
+
442
+
// Close the keyboard and clean up
443
+
pub fn closeKeyboard(self: *Keyboard(buffer_size)) void {
444
+
self.state = .inactive;
445
+
self.input_len = 0;
446
+
self.cursor_pos = 0;
447
+
self.cancel_callback = null;
448
+
self.confirm_callback = null;
449
+
@memset(&self.editor_buffer, 0);
450
+
451
+
// Clean up synth
452
+
if (self.click_synth) |synth| {
453
+
self.playdate.sound.synth.freeSynth(synth);
454
+
self.click_synth = null;
455
+
}
456
+
}
457
+
458
+
pub fn renderKeyboard(self: *Keyboard(buffer_size), pd: *pdapi.PlaydateAPI, font: ?*pdapi.LCDFont) void {
459
+
_ = pd; // We use self.playdate
460
+
self.font = font; // Update font
461
+
self.render();
462
+
}
463
+
464
+
// Helper functions for character set access
465
+
fn getCharacterSet(self: *Keyboard(buffer_size), row: Selected) []const u8 {
466
+
return switch (row) {
467
+
.uppercase => UPPERCASE_CHARS,
468
+
.lowercase => LOWERCASE_CHARS,
469
+
.special => if (self.allow_newlines) SPECIAL_CHARS_WITH_NEWLINE else SPECIAL_CHARS,
470
+
.cancel, .confirm => "", // These don't have character sets
471
+
};
472
+
}
473
+
474
+
// Play a short click sound
475
+
fn playClickSound(self: *Keyboard(buffer_size)) void {
476
+
if (self.click_synth) |synth| {
477
+
// Play a short click at 800Hz for 50ms
478
+
self.playdate.sound.synth.playNote(synth, 800.0, 1.0, 0.05, 0);
479
+
}
480
+
}
481
+
482
+
fn getCurrentCharacter(self: *Keyboard(buffer_size)) u8 {
483
+
const chars = self.getCharacterSet(self.selected);
484
+
if (chars.len == 0) return ' '; // Default for non-character selections
485
+
486
+
// Convert crank position to character index
487
+
const relative_position = self.crank_position - self.initial_crank_position;
488
+
const chars_per_360 = @as(f32, @floatFromInt(chars.len));
489
+
const position_factor = relative_position / 360.0; // Full rotation = full character set
490
+
const char_index = @as(usize, @intCast(@mod(@as(i32, @intFromFloat(position_factor * chars_per_360)), @as(i32, @intCast(chars.len)))));
491
+
return chars[char_index];
492
+
}
493
+
494
+
// Handle button input for keyboard navigation and text entry
495
+
pub fn handleInput(self: *Keyboard(buffer_size)) void {
496
+
if (self.state != .active) return;
497
+
498
+
// Handle button input
499
+
var current: pdapi.PDButtons = undefined;
500
+
var pushed: pdapi.PDButtons = undefined;
501
+
var released: pdapi.PDButtons = undefined;
502
+
self.playdate.system.getButtonState(¤t, &pushed, &released);
503
+
504
+
// Up/Down: Switch between character rows OR move between character/button selection
505
+
if (pushed & pdapi.BUTTON_UP != 0) {
506
+
if (self.selection_mode == .characters) {
507
+
// Switch to previous character row
508
+
self.selected = switch (self.selected) {
509
+
.uppercase => .special,
510
+
.lowercase => .uppercase,
511
+
.special => .lowercase,
512
+
.cancel, .confirm => .uppercase, // Reset to characters if on buttons
513
+
};
514
+
// Reset character tracking when switching rows
515
+
self.previous_selected_char_index = 0;
516
+
} else {
517
+
// Switch from buttons to characters
518
+
self.selection_mode = .characters;
519
+
self.selected = .uppercase;
520
+
// Reset character tracking when switching to characters
521
+
self.previous_selected_char_index = 0;
522
+
}
523
+
}
524
+
525
+
if (pushed & pdapi.BUTTON_DOWN != 0) {
526
+
if (self.selection_mode == .characters) {
527
+
// Check if we should move to buttons (if we're at bottom row)
528
+
self.selected = switch (self.selected) {
529
+
.uppercase => .lowercase,
530
+
.lowercase => .special,
531
+
.special => {
532
+
// Move to button selection instead
533
+
self.selection_mode = .buttons;
534
+
self.selected = .cancel;
535
+
return;
536
+
},
537
+
.cancel, .confirm => .cancel, // Stay on buttons
538
+
};
539
+
// Reset character tracking when switching rows
540
+
self.previous_selected_char_index = 0;
541
+
}
542
+
}
543
+
544
+
// Left/Right: Move text cursor or navigate buttons
545
+
if (pushed & pdapi.BUTTON_LEFT != 0) {
546
+
if (self.selection_mode == .characters) {
547
+
// Move cursor left in text
548
+
if (self.cursor_pos > 0) {
549
+
self.cursor_pos -= 1;
550
+
}
551
+
} else {
552
+
// Navigate buttons
553
+
self.selected = if (self.selected == .cancel) .confirm else .cancel;
554
+
}
555
+
}
556
+
557
+
if (pushed & pdapi.BUTTON_RIGHT != 0) {
558
+
if (self.selection_mode == .characters) {
559
+
// Move cursor right in text
560
+
if (self.cursor_pos < self.input_len) {
561
+
self.cursor_pos += 1;
562
+
}
563
+
} else {
564
+
// Navigate buttons
565
+
self.selected = if (self.selected == .cancel) .confirm else .cancel;
566
+
}
567
+
}
568
+
569
+
// A button: Select current character OR activate button
570
+
if (pushed & pdapi.BUTTON_A != 0) {
571
+
if (self.selection_mode == .characters) {
572
+
const char = self.getCurrentCharacter();
573
+
574
+
// Insert character at cursor position if there's space
575
+
if (self.input_len < self.max_length) {
576
+
// Shift characters to the right to make room for new character
577
+
var i = self.input_len;
578
+
while (i > self.cursor_pos) {
579
+
self.editor_buffer[i] = self.editor_buffer[i - 1];
580
+
i -= 1;
581
+
}
582
+
583
+
// Insert the new character at cursor position
584
+
self.editor_buffer[self.cursor_pos] = char;
585
+
self.input_len += 1;
586
+
self.cursor_pos += 1; // Move cursor to after inserted character
587
+
}
588
+
} else {
589
+
// Button selection
590
+
if (self.selected == .cancel) {
591
+
// Cancel
592
+
self.state = .cancelled;
593
+
if (self.cancel_callback) |callback| {
594
+
callback();
595
+
}
596
+
} else if (self.selected == .confirm) {
597
+
// Confirm
598
+
const copy_len = @min(self.input_len, self.output_buffer.len - 1);
599
+
@memcpy(self.output_buffer[0..copy_len], self.editor_buffer[0..copy_len]);
600
+
self.output_buffer[copy_len] = 0; // Null terminate
601
+
self.state = .completed;
602
+
603
+
if (self.confirm_callback) |callback| {
604
+
// Pass the entered text to the callback
605
+
const entered_text = self.editor_buffer[0..self.input_len];
606
+
callback(entered_text);
607
+
}
608
+
}
609
+
}
610
+
}
611
+
612
+
// B button: Backspace (delete character before cursor)
613
+
if (pushed & pdapi.BUTTON_B != 0) {
614
+
if (self.cursor_pos > 0 and self.input_len > 0) {
615
+
// Shift characters to the left to remove character before cursor
616
+
var i = self.cursor_pos - 1;
617
+
while (i < self.input_len - 1) {
618
+
self.editor_buffer[i] = self.editor_buffer[i + 1];
619
+
i += 1;
620
+
}
621
+
622
+
// Clear the last character and update lengths
623
+
self.input_len -= 1;
624
+
self.cursor_pos -= 1;
625
+
self.editor_buffer[self.input_len] = 0;
626
+
}
627
+
}
628
+
629
+
// Crank: Update absolute position for smooth character scrolling
630
+
self.crank_position = self.playdate.system.getCrankAngle();
631
+
}
632
+
};
633
+
}
+1575
src/playdate_api_definitions.zig
+1575
src/playdate_api_definitions.zig
···
1
+
const std = @import("std");
2
+
const builtin = @import("builtin");
3
+
4
+
pub const PlaydateAPI = extern struct {
5
+
system: *const PlaydateSys,
6
+
file: *const PlaydateFile,
7
+
graphics: *const PlaydateGraphics,
8
+
sprite: *const PlaydateSprite,
9
+
display: *const PlaydateDisplay,
10
+
sound: *const PlaydateSound,
11
+
lua: *const PlaydateLua,
12
+
json: *const PlaydateJSON,
13
+
scoreboards: *const PlaydateScoreboards,
14
+
network: *const PlaydateNetwork,
15
+
};
16
+
17
+
/////////Zig Utility Functions///////////
18
+
pub fn is_compiling_for_playdate_hardware() bool {
19
+
return builtin.os.tag == .freestanding and builtin.cpu.arch.isThumb();
20
+
}
21
+
22
+
////////Buttons//////////////
23
+
pub const PDButtons = c_int;
24
+
pub const BUTTON_LEFT = (1 << 0);
25
+
pub const BUTTON_RIGHT = (1 << 1);
26
+
pub const BUTTON_UP = (1 << 2);
27
+
pub const BUTTON_DOWN = (1 << 3);
28
+
pub const BUTTON_B = (1 << 4);
29
+
pub const BUTTON_A = (1 << 5);
30
+
31
+
///////////////System/////////////////////////
32
+
pub const PDMenuItem = opaque {};
33
+
pub const PDCallbackFunction = *const fn (userdata: ?*anyopaque) callconv(.C) c_int;
34
+
pub const PDMenuItemCallbackFunction = *const fn (userdata: ?*anyopaque) callconv(.C) void;
35
+
pub const PDButtonCallbackFunction = *const fn (
36
+
button: PDButtons,
37
+
down: c_int,
38
+
when: u32,
39
+
userdata: ?*anyopaque,
40
+
) callconv(.C) c_int;
41
+
pub const PDSystemEvent = enum(c_int) {
42
+
EventInit,
43
+
EventInitLua,
44
+
EventLock,
45
+
EventUnlock,
46
+
EventPause,
47
+
EventResume,
48
+
EventTerminate,
49
+
EventKeyPressed, // arg is keycode
50
+
EventKeyReleased,
51
+
EventLowPower,
52
+
};
53
+
pub const PDLanguage = enum(c_int) {
54
+
PDLanguageEnglish,
55
+
PDLanguageJapanese,
56
+
PDLanguageUnknown,
57
+
};
58
+
59
+
pub const AccessRequestCallback = ?*const fn (allowed: bool, userdata: ?*anyopaque) callconv(.C) void;
60
+
pub const AccessReply = enum(c_int) {
61
+
AccessAsk = 0,
62
+
AccessDeny,
63
+
AccessAllow,
64
+
};
65
+
66
+
pub const PDPeripherals = c_int;
67
+
pub const PERIPHERAL_NONE = 0;
68
+
pub const PERIPHERAL_ACCELEROMETER = (1 << 0);
69
+
// ...
70
+
pub const PERIPHERAL_ALL = 0xFFFF;
71
+
72
+
pub const PDStringEncoding = enum(c_int) {
73
+
ASCIIEncoding,
74
+
UTF8Encoding,
75
+
@"16BitLEEncoding",
76
+
};
77
+
78
+
pub const PDDateTime = extern struct {
79
+
year: u16,
80
+
month: u8, // 1-12
81
+
day: u8, // 1-31
82
+
weekday: u8, // 1=monday-7=sunday
83
+
hour: u8, // 0-23
84
+
minute: u8,
85
+
second: u8,
86
+
};
87
+
88
+
pub const PlaydateSys = extern struct {
89
+
realloc: *const fn (ptr: ?*anyopaque, size: usize) callconv(.C) ?*anyopaque,
90
+
formatString: *const fn (ret: ?*[*c]u8, fmt: ?[*:0]const u8, ...) callconv(.C) c_int,
91
+
logToConsole: *const fn (fmt: ?[*:0]const u8, ...) callconv(.C) void,
92
+
@"error": *const fn (fmt: ?[*:0]const u8, ...) callconv(.C) void,
93
+
getLanguage: *const fn () callconv(.C) PDLanguage,
94
+
getCurrentTimeMilliseconds: *const fn () callconv(.C) c_uint,
95
+
getSecondsSinceEpoch: *const fn (milliseconds: ?*c_uint) callconv(.C) c_uint,
96
+
drawFPS: *const fn (x: c_int, y: c_int) callconv(.C) void,
97
+
98
+
setUpdateCallback: *const fn (update: ?PDCallbackFunction, userdata: ?*anyopaque) callconv(.C) void,
99
+
getButtonState: *const fn (current: ?*PDButtons, pushed: ?*PDButtons, released: ?*PDButtons) callconv(.C) void,
100
+
setPeripheralsEnabled: *const fn (mask: PDPeripherals) callconv(.C) void,
101
+
getAccelerometer: *const fn (outx: ?*f32, outy: ?*f32, outz: ?*f32) callconv(.C) void,
102
+
getCrankChange: *const fn () callconv(.C) f32,
103
+
getCrankAngle: *const fn () callconv(.C) f32,
104
+
isCrankDocked: *const fn () callconv(.C) c_int,
105
+
setCrankSoundsDisabled: *const fn (flag: c_int) callconv(.C) c_int, // returns previous setting
106
+
107
+
getFlipped: *const fn () callconv(.C) c_int,
108
+
setAutoLockDisabled: *const fn (disable: c_int) callconv(.C) void,
109
+
110
+
setMenuImage: *const fn (bitmap: ?*LCDBitmap, xOffset: c_int) callconv(.C) void,
111
+
addMenuItem: *const fn (title: ?[*:0]const u8, callback: ?PDMenuItemCallbackFunction, userdata: ?*anyopaque) callconv(.C) ?*PDMenuItem,
112
+
addCheckmarkMenuItem: *const fn (title: ?[*:0]const u8, value: c_int, callback: ?PDMenuItemCallbackFunction, userdata: ?*anyopaque) callconv(.C) ?*PDMenuItem,
113
+
addOptionsMenuItem: *const fn (title: ?[*:0]const u8, optionTitles: [*c]?[*:0]const u8, optionsCount: c_int, f: ?PDMenuItemCallbackFunction, userdata: ?*anyopaque) callconv(.C) ?*PDMenuItem,
114
+
removeAllMenuItems: *const fn () callconv(.C) void,
115
+
removeMenuItem: *const fn (menuItem: ?*PDMenuItem) callconv(.C) void,
116
+
getMenuItemValue: *const fn (menuItem: ?*PDMenuItem) callconv(.C) c_int,
117
+
setMenuItemValue: *const fn (menuItem: ?*PDMenuItem, value: c_int) callconv(.C) void,
118
+
getMenuItemTitle: *const fn (menuItem: ?*PDMenuItem) callconv(.C) ?[*:0]const u8,
119
+
setMenuItemTitle: *const fn (menuItem: ?*PDMenuItem, title: ?[*:0]const u8) callconv(.C) void,
120
+
getMenuItemUserdata: *const fn (menuItem: ?*PDMenuItem) callconv(.C) ?*anyopaque,
121
+
setMenuItemUserdata: *const fn (menuItem: ?*PDMenuItem, ud: ?*anyopaque) callconv(.C) void,
122
+
123
+
getReduceFlashing: *const fn () callconv(.C) c_int,
124
+
125
+
// 1.1
126
+
getElapsedTime: *const fn () callconv(.C) f32,
127
+
resetElapsedTime: *const fn () callconv(.C) void,
128
+
129
+
// 1.4
130
+
getBatteryPercentage: *const fn () callconv(.C) f32,
131
+
getBatteryVoltage: *const fn () callconv(.C) f32,
132
+
133
+
// 1.13
134
+
getTimezoneOffset: *const fn () callconv(.C) i32,
135
+
shouldDisplay24HourTime: *const fn () callconv(.C) c_int,
136
+
convertEpochToDateTime: *const fn (epoch: u32, datetime: ?*PDDateTime) callconv(.C) void,
137
+
convertDateTimeToEpoch: *const fn (datetime: ?*PDDateTime) callconv(.C) u32,
138
+
139
+
//2.0
140
+
clearICache: *const fn () callconv(.C) void,
141
+
142
+
// 2.4
143
+
setButtonCallback: *const fn (
144
+
cb: ?PDButtonCallbackFunction,
145
+
buttonud: ?*anyopaque,
146
+
queuesize: c_int,
147
+
) callconv(.C) void,
148
+
setSerialMessageCallback: *const fn (
149
+
callback: *const fn (data: ?[*:0]const u8) callconv(.C) void,
150
+
) callconv(.C) void,
151
+
vaFormatString: *const fn (
152
+
outstr: [*c][*c]u8,
153
+
fmt: ?[*:0]const u8,
154
+
args: VaList,
155
+
) callconv(.C) c_int,
156
+
parseString: *const fn (
157
+
str: ?[*:0]const u8,
158
+
format: ?[*:0]const u8,
159
+
...,
160
+
) callconv(.C) c_int,
161
+
162
+
// ???
163
+
delay: *const fn (milliseconds: u32) callconv(.C) void,
164
+
165
+
// 2.7
166
+
getServerTime: *const fn (callback: *const fn (time: ?[*:0]const u8, err: ?[*:0]const u8) callconv(.C) void) callconv(.C) void,
167
+
restartGame: *const fn (launchargs: ?[*:0]const u8) callconv(.C) void,
168
+
getLaunchArgs: *const fn (outpath: [*c][*:0]const u8) callconv(.C) ?[*:0]const u8,
169
+
sendMirrorData: *const fn (command: u8, data: [*c]u8, len: c_int) callconv(.C) bool,
170
+
171
+
//NOTE(Daniel Bokser): std.builtin.VaList is not available when targeting Playdate hardware,
172
+
// so we need to directly include it
173
+
const VaList = if (is_compiling_for_playdate_hardware() or builtin.os.tag == .windows)
174
+
@cImport({
175
+
@cInclude("stdarg.h");
176
+
}).va_list
177
+
else
178
+
//NOTE(Daniel Bokser):
179
+
// We must use std.builtin.VaList when building for the Linux simulator.
180
+
// Using stdarg.h results in a compiler error otherwise.
181
+
std.builtin.VaList;
182
+
};
183
+
184
+
////////LCD and Graphics///////////////////////
185
+
pub const LCD_COLUMNS = 400;
186
+
pub const LCD_ROWS = 240;
187
+
pub const LCD_ROWSIZE = 52;
188
+
pub const LCDBitmap = opaque {};
189
+
pub const LCDVideoPlayer = opaque {};
190
+
pub const LCDStreamPlayer = opaque {};
191
+
pub const PlaydateVideo = extern struct {
192
+
loadVideo: *const fn (?[*:0]const u8) callconv(.C) ?*LCDVideoPlayer,
193
+
freePlayer: *const fn (?*LCDVideoPlayer) callconv(.C) void,
194
+
setContext: *const fn (?*LCDVideoPlayer, ?*LCDBitmap) callconv(.C) c_int,
195
+
useScreenContext: *const fn (?*LCDVideoPlayer) callconv(.C) void,
196
+
renderFrame: *const fn (?*LCDVideoPlayer, c_int) callconv(.C) c_int,
197
+
getError: *const fn (?*LCDVideoPlayer) callconv(.C) ?[*:0]const u8,
198
+
getInfo: *const fn (?*LCDVideoPlayer, [*c]c_int, [*c]c_int, [*c]f32, [*c]c_int, [*c]c_int) callconv(.C) void,
199
+
getContext: *const fn (?*LCDVideoPlayer) callconv(.C) ?*LCDBitmap,
200
+
};
201
+
202
+
pub const LCDPattern = [16]u8;
203
+
pub const LCDColor = usize; //Pointer to LCDPattern or a LCDSolidColor value
204
+
pub const LCDSolidColor = enum(c_int) {
205
+
ColorBlack,
206
+
ColorWhite,
207
+
ColorClear,
208
+
ColorXOR,
209
+
};
210
+
pub const LCDBitmapDrawMode = enum(c_int) {
211
+
DrawModeCopy,
212
+
DrawModeWhiteTransparent,
213
+
DrawModeBlackTransparent,
214
+
DrawModeFillWhite,
215
+
DrawModeFillBlack,
216
+
DrawModeXOR,
217
+
DrawModeNXOR,
218
+
DrawModeInverted,
219
+
};
220
+
pub const LCDLineCapStyle = enum(c_int) {
221
+
LineCapStyleButt,
222
+
LineCapStyleSquare,
223
+
LineCapStyleRound,
224
+
};
225
+
226
+
pub const LCDFontLanguage = enum(c_int) {
227
+
LCDFontLanguageEnglish,
228
+
LCDFontLanguageJapanese,
229
+
LCDFontLanguageUnknown,
230
+
};
231
+
232
+
pub const LCDBitmapFlip = enum(c_int) {
233
+
BitmapUnflipped,
234
+
BitmapFlippedX,
235
+
BitmapFlippedY,
236
+
BitmapFlippedXY,
237
+
};
238
+
239
+
pub const LCDPolygonFillRule = enum(c_int) {
240
+
PolygonFillNonZero,
241
+
PolygonFillEvenOdd,
242
+
};
243
+
244
+
pub const PDTextWrappingMode = enum(c_int) {
245
+
WrapClip,
246
+
WrapCharacter,
247
+
WrapWord,
248
+
};
249
+
250
+
pub const PDTextAlignment = enum(c_int) {
251
+
AlignTextLeft,
252
+
AlignTextCenter,
253
+
AlignTextRight,
254
+
};
255
+
256
+
pub const LCDTileMap = opaque {};
257
+
pub const LCDBitmapTable = opaque {};
258
+
pub const LCDFont = opaque {};
259
+
pub const LCDFontPage = opaque {};
260
+
pub const LCDFontGlyph = opaque {};
261
+
pub const LCDFontData = opaque {};
262
+
pub const LCDRect = extern struct {
263
+
left: c_int,
264
+
right: c_int,
265
+
top: c_int,
266
+
bottom: c_int,
267
+
};
268
+
269
+
pub const PlaydateVideostream = extern struct {
270
+
newPlayer: *const fn () callconv(.C) ?*LCDStreamPlayer,
271
+
freePlayer: *const fn (p: ?*LCDStreamPlayer) callconv(.C) void,
272
+
273
+
setBufferSize: *const fn (p: ?*LCDStreamPlayer, video: c_int, audio: c_int) callconv(.C) void,
274
+
275
+
setFile: *const fn (p: ?*LCDStreamPlayer, file: ?*SDFile) callconv(.C) void,
276
+
277
+
setHTTPConnection: *const fn (p: ?*LCDStreamPlayer, conn: ?*HTTPConnection) callconv(.C) void,
278
+
279
+
getFilePlayer: *const fn (p: ?*LCDStreamPlayer) callconv(.C) ?*FilePlayer,
280
+
281
+
getVideoPlayer: *const fn (p: ?*LCDStreamPlayer) callconv(.C) ?*LCDVideoPlayer,
282
+
283
+
// returns true if it drew a frame, else false
284
+
update: *const fn (p: ?*LCDStreamPlayer) callconv(.C) bool,
285
+
286
+
getBufferedFrameCount: *const fn (p: ?*LCDStreamPlayer) callconv(.C) c_int,
287
+
288
+
// uint32_t (*getBytesRead)(LCDStreamPlayer* p);
289
+
getBytesRead: *const fn (p: ?*LCDStreamPlayer) callconv(.C) u32,
290
+
291
+
// 3.0
292
+
setTCPConnection: *const fn (p: ?*LCDStreamPlayer, conn: ?*TCPConnection) callconv(.C) void,
293
+
};
294
+
295
+
pub const PlaydateTilemap = extern struct {
296
+
newTilemap: *const fn () callconv(.C) ?*LCDTileMap,
297
+
freeTilemap: *const fn (m: ?*LCDTileMap) callconv(.C) void,
298
+
299
+
setImageTable: *const fn (m: ?*LCDTileMap, table: ?*LCDBitmapTable) callconv(.C) void,
300
+
getImageTable: *const fn (m: ?*LCDTileMap) callconv(.C) ?*LCDBitmapTable,
301
+
302
+
setSize: *const fn (m: ?*LCDTileMap, tilesWide: c_int, tilesHigh: c_int) callconv(.C) void,
303
+
getSize: *const fn (m: ?*LCDTileMap, tilesWide: ?*c_int, tilesHigh: ?*c_int) callconv(.C) void,
304
+
getPixelSize: *const fn (m: ?*LCDTileMap, outWidth: ?*u32, outHeight: ?*u32) callconv(.C) void,
305
+
306
+
setTiles: *const fn (m: ?*LCDTileMap, indexes: [*c]u16, count: c_int, rowwidth: c_int) callconv(.C) void,
307
+
308
+
setTileAtPosition: *const fn (m: ?*LCDTileMap, x: c_int, y: c_int, idx: u16) callconv(.C) void,
309
+
getTileAtPosition: *const fn (m: ?*LCDTileMap, x: c_int, y: c_int) callconv(.C) c_int,
310
+
311
+
drawAtPoint: *const fn (m: ?*LCDTileMap, x: f32, y: f32) callconv(.C) void,
312
+
};
313
+
314
+
pub const PlaydateGraphics = extern struct {
315
+
video: *const PlaydateVideo,
316
+
// Drawing Functions
317
+
clear: *const fn (color: LCDColor) callconv(.C) void,
318
+
setBackgroundColor: *const fn (color: LCDSolidColor) callconv(.C) void,
319
+
setStencil: *const fn (stencil: ?*LCDBitmap) callconv(.C) void, // deprecated in favor of setStencilImage, which adds a "tile" flag
320
+
setDrawMode: *const fn (mode: LCDBitmapDrawMode) callconv(.C) void,
321
+
setDrawOffset: *const fn (dx: c_int, dy: c_int) callconv(.C) void,
322
+
setClipRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int) callconv(.C) void,
323
+
clearClipRect: *const fn () callconv(.C) void,
324
+
setLineCapStyle: *const fn (endCapStyle: LCDLineCapStyle) callconv(.C) void,
325
+
setFont: *const fn (font: ?*LCDFont) callconv(.C) void,
326
+
setTextTracking: *const fn (tracking: c_int) callconv(.C) void,
327
+
pushContext: *const fn (target: ?*LCDBitmap) callconv(.C) void,
328
+
popContext: *const fn () callconv(.C) void,
329
+
330
+
drawBitmap: *const fn (bitmap: ?*LCDBitmap, x: c_int, y: c_int, flip: LCDBitmapFlip) callconv(.C) void,
331
+
tileBitmap: *const fn (bitmap: ?*LCDBitmap, x: c_int, y: c_int, width: c_int, height: c_int, flip: LCDBitmapFlip) callconv(.C) void,
332
+
drawLine: *const fn (x1: c_int, y1: c_int, x2: c_int, y2: c_int, width: c_int, color: LCDColor) callconv(.C) void,
333
+
fillTriangle: *const fn (x1: c_int, y1: c_int, x2: c_int, y2: c_int, x3: c_int, y3: c_int, color: LCDColor) callconv(.C) void,
334
+
drawRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, color: LCDColor) callconv(.C) void,
335
+
fillRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, color: LCDColor) callconv(.C) void,
336
+
drawEllipse: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, lineWidth: c_int, startAngle: f32, endAngle: f32, color: LCDColor) callconv(.C) void,
337
+
fillEllipse: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, startAngle: f32, endAngle: f32, color: LCDColor) callconv(.C) void,
338
+
drawScaledBitmap: *const fn (bitmap: ?*LCDBitmap, x: c_int, y: c_int, xscale: f32, yscale: f32) callconv(.C) void,
339
+
drawText: *const fn (text: ?*const anyopaque, len: usize, encoding: PDStringEncoding, x: c_int, y: c_int) callconv(.C) c_int,
340
+
341
+
// LCDBitmap
342
+
newBitmap: *const fn (width: c_int, height: c_int, color: LCDColor) callconv(.C) ?*LCDBitmap,
343
+
freeBitmap: *const fn (bitmap: ?*LCDBitmap) callconv(.C) void,
344
+
loadBitmap: *const fn (path: ?[*:0]const u8, outerr: ?*?[*:0]const u8) callconv(.C) ?*LCDBitmap,
345
+
copyBitmap: *const fn (bitmap: ?*LCDBitmap) callconv(.C) ?*LCDBitmap,
346
+
loadIntoBitmap: *const fn (path: ?[*:0]const u8, bitmap: ?*LCDBitmap, outerr: ?*?[*:0]const u8) callconv(.C) void,
347
+
getBitmapData: *const fn (bitmap: ?*LCDBitmap, width: ?*c_int, height: ?*c_int, rowbytes: ?*c_int, mask: ?*[*c]u8, data: ?*[*c]u8) callconv(.C) void,
348
+
clearBitmap: *const fn (bitmap: ?*LCDBitmap, bgcolor: LCDColor) callconv(.C) void,
349
+
rotatedBitmap: *const fn (bitmap: ?*LCDBitmap, rotation: f32, xscale: f32, yscale: f32, allocedSize: ?*c_int) callconv(.C) ?*LCDBitmap,
350
+
351
+
// LCDBitmapTable
352
+
newBitmapTable: *const fn (count: c_int, width: c_int, height: c_int) callconv(.C) ?*LCDBitmapTable,
353
+
freeBitmapTable: *const fn (table: ?*LCDBitmapTable) callconv(.C) void,
354
+
loadBitmapTable: *const fn (path: ?[*:0]const u8, outerr: ?*?[*:0]const u8) callconv(.C) ?*LCDBitmapTable,
355
+
loadIntoBitmapTable: *const fn (path: ?[*:0]const u8, table: ?*LCDBitmapTable, outerr: ?*?[*:0]const u8) callconv(.C) void,
356
+
getTableBitmap: *const fn (table: ?*LCDBitmapTable, idx: c_int) callconv(.C) ?*LCDBitmap,
357
+
358
+
// LCDFont
359
+
loadFont: *const fn (path: ?[*:0]const u8, outErr: ?*?[*:0]const u8) callconv(.C) ?*LCDFont,
360
+
getFontPage: *const fn (font: ?*LCDFont, c: u32) callconv(.C) ?*LCDFontPage,
361
+
getPageGlyph: *const fn (page: ?*LCDFontPage, c: u32, bitmap: ?**LCDBitmap, advance: ?*c_int) callconv(.C) ?*LCDFontGlyph,
362
+
getGlyphKerning: *const fn (glyph: ?*LCDFontGlyph, glyphcode: u32, nextcode: u32) callconv(.C) c_int,
363
+
getTextWidth: *const fn (font: ?*LCDFont, text: ?*const anyopaque, len: usize, encoding: PDStringEncoding, tracking: c_int) callconv(.C) c_int,
364
+
365
+
// raw framebuffer access
366
+
getFrame: *const fn () callconv(.C) [*c]u8, // row stride = LCD_ROWSIZE
367
+
getDisplayFrame: *const fn () callconv(.C) [*c]u8, // row stride = LCD_ROWSIZE
368
+
getDebugBitmap: *const fn () callconv(.C) ?*LCDBitmap, // valid in simulator only, function is null on device
369
+
copyFrameBufferBitmap: *const fn () callconv(.C) ?*LCDBitmap,
370
+
markUpdatedRows: *const fn (start: c_int, end: c_int) callconv(.C) void,
371
+
display: *const fn () callconv(.C) void,
372
+
373
+
// misc util.
374
+
setColorToPattern: *const fn (color: ?*LCDColor, bitmap: ?*LCDBitmap, x: c_int, y: c_int) callconv(.C) void,
375
+
checkMaskCollision: *const fn (bitmap1: ?*LCDBitmap, x1: c_int, y1: c_int, flip1: LCDBitmapFlip, bitmap2: ?*LCDBitmap, x2: c_int, y2: c_int, flip2: LCDBitmapFlip, rect: LCDRect) callconv(.C) c_int,
376
+
377
+
// 1.1
378
+
setScreenClipRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int) callconv(.C) void,
379
+
380
+
// 1.1.1
381
+
fillPolygon: *const fn (nPoints: c_int, coords: [*c]c_int, color: LCDColor, fillRule: LCDPolygonFillRule) callconv(.C) void,
382
+
getFontHeight: *const fn (font: ?*LCDFont) callconv(.C) u8,
383
+
384
+
// 1.7
385
+
getDisplayBufferBitmap: *const fn () callconv(.C) ?*LCDBitmap,
386
+
drawRotatedBitmap: *const fn (bitmap: ?*LCDBitmap, x: c_int, y: c_int, rotation: f32, centerx: f32, centery: f32, xscale: f32, yscale: f32) callconv(.C) void,
387
+
setTextLeading: *const fn (lineHeightAdustment: c_int) callconv(.C) void,
388
+
389
+
// 1.8
390
+
setBitmapMask: *const fn (bitmap: ?*LCDBitmap, mask: ?*LCDBitmap) callconv(.C) c_int,
391
+
getBitmapMask: *const fn (bitmap: ?*LCDBitmap) callconv(.C) ?*LCDBitmap,
392
+
393
+
// 1.10
394
+
setStencilImage: *const fn (stencil: ?*LCDBitmap, tile: c_int) callconv(.C) void,
395
+
396
+
// 1.12
397
+
makeFontFromData: *const fn (data: ?*LCDFontData, wide: c_int) callconv(.C) *LCDFont,
398
+
399
+
// 2.1
400
+
getTextTracking: *const fn () callconv(.C) c_int,
401
+
402
+
// 2.5
403
+
setPixel: *const fn (x: c_int, y: c_int, c: LCDColor) callconv(.C) void,
404
+
getBitmapPixel: *const fn (bitmap: ?*LCDBitmap, x: c_int, y: c_int) callconv(.C) LCDSolidColor,
405
+
getBitmapTableInfo: *const fn (table: ?*LCDBitmapTable, count: ?*c_int, width: ?*c_int) callconv(.C) void,
406
+
407
+
// 2.6
408
+
drawTextInRect: *const fn (text: ?*const anyopaque, len: usize, encoding: PDStringEncoding, x: c_int, y: c_int, width: c_int, height: c_int, wrap: PDTextWrappingMode, @"align": PDTextAlignment) callconv(.C) void,
409
+
410
+
// 2.7
411
+
getTextHeightForMaxWidth: *const fn (font: ?*LCDFont, text: ?[*:0]const u8, len: usize, maxwidth: c_int, encoding: PDStringEncoding, wrap: PDTextWrappingMode, tracking: c_int, extraLeading: c_int) callconv(.C) c_int,
412
+
drawRoundRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, radius: c_int, lineWidth: c_int, color: LCDColor) callconv(.C) void,
413
+
fillRoundRect: *const fn (x: c_int, y: c_int, width: c_int, height: c_int, radius: c_int, color: LCDColor) callconv(.C) void,
414
+
415
+
// 3.0
416
+
tilemap: *const PlaydateTilemap,
417
+
videostream: *const PlaydateVideostream,
418
+
};
419
+
pub const PlaydateDisplay = struct {
420
+
getWidth: *const fn () callconv(.C) c_int,
421
+
getHeight: *const fn () callconv(.C) c_int,
422
+
423
+
setRefreshRate: *const fn (rate: f32) callconv(.C) void,
424
+
425
+
setInverted: *const fn (flag: c_int) callconv(.C) void,
426
+
setScale: *const fn (s: c_uint) callconv(.C) void,
427
+
setMosaic: *const fn (x: c_uint, y: c_uint) callconv(.C) void,
428
+
setFlipped: *const fn (x: c_int, y: c_int) callconv(.C) void,
429
+
setOffset: *const fn (x: c_int, y: c_int) callconv(.C) void,
430
+
431
+
// 2.7
432
+
getRefreshRate: *const fn () callconv(.C) f32,
433
+
getFPS: *const fn () callconv(.C) f32,
434
+
};
435
+
436
+
//////File System/////
437
+
pub const SDFile = opaque {};
438
+
439
+
pub const FileOptions = c_int;
440
+
pub const FILE_READ = (1 << 0);
441
+
pub const FILE_READ_DATA = (1 << 1);
442
+
pub const FILE_WRITE = (1 << 2);
443
+
pub const FILE_APPEND = (2 << 2);
444
+
445
+
pub const SEEK_SET = 0;
446
+
pub const SEEK_CUR = 1;
447
+
pub const SEEK_END = 2;
448
+
449
+
pub const FileStat = extern struct {
450
+
isdir: c_int,
451
+
size: c_uint,
452
+
m_year: c_int,
453
+
m_month: c_int,
454
+
m_day: c_int,
455
+
m_hour: c_int,
456
+
m_minute: c_int,
457
+
m_second: c_int,
458
+
};
459
+
460
+
pub const PlaydateFile = extern struct {
461
+
geterr: *const fn () callconv(.C) ?[*:0]const u8,
462
+
463
+
listfiles: *const fn (
464
+
path: ?[*:0]const u8,
465
+
callback: *const fn (path: ?[*:0]const u8, userdata: ?*anyopaque) callconv(.C) void,
466
+
userdata: ?*anyopaque,
467
+
showhidden: c_int,
468
+
) callconv(.C) c_int,
469
+
stat: *const fn (path: ?[*:0]const u8, stat: ?*FileStat) callconv(.C) c_int,
470
+
mkdir: *const fn (path: ?[*:0]const u8) callconv(.C) c_int,
471
+
unlink: *const fn (name: ?[*:0]const u8, recursive: c_int) callconv(.C) c_int,
472
+
rename: *const fn (from: ?[*:0]const u8, to: ?[*:0]const u8) callconv(.C) c_int,
473
+
474
+
open: *const fn (name: ?[*:0]const u8, mode: FileOptions) callconv(.C) ?*SDFile,
475
+
close: *const fn (file: ?*SDFile) callconv(.C) c_int,
476
+
read: *const fn (file: ?*SDFile, buf: ?*anyopaque, len: c_uint) callconv(.C) c_int,
477
+
write: *const fn (file: ?*SDFile, buf: ?*const anyopaque, len: c_uint) callconv(.C) c_int,
478
+
flush: *const fn (file: ?*SDFile) callconv(.C) c_int,
479
+
tell: *const fn (file: ?*SDFile) callconv(.C) c_int,
480
+
seek: *const fn (file: ?*SDFile, pos: c_int, whence: c_int) callconv(.C) c_int,
481
+
};
482
+
483
+
/////////Audio//////////////
484
+
pub const MicSource = enum(c_int) {
485
+
kMicInputAutodetect = 0,
486
+
kMicInputInternal = 1,
487
+
kMicInputHeadset = 2,
488
+
};
489
+
pub const PlaydateSound = extern struct {
490
+
channel: *const PlaydateSoundChannel,
491
+
fileplayer: *const PlaydateSoundFileplayer,
492
+
sample: *const PlaydateSoundSample,
493
+
sampleplayer: *const PlaydateSoundSampleplayer,
494
+
synth: *const PlaydateSoundSynth,
495
+
sequence: *const PlaydateSoundSequence,
496
+
effect: *const PlaydateSoundEffect,
497
+
lfo: *const PlaydateSoundLFO,
498
+
envelope: *const PlaydateSoundEnvelope,
499
+
source: *const PlaydateSoundSource,
500
+
controlsignal: *const PlaydateControlSignal,
501
+
track: *const PlaydateSoundTrack,
502
+
instrument: *const PlaydateSoundInstrument,
503
+
504
+
getCurrentTime: *const fn () callconv(.C) u32,
505
+
addSource: *const fn (callback: AudioSourceFunction, context: ?*anyopaque, stereo: c_int) callconv(.C) ?*SoundSource,
506
+
507
+
getDefaultChannel: *const fn () callconv(.C) ?*SoundChannel,
508
+
509
+
addChannel: *const fn (channel: ?*SoundChannel) callconv(.C) void,
510
+
removeChannel: *const fn (channel: ?*SoundChannel) callconv(.C) void,
511
+
512
+
setMicCallback: *const fn (callback: RecordCallback, context: ?*anyopaque, source: MicSource) callconv(.C) void,
513
+
getHeadphoneState: *const fn (
514
+
headphone: ?*c_int,
515
+
headsetmic: ?*c_int,
516
+
changeCallback: ?*const fn (headphone: c_int, mic: c_int) callconv(.C) void,
517
+
) callconv(.C) void,
518
+
setOutputsActive: *const fn (headphone: c_int, mic: c_int) callconv(.C) void,
519
+
520
+
// 1.5
521
+
removeSource: *const fn (?*SoundSource) callconv(.C) void,
522
+
523
+
// 1.12
524
+
signal: *const PlaydateSoundSignal,
525
+
526
+
// 2.2
527
+
getError: *const fn () callconv(.C) ?[*:0]const u8,
528
+
};
529
+
530
+
//data is mono
531
+
pub const RecordCallback = *const fn (context: ?*anyopaque, buffer: [*c]i16, length: c_int) callconv(.C) c_int;
532
+
// len is # of samples in each buffer, function should return 1 if it produced output
533
+
pub const AudioSourceFunction = *const fn (context: ?*anyopaque, left: [*c]i16, right: [*c]i16, len: c_int) callconv(.C) c_int;
534
+
pub const SndCallbackProc = *const fn (c: ?*SoundSource, userdata: ?*anyopaque) callconv(.C) void;
535
+
536
+
pub const SoundChannel = opaque {};
537
+
pub const SoundSource = opaque {};
538
+
pub const SoundEffect = opaque {};
539
+
pub const PDSynthSignalValue = opaque {};
540
+
541
+
pub const PlaydateSoundChannel = extern struct {
542
+
newChannel: *const fn () callconv(.C) ?*SoundChannel,
543
+
freeChannel: *const fn (channel: ?*SoundChannel) callconv(.C) void,
544
+
addSource: *const fn (channel: ?*SoundChannel, source: ?*SoundSource) callconv(.C) c_int,
545
+
removeSource: *const fn (channel: ?*SoundChannel, source: ?*SoundSource) callconv(.C) c_int,
546
+
addCallbackSource: *const fn (?*SoundChannel, AudioSourceFunction, ?*anyopaque, c_int) callconv(.C) ?*SoundSource,
547
+
addEffect: *const fn (channel: ?*SoundChannel, effect: ?*SoundEffect) callconv(.C) void,
548
+
removeEffect: *const fn (channel: ?*SoundChannel, effect: ?*SoundEffect) callconv(.C) void,
549
+
setVolume: *const fn (channel: ?*SoundChannel, f32) callconv(.C) void,
550
+
getVolume: *const fn (channel: ?*SoundChannel) callconv(.C) f32,
551
+
setVolumeModulator: *const fn (channel: ?*SoundChannel, mod: ?*PDSynthSignalValue) callconv(.C) void,
552
+
getVolumeModulator: *const fn (channel: ?*SoundChannel) callconv(.C) ?*PDSynthSignalValue,
553
+
setPan: *const fn (channel: ?*SoundChannel, pan: f32) callconv(.C) void,
554
+
setPanModulator: *const fn (channel: ?*SoundChannel, mod: ?*PDSynthSignalValue) callconv(.C) void,
555
+
getPanModulator: *const fn (channel: ?*SoundChannel) callconv(.C) ?*PDSynthSignalValue,
556
+
getDryLevelSignal: *const fn (channe: ?*SoundChannel) callconv(.C) ?*PDSynthSignalValue,
557
+
getWetLevelSignal: *const fn (channel: ?*SoundChannel) callconv(.C) ?*PDSynthSignalValue,
558
+
};
559
+
560
+
pub const FilePlayer = SoundSource;
561
+
pub const PlaydateSoundFileplayer = extern struct {
562
+
newPlayer: *const fn () callconv(.C) ?*FilePlayer,
563
+
freePlayer: *const fn (player: ?*FilePlayer) callconv(.C) void,
564
+
loadIntoPlayer: *const fn (player: ?*FilePlayer, path: ?[*:0]const u8) callconv(.C) c_int,
565
+
setBufferLength: *const fn (player: ?*FilePlayer, bufferLen: f32) callconv(.C) void,
566
+
play: *const fn (player: ?*FilePlayer, repeat: c_int) callconv(.C) c_int,
567
+
isPlaying: *const fn (player: ?*FilePlayer) callconv(.C) c_int,
568
+
pause: *const fn (player: ?*FilePlayer) callconv(.C) void,
569
+
stop: *const fn (player: ?*FilePlayer) callconv(.C) void,
570
+
setVolume: *const fn (player: ?*FilePlayer, left: f32, right: f32) callconv(.C) void,
571
+
getVolume: *const fn (player: ?*FilePlayer, left: ?*f32, right: ?*f32) callconv(.C) void,
572
+
getLength: *const fn (player: ?*FilePlayer) callconv(.C) f32,
573
+
setOffset: *const fn (player: ?*FilePlayer, offset: f32) callconv(.C) void,
574
+
setRate: *const fn (player: ?*FilePlayer, rate: f32) callconv(.C) void,
575
+
setLoopRange: *const fn (player: ?*FilePlayer, start: f32, end: f32) callconv(.C) void,
576
+
didUnderrun: *const fn (player: ?*FilePlayer) callconv(.C) c_int,
577
+
setFinishCallback: *const fn (
578
+
player: ?*FilePlayer,
579
+
callback: ?SndCallbackProc,
580
+
userdata: ?*anyopaque,
581
+
) callconv(.C) void,
582
+
setLoopCallback: *const fn (
583
+
player: ?*FilePlayer,
584
+
callback: ?SndCallbackProc,
585
+
userdata: ?*anyopaque,
586
+
) callconv(.C) void,
587
+
getOffset: *const fn (player: ?*FilePlayer) callconv(.C) f32,
588
+
getRate: *const fn (player: ?*FilePlayer) callconv(.C) f32,
589
+
setStopOnUnderrun: *const fn (player: ?*FilePlayer, flag: c_int) callconv(.C) void,
590
+
fadeVolume: *const fn (
591
+
player: ?*FilePlayer,
592
+
left: f32,
593
+
right: f32,
594
+
len: i32,
595
+
finishCallback: ?SndCallbackProc,
596
+
userdata: ?*anyopaque,
597
+
) callconv(.C) void,
598
+
setMP3StreamSource: *const fn (
599
+
player: ?*FilePlayer,
600
+
dataSource: *const fn (data: [*c]u8, bytes: c_int, userdata: ?*anyopaque) callconv(.C) c_int,
601
+
userdata: ?*anyopaque,
602
+
bufferLen: f32,
603
+
) callconv(.C) void,
604
+
};
605
+
606
+
pub const AudioSample = opaque {};
607
+
pub const SamplePlayer = SoundSource;
608
+
609
+
pub const SoundFormat = enum(c_uint) {
610
+
kSound8bitMono = 0,
611
+
kSound8bitStereo = 1,
612
+
kSound16bitMono = 2,
613
+
kSound16bitStereo = 3,
614
+
kSoundADPCMMono = 4,
615
+
kSoundADPCMStereo = 5,
616
+
};
617
+
pub inline fn SoundFormatIsStereo(f: SoundFormat) bool {
618
+
return @intFromEnum(f) & 1;
619
+
}
620
+
pub inline fn SoundFormatIs16bit(f: SoundFormat) bool {
621
+
return switch (f) {
622
+
.kSound16bitMono,
623
+
.kSound16bitStereo,
624
+
.kSoundADPCMMono,
625
+
.kSoundADPCMStereo,
626
+
=> true,
627
+
else => false,
628
+
};
629
+
}
630
+
pub inline fn SoundFormat_bytesPerFrame(fmt: SoundFormat) u32 {
631
+
return (if (SoundFormatIsStereo(fmt)) 2 else 1) *
632
+
(if (SoundFormatIs16bit(fmt)) 2 else 1);
633
+
}
634
+
635
+
pub const PlaydateSoundSample = extern struct {
636
+
newSampleBuffer: *const fn (byteCount: c_int) callconv(.C) ?*AudioSample,
637
+
loadIntoSample: *const fn (sample: ?*AudioSample, path: ?[*:0]const u8) callconv(.C) c_int,
638
+
load: *const fn (path: ?[*:0]const u8) callconv(.C) ?*AudioSample,
639
+
newSampleFromData: *const fn (data: [*c]u8, format: SoundFormat, sampleRate: u32, byteCount: c_int, shouldFreeData: c_int) callconv(.C) ?*AudioSample,
640
+
getData: *const fn (sample: ?*AudioSample, data: ?*[*c]u8, format: [*c]SoundFormat, sampleRate: ?*u32, byteLength: ?*u32) callconv(.C) void,
641
+
freeSample: *const fn (sample: ?*AudioSample) callconv(.C) void,
642
+
getLength: *const fn (sample: ?*AudioSample) callconv(.C) f32,
643
+
644
+
// 2.4
645
+
decompress: *const fn (sample: ?*AudioSample) callconv(.C) c_int,
646
+
};
647
+
648
+
pub const PlaydateSoundSampleplayer = extern struct {
649
+
newPlayer: *const fn () callconv(.C) ?*SamplePlayer,
650
+
freePlayer: *const fn (?*SamplePlayer) callconv(.C) void,
651
+
setSample: *const fn (player: ?*SamplePlayer, sample: ?*AudioSample) callconv(.C) void,
652
+
play: *const fn (player: ?*SamplePlayer, repeat: c_int, rate: f32) callconv(.C) c_int,
653
+
isPlaying: *const fn (player: ?*SamplePlayer) callconv(.C) c_int,
654
+
stop: *const fn (player: ?*SamplePlayer) callconv(.C) void,
655
+
setVolume: *const fn (player: ?*SamplePlayer, left: f32, right: f32) callconv(.C) void,
656
+
getVolume: *const fn (player: ?*SamplePlayer, left: ?*f32, right: ?*f32) callconv(.C) void,
657
+
getLength: *const fn (player: ?*SamplePlayer) callconv(.C) f32,
658
+
setOffset: *const fn (player: ?*SamplePlayer, offset: f32) callconv(.C) void,
659
+
setRate: *const fn (player: ?*SamplePlayer, rate: f32) callconv(.C) void,
660
+
setPlayRange: *const fn (player: ?*SamplePlayer, start: c_int, end: c_int) callconv(.C) void,
661
+
setFinishCallback: *const fn (
662
+
player: ?*SamplePlayer,
663
+
callback: ?SndCallbackProc,
664
+
userdata: ?*anyopaque,
665
+
) callconv(.C) void,
666
+
setLoopCallback: *const fn (
667
+
player: ?*SamplePlayer,
668
+
callback: ?SndCallbackProc,
669
+
userdata: ?*anyopaque,
670
+
) callconv(.C) void,
671
+
getOffset: *const fn (player: ?*SamplePlayer) callconv(.C) f32,
672
+
getRate: *const fn (player: ?*SamplePlayer) callconv(.C) f32,
673
+
setPaused: *const fn (player: ?*SamplePlayer, flag: c_int) callconv(.C) void,
674
+
};
675
+
676
+
pub const PDSynth = SoundSource;
677
+
pub const SoundWaveform = enum(c_uint) {
678
+
kWaveformSquare = 0,
679
+
kWaveformTriangle = 1,
680
+
kWaveformSine = 2,
681
+
kWaveformNoise = 3,
682
+
kWaveformSawtooth = 4,
683
+
kWaveformPOPhase = 5,
684
+
kWaveformPODigital = 6,
685
+
kWaveformPOVosim = 7,
686
+
};
687
+
pub const NOTE_C4 = 60.0;
688
+
pub const MIDINote = f32;
689
+
pub inline fn pd_noteToFrequency(n: MIDINote) f32 {
690
+
return 440 * std.math.pow(f32, 2, (n - 69) / 12.0);
691
+
}
692
+
pub inline fn pd_frequencyToNote(f: f32) MIDINote {
693
+
return 12 * std.math.log(f32, 2, f) - 36.376316562;
694
+
}
695
+
696
+
// generator render callback
697
+
// samples are in Q8.24 format. left is either the left channel or the single mono channel,
698
+
// right is non-NULL only if the stereo flag was set in the setGenerator() call.
699
+
// nsamples is at most 256 but may be shorter
700
+
// rate is Q0.32 per-frame phase step, drate is per-frame rate step (i.e., do rate += drate every frame)
701
+
// return value is the number of sample frames rendered
702
+
pub const SynthRenderFunc = *const fn (userdata: ?*anyopaque, left: [*c]i32, right: [*c]i32, nsamples: c_int, rate: u32, drate: i32) callconv(.C) c_int;
703
+
704
+
// generator event callbacks
705
+
706
+
// len == -1 if indefinite
707
+
pub const SynthNoteOnFunc = *const fn (userdata: ?*anyopaque, note: MIDINote, velocity: f32, len: f32) callconv(.C) void;
708
+
709
+
pub const SynthReleaseFunc = *const fn (userdata: ?*anyopaque, stop: c_int) callconv(.C) void;
710
+
pub const SynthSetParameterFunc = *const fn (userdata: ?*anyopaque, parameter: c_int, value: f32) callconv(.C) c_int;
711
+
pub const SynthDeallocFunc = *const fn (userdata: ?*anyopaque) callconv(.C) void;
712
+
pub const SynthCopyUserdata = *const fn (userdata: ?*anyopaque) callconv(.C) ?*anyopaque;
713
+
714
+
pub const PlaydateSoundSynth = extern struct {
715
+
newSynth: *const fn () callconv(.C) ?*PDSynth,
716
+
freeSynth: *const fn (synth: ?*PDSynth) callconv(.C) void,
717
+
718
+
setWaveform: *const fn (synth: ?*PDSynth, wave: SoundWaveform) callconv(.C) void,
719
+
setGenerator_deprecated: *const fn (
720
+
synth: ?*PDSynth,
721
+
stereo: c_int,
722
+
render: SynthRenderFunc,
723
+
note_on: SynthNoteOnFunc,
724
+
release: SynthReleaseFunc,
725
+
set_param: SynthSetParameterFunc,
726
+
dealloc: SynthDeallocFunc,
727
+
userdata: ?*anyopaque,
728
+
) callconv(.C) void,
729
+
setSample: *const fn (
730
+
synth: ?*PDSynth,
731
+
sample: ?*AudioSample,
732
+
sustain_start: u32,
733
+
sustain_end: u32,
734
+
) callconv(.C) void,
735
+
736
+
setAttackTime: *const fn (synth: ?*PDSynth, attack: f32) callconv(.C) void,
737
+
setDecayTime: *const fn (synth: ?*PDSynth, decay: f32) callconv(.C) void,
738
+
setSustainLevel: *const fn (synth: ?*PDSynth, sustain: f32) callconv(.C) void,
739
+
setReleaseTime: *const fn (synth: ?*PDSynth, release: f32) callconv(.C) void,
740
+
741
+
setTranspose: *const fn (synth: ?*PDSynth, half_steps: f32) callconv(.C) void,
742
+
743
+
setFrequencyModulator: *const fn (synth: ?*PDSynth, mod: ?*PDSynthSignalValue) callconv(.C) void,
744
+
getFrequencyModulator: *const fn (synth: ?*PDSynth) callconv(.C) ?*PDSynthSignalValue,
745
+
setAmplitudeModulator: *const fn (synth: ?*PDSynth, mod: ?*PDSynthSignalValue) callconv(.C) void,
746
+
getAmplitudeModulator: *const fn (synth: ?*PDSynth) callconv(.C) ?*PDSynthSignalValue,
747
+
748
+
getParameterCount: *const fn (synth: ?*PDSynth) callconv(.C) c_int,
749
+
setParameter: *const fn (synth: ?*PDSynth, parameter: c_int, value: f32) callconv(.C) c_int,
750
+
setParameterModulator: *const fn (synth: ?*PDSynth, parameter: c_int, mod: ?*PDSynthSignalValue) callconv(.C) void,
751
+
getParameterModulator: *const fn (synth: ?*PDSynth, parameter: c_int) callconv(.C) ?*PDSynthSignalValue,
752
+
753
+
playNote: *const fn (synth: ?*PDSynth, freq: f32, vel: f32, len: f32, when: u32) callconv(.C) void,
754
+
playMIDINote: *const fn (synth: ?*PDSynth, note: MIDINote, vel: f32, len: f32, when: u32) callconv(.C) void,
755
+
noteOff: *const fn (synth: ?*PDSynth, when: u32) callconv(.C) void,
756
+
stop: *const fn (synth: ?*PDSynth) callconv(.C) void,
757
+
758
+
setVolume: *const fn (synth: ?*PDSynth, left: f32, right: f32) callconv(.C) void,
759
+
getVolume: *const fn (synth: ?*PDSynth, left: ?*f32, right: ?*f32) callconv(.C) void,
760
+
761
+
isPlaying: *const fn (synth: ?*PDSynth) callconv(.C) c_int,
762
+
763
+
// 1.13
764
+
getEnvelope: *const fn (synth: ?*PDSynth) callconv(.C) ?*PDSynthEnvelope, // synth keeps ownership--don't free this!
765
+
766
+
// 2.2
767
+
setWavetable: *const fn (synth: ?*PDSynth, sample: ?*AudioSample, log2size: c_int, columns: c_int, rows: c_int) callconv(.C) c_int,
768
+
769
+
// 2.4
770
+
setGenerator: *const fn (
771
+
synth: ?*PDSynth,
772
+
stereo: c_int,
773
+
render: SynthRenderFunc,
774
+
noteOn: SynthNoteOnFunc,
775
+
release: SynthReleaseFunc,
776
+
setparam: SynthSetParameterFunc,
777
+
dealloc: SynthDeallocFunc,
778
+
copyUserdata: SynthCopyUserdata,
779
+
userdata: ?*anyopaque,
780
+
) callconv(.C) void,
781
+
copy: *const fn (synth: ?*PDSynth) callconv(.C) ?*PDSynth,
782
+
783
+
// 2.6
784
+
clearEnvelope: *const fn (synth: ?*PDSynth) callconv(.C) void,
785
+
};
786
+
787
+
pub const SequenceTrack = opaque {};
788
+
pub const SoundSequence = opaque {};
789
+
pub const SequenceFinishedCallback = *const fn (seq: ?*SoundSequence, userdata: ?*anyopaque) callconv(.C) void;
790
+
791
+
pub const PlaydateSoundSequence = extern struct {
792
+
newSequence: *const fn () callconv(.C) ?*SoundSequence,
793
+
freeSequence: *const fn (sequence: ?*SoundSequence) callconv(.C) void,
794
+
795
+
loadMidiFile: *const fn (seq: ?*SoundSequence, path: ?[*:0]const u8) callconv(.C) c_int,
796
+
getTime: *const fn (seq: ?*SoundSequence) callconv(.C) u32,
797
+
setTime: *const fn (seq: ?*SoundSequence, time: u32) callconv(.C) void,
798
+
setLoops: *const fn (seq: ?*SoundSequence, loopstart: c_int, loopend: c_int, loops: c_int) callconv(.C) void,
799
+
getTempo_deprecated: *const fn (seq: ?*SoundSequence) callconv(.C) c_int,
800
+
setTempo: *const fn (seq: ?*SoundSequence, stepsPerSecond: c_int) callconv(.C) void,
801
+
getTrackCount: *const fn (seq: ?*SoundSequence) callconv(.C) c_int,
802
+
addTrack: *const fn (seq: ?*SoundSequence) callconv(.C) ?*SequenceTrack,
803
+
getTrackAtIndex: *const fn (seq: ?*SoundSequence, track: c_uint) callconv(.C) ?*SequenceTrack,
804
+
setTrackAtIndex: *const fn (seq: ?*SoundSequence, ?*SequenceTrack, idx: c_uint) callconv(.C) void,
805
+
allNotesOff: *const fn (seq: ?*SoundSequence) callconv(.C) void,
806
+
807
+
// 1.1
808
+
isPlaying: *const fn (seq: ?*SoundSequence) callconv(.C) c_int,
809
+
getLength: *const fn (seq: ?*SoundSequence) callconv(.C) u32,
810
+
play: *const fn (seq: ?*SoundSequence, finishCallback: SequenceFinishedCallback, userdata: ?*anyopaque) callconv(.C) void,
811
+
stop: *const fn (seq: ?*SoundSequence) callconv(.C) void,
812
+
getCurrentStep: *const fn (seq: ?*SoundSequence, timeOffset: ?*c_int) callconv(.C) c_int,
813
+
setCurrentStep: *const fn (seq: ?*SoundSequence, step: c_int, timeOffset: c_int, playNotes: c_int) callconv(.C) void,
814
+
815
+
// 2.5
816
+
getTempo: *const fn (seq: ?*SoundSequence) callconv(.C) f32,
817
+
};
818
+
819
+
pub const EffectProc = *const fn (e: ?*SoundEffect, left: [*c]i32, right: [*c]i32, nsamples: c_int, bufactive: c_int) callconv(.C) c_int;
820
+
821
+
pub const PlaydateSoundEffect = extern struct {
822
+
newEffect: *const fn (proc: ?*const EffectProc, userdata: ?*anyopaque) callconv(.C) ?*SoundEffect,
823
+
freeEffect: *const fn (effect: ?*SoundEffect) callconv(.C) void,
824
+
825
+
setMix: *const fn (effect: ?*SoundEffect, level: f32) callconv(.C) void,
826
+
setMixModulator: *const fn (effect: ?*SoundEffect, signal: ?*PDSynthSignalValue) callconv(.C) void,
827
+
getMixModulator: *const fn (effect: ?*SoundEffect) callconv(.C) ?*PDSynthSignalValue,
828
+
829
+
setUserdata: *const fn (effect: ?*SoundEffect, userdata: ?*anyopaque) callconv(.C) void,
830
+
getUserdata: *const fn (effect: ?*SoundEffect) callconv(.C) ?*anyopaque,
831
+
832
+
twopolefilter: *const PlaydateSoundEffectTwopolefilter,
833
+
onepolefilter: *const PlaydateSoundEffectOnepolefilter,
834
+
bitcrusher: *const PlaydateSoundEffectBitcrusher,
835
+
ringmodulator: *const PlaydateSoundEffectRingmodulator,
836
+
delayline: *const PlaydateSoundEffectDelayline,
837
+
overdrive: *const PlaydateSoundEffectOverdrive,
838
+
};
839
+
pub const LFOType = enum(c_uint) {
840
+
kLFOTypeSquare = 0,
841
+
kLFOTypeTriangle = 1,
842
+
kLFOTypeSine = 2,
843
+
kLFOTypeSampleAndHold = 3,
844
+
kLFOTypeSawtoothUp = 4,
845
+
kLFOTypeSawtoothDown = 5,
846
+
kLFOTypeArpeggiator = 6,
847
+
kLFOTypeFunction = 7,
848
+
};
849
+
pub const PDSynthLFO = opaque {};
850
+
pub const PlaydateSoundLFO = extern struct {
851
+
newLFO: *const fn (LFOType) callconv(.C) ?*PDSynthLFO,
852
+
freeLFO: *const fn (lfo: ?*PDSynthLFO) callconv(.C) void,
853
+
854
+
setType: *const fn (lfo: ?*PDSynthLFO, type: LFOType) callconv(.C) void,
855
+
setRate: *const fn (lfo: ?*PDSynthLFO, rate: f32) callconv(.C) void,
856
+
setPhase: *const fn (lfo: ?*PDSynthLFO, phase: f32) callconv(.C) void,
857
+
setCenter: *const fn (lfo: ?*PDSynthLFO, center: f32) callconv(.C) void,
858
+
setDepth: *const fn (lfo: ?*PDSynthLFO, depth: f32) callconv(.C) void,
859
+
setArpeggiation: *const fn (lfo: ?*PDSynthLFO, nSteps: c_int, steps: [*c]f32) callconv(.C) void,
860
+
setFunction: *const fn (
861
+
lfo: ?*PDSynthLFO,
862
+
lfoFunc: *const fn (lfo: ?*PDSynthLFO, userdata: ?*anyopaque) callconv(.C) f32,
863
+
userdata: ?*anyopaque,
864
+
interpolate: c_int,
865
+
) callconv(.C) void,
866
+
setDelay: *const fn (lfo: ?*PDSynthLFO, holdoff: f32, ramptime: f32) callconv(.C) void,
867
+
setRetrigger: *const fn (lfo: ?*PDSynthLFO, flag: c_int) callconv(.C) void,
868
+
869
+
getValue: *const fn (lfo: ?*PDSynthLFO) callconv(.C) f32,
870
+
871
+
// 1.10
872
+
setGlobal: *const fn (lfo: ?*PDSynthLFO, global: c_int) callconv(.C) void,
873
+
};
874
+
875
+
pub const PDSynthEnvelope = opaque {};
876
+
pub const PlaydateSoundEnvelope = extern struct {
877
+
newEnvelope: *const fn (attack: f32, decay: f32, sustain: f32, release: f32) callconv(.C) ?*PDSynthEnvelope,
878
+
freeEnvelope: *const fn (env: ?*PDSynthEnvelope) callconv(.C) void,
879
+
880
+
setAttack: *const fn (env: ?*PDSynthEnvelope, attack: f32) callconv(.C) void,
881
+
setDecay: *const fn (env: ?*PDSynthEnvelope, decay: f32) callconv(.C) void,
882
+
setSustain: *const fn (env: ?*PDSynthEnvelope, sustain: f32) callconv(.C) void,
883
+
setRelease: *const fn (env: ?*PDSynthEnvelope, release: f32) callconv(.C) void,
884
+
885
+
setLegato: *const fn (env: ?*PDSynthEnvelope, flag: c_int) callconv(.C) void,
886
+
setRetrigger: *const fn (env: ?*PDSynthEnvelope, flag: c_int) callconv(.C) void,
887
+
888
+
getValue: *const fn (env: ?*PDSynthEnvelope) callconv(.C) f32,
889
+
890
+
// 1.13
891
+
setCurvature: *const fn (env: ?*PDSynthEnvelope, amount: f32) callconv(.C) void,
892
+
setVelocitySensitivity: *const fn (env: ?*PDSynthEnvelope, velsens: f32) callconv(.C) void,
893
+
setRateScaling: *const fn (env: ?*PDSynthEnvelope, scaling: f32, start: MIDINote, end: MIDINote) callconv(.C) void,
894
+
};
895
+
896
+
pub const PlaydateSoundSource = extern struct {
897
+
setVolume: *const fn (c: ?*SoundSource, lvol: f32, rvol: f32) callconv(.C) void,
898
+
getVolume: *const fn (c: ?*SoundSource, outl: ?*f32, outr: ?*f32) callconv(.C) void,
899
+
isPlaying: *const fn (c: ?*SoundSource) callconv(.C) c_int,
900
+
setFinishCallback: *const fn (
901
+
c: ?*SoundSource,
902
+
callback: SndCallbackProc,
903
+
userdata: ?*anyopaque,
904
+
) callconv(.C) void,
905
+
};
906
+
907
+
pub const ControlSignal = opaque {};
908
+
pub const PlaydateControlSignal = extern struct {
909
+
newSignal: *const fn () callconv(.C) ?*ControlSignal,
910
+
freeSignal: *const fn (signal: ?*ControlSignal) callconv(.C) void,
911
+
clearEvents: *const fn (control: ?*ControlSignal) callconv(.C) void,
912
+
addEvent: *const fn (control: ?*ControlSignal, step: c_int, value: f32, c_int) callconv(.C) void,
913
+
removeEvent: *const fn (control: ?*ControlSignal, step: c_int) callconv(.C) void,
914
+
getMIDIControllerNumber: *const fn (control: ?*ControlSignal) callconv(.C) c_int,
915
+
};
916
+
917
+
pub const PlaydateSoundTrack = extern struct {
918
+
newTrack: *const fn () callconv(.C) ?*SequenceTrack,
919
+
freeTrack: *const fn (track: ?*SequenceTrack) callconv(.C) void,
920
+
921
+
setInstrument: *const fn (track: ?*SequenceTrack, inst: ?*PDSynthInstrument) callconv(.C) void,
922
+
getInstrument: *const fn (track: ?*SequenceTrack) callconv(.C) ?*PDSynthInstrument,
923
+
924
+
addNoteEvent: *const fn (track: ?*SequenceTrack, step: u32, len: u32, note: MIDINote, velocity: f32) callconv(.C) void,
925
+
removeNoteEvent: *const fn (track: ?*SequenceTrack, step: u32, note: MIDINote) callconv(.C) void,
926
+
clearNotes: *const fn (track: ?*SequenceTrack) callconv(.C) void,
927
+
928
+
getControlSignalCount: *const fn (track: ?*SequenceTrack) callconv(.C) c_int,
929
+
getControlSignal: *const fn (track: ?*SequenceTrack, idx: c_int) callconv(.C) ?*ControlSignal,
930
+
clearControlEvents: *const fn (track: ?*SequenceTrack) callconv(.C) void,
931
+
932
+
getPolyphony: *const fn (track: ?*SequenceTrack) callconv(.C) c_int,
933
+
activeVoiceCount: *const fn (track: ?*SequenceTrack) callconv(.C) c_int,
934
+
935
+
setMuted: *const fn (track: ?*SequenceTrack, mute: c_int) callconv(.C) void,
936
+
937
+
// 1.1
938
+
getLength: *const fn (track: ?*SequenceTrack) callconv(.C) u32,
939
+
getIndexForStep: *const fn (track: ?*SequenceTrack, step: u32) callconv(.C) c_int,
940
+
getNoteAtIndex: *const fn (track: ?*SequenceTrack, index: c_int, outSteo: ?*u32, outLen: ?*u32, outeNote: ?*MIDINote, outVelocity: ?*f32) callconv(.C) c_int,
941
+
942
+
//1.10
943
+
getSignalForController: *const fn (track: ?*SequenceTrack, controller: c_int, create: c_int) callconv(.C) ?*ControlSignal,
944
+
};
945
+
946
+
pub const PDSynthInstrument = SoundSource;
947
+
pub const PlaydateSoundInstrument = extern struct {
948
+
newInstrument: *const fn () callconv(.C) ?*PDSynthInstrument,
949
+
freeInstrument: *const fn (inst: ?*PDSynthInstrument) callconv(.C) void,
950
+
addVoice: *const fn (inst: ?*PDSynthInstrument, synth: ?*PDSynth, rangeStart: MIDINote, rangeEnd: MIDINote, transpose: f32) callconv(.C) c_int,
951
+
playNote: *const fn (inst: ?*PDSynthInstrument, frequency: f32, vel: f32, len: f32, when: u32) callconv(.C) ?*PDSynth,
952
+
playMIDINote: *const fn (inst: ?*PDSynthInstrument, note: MIDINote, vel: f32, len: f32, when: u32) callconv(.C) ?*PDSynth,
953
+
setPitchBend: *const fn (inst: ?*PDSynthInstrument, bend: f32) callconv(.C) void,
954
+
setPitchBendRange: *const fn (inst: ?*PDSynthInstrument, halfSteps: f32) callconv(.C) void,
955
+
setTranspose: *const fn (inst: ?*PDSynthInstrument, halfSteps: f32) callconv(.C) void,
956
+
noteOff: *const fn (inst: ?*PDSynthInstrument, note: MIDINote, when: u32) callconv(.C) void,
957
+
allNotesOff: *const fn (inst: ?*PDSynthInstrument, when: u32) callconv(.C) void,
958
+
setVolume: *const fn (inst: ?*PDSynthInstrument, left: f32, right: f32) callconv(.C) void,
959
+
getVolume: *const fn (inst: ?*PDSynthInstrument, left: ?*f32, right: ?*f32) callconv(.C) void,
960
+
activeVoiceCount: *const fn (inst: ?*PDSynthInstrument) callconv(.C) c_int,
961
+
};
962
+
963
+
pub const PDSynthSignal = opaque {};
964
+
pub const SignalStepFunc = *const fn (userdata: ?*anyopaque, ioframes: [*c]c_int, ifval: ?*f32) callconv(.C) f32;
965
+
// len = -1 for indefinite
966
+
pub const SignalNoteOnFunc = *const fn (userdata: ?*anyopaque, note: MIDINote, vel: f32, len: f32) callconv(.C) void;
967
+
// ended = 0 for note release, = 1 when note stops playing
968
+
pub const SignalNoteOffFunc = *const fn (userdata: ?*anyopaque, stopped: c_int, offset: c_int) callconv(.C) void;
969
+
pub const SignalDeallocFunc = *const fn (userdata: ?*anyopaque) callconv(.C) void;
970
+
pub const PlaydateSoundSignal = struct {
971
+
newSignal: *const fn (step: SignalStepFunc, noteOn: SignalNoteOnFunc, noteOff: SignalNoteOffFunc, dealloc: SignalDeallocFunc, userdata: ?*anyopaque) callconv(.C) ?*PDSynthSignal,
972
+
freeSignal: *const fn (signal: ?*PDSynthSignal) callconv(.C) void,
973
+
getValue: *const fn (signal: ?*PDSynthSignal) callconv(.C) f32,
974
+
setValueScale: *const fn (signal: ?*PDSynthSignal, scale: f32) callconv(.C) void,
975
+
setValueOffset: *const fn (signal: ?*PDSynthSignal, offset: f32) callconv(.C) void,
976
+
};
977
+
978
+
// EFFECTS
979
+
980
+
// A SoundEffect processes the output of a channel's SoundSources
981
+
982
+
pub const TwoPoleFilter = SoundEffect;
983
+
pub const TwoPoleFilterType = enum(c_int) {
984
+
FilterTypeLowPass,
985
+
FilterTypeHighPass,
986
+
FilterTypeBandPass,
987
+
FilterTypeNotch,
988
+
FilterTypePEQ,
989
+
FilterTypeLowShelf,
990
+
FilterTypeHighShelf,
991
+
};
992
+
pub const PlaydateSoundEffectTwopolefilter = extern struct {
993
+
newFilter: *const fn () callconv(.C) ?*TwoPoleFilter,
994
+
freeFilter: *const fn (filter: ?*TwoPoleFilter) callconv(.C) void,
995
+
setType: *const fn (filter: ?*TwoPoleFilter, type: TwoPoleFilterType) callconv(.C) void,
996
+
setFrequency: *const fn (filter: ?*TwoPoleFilter, frequency: f32) callconv(.C) void,
997
+
setFrequencyModulator: *const fn (filter: ?*TwoPoleFilter, signal: ?*PDSynthSignalValue) callconv(.C) void,
998
+
getFrequencyModulator: *const fn (filter: ?*TwoPoleFilter) callconv(.C) ?*PDSynthSignalValue,
999
+
setGain: *const fn (filter: ?*TwoPoleFilter, f32) callconv(.C) void,
1000
+
setResonance: *const fn (filter: ?*TwoPoleFilter, f32) callconv(.C) void,
1001
+
setResonanceModulator: *const fn (filter: ?*TwoPoleFilter, signal: ?*PDSynthSignalValue) callconv(.C) void,
1002
+
getResonanceModulator: *const fn (filter: ?*TwoPoleFilter) callconv(.C) ?*PDSynthSignalValue,
1003
+
};
1004
+
1005
+
pub const OnePoleFilter = SoundEffect;
1006
+
pub const PlaydateSoundEffectOnepolefilter = extern struct {
1007
+
newFilter: *const fn () callconv(.C) ?*OnePoleFilter,
1008
+
freeFilter: *const fn (filter: ?*OnePoleFilter) callconv(.C) void,
1009
+
setParameter: *const fn (filter: ?*OnePoleFilter, parameter: f32) callconv(.C) void,
1010
+
setParameterModulator: *const fn (filter: ?*OnePoleFilter, signal: ?*PDSynthSignalValue) callconv(.C) void,
1011
+
getParameterModulator: *const fn (filter: ?*OnePoleFilter) callconv(.C) ?*PDSynthSignalValue,
1012
+
};
1013
+
1014
+
pub const BitCrusher = SoundEffect;
1015
+
pub const PlaydateSoundEffectBitcrusher = extern struct {
1016
+
newBitCrusher: *const fn () callconv(.C) ?*BitCrusher,
1017
+
freeBitCrusher: *const fn (filter: ?*BitCrusher) callconv(.C) void,
1018
+
setAmount: *const fn (filter: ?*BitCrusher, amount: f32) callconv(.C) void,
1019
+
setAmountModulator: *const fn (filter: ?*BitCrusher, signal: ?*PDSynthSignalValue) callconv(.C) void,
1020
+
getAmountModulator: *const fn (filter: ?*BitCrusher) callconv(.C) ?*PDSynthSignalValue,
1021
+
setUndersampling: *const fn (filter: ?*BitCrusher, undersampling: f32) callconv(.C) void,
1022
+
setUndersampleModulator: *const fn (filter: ?*BitCrusher, signal: ?*PDSynthSignalValue) callconv(.C) void,
1023
+
getUndersampleModulator: *const fn (filter: ?*BitCrusher) callconv(.C) ?*PDSynthSignalValue,
1024
+
};
1025
+
1026
+
pub const RingModulator = SoundEffect;
1027
+
pub const PlaydateSoundEffectRingmodulator = extern struct {
1028
+
newRingmod: *const fn () callconv(.C) ?*RingModulator,
1029
+
freeRingmod: *const fn (filter: ?*RingModulator) callconv(.C) void,
1030
+
setFrequency: *const fn (filter: ?*RingModulator, frequency: f32) callconv(.C) void,
1031
+
setFrequencyModulator: *const fn (filter: ?*RingModulator, signal: ?*PDSynthSignalValue) callconv(.C) void,
1032
+
getFrequencyModulator: *const fn (filter: ?*RingModulator) callconv(.C) ?*PDSynthSignalValue,
1033
+
};
1034
+
1035
+
pub const DelayLine = SoundEffect;
1036
+
pub const DelayLineTap = SoundSource;
1037
+
pub const PlaydateSoundEffectDelayline = extern struct {
1038
+
newDelayLine: *const fn (length: c_int, stereo: c_int) callconv(.C) ?*DelayLine,
1039
+
freeDelayLine: *const fn (filter: ?*DelayLine) callconv(.C) void,
1040
+
setLength: *const fn (filter: ?*DelayLine, frames: c_int) callconv(.C) void,
1041
+
setFeedback: *const fn (filter: ?*DelayLine, fb: f32) callconv(.C) void,
1042
+
addTap: *const fn (filter: ?*DelayLine, delay: c_int) callconv(.C) ?*DelayLineTap,
1043
+
1044
+
// note that DelayLineTap is a SoundSource, not a SoundEffect
1045
+
freeTap: *const fn (tap: ?*DelayLineTap) callconv(.C) void,
1046
+
setTapDelay: *const fn (t: ?*DelayLineTap, frames: c_int) callconv(.C) void,
1047
+
setTapDelayModulator: *const fn (t: ?*DelayLineTap, mod: ?*PDSynthSignalValue) callconv(.C) void,
1048
+
getTapDelayModulator: *const fn (t: ?*DelayLineTap) callconv(.C) ?*PDSynthSignalValue,
1049
+
setTapChannelsFlipped: *const fn (t: ?*DelayLineTap, flip: c_int) callconv(.C) void,
1050
+
};
1051
+
1052
+
pub const Overdrive = SoundEffect;
1053
+
pub const PlaydateSoundEffectOverdrive = extern struct {
1054
+
newOverdrive: *const fn () callconv(.C) ?*Overdrive,
1055
+
freeOverdrive: *const fn (filter: ?*Overdrive) callconv(.C) void,
1056
+
setGain: *const fn (o: ?*Overdrive, gain: f32) callconv(.C) void,
1057
+
setLimit: *const fn (o: ?*Overdrive, limit: f32) callconv(.C) void,
1058
+
setLimitModulator: *const fn (o: ?*Overdrive, mod: ?*PDSynthSignalValue) callconv(.C) void,
1059
+
getLimitModulator: *const fn (o: ?*Overdrive) callconv(.C) ?*PDSynthSignalValue,
1060
+
setOffset: *const fn (o: ?*Overdrive, offset: f32) callconv(.C) void,
1061
+
setOffsetModulator: *const fn (o: ?*Overdrive, mod: ?*PDSynthSignalValue) callconv(.C) void,
1062
+
getOffsetModulator: *const fn (o: ?*Overdrive) callconv(.C) ?*PDSynthSignalValue,
1063
+
};
1064
+
1065
+
//////Sprite/////
1066
+
pub const SpriteCollisionResponseType = enum(c_int) {
1067
+
CollisionTypeSlide,
1068
+
CollisionTypeFreeze,
1069
+
CollisionTypeOverlap,
1070
+
CollisionTypeBounce,
1071
+
};
1072
+
pub const PDRect = extern struct {
1073
+
x: f32,
1074
+
y: f32,
1075
+
width: f32,
1076
+
height: f32,
1077
+
};
1078
+
1079
+
pub fn PDRectMake(x: f32, y: f32, width: f32, height: f32) callconv(.C) PDRect {
1080
+
return .{
1081
+
.x = x,
1082
+
.y = y,
1083
+
.width = width,
1084
+
.height = height,
1085
+
};
1086
+
}
1087
+
1088
+
pub const CollisionPoint = extern struct {
1089
+
x: f32,
1090
+
y: f32,
1091
+
};
1092
+
pub const CollisionVector = extern struct {
1093
+
x: c_int,
1094
+
y: c_int,
1095
+
};
1096
+
1097
+
pub const SpriteCollisionInfo = extern struct {
1098
+
sprite: ?*LCDSprite, // The sprite being moved
1099
+
other: ?*LCDSprite, // The sprite being moved
1100
+
responseType: SpriteCollisionResponseType, // The result of collisionResponse
1101
+
overlaps: u8, // True if the sprite was overlapping other when the collision started. False if it didn’t overlap but tunneled through other.
1102
+
ti: f32, // A number between 0 and 1 indicating how far along the movement to the goal the collision occurred
1103
+
move: CollisionPoint, // The difference between the original coordinates and the actual ones when the collision happened
1104
+
normal: CollisionVector, // The collision normal; usually -1, 0, or 1 in x and y. Use this value to determine things like if your character is touching the ground.
1105
+
touch: CollisionPoint, // The coordinates where the sprite started touching other
1106
+
spriteRect: PDRect, // The rectangle the sprite occupied when the touch happened
1107
+
otherRect: PDRect, // The rectangle the sprite being collided with occupied when the touch happened
1108
+
};
1109
+
1110
+
pub const SpriteQueryInfo = extern struct {
1111
+
sprite: ?*LCDSprite, // The sprite being intersected by the segment
1112
+
// ti1 and ti2 are numbers between 0 and 1 which indicate how far from the starting point of the line segment the collision happened
1113
+
ti1: f32, // entry point
1114
+
ti2: f32, // exit point
1115
+
entryPoint: CollisionPoint, // The coordinates of the first intersection between sprite and the line segment
1116
+
exitPoint: CollisionPoint, // The coordinates of the second intersection between sprite and the line segment
1117
+
};
1118
+
1119
+
pub const LCDSprite = opaque {};
1120
+
pub const CWCollisionInfo = opaque {};
1121
+
pub const CWItemInfo = opaque {};
1122
+
1123
+
pub const LCDSpriteDrawFunction = ?*const fn (sprite: ?*LCDSprite, bounds: PDRect, drawrect: PDRect) callconv(.C) void;
1124
+
pub const LCDSpriteUpdateFunction = ?*const fn (sprite: ?*LCDSprite) callconv(.C) void;
1125
+
pub const LCDSpriteCollisionFilterProc = ?*const fn (sprite: ?*LCDSprite, other: ?*LCDSprite) callconv(.C) SpriteCollisionResponseType;
1126
+
1127
+
pub const PlaydateSprite = extern struct {
1128
+
setAlwaysRedraw: *const fn (flag: c_int) callconv(.C) void,
1129
+
addDirtyRect: *const fn (dirtyRect: LCDRect) callconv(.C) void,
1130
+
drawSprites: *const fn () callconv(.C) void,
1131
+
updateAndDrawSprites: *const fn () callconv(.C) void,
1132
+
1133
+
newSprite: *const fn () callconv(.C) ?*LCDSprite,
1134
+
freeSprite: *const fn (sprite: ?*LCDSprite) callconv(.C) void,
1135
+
copy: *const fn (sprite: ?*LCDSprite) callconv(.C) ?*LCDSprite,
1136
+
1137
+
addSprite: *const fn (sprite: ?*LCDSprite) callconv(.C) void,
1138
+
removeSprite: *const fn (sprite: ?*LCDSprite) callconv(.C) void,
1139
+
removeSprites: *const fn (sprite: [*c]?*LCDSprite, count: c_int) callconv(.C) void,
1140
+
removeAllSprites: *const fn () callconv(.C) void,
1141
+
getSpriteCount: *const fn () callconv(.C) c_int,
1142
+
1143
+
setBounds: *const fn (sprite: ?*LCDSprite, bounds: PDRect) callconv(.C) void,
1144
+
getBounds: *const fn (sprite: ?*LCDSprite) callconv(.C) PDRect,
1145
+
moveTo: *const fn (sprite: ?*LCDSprite, x: f32, y: f32) callconv(.C) void,
1146
+
moveBy: *const fn (sprite: ?*LCDSprite, dx: f32, dy: f32) callconv(.C) void,
1147
+
1148
+
setImage: *const fn (sprite: ?*LCDSprite, image: ?*LCDBitmap, flip: LCDBitmapFlip) callconv(.C) void,
1149
+
getImage: *const fn (sprite: ?*LCDSprite) callconv(.C) ?*LCDBitmap,
1150
+
setSize: *const fn (s: ?*LCDSprite, width: f32, height: f32) callconv(.C) void,
1151
+
setZIndex: *const fn (s: ?*LCDSprite, zIndex: i16) callconv(.C) void,
1152
+
getZIndex: *const fn (sprite: ?*LCDSprite) callconv(.C) i16,
1153
+
1154
+
setDrawMode: *const fn (sprite: ?*LCDSprite, mode: LCDBitmapDrawMode) callconv(.C) LCDBitmapDrawMode,
1155
+
setImageFlip: *const fn (sprite: ?*LCDSprite, flip: LCDBitmapFlip) callconv(.C) void,
1156
+
getImageFlip: *const fn (sprite: ?*LCDSprite) callconv(.C) LCDBitmapFlip,
1157
+
setStencil: *const fn (sprite: ?*LCDSprite, mode: ?*LCDBitmap) callconv(.C) void, // deprecated in favor of setStencilImage()
1158
+
1159
+
setClipRect: *const fn (sprite: ?*LCDSprite, clipRect: LCDRect) callconv(.C) void,
1160
+
clearClipRect: *const fn (sprite: ?*LCDSprite) callconv(.C) void,
1161
+
setClipRectsInRange: *const fn (clipRect: LCDRect, startZ: c_int, endZ: c_int) callconv(.C) void,
1162
+
clearClipRectsInRange: *const fn (startZ: c_int, endZ: c_int) callconv(.C) void,
1163
+
1164
+
setUpdatesEnabled: *const fn (sprite: ?*LCDSprite, flag: c_int) callconv(.C) void,
1165
+
updatesEnabled: *const fn (sprite: ?*LCDSprite) callconv(.C) c_int,
1166
+
setCollisionsEnabled: *const fn (sprite: ?*LCDSprite, flag: c_int) callconv(.C) void,
1167
+
collisionsEnabled: *const fn (sprite: ?*LCDSprite) callconv(.C) c_int,
1168
+
setVisible: *const fn (sprite: ?*LCDSprite, flag: c_int) callconv(.C) void,
1169
+
isVisible: *const fn (sprite: ?*LCDSprite) callconv(.C) c_int,
1170
+
setOpaque: *const fn (sprite: ?*LCDSprite, flag: c_int) callconv(.C) void,
1171
+
markDirty: *const fn (sprite: ?*LCDSprite) callconv(.C) void,
1172
+
1173
+
setTag: *const fn (sprite: ?*LCDSprite, tag: u8) callconv(.C) void,
1174
+
getTag: *const fn (sprite: ?*LCDSprite) callconv(.C) u8,
1175
+
1176
+
setIgnoresDrawOffset: *const fn (sprite: ?*LCDSprite, flag: c_int) callconv(.C) void,
1177
+
1178
+
setUpdateFunction: *const fn (sprite: ?*LCDSprite, func: LCDSpriteUpdateFunction) callconv(.C) void,
1179
+
setDrawFunction: *const fn (sprite: ?*LCDSprite, func: LCDSpriteDrawFunction) callconv(.C) void,
1180
+
1181
+
getPosition: *const fn (s: ?*LCDSprite, x: ?*f32, y: ?*f32) callconv(.C) void,
1182
+
1183
+
// Collisions
1184
+
resetCollisionWorld: *const fn () callconv(.C) void,
1185
+
1186
+
setCollideRect: *const fn (sprite: ?*LCDSprite, collideRect: PDRect) callconv(.C) void,
1187
+
getCollideRect: *const fn (sprite: ?*LCDSprite) callconv(.C) PDRect,
1188
+
clearCollideRect: *const fn (sprite: ?*LCDSprite) callconv(.C) void,
1189
+
1190
+
// caller is responsible for freeing the returned array for all collision methods
1191
+
setCollisionResponseFunction: *const fn (sprite: ?*LCDSprite, func: LCDSpriteCollisionFilterProc) callconv(.C) void,
1192
+
checkCollisions: *const fn (sprite: ?*LCDSprite, goalX: f32, goalY: f32, actualX: ?*f32, actualY: ?*f32, len: ?*c_int) callconv(.C) [*c]SpriteCollisionInfo, // access results using const info = &results[i];
1193
+
moveWithCollisions: *const fn (sprite: ?*LCDSprite, goalX: f32, goalY: f32, actualX: ?*f32, actualY: ?*f32, len: ?*c_int) callconv(.C) [*c]SpriteCollisionInfo,
1194
+
querySpritesAtPoint: *const fn (x: f32, y: f32, len: ?*c_int) callconv(.C) [*c]?*LCDSprite,
1195
+
querySpritesInRect: *const fn (x: f32, y: f32, width: f32, height: f32, len: ?*c_int) callconv(.C) [*c]?*LCDSprite,
1196
+
querySpritesAlongLine: *const fn (x1: f32, y1: f32, x2: f32, y2: f32, len: ?*c_int) callconv(.C) [*c]?*LCDSprite,
1197
+
querySpriteInfoAlongLine: *const fn (x1: f32, y1: f32, x2: f32, y2: f32, len: ?*c_int) callconv(.C) [*c]SpriteQueryInfo, // access results using const info = &results[i];
1198
+
overlappingSprites: *const fn (sprite: ?*LCDSprite, len: ?*c_int) callconv(.C) [*c]?*LCDSprite,
1199
+
allOverlappingSprites: *const fn (len: ?*c_int) callconv(.C) [*c]?*LCDSprite,
1200
+
1201
+
// added in 1.7
1202
+
setStencilPattern: *const fn (sprite: ?*LCDSprite, pattern: [*c]u8) callconv(.C) void, //pattern is 8 bytes
1203
+
clearStencil: *const fn (sprite: ?*LCDSprite) callconv(.C) void,
1204
+
1205
+
setUserdata: *const fn (sprite: ?*LCDSprite, userdata: ?*anyopaque) callconv(.C) void,
1206
+
getUserdata: *const fn (sprite: ?*LCDSprite) callconv(.C) ?*anyopaque,
1207
+
1208
+
// added in 1.10
1209
+
setStencilImage: *const fn (sprite: ?*LCDSprite, stencil: ?*LCDBitmap, tile: c_int) callconv(.C) void,
1210
+
1211
+
// 2.1
1212
+
setCenter: *const fn (s: ?*LCDSprite, x: f32, y: f32) callconv(.C) void,
1213
+
getCenter: *const fn (s: ?*LCDSprite, x: ?*f32, y: ?*f32) callconv(.C) void,
1214
+
1215
+
// 2.7
1216
+
setTilemap: *const fn (s: ?*LCDSprite, tilemap: ?*LCDTileMap) callconv(.C) void,
1217
+
getTilemap: *const fn (s: ?*LCDSprite) callconv(.C) ?*LCDTileMap,
1218
+
};
1219
+
1220
+
////////Lua///////
1221
+
pub const LuaState = ?*anyopaque;
1222
+
pub const LuaCFunction = ?*const fn (state: ?*LuaState) callconv(.C) c_int;
1223
+
pub const LuaUDObject = opaque {};
1224
+
1225
+
//literal value
1226
+
pub const LValType = enum(c_int) {
1227
+
Int = 0,
1228
+
Float = 1,
1229
+
Str = 2,
1230
+
};
1231
+
pub const LuaReg = extern struct {
1232
+
name: ?[*:0]const u8,
1233
+
func: LuaCFunction,
1234
+
};
1235
+
pub const LuaType = enum(c_int) {
1236
+
TypeNil = 0,
1237
+
TypeBool = 1,
1238
+
TypeInt = 2,
1239
+
TypeFloat = 3,
1240
+
TypeString = 4,
1241
+
TypeTable = 5,
1242
+
TypeFunction = 6,
1243
+
TypeThread = 7,
1244
+
TypeObject = 8,
1245
+
};
1246
+
pub const LuaVal = extern struct {
1247
+
name: ?[*:0]const u8,
1248
+
type: LValType,
1249
+
v: extern union {
1250
+
intval: c_uint,
1251
+
floatval: f32,
1252
+
strval: ?[*:0]const u8,
1253
+
},
1254
+
};
1255
+
pub const PlaydateLua = extern struct {
1256
+
// these two return 1 on success, else 0 with an error message in outErr
1257
+
addFunction: *const fn (f: LuaCFunction, name: ?[*:0]const u8, outErr: ?*?[*:0]const u8) callconv(.C) c_int,
1258
+
registerClass: *const fn (name: ?[*:0]const u8, reg: ?*const LuaReg, vals: [*c]const LuaVal, isstatic: c_int, outErr: ?*?[*:0]const u8) callconv(.C) c_int,
1259
+
1260
+
pushFunction: *const fn (f: LuaCFunction) callconv(.C) void,
1261
+
indexMetatable: *const fn () callconv(.C) c_int,
1262
+
1263
+
stop: *const fn () callconv(.C) void,
1264
+
start: *const fn () callconv(.C) void,
1265
+
1266
+
// stack operations
1267
+
getArgCount: *const fn () callconv(.C) c_int,
1268
+
getArgType: *const fn (pos: c_int, outClass: ?*?[*:0]const u8) callconv(.C) LuaType,
1269
+
1270
+
argIsNil: *const fn (pos: c_int) callconv(.C) c_int,
1271
+
getArgBool: *const fn (pos: c_int) callconv(.C) c_int,
1272
+
getArgInt: *const fn (pos: c_int) callconv(.C) c_int,
1273
+
getArgFloat: *const fn (pos: c_int) callconv(.C) f32,
1274
+
getArgString: *const fn (pos: c_int) callconv(.C) ?[*:0]const u8,
1275
+
getArgBytes: *const fn (pos: c_int, outlen: ?*usize) callconv(.C) [*c]const u8,
1276
+
getArgObject: *const fn (pos: c_int, type: ?*i8, ?*?*LuaUDObject) callconv(.C) ?*anyopaque,
1277
+
1278
+
getBitmap: *const fn (c_int) callconv(.C) ?*LCDBitmap,
1279
+
getSprite: *const fn (c_int) callconv(.C) ?*LCDSprite,
1280
+
1281
+
// for returning values back to Lua
1282
+
pushNil: *const fn () callconv(.C) void,
1283
+
pushBool: *const fn (val: c_int) callconv(.C) void,
1284
+
pushInt: *const fn (val: c_int) callconv(.C) void,
1285
+
pushFloat: *const fn (val: f32) callconv(.C) void,
1286
+
pushString: *const fn (str: ?[*:0]const u8) callconv(.C) void,
1287
+
pushBytes: *const fn (str: [*c]const u8, len: usize) callconv(.C) void,
1288
+
pushBitmap: *const fn (bitmap: ?*LCDBitmap) callconv(.C) void,
1289
+
pushSprite: *const fn (sprite: ?*LCDSprite) callconv(.C) void,
1290
+
1291
+
pushObject: *const fn (obj: ?*anyopaque, type: ?*i8, nValues: c_int) callconv(.C) ?*LuaUDObject,
1292
+
retainObject: *const fn (obj: ?*LuaUDObject) callconv(.C) ?*LuaUDObject,
1293
+
releaseObject: *const fn (obj: ?*LuaUDObject) callconv(.C) void,
1294
+
1295
+
setObjectValue: *const fn (obj: ?*LuaUDObject, slot: c_int) callconv(.C) void,
1296
+
getObjectValue: *const fn (obj: ?*LuaUDObject, slot: c_int) callconv(.C) c_int,
1297
+
1298
+
// calling lua from C has some overhead. use sparingly!
1299
+
callFunction_deprecated: *const fn (name: ?[*:0]const u8, nargs: c_int) callconv(.C) void,
1300
+
callFunction: *const fn (name: ?[*:0]const u8, nargs: c_int, outerr: ?*?[*:0]const u8) callconv(.C) c_int,
1301
+
};
1302
+
1303
+
///////JSON///////
1304
+
pub const JSONValueType = enum(c_int) {
1305
+
JSONNull = 0,
1306
+
JSONTrue = 1,
1307
+
JSONFalse = 2,
1308
+
JSONInteger = 3,
1309
+
JSONFloat = 4,
1310
+
JSONString = 5,
1311
+
JSONArray = 6,
1312
+
JSONTable = 7,
1313
+
};
1314
+
pub const JSONValue = extern struct {
1315
+
type: u8,
1316
+
data: extern union {
1317
+
intval: c_int,
1318
+
floatval: f32,
1319
+
stringval: [*c]u8,
1320
+
arrayval: ?*anyopaque,
1321
+
tableval: ?*anyopaque,
1322
+
},
1323
+
};
1324
+
pub inline fn json_intValue(value: JSONValue) c_int {
1325
+
switch (@intFromEnum(value.type)) {
1326
+
.JSONInteger => return value.data.intval,
1327
+
.JSONFloat => return @intFromFloat(value.data.floatval),
1328
+
.JSONString => return std.fmt.parseInt(c_int, std.mem.span(value.data.stringval), 10) catch 0,
1329
+
.JSONTrue => return 1,
1330
+
else => return 0,
1331
+
}
1332
+
}
1333
+
pub inline fn json_floatValue(value: JSONValue) f32 {
1334
+
switch (@as(JSONValueType, @enumFromInt(value.type))) {
1335
+
.JSONInteger => return @floatFromInt(value.data.intval),
1336
+
.JSONFloat => return value.data.floatval,
1337
+
.JSONString => return 0,
1338
+
.JSONTrue => 1.0,
1339
+
else => return 0.0,
1340
+
}
1341
+
}
1342
+
pub inline fn json_boolValue(value: JSONValue) c_int {
1343
+
return if (@as(JSONValueType, @enumFromInt(value.type)) == .JSONString)
1344
+
@intFromBool(value.data.stringval[0] != 0)
1345
+
else
1346
+
json_intValue(value);
1347
+
}
1348
+
pub inline fn json_stringValue(value: JSONValue) [*c]u8 {
1349
+
return if (@as(JSONValueType, @enumFromInt(value.type)) == .JSONString)
1350
+
value.data.stringval
1351
+
else
1352
+
null;
1353
+
}
1354
+
1355
+
// decoder
1356
+
1357
+
pub const JSONDecoder = extern struct {
1358
+
decodeError: *const fn (decoder: ?*JSONDecoder, @"error": ?[*:0]const u8, linenum: c_int) callconv(.C) void,
1359
+
1360
+
// the following functions are each optional
1361
+
willDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, type: JSONValueType) callconv(.C) void,
1362
+
shouldDecodeTableValueForKey: ?*const fn (decoder: ?*JSONDecoder, key: ?[*:0]const u8) callconv(.C) c_int,
1363
+
didDecodeTableValue: ?*const fn (decoder: ?*JSONDecoder, key: ?[*:0]const u8, value: JSONValue) callconv(.C) void,
1364
+
shouldDecodeArrayValueAtIndex: ?*const fn (decoder: ?*JSONDecoder, pos: c_int) callconv(.C) c_int,
1365
+
didDecodeArrayValue: ?*const fn (decoder: ?*JSONDecoder, pos: c_int, value: JSONValue) callconv(.C) void,
1366
+
didDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, type: JSONValueType) callconv(.C) ?*anyopaque,
1367
+
1368
+
userdata: ?*anyopaque,
1369
+
returnString: c_int, // when set, the decoder skips parsing and returns the current subtree as a string
1370
+
path: ?[*:0]const u8, // updated during parsing, reflects current position in tree
1371
+
};
1372
+
1373
+
// convenience functions for setting up a table-only or array-only decoder
1374
+
1375
+
pub inline fn json_setTableDecode(
1376
+
decoder: ?*JSONDecoder,
1377
+
willDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, type: JSONValueType) callconv(.C) void,
1378
+
didDecodeTableValue: ?*const fn (decoder: ?*JSONDecoder, key: ?[*:0]const u8, value: JSONValue) callconv(.C) void,
1379
+
didDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, name: JSONValueType) callconv(.C) ?*anyopaque,
1380
+
) void {
1381
+
decoder.?.didDecodeTableValue = didDecodeTableValue;
1382
+
decoder.?.didDecodeArrayValue = null;
1383
+
decoder.?.willDecodeSublist = willDecodeSublist;
1384
+
decoder.?.didDecodeSublist = didDecodeSublist;
1385
+
}
1386
+
1387
+
pub inline fn json_setArrayDecode(
1388
+
decoder: ?*JSONDecoder,
1389
+
willDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, type: JSONValueType) callconv(.C) void,
1390
+
didDecodeArrayValue: ?*const fn (decoder: ?*JSONDecoder, pos: c_int, value: JSONValue) callconv(.C) void,
1391
+
didDecodeSublist: ?*const fn (decoder: ?*JSONDecoder, name: ?[*:0]const u8, type: JSONValueType) callconv(.C) ?*anyopaque,
1392
+
) void {
1393
+
decoder.?.didDecodeTableValue = null;
1394
+
decoder.?.didDecodeArrayValue = didDecodeArrayValue;
1395
+
decoder.?.willDecodeSublist = willDecodeSublist;
1396
+
decoder.?.didDecodeSublist = didDecodeSublist;
1397
+
}
1398
+
1399
+
pub const JSONReader = extern struct {
1400
+
read: *const fn (userdata: ?*anyopaque, buf: [*c]u8, bufsize: c_int) callconv(.C) c_int,
1401
+
userdata: ?*anyopaque,
1402
+
};
1403
+
pub const writeFunc = *const fn (userdata: ?*anyopaque, str: [*c]const u8, len: c_int) callconv(.C) void;
1404
+
1405
+
pub const JSONEncoder = extern struct {
1406
+
writeStringFunc: writeFunc,
1407
+
userdata: ?*anyopaque,
1408
+
1409
+
state: u32, //this is pretty, startedTable, startedArray and depth bitfields combined
1410
+
1411
+
startArray: *const fn (encoder: ?*JSONEncoder) callconv(.C) void,
1412
+
addArrayMember: *const fn (encoder: ?*JSONEncoder) callconv(.C) void,
1413
+
endArray: *const fn (encoder: ?*JSONEncoder) callconv(.C) void,
1414
+
startTable: *const fn (encoder: ?*JSONEncoder) callconv(.C) void,
1415
+
addTableMember: *const fn (encoder: ?*JSONEncoder, name: [*c]const u8, len: c_int) callconv(.C) void,
1416
+
endTable: *const fn (encoder: ?*JSONEncoder) callconv(.C) void,
1417
+
writeNull: *const fn (encoder: ?*JSONEncoder) callconv(.C) void,
1418
+
writeFalse: *const fn (encoder: ?*JSONEncoder) callconv(.C) void,
1419
+
writeTrue: *const fn (encoder: ?*JSONEncoder) callconv(.C) void,
1420
+
writeInt: *const fn (encoder: ?*JSONEncoder, num: c_int) callconv(.C) void,
1421
+
writeDouble: *const fn (encoder: ?*JSONEncoder, num: f64) callconv(.C) void,
1422
+
writeString: *const fn (encoder: ?*JSONEncoder, str: [*c]const u8, len: c_int) callconv(.C) void,
1423
+
};
1424
+
1425
+
pub const PlaydateJSON = extern struct {
1426
+
initEncoder: *const fn (encoder: ?*JSONEncoder, write: writeFunc, userdata: ?*anyopaque, pretty: c_int) callconv(.C) void,
1427
+
1428
+
decode: *const fn (functions: ?*JSONDecoder, reader: JSONReader, outval: ?*JSONValue) callconv(.C) c_int,
1429
+
decodeString: *const fn (functions: ?*JSONDecoder, jsonString: ?[*:0]const u8, outval: ?*JSONValue) callconv(.C) c_int,
1430
+
};
1431
+
1432
+
///////Scoreboards///////////
1433
+
pub const PDScore = extern struct {
1434
+
rank: u32,
1435
+
value: u32,
1436
+
player: [*c]u8,
1437
+
};
1438
+
pub const PDScoresList = extern struct {
1439
+
boardID: [*c]u8,
1440
+
count: c_uint,
1441
+
lastUpdated: u32,
1442
+
playerIncluded: c_int,
1443
+
limit: c_uint,
1444
+
scores: [*c]PDScore,
1445
+
};
1446
+
pub const PDBoard = extern struct {
1447
+
boardID: [*c]u8,
1448
+
name: [*c]u8,
1449
+
};
1450
+
pub const PDBoardsList = extern struct {
1451
+
count: c_uint,
1452
+
lastUpdated: u32,
1453
+
boards: [*c]PDBoard,
1454
+
};
1455
+
pub const AddScoreCallback = ?*const fn (score: ?*PDScore, errorMessage: ?[*:0]const u8) callconv(.C) void;
1456
+
pub const PersonalBestCallback = ?*const fn (score: ?*PDScore, errorMessage: ?[*:0]const u8) callconv(.C) void;
1457
+
pub const BoardsListCallback = ?*const fn (boards: ?*PDBoardsList, errorMessage: ?[*:0]const u8) callconv(.C) void;
1458
+
pub const ScoresCallback = ?*const fn (scores: ?*PDScoresList, errorMessage: ?[*:0]const u8) callconv(.C) void;
1459
+
1460
+
pub const PlaydateScoreboards = extern struct {
1461
+
addScore: *const fn (boardId: ?[*:0]const u8, value: u32, callback: AddScoreCallback) callconv(.C) c_int,
1462
+
getPersonalBest: *const fn (boardId: ?[*:0]const u8, callback: PersonalBestCallback) callconv(.C) c_int,
1463
+
freeScore: *const fn (score: ?*PDScore) callconv(.C) void,
1464
+
1465
+
getScoreboards: *const fn (callback: BoardsListCallback) callconv(.C) c_int,
1466
+
freeBoardsList: *const fn (boards: ?*PDBoardsList) callconv(.C) void,
1467
+
1468
+
getScores: *const fn (boardId: ?[*:0]const u8, callback: ScoresCallback) callconv(.C) c_int,
1469
+
freeScoresList: *const fn (scores: ?*PDScoresList) callconv(.C) void,
1470
+
};
1471
+
1472
+
///////Network///////////
1473
+
pub const HTTPConnection = opaque {};
1474
+
pub const TCPConnection = opaque {};
1475
+
1476
+
pub const PDNetErr = enum(c_int) {
1477
+
NET_OK = 0,
1478
+
NET_NO_DEVICE = -1,
1479
+
NET_BUSY = -2,
1480
+
NET_WRITE_ERROR = -3,
1481
+
NET_WRITE_BUSY = -4,
1482
+
NET_WRITE_TIMEOUT = -5,
1483
+
NET_READ_ERROR = -6,
1484
+
NET_READ_BUSY = -7,
1485
+
NET_READ_TIMEOUT = -8,
1486
+
NET_READ_OVERFLOW = -9,
1487
+
NET_FRAME_ERROR = -10,
1488
+
NET_BAD_RESPONSE = -11,
1489
+
NET_ERROR_RESPONSE = -12,
1490
+
NET_RESET_TIMEOUT = -13,
1491
+
NET_BUFFER_TOO_SMALL = -14,
1492
+
NET_UNEXPECTED_RESPONSE = -15,
1493
+
NET_NOT_CONNECTED_TO_AP = -16,
1494
+
NET_NOT_IMPLEMENTED = -17,
1495
+
NET_CONNECTION_CLOSED = -18,
1496
+
};
1497
+
1498
+
pub const WifiStatus = enum(c_int) {
1499
+
WifiNotConnected = 0, //< Not connected to an AP
1500
+
WifiConnected, //< Device is connected to an AP
1501
+
WifiNotAvailable, //< A connection has been attempted and no configured AP was available
1502
+
};
1503
+
1504
+
pub const HTTPConnectionCallback = ?*const fn (connection: ?*HTTPConnection) callconv(.C) void;
1505
+
pub const HTTPHeaderCallback = ?*const fn (conn: ?*HTTPConnection, key: ?[*:0]const u8, value: ?[*:0]const u8) callconv(.C) void;
1506
+
1507
+
pub const PlaydateHTTP = extern struct {
1508
+
requestAccess: *const fn (server: ?[*:0]const u8, port: c_int, usessl: bool, purpose: ?[*:0]const u8, requestCallback: AccessRequestCallback, userdata: ?*anyopaque) callconv(.C) AccessReply,
1509
+
1510
+
newConnection: *const fn (server: ?[*:0]const u8, port: c_int, usessl: bool) callconv(.C) ?*HTTPConnection,
1511
+
retain: *const fn (http: ?*HTTPConnection) callconv(.C) ?*HTTPConnection,
1512
+
release: *const fn (http: ?*HTTPConnection) callconv(.C) void,
1513
+
1514
+
setConnectTimeout: *const fn (connection: ?*HTTPConnection, ms: c_int) callconv(.C) void,
1515
+
setKeepAlive: *const fn (connection: ?*HTTPConnection, keepalive: bool) callconv(.C) void,
1516
+
setByteRange: *const fn (connection: ?*HTTPConnection, start: c_int, end: c_int) callconv(.C) void,
1517
+
setUserdata: *const fn (connection: ?*HTTPConnection, userdata: ?*anyopaque) callconv(.C) void,
1518
+
getUserdata: *const fn (connection: ?*HTTPConnection) callconv(.C) ?*anyopaque,
1519
+
1520
+
get: *const fn (connection: ?*HTTPConnection, path: ?[*:0]const u8, headers: ?[*:0]const u8, headerlen: usize) callconv(.C) PDNetErr,
1521
+
post: *const fn (connection: ?*HTTPConnection, path: ?[*:0]const u8, headers: ?[*:0]const u8, headerlen: usize, body: ?[*:0]const u8, bodylen: usize) callconv(.C) PDNetErr,
1522
+
query: *const fn (connection: ?*HTTPConnection, method: ?[*:0]const u8, path: ?[*:0]const u8, headers: ?[*:0]const u8, headerlen: usize, body: ?[*:0]const u8, bodylen: usize) callconv(.C) PDNetErr,
1523
+
getError: *const fn (connection: ?*HTTPConnection) callconv(.C) PDNetErr,
1524
+
getProgress: *const fn (connection: ?*HTTPConnection, read: ?*c_int, total: ?*c_int) callconv(.C) void,
1525
+
getResponseStatus: *const fn (connection: ?*HTTPConnection) callconv(.C) c_int,
1526
+
getBytesAvailable: *const fn (connection: ?*HTTPConnection) callconv(.C) usize,
1527
+
setReadTimeout: *const fn (connection: ?*HTTPConnection, ms: c_int) callconv(.C) void,
1528
+
setReadBufferSize: *const fn (connection: ?*HTTPConnection, bytes: c_int) callconv(.C) void,
1529
+
read: *const fn (connection: ?*HTTPConnection, buf: [*c]u8, buflen: c_uint) callconv(.C) c_int,
1530
+
close: *const fn (connection: ?*HTTPConnection) callconv(.C) void,
1531
+
1532
+
setHeaderReceivedCallback: *const fn (connection: ?*HTTPConnection, headercb: HTTPHeaderCallback) callconv(.C) void,
1533
+
setHeadersReadCallback: *const fn (connection: ?*HTTPConnection, callback: HTTPConnectionCallback) callconv(.C) void,
1534
+
setResponseCallback: *const fn (connection: ?*HTTPConnection, callback: HTTPConnectionCallback) callconv(.C) void,
1535
+
setRequestCompleteCallback: *const fn (connection: ?*HTTPConnection, callback: HTTPConnectionCallback) callconv(.C) void,
1536
+
setConnectionClosedCallback: *const fn (connection: ?*HTTPConnection, callback: HTTPConnectionCallback) callconv(.C) void,
1537
+
};
1538
+
1539
+
pub const TCPConnectionCallback = ?*const fn (connection: ?*TCPConnection, err: PDNetErr) callconv(.C) void;
1540
+
pub const TCPOpenCallback = ?*const fn (connection: ?*TCPConnection, err: PDNetErr, ud: ?*anyopaque) callconv(.C) void;
1541
+
1542
+
pub const PlaydateTCP = extern struct {
1543
+
requestAccess: *const fn (server: ?[*:0]const u8, port: c_int, usessl: bool, purpose: ?[*:0]const u8, requestCallback: AccessRequestCallback, userdata: ?*anyopaque) callconv(.C) AccessReply,
1544
+
1545
+
newConnection: *const fn (server: ?[*:0]const u8, port: c_int, usessl: bool) callconv(.C) ?*TCPConnection,
1546
+
retain: *const fn (tcp: ?*TCPConnection) callconv(.C) ?*TCPConnection,
1547
+
release: *const fn (tcp: ?*TCPConnection) callconv(.C) void,
1548
+
getError: *const fn (connection: ?*TCPConnection) callconv(.C) PDNetErr,
1549
+
1550
+
setConnectTimeout: *const fn (connection: ?*TCPConnection, ms: c_int) callconv(.C) void,
1551
+
setUserdata: *const fn (connection: ?*TCPConnection, userdata: ?*anyopaque) callconv(.C) void,
1552
+
getUserdata: *const fn (connection: ?*TCPConnection) callconv(.C) ?*anyopaque,
1553
+
1554
+
open: *const fn (connection: ?*TCPConnection, cb: TCPOpenCallback, ud: ?*anyopaque) callconv(.C) PDNetErr,
1555
+
close: *const fn (connection: ?*TCPConnection) callconv(.C) PDNetErr,
1556
+
1557
+
setConnectionClosedCallback: *const fn (connection: ?*TCPConnection, callback: TCPConnectionCallback) callconv(.C) void,
1558
+
1559
+
setReadTimeout: *const fn (connection: ?*TCPConnection, ms: c_int) callconv(.C) void,
1560
+
setReadBufferSize: *const fn (connection: ?*TCPConnection, bytes: c_int) callconv(.C) void,
1561
+
getBytesAvailable: *const fn (connection: ?*TCPConnection) callconv(.C) usize,
1562
+
1563
+
read: *const fn (connection: ?*TCPConnection, buffer: [*c]u8, length: usize) callconv(.C) c_int, // returns # of bytes read, or PDNetErr on error
1564
+
write: *const fn (connection: ?*TCPConnection, buffer: [*c]const u8, length: usize) callconv(.C) c_int, // returns # of bytes read, or PDNetErr on error
1565
+
};
1566
+
1567
+
pub const PlaydateNetwork = extern struct {
1568
+
playdate_http: *const PlaydateHTTP,
1569
+
playdate_tcp: *const PlaydateTCP,
1570
+
1571
+
getStatus: *const fn () callconv(.C) WifiStatus,
1572
+
setEnabled: *const fn (flag: bool, callback: ?*const fn (err: PDNetErr) callconv(.C) void) callconv(.C) void,
1573
+
1574
+
reserved: [3]usize,
1575
+
};