waterui-ffi 0.5.1

FFI bindings for the WaterUI cross-platform UI framework
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
//! `WebView` component FFI bindings.
//!
//! This module provides FFI bindings for the `WebView` component, allowing native backends
//! to create and control web views.

use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cell::Cell;
use core::fmt::Write as _;

use crate::array::{WuiArray, WuiData};
use crate::closure::WuiFn;
use crate::reactive::WuiComputed;
use crate::{IntoFFI, IntoRust, WuiEnv, WuiStr};
use base64::Engine;
use cookie::Cookie;
use nami::{Signal, SignalExt};
use suiteki::Str;
use waterui_webview::{
    AssetResponse, AssetServer, BackendEvent, CustomWebViewController, JsReply,
    ScriptInjectionTime, Url, WatcherGuard, WatcherSet, WebView, WebViewConfig, WebViewController,
    WebViewError, WebViewEvent, WebViewHandle,
};

// =============================================================================
// Script Injection Time FFI
// =============================================================================

/// FFI representation of script injection timing.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WuiScriptInjectionTime {
    /// Inject at the start of document loading, before the DOM is constructed.
    DocumentStart = 0,
    /// Inject after the document has finished loading.
    DocumentEnd = 1,
}

impl IntoFFI for ScriptInjectionTime {
    type FFI = WuiScriptInjectionTime;
    fn into_ffi(self) -> Self::FFI {
        match self {
            Self::DocumentStart => WuiScriptInjectionTime::DocumentStart,
            Self::DocumentEnd => WuiScriptInjectionTime::DocumentEnd,
        }
    }
}

impl IntoRust for WuiScriptInjectionTime {
    type Rust = ScriptInjectionTime;
    unsafe fn into_rust(self) -> Self::Rust {
        match self {
            Self::DocumentStart => ScriptInjectionTime::DocumentStart,
            Self::DocumentEnd => ScriptInjectionTime::DocumentEnd,
        }
    }
}

// =============================================================================
// Event FFI Types
// =============================================================================

/// FFI representation of `WebView` event types.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WuiWebViewEventType {
    /// The web view is about to navigate to a new URL.
    WillNavigate = 1,
    /// The web view is loading content.
    Loading = 2,
    /// The web view has finished loading.
    Loaded = 3,
    /// A redirect occurred.
    Redirect = 4,
    /// An SSL error occurred.
    SslError = 5,
    /// A general error occurred.
    Error = 6,
    /// Navigation state changed.
    StateChanged = 7,
}

/// FFI representation of a `WebView` event.
#[repr(C)]
#[derive(Debug)]
pub struct WuiWebViewEvent {
    /// The type of event.
    pub event_type: WuiWebViewEventType,
    /// URL associated with the event (for `WillNavigate`, `SslError`, Error, Redirect from).
    pub url: *mut WuiStr,
    /// Second URL (for Redirect to).
    pub url2: *mut WuiStr,
    /// Error/message string (for `SslError`, Error).
    pub message: *mut WuiStr,
    /// Loading progress (0.0 to 1.0, for Loading event).
    pub progress: f32,
    /// Whether can navigate back (for `StateChanged`).
    pub can_go_back: bool,
    /// Whether can navigate forward (for `StateChanged`).
    pub can_go_forward: bool,
}

/// Converts a URL that a backend reported alongside an event.
///
/// Backends report whatever the engine actually navigated to, which routinely
/// includes `about:blank`, `data:` documents, `blob:` URLs and `file://` paths.
/// This conversion is therefore total: it must not reject any of them, and it
/// must never abort the process — which it previously did, because `Url::parse`
/// accepts only web URLs and this unwrapped its `None`.
fn parse_url(s: &Str) -> Url {
    s.as_str().parse().unwrap_or_else(|error| {
        panic!(
            "WebView backend emitted an unparseable URL {:?}: {error}",
            s.as_str()
        )
    })
}

/// Takes ownership of a string field out of a `WuiWebViewEvent`.
///
/// # Safety
///
/// `value` must be an owning `WuiStr` the backend attached to the event, and may
/// be taken only once.
// SAFETY: the backend fills the string fields its event type defines with owning
// `WuiStr` handles, and each arm reads only its own fields, exactly once.
unsafe fn take_event_string(value: *mut WuiStr) -> Str {
    // SAFETY: the caller contract makes `value` an owning pointer from the matching
    // FFI constructor, so reclaiming the box frees it exactly once.
    let value = unsafe { Box::from_raw(value) };
    // SAFETY: the caller contract makes `value` a valid owning event pointer; it is
    // read once and converted here.
    unsafe { (*value).into_rust() }
}

