janela 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -26
- package/bin/janela.mjs +382 -46
- package/package.json +3 -3
- package/runtime/janela.ts +289 -122
- package/shim/wvshim.cc +726 -67
- package/templates/index.html +31 -0
- package/templates/main.ts +62 -19
- package/templates/react/deps.json +4 -0
- package/templates/react/files/index.html +11 -0
- package/templates/react/files/janela.conf.json +10 -0
- package/templates/react/files/src/App.css +3 -0
- package/templates/react/files/src/App.jsx +38 -0
- package/templates/react/files/src/main.jsx +9 -0
- package/templates/react/files/src-host/main.ts +42 -0
- package/templates/react/files/vite.config.js +6 -0
- package/templates/solid/deps.json +4 -0
- package/templates/solid/files/index.html +11 -0
- package/templates/solid/files/janela.conf.json +10 -0
- package/templates/solid/files/src/App.css +3 -0
- package/templates/solid/files/src/App.jsx +35 -0
- package/templates/solid/files/src/main.jsx +4 -0
- package/templates/solid/files/src-host/main.ts +42 -0
- package/templates/solid/files/vite.config.js +6 -0
- package/templates/svelte/deps.json +7 -0
- package/templates/svelte/files/index.html +11 -0
- package/templates/svelte/files/janela.conf.json +10 -0
- package/templates/svelte/files/src/App.svelte +33 -0
- package/templates/svelte/files/src/main.js +4 -0
- package/templates/svelte/files/src-host/main.ts +42 -0
- package/templates/svelte/files/svelte.config.js +3 -0
- package/templates/svelte/files/vite.config.js +6 -0
- package/templates/vue/deps.json +4 -0
- package/templates/vue/files/index.html +11 -0
- package/templates/vue/files/janela.conf.json +10 -0
- package/templates/vue/files/src/App.vue +39 -0
- package/templates/vue/files/src/main.js +4 -0
- package/templates/vue/files/src-host/main.ts +42 -0
- package/templates/vue/files/vite.config.js +6 -0
package/shim/wvshim.cc
CHANGED
|
@@ -1,26 +1,42 @@
|
|
|
1
|
-
// wvshim.cc — a C-ABI shim over webview.h shaped to scriptc's FFI format
|
|
1
|
+
// wvshim.cc — a C-ABI shim over webview.h shaped to scriptc's FFI format 4.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
3
|
+
// One constraint still drives the design: scriptc has no pointer/u64 type, so
|
|
4
|
+
// webview_t can never cross the boundary. We keep a handle table and hand out
|
|
5
|
+
// int32 indices.
|
|
6
|
+
//
|
|
7
|
+
// What the newer formats removed:
|
|
8
|
+
// * format 3 — callback params may be `string`/`bytes`, so a payload crosses
|
|
9
|
+
// into TS as one argument instead of one FFI call per byte. Payloads going
|
|
10
|
+
// the other way ride `string` params on ordinary functions.
|
|
11
|
+
// * format 4 — callbacks may be `retained`, so the invoke and tick handlers
|
|
12
|
+
// are registered once and live for the app's lifetime. wv_run() is a plain
|
|
13
|
+
// blocking call again; it no longer has to carry a callback whose "call
|
|
14
|
+
// scope" was standing in for "app lifetime".
|
|
13
15
|
|
|
14
16
|
#include "webview.h"
|
|
15
17
|
|
|
16
18
|
#include <atomic>
|
|
17
19
|
#include <chrono>
|
|
18
20
|
#include <cstdint>
|
|
21
|
+
#include <cstdio>
|
|
19
22
|
#include <cstring>
|
|
23
|
+
#include <filesystem>
|
|
24
|
+
#include <fstream>
|
|
25
|
+
#include <memory>
|
|
26
|
+
#include <mutex>
|
|
20
27
|
#include <string>
|
|
21
28
|
#include <thread>
|
|
22
29
|
#include <vector>
|
|
23
30
|
|
|
31
|
+
// Native dialogs need each platform's own toolkit. webview.h has already
|
|
32
|
+
// pulled in the Cocoa bindings (via its own backend headers) and windows.h on
|
|
33
|
+
// Win32; these add only what it does not use itself.
|
|
34
|
+
#if defined(_WIN32)
|
|
35
|
+
#include <commdlg.h>
|
|
36
|
+
#elif !defined(__APPLE__)
|
|
37
|
+
#include <gtk/gtk.h>
|
|
38
|
+
#endif
|
|
39
|
+
|
|
24
40
|
namespace {
|
|
25
41
|
|
|
26
42
|
struct Bind {
|
|
@@ -39,14 +55,17 @@ struct App {
|
|
|
39
55
|
bool used = false;
|
|
40
56
|
std::vector<Bind> binds;
|
|
41
57
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
void *
|
|
58
|
+
// Retained TS handlers, registered once and valid until the app exits. The
|
|
59
|
+
// request rides in as a (ptr, len) string param.
|
|
60
|
+
int32_t (*on_invoke)(const uint8_t *, size_t, void *) = nullptr;
|
|
61
|
+
void *on_invoke_ctx = nullptr;
|
|
62
|
+
void (*on_tick)(void *) = nullptr;
|
|
63
|
+
void *on_tick_ctx = nullptr;
|
|
45
64
|
|
|
46
65
|
// Staging for the in-flight request.
|
|
47
66
|
std::string req; // JSON args array from JS
|
|
48
67
|
std::string cur_id; // webview's call id, needed by webview_return
|
|
49
|
-
std::string reply; // response body
|
|
68
|
+
std::string reply; // response body handed over by TS in one call
|
|
50
69
|
uint32_t seq = 0;
|
|
51
70
|
|
|
52
71
|
// ---- async support ----
|
|
@@ -57,10 +76,6 @@ struct App {
|
|
|
57
76
|
std::atomic<int32_t> tick_ms{16};
|
|
58
77
|
};
|
|
59
78
|
|
|
60
|
-
// Bind index handed to the TS callback for a timer tick rather than an
|
|
61
|
-
// invoke. Real bind indices are small sequential integers.
|
|
62
|
-
const uint32_t TICK_BIND = 0xffffffffu;
|
|
63
|
-
|
|
64
79
|
// Fixed-size table: handles are indices, never pointers.
|
|
65
80
|
App g_apps[8];
|
|
66
81
|
|
|
@@ -74,14 +89,506 @@ std::string to_str(const uint8_t *p, size_t n) {
|
|
|
74
89
|
return std::string(reinterpret_cast<const char *>(p), n);
|
|
75
90
|
}
|
|
76
91
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
92
|
+
// ---- jobs ------------------------------------------------------------------
|
|
93
|
+
//
|
|
94
|
+
// A job is any unit of work whose answer cannot be produced during the FFI
|
|
95
|
+
// call that asks for it. TS starts one, gets an id back immediately, and polls
|
|
96
|
+
// wv_job_status() from its tick loop until the job is terminal.
|
|
97
|
+
//
|
|
98
|
+
// Two kinds use this pool, for opposite reasons:
|
|
99
|
+
// * file I/O — the blocking syscall must happen off the UI thread, so a
|
|
100
|
+
// worker thread does it. A worker touches only its own job and NEVER calls
|
|
101
|
+
// into TS: scriptc's runtime is not thread-safe, so the result is drained
|
|
102
|
+
// later, on the UI thread.
|
|
103
|
+
// * native dialogs — the modal must run ON the UI thread, but not while TS
|
|
104
|
+
// is on the stack (runModal/gtk_dialog_run spin a nested event loop, which
|
|
105
|
+
// would re-enter the tick handler underneath the invoke handler that asked
|
|
106
|
+
// for the dialog). So the job is posted with webview_dispatch and runs at
|
|
107
|
+
// the top of a later turn, with no TS frame beneath it.
|
|
108
|
+
|
|
109
|
+
const int32_t JOB_PENDING = 0;
|
|
110
|
+
const int32_t JOB_OK = 1;
|
|
111
|
+
const int32_t JOB_ERROR = 2;
|
|
112
|
+
|
|
113
|
+
struct Job {
|
|
114
|
+
// Written by the producer (worker thread, or the dispatched dialog) before
|
|
115
|
+
// `status` flips; read by the UI thread only after it observes a terminal
|
|
116
|
+
// status. The release/acquire pair on `status` is what publishes `data`, so
|
|
117
|
+
// no lock is needed for the payload itself.
|
|
118
|
+
std::atomic<int32_t> status{JOB_PENDING};
|
|
119
|
+
std::string data; // payload on success, the error message on failure
|
|
120
|
+
std::thread worker; // unused by dialog jobs, which run on the UI thread
|
|
121
|
+
bool used = false;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Jobs are addressed by index and held behind unique_ptr so the vector may
|
|
125
|
+
// grow without invalidating a worker's pointer to its own job.
|
|
126
|
+
std::mutex g_jobs_mu;
|
|
127
|
+
std::vector<std::unique_ptr<Job>> g_jobs;
|
|
128
|
+
|
|
129
|
+
Job *job_at(int32_t id) {
|
|
130
|
+
std::lock_guard<std::mutex> lock(g_jobs_mu);
|
|
131
|
+
if (id < 0 || static_cast<size_t>(id) >= g_jobs.size()) return nullptr;
|
|
132
|
+
Job *j = g_jobs[id].get();
|
|
133
|
+
return j->used ? j : nullptr;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Reuses a finished slot when one is free, so a long-running app that reads
|
|
137
|
+
// many files does not grow the table without bound.
|
|
138
|
+
int32_t new_job() {
|
|
139
|
+
std::lock_guard<std::mutex> lock(g_jobs_mu);
|
|
140
|
+
for (size_t i = 0; i < g_jobs.size(); i++) {
|
|
141
|
+
if (g_jobs[i]->used) continue;
|
|
142
|
+
if (g_jobs[i]->worker.joinable()) g_jobs[i]->worker.join();
|
|
143
|
+
g_jobs[i]->status.store(JOB_PENDING);
|
|
144
|
+
g_jobs[i]->data.clear();
|
|
145
|
+
g_jobs[i]->used = true;
|
|
146
|
+
return static_cast<int32_t>(i);
|
|
147
|
+
}
|
|
148
|
+
g_jobs.push_back(std::unique_ptr<Job>(new Job()));
|
|
149
|
+
g_jobs.back()->used = true;
|
|
150
|
+
return static_cast<int32_t>(g_jobs.size() - 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Node-shaped messages: janela apps already surface node:fs errors from
|
|
154
|
+
// synchronous handlers, so the async path should read the same.
|
|
155
|
+
std::string fs_error_message(const std::string &path, const char *op) {
|
|
156
|
+
std::error_code ec;
|
|
157
|
+
auto st = std::filesystem::status(path, ec);
|
|
158
|
+
if (st.type() == std::filesystem::file_type::not_found) {
|
|
159
|
+
return "ENOENT: no such file or directory, " + std::string(op) + " '" +
|
|
160
|
+
path + "'";
|
|
161
|
+
}
|
|
162
|
+
if (st.type() == std::filesystem::file_type::directory) {
|
|
163
|
+
return "EISDIR: illegal operation on a directory, " + std::string(op) +
|
|
164
|
+
" '" + path + "'";
|
|
165
|
+
}
|
|
166
|
+
return "EIO: failed to " + std::string(op) + " '" + path + "'";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
void job_finish(Job *j, int32_t status, std::string payload) {
|
|
170
|
+
j->data = std::move(payload);
|
|
171
|
+
j->status.store(status, std::memory_order_release);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
void fs_read_worker(Job *j, std::string path) {
|
|
175
|
+
std::error_code ec;
|
|
176
|
+
if (std::filesystem::is_directory(path, ec)) {
|
|
177
|
+
job_finish(j, JOB_ERROR, fs_error_message(path, "read"));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
std::ifstream in(path, std::ios::binary);
|
|
181
|
+
if (!in) {
|
|
182
|
+
job_finish(j, JOB_ERROR, fs_error_message(path, "open"));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
std::string buf((std::istreambuf_iterator<char>(in)),
|
|
186
|
+
std::istreambuf_iterator<char>());
|
|
187
|
+
if (in.bad()) {
|
|
188
|
+
job_finish(j, JOB_ERROR, fs_error_message(path, "read"));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
job_finish(j, JOB_OK, std::move(buf));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
void fs_write_worker(Job *j, std::string path, std::string data) {
|
|
195
|
+
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
|
196
|
+
if (!out) {
|
|
197
|
+
job_finish(j, JOB_ERROR, fs_error_message(path, "open"));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
out.write(data.data(), static_cast<std::streamsize>(data.size()));
|
|
201
|
+
out.flush();
|
|
202
|
+
if (!out) {
|
|
203
|
+
job_finish(j, JOB_ERROR, fs_error_message(path, "write"));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
job_finish(j, JOB_OK, std::string());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Join every worker. Called at shutdown so no thread outlives the process's
|
|
210
|
+
// orderly exit (and so nothing writes into a job after main returns).
|
|
211
|
+
void jobs_join_all() {
|
|
212
|
+
std::lock_guard<std::mutex> lock(g_jobs_mu);
|
|
213
|
+
for (size_t i = 0; i < g_jobs.size(); i++) {
|
|
214
|
+
if (g_jobs[i]->worker.joinable()) g_jobs[i]->worker.join();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---- native file dialogs ----------------------------------------------------
|
|
219
|
+
//
|
|
220
|
+
// Options arrive as plain FFI params rather than JSON so the shim needs no
|
|
221
|
+
// parser. `filters` is "Name|ext,ext|Name|ext" — the separators cannot appear
|
|
222
|
+
// in an extension, and a filter name containing one is the caller's problem.
|
|
223
|
+
// The answer is a JSON array of paths, or the literal `null` for a cancel.
|
|
224
|
+
|
|
225
|
+
const int32_t DLG_OPEN = 0;
|
|
226
|
+
const int32_t DLG_SAVE = 1;
|
|
227
|
+
const int32_t DLG_MULTIPLE = 1; // flags bit 0
|
|
228
|
+
const int32_t DLG_DIRECTORY = 2; // flags bit 1
|
|
229
|
+
|
|
230
|
+
struct DialogRequest {
|
|
231
|
+
int32_t app = -1;
|
|
232
|
+
int32_t job = -1;
|
|
233
|
+
int32_t kind = DLG_OPEN;
|
|
234
|
+
int32_t flags = 0;
|
|
235
|
+
std::string title;
|
|
236
|
+
std::string default_path;
|
|
237
|
+
std::string default_name;
|
|
238
|
+
std::string filters;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
std::vector<std::string> split_on(const std::string &s, char sep) {
|
|
242
|
+
std::vector<std::string> out;
|
|
243
|
+
if (s.empty()) return out;
|
|
244
|
+
std::string cur;
|
|
245
|
+
for (size_t i = 0; i < s.size(); i++) {
|
|
246
|
+
if (s[i] == sep) {
|
|
247
|
+
out.push_back(cur);
|
|
248
|
+
cur.clear();
|
|
249
|
+
} else {
|
|
250
|
+
cur.push_back(s[i]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
out.push_back(cur);
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
struct Filter {
|
|
258
|
+
std::string name;
|
|
259
|
+
std::vector<std::string> extensions;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
std::vector<Filter> parse_filters(const std::string &spec) {
|
|
263
|
+
std::vector<Filter> out;
|
|
264
|
+
std::vector<std::string> parts = split_on(spec, '|');
|
|
265
|
+
for (size_t i = 0; i + 1 < parts.size(); i += 2) {
|
|
266
|
+
Filter f;
|
|
267
|
+
f.name = parts[i];
|
|
268
|
+
f.extensions = split_on(parts[i + 1], ',');
|
|
269
|
+
if (!f.extensions.empty()) out.push_back(f);
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Paths are UTF-8 and JSON strings are UTF-8, so only the structural
|
|
275
|
+
// characters and C0 controls need escaping.
|
|
276
|
+
std::string json_escape(const std::string &s) {
|
|
277
|
+
std::string out;
|
|
278
|
+
out.reserve(s.size() + 2);
|
|
279
|
+
out.push_back('"');
|
|
280
|
+
for (size_t i = 0; i < s.size(); i++) {
|
|
281
|
+
unsigned char c = static_cast<unsigned char>(s[i]);
|
|
282
|
+
switch (c) {
|
|
283
|
+
case '"': out += "\\\""; break;
|
|
284
|
+
case '\\': out += "\\\\"; break;
|
|
285
|
+
case '\n': out += "\\n"; break;
|
|
286
|
+
case '\r': out += "\\r"; break;
|
|
287
|
+
case '\t': out += "\\t"; break;
|
|
288
|
+
default:
|
|
289
|
+
if (c < 0x20) {
|
|
290
|
+
char buf[7];
|
|
291
|
+
std::snprintf(buf, sizeof(buf), "\\u%04x", c);
|
|
292
|
+
out += buf;
|
|
293
|
+
} else {
|
|
294
|
+
out.push_back(static_cast<char>(c));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
out.push_back('"');
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
std::string json_array(const std::vector<std::string> &items) {
|
|
303
|
+
std::string out = "[";
|
|
304
|
+
for (size_t i = 0; i < items.size(); i++) {
|
|
305
|
+
if (i) out.push_back(',');
|
|
306
|
+
out += json_escape(items[i]);
|
|
307
|
+
}
|
|
308
|
+
out.push_back(']');
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Fills `out` and returns true when the user confirmed; returns false for a
|
|
313
|
+
// cancel. `error` is set only for a platform-level refusal.
|
|
314
|
+
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
315
|
+
std::string &error);
|
|
316
|
+
|
|
317
|
+
#if defined(__APPLE__)
|
|
318
|
+
|
|
319
|
+
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
320
|
+
std::string &error) {
|
|
321
|
+
(void)error;
|
|
322
|
+
using namespace webview::detail;
|
|
323
|
+
objc::autoreleasepool arp;
|
|
324
|
+
|
|
325
|
+
bool save = req.kind == DLG_SAVE;
|
|
326
|
+
id panel = save ? objc::msg_send<id>(objc::get_class("NSSavePanel"),
|
|
327
|
+
objc::selector("savePanel"))
|
|
328
|
+
: cocoa::NSOpenPanel_openPanel();
|
|
329
|
+
if (!panel) return false;
|
|
330
|
+
|
|
331
|
+
if (!req.title.empty()) {
|
|
332
|
+
objc::msg_send<void>(panel, objc::selector("setTitle:"),
|
|
333
|
+
cocoa::NSString_stringWithUTF8String(req.title));
|
|
334
|
+
}
|
|
335
|
+
if (!req.default_path.empty()) {
|
|
336
|
+
id url = objc::msg_send<id>(
|
|
337
|
+
objc::get_class("NSURL"), objc::selector("fileURLWithPath:"),
|
|
338
|
+
cocoa::NSString_stringWithUTF8String(req.default_path));
|
|
339
|
+
objc::msg_send<void>(panel, objc::selector("setDirectoryURL:"), url);
|
|
340
|
+
}
|
|
341
|
+
if (save && !req.default_name.empty()) {
|
|
342
|
+
objc::msg_send<void>(panel, objc::selector("setNameFieldStringValue:"),
|
|
343
|
+
cocoa::NSString_stringWithUTF8String(req.default_name));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
std::vector<Filter> filters = parse_filters(req.filters);
|
|
347
|
+
if (!filters.empty()) {
|
|
348
|
+
// setAllowedFileTypes: is deprecated in favour of UTType on macOS 12+, but
|
|
349
|
+
// still honoured, and it takes plain extension strings — the UTType path
|
|
350
|
+
// would need a type lookup per extension for no gain here.
|
|
351
|
+
id types = objc::msg_send<id>(objc::get_class("NSMutableArray"),
|
|
352
|
+
objc::selector("array"));
|
|
353
|
+
for (size_t i = 0; i < filters.size(); i++) {
|
|
354
|
+
for (size_t j = 0; j < filters[i].extensions.size(); j++) {
|
|
355
|
+
const std::string &ext = filters[i].extensions[j];
|
|
356
|
+
if (ext.empty() || ext == "*") continue;
|
|
357
|
+
objc::msg_send<void>(types, objc::selector("addObject:"),
|
|
358
|
+
cocoa::NSString_stringWithUTF8String(ext));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (objc::msg_send<NSUInteger>(types, objc::selector("count")) > 0) {
|
|
362
|
+
objc::msg_send<void>(panel, objc::selector("setAllowedFileTypes:"), types);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!save) {
|
|
367
|
+
bool want_dirs = (req.flags & DLG_DIRECTORY) != 0;
|
|
368
|
+
cocoa::NSOpenPanel_set_canChooseFiles(panel, !want_dirs);
|
|
369
|
+
cocoa::NSOpenPanel_set_canChooseDirectories(panel, want_dirs);
|
|
370
|
+
cocoa::NSOpenPanel_set_allowsMultipleSelection(
|
|
371
|
+
panel, (req.flags & DLG_MULTIPLE) != 0);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (cocoa::NSSavePanel_runModal(panel) != cocoa::NSModalResponseOK) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
auto path_of = [](id url) -> std::string {
|
|
379
|
+
id path = objc::msg_send<id>(url, objc::selector("path"));
|
|
380
|
+
const char *utf8 = cocoa::NSString_get_UTF8String(path);
|
|
381
|
+
return utf8 ? std::string(utf8) : std::string();
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
if (save) {
|
|
385
|
+
id url = objc::msg_send<id>(panel, objc::selector("URL"));
|
|
386
|
+
if (!url) return false;
|
|
387
|
+
out.push_back(path_of(url));
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
id urls = cocoa::NSOpenPanel_get_URLs(panel);
|
|
392
|
+
NSUInteger n = objc::msg_send<NSUInteger>(urls, objc::selector("count"));
|
|
393
|
+
for (NSUInteger i = 0; i < n; i++) {
|
|
394
|
+
id url = objc::msg_send<id>(urls, objc::selector("objectAtIndex:"), i);
|
|
395
|
+
out.push_back(path_of(url));
|
|
396
|
+
}
|
|
397
|
+
return !out.empty();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#elif defined(_WIN32)
|
|
401
|
+
|
|
402
|
+
std::string from_wide(const wchar_t *w, int wlen) {
|
|
403
|
+
if (!w || wlen == 0) return std::string();
|
|
404
|
+
int n = WideCharToMultiByte(CP_UTF8, 0, w, wlen, nullptr, 0, nullptr, nullptr);
|
|
405
|
+
if (n <= 0) return std::string();
|
|
406
|
+
std::string out(static_cast<size_t>(n), '\0');
|
|
407
|
+
WideCharToMultiByte(CP_UTF8, 0, w, wlen, &out[0], n, nullptr, nullptr);
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
std::wstring to_wide(const std::string &s) {
|
|
412
|
+
if (s.empty()) return std::wstring();
|
|
413
|
+
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(),
|
|
414
|
+
static_cast<int>(s.size()), nullptr, 0);
|
|
415
|
+
if (n <= 0) return std::wstring();
|
|
416
|
+
std::wstring out(static_cast<size_t>(n), L'\0');
|
|
417
|
+
MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast<int>(s.size()), &out[0],
|
|
418
|
+
n);
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
423
|
+
std::string &error) {
|
|
424
|
+
if (req.flags & DLG_DIRECTORY) {
|
|
425
|
+
error = "ENOTSUP: directory selection is not implemented on Windows";
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// GetOpenFileNameW/GetSaveFileNameW render the modern common item dialog on
|
|
430
|
+
// Vista and later as long as no hook is installed, so this buys the current
|
|
431
|
+
// look without the COM ceremony of IFileDialog.
|
|
432
|
+
std::vector<Filter> filters = parse_filters(req.filters);
|
|
433
|
+
std::wstring filter_buf;
|
|
434
|
+
for (size_t i = 0; i < filters.size(); i++) {
|
|
435
|
+
std::string patterns;
|
|
436
|
+
for (size_t j = 0; j < filters[i].extensions.size(); j++) {
|
|
437
|
+
if (j) patterns += ";";
|
|
438
|
+
patterns += "*." + filters[i].extensions[j];
|
|
439
|
+
}
|
|
440
|
+
filter_buf += to_wide(filters[i].name + " (" + patterns + ")");
|
|
441
|
+
filter_buf.push_back(L'\0');
|
|
442
|
+
filter_buf += to_wide(patterns);
|
|
443
|
+
filter_buf.push_back(L'\0');
|
|
444
|
+
}
|
|
445
|
+
if (!filter_buf.empty()) filter_buf.push_back(L'\0');
|
|
446
|
+
|
|
447
|
+
// Multi-select returns "dir\0name\0name\0\0", so the buffer must hold more
|
|
448
|
+
// than one MAX_PATH.
|
|
449
|
+
std::vector<wchar_t> file(32768, L'\0');
|
|
450
|
+
std::wstring initial_name = to_wide(req.default_name);
|
|
451
|
+
if (!initial_name.empty() && initial_name.size() < file.size() - 1) {
|
|
452
|
+
std::memcpy(file.data(), initial_name.c_str(),
|
|
453
|
+
(initial_name.size() + 1) * sizeof(wchar_t));
|
|
454
|
+
}
|
|
455
|
+
std::wstring initial_dir = to_wide(req.default_path);
|
|
456
|
+
std::wstring title = to_wide(req.title);
|
|
457
|
+
|
|
458
|
+
OPENFILENAMEW ofn;
|
|
459
|
+
std::memset(&ofn, 0, sizeof(ofn));
|
|
460
|
+
ofn.lStructSize = sizeof(ofn);
|
|
461
|
+
ofn.hwndOwner = nullptr;
|
|
462
|
+
ofn.lpstrFilter = filter_buf.empty() ? nullptr : filter_buf.c_str();
|
|
463
|
+
ofn.lpstrFile = file.data();
|
|
464
|
+
ofn.nMaxFile = static_cast<DWORD>(file.size());
|
|
465
|
+
ofn.lpstrInitialDir = initial_dir.empty() ? nullptr : initial_dir.c_str();
|
|
466
|
+
ofn.lpstrTitle = title.empty() ? nullptr : title.c_str();
|
|
467
|
+
ofn.Flags = OFN_NOCHANGEDIR | OFN_EXPLORER;
|
|
468
|
+
|
|
469
|
+
if (req.kind == DLG_SAVE) {
|
|
470
|
+
ofn.Flags |= OFN_OVERWRITEPROMPT;
|
|
471
|
+
if (!GetSaveFileNameW(&ofn)) return false;
|
|
472
|
+
out.push_back(from_wide(ofn.lpstrFile, -1));
|
|
473
|
+
if (!out.back().empty()) out.back().pop_back(); // trailing NUL from -1
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
ofn.Flags |= OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
|
|
478
|
+
if (req.flags & DLG_MULTIPLE) ofn.Flags |= OFN_ALLOWMULTISELECT;
|
|
479
|
+
if (!GetOpenFileNameW(&ofn)) return false;
|
|
480
|
+
|
|
481
|
+
// Single selection is one NUL-terminated path; multiple is a directory
|
|
482
|
+
// followed by bare names, all NUL-separated, ending in a double NUL.
|
|
483
|
+
const wchar_t *p = ofn.lpstrFile;
|
|
484
|
+
std::string first = from_wide(p, static_cast<int>(wcslen(p)));
|
|
485
|
+
p += wcslen(p) + 1;
|
|
486
|
+
if (*p == L'\0') {
|
|
487
|
+
out.push_back(first);
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
while (*p) {
|
|
491
|
+
std::string name = from_wide(p, static_cast<int>(wcslen(p)));
|
|
492
|
+
out.push_back(first + "\\" + name);
|
|
493
|
+
p += wcslen(p) + 1;
|
|
494
|
+
}
|
|
495
|
+
return !out.empty();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#else // GTK
|
|
499
|
+
|
|
500
|
+
bool run_file_dialog(const DialogRequest &req, std::vector<std::string> &out,
|
|
501
|
+
std::string &error) {
|
|
502
|
+
(void)error;
|
|
503
|
+
bool save = req.kind == DLG_SAVE;
|
|
504
|
+
bool want_dirs = (req.flags & DLG_DIRECTORY) != 0;
|
|
505
|
+
GtkFileChooserAction action =
|
|
506
|
+
save ? GTK_FILE_CHOOSER_ACTION_SAVE
|
|
507
|
+
: (want_dirs ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER
|
|
508
|
+
: GTK_FILE_CHOOSER_ACTION_OPEN);
|
|
509
|
+
|
|
510
|
+
GtkWidget *dialog = gtk_file_chooser_dialog_new(
|
|
511
|
+
req.title.empty() ? (save ? "Save" : "Open") : req.title.c_str(), nullptr,
|
|
512
|
+
action, "_Cancel", GTK_RESPONSE_CANCEL,
|
|
513
|
+
save ? "_Save" : "_Open", GTK_RESPONSE_ACCEPT, nullptr);
|
|
514
|
+
if (!dialog) return false;
|
|
515
|
+
|
|
516
|
+
GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
|
|
517
|
+
if (!save && (req.flags & DLG_MULTIPLE)) {
|
|
518
|
+
gtk_file_chooser_set_select_multiple(chooser, TRUE);
|
|
519
|
+
}
|
|
520
|
+
if (save) {
|
|
521
|
+
gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
|
|
522
|
+
if (!req.default_name.empty()) {
|
|
523
|
+
gtk_file_chooser_set_current_name(chooser, req.default_name.c_str());
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (!req.default_path.empty()) {
|
|
527
|
+
gtk_file_chooser_set_current_folder(chooser, req.default_path.c_str());
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
std::vector<Filter> filters = parse_filters(req.filters);
|
|
531
|
+
for (size_t i = 0; i < filters.size(); i++) {
|
|
532
|
+
GtkFileFilter *f = gtk_file_filter_new();
|
|
533
|
+
gtk_file_filter_set_name(f, filters[i].name.c_str());
|
|
534
|
+
for (size_t j = 0; j < filters[i].extensions.size(); j++) {
|
|
535
|
+
std::string pattern = "*." + filters[i].extensions[j];
|
|
536
|
+
gtk_file_filter_add_pattern(f, pattern.c_str());
|
|
537
|
+
}
|
|
538
|
+
gtk_file_chooser_add_filter(chooser, f);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
bool ok = gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT;
|
|
542
|
+
if (ok) {
|
|
543
|
+
if (!save && (req.flags & DLG_MULTIPLE)) {
|
|
544
|
+
GSList *names = gtk_file_chooser_get_filenames(chooser);
|
|
545
|
+
for (GSList *it = names; it; it = it->next) {
|
|
546
|
+
char *path = static_cast<char *>(it->data);
|
|
547
|
+
if (path) {
|
|
548
|
+
out.push_back(path);
|
|
549
|
+
g_free(path);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
g_slist_free(names);
|
|
553
|
+
} else {
|
|
554
|
+
char *path = gtk_file_chooser_get_filename(chooser);
|
|
555
|
+
if (path) {
|
|
556
|
+
out.push_back(path);
|
|
557
|
+
g_free(path);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
gtk_widget_destroy(dialog);
|
|
562
|
+
// Let the destroy actually happen before the modal's caller resumes.
|
|
563
|
+
while (gtk_events_pending()) gtk_main_iteration();
|
|
564
|
+
return ok && !out.empty();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
#endif
|
|
568
|
+
|
|
569
|
+
// Runs on the UI thread with no TS frame beneath it — see the note on the job
|
|
570
|
+
// pool above for why that matters.
|
|
571
|
+
void dialog_on_ui_thread(webview_t, void *arg) {
|
|
572
|
+
std::unique_ptr<DialogRequest> req(static_cast<DialogRequest *>(arg));
|
|
573
|
+
Job *j = job_at(req->job);
|
|
574
|
+
if (!j) return;
|
|
575
|
+
std::vector<std::string> picked;
|
|
576
|
+
std::string error;
|
|
577
|
+
bool ok = run_file_dialog(*req, picked, error);
|
|
578
|
+
if (!error.empty()) {
|
|
579
|
+
job_finish(j, JOB_ERROR, error);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// A cancel is a successful call that answers `null`, not a failure.
|
|
583
|
+
job_finish(j, JOB_OK, ok ? json_array(picked) : "null");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// The single C trampoline registered with webview_bind. `arg` is the app
|
|
587
|
+
// index; every binding routes to the one retained invoke handler.
|
|
79
588
|
void trampoline(const char *id, const char *req, void *arg) {
|
|
80
|
-
|
|
81
|
-
App *a = &g_apps[packed >> 32];
|
|
82
|
-
uint32_t bind_index = static_cast<uint32_t>(packed & 0xffffffffu);
|
|
589
|
+
App *a = &g_apps[reinterpret_cast<uintptr_t>(arg)];
|
|
83
590
|
|
|
84
|
-
if (!a->
|
|
591
|
+
if (!a->on_invoke) return; // no TS handler installed
|
|
85
592
|
|
|
86
593
|
a->req = req ? req : "";
|
|
87
594
|
a->cur_id = id ? id : "";
|
|
@@ -89,9 +596,11 @@ void trampoline(const char *id, const char *req, void *arg) {
|
|
|
89
596
|
a->deferred = false;
|
|
90
597
|
a->seq++;
|
|
91
598
|
|
|
92
|
-
// Re-entrancy: this call lands back in TS, which
|
|
93
|
-
//
|
|
94
|
-
int32_t status = a->
|
|
599
|
+
// Re-entrancy: this call lands back in TS, which calls wv_reply() back into
|
|
600
|
+
// this shim before returning.
|
|
601
|
+
int32_t status = a->on_invoke(
|
|
602
|
+
reinterpret_cast<const uint8_t *>(a->req.data()), a->req.size(),
|
|
603
|
+
a->on_invoke_ctx);
|
|
95
604
|
|
|
96
605
|
// An async handler called wv_defer(): the call id now lives in the pending
|
|
97
606
|
// table and wv_resolve() will answer the page later. Returning now would
|
|
@@ -114,9 +623,9 @@ void trampoline(const char *id, const char *req, void *arg) {
|
|
|
114
623
|
// TS it calls stays single-threaded — scriptc's runtime is NOT thread-safe.
|
|
115
624
|
void tick_on_ui_thread(webview_t, void *arg) {
|
|
116
625
|
App *a = &g_apps[reinterpret_cast<uintptr_t>(arg)];
|
|
117
|
-
if (!a->used || !a->
|
|
626
|
+
if (!a->used || !a->on_tick) return; // app quit between dispatch and delivery
|
|
118
627
|
a->seq++;
|
|
119
|
-
a->
|
|
628
|
+
a->on_tick(a->on_tick_ctx);
|
|
120
629
|
}
|
|
121
630
|
|
|
122
631
|
} // namespace
|
|
@@ -131,8 +640,10 @@ int32_t wv_create(int32_t debug) {
|
|
|
131
640
|
// Field-wise reset: App holds a thread and atomics, so it is not
|
|
132
641
|
// copy-assignable from a temporary.
|
|
133
642
|
g_apps[i].binds.clear();
|
|
134
|
-
g_apps[i].
|
|
135
|
-
g_apps[i].
|
|
643
|
+
g_apps[i].on_invoke = nullptr;
|
|
644
|
+
g_apps[i].on_invoke_ctx = nullptr;
|
|
645
|
+
g_apps[i].on_tick = nullptr;
|
|
646
|
+
g_apps[i].on_tick_ctx = nullptr;
|
|
136
647
|
g_apps[i].req.clear();
|
|
137
648
|
g_apps[i].cur_id.clear();
|
|
138
649
|
g_apps[i].reply.clear();
|
|
@@ -189,40 +700,20 @@ int32_t wv_eval(int32_t h, const uint8_t *p, size_t n) {
|
|
|
189
700
|
int32_t wv_bind(int32_t h, const uint8_t *p, size_t n) {
|
|
190
701
|
App *a = app_at(h);
|
|
191
702
|
if (!a) return -1;
|
|
192
|
-
|
|
703
|
+
size_t idx = a->binds.size();
|
|
193
704
|
a->binds.push_back(Bind{to_str(p, n)});
|
|
194
|
-
uintptr_t packed = (static_cast<uintptr_t>(h) << 32) | idx;
|
|
195
705
|
int rc = webview_bind(a->w, a->binds[idx].name.c_str(), trampoline,
|
|
196
|
-
reinterpret_cast<void *>(
|
|
706
|
+
reinterpret_cast<void *>(static_cast<uintptr_t>(h)));
|
|
197
707
|
if (rc != WEBVIEW_ERROR_OK) return -1;
|
|
198
708
|
return static_cast<int32_t>(idx);
|
|
199
709
|
}
|
|
200
710
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (!a) return -1;
|
|
205
|
-
return static_cast<int32_t>(a->req.size());
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
int32_t wv_req_byte(int32_t h, int32_t i) {
|
|
209
|
-
App *a = app_at(h);
|
|
210
|
-
if (!a || i < 0 || static_cast<size_t>(i) >= a->req.size()) return -1;
|
|
211
|
-
return static_cast<uint8_t>(a->req[i]);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ---- reply payload, pushed byte-at-a-time from inside the TS callback ----
|
|
215
|
-
int32_t wv_reply_reset(int32_t h) {
|
|
216
|
-
App *a = app_at(h);
|
|
217
|
-
if (!a) return -1;
|
|
218
|
-
a->reply.clear();
|
|
219
|
-
return 0;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
int32_t wv_reply_push(int32_t h, int32_t byte) {
|
|
711
|
+
// Stage the response body for the call being handled (or, after wv_defer, for
|
|
712
|
+
// the pending call that wv_resolve will answer). One call, whole payload.
|
|
713
|
+
int32_t wv_reply(int32_t h, const uint8_t *p, size_t n) {
|
|
223
714
|
App *a = app_at(h);
|
|
224
715
|
if (!a) return -1;
|
|
225
|
-
a->reply.
|
|
716
|
+
a->reply.assign(reinterpret_cast<const char *>(p), n);
|
|
226
717
|
return 0;
|
|
227
718
|
}
|
|
228
719
|
|
|
@@ -268,7 +759,7 @@ int32_t wv_resolve(int32_t h, int32_t id, int32_t status) {
|
|
|
268
759
|
return 0;
|
|
269
760
|
}
|
|
270
761
|
|
|
271
|
-
// Start pumping
|
|
762
|
+
// Start pumping the retained tick handler every interval_ms. The thread
|
|
272
763
|
// itself only sleeps and posts; all TS execution happens on the UI thread.
|
|
273
764
|
int32_t wv_tick_start(int32_t h, int32_t interval_ms) {
|
|
274
765
|
App *a = app_at(h);
|
|
@@ -295,19 +786,187 @@ int32_t wv_tick_stop(int32_t h) {
|
|
|
295
786
|
return 0;
|
|
296
787
|
}
|
|
297
788
|
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
789
|
+
// ---- async file I/O ---------------------------------------------------------
|
|
790
|
+
//
|
|
791
|
+
// wv_fs_read/wv_fs_write start a worker thread and return immediately with a
|
|
792
|
+
// job id. TS polls wv_job_status() from its tick loop and drains the payload
|
|
793
|
+
// with wv_fs_byte() once the job is terminal. On failure the payload is the
|
|
794
|
+
// error message, so success and failure share one drain path.
|
|
795
|
+
|
|
796
|
+
int32_t wv_fs_read(int32_t h, const uint8_t *p, size_t n) {
|
|
797
|
+
if (!app_at(h)) return -1;
|
|
798
|
+
int32_t id = new_job();
|
|
799
|
+
Job *j = job_at(id);
|
|
800
|
+
if (!j) return -1;
|
|
801
|
+
j->worker = std::thread(fs_read_worker, j, to_str(p, n));
|
|
802
|
+
return id;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
int32_t wv_fs_write(int32_t h, const uint8_t *p, size_t n, const uint8_t *dp,
|
|
806
|
+
size_t dn) {
|
|
807
|
+
if (!app_at(h)) return -1;
|
|
808
|
+
int32_t id = new_job();
|
|
809
|
+
Job *j = job_at(id);
|
|
810
|
+
if (!j) return -1;
|
|
811
|
+
j->worker = std::thread(fs_write_worker, j, to_str(p, n), to_str(dp, dn));
|
|
812
|
+
return id;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// 0 = still running, 1 = done, 2 = failed, -1 = no such job.
|
|
816
|
+
int32_t wv_job_status(int32_t h, int32_t id) {
|
|
817
|
+
if (!app_at(h)) return -1;
|
|
818
|
+
Job *j = job_at(id);
|
|
819
|
+
if (!j) return -1;
|
|
820
|
+
return j->status.load(std::memory_order_acquire);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Hand a finished job's payload to TS in one call. The callback is
|
|
824
|
+
// lifetime:"call", so it runs synchronously here — on the UI thread, the only
|
|
825
|
+
// thread allowed to touch the scriptc runtime. The worker is already done by
|
|
826
|
+
// then (the caller has observed a terminal status), so `data` is stable.
|
|
827
|
+
int32_t wv_job_take(int32_t h, int32_t id,
|
|
828
|
+
void (*sink)(const uint8_t *, size_t, void *), void *ctx) {
|
|
829
|
+
if (!app_at(h)) return -1;
|
|
830
|
+
Job *j = job_at(id);
|
|
831
|
+
if (!j || !sink) return -1;
|
|
832
|
+
if (j->status.load(std::memory_order_acquire) == JOB_PENDING) return -1;
|
|
833
|
+
sink(reinterpret_cast<const uint8_t *>(j->data.data()), j->data.size(), ctx);
|
|
834
|
+
return 0;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Release the slot for reuse. Refuses while the worker is still running, so a
|
|
838
|
+
// job's buffer can never be recycled out from under its own thread.
|
|
839
|
+
int32_t wv_job_free(int32_t h, int32_t id) {
|
|
840
|
+
if (!app_at(h)) return -1;
|
|
841
|
+
Job *j = job_at(id);
|
|
842
|
+
if (!j) return -1;
|
|
843
|
+
if (j->status.load(std::memory_order_acquire) == JOB_PENDING) return -1;
|
|
844
|
+
std::lock_guard<std::mutex> lock(g_jobs_mu);
|
|
845
|
+
if (j->worker.joinable()) j->worker.join();
|
|
846
|
+
j->data.clear();
|
|
847
|
+
j->data.shrink_to_fit();
|
|
848
|
+
j->used = false;
|
|
849
|
+
return 0;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ---- native dialogs ---------------------------------------------------------
|
|
853
|
+
|
|
854
|
+
// Start a file dialog. Returns a job id immediately; the modal itself runs on
|
|
855
|
+
// a later UI-thread turn, so this never blocks the invoke that asked for it.
|
|
856
|
+
// The finished payload is a JSON array of paths, or `null` for a cancel.
|
|
857
|
+
int32_t wv_dialog(int32_t h, int32_t kind, int32_t flags, const uint8_t *tp,
|
|
858
|
+
size_t tn, const uint8_t *pp, size_t pn, const uint8_t *np,
|
|
859
|
+
size_t nn, const uint8_t *fp, size_t fn) {
|
|
860
|
+
App *a = app_at(h);
|
|
861
|
+
if (!a) return -1;
|
|
862
|
+
int32_t id = new_job();
|
|
863
|
+
Job *j = job_at(id);
|
|
864
|
+
if (!j) return -1;
|
|
865
|
+
|
|
866
|
+
DialogRequest *req = new DialogRequest();
|
|
867
|
+
req->app = h;
|
|
868
|
+
req->job = id;
|
|
869
|
+
req->kind = kind;
|
|
870
|
+
req->flags = flags;
|
|
871
|
+
req->title = to_str(tp, tn);
|
|
872
|
+
req->default_path = to_str(pp, pn);
|
|
873
|
+
req->default_name = to_str(np, nn);
|
|
874
|
+
req->filters = to_str(fp, fn);
|
|
875
|
+
|
|
876
|
+
if (webview_dispatch(a->w, dialog_on_ui_thread, req) != WEBVIEW_ERROR_OK) {
|
|
877
|
+
delete req;
|
|
878
|
+
job_finish(j, JOB_ERROR, "EIO: could not post the dialog to the UI thread");
|
|
879
|
+
}
|
|
880
|
+
return id;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ---- window control ---------------------------------------------------------
|
|
884
|
+
|
|
885
|
+
int32_t wv_set_fullscreen(int32_t h, int32_t on) {
|
|
886
|
+
App *a = app_at(h);
|
|
887
|
+
if (!a) return -1;
|
|
888
|
+
void *win = webview_get_window(a->w);
|
|
889
|
+
if (!win) return -1;
|
|
890
|
+
|
|
891
|
+
#if defined(__APPLE__)
|
|
892
|
+
using namespace webview::detail;
|
|
893
|
+
objc::autoreleasepool arp;
|
|
894
|
+
id window = static_cast<id>(win);
|
|
895
|
+
// NSWindowStyleMaskFullScreen. toggleFullScreen: only toggles, so read the
|
|
896
|
+
// current state first and leave it alone when it already matches.
|
|
897
|
+
const NSUInteger full = 1UL << 14;
|
|
898
|
+
NSUInteger mask = objc::msg_send<NSUInteger>(window, objc::selector("styleMask"));
|
|
899
|
+
bool is_full = (mask & full) != 0;
|
|
900
|
+
if (is_full != (on != 0)) {
|
|
901
|
+
objc::msg_send<void>(window, objc::selector("toggleFullScreen:"), nullptr);
|
|
902
|
+
}
|
|
903
|
+
return 0;
|
|
904
|
+
#elif defined(_WIN32)
|
|
905
|
+
HWND hwnd = static_cast<HWND>(win);
|
|
906
|
+
static WINDOWPLACEMENT saved = {sizeof(saved), 0, 0, {0, 0}, {0, 0}, {0, 0, 0, 0}};
|
|
907
|
+
LONG_PTR style = GetWindowLongPtrW(hwnd, GWL_STYLE);
|
|
908
|
+
if (on) {
|
|
909
|
+
MONITORINFO mi;
|
|
910
|
+
mi.cbSize = sizeof(mi);
|
|
911
|
+
if (!GetWindowPlacement(hwnd, &saved) ||
|
|
912
|
+
!GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY), &mi)) {
|
|
913
|
+
return -1;
|
|
914
|
+
}
|
|
915
|
+
SetWindowLongPtrW(hwnd, GWL_STYLE, style & ~WS_OVERLAPPEDWINDOW);
|
|
916
|
+
SetWindowPos(hwnd, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top,
|
|
917
|
+
mi.rcMonitor.right - mi.rcMonitor.left,
|
|
918
|
+
mi.rcMonitor.bottom - mi.rcMonitor.top,
|
|
919
|
+
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
|
920
|
+
} else {
|
|
921
|
+
SetWindowLongPtrW(hwnd, GWL_STYLE, style | WS_OVERLAPPEDWINDOW);
|
|
922
|
+
SetWindowPlacement(hwnd, &saved);
|
|
923
|
+
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
|
924
|
+
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER |
|
|
925
|
+
SWP_FRAMECHANGED);
|
|
926
|
+
}
|
|
927
|
+
return 0;
|
|
928
|
+
#else
|
|
929
|
+
GtkWindow *window = GTK_WINDOW(win);
|
|
930
|
+
if (on) {
|
|
931
|
+
gtk_window_fullscreen(window);
|
|
932
|
+
} else {
|
|
933
|
+
gtk_window_unfullscreen(window);
|
|
934
|
+
}
|
|
935
|
+
return 0;
|
|
936
|
+
#endif
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Register the retained handler for page invokes. Valid until the app exits.
|
|
940
|
+
int32_t wv_on_invoke(int32_t h,
|
|
941
|
+
int32_t (*cb)(const uint8_t *, size_t, void *),
|
|
942
|
+
void *ctx) {
|
|
943
|
+
App *a = app_at(h);
|
|
944
|
+
if (!a) return -1;
|
|
945
|
+
a->on_invoke = cb;
|
|
946
|
+
a->on_invoke_ctx = ctx;
|
|
947
|
+
return 0;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Register the retained handler the ticker pumps on the UI thread.
|
|
951
|
+
int32_t wv_on_tick(int32_t h, void (*cb)(void *), void *ctx) {
|
|
952
|
+
App *a = app_at(h);
|
|
953
|
+
if (!a) return -1;
|
|
954
|
+
a->on_tick = cb;
|
|
955
|
+
a->on_tick_ctx = ctx;
|
|
956
|
+
return 0;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Blocks for the app's lifetime, dispatching into the retained handlers.
|
|
960
|
+
int32_t wv_run(int32_t h) {
|
|
301
961
|
App *a = app_at(h);
|
|
302
962
|
if (!a) return -1;
|
|
303
|
-
a->cb = cb;
|
|
304
|
-
a->cb_ctx = cb_ctx;
|
|
305
963
|
int rc = webview_run(a->w);
|
|
306
|
-
//
|
|
964
|
+
// Nothing may call into TS once run() has returned.
|
|
307
965
|
a->ticking.store(false);
|
|
308
966
|
if (a->ticker.joinable()) a->ticker.join();
|
|
309
|
-
|
|
310
|
-
a->
|
|
967
|
+
jobs_join_all(); // nor may an in-flight read outlive the app
|
|
968
|
+
a->on_invoke = nullptr;
|
|
969
|
+
a->on_tick = nullptr;
|
|
311
970
|
return rc;
|
|
312
971
|
}
|
|
313
972
|
|