Menu
Public repository / master
AkurAI-Framework / crates/ui/src/platform.rs
Latest change fee5b6c2cde89fa6bf8dc51068af7336b56a88b8 - akurai-ui: platform::open_url (hand a URL, e.g. a native shell's scheme, to the window) by Ólafur Búi Ólafsson
//! Small platform services: timers, clock, storage, document.
use crate::callbacks;
use crate::host;
/// Run `f` once after `ms` milliseconds.
pub fn set_timeout(ms: u32, f: impl FnOnce() + 'static) {
let mut f = Some(f);
let mut run = crate::dom::in_scope(crate::reactive::current_scope(), move |()| {
if let Some(f) = f.take() {
f()
}
});
let id = callbacks::once(move |_, _| run(()));
host::set_timeout(ms, id);
}
/// Milliseconds since the Unix epoch.
pub fn now_ms() -> f64 {
host::now_ms()
}
/// The browser's UTC offset in minutes (east positive; Iceland is 0).
pub fn tz_offset_minutes() -> i32 {
host::tz_offset_minutes()
}
/// `localStorage` read (`None` when missing or unavailable).
pub fn storage_get(key: &str) -> Option<String> {
host::storage_get(key)
}
/// `localStorage` write (silently ignored when unavailable).
pub fn storage_set(key: &str, value: &str) {
host::storage_set(key, value)
}
/// Set `document.title`.
pub fn set_title(title: &str) {
host::set_title(title)
}
/// Set an attribute on `<html>` (e.g. `data-theme`).
pub fn set_root_attr(name: &str, value: &str) {
host::set_root_attr(name, value)
}
/// `console.log` (stderr off-browser).
pub fn log(message: &str) {
host::log(message)
}
/// Focus the first focusable element inside `node`.
pub fn focus_first(node: host::Node) -> bool {
host::focus_first(node)
}
/// The focused element, if it has a handle.
pub fn active_element() -> Option<host::Node> {
host::active_element()
}
/// Focus `node`.
pub fn focus(node: host::Node) {
host::focus(node)
}
/// Whether `node` is `ancestor` or inside it.
pub fn contains(ancestor: host::Node, node: host::Node) -> bool {
host::contains(ancestor, node)
}
/// Read a DOM property as text (e.g. an input's current `value`).
pub fn prop(node: host::Node, name: &str) -> String {
host::get_prop_str(node, name)
}
/// A finished microphone clip.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Recording {
/// e.g. `audio/webm;codecs=opus`
pub mime: String,
/// The clip, base64-encoded (ready for a JSON body or a data URL).
pub base64: String,
}
/// Start recording from the microphone. The browser asks for permission on
/// the first use; a refusal surfaces from [`record_stop`].
pub fn record_start() -> bool {
host::record_start()
}
/// Stop recording and receive the clip, or the reason there is none.
pub fn record_stop(done: impl FnOnce(Result<Recording, String>) + 'static) {
let mut done = Some(done);
let mut run = crate::dom::in_scope(
crate::reactive::current_scope(),
move |r: Result<Recording, String>| {
if let Some(done) = done.take() {
done(r)
}
},
);
let id = crate::callbacks::once(move |status, body| {
run(if status == 1 {
let (mime, base64) = body.split_once('\n').unwrap_or(("audio/webm", &body));
Ok(Recording {
mime: mime.to_string(),
base64: base64.to_string(),
})
} else {
Err(body)
})
});
host::record_stop(id);
}
/// Play audio from a URL or a `data:` URL.
pub fn play_audio(src: &str) {
host::play_audio(src)
}
/// Navigate the window to `url` (e.g. a custom scheme a native shell handles;
/// the shell refuses the navigation, so the page stays).
pub fn open_url(url: &str) {
host::open_url(url)
}
/// A fullscreen video layer above the page (live TV): a new `src` switches,
/// the same `src` only changes the volume (0.0-1.0), an empty `src` removes it.
pub fn stream_video(src: &str, volume: f64) {
host::stream_video(src, volume.clamp(0.0, 1.0))
}
/// The page's one long-lived audio stream (a radio): a new `src` switches to it,
/// the same `src` only changes the volume (0.0-1.0, clamped), an empty `src` stops.
pub fn stream_audio(src: &str, volume: f64) {
host::stream_audio(src, volume.clamp(0.0, 1.0))
}
#[cfg(test)]
mod audio_tests {
use super::*;
use crate::reactive::signal;
use crate::testing as t;
#[test]
fn recording_hands_back_the_clip_or_the_refusal_and_audio_plays() {
t::reset();
assert!(record_start());
assert!(t::recording());
let got = signal(None::<Result<Recording, String>>);
record_stop(move |r| got.set(Some(r)));
assert!(!t::recording());
t::finish_recording("audio/webm;codecs=opus", "AAEC");
assert_eq!(
got.get(),
Some(Ok(Recording {
mime: "audio/webm;codecs=opus".into(),
base64: "AAEC".into()
}))
);
record_start();
record_stop(move |r| got.set(Some(r)));
t::fail_recording("NotAllowedError");
assert_eq!(got.get(), Some(Err("NotAllowedError".into())));
play_audio("data:audio/wav;base64,UklGR");
assert_eq!(t::played_audio(), vec!["data:audio/wav;base64,UklGR"]);
}
#[test]
fn one_stream_plays_switches_changes_volume_and_stops() {
t::reset();
assert_eq!(t::audio_stream(), None);
stream_audio("https://radio.example/a", 0.6);
assert_eq!(
t::audio_stream(),
Some(("https://radio.example/a".into(), 0.6))
);
stream_audio("https://radio.example/a", 1.4);
assert_eq!(
t::audio_stream(),
Some(("https://radio.example/a".into(), 1.0))
);
stream_audio("https://radio.example/b", -1.0);
assert_eq!(
t::audio_stream(),
Some(("https://radio.example/b".into(), 0.0))
);
stream_audio("", 0.5);
assert_eq!(t::audio_stream(), None);
}
#[test]
fn open_url_hands_the_url_to_the_host() {
t::reset();
open_url("familyhub://tv/stop");
assert_eq!(t::opened_urls(), vec!["familyhub://tv/stop"]);
}
#[test]
fn one_fullscreen_video_layer_shows_switches_and_goes() {
t::reset();
stream_video("https://tv.example/a.m3u8", 0.8);
assert_eq!(
t::video_stream(),
Some(("https://tv.example/a.m3u8".into(), 0.8))
);
stream_video("https://tv.example/a.m3u8", 2.0);
assert_eq!(
t::video_stream(),
Some(("https://tv.example/a.m3u8".into(), 1.0))
);
stream_video("", 0.0);
assert_eq!(t::video_stream(), None);
}
}