impl IntoRust for WuiWebViewEvent {
    type Rust = BackendEvent;
    unsafe fn into_rust(self) -> Self::Rust {
        match self.event_type {
            WuiWebViewEventType::WillNavigate => BackendEvent::Event(WebViewEvent::WillNavigate {
                // SAFETY: the backend fills the string fields its event type defines
                // with owning `WuiStr` handles, and each arm reads only its
                // own fields, exactly once.
                url: parse_url(&unsafe { take_event_string(self.url) }),
            }),
            WuiWebViewEventType::Loading => BackendEvent::Event(WebViewEvent::Loading {
                progress: self.progress,
            }),
            WuiWebViewEventType::Loaded => BackendEvent::Event(WebViewEvent::Loaded),
            WuiWebViewEventType::Redirect => BackendEvent::Event(WebViewEvent::Redirect {
                // SAFETY: the backend fills the string fields its event type defines
                // with owning `WuiStr` handles, and each arm reads only its
                // own fields, exactly once.
                from: parse_url(&unsafe { take_event_string(self.url) }),
                // SAFETY: the backend fills the string fields its event type defines
                // with owning `WuiStr` handles, and each arm reads only its
                // own fields, exactly once.
                to: parse_url(&unsafe { take_event_string(self.url2) }),
            }),
            WuiWebViewEventType::SslError => {
                BackendEvent::Event(WebViewEvent::Error(WebViewError::Ssl {
                    // SAFETY: the backend fills the string fields its event type defines
                    // with owning `WuiStr` handles, and each arm reads only its
                    // own fields, exactly once.
                    url: parse_url(&unsafe { take_event_string(self.url) }),
                    // SAFETY: the backend fills the string fields its event type defines
                    // with owning `WuiStr` handles, and each arm reads only its
                    // own fields, exactly once.
                    message: unsafe { take_event_string(self.message) },
                }))
            }
            WuiWebViewEventType::Error => {
                BackendEvent::Event(WebViewEvent::Error(WebViewError::LoadFailed(unsafe {
                    // SAFETY: the backend fills the string fields its event type defines
                    // with owning `WuiStr` handles, and each arm reads only its
                    // own fields, exactly once.
                    take_event_string(self.message)
                })))
            }
            WuiWebViewEventType::StateChanged => BackendEvent::NavigationState {
                can_go_back: self.can_go_back,
                can_go_forward: self.can_go_forward,
            },
        }
    }
}

// =============================================================================
// WebViewHandle FFI
// =============================================================================

/// Callback for JavaScript execution results.
#[repr(C)]
#[derive(Debug)]
pub struct WuiJsCallback {
    /// Opaque callback state consumed by `call`.
    pub data: *mut (),
    /// One-shot completion. `success=true` means `result` is the value;
    /// `false` means it is the error. Native must invoke it exactly once.
    pub call: unsafe extern "C" fn(data: *mut (), success: bool, result: WuiStr),
}

/// One-shot completion callback for an owned string result.
#[repr(C)]
#[derive(Debug)]
pub struct WuiStringCallback {
    /// Opaque callback state consumed by `call`.
    pub data: *mut (),
    /// Completes the operation and transfers ownership of `result`.
    pub call: unsafe extern "C" fn(data: *mut (), result: WuiStr),
}

/// Which of the bridge's two channels a handler's reply crosses on.
///
/// The page sees the difference — a value it can use, or a `Uint8Array` — so the
/// answer has to survive the trip out. It used to be dropped here, and every
/// reply reached the page as base64 text.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WuiJsReplyKind {
    /// The bytes are a serialized JSON value; the page receives the value.
    Json = 0,
    /// The bytes are opaque; the page receives a `Uint8Array`.
    Bytes = 1,
}

/// One-shot reply to one bridge call.
///
/// Distinct from [`WuiJsCallback`], which completes a `run_javascript` call and
/// has no channel to choose.
#[repr(C)]
#[derive(Debug)]
pub struct WuiWebViewReply {
    /// Opaque callback state consumed by `call`.
    pub data: *mut (),
    /// One-shot completion. `success=true` means `result` is base64 of the
    /// handler's output and `kind` says how to read it; `false` means `result`
    /// is the error message and `kind` is ignored. Native must invoke it
    /// exactly once.
    pub call:
        unsafe extern "C" fn(data: *mut (), success: bool, kind: WuiJsReplyKind, result: WuiStr),
}

/// Message payload emitted from JavaScript to a native-registered handler.
///
/// `payload_base64` is base64-encoded bytes from JavaScript.
/// `reply` must be called exactly once for request/response semantics.
#[repr(C)]
#[derive(Debug)]
pub struct WuiWebViewMessage {
    /// Base64-encoded message bytes sent from JavaScript.
    pub payload_base64: WuiStr,
    /// One-shot callback used to send a base64-encoded reply back to JavaScript.
    pub reply: WuiWebViewReply,
}

/// FFI representation of a `WebView` handle with function pointers.
///
/// Native backends create this struct with function pointers to their implementation.
#[repr(C)]
#[derive(Debug)]
pub struct WuiWebViewHandle {
    /// Opaque pointer to native `WebView` wrapper.
    pub data: *mut (),

    // Navigation
    /// Navigate back in history.
    pub go_back: unsafe extern "C" fn(*mut ()),
    /// Navigate forward in history.
    pub go_forward: unsafe extern "C" fn(*mut ()),
    /// Navigate to URL.
    pub go_to: unsafe extern "C" fn(*mut (), WuiStr),
    /// Stop loading.
    pub stop: unsafe extern "C" fn(*mut ()),
    /// Refresh/reload page.
    pub refresh: unsafe extern "C" fn(*mut ()),

    // State queries
    /// Returns whether can go back.
    pub can_go_back: unsafe extern "C" fn(*const ()) -> bool,
    /// Returns whether can go forward.
    pub can_go_forward: unsafe extern "C" fn(*const ()) -> bool,

    // Configuration
    /// Set user agent string.
    pub set_user_agent: unsafe extern "C" fn(*mut (), WuiStr),

