janela 0.8.0 → 0.10.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 +23 -4
- package/bin/janela.mjs +266 -18
- package/package.json +4 -3
- package/runtime/ios.ts +483 -0
- package/runtime/janela.ts +88 -82
- package/shim/ios/app.mm +386 -0
- package/shim/wvshim.cc +166 -59
- package/templates/janela.conf.json +5 -0
- package/templates/react/files/janela.conf.json +5 -0
- package/templates/solid/files/janela.conf.json +5 -0
- package/templates/svelte/files/janela.conf.json +5 -0
- package/templates/vue/files/janela.conf.json +5 -0
package/shim/wvshim.cc
CHANGED
|
@@ -8,15 +8,22 @@
|
|
|
8
8
|
// * format 3 — callback params may be `string`/`bytes`, so a payload crosses
|
|
9
9
|
// into TS as one argument instead of one FFI call per byte. Payloads going
|
|
10
10
|
// the other way ride `string` params on ordinary functions.
|
|
11
|
-
// * format 4 — callbacks may be `retained`, so the invoke and
|
|
11
|
+
// * format 4 — callbacks may be `retained`, so the invoke and timer handlers
|
|
12
12
|
// are registered once and live for the app's lifetime. wv_run() is a plain
|
|
13
13
|
// blocking call again; it no longer has to carry a callback whose "call
|
|
14
14
|
// scope" was standing in for "app lifetime".
|
|
15
|
+
//
|
|
16
|
+
// The shell owns scheduling. TS never holds a timer: it registers a
|
|
17
|
+
// continuation under an id and calls wv_schedule(), and this shim calls back
|
|
18
|
+
// into TS with that id once the delay is up. That is the same shape a library
|
|
19
|
+
// -mode host (iOS) must use, where the compiled TS links no event loop at all.
|
|
15
20
|
|
|
16
21
|
#include "webview.h"
|
|
17
22
|
|
|
23
|
+
#include <algorithm>
|
|
18
24
|
#include <atomic>
|
|
19
25
|
#include <chrono>
|
|
26
|
+
#include <condition_variable>
|
|
20
27
|
#include <cstdint>
|
|
21
28
|
#include <cstdio>
|
|
22
29
|
#include <cstring>
|
|
@@ -50,6 +57,13 @@ struct Pending {
|
|
|
50
57
|
std::string call_id;
|
|
51
58
|
};
|
|
52
59
|
|
|
60
|
+
// A continuation TS has parked with the shell: run whatever TS registered
|
|
61
|
+
// under `id` once `due` has passed. The shell owns the clock; TS owns the id.
|
|
62
|
+
struct Timer {
|
|
63
|
+
int32_t id;
|
|
64
|
+
std::chrono::steady_clock::time_point due;
|
|
65
|
+
};
|
|
66
|
+
|
|
53
67
|
struct App {
|
|
54
68
|
webview_t w = nullptr;
|
|
55
69
|
bool used = false;
|
|
@@ -59,8 +73,8 @@ struct App {
|
|
|
59
73
|
// request rides in as a (ptr, len) string param.
|
|
60
74
|
int32_t (*on_invoke)(const uint8_t *, size_t, void *) = nullptr;
|
|
61
75
|
void *on_invoke_ctx = nullptr;
|
|
62
|
-
void (*
|
|
63
|
-
void *
|
|
76
|
+
void (*on_timer)(int32_t, void *) = nullptr;
|
|
77
|
+
void *on_timer_ctx = nullptr;
|
|
64
78
|
|
|
65
79
|
// Staging for the in-flight request.
|
|
66
80
|
std::string req; // JSON args array from JS
|
|
@@ -69,13 +83,26 @@ struct App {
|
|
|
69
83
|
uint32_t seq = 0;
|
|
70
84
|
|
|
71
85
|
// ---- async support ----
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
std::
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
// The held-reply table: an invoke whose answer is not ready yet. The page's
|
|
87
|
+
// promise stays unsettled until wv_resolve() answers this call id.
|
|
88
|
+
std::vector<Pending> pending;
|
|
89
|
+
bool deferred = false; // set by wv_defer() during the current call
|
|
90
|
+
|
|
91
|
+
// The shell's timer queue, and the one thread that watches it. The thread
|
|
92
|
+
// only sleeps and posts — it never touches TS, because scriptc's runtime is
|
|
93
|
+
// not thread-safe. Everything reaches TS through webview_dispatch, on the UI
|
|
94
|
+
// thread, on a LATER turn of the shell's own loop (see timer_on_ui_thread).
|
|
95
|
+
std::vector<Timer> timers;
|
|
96
|
+
std::mutex timers_mu;
|
|
97
|
+
std::condition_variable timers_cv;
|
|
98
|
+
std::thread scheduler;
|
|
99
|
+
std::atomic<bool> scheduling{false};
|
|
77
100
|
};
|
|
78
101
|
|
|
102
|
+
// Reserved timer id: not a TS continuation but "a job changed state, service
|
|
103
|
+
// them". TS allocates its own continuation ids from 1 upwards.
|
|
104
|
+
const int32_t TIMER_JOBS = -1;
|
|
105
|
+
|
|
79
106
|
// Fixed-size table: handles are indices, never pointers.
|
|
80
107
|
App g_apps[8];
|
|
81
108
|
|
|
@@ -92,8 +119,9 @@ std::string to_str(const uint8_t *p, size_t n) {
|
|
|
92
119
|
// ---- jobs ------------------------------------------------------------------
|
|
93
120
|
//
|
|
94
121
|
// A job is any unit of work whose answer cannot be produced during the FFI
|
|
95
|
-
// call that asks for it. TS starts one
|
|
96
|
-
//
|
|
122
|
+
// call that asks for it. TS starts one and gets an id back immediately; when
|
|
123
|
+
// the job reaches a terminal state it posts TIMER_JOBS to the UI thread, and
|
|
124
|
+
// TS then reads wv_job_status() for the jobs it is waiting on.
|
|
97
125
|
//
|
|
98
126
|
// Two kinds use this pool, for opposite reasons:
|
|
99
127
|
// * file I/O — the blocking syscall must happen off the UI thread, so a
|
|
@@ -102,9 +130,9 @@ std::string to_str(const uint8_t *p, size_t n) {
|
|
|
102
130
|
// later, on the UI thread.
|
|
103
131
|
// * native dialogs — the modal must run ON the UI thread, but not while TS
|
|
104
132
|
// is on the stack (runModal/gtk_dialog_run spin a nested event loop, which
|
|
105
|
-
// would re-enter
|
|
106
|
-
//
|
|
107
|
-
//
|
|
133
|
+
// would re-enter TS underneath the invoke handler that asked for the
|
|
134
|
+
// dialog). So the job is posted with webview_dispatch and runs at the top
|
|
135
|
+
// of a later turn, with no TS frame beneath it.
|
|
108
136
|
|
|
109
137
|
const int32_t JOB_PENDING = 0;
|
|
110
138
|
const int32_t JOB_OK = 1;
|
|
@@ -119,6 +147,9 @@ struct Job {
|
|
|
119
147
|
std::string data; // payload on success, the error message on failure
|
|
120
148
|
std::thread worker; // unused by dialog jobs, which run on the UI thread
|
|
121
149
|
bool used = false;
|
|
150
|
+
// Which app to wake when this job finishes. Without the ticker there is
|
|
151
|
+
// nothing polling, so a finished job has to announce itself.
|
|
152
|
+
int32_t app = -1;
|
|
122
153
|
};
|
|
123
154
|
|
|
124
155
|
// Jobs are addressed by index and held behind unique_ptr so the vector may
|
|
@@ -135,7 +166,7 @@ Job *job_at(int32_t id) {
|
|
|
135
166
|
|
|
136
167
|
// Reuses a finished slot when one is free, so a long-running app that reads
|
|
137
168
|
// many files does not grow the table without bound.
|
|
138
|
-
int32_t new_job() {
|
|
169
|
+
int32_t new_job(int32_t app) {
|
|
139
170
|
std::lock_guard<std::mutex> lock(g_jobs_mu);
|
|
140
171
|
for (size_t i = 0; i < g_jobs.size(); i++) {
|
|
141
172
|
if (g_jobs[i]->used) continue;
|
|
@@ -143,10 +174,12 @@ int32_t new_job() {
|
|
|
143
174
|
g_jobs[i]->status.store(JOB_PENDING);
|
|
144
175
|
g_jobs[i]->data.clear();
|
|
145
176
|
g_jobs[i]->used = true;
|
|
177
|
+
g_jobs[i]->app = app;
|
|
146
178
|
return static_cast<int32_t>(i);
|
|
147
179
|
}
|
|
148
180
|
g_jobs.push_back(std::unique_ptr<Job>(new Job()));
|
|
149
181
|
g_jobs.back()->used = true;
|
|
182
|
+
g_jobs.back()->app = app;
|
|
150
183
|
return static_cast<int32_t>(g_jobs.size() - 1);
|
|
151
184
|
}
|
|
152
185
|
|
|
@@ -166,9 +199,18 @@ std::string fs_error_message(const std::string &path, const char *op) {
|
|
|
166
199
|
return "EIO: failed to " + std::string(op) + " '" + path + "'";
|
|
167
200
|
}
|
|
168
201
|
|
|
202
|
+
// Defined below, once the app table is in scope. Posts `id` to the app's UI
|
|
203
|
+
// thread via webview_dispatch, so TS is entered on a later turn of the shell's
|
|
204
|
+
// own loop and never underneath a frame it is already inside.
|
|
205
|
+
void post_timer(int32_t app, int32_t id);
|
|
206
|
+
|
|
169
207
|
void job_finish(Job *j, int32_t status, std::string payload) {
|
|
170
208
|
j->data = std::move(payload);
|
|
171
209
|
j->status.store(status, std::memory_order_release);
|
|
210
|
+
// Nothing polls any more, so a finished job announces itself. Safe from a
|
|
211
|
+
// worker thread: webview_dispatch is the documented cross-thread hand-off,
|
|
212
|
+
// and it only queues — TS runs later, on the UI thread.
|
|
213
|
+
if (j->app >= 0) post_timer(j->app, TIMER_JOBS);
|
|
172
214
|
}
|
|
173
215
|
|
|
174
216
|
void fs_read_worker(Job *j, std::string path) {
|
|
@@ -614,18 +656,67 @@ void trampoline(const char *id, const char *req, void *arg) {
|
|
|
614
656
|
webview_return(a->w, a->cur_id.c_str(), status,
|
|
615
657
|
a->reply.empty() ? "null" : a->reply.c_str());
|
|
616
658
|
// wv_defer() treats a non-empty cur_id as "an invoke is in flight". Clearing
|
|
617
|
-
// it here means a defer from anywhere else — a
|
|
659
|
+
// it here means a defer from anywhere else — a timer, say — fails with -1
|
|
618
660
|
// instead of stealing this already-answered call's id.
|
|
619
661
|
a->cur_id.clear();
|
|
620
662
|
}
|
|
621
663
|
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
void
|
|
625
|
-
|
|
626
|
-
|
|
664
|
+
// The app index and the timer id, packed into the single void* that
|
|
665
|
+
// webview_dispatch carries.
|
|
666
|
+
void *pack_timer(int32_t app, int32_t id) {
|
|
667
|
+
uintptr_t packed = (static_cast<uintptr_t>(static_cast<uint32_t>(app)) << 32) |
|
|
668
|
+
static_cast<uint32_t>(id);
|
|
669
|
+
return reinterpret_cast<void *>(packed);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Runs on the UI thread, posted via webview_dispatch, so the TS it calls stays
|
|
673
|
+
// single-threaded — scriptc's runtime is NOT thread-safe.
|
|
674
|
+
//
|
|
675
|
+
// This is also the one place that guarantees the shell never re-enters TS from
|
|
676
|
+
// inside a frame TS is already in. Everything that wants to reach TS — a due
|
|
677
|
+
// timer, a finished file read, a dismissed dialog — goes through a dispatch
|
|
678
|
+
// and therefore lands at the top of a later turn, with no TS beneath it. That
|
|
679
|
+
// rule is invisible when broken: a violating host gets correct-looking results
|
|
680
|
+
// right up until it doesn't, so it is kept by construction, not by testing.
|
|
681
|
+
void timer_on_ui_thread(webview_t, void *arg) {
|
|
682
|
+
uintptr_t packed = reinterpret_cast<uintptr_t>(arg);
|
|
683
|
+
App *a = &g_apps[packed >> 32];
|
|
684
|
+
int32_t id = static_cast<int32_t>(static_cast<uint32_t>(packed & 0xffffffffu));
|
|
685
|
+
if (!a->used || !a->on_timer) return; // app quit between dispatch and delivery
|
|
627
686
|
a->seq++;
|
|
628
|
-
a->
|
|
687
|
+
a->on_timer(id, a->on_timer_ctx);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
void post_timer(int32_t app, int32_t id) {
|
|
691
|
+
if (app < 0 || app >= 8) return;
|
|
692
|
+
App *a = &g_apps[app];
|
|
693
|
+
if (!a->used || !a->w) return;
|
|
694
|
+
webview_dispatch(a->w, timer_on_ui_thread, pack_timer(app, id));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// The scheduler thread: sleep until the earliest timer is due, hand its id to
|
|
698
|
+
// the UI thread, repeat. It never touches TS and holds no TS state.
|
|
699
|
+
void scheduler_loop(App *a, int32_t h) {
|
|
700
|
+
std::unique_lock<std::mutex> lk(a->timers_mu);
|
|
701
|
+
while (a->scheduling.load()) {
|
|
702
|
+
if (a->timers.empty()) {
|
|
703
|
+
a->timers_cv.wait(lk);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
auto soonest = std::min_element(
|
|
707
|
+
a->timers.begin(), a->timers.end(),
|
|
708
|
+
[](const Timer &x, const Timer &y) { return x.due < y.due; });
|
|
709
|
+
auto due = soonest->due;
|
|
710
|
+
if (due > std::chrono::steady_clock::now()) {
|
|
711
|
+
a->timers_cv.wait_until(lk, due);
|
|
712
|
+
continue; // re-check: an earlier timer may have arrived meanwhile
|
|
713
|
+
}
|
|
714
|
+
int32_t id = soonest->id;
|
|
715
|
+
a->timers.erase(soonest);
|
|
716
|
+
lk.unlock();
|
|
717
|
+
post_timer(h, id);
|
|
718
|
+
lk.lock();
|
|
719
|
+
}
|
|
629
720
|
}
|
|
630
721
|
|
|
631
722
|
} // namespace
|
|
@@ -642,16 +733,16 @@ int32_t wv_create(int32_t debug) {
|
|
|
642
733
|
g_apps[i].binds.clear();
|
|
643
734
|
g_apps[i].on_invoke = nullptr;
|
|
644
735
|
g_apps[i].on_invoke_ctx = nullptr;
|
|
645
|
-
g_apps[i].
|
|
646
|
-
g_apps[i].
|
|
736
|
+
g_apps[i].on_timer = nullptr;
|
|
737
|
+
g_apps[i].on_timer_ctx = nullptr;
|
|
647
738
|
g_apps[i].req.clear();
|
|
648
739
|
g_apps[i].cur_id.clear();
|
|
649
740
|
g_apps[i].reply.clear();
|
|
650
741
|
g_apps[i].seq = 0;
|
|
651
742
|
g_apps[i].pending.clear();
|
|
652
743
|
g_apps[i].deferred = false;
|
|
653
|
-
g_apps[i].
|
|
654
|
-
g_apps[i].
|
|
744
|
+
g_apps[i].scheduling.store(false);
|
|
745
|
+
g_apps[i].timers.clear();
|
|
655
746
|
g_apps[i].w = w;
|
|
656
747
|
g_apps[i].used = true;
|
|
657
748
|
return i;
|
|
@@ -723,8 +814,8 @@ int32_t wv_reply(int32_t h, const uint8_t *p, size_t n) {
|
|
|
723
814
|
// call, and wv_run() is one such call for the app's whole life — so setTimeout
|
|
724
815
|
// and promise continuations in TS never fire while the window is open. These
|
|
725
816
|
// four functions supply the missing loop: TS may postpone an invoke's answer
|
|
726
|
-
// (wv_defer), answer it later (wv_resolve), and
|
|
727
|
-
// on the UI thread
|
|
817
|
+
// (wv_defer), answer it later (wv_resolve), and park a continuation with the
|
|
818
|
+
// shell to be called back on the UI thread when it comes due (wv_schedule).
|
|
728
819
|
|
|
729
820
|
// Postpone the answer to the invoke being handled right now. Returns a
|
|
730
821
|
// pending id to hand back to wv_resolve(), or -1 outside a bind callback.
|
|
@@ -759,43 +850,60 @@ int32_t wv_resolve(int32_t h, int32_t id, int32_t status) {
|
|
|
759
850
|
return 0;
|
|
760
851
|
}
|
|
761
852
|
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
|
|
853
|
+
// Ask the shell to call the retained timer handler with `id` after `ms`.
|
|
854
|
+
//
|
|
855
|
+
// This is the whole of scheduling: TS keeps the continuation, the shell keeps
|
|
856
|
+
// the clock. A zero delay is not a special case — it posts on the next turn of
|
|
857
|
+
// the loop, which is exactly what app.defer() wants, with no timer involved.
|
|
858
|
+
//
|
|
859
|
+
// An idle app now costs nothing at all: with no timers queued the scheduler
|
|
860
|
+
// thread blocks on a condition variable rather than waking every few
|
|
861
|
+
// milliseconds to find nothing to do.
|
|
862
|
+
int32_t wv_schedule(int32_t h, int32_t id, int32_t ms) {
|
|
765
863
|
App *a = app_at(h);
|
|
766
864
|
if (!a) return -1;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
865
|
+
|
|
866
|
+
// Zero delay skips the queue: there is nothing to wait for, and posting
|
|
867
|
+
// straight to the UI thread keeps a defer() chain as short as possible.
|
|
868
|
+
if (ms <= 0) {
|
|
869
|
+
post_timer(h, id);
|
|
870
|
+
return 0;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
{
|
|
874
|
+
std::lock_guard<std::mutex> lock(a->timers_mu);
|
|
875
|
+
a->timers.push_back(
|
|
876
|
+
Timer{id, std::chrono::steady_clock::now() +
|
|
877
|
+
std::chrono::milliseconds(ms)});
|
|
878
|
+
if (!a->scheduling.exchange(true)) {
|
|
879
|
+
a->scheduler = std::thread(scheduler_loop, a, h);
|
|
776
880
|
}
|
|
777
|
-
}
|
|
881
|
+
}
|
|
882
|
+
a->timers_cv.notify_one();
|
|
778
883
|
return 0;
|
|
779
884
|
}
|
|
780
885
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (!a->
|
|
785
|
-
|
|
786
|
-
|
|
886
|
+
// Stop the scheduler thread and drop any timers that never came due. Called on
|
|
887
|
+
// the way out of wv_run(), so nothing can reach TS after the window closes.
|
|
888
|
+
void stop_scheduler(App *a) {
|
|
889
|
+
if (!a->scheduling.exchange(false)) return;
|
|
890
|
+
a->timers_cv.notify_all();
|
|
891
|
+
if (a->scheduler.joinable()) a->scheduler.join();
|
|
892
|
+
std::lock_guard<std::mutex> lock(a->timers_mu);
|
|
893
|
+
a->timers.clear();
|
|
787
894
|
}
|
|
788
895
|
|
|
789
896
|
// ---- async file I/O ---------------------------------------------------------
|
|
790
897
|
//
|
|
791
898
|
// wv_fs_read/wv_fs_write start a worker thread and return immediately with a
|
|
792
|
-
// job id. TS
|
|
899
|
+
// job id. TS is woken with TIMER_JOBS when it finishes, reads wv_job_status()
|
|
900
|
+
// and drains the payload
|
|
793
901
|
// with wv_fs_byte() once the job is terminal. On failure the payload is the
|
|
794
902
|
// error message, so success and failure share one drain path.
|
|
795
903
|
|
|
796
904
|
int32_t wv_fs_read(int32_t h, const uint8_t *p, size_t n) {
|
|
797
905
|
if (!app_at(h)) return -1;
|
|
798
|
-
int32_t id = new_job();
|
|
906
|
+
int32_t id = new_job(h);
|
|
799
907
|
Job *j = job_at(id);
|
|
800
908
|
if (!j) return -1;
|
|
801
909
|
j->worker = std::thread(fs_read_worker, j, to_str(p, n));
|
|
@@ -805,7 +913,7 @@ int32_t wv_fs_read(int32_t h, const uint8_t *p, size_t n) {
|
|
|
805
913
|
int32_t wv_fs_write(int32_t h, const uint8_t *p, size_t n, const uint8_t *dp,
|
|
806
914
|
size_t dn) {
|
|
807
915
|
if (!app_at(h)) return -1;
|
|
808
|
-
int32_t id = new_job();
|
|
916
|
+
int32_t id = new_job(h);
|
|
809
917
|
Job *j = job_at(id);
|
|
810
918
|
if (!j) return -1;
|
|
811
919
|
j->worker = std::thread(fs_write_worker, j, to_str(p, n), to_str(dp, dn));
|
|
@@ -906,7 +1014,7 @@ int32_t wv_dialog(int32_t h, int32_t kind, int32_t flags, const uint8_t *tp,
|
|
|
906
1014
|
size_t nn, const uint8_t *fp, size_t fn) {
|
|
907
1015
|
App *a = app_at(h);
|
|
908
1016
|
if (!a) return -1;
|
|
909
|
-
int32_t id = new_job();
|
|
1017
|
+
int32_t id = new_job(h);
|
|
910
1018
|
Job *j = job_at(id);
|
|
911
1019
|
if (!j) return -1;
|
|
912
1020
|
|
|
@@ -994,12 +1102,13 @@ int32_t wv_on_invoke(int32_t h,
|
|
|
994
1102
|
return 0;
|
|
995
1103
|
}
|
|
996
1104
|
|
|
997
|
-
// Register the retained handler the
|
|
998
|
-
|
|
1105
|
+
// Register the retained handler the shell calls when a scheduled id comes due
|
|
1106
|
+
// (and with TIMER_JOBS when a file read or dialog finishes).
|
|
1107
|
+
int32_t wv_on_timer(int32_t h, void (*cb)(int32_t, void *), void *ctx) {
|
|
999
1108
|
App *a = app_at(h);
|
|
1000
1109
|
if (!a) return -1;
|
|
1001
|
-
a->
|
|
1002
|
-
a->
|
|
1110
|
+
a->on_timer = cb;
|
|
1111
|
+
a->on_timer_ctx = ctx;
|
|
1003
1112
|
return 0;
|
|
1004
1113
|
}
|
|
1005
1114
|
|
|
@@ -1009,11 +1118,10 @@ int32_t wv_run(int32_t h) {
|
|
|
1009
1118
|
if (!a) return -1;
|
|
1010
1119
|
int rc = webview_run(a->w);
|
|
1011
1120
|
// Nothing may call into TS once run() has returned.
|
|
1012
|
-
a
|
|
1013
|
-
if (a->ticker.joinable()) a->ticker.join();
|
|
1121
|
+
stop_scheduler(a);
|
|
1014
1122
|
jobs_join_all(); // nor may an in-flight read outlive the app
|
|
1015
1123
|
a->on_invoke = nullptr;
|
|
1016
|
-
a->
|
|
1124
|
+
a->on_timer = nullptr;
|
|
1017
1125
|
return rc;
|
|
1018
1126
|
}
|
|
1019
1127
|
|
|
@@ -1026,8 +1134,7 @@ int32_t wv_terminate(int32_t h) {
|
|
|
1026
1134
|
int32_t wv_destroy(int32_t h) {
|
|
1027
1135
|
App *a = app_at(h);
|
|
1028
1136
|
if (!a) return -1;
|
|
1029
|
-
a
|
|
1030
|
-
if (a->ticker.joinable()) a->ticker.join();
|
|
1137
|
+
stop_scheduler(a);
|
|
1031
1138
|
webview_destroy(a->w);
|
|
1032
1139
|
a->used = false;
|
|
1033
1140
|
a->w = nullptr;
|