diff --git a/Cargo.toml b/Cargo.toml index 1e7dab4b..c876260b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ # members = ["packages/dom"] # members = ["packages/blitz", "packages/dom", "packages/dioxus-blitz"] # exclude = ["packages/blitz", "packages/dioxus-blitz"] -members = ["packages/blitz", "packages/dom", "packages/dioxus-blitz"] +members = ["packages/blitz", "packages/dom", "packages/dioxus-blitz", "packages/net", "packages/traits"] resolver = "2" [workspace.dependencies] @@ -55,6 +55,8 @@ incremental = false # mozbuild = "0.1.0" blitz = { path = "./packages/blitz" } blitz-dom = { path = "./packages/dom" } +blitz-net = { path = "./packages/net" } +blitz-traits = { path = "./packages/traits" } comrak = { git = "https://github.com/nicoburns/comrak", branch = "tasklist-class", default-features = false, features = ["syntect"] } png = { version = "0.17" } dioxus-blitz = { path = "./packages/dioxus-blitz" } diff --git a/examples/screenshot.rs b/examples/screenshot.rs index b0a62fcc..5befbf38 100644 --- a/examples/screenshot.rs +++ b/examples/screenshot.rs @@ -1,14 +1,19 @@ //! Load first CLI argument as a url. Fallback to google.com if no CLI argument is provided. use blitz::render_to_buffer; +use blitz_dom::util::Resource; use blitz_dom::{HtmlDocument, Viewport}; +use blitz_net::{MpscCallback, Provider}; +use blitz_traits::net::{SharedCallback, SharedProvider}; use reqwest::Url; +use std::sync::Arc; use std::{ fs::File, io::Write, path::{Path, PathBuf}, time::Instant, }; +use tokio::runtime::Handle; const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"; @@ -46,13 +51,36 @@ async fn main() { .and_then(|arg| arg.parse().ok()) .unwrap_or(1200); + let (mut recv, callback) = MpscCallback::new(); + let net = Arc::new(Provider::new( + Handle::current(), + Arc::new(callback) as SharedCallback, + )); + + timer.time("Setup document prerequisites"); + // Create HtmlDocument - let mut document = HtmlDocument::from_html(&html, Some(url.clone()), Vec::new()); + let mut document = HtmlDocument::from_html( + &html, + Some(url.clone()), + Vec::new(), + Arc::clone(&net) as SharedProvider, + ); + + timer.time("Parsed document"); + document .as_mut() .set_viewport(Viewport::new(width * scale, height * scale, scale as f32)); - timer.time("Created document (+ fetched assets)"); + while let Some(res) = recv.recv().await { + document.as_mut().load_resource(res); + if net.is_empty() { + break; + } + } + + timer.time("Fetched assets"); // Compute style, layout, etc for HtmlDocument document.as_mut().resolve(); diff --git a/packages/dioxus-blitz/Cargo.toml b/packages/dioxus-blitz/Cargo.toml index badf1515..87ded9ba 100644 --- a/packages/dioxus-blitz/Cargo.toml +++ b/packages/dioxus-blitz/Cargo.toml @@ -26,6 +26,8 @@ style = { workspace = true } tracing = { workspace = true, optional = true } blitz = { path = "../blitz" } blitz-dom = { path = "../dom" } +blitz-net = { path = "../net" } +blitz-traits = { path = "../traits" } url = { version = "2.5.0", features = ["serde"] } ureq = "2.9" rustc-hash = "1.1.0" diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index f933a4db..40d38b23 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -23,6 +23,7 @@ use rustc_hash::FxHashMap; use style::{ data::{ElementData, ElementStyles}, properties::{style_structs::Font, ComputedValues}, + stylesheets::Origin, }; use super::event_handler::{NativeClickData, NativeConverter, NativeFormData}; @@ -269,7 +270,7 @@ impl DioxusDocument { root_node.children.push(html_element_id); // Include default and user-specified stylesheets - doc.add_stylesheet(DEFAULT_CSS); + doc.add_user_agent_stylesheet(DEFAULT_CSS); let state = DioxusState::create(&mut doc); let mut doc = Self { @@ -492,7 +493,8 @@ impl WriteMutations for MutationWriter<'_> { let parent = self.doc.get_node(parent).unwrap(); if let NodeData::Element(ref element) = parent.raw_dom_data { if element.name.local.as_ref() == "style" { - self.doc.add_stylesheet(value); + let sheet = self.doc.make_stylesheet(value, Origin::Author); + self.doc.add_stylesheet_for_node(sheet, parent.id); } } } @@ -617,7 +619,6 @@ impl WriteMutations for MutationWriter<'_> { // todo: this is very inefficient for inline styles - lots of string cloning going on let changed = text.content != value; text.content = value.to_string(); - let contents = text.content.clone(); if let Some(parent) = node.parent { // if the text is the child of a style element, we want to put the style into the stylesheet cache @@ -625,8 +626,8 @@ impl WriteMutations for MutationWriter<'_> { if let NodeData::Element(ref element) = parent.raw_dom_data { // Only set stylsheets if the text content has *changed* - we need to unload if changed && element.name.local.as_ref() == "style" { - self.doc.add_stylesheet(value); - self.doc.remove_stylehsheet(&contents); + let sheet = self.doc.make_stylesheet(value, Origin::Author); + self.doc.add_stylesheet_for_node(sheet, parent.id); } } } diff --git a/packages/dioxus-blitz/src/lib.rs b/packages/dioxus-blitz/src/lib.rs index f53914fd..0897d31b 100644 --- a/packages/dioxus-blitz/src/lib.rs +++ b/packages/dioxus-blitz/src/lib.rs @@ -21,22 +21,29 @@ mod menu; #[cfg(feature = "accessibility")] mod accessibility; +use crate::application::Application; +pub use crate::documents::DioxusDocument; +pub use crate::waker::BlitzEvent; +use crate::waker::BlitzWindowEvent; +use crate::window::View; +pub use crate::window::WindowConfig; +use blitz_dom::util::Resource; use blitz_dom::{DocumentLike, HtmlDocument}; +use blitz_net::Provider; +use blitz_traits::net::SharedCallback; use dioxus::prelude::{ComponentFunction, Element, VirtualDom}; +use std::ops::DerefMut; +use std::sync::{Arc, Mutex}; +use tokio::runtime::Runtime; use url::Url; +use winit::event_loop::EventLoopProxy; +use winit::window::WindowId; use winit::{ dpi::LogicalSize, event_loop::{ControlFlow, EventLoop}, window::Window, }; -use crate::application::Application; -use crate::window::View; - -pub use crate::documents::DioxusDocument; -pub use crate::waker::BlitzEvent; -pub use crate::window::WindowConfig; - pub mod exports { pub use dioxus; } @@ -67,7 +74,14 @@ pub fn launch_cfg_with_props( let vdom = VirtualDom::new_with_props(root, props); let document = DioxusDocument::new(vdom); - launch_with_document(document) + // Turn on the runtime and enter it + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + let _guard = rt.enter(); + launch_with_document(document, rt, None) } pub fn launch_url(url: &str) { @@ -99,11 +113,25 @@ pub fn launch_static_html(html: &str) { } pub fn launch_static_html_cfg(html: &str, cfg: Config) { - let document = HtmlDocument::from_html(html, cfg.base_url, cfg.stylesheets); - launch_with_document(document) + // Turn on the runtime and enter it + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + let _guard = rt.enter(); + + let net_callback = Arc::new(Callback::new()); + let net = Provider::new( + rt.handle().clone(), + Arc::clone(&net_callback) as SharedCallback, + ); + + let document = HtmlDocument::from_html(html, cfg.base_url, cfg.stylesheets, Arc::new(net)); + launch_with_document(document, rt, Some(net_callback)); } -fn launch_with_document(doc: impl DocumentLike) { +fn launch_with_document(doc: impl DocumentLike, rt: Runtime, net_callback: Option>) { let mut window_attrs = Window::default_attributes(); if !cfg!(all(target_os = "android", target_os = "ios")) { window_attrs.inner_size = Some( @@ -114,20 +142,12 @@ fn launch_with_document(doc: impl DocumentLike) { .into(), ); } - let window = WindowConfig::new(doc); + let window = WindowConfig::new(doc, net_callback); - launch_with_window(window) + launch_with_window(window, rt) } -fn launch_with_window(window: WindowConfig) { - // Turn on the runtime and enter it - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - - let _guard = rt.enter(); - +fn launch_with_window(window: WindowConfig, rt: Runtime) { // Build an event loop for the application let mut ev_builder = EventLoop::::with_user_event(); #[cfg(target_os = "android")] @@ -182,3 +202,43 @@ pub fn set_android_app(app: android_activity::AndroidApp) { pub fn current_android_app(app: android_activity::AndroidApp) -> AndroidApp { ANDROID_APP.get().unwrap().clone() } + +pub struct Callback(Mutex); +enum CallbackInner { + Window(WindowId, EventLoopProxy), + Queue(Vec), +} +impl Callback { + fn new() -> Self { + Self(Mutex::new(CallbackInner::Queue(Vec::new()))) + } + fn init(self: Arc, window_id: WindowId, proxy: &EventLoopProxy) { + let old = std::mem::replace( + self.0.lock().unwrap().deref_mut(), + CallbackInner::Window(window_id, proxy.clone()), + ); + match old { + CallbackInner::Window(..) => {} + CallbackInner::Queue(mut queue) => queue + .drain(..) + .for_each(|res| Self::send_event(&window_id, proxy, res)), + } + } + fn send_event(window_id: &WindowId, proxy: &EventLoopProxy, data: Resource) { + proxy + .send_event(BlitzEvent::Window { + window_id: *window_id, + data: BlitzWindowEvent::ResourceLoad(data), + }) + .unwrap() + } +} +impl blitz_traits::net::Callback for Callback { + type Data = Resource; + fn call(self: Arc, data: Self::Data) { + match self.0.lock().unwrap().deref_mut() { + CallbackInner::Window(wid, proxy) => Self::send_event(wid, proxy, data), + CallbackInner::Queue(queue) => queue.push(data), + } + } +} diff --git a/packages/dioxus-blitz/src/waker.rs b/packages/dioxus-blitz/src/waker.rs index 0103e03b..e34811eb 100644 --- a/packages/dioxus-blitz/src/waker.rs +++ b/packages/dioxus-blitz/src/waker.rs @@ -5,6 +5,7 @@ use winit::{event_loop::EventLoopProxy, window::WindowId}; #[cfg(feature = "accessibility")] use accesskit_winit::Event as AccessibilityEvent; use accesskit_winit::WindowEvent as AccessibilityWindowEvent; +use blitz_dom::util::Resource; #[derive(Debug, Clone)] pub enum BlitzEvent { @@ -12,7 +13,6 @@ pub enum BlitzEvent { window_id: WindowId, data: BlitzWindowEvent, }, - /// A hotreload event, basically telling us to update our templates. #[cfg(all( feature = "hot-reload", @@ -22,6 +22,14 @@ pub enum BlitzEvent { ))] HotReloadEvent(dioxus_hot_reload::HotReloadMsg), } +impl From<(WindowId, Resource)> for BlitzEvent { + fn from((window_id, resource): (WindowId, Resource)) -> Self { + BlitzEvent::Window { + window_id, + data: BlitzWindowEvent::ResourceLoad(resource), + } + } +} #[cfg(feature = "accessibility")] impl From for BlitzEvent { @@ -37,7 +45,7 @@ impl From for BlitzEvent { #[derive(Debug, Clone)] pub enum BlitzWindowEvent { Poll, - + ResourceLoad(Resource), /// An accessibility event from `accesskit`. #[cfg(feature = "accessibility")] Accessibility(Arc), diff --git a/packages/dioxus-blitz/src/window.rs b/packages/dioxus-blitz/src/window.rs index 05a34695..d5e52be7 100644 --- a/packages/dioxus-blitz/src/window.rs +++ b/packages/dioxus-blitz/src/window.rs @@ -1,6 +1,6 @@ use crate::accessibility::AccessibilityState; -use crate::stylo_to_winit; use crate::waker::{create_waker, BlitzEvent, BlitzWindowEvent}; +use crate::{stylo_to_winit, Callback}; use blitz::{Devtools, Renderer}; use blitz_dom::events::{EventData, RendererEvent}; use blitz_dom::{DocumentLike, Viewport}; @@ -22,18 +22,28 @@ use crate::menu::init_menu; pub struct WindowConfig { doc: Doc, attributes: WindowAttributes, + callback: Option>, } impl WindowConfig { - pub fn new(doc: Doc) -> Self { + pub fn new(doc: Doc, callback: Option>) -> Self { WindowConfig { doc, attributes: Window::default_attributes(), + callback, } } - pub fn with_attributes(doc: Doc, attributes: WindowAttributes) -> Self { - WindowConfig { doc, attributes } + pub fn with_attributes( + doc: Doc, + attributes: WindowAttributes, + callback: Option>, + ) -> Self { + WindowConfig { + doc, + attributes, + callback, + } } } @@ -75,6 +85,10 @@ impl View { ) -> Self { let winit_window = Arc::from(event_loop.create_window(config.attributes).unwrap()); + if let Some(callback) = config.callback { + callback.init(winit_window.id(), proxy); + } + // TODO: make this conditional on text input focus winit_window.set_ime_allowed(true); @@ -237,6 +251,10 @@ impl View { BlitzWindowEvent::Poll => { self.poll(); } + BlitzWindowEvent::ResourceLoad(resource) => { + self.dom.as_mut().load_resource(resource); + self.request_redraw(); + } #[cfg(feature = "accessibility")] BlitzWindowEvent::Accessibility(accessibility_event) => { match &*accessibility_event { diff --git a/packages/dom/Cargo.toml b/packages/dom/Cargo.toml index 52827d9a..2bb81b93 100644 --- a/packages/dom/Cargo.toml +++ b/packages/dom/Cargo.toml @@ -8,6 +8,7 @@ default = ["tracing"] tracing = ["dep:tracing"] [dependencies] +blitz-traits = { path = "../traits" } style = { workspace = true, features = ["servo"] } selectors = { workspace = true } style_config = { workspace = true } @@ -26,7 +27,6 @@ string_cache = "0.8.7" html-escape = "0.2.13" url = { version = "2.5.0", features = ["serde"] } data-url = "0.3.1" -ureq = "2.9" image = "0.25.2" winit = { version = "0.30.4", default-features = false } usvg = "0.42.0" diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index b8b92296..9f5a7145 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -1,15 +1,16 @@ use crate::events::{EventData, HitResult, RendererEvent}; -use crate::node::{NodeSpecificData, TextBrush}; +use crate::node::{ImageData, NodeSpecificData, TextBrush}; use crate::{ElementNodeData, Node, NodeData, TextNodeData, Viewport}; use app_units::Au; use html5ever::local_name; use peniko::kurbo; // use quadtree_rs::Quadtree; +use crate::util::Resource; use parley::editor::{PointerButton, TextEvent}; use selectors::{matching::QuirksMode, Element}; use slab::Slab; use std::any::Any; -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use style::selector_parser::ServoElementSnapshot; use style::servo::media_queries::FontMetricsProvider; use style::servo_arc::Arc as ServoArc; @@ -99,7 +100,11 @@ pub struct Document { // Scroll within our viewport pub(crate) viewport_scroll: kurbo::Point, - pub(crate) stylesheets: HashMap, + /// Stylesheets added by the useragent + /// where the key is the hashed CSS + pub(crate) ua_stylesheets: HashMap, + + pub(crate) nodes_to_stylesheet: BTreeMap, /// A Parley font context pub(crate) font_ctx: parley::FontContext, @@ -232,7 +237,8 @@ impl Document { viewport_scroll: kurbo::Point::ZERO, base_url: None, // quadtree: Quadtree::new(20), - stylesheets: HashMap::new(), + ua_stylesheets: HashMap::new(), + nodes_to_stylesheet: BTreeMap::new(), font_ctx, layout_ctx: parley::LayoutContext::new(), @@ -516,24 +522,31 @@ impl Document { pub fn process_style_element(&mut self, target_id: usize) { let css = self.nodes[target_id].text_content(); let css = html_escape::decode_html_entities(&css); - self.add_stylesheet(&css); + let sheet = self.make_stylesheet(&css, Origin::Author); + self.add_stylesheet_for_node(sheet, target_id); } - pub fn remove_stylehsheet(&mut self, contents: &str) { - if let Some(sheet) = self.stylesheets.remove(contents) { + pub fn remove_user_agent_stylesheet(&mut self, contents: &str) { + if let Some(sheet) = self.ua_stylesheets.remove(contents) { self.stylist.remove_stylesheet(sheet, &self.guard.read()); } } - pub fn add_stylesheet(&mut self, css: &str) { + pub fn add_user_agent_stylesheet(&mut self, css: &str) { + let sheet = self.make_stylesheet(css, Origin::UserAgent); + self.ua_stylesheets.insert(css.to_string(), sheet.clone()); + self.stylist.append_stylesheet(sheet, &self.guard.read()); + } + + pub fn make_stylesheet(&self, css: impl AsRef, origin: Origin) -> DocumentStyleSheet { let data = Stylesheet::from_str( - css, + css.as_ref(), UrlExtraData::from( "data:text/css;charset=utf-8;base64," .parse::() .unwrap(), ), - Origin::UserAgent, + origin, ServoArc::new(self.guard.wrap(MediaList::empty())), self.guard.clone(), None, @@ -542,14 +555,53 @@ impl Document { AllowImportRules::Yes, ); - let sheet = DocumentStyleSheet(ServoArc::new(data)); + DocumentStyleSheet(ServoArc::new(data)) + } - self.stylesheets.insert(css.to_string(), sheet.clone()); + pub fn add_stylesheet_for_node(&mut self, stylesheet: DocumentStyleSheet, node_id: usize) { + let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone()); - self.stylist.append_stylesheet(sheet, &self.guard.read()); + if let Some(old) = old { + self.stylist.remove_stylesheet(old, &self.guard.read()) + } + + // TODO: Nodes could potentially get reused so ordering by node_id might be wrong. + let insertion_point = self + .nodes_to_stylesheet + .range((Bound::Excluded(node_id), Bound::Unbounded)) + .next() + .map(|(_, sheet)| sheet); + + if let Some(insertion_point) = insertion_point { + self.stylist.insert_stylesheet_before( + stylesheet, + insertion_point.clone(), + &self.guard.read(), + ) + } else { + self.stylist + .append_stylesheet(stylesheet, &self.guard.read()) + } + } - self.stylist - .force_stylesheet_origins_dirty(Origin::Author.into()); + pub fn load_resource(&mut self, resource: Resource) { + match resource { + Resource::Css(node_id, css) => { + self.add_stylesheet_for_node(css, node_id); + } + Resource::Image(node_id, image) => { + let node = self.get_node_mut(node_id).unwrap(); + node.element_data_mut().unwrap().node_specific_data = + NodeSpecificData::Image(ImageData::new(image)) + } + Resource::Svg(node_id, tree) => { + let node = self.get_node_mut(node_id).unwrap(); + node.element_data_mut().unwrap().node_specific_data = NodeSpecificData::Svg(*tree) + } + Resource::Font(bytes) => { + self.font_ctx.collection.register_fonts(bytes.to_vec()); + } + } } pub fn snapshot_node(&mut self, node_id: usize) { @@ -757,6 +809,11 @@ impl Document { resolve_layout_children_recursive(self, root_node_id); pub fn resolve_layout_children_recursive(doc: &mut Document, node_id: usize) { + doc.nodes[node_id].is_inline_root = false; + if let Some(element_data) = doc.nodes[node_id].element_data_mut() { + element_data.take_inline_layout(); + } + doc.ensure_layout_children(node_id); let children = std::mem::take(&mut doc.nodes[node_id].children); diff --git a/packages/dom/src/html_document.rs b/packages/dom/src/html_document.rs index 7924267a..286de5a4 100644 --- a/packages/dom/src/html_document.rs +++ b/packages/dom/src/html_document.rs @@ -1,7 +1,9 @@ use crate::events::RendererEvent; use crate::{Document, DocumentHtmlParser, DocumentLike, Viewport}; +use crate::util::Resource; use crate::DEFAULT_CSS; +use blitz_traits::net::SharedProvider; pub struct HtmlDocument { inner: Document, @@ -31,7 +33,12 @@ impl DocumentLike for HtmlDocument { } impl HtmlDocument { - pub fn from_html(html: &str, base_url: Option, stylesheets: Vec) -> Self { + pub fn from_html( + html: &str, + base_url: Option, + stylesheets: Vec, + net: SharedProvider, + ) -> Self { // Spin up the virtualdom and include the default stylesheet let viewport = Viewport::new(0, 0, 1.0); let mut dom = Document::new(viewport); @@ -42,13 +49,13 @@ impl HtmlDocument { } // Include default and user-specified stylesheets - dom.add_stylesheet(DEFAULT_CSS); + dom.add_user_agent_stylesheet(DEFAULT_CSS); for ss in &stylesheets { - dom.add_stylesheet(ss); + dom.add_user_agent_stylesheet(ss); } // Parse HTML string into document - DocumentHtmlParser::parse_into_doc(&mut dom, html); + DocumentHtmlParser::parse_into_doc(&mut dom, net, html); HtmlDocument { inner: dom } } diff --git a/packages/dom/src/htmlsink.rs b/packages/dom/src/htmlsink.rs index fd222147..abea173a 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -1,11 +1,11 @@ +use crate::util::{CssHandler, ImageHandler, Resource}; use std::borrow::Cow; use std::cell::{Cell, Ref, RefCell, RefMut}; use std::collections::HashSet; -use std::sync::Arc; -use crate::node::{Attribute, ElementNodeData, ImageData, Node, NodeData, NodeSpecificData}; -use crate::util::ImageOrSvg; +use crate::node::{Attribute, ElementNodeData, Node, NodeData}; use crate::Document; +use blitz_traits::net::SharedProvider; use html5ever::local_name; use html5ever::{ tendril::{StrTendril, TendrilSink}, @@ -32,20 +32,27 @@ pub struct DocumentHtmlParser<'a> { /// The document's quirks mode. pub quirks_mode: Cell, + + pub net_provider: SharedProvider, } impl<'a> DocumentHtmlParser<'a> { - pub fn new(doc: &mut Document) -> DocumentHtmlParser { + pub fn new(doc: &mut Document, net_provider: SharedProvider) -> DocumentHtmlParser { DocumentHtmlParser { doc: RefCell::new(doc), style_nodes: RefCell::new(Vec::new()), errors: RefCell::new(Vec::new()), quirks_mode: Cell::new(QuirksMode::NoQuirks), + net_provider, } } - pub fn parse_into_doc<'d>(doc: &'d mut Document, html: &str) -> &'d mut Document { - let sink = Self::new(doc); + pub fn parse_into_doc<'d>( + doc: &'d mut Document, + net: SharedProvider, + html: &str, + ) -> &'d mut Document { + let sink = Self::new(doc, net); html5ever::parse_document(sink, Default::default()) .from_utf8() .read_from(&mut html.as_bytes()) @@ -99,15 +106,16 @@ impl<'a> DocumentHtmlParser<'a> { if let (Some("stylesheet"), Some(href)) = (rel_attr, href_attr) { let url = self.doc.borrow().resolve_url(href); - match crate::util::fetch_string(url.as_str()) { - Ok(css) => { - let css = html_escape::decode_html_entities(&css); - drop(url); - drop(node); - self.doc.borrow_mut().add_stylesheet(&css); - } - Err(_) => eprintln!("Error fetching stylesheet {}", url), - } + let guard = self.doc.borrow().guard.clone(); + self.net_provider.fetch( + url.clone(), + Box::new(CssHandler { + node: target_id, + source_url: url, + guard, + provider: self.net_provider.clone(), + }), + ); } } @@ -116,28 +124,8 @@ impl<'a> DocumentHtmlParser<'a> { if let Some(raw_src) = node.attr(local_name!("src")) { if !raw_src.is_empty() { let src = self.doc.borrow().resolve_url(raw_src); - drop(node); - - // FIXME: Image fetching should not be a synchronous network request during parsing - let image_result = crate::util::fetch_image(src.as_str()); - match image_result { - Ok(ImageOrSvg::Image(image)) => { - self.node_mut(target_id) - .element_data_mut() - .unwrap() - .node_specific_data = - NodeSpecificData::Image(ImageData::new(Arc::new(image))); - } - Ok(ImageOrSvg::Svg(svg)) => { - self.node_mut(target_id) - .element_data_mut() - .unwrap() - .node_specific_data = NodeSpecificData::Svg(svg); - } - Err(_) => { - eprintln!("Error fetching image {}", src); - } - } + self.net_provider + .fetch(src, Box::new(ImageHandler::new(target_id))); } } } @@ -387,11 +375,13 @@ impl<'b> TreeSink for DocumentHtmlParser<'b> { #[test] fn parses_some_html() { use crate::Viewport; + use blitz_traits::net::DummyProvider; + use std::sync::Arc; let html = "