    /// Takes ownership of a reactive redirect-following signal.
    pub set_redirects_enabled: unsafe extern "C" fn(*mut (), *mut WuiComputed<bool>),

    // Script injection
    /// Inject a script that runs on every page load.
    ///
    /// The first string is a key naming the script: injecting again under a key
    /// already in use replaces that script rather than adding a second copy.
    /// The mirrored-state seed relies on it, being re-rendered and replaced
    /// before every navigation.
    pub inject_script: unsafe extern "C" fn(*mut (), WuiStr, WuiStr, WuiScriptInjectionTime),

    // Event watching
    /// Set event callback. Native calls this when events occur.
    pub watch: unsafe extern "C" fn(*mut (), WuiFn<WuiWebViewEvent>),

    // JS-to-native messaging
    /// Register a named handler that can be called from JavaScript.
    ///
    /// Backends are expected to provide a Promise-based API where possible:
    /// JavaScript sends `payload_base64` and receives a base64 reply.
    pub add_handler: Option<unsafe extern "C" fn(*mut (), WuiStr, WuiFn<WuiWebViewMessage>)>,
    /// Removes a previously added handler.
    pub remove_handler: Option<unsafe extern "C" fn(*mut (), WuiStr)>,
    /// Restricts which documents may reach the bridge.
    ///
    /// Receives newline-separated rule tokens: `*` for every origin, `file:` for
    /// any local file, and otherwise an exact `scheme://host[:port]` to compare
    /// the calling frame's origin against. **The empty string denies every
    /// document** — that is what the default policy resolves to when the view is
    /// opened at a URL with no origin — and a backend that treats it as `*`
    /// hands every registered handler to whatever the view navigates to.
    pub set_bridge_origins: Option<unsafe extern "C" fn(*mut (), WuiStr)>,

    // Cookies
    /// Sets a cookie for the web view. The string is a Set-Cookie header value.
    pub set_cookie: Option<unsafe extern "C" fn(*mut (), WuiStr)>,
    /// Gets cookies asynchronously as newline-separated Set-Cookie strings.
    pub get_cookies: Option<unsafe extern "C" fn(*const (), WuiStringCallback)>,

    // JavaScript
    /// Execute JavaScript on the currently loaded page and call callback with result.
    ///
    /// The raw path: the value comes back however the engine marshals it.
    pub run_javascript: unsafe extern "C" fn(*mut (), WuiStr, WuiJsCallback),

    /// Run the string as the body of an `async` function and **await** the
    /// promise it returns, then call the callback with the resolved value.
    ///
    /// Every typed evaluation goes through here, because the shared wrapper in
    /// `js/eval.js` is `async`: an engine API that does not await — `WebKit`'s
    /// `evaluateJavaScript`, `webkit_web_view_evaluate_javascript`,
    /// `WebView.evaluateJavascript` — hands back the promise object instead of
    /// the JSON envelope, and every `eval!`/`exec!` fails while mirrored state
    /// silently stops reaching the page. Use `callAsyncJavaScript`,
    /// `webkit_web_view_call_async_javascript_function`, `Runtime.evaluate`
    /// with `awaitPromise`, or resolve the promise in JavaScript and report the
    /// result through the backend's own bridge.
    pub call_async_javascript: unsafe extern "C" fn(*mut (), WuiStr, WuiJsCallback),

    // Asset origin
    /// The origin this view serves bundled assets under, when it was created
    /// with a `WuiAssetServer`: the engine's own answer — `waterui://localhost`
    /// on the `WebKit` family and CEF, `https://waterui.localhost` where only
    /// `https` can be a secure context. An empty string means the view was
    /// opened without an asset server, and a `None` entry point means the
    /// backend has no interception facility to stand one on — which
    /// `WebView::open_assets` reports as the configuration error it is.
    pub asset_origin: Option<unsafe extern "C" fn(*const ()) -> WuiStr>,

    // Cleanup
    /// Release the native handle.
    pub drop: unsafe extern "C" fn(*mut ()),
}

/// Rust wrapper that implements `WebViewHandle` by delegating to FFI function pointers.
///
/// This struct is public so that the FFI layer can downcast `AnyWebViewHandle`
/// to extract the native webview pointer for rendering.
pub struct FfiWebViewHandle {
    ffi: WuiWebViewHandle,
    /// Rust-side watchers, so the single `WuiFn` trampoline installed on the
    /// native handle can fan out to all of them and each can be removed.
    watchers: WatcherSet<BackendEvent>,
    watcher_installed: Cell<bool>,
}

impl core::fmt::Debug for FfiWebViewHandle {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("FfiWebViewHandle")
            .field("ffi", &self.ffi)
            .field("watcher_installed", &self.watcher_installed)
            .finish_non_exhaustive()
    }
}

impl FfiWebViewHandle {
    pub(crate) fn new(ffi: WuiWebViewHandle) -> Self {
        Self {
            ffi,
            watchers: WatcherSet::new(),
            watcher_installed: Cell::new(false),
        }
    }

    /// Returns the raw pointer to the native `WebView` wrapper.
    ///
    /// This pointer points to the native `WebViewWrapper` (Swift/Kotlin)
    /// which contains the underlying `WKWebView` or Android `WebView`.
    pub const fn native_ptr(&self) -> *mut () {
        self.ffi.data
    }
}

