use std::collections::HashMap;
use std::io;
use std::sync::{Arc, Mutex};
use rich_embed::PageState;
use serde_json::Value;
use crate::engine::{
Engine, EngineEvent, Format, Frame, HistoryStep, MediaOp, PageAction, PageSnapshot, Playback,
Rect, TabId,
};
#[derive(Default)]
pub struct FakeState {
pub pages: HashMap<String, PageSnapshot>,
pub current: Option<String>,
pub history: Vec<String>,
pub at: usize,
pub actions: Vec<PageAction>,
pub opened: Vec<String>,
pub events: Vec<EngineEvent>,
pub media: HashMap<i64, Playback>,
}
pub type OnAct = Box<dyn FnMut(&PageAction, &mut FakeState) + Send>;
pub struct FakeEngine {
pub state: Arc<Mutex<FakeState>>,
on_act: Option<OnAct>,
}
pub fn snapshot_file(path: &std::path::Path) -> io::Result<PageSnapshot> {
let all: Value = serde_json::from_str(&std::fs::read_to_string(path)?)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
Ok(PageSnapshot {
dom: all["dom"].clone(),
ax: all["ax"].clone(),
})
}
impl FakeEngine {
pub fn from_dir(dir: &std::path::Path) -> io::Result<FakeEngine> {
let mut state = FakeState::default();
for entry in std::fs::read_dir(dir)? {
let path = entry?.path();
if path.extension().is_some_and(|e| e == "json") {
let name = path
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string();
state.pages.insert(name, snapshot_file(&path)?);
}
}
Ok(FakeEngine {
state: Arc::new(Mutex::new(state)),
on_act: None,
})
}
#[must_use]
pub fn on_act(
mut self,
hook: impl FnMut(&PageAction, &mut FakeState) + Send + 'static,
) -> FakeEngine {
self.on_act = Some(Box::new(hook));
self
}
fn lock(&self) -> std::sync::MutexGuard<'_, FakeState> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
}
impl FakeState {
pub fn go(&mut self, url: &str) {
let name = url
.rsplit('/')
.next()
.unwrap_or(url)
.split(['.', '?', '#'])
.next()
.unwrap_or_default()
.to_string();
self.history
.truncate(self.at + usize::from(!self.history.is_empty()));
self.history.push(name.clone());
self.at = self.history.len() - 1;
self.current = Some(name);
self.events.push(EngineEvent::Navigated(url.to_string()));
}
}
pub fn follow(action: &PageAction, state: &mut FakeState) {
let PageAction::Click(node) = action else {
return;
};
let Some(page) = state
.current
.as_ref()
.and_then(|name| state.pages.get(name))
else {
return;
};
let model = crate::model::build(page, &mut crate::model::Revisions::new());
if let Some(href) = model.control(*node).and_then(|c| c.href.clone()) {
let target = href.rsplit('/').next().unwrap_or_default().to_string();
state.go(&target);
}
}
impl Engine for FakeEngine {
fn resize(&mut self, _columns: u16, _rows: u16) -> io::Result<()> {
Ok(())
}
fn open(&mut self, _tab: TabId, url: &str) -> io::Result<()> {
let mut state = self.lock();
state.opened.push(url.to_string());
state.go(url);
Ok(())
}
fn history(&mut self, _tab: TabId, step: HistoryStep) -> io::Result<()> {
let mut state = self.lock();
match step {
HistoryStep::Back if state.at > 0 => state.at -= 1,
HistoryStep::Forward if state.at + 1 < state.history.len() => state.at += 1,
_ => {}
}
let at = state.at;
state.current = state.history.get(at).cloned();
state.events.push(EngineEvent::Loaded);
Ok(())
}
fn state(&self, _tab: TabId) -> PageState {
let state = self.lock();
let url = state
.current
.as_ref()
.and_then(|name| state.pages.get(name))
.map(|s| {
let strings = &s.dom["strings"];
s.dom["documents"][0]["documentURL"]
.as_u64()
.and_then(|i| strings[i as usize].as_str())
.unwrap_or_default()
.to_string()
})
.unwrap_or_default();
PageState {
url,
can_go_back: state.at > 0,
can_go_forward: state.at + 1 < state.history.len(),
..PageState::default()
}
}
fn snapshot(&mut self, _tab: TabId) -> io::Result<PageSnapshot> {
let state = self.lock();
state
.current
.as_ref()
.and_then(|name| state.pages.get(name))
.cloned()
.ok_or_else(|| io::Error::other("no page"))
}
fn capture(&mut self, _tab: TabId, region: Rect, format: Format) -> io::Result<Frame> {
let (w, h) = (region.width.max(1.0) as u32, region.height.max(1.0) as u32);
let image = image::RgbImage::from_pixel(w, h, image::Rgb([30, 90, 220]));
let mut bytes = Vec::new();
image
.write_to(
&mut std::io::Cursor::new(&mut bytes),
image::ImageFormat::Png,
)
.map_err(io::Error::other)?;
Ok(Frame { format, bytes })
}
fn act(&mut self, _tab: TabId, action: &PageAction) -> io::Result<()> {
let mut hook = self.on_act.take();
{
let mut state = self.lock();
state.actions.push(action.clone());
if let PageAction::Media { node, op } = action {
if let Some(p) = state.media.get_mut(node) {
match op {
MediaOp::Play => p.paused = false,
MediaOp::Pause => p.paused = true,
MediaOp::Mute(on) => p.muted = *on,
MediaOp::Volume(v) => p.volume = *v,
}
}
state.events.push(EngineEvent::Changed);
}
match hook.as_mut() {
Some(hook) => hook(action, &mut state),
None => follow(action, &mut state),
}
}
self.on_act = hook;
Ok(())
}
fn playback(&mut self, _tab: TabId, node: i64) -> io::Result<Playback> {
self.lock()
.media
.get(&node)
.copied()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no media"))
}
fn events(&mut self) -> Vec<EngineEvent> {
std::mem::take(&mut self.lock().events)
}
}