use core::{
future::Future,
pin::Pin,
task::{Context, Poll, Waker},
};
use std::{
cell::{Cell, RefCell},
fmt::Debug,
rc::Rc,
};
use embassy_futures::select::{Either, select};
use embedded_graphics::{mono_font::MonoFont, pixelcolor::Rgb888};
use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
use web_sys::{HtmlCanvasElement, window};
use super::{
ButtonWasm, ClockSyncWasm, CydSimulatorControlWasm, CydSimulatorWasm, CydWasm,
DnsSimulatorWasm, FlashBlockWasm, WifiSimulatorWasm,
};
use crate::cyd::display::Orientation;
use crate::flash_block::FlashBlock as _;
#[derive(Clone, Copy)]
pub struct Config {
pub storage_namespace: &'static str,
pub initial_orientation: Orientation,
pub background_color: Rgb888,
pub foreground_color: Rgb888,
pub font: &'static MonoFont<'static>,
}
impl Config {
pub const fn new(
storage_namespace: &'static str,
initial_orientation: Orientation,
background_color: Rgb888,
foreground_color: Rgb888,
font: &'static MonoFont<'static>,
) -> Self {
Self {
storage_namespace,
initial_orientation,
background_color,
foreground_color,
font,
}
}
}
#[derive(Clone, Copy)]
pub struct PageInfo {
pub title: &'static str,
pub preview: &'static str,
pub description: &'static str,
pub controls: &'static str,
pub core_code_url: &'static str,
}
impl PageInfo {
pub const fn new(
title: &'static str,
preview: &'static str,
description: &'static str,
controls: &'static str,
core_code_url: &'static str,
) -> Self {
Self {
title,
preview,
description,
controls,
core_code_url,
}
}
}
pub struct Capabilities {
pub cyd: CydWasm,
pub button: ButtonWasm,
pub clock_sync: ClockSyncWasm,
pub wifi_simulator: WifiSimulatorWasm,
pub dns_simulator: DnsSimulatorWasm,
}
pub enum Command {
Restart,
CalibrationNotNeeded,
ResetWifi,
Reorientate(Orientation),
Stop,
}
#[wasm_bindgen(js_name = CydWebNoticeSeverity)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NoticeSeverity {
Info,
Warning,
Fatal,
}
#[wasm_bindgen(js_name = CydWebNotice)]
#[derive(Clone, Debug)]
pub struct Notice {
id: String,
severity: NoticeSeverity,
detail: Option<String>,
}
impl Notice {
fn new(id: impl Into<String>, severity: NoticeSeverity) -> Self {
Self {
id: id.into(),
severity,
detail: None,
}
}
fn fatal(detail: String) -> Self {
Self {
id: "runtime-error".into(),
severity: NoticeSeverity::Fatal,
detail: Some(detail),
}
}
}
#[wasm_bindgen(js_class = CydWebNotice)]
impl Notice {
pub fn id(&self) -> String {
self.id.clone()
}
pub fn severity(&self) -> NoticeSeverity {
self.severity
}
pub fn detail(&self) -> Option<String> {
self.detail.clone()
}
}
#[derive(Clone, Copy)]
enum HostRequest {
Restart,
ClearStorage,
}
struct LifecycleState {
request: Option<HostRequest>,
waker: Option<Waker>,
}
#[derive(Clone)]
struct LifecycleSignal {
state: Rc<RefCell<LifecycleState>>,
}
impl LifecycleSignal {
fn new() -> Self {
Self {
state: Rc::new(RefCell::new(LifecycleState {
request: None,
waker: None,
})),
}
}
fn request(&self, request: HostRequest) {
let waker = {
let mut state = self.state.borrow_mut();
state.request = Some(request);
state.waker.take()
};
if let Some(waker) = waker {
waker.wake();
}
}
async fn wait(&self) -> HostRequest {
LifecycleRequestFuture {
signal: self.clone(),
}
.await
}
}
struct LifecycleRequestFuture {
signal: LifecycleSignal,
}
impl Future for LifecycleRequestFuture {
type Output = HostRequest;
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
let mut state = self.signal.state.borrow_mut();
if let Some(request) = state.request.take() {
Poll::Ready(request)
} else {
state.waker = Some(context.waker().clone());
Poll::Pending
}
}
}
struct SupervisorState {
live_control: Option<CydSimulatorControlWasm>,
notices: std::collections::VecDeque<Notice>,
orientation: Orientation,
stopped: bool,
page_info: PageInfo,
clock_time_of_day: Rc<Cell<Option<u32>>>,
clock_control_visible: Rc<Cell<bool>>,
}
#[wasm_bindgen(js_name = CydWebAppHandle)]
pub struct Handle {
state: Rc<RefCell<SupervisorState>>,
lifecycle_signal: LifecycleSignal,
}
impl Handle {
fn new(state: Rc<RefCell<SupervisorState>>, lifecycle_signal: LifecycleSignal) -> Self {
Self {
state,
lifecycle_signal,
}
}
fn with_control(&self, action: impl FnOnce(&CydSimulatorControlWasm)) {
let state = self.state.borrow();
if !state.stopped {
if let Some(control) = state.live_control.as_ref() {
action(control);
}
}
}
}
#[wasm_bindgen(js_class = CydWebAppHandle)]
impl Handle {
pub fn touch_down(&self, position_x: f32, position_y: f32) {
self.with_control(|control| control.touch_down(position_x, position_y));
}
pub fn touch_move(&self, position_x: f32, position_y: f32) {
self.with_control(|control| control.touch_move(position_x, position_y));
}
pub fn touch_up(&self) {
self.with_control(CydSimulatorControlWasm::touch_up);
}
pub fn boot_down(&self) {
self.with_control(CydSimulatorControlWasm::boot_down);
}
pub fn boot_up(&self) {
self.with_control(CydSimulatorControlWasm::boot_up);
}
pub fn orientation_is_inverted(&self) -> bool {
self.state
.borrow()
.live_control
.as_ref()
.is_some_and(CydSimulatorControlWasm::orientation_is_inverted)
}
pub fn take_notice(&self) -> Option<Notice> {
self.state.borrow_mut().notices.pop_front()
}
pub fn request_restart(&self) {
self.lifecycle_signal.request(HostRequest::Restart);
}
pub fn clear_storage_and_restart(&self) {
self.lifecycle_signal.request(HostRequest::ClearStorage);
}
pub fn clock_control_is_visible(&self) -> bool {
self.state.borrow().clock_control_visible.get()
}
pub fn set_clock_time_of_day(&self, seconds_of_day: u32) -> Result<(), JsValue> {
if seconds_of_day >= 86_400 {
return Err(JsValue::from_str(
"time of day must be between 0 and 86399 seconds",
));
}
self.state
.borrow()
.clock_time_of_day
.set(Some(seconds_of_day));
Ok(())
}
pub fn use_live_clock(&self) {
self.state.borrow().clock_time_of_day.set(None);
}
pub fn page_title(&self) -> String {
self.state.borrow().page_info.title.into()
}
pub fn page_preview(&self) -> String {
self.state.borrow().page_info.preview.into()
}
pub fn page_description(&self) -> String {
self.state.borrow().page_info.description.into()
}
pub fn page_controls(&self) -> String {
self.state.borrow().page_info.controls.into()
}
pub fn page_core_code_url(&self) -> String {
self.state.borrow().page_info.core_code_url.into()
}
}
pub fn start<Run, Error>(
canvas_id: &str,
config: Config,
page_info: PageInfo,
inner_main: Run,
) -> Result<Handle, JsValue>
where
Run: AsyncFnMut(Capabilities) -> Result<Command, Error> + 'static,
Error: Debug + 'static,
{
let canvas = canvas(canvas_id)?;
let mut orientation_flash_block =
FlashBlockWasm::new(&format!("{}/orientation", config.storage_namespace))
.map_err(|error| JsValue::from_str(&format!("orientation storage: {error:?}")))?;
let orientation = orientation_flash_block
.load::<Orientation>()
.map_err(|error| JsValue::from_str(&format!("orientation load: {error:?}")))?
.unwrap_or(config.initial_orientation);
let simulator = CydSimulatorWasm::new_with_style(
canvas.clone(),
orientation,
config.background_color,
config.foreground_color,
config.font,
)?;
let (cyd, button, control) = simulator.into_parts();
let state = Rc::new(RefCell::new(SupervisorState {
live_control: Some(control),
notices: std::collections::VecDeque::new(),
orientation,
stopped: false,
page_info,
clock_time_of_day: Rc::new(Cell::new(None)),
clock_control_visible: Rc::new(Cell::new(false)),
}));
let lifecycle_signal = LifecycleSignal::new();
let handle = Handle::new(state.clone(), lifecycle_signal.clone());
wasm_bindgen_futures::spawn_local(supervise(
canvas,
config,
orientation_flash_block,
state,
lifecycle_signal,
inner_main,
Some((cyd, button)),
));
Ok(handle)
}
fn canvas(canvas_id: &str) -> Result<HtmlCanvasElement, JsValue> {
let document = window()
.ok_or_else(|| JsValue::from_str("browser window unavailable"))?
.document()
.ok_or_else(|| JsValue::from_str("document unavailable"))?;
document
.get_element_by_id(canvas_id)
.ok_or_else(|| JsValue::from_str("canvas element unavailable"))?
.dyn_into::<HtmlCanvasElement>()
.map_err(Into::into)
}
async fn supervise<Run, Error>(
canvas: HtmlCanvasElement,
config: Config,
mut orientation_flash_block: FlashBlockWasm,
state: Rc<RefCell<SupervisorState>>,
lifecycle_signal: LifecycleSignal,
mut inner_main: Run,
initial_session: Option<(CydWasm, ButtonWasm)>,
) where
Run: AsyncFnMut(Capabilities) -> Result<Command, Error> + 'static,
Error: Debug + 'static,
{
let mut session = initial_session;
loop {
let (cyd, button) = match session.take() {
Some(session) => session,
None => {
let orientation = state.borrow().orientation;
match CydSimulatorWasm::new_with_style(
canvas.clone(),
orientation,
config.background_color,
config.foreground_color,
config.font,
) {
Ok(simulator) => {
let (cyd, button, control) = simulator.into_parts();
state.borrow_mut().live_control = Some(control);
(cyd, button)
}
Err(error) => {
fatal(&state, format!("simulator construction failed: {error:?}"));
break;
}
}
}
};
let (clock_time_of_day, clock_control_visible) = {
let state_ref = state.borrow();
(
state_ref.clock_time_of_day.clone(),
state_ref.clock_control_visible.clone(),
)
};
let clock_sync =
ClockSyncWasm::new_with_control_state(clock_time_of_day, clock_control_visible);
let application = Capabilities {
cyd,
button,
clock_sync,
wifi_simulator: WifiSimulatorWasm::new(config.storage_namespace),
dns_simulator: DnsSimulatorWasm::standard(),
};
let command = match select(inner_main(application), lifecycle_signal.wait()).await {
Either::First(result) => match result {
Ok(command) => command,
Err(error) => {
fatal(&state, format!("application failed: {error:?}"));
break;
}
},
Either::Second(request) => {
match apply_host_request(request, &config, &mut orientation_flash_block, &state) {
Ok(()) => Command::Restart,
Err(error) => {
fatal(&state, error);
break;
}
}
}
};
release_control(&state);
match command {
Command::Stop => break,
Command::Restart => {}
Command::ResetWifi => {
WifiSimulatorWasm::new(config.storage_namespace).reset();
state
.borrow_mut()
.notices
.push_back(Notice::new("wifi-simulated", NoticeSeverity::Info));
}
Command::CalibrationNotNeeded => state
.borrow_mut()
.notices
.push_back(Notice::new("calibration-not-needed", NoticeSeverity::Info)),
Command::Reorientate(orientation) => {
if let Err(error) = orientation_flash_block.save(&orientation) {
fatal(&state, format!("orientation save failed: {error:?}"));
break;
}
state.borrow_mut().orientation = orientation;
}
}
}
release_control(&state);
state.borrow_mut().stopped = true;
}
fn release_control(state: &Rc<RefCell<SupervisorState>>) {
let control = state.borrow_mut().live_control.take();
if let Some(control) = control {
control.reset_transient_state();
}
}
fn fatal(state: &Rc<RefCell<SupervisorState>>, message: String) {
state.borrow_mut().notices.push_back(Notice::fatal(message));
}
fn apply_host_request(
request: HostRequest,
config: &Config,
flash: &mut FlashBlockWasm,
state: &Rc<RefCell<SupervisorState>>,
) -> Result<(), String> {
if matches!(request, HostRequest::ClearStorage) {
flash
.clear()
.map_err(|error| format!("storage clear failed: {error:?}"))?;
state.borrow_mut().orientation = config.initial_orientation;
}
Ok(())
}