impl Drop for FfiWebViewHandle {
    fn drop(&mut self) {
        unsafe {
            // SAFETY: `ffi.data` and this function pointer were registered together
            // by the backend for this controller, which is alive for as long
            // as `self` is.
            (self.ffi.drop)(self.ffi.data);
        }
    }
}

impl WebViewHandle for FfiWebViewHandle {
    fn go_back(&self) {
        // SAFETY: `ffi.data` and this function pointer were registered together by
        // the backend for this controller, which is alive for as long as
        // `self` is.
        unsafe { (self.ffi.go_back)(self.ffi.data) }
    }

    fn go_forward(&self) {
        // SAFETY: `ffi.data` and this function pointer were registered together by
        // the backend for this controller, which is alive for as long as
        // `self` is.
        unsafe { (self.ffi.go_forward)(self.ffi.data) }
    }

    fn go_to(&self, url: &Url) {
        let owned_url = Str::from(url.as_str().to_string());
        // SAFETY: `ffi.data` and this function pointer were registered together by
        // the backend for this controller, which is alive for as long as
        // `self` is.
        unsafe { (self.ffi.go_to)(self.ffi.data, owned_url.into_ffi()) }
    }

    fn stop(&self) {
        // SAFETY: `ffi.data` and this function pointer were registered together by
        // the backend for this controller, which is alive for as long as
        // `self` is.
        unsafe { (self.ffi.stop)(self.ffi.data) }
    }

    fn refresh(&self) {
        // SAFETY: `ffi.data` and this function pointer were registered together by
        // the backend for this controller, which is alive for as long as
        // `self` is.
        unsafe { (self.ffi.refresh)(self.ffi.data) }
    }

    fn can_go_back(&self) -> bool {
        // SAFETY: `ffi.data` and this function pointer were registered together by
        // the backend for this controller, which is alive for as long as
        // `self` is.
        unsafe { (self.ffi.can_go_back)(self.ffi.data) }
    }

    fn can_go_forward(&self) -> bool {
        // SAFETY: `ffi.data` and this function pointer were registered together by
        // the backend for this controller, which is alive for as long as
        // `self` is.
        unsafe { (self.ffi.can_go_forward)(self.ffi.data) }
    }

    fn set_user_agent(&self, user_agent: &str) {
        let owned_ua = Str::from(user_agent.to_string());
        // SAFETY: `ffi.data` and this function pointer were registered together by
        // the backend for this controller, which is alive for as long as
        // `self` is.
        unsafe { (self.ffi.set_user_agent)(self.ffi.data, owned_ua.into_ffi()) }
    }

    fn set_redirects_enabled(&self, enabled: impl Signal<Output = bool>) {
        unsafe {
            // SAFETY: `ffi.data` and this function pointer were registered together
            // by the backend for this controller, which is alive for as long
            // as `self` is.
            (self.ffi.set_redirects_enabled)(self.ffi.data, enabled.computed().into_ffi());
        }
    }

    fn inject_script(&self, key: &str, script: &str, time: ScriptInjectionTime) {
        let owned_key = Str::from(key.to_string());
        let owned_script = Str::from(script.to_string());
        // SAFETY: `ffi.data` and this function pointer were registered together by
        // the backend for this controller, which is alive for as long as
        // `self` is.
        unsafe {
            (self.ffi.inject_script)(
                self.ffi.data,
                owned_key.into_ffi(),
                owned_script.into_ffi(),
                time.into_ffi(),
            );
        }
    }

    fn watch(&self, f: impl Fn(BackendEvent) + 'static) -> WatcherGuard {
        let guard = self.watchers.insert(f);

        if self.watcher_installed.replace(true) {
            return guard;
        }

        let watchers = self.watchers.clone();
        // Wrap a single Rust closure in a WuiFn that converts FFI events to Rust events
        // and fan-outs to all registered watchers.
        let callback = WuiFn::from(move |ffi_event: WuiWebViewEvent| {
            // SAFETY: the caller contract makes `ffi_event` an owning handle from
            // the matching FFI constructor; it is consumed here and not
            // observed again.
            let event = unsafe { ffi_event.into_rust() };
            watchers.emit(&event);
        });

        // SAFETY: `ffi.data` and this function pointer were registered together by
        // the backend for this controller, which is alive for as long as
        // `self` is.
        unsafe { (self.ffi.watch)(self.ffi.data, callback) };
        guard
    }

