///|
pub struct TimerSource {
start_fn : (Double, (@core.Frame) -> Unit) -> (() -> Unit)?
}
///|
pub(all) struct RouteQueryParam {
key : String
value : String
} derive(Eq, Debug, ToJson)
///|
pub extend RouteQueryParam with Debug::{to_repr}
///|
pub extend RouteQueryParam with Eq::{not_equal, equal}
///|
pub extend RouteQueryParam with ToJson::{to_json}
///|
pub(all) struct RouteLocation {
path : String
query : Array[RouteQueryParam]
} derive(Eq, Debug, ToJson)
///|
pub extend RouteLocation with Debug::{to_repr}
///|
pub extend RouteLocation with Eq::{not_equal, equal}
///|
pub extend RouteLocation with ToJson::{to_json}
///|
pub(all) struct RouteEvent {
location : RouteLocation
source : String
} derive(Eq, Debug, ToJson)
///|
pub extend RouteEvent with Debug::{to_repr}
///|
pub extend RouteEvent with Eq::{not_equal, equal}
///|
pub extend RouteEvent with ToJson::{to_json}
///|
priv struct RouteHandler {
token : Int
handle : (RouteEvent) -> Unit
}
///|
pub struct RouteSource {
priv mut next_token : Int
priv mut handlers : Array[RouteHandler]
}
///|
pub fn TimerSource::new(
start~ : (Double, (@core.Frame) -> Unit) -> (() -> Unit)?,
) -> TimerSource {
{ start_fn: start, }
}
///|
/// Adapt a native timer callback into an app-facing timer source. Platform
/// backends own the native start/cancel functions; services owns only the
/// neutral frame and cancellation semantics.
pub fn TimerSource::from_native_timer(
start~ : (FuncRef[(() -> Unit) -> Unit], Double, () -> Unit) -> UInt64,
cancel~ : (UInt64) -> Unit,
clock? : (() -> Double)? = None,
) -> TimerSource {
TimerSource::new(start=(interval_ms, dispatch) => {
let tick = match clock {
None =>
() => dispatch(@core.Frame::new(time_ms=0.0, delta_ms=interval_ms))
Some(now) => {
let last_time_ms = Ref(now())
() => {
let now_ms = now()
let delta_ms = (now_ms - last_time_ms.val).max(0.0)
last_time_ms.val = now_ms
dispatch(@core.Frame::new(time_ms=now_ms, delta_ms~))
}
}
}
let handle = start(fn(callback) { callback() }, interval_ms, tick)
Some(() => cancel(handle))
})
}
///|
pub fn[Msg] TimerSource::subscription(
self : TimerSource,
interval_ms~ : Double,
map~ : (@core.Frame) -> Msg,
key? : String = "app:timer",
label? : String = "App timer",
) -> @core.Subscription[Msg] {
@core.Subscription::timer(key~, label~, start=dispatch => {
(self.start_fn)(interval_ms, frame => dispatch(map(frame)))
})
}
///|
pub fn RouteLocation::new(
path~ : String,
query? : Array[RouteQueryParam] = [],
) -> RouteLocation {
{ path, query, }
}
///|
pub fn RouteQueryParam::new(key~ : String, value~ : String) -> RouteQueryParam {
{ key, value, }
}
///|
pub fn RouteLocation::parse(value : String) -> RouteLocation {
match route_split_once(value, '?') {
Some((path, query_string)) => {
let query : Array[RouteQueryParam] = []
for pair_view in query_string.split("&") {
let pair = pair_view.to_owned()
if pair != "" {
match route_split_once(pair, '=') {
Some((key, value)) => query.push({ key, value, })
None => query.push({ key: pair, value: "", })
}
}
}
RouteLocation::new(path~, query~)
}
None => RouteLocation::new(path=value)
}
}
///|
pub fn RouteLocation::query_value(
self : RouteLocation,
key : String,
) -> String? {
for param in self.query {
if param.key == key {
return Some(param.value)
}
}
None
}
///|
pub fn RouteLocation::to_route(self : RouteLocation) -> String {
if self.query.is_empty() {
self.path
} else {
self.path +
"?" +
self.query.map(param => "\{param.key}=\{param.value}").join("&")
}
}
///|
pub fn RouteEvent::new(
location~ : RouteLocation,
source? : String = "host-route",
) -> RouteEvent {
{ location, source, }
}
///|
pub fn RouteEvent::from_route(
route~ : String,
source? : String = "host-route",
) -> RouteEvent {
RouteEvent::new(location=RouteLocation::parse(route), source~)
}
///|
pub fn RouteEvent::from_path(
path : String,
source? : String = "host-route",
) -> RouteEvent {
RouteEvent::new(location=RouteLocation::new(path~), source~)
}
///|
pub fn RouteEvent::route(self : RouteEvent) -> String {
self.location.to_route()
}
///|
pub fn RouteSource::new() -> RouteSource {
{ next_token: 1, handlers: [], }
}
///|
pub fn RouteSource::publish(self : RouteSource, event : RouteEvent) -> Int {
let handlers = self.handlers.copy()
for handler in handlers {
(handler.handle)(event)
}
handlers.length()
}
///|
pub fn RouteSource::active_handler_count(self : RouteSource) -> Int {
self.handlers.length()
}
///|
pub fn[Msg] RouteSource::subscription(
self : RouteSource,
map~ : (RouteEvent) -> Msg?,
key? : String = "app:route",
label? : String = "App route",
) -> @core.Subscription[Msg] {
@core.Subscription::route_event(key~, label~, start=dispatch => {
let token = self.next_token
self.next_token = self.next_token + 1
self.handlers.push({
token,
handle: event => {
match map(event) {
Some(message) => dispatch(message)
None => ()
}
},
})
Some(() => {
self.handlers = self.handlers.filter(handler => handler.token != token)
})
})
}
///|
pub struct AppEnvironment {
services_value : AppServices
timer_value : TimerSource?
routes_value : RouteSource?
clock_value : (() -> Double)?
}
///|
pub fn AppEnvironment::new(
services? : AppServices = AppServices::unavailable(),
timer? : TimerSource? = None,
routes? : RouteSource? = None,
clock? : (() -> Double)? = None,
) -> AppEnvironment {
{
services_value: services,
timer_value: timer,
routes_value: routes,
clock_value: clock,
}
}
///|
pub fn AppEnvironment::services(self : AppEnvironment) -> AppServices {
self.services_value
}
///|
pub fn AppEnvironment::timer(self : AppEnvironment) -> TimerSource? {
self.timer_value
}
///|
pub fn AppEnvironment::routes(self : AppEnvironment) -> RouteSource? {
self.routes_value
}
///|
/// Monotonic milliseconds on a real host clock, when the backend provides
/// one. Apps use it to stamp animations and to measure event spacing (the
/// core pointer contract does not carry event timestamps). It shares the
/// timeline with the host's frame clock, so a stamp taken here compares
/// directly against `ViewPaintContext.now_ms`.
pub fn AppEnvironment::clock(self : AppEnvironment) -> (() -> Double)? {
self.clock_value
}
///|
fn route_split_once(value : String, separator : Char) -> (String, String)? {
let before : Array[Char] = []
let after : Array[Char] = []
let mut found = false
for ch in value {
if !found && ch == separator {
found = true
} else if found {
after.push(ch)
} else {
before.push(ch)
}
}
if found {
Some((String::from_array(before), String::from_array(after)))
} else {
None
}
}