use std::{
cell::RefCell,
collections::HashMap,
rc::Rc,
};
use freya_core::prelude::*;
use crate::{
content::HtmlContent,
state::BlitzState,
};
#[derive(Clone, PartialEq)]
pub enum HtmlSource {
Url(String),
Html(HtmlContent),
}
impl HtmlSource {
pub fn url(url: impl Into<String>) -> Self {
Self::Url(url.into())
}
pub fn html(html: impl Into<HtmlContent>) -> Self {
Self::Html(html.into())
}
}
pub struct ElementHit {
pub tag: String,
pub attributes: HashMap<String, String>,
}
struct HtmlHistory {
entries: Vec<HtmlSource>,
index: usize,
}
#[derive(PartialEq, Clone, Copy)]
pub struct HtmlHandle {
history: State<HtmlHistory>,
view: State<Option<Rc<RefCell<BlitzState>>>>,
}
impl HtmlHandle {
pub fn create(initial: HtmlSource) -> Self {
let mut handle = Self::create_empty();
handle.push(initial);
handle
}
pub fn create_empty() -> Self {
Self {
history: State::create(HtmlHistory {
entries: Vec::new(),
index: 0,
}),
view: State::create(None),
}
}
pub fn create_global_empty() -> Self {
Self {
history: State::create_global(HtmlHistory {
entries: Vec::new(),
index: 0,
}),
view: State::create_global(None),
}
}
pub(crate) fn attach(&mut self, view: Rc<RefCell<BlitzState>>) {
*self.view.write() = Some(view);
self.reload();
}
pub fn navigate(&mut self, url: impl Into<String>) {
self.push(HtmlSource::Url(url.into()));
}
pub fn load_html(&mut self, html: impl Into<HtmlContent>) {
self.push(HtmlSource::Html(html.into()));
}
pub fn replace_html(&mut self, html: impl Into<HtmlContent>) {
let source = HtmlSource::Html(html.into());
self.load(&source);
let mut history = self.history.write();
let index = history.index;
history.entries.truncate(index);
history.entries.push(source);
}
pub fn back(&mut self) {
if self.can_go_back() {
self.history.write().index -= 1;
self.reload();
}
}
pub fn forward(&mut self) {
if self.can_go_forward() {
self.history.write().index += 1;
self.reload();
}
}
pub fn reload(&self) {
if let Some(source) = self.current_source() {
self.load(&source);
}
}
pub fn can_go_back(&self) -> bool {
self.history.read().index > 0
}
pub fn can_go_forward(&self) -> bool {
let history = self.history.read();
history.index + 1 < history.entries.len()
}
pub fn elements_at(&self, x: f32, y: f32) -> Vec<ElementHit> {
let Some(view) = self.view.peek().clone() else {
return Vec::new();
};
view.borrow_mut().elements_at(x, y)
}
pub fn current_url(&self) -> Option<String> {
let history = self.history.read();
match history.entries.get(history.index) {
Some(HtmlSource::Url(url)) => Some(url.clone()),
_ => None,
}
}
fn push(&mut self, source: HtmlSource) {
self.load(&source);
let mut history = self.history.write();
let index = if history.entries.is_empty() {
0
} else {
history.index + 1
};
history.entries.truncate(index);
history.entries.push(source);
history.index = index;
}
fn current_source(&self) -> Option<HtmlSource> {
let history = self.history.peek();
history.entries.get(history.index).cloned()
}
fn load(&self, source: &HtmlSource) {
let Some(view) = self.view.peek().clone() else {
return;
};
match source {
HtmlSource::Url(url) => {
spawn(load_url(view, url.clone()));
}
HtmlSource::Html(content) => view.borrow_mut().load(
&content.html,
content.base_url.as_ref().map(ToString::to_string),
),
}
}
}
async fn load_url(view: Rc<RefCell<BlitzState>>, url: String) {
let fetched = match url::Url::parse(&url) {
Ok(parsed) => freya_components::http::fetch(parsed).await,
Err(err) => Err(err.into()),
};
match fetched {
Ok(bytes) => view
.borrow_mut()
.load(&String::from_utf8_lossy(&bytes), Some(url)),
Err(err) => tracing::error!("Failed to load {url}: {err}"),
}
}
pub fn use_html(init: impl FnOnce() -> HtmlSource) -> HtmlHandle {
use_hook(move || HtmlHandle::create(init()))
}