    fn add_handler(&self, name: &str, handler: Box<waterui_webview::ScriptMessageHandler>) {
        let add_handler = self
            .ffi
            .add_handler
            .expect("WebView backend must implement `add_handler`");

        let engine = base64::engine::general_purpose::STANDARD;
        let name = Str::from(name.to_string());
        let callback = WuiFn::from(move |msg: WuiWebViewMessage| {
            // SAFETY: the caller contract makes `payload_base64` an owning handle
            // from the matching FFI constructor; it is consumed here and not
            // observed again.
            let payload_b64: Str = unsafe { msg.payload_base64.into_rust() };
            let payload = match engine.decode(payload_b64.as_str()) {
                Ok(bytes) => bytes,
                Err(err) => {
                    let message = Str::from(err.to_string());
                    // SAFETY: the backend registered `reply.call` with `reply.data`
                    // for this message; the early return below makes this the only
                    // reply sent.
                    unsafe {
                        (msg.reply.call)(
                            msg.reply.data,
                            false,
                            WuiJsReplyKind::Json,
                            message.into_ffi(),
                        );
                    };
                    return;
                }
            };

            // The reply callback is one-shot and may be invoked later, which is
            // what lets a handler be asynchronous without the backend changing.
            let future = handler(&payload);
            let reply_call = msg.reply.call;
            let reply_data = msg.reply.data;
            executor_core::spawn_local(async move {
                let engine = base64::engine::general_purpose::STANDARD;
                let (success, kind, payload) = match future.await {
                    Ok(JsReply::Json(json)) => (true, WuiJsReplyKind::Json, engine.encode(json)),
                    Ok(JsReply::Bytes(bytes)) => {
                        (true, WuiJsReplyKind::Bytes, engine.encode(bytes))
                    }
                    Err(message) => (false, WuiJsReplyKind::Json, message),
                };
                // SAFETY: as above — the failure path returned early, so this is
                // the single reply for this message.
                unsafe { (reply_call)(reply_data, success, kind, Str::from(payload).into_ffi()) };
            })
            .detach();
        });

        // SAFETY: `add_handler` and `ffi.data` were registered together for this
        // controller, which outlives the call; the name and callback are passed by
        // value into backend ownership.
        unsafe { add_handler(self.ffi.data, name.into_ffi(), callback) }
    }

    fn set_bridge_origins(&self, policy: waterui_webview::OriginPolicy) {
        // Enforced natively by the backend: it is the only side that can
        // authenticate the frame a call arrived from. The policy is handed over
        // as its URI patterns, which is what the platform filters accept.
        let Some(set) = self.ffi.set_bridge_origins else {
            return;
        };
        // The wire form keeps deny-all (`""`) distinct from allow-all (`"*"`).
        // Collapsing the two is what made a view opened at a `file:` URL — whose
        // default policy admits nothing, because a local document has no origin
        // to compare — hand every handler to whatever it navigated to next.
        let patterns = policy.wire();
        // SAFETY: `set` and `ffi.data` come from the same registration.
        // SAFETY: as for `add_handler` — same controller, same registration.
        unsafe { set(self.ffi.data, patterns.into_ffi()) }
    }

    fn remove_handler(&self, name: &str) {
        let remove_handler = self
            .ffi
            .remove_handler
            .expect("WebView backend must implement `remove_handler`");

        let name = Str::from(name.to_string());
        // SAFETY: as for `add_handler` — same controller, same registration.
        unsafe { remove_handler(self.ffi.data, name.into_ffi()) }
    }

    fn set_cookie(&self, cookie: Cookie<'static>) {
        let set_cookie = self
            .ffi
            .set_cookie
            .expect("WebView backend must implement `set_cookie`");
        let cookie = Str::from(cookie.to_string());
        // SAFETY: as for `add_handler` — same controller, same registration.
        unsafe { set_cookie(self.ffi.data, cookie.into_ffi()) }
    }

    #[expect(
        clippy::future_not_send,
        reason = "WaterUI webview bridge futures resolve on the main-thread local executor and carry non-`Send` `Str` payloads by design"
    )]
    fn get_cookies(&self) -> impl core::future::Future<Output = Vec<Cookie<'static>>> {
        unsafe extern "C" fn cookies_callback(data: *mut (), result: WuiStr) {
            // SAFETY: `data` is the sender this callback was registered with, boxed
            // by the caller below; the backend invokes the callback once, so the box
            // is reclaimed once.
            let sender = unsafe { Box::from_raw(data.cast::<async_channel::Sender<Str>>()) };
            // SAFETY: the caller contract makes `result` an owning handle from the
            // matching FFI constructor; it is consumed here and not observed
            // again.
            let result = unsafe { result.into_rust() };
            let _ = sender.try_send(result);
        }

        let get_cookies = self
            .ffi
            .get_cookies
            .expect("WebView backend must implement `get_cookies`");
        let (sender, receiver) = async_channel::bounded::<Str>(1);
        let callback_data = Box::into_raw(Box::new(sender)).cast::<()>();

        // SAFETY: `get_cookies` and `ffi.data` come from the same registration, and
        // the callback below owns the boxed sender it will reclaim.
        unsafe {
            get_cookies(
                self.ffi.data.cast_const(),
                WuiStringCallback {
                    data: callback_data,
                    call: cookies_callback,
                },
            );
        }

        async move {
            // A web view torn down while the query was in flight drops the
            // callback without invoking it; that is a teardown race, not a
            // reason to suspend the caller's task forever.
            let Ok(text) = receiver.recv().await else {
                return Vec::new();
            };
            text.as_str()
                .lines()
                .filter_map(|line| match Cookie::parse(line.to_string()) {
                    Ok(cookie) => Some(cookie.into_owned()),
                    Err(error) => {
                        // The store holds whatever the pages put there, so one
                        // unparseable entry must not take down the app inside a
                        // getter.
                        tracing::warn!(%error, "skipping a cookie the web view could not parse");
                        None
                    }
                })
                .collect()
        }
    }

    #[expect(
        clippy::future_not_send,
        reason = "WaterUI webview bridge futures resolve on the main-thread local executor and carry non-`Send` `Str` payloads by design"
    )]
    fn run_javascript(&self, script: &str) -> impl core::future::Future<Output = Result<Str, Str>> {
        // SAFETY: the two come from the same registration; `evaluate_through`
        // documents what it needs of them.
        unsafe { self.evaluate_through(self.ffi.run_javascript, script) }
    }

    #[expect(
        clippy::future_not_send,
        reason = "WaterUI webview bridge futures resolve on the main-thread local executor and carry non-`Send` `Str` payloads by design"
    )]
    fn call_async_javascript(
        &self,
        body: &str,
    ) -> impl core::future::Future<Output = Result<Str, Str>> {
        // SAFETY: as above.
        unsafe { self.evaluate_through(self.ffi.call_async_javascript, body) }
    }

    fn asset_origin(&self) -> Option<Url> {
        let asset_origin = self.ffi.asset_origin?;
        // SAFETY: `asset_origin` and `ffi.data` come from the same registration,
        // and the returned `WuiStr` is an owning handle consumed here once.
        let origin: Str = unsafe { asset_origin(self.ffi.data.cast_const()).into_rust() };
        if origin.is_empty() {
            return None;
        }
        Some(parse_url(&origin))
    }
}

