///|
/// Per-view callbacks and observable lifecycle. The native backend owns
/// command queuing before Ready; this registry only routes callbacks.
priv struct Registration {
lifecycle : WebViewLifecycle
navigation_state : NavigationState
on_event : (WebViewEvent) -> Unit
page_message_source : Ref[String?]
on_navigation : (String) -> NavigationDecision
on_new_window : (String) -> NewWindowDecision
on_media_permission : (MediaPermissionRequest) -> MediaPermissionDecision
}
///|
let registrations : Map[UInt64, Registration] = Map([])
///|
let callback_installation : Map[Int, Unit] = Map([])
///|
let custom_schemes_locked : Map[Int, Unit] = Map([])
///|
fn ensure_native_callbacks() -> Unit {
if !callback_installation.contains(0) {
install_native_callbacks(
dispatch_native_event, decide_navigation, decide_new_window, dispatch_protocol_request,
decide_media_permission,
)
callback_installation.set(0, ())
}
}
///|
fn register_view(handle : UInt64, options : WebViewOptions) -> Unit {
registrations.set(handle, {
lifecycle: Creating,
navigation_state: NavigationState::new(),
on_event: options.on_event,
page_message_source: Ref(None),
on_navigation: options.on_navigation,
on_new_window: options.on_new_window,
on_media_permission: options.on_media_permission,
})
}
///|
fn register_ohos_view(handle : UInt64, options : OhosAttachOptions) -> Unit {
registrations.set(handle, {
lifecycle: Creating,
navigation_state: NavigationState::new(),
on_event: options.on_event,
page_message_source: Ref(None),
on_navigation: allow_navigation,
on_new_window: deny_new_window,
on_media_permission: deny_media_permission,
})
}
///|
fn unregister_view(handle : UInt64) -> Unit {
ignore(registrations.remove(handle))
}
///|
fn lifecycle_of(handle : UInt64) -> WebViewLifecycle {
match registrations.get(handle) {
Some(registration) => registration.lifecycle
None => Destroyed
}
}
///|
fn navigation_state_of(handle : UInt64) -> NavigationState {
match registrations.get(handle) {
Some(registration) => registration.navigation_state
None => NavigationState::new()
}
}
///|
fn update_lifecycle(handle : UInt64, lifecycle : WebViewLifecycle) -> Unit {
match registrations.get(handle) {
Some(registration) =>
registrations.set(handle, {
lifecycle,
navigation_state: registration.navigation_state,
on_event: registration.on_event,
page_message_source: registration.page_message_source,
on_navigation: registration.on_navigation,
on_new_window: registration.on_new_window,
on_media_permission: registration.on_media_permission,
})
None => ()
}
}
///|
fn dispatch_native_event(
handle : UInt64,
kind : Int,
value : Bytes,
detail : Bytes,
code : Int,
) -> Unit {
match registrations.get(handle) {
Some(registration) => {
let previous_source = registration.page_message_source.val
defer {
registration.page_message_source.val = previous_source
}
registration.page_message_source.val = if kind == 3 &&
code == 0 &&
!detail.is_empty() {
Some(decode_utf8(detail))
} else {
None
}
let event = decode_native_event(kind, value, detail, code)
match event {
Ready => update_lifecycle(handle, Ready)
CreationFailed(error) => update_lifecycle(handle, Failed(error))
ProcessFailed(error) => update_lifecycle(handle, Failed(error))
_ => ()
}
update_navigation_state(handle, event)
(registration.on_event)(event)
}
None => ()
}
}
///|
fn update_navigation_state(handle : UInt64, event : WebViewEvent) -> Unit {
match registrations.get(handle) {
Some(registration) => {
let navigation_state = match event {
SourceChanged(url) =>
{
url,
can_go_back: registration.navigation_state.can_go_back,
can_go_forward: registration.navigation_state.can_go_forward,
}
HistoryChanged(can_go_back, can_go_forward) =>
{
url: registration.navigation_state.url,
can_go_back,
can_go_forward,
}
_ => return
}
registrations.set(handle, {
lifecycle: registration.lifecycle,
navigation_state,
on_event: registration.on_event,
page_message_source: registration.page_message_source,
on_navigation: registration.on_navigation,
on_new_window: registration.on_new_window,
on_media_permission: registration.on_media_permission,
})
}
None => ()
}
}
///|
fn decide_navigation(handle : UInt64, url : Bytes) -> Int {
match registrations.get(handle) {
Some(registration) =>
match (registration.on_navigation)(decode_utf8(url)) {
Allow => 1
Deny => 0
}
None => 0
}
}
///|
fn decide_new_window(handle : UInt64, url : Bytes) -> Int {
match registrations.get(handle) {
Some(registration) =>
match (registration.on_new_window)(decode_utf8(url)) {
Deny => 0
NavigateCurrent => 1
}
None => 0
}
}
///|
fn dispatch_protocol_request(
handle : UInt64,
request_id : Bytes,
scheme : Bytes,
http_method : Bytes,
uri : Bytes,
headers : Bytes,
body : Bytes,
) -> Unit {
match registrations.get(handle) {
Some(registration) =>
(registration.on_event)(
ProtocolRequest({
id: decode_utf8(request_id),
scheme: decode_utf8(scheme),
http_method: decode_utf8(http_method),
uri: decode_utf8(uri),
headers: decode_http_headers(headers),
body,
}),
)
None => ()
}
}
///|
fn decide_media_permission(handle : UInt64, kind : Int, origin : Bytes) -> Int {
match media_permission_kind_from_native(kind) {
Unknown(_) => 0
kind =>
match registrations.get(handle) {
Some(registration) =>
match
(registration.on_media_permission)({
kind,
origin: decode_utf8(origin),
}) {
Allow => 1
Deny => 0
}
None => 0
}
}
}
///|
fn decode_native_event(
kind : Int,
value : Bytes,
detail : Bytes,
code : Int,
) -> WebViewEvent {
let value = decode_utf8(value)
let detail = decode_utf8(detail)
match kind {
1 => Ready
2 => CreationFailed(NativeFailure(code, detail))
3 => PageMessage(value)
4 => NavigationStarting(value)
5 => SourceChanged(value)
6 =>
if code == 0 {
NavigationCompleted(value)
} else {
NavigationFailed(value, NativeFailure(code, detail))
}
7 => TitleChanged(value)
8 =>
HistoryChanged(
contains_text(detail, "back=1"),
contains_text(detail, "forward=1"),
)
9 =>
if code == 0 {
ScriptResult(detail, value)
} else {
ScriptFailed(detail, NativeFailure(code, value))
}
10 => ProtocolCancelled(value)
11 => ProcessFailed(NativeFailure(code, detail))
_ => CreationFailed(NativeFailure(code, detail))
}
}
///|
fn decode_utf8(bytes : BytesView) -> String {
@utf8.decode_lossy(bytes[:], ignore_bom=true)
}
///|
fn encode_utf8(text : String) -> Bytes {
@utf8.encode(text[:], bom=false)
}
///|
/// Header wire format: `u32be count`, followed by each UTF-8 name and value
/// prefixed by its `u32be` byte length. The format is shared by every backend.
fn encode_http_headers(headers : Array[HttpHeader]) -> Bytes {
let output : Array[Byte] = []
append_u32be(output, headers.length())
for header in headers {
let name = encode_utf8(header.name)
let value = encode_utf8(header.value)
append_u32be(output, name.length())
append_bytes(output, name)
append_u32be(output, value.length())
append_bytes(output, value)
}
Bytes::from_array(output)
}
///|
fn decode_http_headers(bytes : Bytes) -> Array[HttpHeader] {
let mut offset = 0
let count = match read_u32be(bytes, offset) {
Some(value) => value
None => return []
}
offset += 4
if count < 0 || count > bytes.length() / 8 {
return []
}
let headers : Array[HttpHeader] = []
for _ in 0.. value
None => return []
}
offset += 4
if name_length < 0 || name_length > bytes.length() - offset {
return []
}
let name = decode_utf8(bytes[offset:offset + name_length])
offset += name_length
let value_length = match read_u32be(bytes, offset) {
Some(value) => value
None => return []
}
offset += 4
if value_length < 0 || value_length > bytes.length() - offset {
return []
}
let value = decode_utf8(bytes[offset:offset + value_length])
offset += value_length
headers.push({ name, value, })
}
if offset == bytes.length() {
headers
} else {
[]
}
}
///|
fn append_u32be(output : Array[Byte], value : Int) -> Unit {
output.push(((value >> 24) & 0xff).to_byte())
output.push(((value >> 16) & 0xff).to_byte())
output.push(((value >> 8) & 0xff).to_byte())
output.push((value & 0xff).to_byte())
}
///|
fn append_bytes(output : Array[Byte], value : Bytes) -> Unit {
for byte in value {
output.push(byte)
}
}
///|
fn read_u32be(bytes : Bytes, offset : Int) -> Int? {
if offset < 0 || offset + 4 > bytes.length() {
None
} else {
Some(
(bytes[offset].to_int() << 24) |
(bytes[offset + 1].to_int() << 16) |
(bytes[offset + 2].to_int() << 8) |
bytes[offset + 3].to_int(),
)
}
}
///|
fn media_permission_kind_from_native(kind : Int) -> MediaPermissionKind {
match kind {
1 => Camera
2 => Microphone
3 => CameraAndMicrophone
_ => Unknown(kind)
}
}
///|
fn contains_text(text : String, needle : String) -> Bool {
let text = text.to_array()
let needle = needle.to_array()
if needle.length() == 0 {
true
} else if needle.length() > text.length() {
false
} else {
for start in 0..<=(text.length() - needle.length()) {
let mut matched = true
for offset in 0..