Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 71 additions & 96 deletions apps/browser/src/document_loader.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use std::sync::Arc;

use blitz_dom::{DocumentConfig, FontContext};
use blitz_html::{HtmlDocument, HtmlProvider};
use blitz_traits::{net::Request, shell::ShellProvider};
use dioxus_core::Task;
use dioxus_native::{SubDocumentAttr, prelude::*};
use linebender_resource_handle::Blob;

use crate::StdNetProvider;
use crate::history::{BrowserNavProvider, History, SyncStore};

pub enum DocumentLoaderStatus {
Loading { request_id: usize, task: Task },
Loading,
Idle,
}

#[derive(Clone)]
pub struct LoadedDocument {
pub document: SubDocumentAttr,
pub html_source: String,
pub title: String,
}

pub struct DocumentLoader {
pub font_ctx: FontContext,
pub net_provider: Arc<StdNetProvider>,
pub status: Signal<DocumentLoaderStatus>,
pub request_id_counter: AtomicUsize,
pub doc: Signal<Option<SubDocumentAttr>>,
pub history: SyncStore<History>,
pub html_source: Signal<String>,
pub title: Signal<String>,
pub reload_generation: Signal<u64>,
}

pub fn make_doc_config(
Expand All @@ -49,12 +49,7 @@ pub fn make_doc_config(
}

impl DocumentLoader {
pub fn new(
net_provider: Arc<StdNetProvider>,
history: SyncStore<History>,
html_source: Signal<String>,
title: Signal<String>,
) -> Self {
pub fn new(net_provider: Arc<StdNetProvider>, history: SyncStore<History>) -> Self {
let mut font_ctx = FontContext::default();
font_ctx
.collection
Expand All @@ -64,108 +59,88 @@ impl DocumentLoader {
font_ctx,
net_provider,
status: Signal::new(DocumentLoaderStatus::Idle),
request_id_counter: AtomicUsize::new(0),
doc: Signal::new(None),
history,
html_source,
title,
reload_generation: Signal::new(0),
}
}

pub fn load_document(&self, req: Request) {
pub fn reload(&self) {
let mut reload_generation = self.reload_generation;
*reload_generation.write() += 1;
}

pub fn reload_generation(&self) -> u64 {
*self.reload_generation.read()
}

pub async fn load_document(&self, req: Request) -> LoadedDocument {
if req.url.scheme() == "about" && req.url.path() == "newtab" {
if let DocumentLoaderStatus::Loading { task, .. } = *self.status.peek() {
task.cancel();
}
let config = make_doc_config(
None,
Arc::clone(&self.net_provider),
self.history,
self.font_ctx.clone(),
);
let html = include_str!("../assets/start.html");
*self.html_source.write_unchecked() = html.to_string();
let document = HtmlDocument::from_html(html, config).into_inner();
*self.title.write_unchecked() = String::new();
*self.doc.write_unchecked() = Some(SubDocumentAttr::new(document));
*self.status.write_unchecked() = DocumentLoaderStatus::Idle;
return;
return LoadedDocument {
document: SubDocumentAttr::new(document),
html_source: html.to_string(),
title: String::new(),
};
}

let request_id = self.request_id_counter.fetch_add(1, Ordering::Relaxed);
let net_provider = Arc::clone(&self.net_provider);
let font_ctx = self.font_ctx.clone();
let status = self.status;
let doc_signal = self.doc;
let history = self.history;
let html_source = self.html_source;
let title = self.title;

if let DocumentLoaderStatus::Loading { task, .. } = *self.status.peek() {
task.cancel();
};

let task = spawn(async move {
let response = net_provider.fetch_async(req).await;

// Discard response if a newer navigation has started
if let DocumentLoaderStatus::Loading {
request_id: stored_id,
..
} = *status.peek()
{
if request_id != stored_id {
tracing::debug!("Ignoring stale navigation response (id {request_id})");
return;

let response = net_provider.fetch_async(req).await;

match response {
Ok((resolved_url, bytes)) => {
tracing::info!("Loaded {}", resolved_url);
let config = make_doc_config(Some(resolved_url), net_provider, history, font_ctx);

let bytes_str;
let html: &str = if bytes.is_empty() {
include_str!("../assets/404.html")
} else {
bytes_str = String::from_utf8_lossy(&bytes);
&bytes_str
};

let document = HtmlDocument::from_html(html, config).into_inner();
let parsed_title = document
.find_title_node()
.map(|n| n.text_content())
.unwrap_or_default();
LoadedDocument {
document: SubDocumentAttr::new(document),
html_source: html.to_string(),
title: parsed_title,
}
}

match response {
Ok((resolved_url, bytes)) => {
tracing::info!("Loaded {}", resolved_url);
let config =
make_doc_config(Some(resolved_url), net_provider, history, font_ctx);

let bytes_str;
let html: &str = if bytes.is_empty() {
include_str!("../assets/404.html")
} else {
bytes_str = String::from_utf8_lossy(&bytes);
&bytes_str
};

*html_source.write_unchecked() = html.to_string();

let document = HtmlDocument::from_html(html, config).into_inner();
let parsed_title = document
.find_title_node()
.map(|n| n.text_content())
.unwrap_or_default();
*title.write_unchecked() = parsed_title;
*doc_signal.write_unchecked() = Some(SubDocumentAttr::new(document));
Err(err) => {
tracing::error!("Error loading document: {:?}", err);

let error_msg = format!("{err:?}");
let config = make_doc_config(None, net_provider, history, font_ctx);

let error_html = include_str!("../assets/error.html");
let mut document = HtmlDocument::from_html(error_html, config).into_inner();
if let Some(text_node) = document
.get_element_by_id("error")
.and_then(|el| document.get_node(el))
.and_then(|node| node.children.first().copied())
{
document.mutate().set_node_text(text_node, &error_msg);
}
Err(err) => {
tracing::error!("Error loading document: {:?}", err);

let error_msg = format!("{err:?}");
let config = make_doc_config(None, net_provider, history, font_ctx);

let error_html = include_str!("../assets/error.html");
let mut document = HtmlDocument::from_html(error_html, config).into_inner();
if let Some(text_node) = document
.get_element_by_id("error")
.and_then(|el| document.get_node(el))
.and_then(|node| node.children.first().copied())
{
document.mutate().set_node_text(text_node, &error_msg);
}
*title.write_unchecked() = String::new();
*doc_signal.write_unchecked() = Some(SubDocumentAttr::new(document));
LoadedDocument {
document: SubDocumentAttr::new(document),
html_source: error_html.to_string(),
title: String::new(),
}
}
*status.write_unchecked() = DocumentLoaderStatus::Idle;
});

*status.write_unchecked() = DocumentLoaderStatus::Loading { request_id, task };
}
}
}
6 changes: 3 additions & 3 deletions apps/browser/src/history.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use blitz_traits::navigation::{NavigationOptions, NavigationProvider};
use blitz_traits::net::{Request, Url};
use blitz_traits::net::Request;
use dioxus_native::prelude::*;

pub type SyncStore<T> = Store<T, CopyValue<T, SyncStorage>>;
Expand All @@ -11,9 +11,9 @@ pub struct History {
}

impl History {
pub fn new(initial_url: Url) -> Self {
pub fn new(initial_request: Request) -> Self {
Self {
urls: vec![Request::get(initial_url)],
urls: vec![initial_request],
current: 0,
}
}
Expand Down
53 changes: 14 additions & 39 deletions apps/browser/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,11 @@ mod tab;
mod tab_strip;
mod toolbar;

use history::HistoryNav;
use status_bar::StatusBar;
use tab::{Tab, TabId, active_tab, tab_title_or_url};
use tab::{Tab, TabId, TabStoreImplExt, TabWebView, active_tab, open_tab, tab_title_or_url};
use tab_strip::TabStrip;
use toolbar::Toolbar;

#[component]
fn TabView(tab: Tab, is_active: bool) -> Element {
use_effect(move || {
let request = (*tab.history.current_url().read()).clone();
tab.loader.load_document(request);
});

let mut tab_node_handle = tab.node_handle;
rsx!(
web-view {
class: "webview",
style: if is_active { "display: block" } else { "display: none" },
"__webview_document": tab.document.cloned(),
onmounted: move |evt: Event<MountedData>| {
let node_handle = evt.downcast::<NodeHandle>().unwrap();
*tab_node_handle.write() = Some(node_handle.clone());
},
}
)
}

static BROWSER_UI_STYLES: Asset = asset!("../assets/browser.css");
pub(crate) const IS_MOBILE: bool = cfg!(any(target_os = "android", target_os = "ios"));
pub(crate) const HOME_URL_STR: &str = "about:newtab";
Expand Down Expand Up @@ -91,16 +69,15 @@ fn app() -> Element {
let url_input_handle: Signal<Option<NodeHandle>> = use_signal(|| None);
let url_input_value = use_signal(|| home_url.to_string());

let mut tabs: Signal<Vec<Tab>> =
use_hook(|| Signal::new(vec![Tab::new(home_url.clone(), net_provider.clone())]));
let mut active_tab_id: Signal<TabId> =
use_hook(|| Signal::new(tabs.read().first().map(|t| t.id).unwrap_or(0)));
let tabs: Store<Vec<Tab>> = use_store(Vec::new);
let mut active_tab_id: Signal<TabId> = use_hook(|| {
let tab = open_tab(tabs, home_url.clone(), net_provider.clone());
Signal::new(tab.tab_id())
});

let open_new_tab = use_callback(move |url: Url| {
let new_tab = Tab::new(url, net_provider.clone());
let new_id = new_tab.id;
tabs.write().push(new_tab);
active_tab_id.set(new_id);
let new_id = open_tab(tabs, url, net_provider.clone());
active_tab_id.set(new_id.tab_id());
if let Some(handle) = url_input_handle() {
drop(handle.set_focus(true));
}
Expand Down Expand Up @@ -128,7 +105,7 @@ fn app() -> Element {
#[cfg(not(feature = "vello"))]
let fps_overlay_el = rsx!();

let window_title = tab_title_or_url(&active_tab(&tabs, active_tab_id()));
let window_title = tab_title_or_url(active_tab(tabs, active_tab_id()));

rsx!(
div {
Expand All @@ -151,13 +128,11 @@ fn app() -> Element {
active_tab_id,
show_fps,
}
for tab in tabs() {
{
let id = tab.id;
let is_active = id == active_tab_id();
rsx!(
TabView { key: "{id}", tab, is_active }
)
for tab in tabs.iter() {
TabWebView {
key: "{tab.tab_id()}",
tab,
active_tab_id,
}
}
{fps_overlay_el}
Expand Down
25 changes: 14 additions & 11 deletions apps/browser/src/status_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ use dioxus_native::prelude::*;

use crate::document_loader::DocumentLoaderStatus;
use crate::history::HistoryNav;
use crate::tab::{Tab, TabId, active_tab};
use crate::tab::{Tab, TabId, TabStoreExt, TabStoreImplExt, active_tab};

fn hovered_href(tab: &Tab) -> Option<String> {
let nh = tab.node_handle.peek();
fn hovered_href<L>(tab: Store<Tab, L>) -> Option<String>
where
L: Copy + Readable<Target = Tab> + 'static,
{
let nh = tab.node_handle().peek_unchecked();
let handle = (*nh).as_ref()?;
let node_id = handle.node_id();
// Skip this cycle if the event loop currently holds a mutable borrow.
Expand All @@ -34,7 +37,7 @@ fn hovered_href(tab: &Tab) -> Option<String> {
}

#[component]
pub fn StatusBar(tabs: Signal<Vec<Tab>>, active_tab_id: Signal<TabId>) -> Element {
pub fn StatusBar(tabs: Store<Vec<Tab>>, active_tab_id: Signal<TabId>) -> Element {
let mut hover_url: Signal<String> = use_signal(String::new);

// Hover state lives inside blitz-dom's BaseDocument, not a Dioxus signal,
Expand All @@ -44,13 +47,13 @@ pub fn StatusBar(tabs: Signal<Vec<Tab>>, active_tab_id: Signal<TabId>) -> Elemen
loop {
tokio::time::sleep(Duration::from_millis(100)).await;

let tab = active_tab(&tabs, *active_tab_id.peek());
let raw_href = hovered_href(&tab);
let tab = active_tab(tabs, active_tab_id());
let raw_href = hovered_href(tab);
// All doc borrows dropped here; safe to read history.
let found = match raw_href {
None => String::new(),
Some(raw) => {
let base = tab.history.current_url().read().url.clone();
let base = tab.nav_history().current_url().read().url.clone();
base.join(&raw).map(|u| u.to_string()).unwrap_or(raw)
}
};
Expand All @@ -61,18 +64,18 @@ pub fn StatusBar(tabs: Signal<Vec<Tab>>, active_tab_id: Signal<TabId>) -> Elemen
});
});

let tab = active_tab(&tabs, active_tab_id());
let tab = active_tab(tabs, active_tab_id());
let is_loading = matches!(
*tab.loader.status.read(),
DocumentLoaderStatus::Loading { .. }
*tab.loader_rc().status.read(),
DocumentLoaderStatus::Loading
);

let status_text = {
let hov = hover_url.read();
if !hov.is_empty() {
hov.clone()
} else if is_loading {
format!("Loading {}…", tab.history.current_url().read().url)
format!("Loading {}…", tab.nav_history().current_url().read().url)
} else {
String::new()
}
Expand Down
Loading
Loading