impl FfiWebViewHandle {
    /// Drives one of the two evaluation entry points, which differ only in
    /// whether the backend awaits the promise the source produces.
    ///
    /// # Safety
    ///
    /// `evaluate` must be a function pointer from the same registration as
    /// `self.ffi.data`.
    #[expect(
        clippy::future_not_send,
        reason = "WaterUI webview bridge futures resolve on the main-thread local executor and carry non-`Send` `Str` payloads by design"
    )]
    unsafe fn evaluate_through(
        &self,
        evaluate: unsafe extern "C" fn(*mut (), WuiStr, WuiJsCallback),
        source: &str,
    ) -> impl core::future::Future<Output = Result<Str, Str>> {
        unsafe extern "C" fn js_callback_trampoline(data: *mut (), success: bool, result: WuiStr) {
            let sender =
                // SAFETY: as for the cookie callback — `data` is the boxed sender
                // this callback was registered with, invoked once.
                unsafe { Box::from_raw(data.cast::<async_channel::Sender<Result<Str, Str>>>()) };
            // SAFETY: the caller contract makes `result` an owning handle from the
            // matching FFI constructor; it is consumed here and not observed
            // again.
            let result = unsafe { result.into_rust() };
            let _ = sender.try_send(if success { Ok(result) } else { Err(result) });
        }

        let (sender, receiver) = async_channel::bounded::<Result<Str, Str>>(1);
        let callback_data = Box::into_raw(Box::new(sender)).cast::<()>();

        let ffi_callback = WuiJsCallback {
            data: callback_data,
            call: js_callback_trampoline,
        };

        let owned_source = Str::from(source.to_string());
        unsafe {
            // SAFETY: the caller guarantees `evaluate` and `ffi.data` come from
            // the same registration, and that controller is alive for as long as
            // `self` is.
            evaluate(self.ffi.data, owned_source.into_ffi(), ffi_callback);
        }

        async move {
            // A backend that is torn down mid-evaluation drops the callback
            // without invoking it. Reporting that as an error keeps the caller's
            // future resolving: awaiting a channel that will never receive left
            // the task suspended forever, holding everything it had captured.
            receiver.recv().await.unwrap_or_else(|_| {
                Err(Str::from_static(
                    "the web view was torn down before the script completed",
                ))
            })
        }
    }
}

// =============================================================================
// WebView Raw View
// =============================================================================

// =============================================================================
// Shared JavaScript bridge
// =============================================================================

/// One parsed `waterui.invoke(...)` request.
#[repr(C)]
#[derive(Debug)]
pub struct WuiBridgeRequest {
    /// Whether the envelope parsed. When false every other field is empty and the
    /// call must be ignored.
    pub ok: bool,
    /// Correlates the reply with the page's pending promise.
    pub id: u64,
    /// The handler name the page asked for.
    pub name: WuiStr,
    /// The payload, base64-encoded to match `WuiWebViewMessage`.
    pub payload_base64: WuiStr,
}

/// Returns the bridge script every backend injects at document start.
///
/// Backends that route messages themselves — the Apple backend keeps its handler
/// table in Swift — use this together with
/// [`waterui_webview_parse_bridge_request`] and
/// [`waterui_webview_bridge_reply_script`], so the envelope format stays defined
/// only in `waterui_webview::bridge`.
#[unsafe(no_mangle)]
pub extern "C" fn waterui_webview_bridge_script() -> WuiStr {
    Str::from_static(waterui_webview::DOCUMENT_START_SCRIPT).into_ffi()
}

/// Parses one envelope produced by the bridge script.
///
/// # Safety
///
/// `envelope` must be an owning `WuiStr`; it is consumed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_webview_parse_bridge_request(
    envelope: WuiStr,
) -> WuiBridgeRequest {
    // SAFETY: the caller contract makes `envelope` an owning handle from the
    // matching FFI constructor; it is consumed here and not observed again.
    let envelope: Str = unsafe { envelope.into_rust() };
    match waterui_webview::bridge::Request::parse(envelope.as_str()) {
        Ok(request) => WuiBridgeRequest {
            ok: true,
            id: request.id,
            name: Str::from(request.name).into_ffi(),
            payload_base64: Str::from(
                base64::engine::general_purpose::STANDARD.encode(&request.payload),
            )
            .into_ffi(),
        },
        Err(error) => {
            tracing::warn!(%error, "page script sent a malformed WaterUI bridge request");
            WuiBridgeRequest {
                ok: false,
                id: 0,
                name: Str::from_static("").into_ffi(),
                payload_base64: Str::from_static("").into_ffi(),
            }
        }
    }
}