hello world

"; let viewport = Viewport::new(800, 600, 1.0); let mut doc = Document::new(viewport); - let sink = DocumentHtmlParser::new(&mut doc); + let sink = DocumentHtmlParser::new(&mut doc, Arc::new(DummyProvider::default())); html5ever::parse_document(sink, Default::default()) .from_utf8() diff --git a/packages/dom/src/util.rs b/packages/dom/src/util.rs index fde88ec5..0101074c 100644 --- a/packages/dom/src/util.rs +++ b/packages/dom/src/util.rs @@ -1,152 +1,123 @@ +use crate::node::{Node, NodeData}; +use image::DynamicImage; +use selectors::context::QuirksMode; +use std::str::FromStr; use std::{ - io::{Cursor, Read}, + io::Cursor, sync::{Arc, OnceLock}, - time::Instant, }; - -use crate::node::{Node, NodeData}; -use image::DynamicImage; - -const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"; -const FILE_SIZE_LIMIT: u64 = 1_000_000_000; // 1GB +use style::{ + color::AbsoluteColor, + media_queries::MediaList, + servo_arc::Arc as ServoArc, + shared_lock::SharedRwLock, + stylesheets::{AllowImportRules, DocumentStyleSheet, Origin, Stylesheet}, +}; +use url::Url; +use usvg::Tree; static FONT_DB: OnceLock> = OnceLock::new(); -pub(crate) enum FetchErr { - UrlParse(url::ParseError), - Ureq(Box), - FileIo(std::io::Error), +#[derive(Clone, Debug)] +pub enum Resource { + Image(usize, Arc), + Svg(usize, Box), + Css(usize, DocumentStyleSheet), + Font(Bytes), } -impl From for FetchErr { - fn from(value: url::ParseError) -> Self { - Self::UrlParse(value) - } +pub(crate) struct CssHandler { + pub node: usize, + pub source_url: Url, + pub guard: SharedRwLock, + pub provider: SharedProvider, } -impl From> for FetchErr { - fn from(value: Box) -> Self { - Self::Ureq(value) +impl RequestHandler for CssHandler { + type Data = Resource; + fn bytes(self: Box, bytes: Bytes, callback: SharedCallback) { + let css = std::str::from_utf8(&bytes).expect("Invalid UTF8"); + let escaped_css = html_escape::decode_html_entities(css); + let sheet = Stylesheet::from_str( + &escaped_css, + self.source_url.into(), + Origin::Author, + ServoArc::new(self.guard.wrap(MediaList::empty())), + self.guard.clone(), + None, + None, + QuirksMode::NoQuirks, + AllowImportRules::Yes, + ); + let read_guard = self.guard.read(); + + sheet + .rules(&read_guard) + .iter() + .filter_map(|rule| match rule { + CssRule::FontFace(font_face) => font_face.read_with(&read_guard).sources.as_ref(), + _ => None, + }) + .flat_map(|source_list| &source_list.0) + .filter_map(|source| match source { + Source::Url(url_source) => Some(url_source), + _ => None, + }) + .for_each(|url_source| { + self.provider.fetch( + Url::from_str(url_source.url.as_str()).unwrap(), + Box::new(FontFaceHandler), + ) + }); + + callback.call(Resource::Css( + self.node, + DocumentStyleSheet(ServoArc::new(sheet)), + )) } } -impl From for FetchErr { - fn from(value: std::io::Error) -> Self { - Self::FileIo(value) +struct FontFaceHandler; +impl RequestHandler for FontFaceHandler { + type Data = Resource; + fn bytes(self: Box, bytes: Bytes, callback: SharedCallback) { + callback.call(Resource::Font(bytes)) } } - -pub(crate) fn fetch_blob(url: &str) -> Result, FetchErr> { - let start = Instant::now(); - - // Handle data URIs - if url.starts_with("data:") { - let data_url = data_url::DataUrl::process(url).unwrap(); - let decoded = data_url.decode_to_vec().expect("Invalid data url"); - return Ok(decoded.0); +pub(crate) struct ImageHandler(usize); +impl ImageHandler { + pub(crate) fn new(node_id: usize) -> Self { + Self(node_id) } - - // Handle file:// URLs - let parsed_url = Url::parse(url)?; - if parsed_url.scheme() == "file" { - let file_content = std::fs::read(parsed_url.path())?; - return Ok(file_content); - } - - let resp = ureq::get(url) - .set("User-Agent", USER_AGENT) - .call() - .map_err(Box::new)?; - - let len: usize = resp - .header("Content-Length") - .and_then(|c| c.parse().ok()) - .unwrap_or(0); - let mut bytes: Vec = Vec::with_capacity(len); - - resp.into_reader() - .take(FILE_SIZE_LIMIT) - .read_to_end(&mut bytes) - .unwrap(); - - let time = (Instant::now() - start).as_millis(); - println!("Fetched {} in {}ms", url, time); - - Ok(bytes) } - -pub(crate) fn fetch_string(url: &str) -> Result { - fetch_blob(url).map(|vec| String::from_utf8(vec).expect("Invalid UTF8")) -} - -// pub(crate) fn fetch_buffered_stream( -// url: &str, -// ) -> Result { -// let resp = ureq::get(url).set("User-Agent", USER_AGENT).call()?; -// Ok(BufReader::new(resp.into_reader().take(FILE_SIZE_LIMIT))) -// } - -pub(crate) enum ImageOrSvg { - Image(DynamicImage), - Svg(usvg::Tree), -} - -#[allow(unused)] -pub(crate) enum ImageFetchErr { - UrlParse(url::ParseError), - Ureq(Box), - FileIo(std::io::Error), - ImageParse(image::error::ImageError), - SvgParse(usvg::Error), -} - -impl From for ImageFetchErr { - fn from(value: FetchErr) -> Self { - match value { - FetchErr::UrlParse(err) => Self::UrlParse(err), - FetchErr::Ureq(err) => Self::Ureq(err), - FetchErr::FileIo(err) => Self::FileIo(err), - } - } -} -impl From for ImageFetchErr { - fn from(value: image::error::ImageError) -> Self { - Self::ImageParse(value) - } -} -impl From for ImageFetchErr { - fn from(value: usvg::Error) -> Self { - Self::SvgParse(value) +impl RequestHandler for ImageHandler { + type Data = Resource; + fn bytes(self: Box, bytes: Bytes, callback: SharedCallback) { + // Try parse image + if let Ok(image) = image::ImageReader::new(Cursor::new(&bytes)) + .with_guessed_format() + .expect("IO errors impossible with Cursor") + .decode() + { + callback.call(Resource::Image(self.0, Arc::new(image))); + return; + }; + // Try parse SVG + + // TODO: Use fontique + let fontdb = FONT_DB.get_or_init(|| { + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_system_fonts(); + Arc::new(fontdb) + }); + + let options = usvg::Options { + fontdb: fontdb.clone(), + ..Default::default() + }; + + let tree = Tree::from_data(&bytes, &options).unwrap(); + callback.call(Resource::Svg(self.0, Box::new(tree))); } } -pub(crate) fn fetch_image(url: &str) -> Result { - let blob = crate::util::fetch_blob(url)?; - - // Try parse image - if let Ok(image) = image::ImageReader::new(Cursor::new(&blob)) - .with_guessed_format() - .expect("IO errors impossible with Cursor") - .decode() - { - return Ok(ImageOrSvg::Image(image)); - }; - - // Try parse SVG - - // TODO: Use fontique - let fontdb = FONT_DB.get_or_init(|| { - let mut fontdb = usvg::fontdb::Database::new(); - fontdb.load_system_fonts(); - Arc::new(fontdb) - }); - - let options = usvg::Options { - fontdb: fontdb.clone(), - ..Default::default() - }; - - let tree = usvg::Tree::from_data(&blob, &options)?; - Ok(ImageOrSvg::Svg(tree)) -} - // Debug print an RcDom pub fn walk_tree(indent: usize, node: &Node) { // Skip all-whitespace text nodes entirely @@ -212,9 +183,10 @@ pub fn walk_tree(indent: usize, node: &Node) { } } +use blitz_traits::net::{Bytes, RequestHandler, SharedCallback, SharedProvider}; use peniko::Color as PenikoColor; -use style::color::AbsoluteColor; -use url::Url; +use style::font_face::Source; +use style::stylesheets::{CssRule, StylesheetInDocument}; pub trait ToPenikoColor { fn as_peniko(&self) -> PenikoColor; diff --git a/packages/net/Cargo.toml b/packages/net/Cargo.toml new file mode 100644 index 00000000..bdcad152 --- /dev/null +++ b/packages/net/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "blitz-net" +version = "0.1.0" +edition = "2021" + +[dependencies] +blitz-traits = { path = "../traits" } +tokio = { workspace = true } +reqwest = { version = "0.12.7" } +data-url = "0.3.1" +thiserror = "1.0.63" diff --git a/packages/net/src/lib.rs b/packages/net/src/lib.rs new file mode 100644 index 00000000..945e8cc1 --- /dev/null +++ b/packages/net/src/lib.rs @@ -0,0 +1,98 @@ +use blitz_traits::net::{BoxedHandler, Bytes, Callback, NetProvider, SharedCallback, Url}; +use data_url::DataUrl; +use reqwest::Client; +use std::sync::Arc; +use thiserror::Error; +use tokio::{ + runtime::Handle, + sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, +}; + +const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"; + +pub struct Provider { + rt: Handle, + client: Client, + callback: SharedCallback, +} +impl Provider { + pub fn new(rt_handle: Handle, callback: SharedCallback) -> Self { + Self { + rt: rt_handle, + client: Client::new(), + callback, + } + } +} +impl Provider { + pub fn is_empty(&self) -> bool { + Arc::strong_count(&self.callback) == 1 + } + async fn fetch_inner( + client: Client, + url: Url, + callback: SharedCallback, + handler: BoxedHandler, + ) -> Result<(), ProviderError> { + match url.scheme() { + "data" => { + let data_url = DataUrl::process(url.as_str())?; + let decoded = data_url.decode_to_vec()?; + handler.bytes(Bytes::from(decoded.0), callback); + } + "file" => { + let file_content = std::fs::read(url.path())?; + handler.bytes(Bytes::from(file_content), callback); + } + _ => { + let response = client + .request(handler.method(), url) + .header("User-Agent", USER_AGENT) + .send() + .await?; + handler.bytes(response.bytes().await?, callback); + } + } + Ok(()) + } +} + +impl NetProvider for Provider { + type Data = D; + fn fetch(&self, url: Url, handler: BoxedHandler) { + let client = self.client.clone(); + let callback = Arc::clone(&self.callback); + drop(self.rt.spawn(async { + let res = Self::fetch_inner(client, url, callback, handler).await; + if let Err(e) = res { + eprintln!("{e}"); + } + })); + } +} + +#[derive(Error, Debug)] +enum ProviderError { + #[error("{0}")] + Io(#[from] std::io::Error), + #[error("{0}")] + DataUrl(#[from] data_url::DataUrlError), + #[error("{0}")] + DataUrlBas64(#[from] data_url::forgiving_base64::InvalidBase64), + #[error("{0}")] + ReqwestError(#[from] reqwest::Error), +} + +pub struct MpscCallback(UnboundedSender); +impl MpscCallback { + pub fn new() -> (UnboundedReceiver, Self) { + let (send, recv) = unbounded_channel(); + (recv, Self(send)) + } +} +impl Callback for MpscCallback { + type Data = T; + fn call(self: Arc, data: Self::Data) { + let _ = self.0.send(data); + } +} diff --git a/packages/traits/Cargo.toml b/packages/traits/Cargo.toml new file mode 100644 index 00000000..e450a140 --- /dev/null +++ b/packages/traits/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "blitz-traits" +version = "0.1.0" +edition = "2021" + +[dependencies] +http = "1.1.0" +url = "2.5.2" +bytes = "1.7.1" \ No newline at end of file diff --git a/packages/traits/src/lib.rs b/packages/traits/src/lib.rs new file mode 100644 index 00000000..f9faf2ff --- /dev/null +++ b/packages/traits/src/lib.rs @@ -0,0 +1 @@ +pub mod net; diff --git a/packages/traits/src/net.rs b/packages/traits/src/net.rs new file mode 100644 index 00000000..f16202d9 --- /dev/null +++ b/packages/traits/src/net.rs @@ -0,0 +1,38 @@ +pub use bytes::Bytes; +pub use http::Method; +use std::marker::PhantomData; +use std::sync::Arc; +pub use url::Url; + +pub type BoxedHandler = Box>; +pub type SharedCallback = Arc>; +pub type SharedProvider = Arc>; + +pub trait NetProvider: Send + Sync + 'static { + type Data; + fn fetch(&self, url: Url, handler: BoxedHandler); +} + +pub trait RequestHandler: Send + Sync + 'static { + type Data; + fn bytes(self: Box, bytes: Bytes, callback: SharedCallback); + fn method(&self) -> Method { + Method::GET + } +} + +pub trait Callback: Send + Sync + 'static { + type Data; + fn call(self: Arc, data: Self::Data); +} + +pub struct DummyProvider(PhantomData); +impl Default for DummyProvider { + fn default() -> Self { + Self(PhantomData) + } +} +impl NetProvider for DummyProvider { + type Data = D; + fn fetch(&self, _url: Url, _handler: BoxedHandler) {} +}