use embedded_graphics::{
mono_font::{MonoFont, ascii::FONT_6X10},
pixelcolor::Rgb888,
};
use std::{cell::RefCell, thread_local, vec::Vec};
use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
use super::{ButtonWasm, ButtonWasmSource, CydTouchWasmSource, CydWasm, next_animation_frame};
use crate::button::Button;
use crate::cyd::display::Orientation;
use crate::wifi_auto::WifiAutoEvent;
const WIFI_CAPTIVE_PORTAL_WAIT_FRAMES: usize = 15;
const WIFI_CONNECT_WAIT_FRAMES: usize = 90;
const BACKGROUND_COLOR: Rgb888 = Rgb888::new(10, 10, 12); const FOREGROUND_COLOR: Rgb888 = Rgb888::new(230, 230, 230);
pub struct CydSimulatorWasm {
cyd: CydWasm,
button_source: ButtonWasmSource,
control: CydSimulatorControlWasm,
}
#[wasm_bindgen]
#[derive(Clone)]
pub struct CydSimulatorControlWasm {
touch_source: Option<CydTouchWasmSource>,
button_source: ButtonWasmSource,
orientation: Orientation,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WifiConnectOutcome {
Connected,
ResetRequested,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum WifiSimulatorPhase {
Disconnected,
CaptivePortal,
Connecting,
Connected,
}
thread_local! {
static WIFI_SIMULATOR_PHASES: RefCell<Vec<(&'static str, WifiSimulatorPhase)>> =
const { RefCell::new(Vec::new()) };
}
pub struct WifiSimulatorWasm {
storage_namespace: &'static str,
}
impl WifiSimulatorWasm {
#[must_use]
pub const fn new(storage_namespace: &'static str) -> Self {
Self { storage_namespace }
}
pub fn reset(&self) {
set_phase(self.storage_namespace, WifiSimulatorPhase::Disconnected);
}
fn phase(&self) -> WifiSimulatorPhase {
WIFI_SIMULATOR_PHASES.with(|phases| {
phases
.borrow()
.iter()
.find(|(namespace, _)| *namespace == self.storage_namespace)
.map_or(WifiSimulatorPhase::Disconnected, |(_, phase)| *phase)
})
}
pub async fn connect<OnEvent, Error>(
&self,
button: &mut ButtonWasm,
mut on_event: OnEvent,
) -> Result<WifiConnectOutcome, Error>
where
OnEvent: AsyncFnMut(WifiAutoEvent) -> Result<(), Error>,
{
if self.phase() == WifiSimulatorPhase::Connected {
return Ok(WifiConnectOutcome::Connected);
}
set_phase(self.storage_namespace, WifiSimulatorPhase::CaptivePortal);
on_event(WifiAutoEvent::CaptivePortalReady).await?;
if wait_for_wifi_frames(button, WIFI_CAPTIVE_PORTAL_WAIT_FRAMES).await {
return Ok(WifiConnectOutcome::ResetRequested);
}
set_phase(self.storage_namespace, WifiSimulatorPhase::Connecting);
on_event(WifiAutoEvent::Connecting {
try_index: 0,
try_count: 1,
})
.await?;
if wait_for_wifi_frames(button, WIFI_CONNECT_WAIT_FRAMES).await {
return Ok(WifiConnectOutcome::ResetRequested);
}
set_phase(self.storage_namespace, WifiSimulatorPhase::Connected);
Ok(WifiConnectOutcome::Connected)
}
}
fn set_phase(storage_namespace: &'static str, phase: WifiSimulatorPhase) {
WIFI_SIMULATOR_PHASES.with(|phases| {
let mut phases = phases.borrow_mut();
if let Some((_, current_phase)) = phases
.iter_mut()
.find(|(namespace, _)| *namespace == storage_namespace)
{
*current_phase = phase;
} else {
phases.push((storage_namespace, phase));
}
});
}
impl CydSimulatorWasm {
pub fn new(canvas: HtmlCanvasElement, orientation: Orientation) -> Result<Self, JsValue> {
Self::new_with_style(
canvas,
orientation,
BACKGROUND_COLOR,
FOREGROUND_COLOR,
&FONT_6X10,
)
}
pub fn new_with_style(
canvas: HtmlCanvasElement,
orientation: Orientation,
background_color: Rgb888,
foreground_color: Rgb888,
font: &'static MonoFont<'static>,
) -> Result<Self, JsValue> {
let context = canvas
.get_context("2d")?
.ok_or_else(|| JsValue::from_str("2D canvas context unavailable"))?
.dyn_into::<CanvasRenderingContext2d>()?;
canvas.set_width(orientation.width());
canvas.set_height(orientation.height());
let touch_source = CydTouchWasmSource::new();
let button_source = ButtonWasmSource::new();
let cyd = CydWasm::new(
context,
orientation,
background_color,
foreground_color,
font,
touch_source.clone(),
);
let control = CydSimulatorControlWasm {
touch_source: Some(touch_source),
button_source: button_source.clone(),
orientation,
};
Ok(Self {
cyd,
button_source,
control,
})
}
pub fn into_parts(self) -> (CydWasm, ButtonWasm, CydSimulatorControlWasm) {
let Self {
cyd,
button_source,
control,
} = self;
(cyd, button_source.button(), control)
}
}
impl CydSimulatorControlWasm {
#[must_use]
pub const fn orientation(&self) -> Orientation {
self.orientation
}
}
async fn wait_for_wifi_frames(button: &ButtonWasm, frame_count: usize) -> bool {
for _ in 0..frame_count {
if button.is_pressed() {
return true;
}
next_animation_frame().await;
}
false
}
#[wasm_bindgen]
impl CydSimulatorControlWasm {
#[wasm_bindgen(js_name = orientation_is_inverted)]
pub fn orientation_is_inverted(&self) -> bool {
matches!(
self.orientation,
Orientation::LandscapeInverted | Orientation::PortraitInverted
)
}
#[wasm_bindgen(js_name = touch_down)]
pub fn touch_down(&self, x: f32, y: f32) {
let point = map_to_landscape(self.orientation, x, y);
if let Some(touch_source) = &self.touch_source {
touch_source.touch_down(point.0, point.1);
}
}
#[wasm_bindgen(js_name = touch_move)]
pub fn touch_move(&self, x: f32, y: f32) {
let point = map_to_landscape(self.orientation, x, y);
if let Some(touch_source) = &self.touch_source {
touch_source.touch_move(point.0, point.1);
}
}
#[wasm_bindgen(js_name = touch_up)]
pub fn touch_up(&self) {
if let Some(touch_source) = &self.touch_source {
touch_source.touch_up();
}
}
#[wasm_bindgen(js_name = boot_down)]
pub fn boot_down(&self) {
self.button_source.press();
}
#[wasm_bindgen(js_name = boot_up)]
pub fn boot_up(&self) {
self.button_source.release();
}
pub fn reset_transient_state(&self) {
if let Some(touch_source) = &self.touch_source {
touch_source.touch_up();
}
self.button_source.release();
}
}
fn map_to_landscape(orientation: Orientation, x: f32, y: f32) -> (f32, f32) {
match orientation {
Orientation::Landscape => (x, y),
Orientation::Portrait => (319.0 - y, x),
Orientation::LandscapeInverted => (319.0 - x, 239.0 - y),
Orientation::PortraitInverted => (y, 239.0 - x),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn orientation_mapping_round_trips() {
for orientation in [
Orientation::Landscape,
Orientation::Portrait,
Orientation::LandscapeInverted,
Orientation::PortraitInverted,
] {
let landscape_point = map_to_landscape(orientation, 37.0, 83.0);
let logical_point = match orientation {
Orientation::Landscape => landscape_point,
Orientation::Portrait => (landscape_point.1, 319.0 - landscape_point.0),
Orientation::LandscapeInverted => {
(319.0 - landscape_point.0, 239.0 - landscape_point.1)
}
Orientation::PortraitInverted => (239.0 - landscape_point.1, landscape_point.0),
};
assert_eq!(logical_point, (37.0, 83.0));
}
}
#[test]
fn wifi_state_is_scoped_by_storage_namespace() {
set_phase("app-a", WifiSimulatorPhase::Connected);
assert_eq!(
WifiSimulatorWasm::new("app-a").phase(),
WifiSimulatorPhase::Connected
);
assert_eq!(
WifiSimulatorWasm::new("app-b").phase(),
WifiSimulatorPhase::Disconnected
);
WifiSimulatorWasm::new("app-a").reset();
assert_eq!(
WifiSimulatorWasm::new("app-a").phase(),
WifiSimulatorPhase::Disconnected
);
assert_eq!(
WifiSimulatorWasm::new("app-b").phase(),
WifiSimulatorPhase::Disconnected
);
}
}