/// Renders the JavaScript that settles one bridge call.
///
/// # Safety
///
/// `payload_base64` must be an owning `WuiStr`; it is consumed. On success it is
/// base64 handler output and `kind` says whether those bytes are a JSON value or
/// opaque; otherwise it is an error message and `kind` is ignored.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_webview_bridge_reply_script(
    id: u64,
    success: bool,
    kind: WuiJsReplyKind,
    payload_base64: WuiStr,
) -> WuiStr {
    // SAFETY: the caller contract makes `payload_base64` an owning handle from the
    // matching FFI constructor; it is consumed here and not observed again.
    let payload: Str = unsafe { payload_base64.into_rust() };
    let reply = if success {
        match base64::engine::general_purpose::STANDARD.decode(payload.as_str()) {
            Ok(bytes) => match kind {
                WuiJsReplyKind::Json => waterui_webview::bridge::Reply::Json(bytes),
                WuiJsReplyKind::Bytes => waterui_webview::bridge::Reply::Bytes(bytes),
            },
            Err(error) => waterui_webview::bridge::Reply::failure(&error),
        }
    } else {
        waterui_webview::bridge::Reply::failure(&payload.as_str())
    };
    Str::from(reply.resolve_script(id)).into_ffi()
}

opaque!(WuiWebView, WebView);
ffi_view!(WebView, *mut WuiWebView, webview, any());

/// Gets the native handle pointer from a `WebView`.
///
/// Returns the opaque pointer to the native `WebView` wrapper (Swift/Kotlin).
/// This pointer can be used by native backends to access the underlying
/// `WKWebView` or Android `WebView`.
///
/// # Safety
///
/// - The caller must ensure that `webview` is a valid pointer to a `WuiWebView`.
/// - The `WebView` must have been created via the FFI `WebViewController` (i.e., the handle
///   must be an `FfiWebViewHandle`). This is guaranteed when the native backend properly
///   installed the `WebViewController` via `waterui_env_install_webview_controller`.
///
/// # Panics
///
/// Panics if the `WebView`'s handle was not created via the FFI `WebViewController`
/// (i.e., it does not downcast to `FfiWebViewHandle`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_webview_native_handle(webview: *mut WuiWebView) -> *mut () {
    // SAFETY: the caller contract requires `webview` to be a valid handle that stays
    // alive for this call; it is only borrowed.
    unsafe {
        let webview = crate::borrow_ffi(webview);
        let handle = webview.0.handle();
        let ffi_handle = handle.downcast_ref::<FfiWebViewHandle>().unwrap_or_else(|| {
            panic!(
                "waterui_webview_native_handle requires a WebView created by the backend-installed FFI WebViewController"
            )
        });
        ffi_handle.native_ptr()
    }
}

// =============================================================================
// Asset server FFI
// =============================================================================

/// The [`AssetServer`] a native web view owns.
///
/// `FfiWebViewController` boxes the server a `WebView` was opened with and hands
/// the pointer to the backend's [`WuiCreateWebViewFn`] inside
/// [`WuiWebViewConfig`]. The backend holds it for the life of the native view,
/// answers the engine's interception facility through
/// [`waterui_webview_asset_server_respond`], and frees it with
/// [`waterui_webview_asset_server_free`] when the view dies.
pub struct WuiAssetServer {
    pub(crate) server: AssetServer,
}

impl core::fmt::Debug for WuiAssetServer {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("WuiAssetServer").finish_non_exhaustive()
    }
}

impl IntoFFI for AssetServer {
    type FFI = *mut WuiAssetServer;
    fn into_ffi(self) -> Self::FFI {
        Box::into_raw(Box::new(WuiAssetServer { server: self }))
    }
}

impl IntoFFI for Option<AssetServer> {
    type FFI = *mut WuiAssetServer;
    fn into_ffi(self) -> Self::FFI {
        self.map_or_else(core::ptr::null_mut, IntoFFI::into_ffi)
    }
}

/// The creation-time inputs a native view is opened with.
///
/// Engines register their interception facility while the view is constructed,
/// so the asset server arrives here rather than through a handle method that
/// could only run after the fact.
#[repr(C)]
#[derive(Debug)]
pub struct WuiWebViewConfig {
    /// The asset server the view's local asset origin answers through, or null
    /// when the view serves no bundled assets. Ownership passes to the native
    /// view; it frees the pointer with `waterui_webview_asset_server_free`.
    pub asset_server: *mut WuiAssetServer,
}

/// What the asset origin answered — the FFI shape of [`AssetResponse`].
#[repr(C)]
#[derive(Debug)]
pub struct WuiAssetResponse {
    /// The HTTP status code.
    pub status: u16,
    /// The response headers as `"Name: value"` lines joined by `\n`.
    pub headers: WuiStr,
    /// The response body.
    pub body: WuiData,
}

impl IntoFFI for AssetResponse {
    type FFI = WuiAssetResponse;
    fn into_ffi(self) -> Self::FFI {
        let mut headers = String::new();
        for (name, value) in &self.headers {
            // A header name or value can never contain a newline, so the lines
            // form is lossless.
            let _ = writeln!(headers, "{}: {}", name.as_str(), value.as_str());
        }
        WuiAssetResponse {
            status: self.status,
            headers: Str::from(headers).into_ffi(),
            body: WuiArray::new(self.body),
        }
    }
}

/// Serves one request the native engine intercepted on the asset origin.
///
/// Callable from any thread — the server behind `server` is `Send + Sync`, and
/// engines invoke this from whatever thread their network stack uses (a
/// `WKURLSchemeHandler` callback, a `WebViewClient` worker thread, a `WebKit` URI
/// scheme task, a CEF IO thread). GET and HEAD are the only methods served —
/// anything else is refused with `405` without consulting the server — and a
/// path that escapes the asset root is refused with `404`, so a backend must
/// route every intercepted request through here rather than only the shapes it
/// expects.
///
/// # Safety
///
/// - `server` must be a live pointer the backend received inside
///   [`WuiWebViewConfig`]; it is borrowed for the call, not consumed.
/// - `method`, `path` and `query` are owning `WuiStr`s and are consumed; an
///   empty `query` means the request carried none.
/// - The returned response is owned by the caller and freed with
///   [`waterui_webview_asset_response_free`] once its fields have been read.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_webview_asset_server_respond(
    server: *const WuiAssetServer,
    method: WuiStr,
    path: WuiStr,
    query: WuiStr,
) -> WuiAssetResponse {
    // SAFETY: the caller contract makes each an owning handle from the matching
    // FFI constructor, consumed here exactly once.
    let method: Str = unsafe { method.into_rust() };
    // SAFETY: as above.
    let path: Str = unsafe { path.into_rust() };
    // SAFETY: as above.
    let query: Str = unsafe { query.into_rust() };
    // SAFETY: the caller contract makes `server` a live `WuiAssetServer` for the
    // view's lifetime; it is only borrowed.
    let server = unsafe { &*server };
    waterui_webview::assets::dispatch(
        &server.server,
        method.as_str(),
        path.as_str(),
        if query.is_empty() {
            None
        } else {
            Some(query.as_str())
        },
    )
    .into_ffi()
}

/// Frees a [`WuiAssetResponse`] produced by [`waterui_webview_asset_server_respond`].
///
/// # Safety
///
/// `response` must be an owning handle from that function, freed once.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_webview_asset_response_free(response: WuiAssetResponse) {
    // SAFETY: the caller contract makes both fields owning handles consumed once
    // here — the headers by `into_rust`, the body buffer by `consume`.
    let _headers: Str = unsafe { response.headers.into_rust() };
    response.body.consume();
}

/// Releases the [`WuiAssetServer`] a native view was created with.
///
/// The backend calls this when the native view dies; a null pointer is a no-op
/// so the same teardown path serves views opened without assets.
///
/// # Safety
///
/// `server` must be null or a pointer the backend received inside
/// [`WuiWebViewConfig`] that has not been freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_webview_asset_server_free(server: *mut WuiAssetServer) {
    if server.is_null() {
        return;
    }
    // SAFETY: the caller contract makes `server` an owning pointer from
    // `FfiWebViewController::open` that has not been freed, so reclaiming the
    // box frees it exactly once.
    unsafe { drop(Box::from_raw(server)) };
}

// =============================================================================
// WebViewController Installation
// =============================================================================

/// Type for the native function that creates a new `WebView`.
///
/// Receives the creation-time [`WuiWebViewConfig`]; ownership of every owning
/// pointer inside it passes to the backend.
pub type WuiCreateWebViewFn = unsafe extern "C" fn(WuiWebViewConfig) -> WuiWebViewHandle;

/// FFI-compatible `WebViewController` implementation.
struct FfiWebViewController {
    create_fn: WuiCreateWebViewFn,
}

impl CustomWebViewController for FfiWebViewController {
    fn open(&self, config: WebViewConfig) -> impl WebViewHandle {
        let config = WuiWebViewConfig {
            asset_server: config.asset_server.into_ffi(),
        };
        // SAFETY: `create_fn` is the backend's constructor, registered on this
        // factory; `config` is passed by value, handing ownership of the boxed
        // server to the native view it returns.
        let handle = unsafe { (self.create_fn)(config) };
        FfiWebViewHandle::new(handle)
    }
}

/// Installs a `WebViewController` into the environment from a native factory function.
///
/// Native backends call this during initialization to register their `WebView` factory.
/// The factory creates blank `WebViews` that can be navigated with `go_to()`.
///
/// # Safety
///
/// The caller must ensure that:
/// - `env` is a valid pointer to a `WuiEnv`
/// - `create_fn` is a valid function pointer that returns a properly initialized `WuiWebViewHandle`
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_env_install_webview_controller(
    env: *mut WuiEnv,
    create_fn: WuiCreateWebViewFn,
) {
    // SAFETY: the caller contract requires `env` to be a valid handle, alive and not
    // otherwise borrowed for this call; the exclusive borrow ends here.
    let env = unsafe { crate::borrow_ffi_mut(env) };

    let controller = WebViewController::new(FfiWebViewController { create_fn });
    env.insert(controller);
}

/// Returns whether a `WebView` controller is already installed in the environment.
///
/// # Safety
///
/// `env` must be a valid pointer to a live `WuiEnv`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn waterui_env_has_webview_controller(env: *const WuiEnv) -> bool {
    // SAFETY: the caller contract requires `env` to be a valid handle that stays
    // alive for this call; it is only borrowed.
    let env = unsafe { crate::borrow_ffi(env) };
    env.get::<WebViewController>().is_some()
}