From 3c5f8fe72c837631f5ec92f88e1bffd080738bad Mon Sep 17 00:00:00 2001 From: koko Date: Tue, 20 Aug 2024 09:49:50 +0200 Subject: [PATCH 01/24] Adding async netcode --- Cargo.toml | 2 +- packages/dioxus-blitz/Cargo.toml | 2 + packages/dioxus-blitz/src/application.rs | 2 +- packages/dioxus-blitz/src/lib.rs | 31 ++++-- packages/dioxus-blitz/src/waker.rs | 15 ++- packages/dioxus-blitz/src/window.rs | 45 +++++++- packages/dom/src/html_document.rs | 5 +- packages/dom/src/htmlsink.rs | 53 +++------- packages/dom/src/util.rs | 124 ++++++++++++++--------- packages/net/Cargo.toml | 13 +++ packages/net/src/lib.rs | 77 ++++++++++++++ 11 files changed, 262 insertions(+), 107 deletions(-) create mode 100644 packages/net/Cargo.toml create mode 100644 packages/net/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 55060ab5..d990357d 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"] resolver = "2" [workspace.dependencies] diff --git a/packages/dioxus-blitz/Cargo.toml b/packages/dioxus-blitz/Cargo.toml index badf1515..f7573f96 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" } +html-escape = "0.2.13" url = { version = "2.5.0", features = ["serde"] } ureq = "2.9" rustc-hash = "1.1.0" diff --git a/packages/dioxus-blitz/src/application.rs b/packages/dioxus-blitz/src/application.rs index 10db473a..c6c41000 100644 --- a/packages/dioxus-blitz/src/application.rs +++ b/packages/dioxus-blitz/src/application.rs @@ -46,7 +46,7 @@ impl ApplicationHandler for Application { // Initialise pending windows for window_config in self.pending_windows.drain(..) { - let mut view = View::init(window_config, event_loop, &self.proxy); + let mut view = View::init(window_config, event_loop, &self.proxy, &self.rt); view.resume(&self.rt); if !view.renderer.is_active() { continue; diff --git a/packages/dioxus-blitz/src/lib.rs b/packages/dioxus-blitz/src/lib.rs index 19a00312..57edaeb1 100644 --- a/packages/dioxus-blitz/src/lib.rs +++ b/packages/dioxus-blitz/src/lib.rs @@ -21,11 +21,13 @@ mod menu; #[cfg(feature = "accessibility")] mod accessibility; +use std::sync::Arc; use blitz_dom::{DocumentLike, HtmlDocument}; use dioxus::prelude::{ComponentFunction, Element, VirtualDom}; +use tokio::runtime::Runtime; use url::Url; use winit::event_loop::{ControlFlow, EventLoop}; - +use blitz_dom::util::Resource; use crate::application::Application; use crate::window::View; @@ -62,9 +64,17 @@ pub fn launch_cfg_with_props( // We're going to need to hit it with a special waker let vdom = VirtualDom::new_with_props(root, props); let document = DioxusDocument::new(vdom); - let window = WindowConfig::new(document, 800.0, 600.0); - launch_with_window(window) + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + let net = Arc::new(blitz_net::Net::new(&rt)); + + let window = WindowConfig::new(document, 800.0, 600.0, net); + + launch_with_window(window, rt) } pub fn launch_url(url: &str) { @@ -96,12 +106,6 @@ 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); - let window = WindowConfig::new(document, 800.0, 600.0); - launch_with_window(window) -} - -fn launch_with_window(window: WindowConfig) { // Turn on the runtime and enter it let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -110,6 +114,15 @@ fn launch_with_window(window: WindowConfig) { let _guard = rt.enter(); + let net_provider = Arc::new(blitz_net::Net::new(&rt)); + + let document = HtmlDocument::from_html(html, cfg.base_url, cfg.stylesheets, Arc::clone(&net_provider)); + let window = WindowConfig::new(document, 800.0, 600.0, net_provider); + launch_with_window(window, rt) +} + +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")] diff --git a/packages/dioxus-blitz/src/waker.rs b/packages/dioxus-blitz/src/waker.rs index 0103e03b..021180f9 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, (usize, Resource))> for BlitzEvent { + fn from((window_id, (node_id, resource)): (WindowId, (usize, Resource))) -> Self { + BlitzEvent::Window { + window_id, + data: BlitzWindowEvent::ResourceLoad { node_id, resource } + } + } +} #[cfg(feature = "accessibility")] impl From for BlitzEvent { @@ -37,7 +45,10 @@ impl From for BlitzEvent { #[derive(Debug, Clone)] pub enum BlitzWindowEvent { Poll, - + ResourceLoad { + node_id: usize, + resource: 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 74c883f4..1fb4de08 100644 --- a/packages/dioxus-blitz/src/window.rs +++ b/packages/dioxus-blitz/src/window.rs @@ -11,30 +11,35 @@ use wgpu::rwh::HasWindowHandle; use std::sync::Arc; use std::task::Waker; +use tokio::runtime::Runtime; use winit::dpi::LogicalSize; use winit::event::{ElementState, MouseButton}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; use winit::window::{WindowAttributes, WindowId}; use winit::{event::Modifiers, event::WindowEvent, keyboard::KeyCode, window::Window}; - +use blitz_dom::node::{ImageData, NodeSpecificData}; +use blitz_dom::util::Resource; +use blitz_net::Net; #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))] use crate::menu::init_menu; pub struct WindowConfig { doc: Doc, attributes: WindowAttributes, + net: Arc> } impl WindowConfig { - pub fn new(doc: Doc, width: f32, height: f32) -> Self { + pub fn new(doc: Doc, width: f32, height: f32, net: Arc>) -> Self { WindowConfig { doc, attributes: Window::default_attributes().with_inner_size(LogicalSize { width, height }), + net } } - pub fn with_attributes(doc: Doc, attributes: WindowAttributes) -> Self { - WindowConfig { doc, attributes } + pub fn with_attributes(doc: Doc, attributes: WindowAttributes, net: Arc>) -> Self { + WindowConfig { doc, attributes, net } } } @@ -46,6 +51,8 @@ pub(crate) struct View { event_loop_proxy: EventLoopProxy, window: Arc, + net: Arc>, + /// The actual viewport of the page that we're getting a glimpse of. /// We need this since the part of the page that's being viewed might not be the page in its entirety. /// This will let us opt of rendering some stuff @@ -73,9 +80,12 @@ impl View { config: WindowConfig, event_loop: &ActiveEventLoop, proxy: &EventLoopProxy, + rt: &Runtime ) -> Self { let winit_window = Arc::from(event_loop.create_window(config.attributes).unwrap()); - + + rt.spawn(Net::resolve(Arc::clone(&config.net), proxy.clone(), winit_window.id())); + // TODO: make this conditional on text input focus winit_window.set_ime_allowed(true); @@ -91,6 +101,7 @@ impl View { event_loop_proxy: proxy.clone(), window: winit_window.clone(), + net: config.net, dom: config.doc, viewport, devtools: Default::default(), @@ -237,6 +248,30 @@ impl View { BlitzWindowEvent::Poll => { self.poll(); } + BlitzWindowEvent::ResourceLoad { node_id, resource} => { + match resource { + Resource::Css(css) => { + let css = html_escape::decode_html_entities(&css); + self.dom.as_mut().add_stylesheet(&css); + } + Resource::Image(image) => { + let node = self.dom.as_mut().get_node_mut(node_id).unwrap(); + node + .element_data_mut() + .unwrap() + .node_specific_data + = NodeSpecificData::Image(ImageData::new(Arc::new(image))) + } + Resource::Svg(tree) => { + let node = self.dom.as_mut().get_node_mut(node_id).unwrap(); + node + .element_data_mut() + .unwrap() + .node_specific_data = NodeSpecificData::Svg(tree) + } + } + self.request_redraw(); + }, #[cfg(feature = "accessibility")] BlitzWindowEvent::Accessibility(accessibility_event) => { match &*accessibility_event { diff --git a/packages/dom/src/html_document.rs b/packages/dom/src/html_document.rs index 7924267a..580abb05 100644 --- a/packages/dom/src/html_document.rs +++ b/packages/dom/src/html_document.rs @@ -2,6 +2,7 @@ use crate::events::RendererEvent; use crate::{Document, DocumentHtmlParser, DocumentLike, Viewport}; use crate::DEFAULT_CSS; +use crate::util::{NetProvider, Resource}; pub struct HtmlDocument { inner: Document, @@ -31,7 +32,7 @@ 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: N) -> Self { // Spin up the virtualdom and include the default stylesheet let viewport = Viewport::new(0, 0, 1.0); let mut dom = Document::new(viewport); @@ -48,7 +49,7 @@ impl HtmlDocument { } // 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 08668d65..318eedc3 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -1,9 +1,9 @@ use std::borrow::Cow; -use std::collections::HashSet; -use std::sync::Arc; +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex}; use crate::node::{Attribute, ElementNodeData, ImageData, Node, NodeData, NodeSpecificData}; -use crate::util::ImageOrSvg; +use crate::util::{Resource, NetProvider}; use crate::Document; use html5ever::local_name; use html5ever::{ @@ -21,7 +21,7 @@ fn html5ever_to_blitz_attr(attr: html5ever::Attribute) -> Attribute { } } -pub struct DocumentHtmlParser<'a> { +pub struct DocumentHtmlParser<'a, N> { doc: &'a mut Document, style_nodes: Vec, @@ -31,20 +31,23 @@ pub struct DocumentHtmlParser<'a> { /// The document's quirks mode. pub quirks_mode: QuirksMode, + + net_provider: N, } -impl<'a> DocumentHtmlParser<'a> { - pub fn new(doc: &mut Document) -> DocumentHtmlParser { +impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { + pub fn new(doc: &mut Document, net_provider: N) -> DocumentHtmlParser { DocumentHtmlParser { doc, style_nodes: Vec::new(), errors: Vec::new(), quirks_mode: 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: N, 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()) @@ -94,13 +97,7 @@ impl<'a> DocumentHtmlParser<'a> { if let (Some("stylesheet"), Some(href)) = (rel_attr, href_attr) { let url = self.doc.resolve_url(href); - match crate::util::fetch_string(url.as_str()) { - Ok(css) => { - let css = html_escape::decode_html_entities(&css); - self.doc.add_stylesheet(&css); - } - Err(_) => eprintln!("Error fetching stylesheet {}", url), - } + self.net_provider.fetch(url, target_id, crate::util::fetch_css); } } @@ -109,27 +106,7 @@ impl<'a> DocumentHtmlParser<'a> { if let Some(raw_src) = node.attr(local_name!("src")) { if !raw_src.is_empty() { let src = self.doc.resolve_url(raw_src); - - // 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, target_id, crate::util::fetch_image); } } } @@ -156,7 +133,7 @@ impl<'a> DocumentHtmlParser<'a> { } } -impl<'b> TreeSink for DocumentHtmlParser<'b> { +impl<'b, N: NetProvider> TreeSink for DocumentHtmlParser<'b, N> { type Output = &'b mut Document; // we use the ID of the nodes in the tree as the handle @@ -381,7 +358,7 @@ fn parses_some_html() { 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, crate::util::SyncProvider); html5ever::parse_document(sink, Default::default()) .from_utf8() diff --git a/packages/dom/src/util.rs b/packages/dom/src/util.rs index fde88ec5..613a1690 100644 --- a/packages/dom/src/util.rs +++ b/packages/dom/src/util.rs @@ -3,7 +3,8 @@ use std::{ sync::{Arc, OnceLock}, time::Instant, }; - +use std::cell::RefCell; +use std::sync::Mutex; use crate::node::{Node, NodeData}; use image::DynamicImage; @@ -12,6 +13,70 @@ const FILE_SIZE_LIMIT: u64 = 1_000_000_000; // 1GB static FONT_DB: OnceLock> = OnceLock::new(); +pub trait NetProvider { + fn fetch(&self, url: Url, i: I, handler: F) + where + F: Fn(&[u8]) -> T + Send + Sync + 'static; +} +impl> NetProvider for Arc

{ + fn fetch(&self, url: Url, i: I, handler: F) + where + F: Fn(&[u8]) -> T + Send + Sync + 'static + { + self.as_ref().fetch(url, i, handler) + } +} + +#[derive(Clone, Debug)] +pub enum Resource { + Css(String), + Image(DynamicImage), + Svg(Tree) +} + +pub struct SyncProvider(RefCell>); +impl NetProvider for SyncProvider { + fn fetch(&self, url: Url, i: I, handler: F) + where + F: Fn(&[u8]) -> T + { + let res = match url.scheme() { + "data" => { + let data_url = data_url::DataUrl::process(url.as_str()).unwrap(); + let decoded = data_url.decode_to_vec().expect("Invalid data url"); + decoded.0 + } + "file" => { + let file_content = std::fs::read(url.path()).unwrap(); + file_content + } + _ => { + let response = ureq::get(url.as_str()) + .set("User-Agent", USER_AGENT) + .call() + .map_err(Box::new); + + let Ok(response) = response else { + tracing::error!("{}", response.unwrap_err()); + return; + }; + let len: usize = response + .header("Content-Length") + .and_then(|c| c.parse().ok()) + .unwrap_or(0); + let mut bytes: Vec = Vec::with_capacity(len); + + response.into_reader() + .take(FILE_SIZE_LIMIT) + .read_to_end(&mut bytes) + .unwrap(); + bytes + } + }; + self.0.borrow_mut().push((i, handler(&res))); + } +} + pub(crate) enum FetchErr { UrlParse(url::ParseError), Ureq(Box), @@ -33,47 +98,9 @@ impl From for FetchErr { } } -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); - } - - // 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_css(bytes: &[u8]) -> Resource{ + let str = String::from_utf8(bytes.into()).expect("Invalid UTF8"); + Resource::Css(str) } // pub(crate) fn fetch_buffered_stream( @@ -117,18 +144,16 @@ impl From for ImageFetchErr { } } -pub(crate) fn fetch_image(url: &str) -> Result { - let blob = crate::util::fetch_blob(url)?; +pub(crate) fn fetch_image(bytes: &[u8]) -> Resource { // Try parse image - if let Ok(image) = image::ImageReader::new(Cursor::new(&blob)) + if let Ok(image) = image::ImageReader::new(Cursor::new(bytes)) .with_guessed_format() .expect("IO errors impossible with Cursor") .decode() { - return Ok(ImageOrSvg::Image(image)); + return Resource::Image(image); }; - // Try parse SVG // TODO: Use fontique @@ -143,8 +168,8 @@ pub(crate) fn fetch_image(url: &str) -> Result { ..Default::default() }; - let tree = usvg::Tree::from_data(&blob, &options)?; - Ok(ImageOrSvg::Svg(tree)) + let tree = usvg::Tree::from_data(bytes, &options).unwrap(); + Resource::Svg(tree) } // Debug print an RcDom @@ -215,6 +240,7 @@ pub fn walk_tree(indent: usize, node: &Node) { use peniko::Color as PenikoColor; use style::color::AbsoluteColor; use url::Url; +use usvg::Tree; 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..fbe3e9ae --- /dev/null +++ b/packages/net/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "blitz-net" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { workspace = true } +reqwest = "0.12.7" +url = "2.5.2" +data-url = "0.3.1" +dom = { path = "../dom", package = "blitz-dom" } +winit = "0.30.5" +futures-util = "0.3.30" \ No newline at end of file diff --git a/packages/net/src/lib.rs b/packages/net/src/lib.rs new file mode 100644 index 00000000..74fc6bbd --- /dev/null +++ b/packages/net/src/lib.rs @@ -0,0 +1,77 @@ +use std::cell::{RefCell, UnsafeCell}; +use std::future::Future; +use std::ops::Deref; +use std::sync::{Arc, Mutex, Weak}; +use std::time::Duration; +use futures_util::future::Map; +use futures_util::{FutureExt, StreamExt}; +use reqwest::{Client, Request}; +use tokio::runtime::{Handle, Runtime}; +pub use url::Url; +use futures_util::stream::FuturesUnordered; +use tokio::task::{JoinError, JoinHandle}; +use winit::event_loop::EventLoopProxy; +use winit::window::WindowId; + +const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"; + +pub struct Net { + rt: Handle, + client: Client, + pub futures: UnsafeCell>>, +} +impl Net { + pub fn new(rt: &Runtime) -> Self { + Self { + rt: rt.handle().clone(), + client: Client::new(), + futures: UnsafeCell::new(FuturesUnordered::new()), + } + } + pub async fn resolve>(this: Arc, event_loop_proxy: EventLoopProxy

, window_id: WindowId) { + loop { + while let Some(ir) = unsafe { &mut *this.futures.get()}.next().await { + if let Some(ir) = ir { + let _ = event_loop_proxy.send_event((window_id, ir).into()); + } + } + tokio::time::sleep(Duration::from_millis(500)).await; + } + } +} + +type JoinMap = Map, fn(Result<(I, T), JoinError>)->Option<(I, T)>>; + +impl dom::util::NetProvider for Net { + fn fetch(&self, url: Url, i: I, handler: F) + where + F: Fn(&[u8]) -> T + Send + Sync + 'static + { + let client = self.client.clone(); + unsafe { &*self.futures.get() }.push(self.rt.spawn(async move { + match url.scheme() { + "data" => { + let data_url = data_url::DataUrl::process(url.as_str()).unwrap(); + let decoded = data_url.decode_to_vec().expect("Invalid data url"); + (i, handler(decoded.0.deref())) + } + "file" => { + let file_content = std::fs::read(url.path()).unwrap(); + (i, handler(file_content.deref())) + } + _ => { + let response = client.get(url) + .header("User-Agent", USER_AGENT) + .send() + .await + .unwrap(); + (i, handler(response.bytes().await.unwrap().deref())) + } + } + }).map(Result::ok)); + } +} + +unsafe impl Send for Net {} +unsafe impl Sync for Net {} + From d8106dd687131038f55aaa0cf20e103e7e38ef59 Mon Sep 17 00:00:00 2001 From: koko Date: Tue, 20 Aug 2024 21:26:29 +0200 Subject: [PATCH 02/24] Restructuring of the components --- packages/dioxus-blitz/Cargo.toml | 2 +- packages/dioxus-blitz/src/lib.rs | 26 +++--- packages/dioxus-blitz/src/waker.rs | 4 +- packages/dioxus-blitz/src/window.rs | 61 ++++++------ packages/dom/Cargo.toml | 2 +- packages/dom/src/html_document.rs | 10 +- packages/dom/src/htmlsink.rs | 19 ++-- packages/dom/src/util.rs | 139 ++-------------------------- packages/net/Cargo.toml | 17 +++- packages/net/src/blocking.rs | 55 +++++++++++ packages/net/src/lib.rs | 93 ++++++------------- packages/net/src/non_blocking.rs | 80 ++++++++++++++++ 12 files changed, 251 insertions(+), 257 deletions(-) create mode 100644 packages/net/src/blocking.rs create mode 100644 packages/net/src/non_blocking.rs diff --git a/packages/dioxus-blitz/Cargo.toml b/packages/dioxus-blitz/Cargo.toml index f7573f96..2b8b4377 100644 --- a/packages/dioxus-blitz/Cargo.toml +++ b/packages/dioxus-blitz/Cargo.toml @@ -26,7 +26,7 @@ style = { workspace = true } tracing = { workspace = true, optional = true } blitz = { path = "../blitz" } blitz-dom = { path = "../dom" } -blitz-net = { path = "../net" } +blitz-net = { path = "../net", features = ["non_blocking"] } html-escape = "0.2.13" url = { version = "2.5.0", features = ["serde"] } ureq = "2.9" diff --git a/packages/dioxus-blitz/src/lib.rs b/packages/dioxus-blitz/src/lib.rs index 57edaeb1..d7984b17 100644 --- a/packages/dioxus-blitz/src/lib.rs +++ b/packages/dioxus-blitz/src/lib.rs @@ -21,15 +21,15 @@ mod menu; #[cfg(feature = "accessibility")] mod accessibility; -use std::sync::Arc; +use crate::application::Application; +use crate::window::View; use blitz_dom::{DocumentLike, HtmlDocument}; +use blitz_net::AsyncProvider; use dioxus::prelude::{ComponentFunction, Element, VirtualDom}; +use std::sync::Arc; use tokio::runtime::Runtime; use url::Url; use winit::event_loop::{ControlFlow, EventLoop}; -use blitz_dom::util::Resource; -use crate::application::Application; -use crate::window::View; pub use crate::documents::DioxusDocument; pub use crate::waker::BlitzEvent; @@ -70,10 +70,10 @@ pub fn launch_cfg_with_props( .build() .unwrap(); - let net = Arc::new(blitz_net::Net::new(&rt)); - + let net = Arc::new(AsyncProvider::new(&rt)); + let window = WindowConfig::new(document, 800.0, 600.0, net); - + launch_with_window(window, rt) } @@ -114,15 +114,19 @@ pub fn launch_static_html_cfg(html: &str, cfg: Config) { let _guard = rt.enter(); - let net_provider = Arc::new(blitz_net::Net::new(&rt)); - - let document = HtmlDocument::from_html(html, cfg.base_url, cfg.stylesheets, Arc::clone(&net_provider)); + let net_provider = Arc::new(AsyncProvider::new(&rt)); + + let document = HtmlDocument::from_html( + html, + cfg.base_url, + cfg.stylesheets, + Arc::clone(&net_provider), + ); let window = WindowConfig::new(document, 800.0, 600.0, net_provider); launch_with_window(window, rt) } 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")] diff --git a/packages/dioxus-blitz/src/waker.rs b/packages/dioxus-blitz/src/waker.rs index 021180f9..d17f103a 100644 --- a/packages/dioxus-blitz/src/waker.rs +++ b/packages/dioxus-blitz/src/waker.rs @@ -26,7 +26,7 @@ impl From<(WindowId, (usize, Resource))> for BlitzEvent { fn from((window_id, (node_id, resource)): (WindowId, (usize, Resource))) -> Self { BlitzEvent::Window { window_id, - data: BlitzWindowEvent::ResourceLoad { node_id, resource } + data: BlitzWindowEvent::ResourceLoad { node_id, resource }, } } } @@ -47,7 +47,7 @@ pub enum BlitzWindowEvent { Poll, ResourceLoad { node_id: usize, - resource: Resource + resource: Resource, }, /// An accessibility event from `accesskit`. #[cfg(feature = "accessibility")] diff --git a/packages/dioxus-blitz/src/window.rs b/packages/dioxus-blitz/src/window.rs index 1fb4de08..3b7d66f5 100644 --- a/packages/dioxus-blitz/src/window.rs +++ b/packages/dioxus-blitz/src/window.rs @@ -9,6 +9,11 @@ use winit::keyboard::PhysicalKey; #[allow(unused)] use wgpu::rwh::HasWindowHandle; +#[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))] +use crate::menu::init_menu; +use blitz_dom::node::{ImageData, NodeSpecificData}; +use blitz_dom::util::Resource; +use blitz_net::AsyncProvider; use std::sync::Arc; use std::task::Waker; use tokio::runtime::Runtime; @@ -17,29 +22,37 @@ use winit::event::{ElementState, MouseButton}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; use winit::window::{WindowAttributes, WindowId}; use winit::{event::Modifiers, event::WindowEvent, keyboard::KeyCode, window::Window}; -use blitz_dom::node::{ImageData, NodeSpecificData}; -use blitz_dom::util::Resource; -use blitz_net::Net; -#[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))] -use crate::menu::init_menu; pub struct WindowConfig { doc: Doc, attributes: WindowAttributes, - net: Arc> + net: Arc>, } impl WindowConfig { - pub fn new(doc: Doc, width: f32, height: f32, net: Arc>) -> Self { + pub fn new( + doc: Doc, + width: f32, + height: f32, + net: Arc>, + ) -> Self { WindowConfig { doc, attributes: Window::default_attributes().with_inner_size(LogicalSize { width, height }), - net + net, } } - pub fn with_attributes(doc: Doc, attributes: WindowAttributes, net: Arc>) -> Self { - WindowConfig { doc, attributes, net } + pub fn with_attributes( + doc: Doc, + attributes: WindowAttributes, + net: Arc>, + ) -> Self { + WindowConfig { + doc, + attributes, + net, + } } } @@ -51,8 +64,6 @@ pub(crate) struct View { event_loop_proxy: EventLoopProxy, window: Arc, - net: Arc>, - /// The actual viewport of the page that we're getting a glimpse of. /// We need this since the part of the page that's being viewed might not be the page in its entirety. /// This will let us opt of rendering some stuff @@ -80,12 +91,12 @@ impl View { config: WindowConfig, event_loop: &ActiveEventLoop, proxy: &EventLoopProxy, - rt: &Runtime + rt: &Runtime, ) -> Self { let winit_window = Arc::from(event_loop.create_window(config.attributes).unwrap()); - - rt.spawn(Net::resolve(Arc::clone(&config.net), proxy.clone(), winit_window.id())); - + + rt.spawn(Arc::clone(&config.net).resolve(proxy.clone(), winit_window.id())); + // TODO: make this conditional on text input focus winit_window.set_ime_allowed(true); @@ -101,7 +112,6 @@ impl View { event_loop_proxy: proxy.clone(), window: winit_window.clone(), - net: config.net, dom: config.doc, viewport, devtools: Default::default(), @@ -248,7 +258,7 @@ impl View { BlitzWindowEvent::Poll => { self.poll(); } - BlitzWindowEvent::ResourceLoad { node_id, resource} => { + BlitzWindowEvent::ResourceLoad { node_id, resource } => { match resource { Resource::Css(css) => { let css = html_escape::decode_html_entities(&css); @@ -256,22 +266,17 @@ impl View { } Resource::Image(image) => { let node = self.dom.as_mut().get_node_mut(node_id).unwrap(); - node - .element_data_mut() - .unwrap() - .node_specific_data - = NodeSpecificData::Image(ImageData::new(Arc::new(image))) + node.element_data_mut().unwrap().node_specific_data = + NodeSpecificData::Image(ImageData::new(Arc::new(image))) } Resource::Svg(tree) => { let node = self.dom.as_mut().get_node_mut(node_id).unwrap(); - node - .element_data_mut() - .unwrap() - .node_specific_data = NodeSpecificData::Svg(tree) + node.element_data_mut().unwrap().node_specific_data = + NodeSpecificData::Svg(tree) } } 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..5ad4e698 100644 --- a/packages/dom/Cargo.toml +++ b/packages/dom/Cargo.toml @@ -8,6 +8,7 @@ default = ["tracing"] tracing = ["dep:tracing"] [dependencies] +blitz-net = { path = "../net" } 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/html_document.rs b/packages/dom/src/html_document.rs index 580abb05..47457cec 100644 --- a/packages/dom/src/html_document.rs +++ b/packages/dom/src/html_document.rs @@ -1,8 +1,9 @@ use crate::events::RendererEvent; use crate::{Document, DocumentHtmlParser, DocumentLike, Viewport}; +use crate::util::Resource; use crate::DEFAULT_CSS; -use crate::util::{NetProvider, Resource}; +use blitz_net::NetProvider; pub struct HtmlDocument { inner: Document, @@ -32,7 +33,12 @@ impl DocumentLike for HtmlDocument { } impl HtmlDocument { - pub fn from_html>(html: &str, base_url: Option, stylesheets: Vec, net: N) -> Self { + pub fn from_html>( + html: &str, + base_url: Option, + stylesheets: Vec, + net: N, + ) -> Self { // Spin up the virtualdom and include the default stylesheet let viewport = Viewport::new(0, 0, 1.0); let mut dom = Document::new(viewport); diff --git a/packages/dom/src/htmlsink.rs b/packages/dom/src/htmlsink.rs index 318eedc3..35a2b45f 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -1,10 +1,10 @@ use std::borrow::Cow; -use std::collections::{HashMap, HashSet}; -use std::sync::{Arc, Mutex}; +use std::collections::HashSet; -use crate::node::{Attribute, ElementNodeData, ImageData, Node, NodeData, NodeSpecificData}; -use crate::util::{Resource, NetProvider}; +use crate::node::{Attribute, ElementNodeData, Node, NodeData}; +use crate::util::Resource; use crate::Document; +use blitz_net::NetProvider; use html5ever::local_name; use html5ever::{ tendril::{StrTendril, TendrilSink}, @@ -31,7 +31,7 @@ pub struct DocumentHtmlParser<'a, N> { /// The document's quirks mode. pub quirks_mode: QuirksMode, - + net_provider: N, } @@ -97,7 +97,8 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { if let (Some("stylesheet"), Some(href)) = (rel_attr, href_attr) { let url = self.doc.resolve_url(href); - self.net_provider.fetch(url, target_id, crate::util::fetch_css); + self.net_provider + .fetch(url, target_id, crate::util::fetch_css); } } @@ -106,7 +107,8 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { if let Some(raw_src) = node.attr(local_name!("src")) { if !raw_src.is_empty() { let src = self.doc.resolve_url(raw_src); - self.net_provider.fetch(src, target_id, crate::util::fetch_image); + self.net_provider + .fetch(src, target_id, crate::util::fetch_image); } } } @@ -354,11 +356,12 @@ impl<'b, N: NetProvider> TreeSink for DocumentHtmlParser<'b, N> #[test] fn parses_some_html() { use crate::Viewport; + use blitz_net::DummyProvider; let html = "

hello world

"; let viewport = Viewport::new(800, 600, 1.0); let mut doc = Document::new(viewport); - let sink = DocumentHtmlParser::new(&mut doc, crate::util::SyncProvider); + let sink = DocumentHtmlParser::new(&mut doc, DummyProvider); html5ever::parse_document(sink, Default::default()) .from_utf8() diff --git a/packages/dom/src/util.rs b/packages/dom/src/util.rs index 613a1690..1fe71317 100644 --- a/packages/dom/src/util.rs +++ b/packages/dom/src/util.rs @@ -1,151 +1,25 @@ +use crate::node::{Node, NodeData}; +use image::DynamicImage; use std::{ - io::{Cursor, Read}, + io::Cursor, sync::{Arc, OnceLock}, - time::Instant, }; -use std::cell::RefCell; -use std::sync::Mutex; -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 static FONT_DB: OnceLock> = OnceLock::new(); -pub trait NetProvider { - fn fetch(&self, url: Url, i: I, handler: F) - where - F: Fn(&[u8]) -> T + Send + Sync + 'static; -} -impl> NetProvider for Arc

{ - fn fetch(&self, url: Url, i: I, handler: F) - where - F: Fn(&[u8]) -> T + Send + Sync + 'static - { - self.as_ref().fetch(url, i, handler) - } -} - #[derive(Clone, Debug)] pub enum Resource { Css(String), Image(DynamicImage), - Svg(Tree) -} - -pub struct SyncProvider(RefCell>); -impl NetProvider for SyncProvider { - fn fetch(&self, url: Url, i: I, handler: F) - where - F: Fn(&[u8]) -> T - { - let res = match url.scheme() { - "data" => { - let data_url = data_url::DataUrl::process(url.as_str()).unwrap(); - let decoded = data_url.decode_to_vec().expect("Invalid data url"); - decoded.0 - } - "file" => { - let file_content = std::fs::read(url.path()).unwrap(); - file_content - } - _ => { - let response = ureq::get(url.as_str()) - .set("User-Agent", USER_AGENT) - .call() - .map_err(Box::new); - - let Ok(response) = response else { - tracing::error!("{}", response.unwrap_err()); - return; - }; - let len: usize = response - .header("Content-Length") - .and_then(|c| c.parse().ok()) - .unwrap_or(0); - let mut bytes: Vec = Vec::with_capacity(len); - - response.into_reader() - .take(FILE_SIZE_LIMIT) - .read_to_end(&mut bytes) - .unwrap(); - bytes - } - }; - self.0.borrow_mut().push((i, handler(&res))); - } -} - -pub(crate) enum FetchErr { - UrlParse(url::ParseError), - Ureq(Box), - FileIo(std::io::Error), -} -impl From for FetchErr { - fn from(value: url::ParseError) -> Self { - Self::UrlParse(value) - } -} -impl From> for FetchErr { - fn from(value: Box) -> Self { - Self::Ureq(value) - } -} -impl From for FetchErr { - fn from(value: std::io::Error) -> Self { - Self::FileIo(value) - } + Svg(Tree), } -pub(crate) fn fetch_css(bytes: &[u8]) -> Resource{ +pub(crate) fn fetch_css(bytes: &[u8]) -> Resource { let str = String::from_utf8(bytes.into()).expect("Invalid UTF8"); Resource::Css(str) } -// 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) - } -} - pub(crate) fn fetch_image(bytes: &[u8]) -> Resource { - // Try parse image if let Ok(image) = image::ImageReader::new(Cursor::new(bytes)) .with_guessed_format() @@ -168,7 +42,7 @@ pub(crate) fn fetch_image(bytes: &[u8]) -> Resource { ..Default::default() }; - let tree = usvg::Tree::from_data(bytes, &options).unwrap(); + let tree = Tree::from_data(bytes, &options).unwrap(); Resource::Svg(tree) } @@ -239,7 +113,6 @@ pub fn walk_tree(indent: usize, node: &Node) { use peniko::Color as PenikoColor; use style::color::AbsoluteColor; -use url::Url; use usvg::Tree; pub trait ToPenikoColor { diff --git a/packages/net/Cargo.toml b/packages/net/Cargo.toml index fbe3e9ae..26291c48 100644 --- a/packages/net/Cargo.toml +++ b/packages/net/Cargo.toml @@ -4,10 +4,17 @@ version = "0.1.0" edition = "2021" [dependencies] -tokio = { workspace = true } -reqwest = "0.12.7" +tokio = { workspace = true, optional = true } +reqwest = { version = "0.12.7", optional = true } +winit = { version = "0.30.5", optional = true } +futures-util = { version = "0.3.30", optional = true } +ureq = { version = "2.10.1", optional = true} + url = "2.5.2" data-url = "0.3.1" -dom = { path = "../dom", package = "blitz-dom" } -winit = "0.30.5" -futures-util = "0.3.30" \ No newline at end of file +tracing = "0.1.40" + +[features] +default = ["non_blocking"] +blocking = ["dep:ureq"] +non_blocking = ["dep:tokio", "dep:winit", "dep:futures-util", "dep:reqwest"] \ No newline at end of file diff --git a/packages/net/src/blocking.rs b/packages/net/src/blocking.rs new file mode 100644 index 00000000..6a854c75 --- /dev/null +++ b/packages/net/src/blocking.rs @@ -0,0 +1,55 @@ +use super::{NetProvider, USER_AGENT}; +use std::cell::RefCell; +use std::io::Read; +use url::Url; + +const FILE_SIZE_LIMIT: u64 = 1_000_000_000; // 1GB + +pub struct SyncProvider(pub RefCell>); +impl SyncProvider { + pub fn new() -> Self { + Self(RefCell::new(Vec::new())) + } +} +impl NetProvider for SyncProvider { + fn fetch(&self, url: Url, i: I, handler: F) + where + F: Fn(&[u8]) -> T, + { + let res = match url.scheme() { + "data" => { + let data_url = data_url::DataUrl::process(url.as_str()).unwrap(); + let decoded = data_url.decode_to_vec().expect("Invalid data url"); + decoded.0 + } + "file" => { + let file_content = std::fs::read(url.path()).unwrap(); + file_content + } + _ => { + let response = ureq::get(url.as_str()) + .set("User-Agent", USER_AGENT) + .call() + .map_err(Box::new); + + let Ok(response) = response else { + tracing::error!("{}", response.unwrap_err()); + return; + }; + let len: usize = response + .header("Content-Length") + .and_then(|c| c.parse().ok()) + .unwrap_or(0); + let mut bytes: Vec = Vec::with_capacity(len); + + response + .into_reader() + .take(FILE_SIZE_LIMIT) + .read_to_end(&mut bytes) + .unwrap(); + bytes + } + }; + self.0.borrow_mut().push((i, handler(&res))); + } +} diff --git a/packages/net/src/lib.rs b/packages/net/src/lib.rs index 74fc6bbd..e5c2b4cf 100644 --- a/packages/net/src/lib.rs +++ b/packages/net/src/lib.rs @@ -1,77 +1,38 @@ -use std::cell::{RefCell, UnsafeCell}; -use std::future::Future; -use std::ops::Deref; -use std::sync::{Arc, Mutex, Weak}; -use std::time::Duration; -use futures_util::future::Map; -use futures_util::{FutureExt, StreamExt}; -use reqwest::{Client, Request}; -use tokio::runtime::{Handle, Runtime}; -pub use url::Url; -use futures_util::stream::FuturesUnordered; -use tokio::task::{JoinError, JoinHandle}; -use winit::event_loop::EventLoopProxy; -use winit::window::WindowId; +use std::sync::Arc; +use url::Url; + +#[cfg(feature = "blocking")] +mod blocking; +#[cfg(feature = "non_blocking")] +mod non_blocking; + +#[cfg(feature = "blocking")] +pub use blocking::SyncProvider; + +#[cfg(feature = "non_blocking")] +pub use non_blocking::AsyncProvider; const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"; -pub struct Net { - rt: Handle, - client: Client, - pub futures: UnsafeCell>>, +pub trait NetProvider { + fn fetch(&self, url: Url, i: I, handler: F) + where + F: Fn(&[u8]) -> T + Send + Sync + 'static; } -impl Net { - pub fn new(rt: &Runtime) -> Self { - Self { - rt: rt.handle().clone(), - client: Client::new(), - futures: UnsafeCell::new(FuturesUnordered::new()), - } - } - pub async fn resolve>(this: Arc, event_loop_proxy: EventLoopProxy

, window_id: WindowId) { - loop { - while let Some(ir) = unsafe { &mut *this.futures.get()}.next().await { - if let Some(ir) = ir { - let _ = event_loop_proxy.send_event((window_id, ir).into()); - } - } - tokio::time::sleep(Duration::from_millis(500)).await; - } +impl> NetProvider for Arc

{ + fn fetch(&self, url: Url, i: I, handler: F) + where + F: Fn(&[u8]) -> T + Send + Sync + 'static, + { + self.as_ref().fetch(url, i, handler) } } -type JoinMap = Map, fn(Result<(I, T), JoinError>)->Option<(I, T)>>; - -impl dom::util::NetProvider for Net { - fn fetch(&self, url: Url, i: I, handler: F) +pub struct DummyProvider; +impl NetProvider for DummyProvider { + fn fetch(&self, _url: Url, _i: I, _handler: F) where - F: Fn(&[u8]) -> T + Send + Sync + 'static + F: Fn(&[u8]) -> T + Send + Sync + 'static, { - let client = self.client.clone(); - unsafe { &*self.futures.get() }.push(self.rt.spawn(async move { - match url.scheme() { - "data" => { - let data_url = data_url::DataUrl::process(url.as_str()).unwrap(); - let decoded = data_url.decode_to_vec().expect("Invalid data url"); - (i, handler(decoded.0.deref())) - } - "file" => { - let file_content = std::fs::read(url.path()).unwrap(); - (i, handler(file_content.deref())) - } - _ => { - let response = client.get(url) - .header("User-Agent", USER_AGENT) - .send() - .await - .unwrap(); - (i, handler(response.bytes().await.unwrap().deref())) - } - } - }).map(Result::ok)); } } - -unsafe impl Send for Net {} -unsafe impl Sync for Net {} - diff --git a/packages/net/src/non_blocking.rs b/packages/net/src/non_blocking.rs new file mode 100644 index 00000000..d1263a11 --- /dev/null +++ b/packages/net/src/non_blocking.rs @@ -0,0 +1,80 @@ +use super::{NetProvider, USER_AGENT}; +use futures_util::stream::FuturesUnordered; +use futures_util::StreamExt; +use reqwest::Client; +use std::ops::Deref; +use std::sync::Arc; +use std::time::Duration; +use tokio::runtime::{Handle, Runtime}; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; +use url::Url; +use winit::event_loop::EventLoopProxy; +use winit::window::WindowId; + +pub struct AsyncProvider { + rt: Handle, + client: Client, + futures: Mutex>>, +} +impl AsyncProvider { + pub fn new(rt: &Runtime) -> Self { + Self { + rt: rt.handle().clone(), + client: Client::new(), + futures: Mutex::new(FuturesUnordered::new()), + } + } + pub async fn resolve>( + self: Arc, + event_loop_proxy: EventLoopProxy

, + window_id: WindowId, + ) { + loop { + while let Some(ir) = self.futures.lock().await.next().await { + if let Ok(ir) = ir { + let _ = event_loop_proxy.send_event((window_id, ir).into()); + } + } + tokio::time::sleep(Duration::from_millis(150)).await; + } + } +} + +impl NetProvider for AsyncProvider { + fn fetch(&self, url: Url, i: I, handler: F) + where + F: Fn(&[u8]) -> T + Send + Sync + 'static, + { + let client = self.client.clone(); + self.futures.blocking_lock().push(self.rt.spawn(async move { + match url.scheme() { + "data" => { + let data_url = data_url::DataUrl::process(url.as_str()).unwrap(); + let decoded = data_url.decode_to_vec().expect("Invalid data url"); + (i, handler(decoded.0.deref())) + } + "file" => { + let file_content = std::fs::read(url.path()).unwrap(); + (i, handler(file_content.deref())) + } + _ => { + let url_str = url.as_str().to_string(); + let start = tokio::time::Instant::now(); + let response = client + .get(url) + .header("User-Agent", USER_AGENT) + .send() + .await + .unwrap(); + let res = handler(response.bytes().await.unwrap().deref()); + println!("Loaded {} in: {}ms", url_str, start.elapsed().as_millis()); + (i, res) + } + } + })); + } +} + +unsafe impl Send for AsyncProvider {} +unsafe impl Sync for AsyncProvider {} From 4b338f220ad5d2a976ddd555f8b65c442da3be1e Mon Sep 17 00:00:00 2001 From: koko Date: Tue, 20 Aug 2024 22:59:50 +0200 Subject: [PATCH 03/24] Fixing screenshot example. --- Cargo.toml | 1 + examples/screenshot.rs | 10 +++++++++- packages/dioxus-blitz/src/window.rs | 18 +----------------- packages/dom/src/document.rs | 22 +++++++++++++++++++++- packages/dom/src/htmlsink.rs | 4 ++-- packages/net/src/lib.rs | 17 +++++++++++++++++ 6 files changed, 51 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d990357d..9f708a42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ incremental = false # mozbuild = "0.1.0" blitz = { path = "./packages/blitz" } blitz-dom = { path = "./packages/dom" } +blitz-net = { path = "./packages/net", features = ["blocking"]} comrak = { version = "0.21.0", 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..784125bb 100644 --- a/examples/screenshot.rs +++ b/examples/screenshot.rs @@ -2,6 +2,7 @@ use blitz::render_to_buffer; use blitz_dom::{HtmlDocument, Viewport}; +use blitz_net::SyncProvider; use reqwest::Url; use std::{ fs::File, @@ -46,8 +47,15 @@ async fn main() { .and_then(|arg| arg.parse().ok()) .unwrap_or(1200); + let net = SyncProvider::new(); + // 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(), &net); + + for (node_id, resource) in net.0.into_inner().drain(..) { + document.as_mut().load_resource(node_id, resource) + } + document .as_mut() .set_viewport(Viewport::new(width * scale, height * scale, scale as f32)); diff --git a/packages/dioxus-blitz/src/window.rs b/packages/dioxus-blitz/src/window.rs index 3b7d66f5..76a08bd7 100644 --- a/packages/dioxus-blitz/src/window.rs +++ b/packages/dioxus-blitz/src/window.rs @@ -11,7 +11,6 @@ use wgpu::rwh::HasWindowHandle; #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))] use crate::menu::init_menu; -use blitz_dom::node::{ImageData, NodeSpecificData}; use blitz_dom::util::Resource; use blitz_net::AsyncProvider; use std::sync::Arc; @@ -259,22 +258,7 @@ impl View { self.poll(); } BlitzWindowEvent::ResourceLoad { node_id, resource } => { - match resource { - Resource::Css(css) => { - let css = html_escape::decode_html_entities(&css); - self.dom.as_mut().add_stylesheet(&css); - } - Resource::Image(image) => { - let node = self.dom.as_mut().get_node_mut(node_id).unwrap(); - node.element_data_mut().unwrap().node_specific_data = - NodeSpecificData::Image(ImageData::new(Arc::new(image))) - } - Resource::Svg(tree) => { - let node = self.dom.as_mut().get_node_mut(node_id).unwrap(); - node.element_data_mut().unwrap().node_specific_data = - NodeSpecificData::Svg(tree) - } - } + self.dom.as_mut().load_resource(node_id, resource); self.request_redraw(); } #[cfg(feature = "accessibility")] diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 8e2d3aaa..72abf767 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -1,14 +1,16 @@ use crate::events::{EventData, HitResult, RendererEvent}; -use crate::node::TextBrush; +use crate::node::{ImageData, NodeSpecificData, TextBrush}; use crate::{Node, NodeData, TextNodeData, Viewport}; use app_units::Au; 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::sync::Arc; use style::selector_parser::ServoElementSnapshot; use style::servo::media_queries::FontMetricsProvider; use style::servo_arc::Arc as ServoArc; @@ -466,6 +468,24 @@ impl Document { .force_stylesheet_origins_dirty(Origin::Author.into()); } + pub fn load_resource(&mut self, node_id: usize, resource: Resource) { + match resource { + Resource::Css(css) => { + let css = html_escape::decode_html_entities(&css); + self.add_stylesheet(&css); + } + Resource::Image(image) => { + let node = self.get_node_mut(node_id).unwrap(); + node.element_data_mut().unwrap().node_specific_data = + NodeSpecificData::Image(ImageData::new(Arc::new(image))) + } + Resource::Svg(tree) => { + let node = self.get_node_mut(node_id).unwrap(); + node.element_data_mut().unwrap().node_specific_data = NodeSpecificData::Svg(tree) + } + } + } + pub fn snapshot_node(&mut self, node_id: usize) { let node = &mut self.nodes[node_id]; let opaque_node_id = TNode::opaque(&&*node); diff --git a/packages/dom/src/htmlsink.rs b/packages/dom/src/htmlsink.rs index 35a2b45f..17e367b8 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::collections::HashSet; use crate::node::{Attribute, ElementNodeData, Node, NodeData}; -use crate::util::Resource; +use crate::util::{fetch_css, Resource}; use crate::Document; use blitz_net::NetProvider; use html5ever::local_name; @@ -98,7 +98,7 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { if let (Some("stylesheet"), Some(href)) = (rel_attr, href_attr) { let url = self.doc.resolve_url(href); self.net_provider - .fetch(url, target_id, crate::util::fetch_css); + .fetch(url, target_id, fetch_css); } } diff --git a/packages/net/src/lib.rs b/packages/net/src/lib.rs index e5c2b4cf..396ec969 100644 --- a/packages/net/src/lib.rs +++ b/packages/net/src/lib.rs @@ -1,3 +1,4 @@ +use std::rc::Rc; use std::sync::Arc; use url::Url; @@ -27,6 +28,22 @@ impl> NetProvider for Arc

{ self.as_ref().fetch(url, i, handler) } } +impl> NetProvider for Rc

{ + fn fetch(&self, url: Url, i: I, handler: F) + where + F: Fn(&[u8]) -> T + Send + Sync + 'static, + { + self.as_ref().fetch(url, i, handler) + } +} +impl> NetProvider for &P { + fn fetch(&self, url: Url, i: I, handler: F) + where + F: Fn(&[u8]) -> T + Send + Sync + 'static, + { + NetProvider::fetch(*self, url, i, handler); + } +} pub struct DummyProvider; impl NetProvider for DummyProvider { From afd42307216c664e0e1997cb04bcf594972c0ddc Mon Sep 17 00:00:00 2001 From: koko Date: Sun, 1 Sep 2024 12:16:38 +0200 Subject: [PATCH 04/24] Refactor some of the net api --- packages/net/Cargo.toml | 1 + packages/net/src/lib.rs | 61 ++++------ packages/net/src/non_blocking.rs | 80 ------------- packages/net/src/{ => provider}/blocking.rs | 57 ++++++---- packages/net/src/provider/dummy.rs | 11 ++ packages/net/src/provider/mod.rs | 13 +++ packages/net/src/provider/non_blocking.rs | 119 ++++++++++++++++++++ 7 files changed, 201 insertions(+), 141 deletions(-) delete mode 100644 packages/net/src/non_blocking.rs rename packages/net/src/{ => provider}/blocking.rs (50%) create mode 100644 packages/net/src/provider/dummy.rs create mode 100644 packages/net/src/provider/mod.rs create mode 100644 packages/net/src/provider/non_blocking.rs diff --git a/packages/net/Cargo.toml b/packages/net/Cargo.toml index 26291c48..3c3ec5af 100644 --- a/packages/net/Cargo.toml +++ b/packages/net/Cargo.toml @@ -13,6 +13,7 @@ ureq = { version = "2.10.1", optional = true} url = "2.5.2" data-url = "0.3.1" tracing = "0.1.40" +thiserror = "1.0.63" [features] default = ["non_blocking"] diff --git a/packages/net/src/lib.rs b/packages/net/src/lib.rs index 396ec969..6c52a19c 100644 --- a/packages/net/src/lib.rs +++ b/packages/net/src/lib.rs @@ -1,55 +1,36 @@ -use std::rc::Rc; -use std::sync::Arc; -use url::Url; - -#[cfg(feature = "blocking")] -mod blocking; -#[cfg(feature = "non_blocking")] -mod non_blocking; +mod provider; -#[cfg(feature = "blocking")] -pub use blocking::SyncProvider; +use std::ops::Deref; +use url::Url; -#[cfg(feature = "non_blocking")] -pub use non_blocking::AsyncProvider; +pub use provider::*; const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"; pub trait NetProvider { - fn fetch(&self, url: Url, i: I, handler: F) - where - F: Fn(&[u8]) -> T + Send + Sync + 'static; -} -impl> NetProvider for Arc

{ - fn fetch(&self, url: Url, i: I, handler: F) + fn fetch(&self, url: Url, i: I, handler: H) where - F: Fn(&[u8]) -> T + Send + Sync + 'static, - { - self.as_ref().fetch(url, i, handler) - } + H: RequestHandler; } -impl> NetProvider for Rc

{ - fn fetch(&self, url: Url, i: I, handler: F) - where - F: Fn(&[u8]) -> T + Send + Sync + 'static, - { - self.as_ref().fetch(url, i, handler) - } -} -impl> NetProvider for &P { - fn fetch(&self, url: Url, i: I, handler: F) + +impl NetProvider for D +where + P: NetProvider, + D: Deref, +{ + fn fetch(&self, url: Url, i: I, handler: H) where - F: Fn(&[u8]) -> T + Send + Sync + 'static, + H: RequestHandler, { - NetProvider::fetch(*self, url, i, handler); + self.deref().fetch(url, i, handler) } } -pub struct DummyProvider; -impl NetProvider for DummyProvider { - fn fetch(&self, _url: Url, _i: I, _handler: F) - where - F: Fn(&[u8]) -> T + Send + Sync + 'static, - { +pub trait RequestHandler: Send + Sync + 'static { + fn bytes(self, bytes: &[u8]) -> T; +} +impl T + Sync + Send + 'static, T> RequestHandler for F { + fn bytes(self, bytes: &[u8]) -> T { + self(bytes) } } diff --git a/packages/net/src/non_blocking.rs b/packages/net/src/non_blocking.rs deleted file mode 100644 index d1263a11..00000000 --- a/packages/net/src/non_blocking.rs +++ /dev/null @@ -1,80 +0,0 @@ -use super::{NetProvider, USER_AGENT}; -use futures_util::stream::FuturesUnordered; -use futures_util::StreamExt; -use reqwest::Client; -use std::ops::Deref; -use std::sync::Arc; -use std::time::Duration; -use tokio::runtime::{Handle, Runtime}; -use tokio::sync::Mutex; -use tokio::task::JoinHandle; -use url::Url; -use winit::event_loop::EventLoopProxy; -use winit::window::WindowId; - -pub struct AsyncProvider { - rt: Handle, - client: Client, - futures: Mutex>>, -} -impl AsyncProvider { - pub fn new(rt: &Runtime) -> Self { - Self { - rt: rt.handle().clone(), - client: Client::new(), - futures: Mutex::new(FuturesUnordered::new()), - } - } - pub async fn resolve>( - self: Arc, - event_loop_proxy: EventLoopProxy

, - window_id: WindowId, - ) { - loop { - while let Some(ir) = self.futures.lock().await.next().await { - if let Ok(ir) = ir { - let _ = event_loop_proxy.send_event((window_id, ir).into()); - } - } - tokio::time::sleep(Duration::from_millis(150)).await; - } - } -} - -impl NetProvider for AsyncProvider { - fn fetch(&self, url: Url, i: I, handler: F) - where - F: Fn(&[u8]) -> T + Send + Sync + 'static, - { - let client = self.client.clone(); - self.futures.blocking_lock().push(self.rt.spawn(async move { - match url.scheme() { - "data" => { - let data_url = data_url::DataUrl::process(url.as_str()).unwrap(); - let decoded = data_url.decode_to_vec().expect("Invalid data url"); - (i, handler(decoded.0.deref())) - } - "file" => { - let file_content = std::fs::read(url.path()).unwrap(); - (i, handler(file_content.deref())) - } - _ => { - let url_str = url.as_str().to_string(); - let start = tokio::time::Instant::now(); - let response = client - .get(url) - .header("User-Agent", USER_AGENT) - .send() - .await - .unwrap(); - let res = handler(response.bytes().await.unwrap().deref()); - println!("Loaded {} in: {}ms", url_str, start.elapsed().as_millis()); - (i, res) - } - } - })); - } -} - -unsafe impl Send for AsyncProvider {} -unsafe impl Sync for AsyncProvider {} diff --git a/packages/net/src/blocking.rs b/packages/net/src/provider/blocking.rs similarity index 50% rename from packages/net/src/blocking.rs rename to packages/net/src/provider/blocking.rs index 6a854c75..b17abac9 100644 --- a/packages/net/src/blocking.rs +++ b/packages/net/src/provider/blocking.rs @@ -1,6 +1,7 @@ -use super::{NetProvider, USER_AGENT}; +use crate::{NetProvider, RequestHandler, USER_AGENT}; use std::cell::RefCell; use std::io::Read; +use thiserror::Error; use url::Url; const FILE_SIZE_LIMIT: u64 = 1_000_000_000; // 1GB @@ -10,46 +11,60 @@ impl SyncProvider { pub fn new() -> Self { Self(RefCell::new(Vec::new())) } -} -impl NetProvider for SyncProvider { - fn fetch(&self, url: Url, i: I, handler: F) - where - F: Fn(&[u8]) -> T, - { - let res = match url.scheme() { + fn fetch_inner(&self, url: Url) -> Result, SyncProviderError> { + Ok(match url.scheme() { "data" => { - let data_url = data_url::DataUrl::process(url.as_str()).unwrap(); - let decoded = data_url.decode_to_vec().expect("Invalid data url"); + let data_url = data_url::DataUrl::process(url.as_str())?; + let decoded = data_url.decode_to_vec()?; decoded.0 } "file" => { - let file_content = std::fs::read(url.path()).unwrap(); + let file_content = std::fs::read(url.path())?; file_content } _ => { let response = ureq::get(url.as_str()) .set("User-Agent", USER_AGENT) - .call() - .map_err(Box::new); + .call()?; - let Ok(response) = response else { - tracing::error!("{}", response.unwrap_err()); - return; - }; let len: usize = response .header("Content-Length") .and_then(|c| c.parse().ok()) .unwrap_or(0); let mut bytes: Vec = Vec::with_capacity(len); - response .into_reader() .take(FILE_SIZE_LIMIT) - .read_to_end(&mut bytes) - .unwrap(); + .read_to_end(&mut bytes)?; bytes } + }) + } +} +impl NetProvider for SyncProvider { + fn fetch(&self, url: Url, i: I, handler: H) + where + H: RequestHandler, + { + let res = match self.fetch_inner(url) { + Ok(v) => v, + Err(e) => { + tracing::error!("{e}"); + return; + } }; - self.0.borrow_mut().push((i, handler(&res))); + self.0.borrow_mut().push((i, handler.bytes(&res))); } } + +#[derive(Error, Debug)] +enum SyncProviderError { + #[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}")] + Ureq(#[from] ureq::Error), +} diff --git a/packages/net/src/provider/dummy.rs b/packages/net/src/provider/dummy.rs new file mode 100644 index 00000000..6aeb6a3f --- /dev/null +++ b/packages/net/src/provider/dummy.rs @@ -0,0 +1,11 @@ +use crate::{NetProvider, RequestHandler}; +use url::Url; + +pub struct DummyProvider; +impl NetProvider for DummyProvider { + fn fetch(&self, _url: Url, _i: I, _handler: H) + where + H: RequestHandler, + { + } +} diff --git a/packages/net/src/provider/mod.rs b/packages/net/src/provider/mod.rs new file mode 100644 index 00000000..35883b72 --- /dev/null +++ b/packages/net/src/provider/mod.rs @@ -0,0 +1,13 @@ +#[cfg(feature = "blocking")] +mod blocking; +mod dummy; +#[cfg(feature = "non_blocking")] +mod non_blocking; + +#[cfg(feature = "non_blocking")] +pub use non_blocking::AsyncProvider; + +#[cfg(feature = "blocking")] +pub use blocking::SyncProvider; + +pub use dummy::DummyProvider; diff --git a/packages/net/src/provider/non_blocking.rs b/packages/net/src/provider/non_blocking.rs new file mode 100644 index 00000000..2b82e1d1 --- /dev/null +++ b/packages/net/src/provider/non_blocking.rs @@ -0,0 +1,119 @@ +use crate::{NetProvider, RequestHandler, USER_AGENT}; +use data_url::DataUrl; +use futures_util::{stream::FuturesUnordered, StreamExt}; +use reqwest::Client; +use std::{fmt::Display, sync::Arc, time::Duration}; +use thiserror::Error; +use tokio::{ + runtime::{Handle, Runtime}, + sync::Mutex, + task::JoinHandle, +}; +use url::Url; +use winit::{event_loop::EventLoopProxy, window::WindowId}; + +type TaskHandle = JoinHandle<(I, Result)>; + +pub struct AsyncProvider { + rt: Handle, + client: Client, + futures: Mutex>>, +} +impl AsyncProvider { + pub fn new(rt: &Runtime) -> Self { + Self { + rt: rt.handle().clone(), + client: Client::new(), + futures: Mutex::new(FuturesUnordered::new()), + } + } + pub async fn resolve>( + self: Arc, + event_loop_proxy: EventLoopProxy

, + window_id: WindowId, + ) { + let mut interval = tokio::time::interval(Duration::from_millis(100)); + + 'thread: loop { + interval.tick().await; + while let Some(ir) = self.futures.lock().await.next().await { + match ir { + Ok((i, Ok(t))) => { + let e = event_loop_proxy.send_event((window_id, (i, t)).into()); + if e.is_err() { + break 'thread; + } + } + Ok((i, Err(e))) => { + tracing::error!("Fetch failed for node {i}, with {e:?}") + } + Err(e) => { + tracing::error!("Fetch thread failed with {e}") + } + } + } + } + } +} +impl AsyncProvider { + async fn fetch_inner>( + client: Client, + url: Url, + handler: H, + ) -> Result { + match url.scheme() { + "data" => { + let data_url = DataUrl::process(url.as_str())?; + let decoded = data_url.decode_to_vec()?; + Ok(handler.bytes(&decoded.0)) + } + "file" => { + let file_content = std::fs::read(url.path())?; + Ok(handler.bytes(&file_content)) + } + _ => { + let start = tokio::time::Instant::now(); + let response = client + .get(url.clone()) + .header("User-Agent", USER_AGENT) + .send() + .await?; + let res = handler.bytes(&response.bytes().await?); + tracing::info!( + "Loaded {} in: {}ms", + url.as_str(), + start.elapsed().as_millis() + ); + Ok(res) + } + } + } +} + +impl NetProvider for AsyncProvider { + fn fetch(&self, url: Url, i: I, handler: H) + where + H: RequestHandler, + { + let client = self.client.clone(); + + let join = self.rt.spawn(async { + let fetch = Self::fetch_inner(client, url, handler).await; + (i, fetch) + }); + + self.futures.blocking_lock().push(join); + } +} + +#[derive(Error, Debug)] +enum AsyncProviderError { + #[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), +} From 5075935c9ee63d02b96ae9beba89bbb91adf7402 Mon Sep 17 00:00:00 2001 From: koko Date: Sun, 1 Sep 2024 12:22:05 +0200 Subject: [PATCH 05/24] Parse stylsheets on the fetch thread. --- packages/dom/src/htmlsink.rs | 17 ++++++++++------ packages/dom/src/util.rs | 39 ++++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/packages/dom/src/htmlsink.rs b/packages/dom/src/htmlsink.rs index 17e367b8..c9461be6 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -1,8 +1,5 @@ -use std::borrow::Cow; -use std::collections::HashSet; - use crate::node::{Attribute, ElementNodeData, Node, NodeData}; -use crate::util::{fetch_css, Resource}; +use crate::util::{CssHandler, Resource}; use crate::Document; use blitz_net::NetProvider; use html5ever::local_name; @@ -11,6 +8,8 @@ use html5ever::{ tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink}, ExpandedName, QualName, }; +use std::borrow::Cow; +use std::collections::HashSet; /// Convert an html5ever Attribute which uses tendril for its value to a blitz Attribute /// which uses String. @@ -97,8 +96,14 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { if let (Some("stylesheet"), Some(href)) = (rel_attr, href_attr) { let url = self.doc.resolve_url(href); - self.net_provider - .fetch(url, target_id, fetch_css); + self.net_provider.fetch( + url.clone(), + target_id, + CssHandler { + source_url: url, + guard: self.doc.guard.clone(), + }, + ); } } diff --git a/packages/dom/src/util.rs b/packages/dom/src/util.rs index 1fe71317..e76f5f3b 100644 --- a/packages/dom/src/util.rs +++ b/packages/dom/src/util.rs @@ -1,22 +1,51 @@ use crate::node::{Node, NodeData}; +use blitz_net::RequestHandler; use image::DynamicImage; +use selectors::context::QuirksMode; use std::{ io::Cursor, sync::{Arc, OnceLock}, }; +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(); #[derive(Clone, Debug)] pub enum Resource { - Css(String), Image(DynamicImage), Svg(Tree), + Css(DocumentStyleSheet), } -pub(crate) fn fetch_css(bytes: &[u8]) -> Resource { - let str = String::from_utf8(bytes.into()).expect("Invalid UTF8"); - Resource::Css(str) +pub(crate) struct CssHandler { + pub source_url: Url, + pub guard: SharedRwLock, +} +impl RequestHandler for CssHandler { + fn bytes(self, bytes: &[u8]) -> Resource { + let css = String::from_utf8(bytes.into()).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, + None, + None, + QuirksMode::NoQuirks, + AllowImportRules::Yes, + ); + Resource::Css(DocumentStyleSheet(ServoArc::new(sheet))) + } } pub(crate) fn fetch_image(bytes: &[u8]) -> Resource { @@ -112,8 +141,6 @@ pub fn walk_tree(indent: usize, node: &Node) { } use peniko::Color as PenikoColor; -use style::color::AbsoluteColor; -use usvg::Tree; pub trait ToPenikoColor { fn as_peniko(&self) -> PenikoColor; From 54cad6877bf174df0fe831339781f9a2f6155261 Mon Sep 17 00:00:00 2001 From: koko Date: Sun, 1 Sep 2024 12:33:12 +0200 Subject: [PATCH 06/24] Make it possible to insert stylesheets at the correct position for unordered loading --- .../src/documents/dioxus_document.rs | 14 ++--- packages/dom/src/document.rs | 60 ++++++++++++++----- packages/dom/src/html_document.rs | 4 +- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index 94433fe3..ce9ef5b8 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -7,6 +7,7 @@ use blitz_dom::{ ElementNodeData, NodeData, QualName, TextNodeData, Viewport, DEFAULT_CSS, }; +use super::event_handler::{NativeClickData, NativeConverter}; use dioxus::{ dioxus_core::{ AttributeValue, ElementId, Template, TemplateAttribute, TemplateNode, VirtualDom, @@ -19,10 +20,9 @@ use rustc_hash::FxHashMap; use style::{ data::{ElementData, ElementStyles}, properties::{style_structs::Font, ComputedValues}, + stylesheets::Origin, }; -use super::event_handler::{NativeClickData, NativeConverter}; - type NodeId = usize; fn qual_name(local_name: &str, namespace: Option<&str>) -> QualName { @@ -163,7 +163,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 { @@ -374,7 +374,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.append_or_insert_stylesheet(sheet, parent.id); } } } @@ -496,7 +497,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 @@ -504,8 +504,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.append_or_insert_stylesheet(sheet, parent.id); } } } diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 72abf767..36de95ed 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -9,7 +9,7 @@ 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 std::sync::Arc; use style::selector_parser::ServoElementSnapshot; use style::servo::media_queries::FontMetricsProvider; @@ -97,7 +97,8 @@ pub struct Document { // Viewport details such as the dimensions, HiDPI scale, and zoom factor, pub(crate) viewport: Viewport, - pub(crate) stylesheets: HashMap, + pub(crate) ua_stylesheets: HashMap, + pub(crate) nodes_to_stylesheet: BTreeMap, /// A Parley font context pub(crate) font_ctx: parley::FontContext, @@ -203,7 +204,8 @@ impl Document { viewport, base_url: None, // quadtree: Quadtree::new(20), - stylesheets: HashMap::new(), + ua_stylesheets: HashMap::new(), + nodes_to_stylesheet: BTreeMap::new(), font_ctx: parley::FontContext::default(), layout_ctx: parley::LayoutContext::new(), @@ -432,24 +434,34 @@ 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.append_or_insert_stylesheet(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()); + + self.stylist + .force_stylesheet_origins_dirty(Origin::UserAgent.into()); + } + + 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, @@ -458,11 +470,31 @@ impl Document { AllowImportRules::Yes, ); - let sheet = DocumentStyleSheet(ServoArc::new(data)); + DocumentStyleSheet(ServoArc::new(data)) + } + pub fn append_or_insert_stylesheet(&mut self, stylesheet: DocumentStyleSheet, node_id: usize) { + let old = self.nodes_to_stylesheet.insert(node_id, stylesheet.clone()); - self.stylesheets.insert(css.to_string(), sheet.clone()); + if let Some(old) = old { + self.stylist.remove_stylesheet(old, &self.guard.read()) + } - self.stylist.append_stylesheet(sheet, &self.guard.read()); + 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()); @@ -471,8 +503,8 @@ impl Document { pub fn load_resource(&mut self, node_id: usize, resource: Resource) { match resource { Resource::Css(css) => { - let css = html_escape::decode_html_entities(&css); - self.add_stylesheet(&css); + self.append_or_insert_stylesheet(css, node_id); + self.resolve() } Resource::Image(image) => { let node = self.get_node_mut(node_id).unwrap(); diff --git a/packages/dom/src/html_document.rs b/packages/dom/src/html_document.rs index 47457cec..46d84475 100644 --- a/packages/dom/src/html_document.rs +++ b/packages/dom/src/html_document.rs @@ -49,9 +49,9 @@ 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 From eb74859dd150a7c6560b925bcc7cebc1bad40ee3 Mon Sep 17 00:00:00 2001 From: koko Date: Thu, 5 Sep 2024 09:00:17 +0200 Subject: [PATCH 07/24] improve net api --- examples/screenshot.rs | 8 +-- packages/dioxus-blitz/src/waker.rs | 11 ++-- packages/dioxus-blitz/src/window.rs | 15 ++--- packages/dom/src/document.rs | 18 ++---- packages/dom/src/html_document.rs | 2 +- packages/dom/src/htmlsink.rs | 11 ++-- packages/dom/src/util.rs | 68 +++++++++++++---------- packages/net/src/lib.rs | 12 ++-- packages/net/src/provider/blocking.rs | 10 ++-- packages/net/src/provider/dummy.rs | 4 +- packages/net/src/provider/non_blocking.rs | 31 +++++------ 11 files changed, 90 insertions(+), 100 deletions(-) diff --git a/examples/screenshot.rs b/examples/screenshot.rs index 784125bb..da4120a3 100644 --- a/examples/screenshot.rs +++ b/examples/screenshot.rs @@ -52,14 +52,14 @@ async fn main() { // Create HtmlDocument let mut document = HtmlDocument::from_html(&html, Some(url.clone()), Vec::new(), &net); - for (node_id, resource) in net.0.into_inner().drain(..) { - document.as_mut().load_resource(node_id, resource) - } - document .as_mut() .set_viewport(Viewport::new(width * scale, height * scale, scale as f32)); + for resource in net.0.into_inner().drain(..) { + document.as_mut().load_resource(resource) + } + timer.time("Created document (+ fetched assets)"); // Compute style, layout, etc for HtmlDocument diff --git a/packages/dioxus-blitz/src/waker.rs b/packages/dioxus-blitz/src/waker.rs index d17f103a..e34811eb 100644 --- a/packages/dioxus-blitz/src/waker.rs +++ b/packages/dioxus-blitz/src/waker.rs @@ -22,11 +22,11 @@ pub enum BlitzEvent { ))] HotReloadEvent(dioxus_hot_reload::HotReloadMsg), } -impl From<(WindowId, (usize, Resource))> for BlitzEvent { - fn from((window_id, (node_id, resource)): (WindowId, (usize, Resource))) -> Self { +impl From<(WindowId, Resource)> for BlitzEvent { + fn from((window_id, resource): (WindowId, Resource)) -> Self { BlitzEvent::Window { window_id, - data: BlitzWindowEvent::ResourceLoad { node_id, resource }, + data: BlitzWindowEvent::ResourceLoad(resource), } } } @@ -45,10 +45,7 @@ impl From for BlitzEvent { #[derive(Debug, Clone)] pub enum BlitzWindowEvent { Poll, - ResourceLoad { - node_id: usize, - resource: Resource, - }, + 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 76a08bd7..81134a71 100644 --- a/packages/dioxus-blitz/src/window.rs +++ b/packages/dioxus-blitz/src/window.rs @@ -25,16 +25,11 @@ use winit::{event::Modifiers, event::WindowEvent, keyboard::KeyCode, window::Win pub struct WindowConfig { doc: Doc, attributes: WindowAttributes, - net: Arc>, + net: Arc>, } impl WindowConfig { - pub fn new( - doc: Doc, - width: f32, - height: f32, - net: Arc>, - ) -> Self { + pub fn new(doc: Doc, width: f32, height: f32, net: Arc>) -> Self { WindowConfig { doc, attributes: Window::default_attributes().with_inner_size(LogicalSize { width, height }), @@ -45,7 +40,7 @@ impl WindowConfig { pub fn with_attributes( doc: Doc, attributes: WindowAttributes, - net: Arc>, + net: Arc>, ) -> Self { WindowConfig { doc, @@ -257,8 +252,8 @@ impl View { BlitzWindowEvent::Poll => { self.poll(); } - BlitzWindowEvent::ResourceLoad { node_id, resource } => { - self.dom.as_mut().load_resource(node_id, resource); + BlitzWindowEvent::ResourceLoad(resource) => { + self.dom.as_mut().load_resource(resource); self.request_redraw(); } #[cfg(feature = "accessibility")] diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 36de95ed..f586b837 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -76,12 +76,12 @@ pub struct Document { /// We pin the tree to a guarantee to the nodes it creates that the tree is stable in memory. /// /// There is no way to create the tree - publicly or privately - that would invalidate that invariant. - pub(crate) nodes: Box>, + pub nodes: Box>, pub(crate) guard: SharedRwLock, /// The styling engine of firefox - pub(crate) stylist: Stylist, + pub stylist: Stylist, // caching for the stylist pub(crate) snapshots: SnapshotMap, @@ -448,9 +448,6 @@ impl Document { 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()); - - self.stylist - .force_stylesheet_origins_dirty(Origin::UserAgent.into()); } pub fn make_stylesheet(&self, css: impl AsRef, origin: Origin) -> DocumentStyleSheet { @@ -495,23 +492,20 @@ impl Document { self.stylist .append_stylesheet(stylesheet, &self.guard.read()) } - - self.stylist - .force_stylesheet_origins_dirty(Origin::Author.into()); } - pub fn load_resource(&mut self, node_id: usize, resource: Resource) { + pub fn load_resource(&mut self, resource: Resource) { match resource { - Resource::Css(css) => { + Resource::Css(node_id, css) => { self.append_or_insert_stylesheet(css, node_id); self.resolve() } - Resource::Image(image) => { + 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(Arc::new(image))) } - Resource::Svg(tree) => { + 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) } diff --git a/packages/dom/src/html_document.rs b/packages/dom/src/html_document.rs index 46d84475..9284a4b0 100644 --- a/packages/dom/src/html_document.rs +++ b/packages/dom/src/html_document.rs @@ -33,7 +33,7 @@ impl DocumentLike for HtmlDocument { } impl HtmlDocument { - pub fn from_html>( + pub fn from_html>( html: &str, base_url: Option, stylesheets: Vec, diff --git a/packages/dom/src/htmlsink.rs b/packages/dom/src/htmlsink.rs index c9461be6..78f56820 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -1,5 +1,5 @@ use crate::node::{Attribute, ElementNodeData, Node, NodeData}; -use crate::util::{CssHandler, Resource}; +use crate::util::{CssHandler, ImageHandler, Resource}; use crate::Document; use blitz_net::NetProvider; use html5ever::local_name; @@ -34,7 +34,7 @@ pub struct DocumentHtmlParser<'a, N> { net_provider: N, } -impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { +impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { pub fn new(doc: &mut Document, net_provider: N) -> DocumentHtmlParser { DocumentHtmlParser { doc, @@ -98,8 +98,8 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { let url = self.doc.resolve_url(href); self.net_provider.fetch( url.clone(), - target_id, CssHandler { + node: target_id, source_url: url, guard: self.doc.guard.clone(), }, @@ -112,8 +112,7 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { if let Some(raw_src) = node.attr(local_name!("src")) { if !raw_src.is_empty() { let src = self.doc.resolve_url(raw_src); - self.net_provider - .fetch(src, target_id, crate::util::fetch_image); + self.net_provider.fetch(src, ImageHandler::new(target_id)); } } } @@ -140,7 +139,7 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { } } -impl<'b, N: NetProvider> TreeSink for DocumentHtmlParser<'b, N> { +impl<'b, N: NetProvider> TreeSink for DocumentHtmlParser<'b, N> { type Output = &'b mut Document; // we use the ID of the nodes in the tree as the handle diff --git a/packages/dom/src/util.rs b/packages/dom/src/util.rs index e76f5f3b..c3973677 100644 --- a/packages/dom/src/util.rs +++ b/packages/dom/src/util.rs @@ -20,12 +20,13 @@ static FONT_DB: OnceLock> = OnceLock::new(); #[derive(Clone, Debug)] pub enum Resource { - Image(DynamicImage), - Svg(Tree), - Css(DocumentStyleSheet), + Image(usize, DynamicImage), + Svg(usize, Tree), + Css(usize, DocumentStyleSheet), } pub(crate) struct CssHandler { + pub node: usize, pub source_url: Url, pub guard: SharedRwLock, } @@ -44,35 +45,42 @@ impl RequestHandler for CssHandler { QuirksMode::NoQuirks, AllowImportRules::Yes, ); - Resource::Css(DocumentStyleSheet(ServoArc::new(sheet))) + Resource::Css(self.node, DocumentStyleSheet(ServoArc::new(sheet))) } } - -pub(crate) fn fetch_image(bytes: &[u8]) -> Resource { - // Try parse image - if let Ok(image) = image::ImageReader::new(Cursor::new(bytes)) - .with_guessed_format() - .expect("IO errors impossible with Cursor") - .decode() - { - return Resource::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 = Tree::from_data(bytes, &options).unwrap(); - Resource::Svg(tree) +pub(crate) struct ImageHandler(usize); +impl ImageHandler { + pub(crate) fn new(node_id: usize) -> Self { + Self(node_id) + } +} +impl RequestHandler for ImageHandler { + fn bytes(self, bytes: &[u8]) -> Resource { + // Try parse image + if let Ok(image) = image::ImageReader::new(Cursor::new(bytes)) + .with_guessed_format() + .expect("IO errors impossible with Cursor") + .decode() + { + return Resource::Image(self.0, 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 = Tree::from_data(bytes, &options).unwrap(); + Resource::Svg(self.0, tree) + } } // Debug print an RcDom diff --git a/packages/net/src/lib.rs b/packages/net/src/lib.rs index 6c52a19c..988c9e1b 100644 --- a/packages/net/src/lib.rs +++ b/packages/net/src/lib.rs @@ -7,22 +7,22 @@ pub use provider::*; const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"; -pub trait NetProvider { - fn fetch(&self, url: Url, i: I, handler: H) +pub trait NetProvider { + fn fetch(&self, url: Url, handler: H) where H: RequestHandler; } -impl NetProvider for D +impl NetProvider for D where - P: NetProvider, + P: NetProvider, D: Deref, { - fn fetch(&self, url: Url, i: I, handler: H) + fn fetch(&self, url: Url, handler: H) where H: RequestHandler, { - self.deref().fetch(url, i, handler) + self.deref().fetch(url, handler) } } diff --git a/packages/net/src/provider/blocking.rs b/packages/net/src/provider/blocking.rs index b17abac9..fa6ff031 100644 --- a/packages/net/src/provider/blocking.rs +++ b/packages/net/src/provider/blocking.rs @@ -6,8 +6,8 @@ use url::Url; const FILE_SIZE_LIMIT: u64 = 1_000_000_000; // 1GB -pub struct SyncProvider(pub RefCell>); -impl SyncProvider { +pub struct SyncProvider(pub RefCell>); +impl SyncProvider { pub fn new() -> Self { Self(RefCell::new(Vec::new())) } @@ -41,8 +41,8 @@ impl SyncProvider { }) } } -impl NetProvider for SyncProvider { - fn fetch(&self, url: Url, i: I, handler: H) +impl NetProvider for SyncProvider { + fn fetch(&self, url: Url, handler: H) where H: RequestHandler, { @@ -53,7 +53,7 @@ impl NetProvider for SyncProvider { return; } }; - self.0.borrow_mut().push((i, handler.bytes(&res))); + self.0.borrow_mut().push(handler.bytes(&res)); } } diff --git a/packages/net/src/provider/dummy.rs b/packages/net/src/provider/dummy.rs index 6aeb6a3f..335609d0 100644 --- a/packages/net/src/provider/dummy.rs +++ b/packages/net/src/provider/dummy.rs @@ -2,8 +2,8 @@ use crate::{NetProvider, RequestHandler}; use url::Url; pub struct DummyProvider; -impl NetProvider for DummyProvider { - fn fetch(&self, _url: Url, _i: I, _handler: H) +impl NetProvider for DummyProvider { + fn fetch(&self, _url: Url, _handler: H) where H: RequestHandler, { diff --git a/packages/net/src/provider/non_blocking.rs b/packages/net/src/provider/non_blocking.rs index 2b82e1d1..a088f9ba 100644 --- a/packages/net/src/provider/non_blocking.rs +++ b/packages/net/src/provider/non_blocking.rs @@ -2,7 +2,7 @@ use crate::{NetProvider, RequestHandler, USER_AGENT}; use data_url::DataUrl; use futures_util::{stream::FuturesUnordered, StreamExt}; use reqwest::Client; -use std::{fmt::Display, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; use thiserror::Error; use tokio::{ runtime::{Handle, Runtime}, @@ -12,14 +12,14 @@ use tokio::{ use url::Url; use winit::{event_loop::EventLoopProxy, window::WindowId}; -type TaskHandle = JoinHandle<(I, Result)>; +type TaskHandle = JoinHandle>; -pub struct AsyncProvider { +pub struct AsyncProvider { rt: Handle, client: Client, - futures: Mutex>>, + futures: Mutex>>, } -impl AsyncProvider { +impl AsyncProvider { pub fn new(rt: &Runtime) -> Self { Self { rt: rt.handle().clone(), @@ -27,7 +27,7 @@ impl AsyncProvider { futures: Mutex::new(FuturesUnordered::new()), } } - pub async fn resolve>( + pub async fn resolve>( self: Arc, event_loop_proxy: EventLoopProxy

, window_id: WindowId, @@ -38,14 +38,14 @@ impl AsyncProvider { interval.tick().await; while let Some(ir) = self.futures.lock().await.next().await { match ir { - Ok((i, Ok(t))) => { - let e = event_loop_proxy.send_event((window_id, (i, t)).into()); + Ok(Ok(t)) => { + let e = event_loop_proxy.send_event((window_id, t).into()); if e.is_err() { break 'thread; } } - Ok((i, Err(e))) => { - tracing::error!("Fetch failed for node {i}, with {e:?}") + Ok(Err(e)) => { + tracing::error!("Fetch failed with {e:?}") } Err(e) => { tracing::error!("Fetch thread failed with {e}") @@ -55,7 +55,7 @@ impl AsyncProvider { } } } -impl AsyncProvider { +impl AsyncProvider { async fn fetch_inner>( client: Client, url: Url, @@ -90,17 +90,14 @@ impl AsyncProvider { } } -impl NetProvider for AsyncProvider { - fn fetch(&self, url: Url, i: I, handler: H) +impl NetProvider for AsyncProvider { + fn fetch(&self, url: Url, handler: H) where H: RequestHandler, { let client = self.client.clone(); - let join = self.rt.spawn(async { - let fetch = Self::fetch_inner(client, url, handler).await; - (i, fetch) - }); + let join = self.rt.spawn(Self::fetch_inner(client, url, handler)); self.futures.blocking_lock().push(join); } From a88f23923dd5d5ce4cc71439b057b8c707d13bc0 Mon Sep 17 00:00:00 2001 From: koko Date: Thu, 5 Sep 2024 09:50:04 +0200 Subject: [PATCH 08/24] Use an request method provided by Handler instead of GET --- packages/net/Cargo.toml | 3 ++- packages/net/src/lib.rs | 5 +++++ packages/net/src/provider/blocking.rs | 10 +++++++--- packages/net/src/provider/non_blocking.rs | 3 ++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/net/Cargo.toml b/packages/net/Cargo.toml index 3c3ec5af..840f48f0 100644 --- a/packages/net/Cargo.toml +++ b/packages/net/Cargo.toml @@ -8,8 +8,9 @@ tokio = { workspace = true, optional = true } reqwest = { version = "0.12.7", optional = true } winit = { version = "0.30.5", optional = true } futures-util = { version = "0.3.30", optional = true } -ureq = { version = "2.10.1", optional = true} +ureq = { version = "2.10.1", optional = true } +http = "1.1.0" url = "2.5.2" data-url = "0.3.1" tracing = "0.1.40" diff --git a/packages/net/src/lib.rs b/packages/net/src/lib.rs index 988c9e1b..42a69d1f 100644 --- a/packages/net/src/lib.rs +++ b/packages/net/src/lib.rs @@ -3,6 +3,8 @@ mod provider; use std::ops::Deref; use url::Url; +pub use http::Method; + pub use provider::*; const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"; @@ -28,6 +30,9 @@ where pub trait RequestHandler: Send + Sync + 'static { fn bytes(self, bytes: &[u8]) -> T; + fn method(&self) -> Method { + Method::GET + } } impl T + Sync + Send + 'static, T> RequestHandler for F { fn bytes(self, bytes: &[u8]) -> T { diff --git a/packages/net/src/provider/blocking.rs b/packages/net/src/provider/blocking.rs index fa6ff031..ac4094dd 100644 --- a/packages/net/src/provider/blocking.rs +++ b/packages/net/src/provider/blocking.rs @@ -11,7 +11,11 @@ impl SyncProvider { pub fn new() -> Self { Self(RefCell::new(Vec::new())) } - fn fetch_inner(&self, url: Url) -> Result, SyncProviderError> { + fn fetch_inner>( + &self, + url: Url, + handler: &H, + ) -> Result, SyncProviderError> { Ok(match url.scheme() { "data" => { let data_url = data_url::DataUrl::process(url.as_str())?; @@ -23,7 +27,7 @@ impl SyncProvider { file_content } _ => { - let response = ureq::get(url.as_str()) + let response = ureq::request(handler.method().as_str(), url.as_str()) .set("User-Agent", USER_AGENT) .call()?; @@ -46,7 +50,7 @@ impl NetProvider for SyncProvider { where H: RequestHandler, { - let res = match self.fetch_inner(url) { + let res = match self.fetch_inner(url, &handler) { Ok(v) => v, Err(e) => { tracing::error!("{e}"); diff --git a/packages/net/src/provider/non_blocking.rs b/packages/net/src/provider/non_blocking.rs index a088f9ba..12b65bef 100644 --- a/packages/net/src/provider/non_blocking.rs +++ b/packages/net/src/provider/non_blocking.rs @@ -73,8 +73,9 @@ impl AsyncProvider { } _ => { let start = tokio::time::Instant::now(); + let response = client - .get(url.clone()) + .request(handler.method(), url.clone()) .header("User-Agent", USER_AGENT) .send() .await?; From 407942eea0b31f61be7f0d9b3b6198cc3d24d9e9 Mon Sep 17 00:00:00 2001 From: koko Date: Thu, 5 Sep 2024 21:32:48 +0200 Subject: [PATCH 09/24] improve non_blocking provider --- packages/dioxus-blitz/src/application.rs | 2 +- packages/dioxus-blitz/src/window.rs | 4 +-- packages/net/Cargo.toml | 2 +- packages/net/src/provider/non_blocking.rs | 38 ++++++++++++----------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/dioxus-blitz/src/application.rs b/packages/dioxus-blitz/src/application.rs index c6c41000..10db473a 100644 --- a/packages/dioxus-blitz/src/application.rs +++ b/packages/dioxus-blitz/src/application.rs @@ -46,7 +46,7 @@ impl ApplicationHandler for Application { // Initialise pending windows for window_config in self.pending_windows.drain(..) { - let mut view = View::init(window_config, event_loop, &self.proxy, &self.rt); + let mut view = View::init(window_config, event_loop, &self.proxy); view.resume(&self.rt); if !view.renderer.is_active() { continue; diff --git a/packages/dioxus-blitz/src/window.rs b/packages/dioxus-blitz/src/window.rs index 81134a71..90bcef3b 100644 --- a/packages/dioxus-blitz/src/window.rs +++ b/packages/dioxus-blitz/src/window.rs @@ -15,7 +15,6 @@ use blitz_dom::util::Resource; use blitz_net::AsyncProvider; use std::sync::Arc; use std::task::Waker; -use tokio::runtime::Runtime; use winit::dpi::LogicalSize; use winit::event::{ElementState, MouseButton}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; @@ -85,11 +84,10 @@ impl View { config: WindowConfig, event_loop: &ActiveEventLoop, proxy: &EventLoopProxy, - rt: &Runtime, ) -> Self { let winit_window = Arc::from(event_loop.create_window(config.attributes).unwrap()); - rt.spawn(Arc::clone(&config.net).resolve(proxy.clone(), winit_window.id())); + Arc::clone(&config.net).resolve(proxy.clone(), winit_window.id()); // TODO: make this conditional on text input focus winit_window.set_ime_allowed(true); diff --git a/packages/net/Cargo.toml b/packages/net/Cargo.toml index 840f48f0..4b1b8198 100644 --- a/packages/net/Cargo.toml +++ b/packages/net/Cargo.toml @@ -17,6 +17,6 @@ tracing = "0.1.40" thiserror = "1.0.63" [features] -default = ["non_blocking"] +default = [] blocking = ["dep:ureq"] non_blocking = ["dep:tokio", "dep:winit", "dep:futures-util", "dep:reqwest"] \ No newline at end of file diff --git a/packages/net/src/provider/non_blocking.rs b/packages/net/src/provider/non_blocking.rs index 12b65bef..f38a2a59 100644 --- a/packages/net/src/provider/non_blocking.rs +++ b/packages/net/src/provider/non_blocking.rs @@ -19,7 +19,7 @@ pub struct AsyncProvider { client: Client, futures: Mutex>>, } -impl AsyncProvider { +impl AsyncProvider { pub fn new(rt: &Runtime) -> Self { Self { rt: rt.handle().clone(), @@ -27,32 +27,34 @@ impl AsyncProvider { futures: Mutex::new(FuturesUnordered::new()), } } - pub async fn resolve>( + pub fn resolve + Send>( self: Arc, event_loop_proxy: EventLoopProxy

, window_id: WindowId, ) { - let mut interval = tokio::time::interval(Duration::from_millis(100)); + self.rt.clone().spawn(async move { + let mut interval = tokio::time::interval(Duration::from_millis(100)); - 'thread: loop { - interval.tick().await; - while let Some(ir) = self.futures.lock().await.next().await { - match ir { - Ok(Ok(t)) => { - let e = event_loop_proxy.send_event((window_id, t).into()); - if e.is_err() { - break 'thread; + 'thread: loop { + interval.tick().await; + while let Some(ir) = self.futures.lock().await.next().await { + match ir { + Ok(Ok(t)) => { + let e = event_loop_proxy.send_event((window_id, t).into()); + if e.is_err() { + break 'thread; + } + } + Ok(Err(e)) => { + tracing::error!("Fetch failed with {e:?}") + } + Err(e) => { + tracing::error!("Fetch thread failed with {e}") } - } - Ok(Err(e)) => { - tracing::error!("Fetch failed with {e:?}") - } - Err(e) => { - tracing::error!("Fetch thread failed with {e}") } } } - } + }); } } impl AsyncProvider { From f5a65ead7aea9fe97f05d9e5f919a32cd32a8526 Mon Sep 17 00:00:00 2001 From: koko Date: Sat, 7 Sep 2024 16:45:52 +0200 Subject: [PATCH 10/24] Workaround for css loading --- packages/dom/src/document.rs | 1 - packages/dom/src/htmlsink.rs | 9 +++++- packages/dom/src/util.rs | 6 +++- packages/net/src/lib.rs | 9 ++++++ packages/net/src/provider/blocking.rs | 16 ++++++++-- packages/net/src/provider/dummy.rs | 4 +++ packages/net/src/provider/non_blocking.rs | 39 ++++++++++++++++++++--- 7 files changed, 74 insertions(+), 10 deletions(-) diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index f586b837..5cdfd321 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -498,7 +498,6 @@ impl Document { match resource { Resource::Css(node_id, css) => { self.append_or_insert_stylesheet(css, node_id); - self.resolve() } Resource::Image(node_id, image) => { let node = self.get_node_mut(node_id).unwrap(); diff --git a/packages/dom/src/htmlsink.rs b/packages/dom/src/htmlsink.rs index 78f56820..d13f5d03 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -1,5 +1,5 @@ use crate::node::{Attribute, ElementNodeData, Node, NodeData}; -use crate::util::{CssHandler, ImageHandler, Resource}; +use crate::util::{CssHandler, CssMarker, ImageHandler, Resource}; use crate::Document; use blitz_net::NetProvider; use html5ever::local_name; @@ -151,6 +151,13 @@ impl<'b, N: NetProvider> TreeSink for DocumentHtmlParser<'b, N> { self.doc.process_style_element(*id); } + let st = self.net_provider.resolve_all(CssMarker); + if let Some(st) = st { + for res in st { + self.doc.load_resource(res); + } + } + // Compute child_idx fields. self.doc.flush_child_indexes(0, 0, 0); diff --git a/packages/dom/src/util.rs b/packages/dom/src/util.rs index c3973677..c55917fd 100644 --- a/packages/dom/src/util.rs +++ b/packages/dom/src/util.rs @@ -2,6 +2,7 @@ use crate::node::{Node, NodeData}; use blitz_net::RequestHandler; use image::DynamicImage; use selectors::context::QuirksMode; +use std::any::{Any, TypeId}; use std::{ io::Cursor, sync::{Arc, OnceLock}, @@ -24,7 +25,7 @@ pub enum Resource { Svg(usize, Tree), Css(usize, DocumentStyleSheet), } - +pub(crate) struct CssMarker; pub(crate) struct CssHandler { pub node: usize, pub source_url: Url, @@ -47,6 +48,9 @@ impl RequestHandler for CssHandler { ); Resource::Css(self.node, DocumentStyleSheet(ServoArc::new(sheet))) } + fn special(&self) -> Option { + Some(CssMarker.type_id()) + } } pub(crate) struct ImageHandler(usize); impl ImageHandler { diff --git a/packages/net/src/lib.rs b/packages/net/src/lib.rs index 42a69d1f..38998d38 100644 --- a/packages/net/src/lib.rs +++ b/packages/net/src/lib.rs @@ -1,5 +1,6 @@ mod provider; +use std::any::{Any, TypeId}; use std::ops::Deref; use url::Url; @@ -7,12 +8,14 @@ pub use http::Method; pub use provider::*; +#[cfg(any(feature = "blocking", feature = "non_blocking"))] const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"; pub trait NetProvider { fn fetch(&self, url: Url, handler: H) where H: RequestHandler; + fn resolve_all(&self, marker: M) -> Option>; } impl NetProvider for D @@ -26,6 +29,9 @@ where { self.deref().fetch(url, handler) } + fn resolve_all(&self, marker: M) -> Option> { + self.deref().resolve_all(marker) + } } pub trait RequestHandler: Send + Sync + 'static { @@ -33,6 +39,9 @@ pub trait RequestHandler: Send + Sync + 'static { fn method(&self) -> Method { Method::GET } + fn special(&self) -> Option { + None + } } impl T + Sync + Send + 'static, T> RequestHandler for F { fn bytes(self, bytes: &[u8]) -> T { diff --git a/packages/net/src/provider/blocking.rs b/packages/net/src/provider/blocking.rs index ac4094dd..e9fa7167 100644 --- a/packages/net/src/provider/blocking.rs +++ b/packages/net/src/provider/blocking.rs @@ -1,15 +1,17 @@ use crate::{NetProvider, RequestHandler, USER_AGENT}; +use std::any::{Any, TypeId}; use std::cell::RefCell; +use std::collections::HashMap; use std::io::Read; use thiserror::Error; use url::Url; const FILE_SIZE_LIMIT: u64 = 1_000_000_000; // 1GB -pub struct SyncProvider(pub RefCell>); +pub struct SyncProvider(pub RefCell>, pub RefCell>>); impl SyncProvider { pub fn new() -> Self { - Self(RefCell::new(Vec::new())) + Self(RefCell::new(Vec::new()), RefCell::default()) } fn fetch_inner>( &self, @@ -57,7 +59,15 @@ impl NetProvider for SyncProvider { return; } }; - self.0.borrow_mut().push(handler.bytes(&res)); + if let Some(t) = handler.special() { + let mut entry = self.1.borrow_mut(); + entry.entry(t).or_default().push(handler.bytes(&res)); + } else { + self.0.borrow_mut().push(handler.bytes(&res)); + } + } + fn resolve_all(&self, marker: M) -> Option> { + self.1.borrow_mut().remove(&marker.type_id()) } } diff --git a/packages/net/src/provider/dummy.rs b/packages/net/src/provider/dummy.rs index 335609d0..dcef86fe 100644 --- a/packages/net/src/provider/dummy.rs +++ b/packages/net/src/provider/dummy.rs @@ -1,4 +1,5 @@ use crate::{NetProvider, RequestHandler}; +use std::any::Any; use url::Url; pub struct DummyProvider; @@ -8,4 +9,7 @@ impl NetProvider for DummyProvider { H: RequestHandler, { } + fn resolve_all(&self, _marker: M) -> Option> { + None + } } diff --git a/packages/net/src/provider/non_blocking.rs b/packages/net/src/provider/non_blocking.rs index f38a2a59..8d6f49f4 100644 --- a/packages/net/src/provider/non_blocking.rs +++ b/packages/net/src/provider/non_blocking.rs @@ -2,6 +2,8 @@ use crate::{NetProvider, RequestHandler, USER_AGENT}; use data_url::DataUrl; use futures_util::{stream::FuturesUnordered, StreamExt}; use reqwest::Client; +use std::any::{Any, TypeId}; +use std::collections::HashMap; use std::{sync::Arc, time::Duration}; use thiserror::Error; use tokio::{ @@ -18,6 +20,7 @@ pub struct AsyncProvider { rt: Handle, client: Client, futures: Mutex>>, + special: Mutex>>>, } impl AsyncProvider { pub fn new(rt: &Runtime) -> Self { @@ -25,6 +28,7 @@ impl AsyncProvider { rt: rt.handle().clone(), client: Client::new(), futures: Mutex::new(FuturesUnordered::new()), + special: Mutex::default(), } } pub fn resolve + Send>( @@ -99,10 +103,37 @@ impl NetProvider for AsyncProvider { H: RequestHandler, { let client = self.client.clone(); - - let join = self.rt.spawn(Self::fetch_inner(client, url, handler)); - - self.futures.blocking_lock().push(join); + if let Some(t) = handler.special() { + let join = self.rt.spawn(Self::fetch_inner(client, url, handler)); + self.special + .blocking_lock() + .entry(t) + .or_default() + .push(join); + } else { + let join = self.rt.spawn(Self::fetch_inner(client, url, handler)); + self.futures.blocking_lock().push(join); + } + } + fn resolve_all(&self, marker: M) -> Option> { + self.rt.block_on(async { + let mut futures = self.special.lock().await.remove(&marker.type_id())?; + let mut out = vec![]; + while let Some(ir) = futures.next().await { + match ir { + Ok(Ok(t)) => { + out.push(t); + } + Ok(Err(e)) => { + tracing::error!("Fetch failed with {e:?}") + } + Err(e) => { + tracing::error!("Fetch thread failed with {e}") + } + } + } + Some(out) + }) } } From 0f3ef6e0e97448be0ee1044cdea0008896af8902 Mon Sep 17 00:00:00 2001 From: koko Date: Wed, 11 Sep 2024 00:06:12 +0200 Subject: [PATCH 11/24] Remove workaround --- packages/dom/src/htmlsink.rs | 9 +----- packages/dom/src/util.rs | 5 --- packages/net/src/lib.rs | 8 ----- packages/net/src/provider/blocking.rs | 16 ++-------- packages/net/src/provider/dummy.rs | 4 --- packages/net/src/provider/non_blocking.rs | 37 ++--------------------- 6 files changed, 6 insertions(+), 73 deletions(-) diff --git a/packages/dom/src/htmlsink.rs b/packages/dom/src/htmlsink.rs index d13f5d03..78f56820 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -1,5 +1,5 @@ use crate::node::{Attribute, ElementNodeData, Node, NodeData}; -use crate::util::{CssHandler, CssMarker, ImageHandler, Resource}; +use crate::util::{CssHandler, ImageHandler, Resource}; use crate::Document; use blitz_net::NetProvider; use html5ever::local_name; @@ -151,13 +151,6 @@ impl<'b, N: NetProvider> TreeSink for DocumentHtmlParser<'b, N> { self.doc.process_style_element(*id); } - let st = self.net_provider.resolve_all(CssMarker); - if let Some(st) = st { - for res in st { - self.doc.load_resource(res); - } - } - // Compute child_idx fields. self.doc.flush_child_indexes(0, 0, 0); diff --git a/packages/dom/src/util.rs b/packages/dom/src/util.rs index c55917fd..93c3ce6e 100644 --- a/packages/dom/src/util.rs +++ b/packages/dom/src/util.rs @@ -2,7 +2,6 @@ use crate::node::{Node, NodeData}; use blitz_net::RequestHandler; use image::DynamicImage; use selectors::context::QuirksMode; -use std::any::{Any, TypeId}; use std::{ io::Cursor, sync::{Arc, OnceLock}, @@ -25,7 +24,6 @@ pub enum Resource { Svg(usize, Tree), Css(usize, DocumentStyleSheet), } -pub(crate) struct CssMarker; pub(crate) struct CssHandler { pub node: usize, pub source_url: Url, @@ -48,9 +46,6 @@ impl RequestHandler for CssHandler { ); Resource::Css(self.node, DocumentStyleSheet(ServoArc::new(sheet))) } - fn special(&self) -> Option { - Some(CssMarker.type_id()) - } } pub(crate) struct ImageHandler(usize); impl ImageHandler { diff --git a/packages/net/src/lib.rs b/packages/net/src/lib.rs index 38998d38..acc64dc9 100644 --- a/packages/net/src/lib.rs +++ b/packages/net/src/lib.rs @@ -1,6 +1,5 @@ mod provider; -use std::any::{Any, TypeId}; use std::ops::Deref; use url::Url; @@ -15,7 +14,6 @@ pub trait NetProvider { fn fetch(&self, url: Url, handler: H) where H: RequestHandler; - fn resolve_all(&self, marker: M) -> Option>; } impl NetProvider for D @@ -29,9 +27,6 @@ where { self.deref().fetch(url, handler) } - fn resolve_all(&self, marker: M) -> Option> { - self.deref().resolve_all(marker) - } } pub trait RequestHandler: Send + Sync + 'static { @@ -39,9 +34,6 @@ pub trait RequestHandler: Send + Sync + 'static { fn method(&self) -> Method { Method::GET } - fn special(&self) -> Option { - None - } } impl T + Sync + Send + 'static, T> RequestHandler for F { fn bytes(self, bytes: &[u8]) -> T { diff --git a/packages/net/src/provider/blocking.rs b/packages/net/src/provider/blocking.rs index e9fa7167..ac4094dd 100644 --- a/packages/net/src/provider/blocking.rs +++ b/packages/net/src/provider/blocking.rs @@ -1,17 +1,15 @@ use crate::{NetProvider, RequestHandler, USER_AGENT}; -use std::any::{Any, TypeId}; use std::cell::RefCell; -use std::collections::HashMap; use std::io::Read; use thiserror::Error; use url::Url; const FILE_SIZE_LIMIT: u64 = 1_000_000_000; // 1GB -pub struct SyncProvider(pub RefCell>, pub RefCell>>); +pub struct SyncProvider(pub RefCell>); impl SyncProvider { pub fn new() -> Self { - Self(RefCell::new(Vec::new()), RefCell::default()) + Self(RefCell::new(Vec::new())) } fn fetch_inner>( &self, @@ -59,15 +57,7 @@ impl NetProvider for SyncProvider { return; } }; - if let Some(t) = handler.special() { - let mut entry = self.1.borrow_mut(); - entry.entry(t).or_default().push(handler.bytes(&res)); - } else { - self.0.borrow_mut().push(handler.bytes(&res)); - } - } - fn resolve_all(&self, marker: M) -> Option> { - self.1.borrow_mut().remove(&marker.type_id()) + self.0.borrow_mut().push(handler.bytes(&res)); } } diff --git a/packages/net/src/provider/dummy.rs b/packages/net/src/provider/dummy.rs index dcef86fe..335609d0 100644 --- a/packages/net/src/provider/dummy.rs +++ b/packages/net/src/provider/dummy.rs @@ -1,5 +1,4 @@ use crate::{NetProvider, RequestHandler}; -use std::any::Any; use url::Url; pub struct DummyProvider; @@ -9,7 +8,4 @@ impl NetProvider for DummyProvider { H: RequestHandler, { } - fn resolve_all(&self, _marker: M) -> Option> { - None - } } diff --git a/packages/net/src/provider/non_blocking.rs b/packages/net/src/provider/non_blocking.rs index 8d6f49f4..715800ad 100644 --- a/packages/net/src/provider/non_blocking.rs +++ b/packages/net/src/provider/non_blocking.rs @@ -2,8 +2,6 @@ use crate::{NetProvider, RequestHandler, USER_AGENT}; use data_url::DataUrl; use futures_util::{stream::FuturesUnordered, StreamExt}; use reqwest::Client; -use std::any::{Any, TypeId}; -use std::collections::HashMap; use std::{sync::Arc, time::Duration}; use thiserror::Error; use tokio::{ @@ -20,7 +18,6 @@ pub struct AsyncProvider { rt: Handle, client: Client, futures: Mutex>>, - special: Mutex>>>, } impl AsyncProvider { pub fn new(rt: &Runtime) -> Self { @@ -28,7 +25,6 @@ impl AsyncProvider { rt: rt.handle().clone(), client: Client::new(), futures: Mutex::new(FuturesUnordered::new()), - special: Mutex::default(), } } pub fn resolve + Send>( @@ -103,37 +99,8 @@ impl NetProvider for AsyncProvider { H: RequestHandler, { let client = self.client.clone(); - if let Some(t) = handler.special() { - let join = self.rt.spawn(Self::fetch_inner(client, url, handler)); - self.special - .blocking_lock() - .entry(t) - .or_default() - .push(join); - } else { - let join = self.rt.spawn(Self::fetch_inner(client, url, handler)); - self.futures.blocking_lock().push(join); - } - } - fn resolve_all(&self, marker: M) -> Option> { - self.rt.block_on(async { - let mut futures = self.special.lock().await.remove(&marker.type_id())?; - let mut out = vec![]; - while let Some(ir) = futures.next().await { - match ir { - Ok(Ok(t)) => { - out.push(t); - } - Ok(Err(e)) => { - tracing::error!("Fetch failed with {e:?}") - } - Err(e) => { - tracing::error!("Fetch thread failed with {e}") - } - } - } - Some(out) - }) + let join = self.rt.spawn(Self::fetch_inner(client, url, handler)); + self.futures.blocking_lock().push(join); } } From effed75616862bb295cc2b8122955551c1357c39 Mon Sep 17 00:00:00 2001 From: koko Date: Wed, 11 Sep 2024 00:09:54 +0200 Subject: [PATCH 12/24] Reset inline nodes --- packages/dom/src/document.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 6bdfc80f..386f25e3 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -588,6 +588,7 @@ impl Document { match resource { Resource::Css(node_id, css) => { self.append_or_insert_stylesheet(css, node_id); + self.resolve(); } Resource::Image(node_id, image) => { let node = self.get_node_mut(node_id).unwrap(); @@ -806,6 +807,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.node_specific_data.take_inline_layout(); + } + doc.ensure_layout_children(node_id); let children = std::mem::take(&mut doc.nodes[node_id].children); From 328194a806c9a580cfdf5549b6a12aaf94365cf9 Mon Sep 17 00:00:00 2001 From: koko Date: Wed, 11 Sep 2024 00:51:51 +0200 Subject: [PATCH 13/24] Reduce size of Resource --- packages/dom/src/document.rs | 8 +++----- packages/dom/src/util.rs | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 386f25e3..12a57c60 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -1,5 +1,5 @@ use crate::events::{EventData, HitResult, RendererEvent}; -use crate::node::{Attribute, ImageData, NodeSpecificData, TextBrush}; +use crate::node::{ImageData, NodeSpecificData, TextBrush}; use crate::{ElementNodeData, Node, NodeData, TextNodeData, Viewport}; use app_units::Au; use html5ever::{local_name, namespace_url, ns, QualName}; @@ -11,7 +11,6 @@ use selectors::{matching::QuirksMode, Element}; use slab::Slab; use std::any::Any; use std::collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; -use std::sync::Arc; use style::selector_parser::ServoElementSnapshot; use style::servo::media_queries::FontMetricsProvider; use style::servo_arc::Arc as ServoArc; @@ -592,12 +591,11 @@ impl Document { } 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(Arc::new(image))) + 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) + node.element_data_mut().unwrap().node_specific_data = NodeSpecificData::Svg(*tree) } } } diff --git a/packages/dom/src/util.rs b/packages/dom/src/util.rs index 93c3ce6e..2949d46b 100644 --- a/packages/dom/src/util.rs +++ b/packages/dom/src/util.rs @@ -20,8 +20,8 @@ static FONT_DB: OnceLock> = OnceLock::new(); #[derive(Clone, Debug)] pub enum Resource { - Image(usize, DynamicImage), - Svg(usize, Tree), + Image(usize, Arc), + Svg(usize, Box), Css(usize, DocumentStyleSheet), } pub(crate) struct CssHandler { @@ -61,7 +61,7 @@ impl RequestHandler for ImageHandler { .expect("IO errors impossible with Cursor") .decode() { - return Resource::Image(self.0, image); + return Resource::Image(self.0, Arc::new(image)); }; // Try parse SVG @@ -78,7 +78,7 @@ impl RequestHandler for ImageHandler { }; let tree = Tree::from_data(bytes, &options).unwrap(); - Resource::Svg(self.0, tree) + Resource::Svg(self.0, Box::new(tree)) } } From 98bad27d091f33e6eb3d4f2e2b0321d0a5b66c90 Mon Sep 17 00:00:00 2001 From: koko Date: Wed, 11 Sep 2024 01:00:24 +0200 Subject: [PATCH 14/24] Merge main into net --- examples/form.rs | 9 +- examples/gradient.rs | 95 +++- packages/blitz/src/renderer/render.rs | 416 ++++++++++++++---- .../src/documents/dioxus_document.rs | 62 ++- packages/dom/src/document.rs | 27 +- packages/dom/src/layout/construct.rs | 17 + packages/dom/src/node.rs | 17 + packages/dom/src/stylo.rs | 6 +- 8 files changed, 519 insertions(+), 130 deletions(-) diff --git a/examples/form.rs b/examples/form.rs index f9dbc307..22610dc7 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -20,11 +20,10 @@ fn app() -> Element { id: "check1", name: "check1", value: "check1", - checked: Some("").filter(|_| checkbox_checked()), - oninput: move |ev| { - dbg!(ev); - checkbox_checked.set(!checkbox_checked()); - }, + checked: checkbox_checked(), + // This works too + // checked: "{checkbox_checked}", + oninput: move |ev| checkbox_checked.set(!ev.checked()), } label { r#for: "check1", diff --git a/examples/gradient.rs b/examples/gradient.rs index 02ae068f..a2d99f9d 100644 --- a/examples/gradient.rs +++ b/examples/gradient.rs @@ -8,32 +8,83 @@ fn app() -> Element { rsx! { style { {CSS} } div { - // https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient - class: "flex flex-row", - div { background: "linear-gradient(#e66465, #9198e5)", id: "a", "Vertical Gradient"} - div { background: "linear-gradient(0.25turn, #3f87a6, #ebf8e1, #f69d3c)", id: "a", "Horizontal Gradient"} - div { background: "linear-gradient(to left, #333, #333 50%, #eee 75%, #333 75%)", id: "a", "Multi stop Gradient"} - div { background: r#"linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%), - linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%), - linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%)"#, id: "a", "Complex Gradient"} - } - div { - class: "flex flex-row", - div { background: "linear-gradient(to right, red 0%, blue 100%)", id: "a", "Unhinted Gradient"} - div { background: "linear-gradient(to right, red 0%, 0%, blue 100%)", id: "a", "0% Hinted"} - div { background: "linear-gradient(to right, red 0%, 25%, blue 100%)", id: "a", "25% Hinted"} - div { background: "linear-gradient(to right, red 0%, 50%, blue 100%)", id: "a", "50% Hinted"} - div { background: "linear-gradient(to right, red 0%, 100%, blue 100%)", id: "a", "100% Hinted"} - div { background: "linear-gradient(to right, yellow, red 10%, 10%, blue 100%)", id: "a", "10% Mixed Hinted"} + class: "grid-container", + div { id: "a1" } + div { id: "a2" } + div { id: "a3" } + div { id: "a4" } + + div { id: "b1" } + div { id: "b2" } + div { id: "b3" } + div { id: "b4" } + div { id: "b5" } + + div { id: "c1" } + div { id: "c2" } + div { id: "c3" } + + div { id: "d1" } + div { id: "d2" } + div { id: "d3" } + div { id: "d4" } + div { id: "d5" } + + div { id: "e1" } + div { id: "e2" } + div { id: "e3" } + div { id: "e4" } + div { id: "e5" } } } } const CSS: &str = r#" -.flex { display: flex; } -.flex-row { flex-direction: row; } -#a { - height:300px; - width: 300px; +.grid-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 10px; + width: 95vw; + height: 95vh; } + +div { + min-width: 100px; + min-height: 100px; +} + +#a1 { background: linear-gradient(#e66465, #9198e5) } +#a2 { background: linear-gradient(0.25turn, #3f87a6, #ebf8e1, #f69d3c) } +#a3 { background: linear-gradient(to left, #333, #333 50%, #eee 75%, #333 75%) } +#a4 { background: linear-gradient(217deg, rgba(255,0,0,.8), rgba(255,0,0,0) 70.71%), + linear-gradient(127deg, rgba(0,255,0,.8), rgba(0,255,0,0) 70.71%), + linear-gradient(336deg, rgba(0,0,255,.8), rgba(0,0,255,0) 70.71%) } + +#b1 { background: linear-gradient(to right, red 0%, 0%, blue 100%) } +#b2 { background: linear-gradient(to right, red 0%, 25%, blue 100%) } +#b3 { background: linear-gradient(to right, red 0%, 50%, blue 100%) } +#b4 { background: linear-gradient(to right, red 0%, 100%, blue 100%) } +#b5 { background: linear-gradient(to right, yellow, red 10%, 10%, blue 100%) } + +#c1 { background: repeating-linear-gradient(#e66465, #e66465 20px, #9198e5 20px, #9198e5 25px) } +#c2 { background: repeating-linear-gradient(45deg, #3f87a6, #ebf8e1 15%, #f69d3c 20%) } +#c3 { background: repeating-linear-gradient(transparent, #4d9f0c 40px), + repeating-linear-gradient(0.25turn, transparent, #3f87a6 20px) } + +#d1 { background: radial-gradient(circle, red 20px, black 21px, blue) } +#d2 { background: radial-gradient(closest-side, #3f87a6, #ebf8e1, #f69d3c) } +#d3 { background: radial-gradient(circle at 100%, #333, #333 50%, #eee 75%, #333 75%) } +#d4 { background: radial-gradient(ellipse at top, #e66465, transparent), + radial-gradient(ellipse at bottom, #4d9f0c, transparent) } +#d5 { background: radial-gradient(closest-corner circle at 20px 30px, red, yellow, green) } +#e1 { background: repeating-conic-gradient(red 0%, yellow 15%, red 33%) } +#e2 { background: repeating-conic-gradient( + from 45deg at 10% 50%, + brown 0deg 10deg, + darkgoldenrod 10deg 20deg, + chocolate 20deg 30deg +) } +#e3 { background: repeating-radial-gradient(#e66465, #9198e5 20%) } +#e4 { background: repeating-radial-gradient(closest-side, #3f87a6, #ebf8e1, #f69d3c) } +#e5 { background: repeating-radial-gradient(circle at 100%, #333, #333 10px, #eee 10px, #eee 20px) } "#; diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index 6e0c2568..b46a8ba5 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -22,7 +22,6 @@ use style::{ Percentage, }, generics::{ - color::Color as StyloColor, image::{ EndingShape, GenericGradient, GenericGradientItem, GenericImage, GradientFlags, }, @@ -39,8 +38,14 @@ use style::{ use image::{imageops::FilterType, DynamicImage}; use parley::layout::PositionedLayoutItem; +use style::values::generics::color::GenericColor; +use style::values::generics::image::{ + GenericCircle, GenericEllipse, GenericEndingShape, ShapeExtent, +}; +use style::values::specified::percentage::ToPercentage; use taffy::prelude::Layout; use vello::kurbo::{BezPath, Cap, Join}; +use vello::peniko::Gradient; use vello::{ kurbo::{Affine, Point, Rect, Shape, Stroke, Vec2}, peniko::{self, Brush, Color, Fill, Mix}, @@ -706,8 +711,8 @@ impl ElementCx<'_> { // Draw background color (if any) self.draw_solid_frame(scene); - - for segment in &self.style.get_background().background_image.0 { + let segments = &self.style.get_background().background_image.0; + for segment in segments.iter().rev() { match segment { None => { // Do nothing @@ -748,10 +753,10 @@ impl ElementCx<'_> { GenericGradient::Linear { direction, items, - // repeating, + flags, // compat_mode, .. - } => self.draw_linear_gradient(scene, direction, items), + } => self.draw_linear_gradient(scene, direction, items, *flags), GenericGradient::Radial { shape, position, @@ -775,6 +780,7 @@ impl ElementCx<'_> { scene: &mut Scene, direction: &LineDirection, items: &GradientSlice, + flags: GradientFlags, ) { let bb = self.frame.outer_rect.bounding_box(); @@ -783,20 +789,11 @@ impl ElementCx<'_> { let rect = self.frame.inner_rect; let (start, end) = match direction { LineDirection::Angle(angle) => { - let start = Point::new( - self.frame.inner_rect.x0 + rect.width() / 2.0, - self.frame.inner_rect.y0, - ); - let end = Point::new( - self.frame.inner_rect.x0 + rect.width() / 2.0, - self.frame.inner_rect.y1, - ); - - // rotate the lind around the center - let line = Affine::rotate_about(-angle.radians64(), center) - * vello::kurbo::Line::new(start, end); - - (line.p0, line.p1) + let angle = -angle.radians64() + std::f64::consts::PI; + let offset_length = rect.width() / 2.0 * angle.sin().abs() + + rect.height() / 2.0 * angle.cos().abs(); + let offset_vec = Vec2::new(angle.sin(), angle.cos()) * offset_length; + (center - offset_vec, center + offset_vec) } LineDirection::Horizontal(horizontal) => { let start = Point::new( @@ -846,41 +843,64 @@ impl ElementCx<'_> { (Point::new(start_x, start_y), Point::new(end_x, end_y)) } }; - let mut gradient = peniko::Gradient { - kind: peniko::GradientKind::Linear { start, end }, - extend: Default::default(), - stops: Default::default(), - }; + let gradient_length = CSSPixelLength::new((start.distance(end) / self.scale) as f32); + let repeating = flags.contains(GradientFlags::REPEATING); + + let mut gradient = peniko::Gradient::new_linear(start, end).with_extend(if repeating { + peniko::Extend::Repeat + } else { + peniko::Extend::Pad + }); + + let (first_offset, last_offset) = + Self::resolve_length_color_stops(items, gradient_length, &mut gradient, repeating); + if repeating && gradient.stops.len() > 1 { + gradient.kind = peniko::GradientKind::Linear { + start: start + (end - start) * first_offset as f64, + end: end + (start - end) * (1.0 - last_offset) as f64, + }; + } + let brush = peniko::BrushRef::Gradient(&gradient); + scene.fill(peniko::Fill::NonZero, self.transform, brush, None, &shape); + } + + #[inline] + fn resolve_color_stops( + items: &OwnedSlice, T>>, + gradient_length: CSSPixelLength, + gradient: &mut Gradient, + repeating: bool, + item_resolver: impl Fn(CSSPixelLength, &T) -> Option, + ) -> (f32, f32) { let mut hint: Option = None; for (idx, item) in items.iter().enumerate() { let (color, offset) = match item { GenericGradientItem::SimpleColorStop(color) => { let step = 1.0 / (items.len() as f32 - 1.0); - let offset = step * idx as f32; - let color = color.as_vello(); - (color, offset) + (color.as_vello(), step * idx as f32) } GenericGradientItem::ComplexColorStop { color, position } => { - match position.to_percentage().map(|pos| pos.0) { - Some(offset) => { - let color = color.as_vello(); - (color, offset) - } - // TODO: implement absolute and calc stops - None => continue, + let offset = item_resolver(gradient_length, position); + if let Some(offset) = offset { + (color.as_vello(), offset) + } else { + continue; } } GenericGradientItem::InterpolationHint(position) => { - hint = match position.to_percentage() { - Some(Percentage(percentage)) => Some(percentage), - _ => None, - }; + hint = item_resolver(gradient_length, position); continue; } }; + if idx == 0 && !repeating && offset != 0.0 { + gradient + .stops + .push(peniko::ColorStop { color, offset: 0.0 }); + } + match hint { None => gradient.stops.push(peniko::ColorStop { color, offset }), Some(hint) => { @@ -916,34 +936,111 @@ impl ElementCx<'_> { } else if hint == (last_stop.offset + offset) / 2.0 { gradient.stops.push(peniko::ColorStop { color, offset }); } else { - let mid_offset = last_stop.offset * (1.0 - hint) + offset * hint; - let multiplier = hint.powf(0.5f32.log(mid_offset)); - let mid_color = Color::rgba8( - (last_stop.color.r as f32 - + multiplier * (color.r as f32 - last_stop.color.r as f32)) - as u8, - (last_stop.color.g as f32 - + multiplier * (color.g as f32 - last_stop.color.g as f32)) - as u8, - (last_stop.color.b as f32 - + multiplier * (color.b as f32 - last_stop.color.b as f32)) - as u8, - (last_stop.color.a as f32 - + multiplier * (color.a as f32 - last_stop.color.a as f32)) - as u8, - ); - tracing::info!("Gradient stop {:?}", mid_color); - gradient.stops.push(peniko::ColorStop { - color: mid_color, - offset: mid_offset, - }); + let mid_point = (hint - last_stop.offset) / (offset - last_stop.offset); + let mut interpolate_stop = |cur_offset: f32| { + let relative_offset = + (cur_offset - last_stop.offset) / (offset - last_stop.offset); + let multiplier = relative_offset.powf(0.5f32.log(mid_point)); + let color = Color::rgba8( + (last_stop.color.r as f32 + + multiplier * (color.r as f32 - last_stop.color.r as f32)) + as u8, + (last_stop.color.g as f32 + + multiplier * (color.g as f32 - last_stop.color.g as f32)) + as u8, + (last_stop.color.b as f32 + + multiplier * (color.b as f32 - last_stop.color.b as f32)) + as u8, + (last_stop.color.a as f32 + + multiplier * (color.a as f32 - last_stop.color.a as f32)) + as u8, + ); + gradient.stops.push(peniko::ColorStop { + color, + offset: cur_offset, + }); + }; + if mid_point > 0.5 { + for i in 0..7 { + interpolate_stop( + last_stop.offset + + (hint - last_stop.offset) * (7.0 + i as f32) / 13.0, + ); + } + interpolate_stop(hint + (offset - hint) / 3.0); + interpolate_stop(hint + (offset - hint) * 2.0 / 3.0); + } else { + interpolate_stop(last_stop.offset + (hint - last_stop.offset) / 3.0); + interpolate_stop( + last_stop.offset + (hint - last_stop.offset) * 2.0 / 3.0, + ); + for i in 0..7 { + interpolate_stop(hint + (offset - hint) * (i as f32) / 13.0); + } + } gradient.stops.push(peniko::ColorStop { color, offset }); } } } } - let brush = peniko::BrushRef::Gradient(&gradient); - scene.fill(peniko::Fill::NonZero, self.transform, brush, None, &shape); + + // Post-process the stops for repeating gradients + if repeating && gradient.stops.len() > 1 { + let first_offset = gradient.stops.first().unwrap().offset; + let last_offset = gradient.stops.last().unwrap().offset; + if first_offset != 0.0 || last_offset != 1.0 { + let scale_inv = 1e-7_f32.max(1.0 / (last_offset - first_offset)); + for stop in &mut gradient.stops { + stop.offset = (stop.offset - first_offset) * scale_inv; + } + } + (first_offset, last_offset) + } else { + (0.0, 1.0) + } + } + + #[inline] + fn resolve_length_color_stops( + items: &OwnedSlice, LengthPercentage>>, + gradient_length: CSSPixelLength, + gradient: &mut Gradient, + repeating: bool, + ) -> (f32, f32) { + Self::resolve_color_stops( + items, + gradient_length, + gradient, + repeating, + |gradient_length: CSSPixelLength, position: &LengthPercentage| -> Option { + position + .to_percentage_of(gradient_length) + .map(|percentage| percentage.to_percentage()) + }, + ) + } + + #[inline] + fn resolve_angle_color_stops( + items: &OwnedSlice, AngleOrPercentage>>, + gradient_length: CSSPixelLength, + gradient: &mut Gradient, + repeating: bool, + ) -> (f32, f32) { + Self::resolve_color_stops( + items, + gradient_length, + gradient, + repeating, + |_gradient_length: CSSPixelLength, position: &AngleOrPercentage| -> Option { + match position { + AngleOrPercentage::Angle(angle) => { + Some(angle.radians() / (std::f64::consts::PI * 2.0) as f32) + } + AngleOrPercentage::Percentage(percentage) => Some(percentage.to_percentage()), + } + }, + ) } // fn draw_image_frame(&self, scene: &mut Scene) {} @@ -1189,31 +1286,202 @@ impl ElementCx<'_> { fn draw_radial_gradient( &self, - _scene: &mut Scene, - _shape: &EndingShape, NonNegative>, - _position: &GenericPosition, - _items: &OwnedSlice, LengthPercentage>>, - _flags: GradientFlags, + scene: &mut Scene, + shape: &EndingShape, NonNegative>, + position: &GenericPosition, + items: &OwnedSlice, LengthPercentage>>, + flags: GradientFlags, ) { - unimplemented!() + let bez_path = self.frame.frame(); + let rect = self.frame.inner_rect; + let repeating = flags.contains(GradientFlags::REPEATING); + + let mut gradient = + peniko::Gradient::new_radial((0.0, 0.0), 1.0).with_extend(if repeating { + peniko::Extend::Repeat + } else { + peniko::Extend::Pad + }); + + let (width_px, height_px) = ( + position + .horizontal + .resolve(CSSPixelLength::new(rect.width() as f32)) + .px() as f64, + position + .vertical + .resolve(CSSPixelLength::new(rect.height() as f32)) + .px() as f64, + ); + + let gradient_scale: Option = match shape { + GenericEndingShape::Circle(circle) => { + let scale = match circle { + GenericCircle::Extent(extent) => match extent { + ShapeExtent::FarthestSide => width_px + .max(rect.width() - width_px) + .max(height_px.max(rect.height() - height_px)), + ShapeExtent::ClosestSide => width_px + .min(rect.width() - width_px) + .min(height_px.min(rect.height() - height_px)), + ShapeExtent::FarthestCorner => { + (width_px.max(rect.width() - width_px) + + height_px.max(rect.height() - height_px)) + * 0.5_f64.sqrt() + } + ShapeExtent::ClosestCorner => { + (width_px.min(rect.width() - width_px) + + height_px.min(rect.height() - height_px)) + * 0.5_f64.sqrt() + } + _ => 0.0, + }, + GenericCircle::Radius(radius) => radius.0.px() as f64, + }; + Some(Vec2::new(scale, scale)) + } + GenericEndingShape::Ellipse(ellipse) => match ellipse { + GenericEllipse::Extent(extent) => match extent { + ShapeExtent::FarthestCorner | ShapeExtent::FarthestSide => { + let mut scale = Vec2::new( + width_px.max(rect.width() - width_px), + height_px.max(rect.height() - height_px), + ); + if *extent == ShapeExtent::FarthestCorner { + scale *= 2.0_f64.sqrt(); + } + Some(scale) + } + ShapeExtent::ClosestCorner | ShapeExtent::ClosestSide => { + let mut scale = Vec2::new( + width_px.min(rect.width() - width_px), + height_px.min(rect.height() - height_px), + ); + if *extent == ShapeExtent::ClosestCorner { + scale *= 2.0_f64.sqrt(); + } + Some(scale) + } + _ => None, + }, + GenericEllipse::Radii(x, y) => Some(Vec2::new( + x.0.resolve(CSSPixelLength::new(rect.width() as f32)).px() as f64, + y.0.resolve(CSSPixelLength::new(rect.height() as f32)).px() as f64, + )), + }, + }; + + let gradient_transform = { + // If the gradient has no valid scale, we don't need to calculate the color stops + if let Some(gradient_scale) = gradient_scale { + let (first_offset, last_offset) = Self::resolve_length_color_stops( + items, + CSSPixelLength::new(gradient_scale.x as f32), + &mut gradient, + repeating, + ); + let scale = if repeating && gradient.stops.len() >= 2 { + (last_offset - first_offset) as f64 + } else { + 1.0 + }; + Some( + Affine::scale_non_uniform(gradient_scale.x * scale, gradient_scale.y * scale) + .then_translate(self.get_translation(position, rect)), + ) + } else { + None + } + }; + + let brush = peniko::BrushRef::Gradient(&gradient); + scene.fill( + peniko::Fill::NonZero, + self.transform, + brush, + gradient_transform, + &bez_path, + ); } fn draw_conic_gradient( &self, - _scene: &mut Scene, - _angle: &Angle, - _position: &GenericPosition, - _items: &OwnedSlice, AngleOrPercentage>>, - _flags: GradientFlags, + scene: &mut Scene, + angle: &Angle, + position: &GenericPosition, + items: &OwnedSlice, AngleOrPercentage>>, + flags: GradientFlags, ) { - unimplemented!() + let bez_path = self.frame.frame(); + let rect = self.frame.inner_rect; + + let repeating = flags.contains(GradientFlags::REPEATING); + let mut gradient = peniko::Gradient::new_sweep((0.0, 0.0), 0.0, std::f32::consts::PI * 2.0) + .with_extend(if repeating { + peniko::Extend::Repeat + } else { + peniko::Extend::Pad + }); + + let (first_offset, last_offset) = Self::resolve_angle_color_stops( + items, + CSSPixelLength::new(1.0), + &mut gradient, + repeating, + ); + if repeating && gradient.stops.len() >= 2 { + gradient.kind = peniko::GradientKind::Sweep { + center: Point::new(0.0, 0.0), + start_angle: std::f32::consts::PI * 2.0 * first_offset, + end_angle: std::f32::consts::PI * 2.0 * last_offset, + }; + } + + let brush = peniko::BrushRef::Gradient(&gradient); + + scene.fill( + peniko::Fill::NonZero, + self.transform, + brush, + Some( + Affine::rotate(angle.radians() as f64 - std::f64::consts::PI / 2.0) + .then_translate(self.get_translation(position, rect)), + ), + &bez_path, + ); + } + + #[inline] + fn get_translation( + &self, + position: &GenericPosition, + rect: Rect, + ) -> Vec2 { + Vec2::new( + self.frame.inner_rect.x0 + + position + .horizontal + .resolve(CSSPixelLength::new(rect.width() as f32)) + .px() as f64, + self.frame.inner_rect.y0 + + position + .vertical + .resolve(CSSPixelLength::new(rect.height() as f32)) + .px() as f64, + ) } fn draw_input(&self, scene: &mut Scene) { if self.element.local_name() == "input" && matches!(self.element.attr(local_name!("type")), Some("checkbox")) { - let checked = self.element.attr(local_name!("checked")).is_some(); + let Some(checked) = self + .element + .element_data() + .and_then(|data| data.checkbox_input_checked()) + else { + return; + }; let disabled = self.element.attr(local_name!("disabled")).is_some(); // TODO this should be coming from css accent-color, but I couldn't find how to retrieve it diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index 85667867..6b06df87 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -3,8 +3,11 @@ use std::{collections::HashMap, rc::Rc}; use blitz_dom::{ - events::EventData, local_name, namespace_url, node::Attribute, ns, Atom, Document, - DocumentLike, ElementNodeData, Node, NodeData, QualName, TextNodeData, Viewport, DEFAULT_CSS, + events::EventData, + local_name, namespace_url, + node::{Attribute, NodeSpecificData}, + ns, Atom, Document, DocumentLike, ElementNodeData, Node, NodeData, QualName, TextNodeData, + Viewport, DEFAULT_CSS, }; use dioxus::{ @@ -189,7 +192,10 @@ impl DioxusDocument { // - if value is not specified, it defaults to 'on' if let Some(name) = form_input.attr(local_name!("name")) { if form_input.attr(local_name!("type")) == Some("checkbox") - && form_input.attr(local_name!("checked")) == Some("true") + && form_input + .element_data() + .and_then(|data| data.checkbox_input_checked()) + .unwrap_or(false) { let value = form_input .attr(local_name!("value")) @@ -203,13 +209,14 @@ impl DioxusDocument { } else { Default::default() }; - let form_data = NativeFormData { - value: element_node_data + let value = match element_node_data.node_specific_data { + NodeSpecificData::CheckboxInput(checked) => checked.to_string(), + _ => element_node_data .attr(local_name!("value")) .unwrap_or_default() .to_string(), - values, }; + let form_data = NativeFormData { value, values }; Rc::new(PlatformEventData::new(Box::new(form_data))) } @@ -563,8 +570,11 @@ impl WriteMutations for MutationWriter<'_> { let node_id = self.state.element_to_node_id(id); let node = self.doc.get_node_mut(node_id).unwrap(); if let NodeData::Element(ref mut element) = node.raw_dom_data { - // FIXME: support non-text attributes - if let AttributeValue::Text(val) = value { + if element.name.local == local_name!("input") && name == "checked" { + set_input_checked_state(element, value); + } + // FIXME: support other non-text attributes + else if let AttributeValue::Text(val) = value { // FIXME check namespace let existing_attr = element .attrs @@ -669,6 +679,42 @@ impl WriteMutations for MutationWriter<'_> { } } +/// Set 'checked' state on an input based on given attributevalue +fn set_input_checked_state(element: &mut ElementNodeData, value: &AttributeValue) { + let checked: bool; + match value { + AttributeValue::Bool(checked_bool) => { + checked = *checked_bool; + } + AttributeValue::Text(val) => { + if let Ok(checked_bool) = val.parse() { + checked = checked_bool; + } else { + return; + }; + } + _ => { + return; + } + }; + match element.node_specific_data { + NodeSpecificData::CheckboxInput(ref mut checked_mut) => *checked_mut = checked, + // If we have just constructed the element, set the node attribute, + // and NodeSpecificData will be created from that later + // this simulates the checked attribute being set in html, + // and the element's checked property being set from that + NodeSpecificData::None => element.attrs.push(Attribute { + name: QualName { + prefix: None, + ns: ns!(html), + local: local_name!("checked"), + }, + value: checked.to_string(), + }), + _ => {} + } +} + fn create_template_node(doc: &mut Document, node: &TemplateNode) -> NodeId { match node { TemplateNode::Element { diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 12a57c60..1b2156fe 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -2,7 +2,7 @@ use crate::events::{EventData, HitResult, RendererEvent}; use crate::node::{ImageData, NodeSpecificData, TextBrush}; use crate::{ElementNodeData, Node, NodeData, TextNodeData, Viewport}; use app_units::Au; -use html5ever::{local_name, namespace_url, ns, QualName}; +use html5ever::local_name; use peniko::kurbo; // use quadtree_rs::Quadtree; use crate::util::Resource; @@ -316,24 +316,10 @@ impl Document { } pub fn toggle_checkbox(el: &mut ElementNodeData) { - let is_checked = el - .attrs - .iter() - .any(|attr| attr.name.local == local_name!("checked")); - - if is_checked { - el.attrs - .retain(|attr| attr.name.local != local_name!("checked")) - } else { - el.attrs.push(Attribute { - name: QualName { - prefix: None, - ns: ns!(html), - local: local_name!("checked"), - }, - value: String::new(), - }) - } + let Some(is_checked) = el.checkbox_input_checked_mut() else { + return; + }; + *is_checked = !*is_checked; } pub fn root_node(&self) -> &Node { @@ -591,7 +577,8 @@ impl Document { } 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)) + 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(); diff --git a/packages/dom/src/layout/construct.rs b/packages/dom/src/layout/construct.rs index bf62b0a3..2b61e2b2 100644 --- a/packages/dom/src/layout/construct.rs +++ b/packages/dom/src/layout/construct.rs @@ -44,6 +44,9 @@ pub(crate) fn collect_layout_children( ) { create_text_editor(doc, container_node_id, false); return; + } else if type_attr == Some("checkbox") { + create_checkbox_input(doc, container_node_id); + return; } } @@ -303,6 +306,20 @@ fn create_text_editor(doc: &mut Document, input_element_id: usize, is_multiline: } } +fn create_checkbox_input(doc: &mut Document, input_element_id: usize) { + let node = &mut doc.nodes[input_element_id]; + + let element = &mut node.raw_dom_data.downcast_element_mut().unwrap(); + if !matches!( + element.node_specific_data, + NodeSpecificData::CheckboxInput(_) + ) { + let checked = element.attr_parsed(local_name!("checked")).unwrap_or(false); + + element.node_specific_data = NodeSpecificData::CheckboxInput(checked); + } +} + pub(crate) fn build_inline_layout( doc: &mut Document, inline_context_root_node_id: usize, diff --git a/packages/dom/src/node.rs b/packages/dom/src/node.rs index 870ba44b..1a0da2ba 100644 --- a/packages/dom/src/node.rs +++ b/packages/dom/src/node.rs @@ -391,6 +391,20 @@ impl ElementNodeData { } } + pub fn checkbox_input_checked(&self) -> Option { + match self.node_specific_data { + NodeSpecificData::CheckboxInput(checked) => Some(checked), + _ => None, + } + } + + pub fn checkbox_input_checked_mut(&mut self) -> Option<&mut bool> { + match self.node_specific_data { + NodeSpecificData::CheckboxInput(ref mut checked) => Some(checked), + _ => None, + } + } + pub fn inline_layout_data(&self) -> Option<&TextLayout> { match self.node_specific_data { NodeSpecificData::InlineRoot(ref data) => Some(data), @@ -503,6 +517,8 @@ pub enum NodeSpecificData { TableRoot(Arc), /// Parley text editor (text inputs) TextInput(TextInputData), + /// Checkbox checked state + CheckboxInput(bool), /// No data (for nodes that don't need any node-specific data) None, } @@ -515,6 +531,7 @@ impl std::fmt::Debug for NodeSpecificData { NodeSpecificData::InlineRoot(_) => f.write_str("NodeSpecificData::InlineRoot"), NodeSpecificData::TableRoot(_) => f.write_str("NodeSpecificData::TableRoot"), NodeSpecificData::TextInput(_) => f.write_str("NodeSpecificData::TextInput"), + NodeSpecificData::CheckboxInput(_) => f.write_str("NodeSpecificData::CheckboxInput"), NodeSpecificData::None => f.write_str("NodeSpecificData::None"), } } diff --git a/packages/dom/src/stylo.rs b/packages/dom/src/stylo.rs index 7ea9e0eb..408483a5 100644 --- a/packages/dom/src/stylo.rs +++ b/packages/dom/src/stylo.rs @@ -407,7 +407,11 @@ impl<'a> selectors::Element for BlitzNode<'a> { && elem.attr(local_name!("href")).is_some() }) .unwrap_or(false), - NonTSPseudoClass::Checked => false, + NonTSPseudoClass::Checked => self + .raw_dom_data + .downcast_element() + .and_then(|elem| elem.checkbox_input_checked()) + .unwrap_or(false), NonTSPseudoClass::Valid => false, NonTSPseudoClass::Invalid => false, NonTSPseudoClass::Defined => false, From 5f6bd08b7ddfc055c61d7b5e8babc5bc4ace05bc Mon Sep 17 00:00:00 2001 From: koko Date: Sat, 14 Sep 2024 00:16:37 +0200 Subject: [PATCH 15/24] Moving to dynamic dispatch, adding callback, adding traits crate, removing sync provider (im sorry) --- Cargo.toml | 5 +- examples/screenshot.rs | 32 ++++-- packages/dioxus-blitz/Cargo.toml | 3 +- packages/dioxus-blitz/src/lib.rs | 75 +++++++++++--- packages/dioxus-blitz/src/window.rs | 18 ++-- packages/dom/Cargo.toml | 2 +- packages/dom/src/document.rs | 2 +- packages/dom/src/html_document.rs | 6 +- packages/dom/src/htmlsink.rs | 33 +++--- packages/dom/src/util.rs | 22 ++-- packages/net/Cargo.toml | 17 +--- packages/net/src/lib.rs | 112 +++++++++++++++------ packages/net/src/provider/blocking.rs | 74 -------------- packages/net/src/provider/dummy.rs | 11 -- packages/net/src/provider/mod.rs | 13 --- packages/net/src/provider/non_blocking.rs | 117 ---------------------- packages/traits/Cargo.toml | 8 ++ packages/traits/src/lib.rs | 1 + packages/traits/src/net.rs | 42 ++++++++ 19 files changed, 275 insertions(+), 318 deletions(-) delete mode 100644 packages/net/src/provider/blocking.rs delete mode 100644 packages/net/src/provider/dummy.rs delete mode 100644 packages/net/src/provider/mod.rs delete mode 100644 packages/net/src/provider/non_blocking.rs create mode 100644 packages/traits/Cargo.toml create mode 100644 packages/traits/src/lib.rs create mode 100644 packages/traits/src/net.rs diff --git a/Cargo.toml b/Cargo.toml index 3608fd38..92435c1a 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", "packages/net"] +members = ["packages/blitz", "packages/dom", "packages/dioxus-blitz", "packages/net", "packages/traits"] resolver = "2" [workspace.dependencies] @@ -55,7 +55,8 @@ incremental = false # mozbuild = "0.1.0" blitz = { path = "./packages/blitz" } blitz-dom = { path = "./packages/dom" } -blitz-net = { path = "./packages/net", features = ["blocking"]} +blitz-net = { path = "./packages/net" } +blitz-traits = { path = "./packages/traits" } comrak = { version = "0.21.0", 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 da4120a3..9758dc7e 100644 --- a/examples/screenshot.rs +++ b/examples/screenshot.rs @@ -1,15 +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::SyncProvider; +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"; @@ -47,20 +51,36 @@ async fn main() { .and_then(|arg| arg.parse().ok()) .unwrap_or(1200); - let net = SyncProvider::new(); + let (mut recv, callback) = MpscCallback::new(); + let net = Arc::new(Provider::new( + Handle::current(), + Arc::new(callback) as SharedCallback, + )); + + timer.time("Setup document prerequsits"); // Create HtmlDocument - let mut document = HtmlDocument::from_html(&html, Some(url.clone()), Vec::new(), &net); + 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)); - for resource in net.0.into_inner().drain(..) { - document.as_mut().load_resource(resource) + while let Some(res) = recv.recv().await { + document.as_mut().load_resource(res); + if net.is_empty() { + break; + } } - timer.time("Created document (+ fetched assets)"); + 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 2b8b4377..fe37039d 100644 --- a/packages/dioxus-blitz/Cargo.toml +++ b/packages/dioxus-blitz/Cargo.toml @@ -26,7 +26,8 @@ style = { workspace = true } tracing = { workspace = true, optional = true } blitz = { path = "../blitz" } blitz-dom = { path = "../dom" } -blitz-net = { path = "../net", features = ["non_blocking"] } +blitz-net = { path = "../net" } +blitz-traits = { path = "../traits" } html-escape = "0.2.13" url = { version = "2.5.0", features = ["serde"] } ureq = "2.9" diff --git a/packages/dioxus-blitz/src/lib.rs b/packages/dioxus-blitz/src/lib.rs index bdc250fb..0897d31b 100644 --- a/packages/dioxus-blitz/src/lib.rs +++ b/packages/dioxus-blitz/src/lib.rs @@ -22,24 +22,28 @@ mod menu; 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::AsyncProvider; +use blitz_net::Provider; +use blitz_traits::net::SharedCallback; use dioxus::prelude::{ComponentFunction, Element, VirtualDom}; -use std::sync::Arc; +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, }; -pub use crate::documents::DioxusDocument; -pub use crate::waker::BlitzEvent; -pub use crate::window::WindowConfig; - pub mod exports { pub use dioxus; } @@ -116,17 +120,18 @@ pub fn launch_static_html_cfg(html: &str, cfg: Config) { .unwrap(); let _guard = rt.enter(); - let net = AsyncProvider::new(&rt); - let document = HtmlDocument::from_html(html, cfg.base_url, cfg.stylesheets, &net); - launch_with_document(document, rt, Some(Arc::new(net))); + 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, - rt: Runtime, - net: Option>>, -) { +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( @@ -137,7 +142,7 @@ fn launch_with_document( .into(), ); } - let window = WindowConfig::new(doc, net); + let window = WindowConfig::new(doc, net_callback); launch_with_window(window, rt) } @@ -197,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/window.rs b/packages/dioxus-blitz/src/window.rs index ccf3c1c3..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}; @@ -9,8 +9,6 @@ use winit::keyboard::PhysicalKey; #[allow(unused)] use wgpu::rwh::HasWindowHandle; -use blitz_dom::util::Resource; -use blitz_net::AsyncProvider; use std::sync::Arc; use std::task::Waker; use winit::event::{ElementState, MouseButton}; @@ -24,27 +22,27 @@ use crate::menu::init_menu; pub struct WindowConfig { doc: Doc, attributes: WindowAttributes, - net: Option>>, + callback: Option>, } impl WindowConfig { - pub fn new(doc: Doc, net: Option>>) -> Self { + pub fn new(doc: Doc, callback: Option>) -> Self { WindowConfig { doc, attributes: Window::default_attributes(), - net, + callback, } } pub fn with_attributes( doc: Doc, attributes: WindowAttributes, - net: Option>>, + callback: Option>, ) -> Self { WindowConfig { doc, attributes, - net, + callback, } } } @@ -87,8 +85,8 @@ impl View { ) -> Self { let winit_window = Arc::from(event_loop.create_window(config.attributes).unwrap()); - if let Some(net) = config.net { - net.resolve(proxy.clone(), winit_window.id()); + if let Some(callback) = config.callback { + callback.init(winit_window.id(), proxy); } // TODO: make this conditional on text input focus diff --git a/packages/dom/Cargo.toml b/packages/dom/Cargo.toml index 5ad4e698..2bb81b93 100644 --- a/packages/dom/Cargo.toml +++ b/packages/dom/Cargo.toml @@ -8,7 +8,7 @@ default = ["tracing"] tracing = ["dep:tracing"] [dependencies] -blitz-net = { path = "../net" } +blitz-traits = { path = "../traits" } style = { workspace = true, features = ["servo"] } selectors = { workspace = true } style_config = { workspace = true } diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 1b2156fe..b5efc162 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -551,6 +551,7 @@ impl Document { 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)) @@ -573,7 +574,6 @@ impl Document { match resource { Resource::Css(node_id, css) => { self.append_or_insert_stylesheet(css, node_id); - self.resolve(); } Resource::Image(node_id, image) => { let node = self.get_node_mut(node_id).unwrap(); diff --git a/packages/dom/src/html_document.rs b/packages/dom/src/html_document.rs index 9284a4b0..286de5a4 100644 --- a/packages/dom/src/html_document.rs +++ b/packages/dom/src/html_document.rs @@ -3,7 +3,7 @@ use crate::{Document, DocumentHtmlParser, DocumentLike, Viewport}; use crate::util::Resource; use crate::DEFAULT_CSS; -use blitz_net::NetProvider; +use blitz_traits::net::SharedProvider; pub struct HtmlDocument { inner: Document, @@ -33,11 +33,11 @@ impl DocumentLike for HtmlDocument { } impl HtmlDocument { - pub fn from_html>( + pub fn from_html( html: &str, base_url: Option, stylesheets: Vec, - net: N, + net: SharedProvider, ) -> Self { // Spin up the virtualdom and include the default stylesheet let viewport = Viewport::new(0, 0, 1.0); diff --git a/packages/dom/src/htmlsink.rs b/packages/dom/src/htmlsink.rs index 78f56820..2c9b7327 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -1,7 +1,7 @@ use crate::node::{Attribute, ElementNodeData, Node, NodeData}; use crate::util::{CssHandler, ImageHandler, Resource}; use crate::Document; -use blitz_net::NetProvider; +use blitz_traits::net::SharedProvider; use html5ever::local_name; use html5ever::{ tendril::{StrTendril, TendrilSink}, @@ -20,7 +20,7 @@ fn html5ever_to_blitz_attr(attr: html5ever::Attribute) -> Attribute { } } -pub struct DocumentHtmlParser<'a, N> { +pub struct DocumentHtmlParser<'a> { doc: &'a mut Document, style_nodes: Vec, @@ -31,11 +31,11 @@ pub struct DocumentHtmlParser<'a, N> { /// The document's quirks mode. pub quirks_mode: QuirksMode, - net_provider: N, + net_provider: SharedProvider, } -impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { - pub fn new(doc: &mut Document, net_provider: N) -> DocumentHtmlParser { +impl<'a> DocumentHtmlParser<'a> { + pub fn new(doc: &mut Document, net_provider: SharedProvider) -> DocumentHtmlParser { DocumentHtmlParser { doc, style_nodes: Vec::new(), @@ -45,7 +45,11 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { } } - pub fn parse_into_doc<'d>(doc: &'d mut Document, net: N, html: &str) -> &'d mut Document { + 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() @@ -98,11 +102,11 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { let url = self.doc.resolve_url(href); self.net_provider.fetch( url.clone(), - CssHandler { + Box::new(CssHandler { node: target_id, source_url: url, guard: self.doc.guard.clone(), - }, + }), ); } } @@ -112,7 +116,8 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { if let Some(raw_src) = node.attr(local_name!("src")) { if !raw_src.is_empty() { let src = self.doc.resolve_url(raw_src); - self.net_provider.fetch(src, ImageHandler::new(target_id)); + self.net_provider + .fetch(src, Box::new(ImageHandler::new(target_id))); } } } @@ -139,7 +144,7 @@ impl<'a, N: NetProvider> DocumentHtmlParser<'a, N> { } } -impl<'b, N: NetProvider> TreeSink for DocumentHtmlParser<'b, N> { +impl<'b> TreeSink for DocumentHtmlParser<'b> { type Output = &'b mut Document; // we use the ID of the nodes in the tree as the handle @@ -360,12 +365,16 @@ impl<'b, N: NetProvider> TreeSink for DocumentHtmlParser<'b, N> { #[test] fn parses_some_html() { use crate::Viewport; - use blitz_net::DummyProvider; + 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, DummyProvider); + let sink = DocumentHtmlParser::new( + &mut doc, + Arc::new(DummyProvider::default()) as SharedProvider, + ); html5ever::parse_document(sink, Default::default()) .from_utf8() diff --git a/packages/dom/src/util.rs b/packages/dom/src/util.rs index 2949d46b..b5c0a171 100644 --- a/packages/dom/src/util.rs +++ b/packages/dom/src/util.rs @@ -1,5 +1,4 @@ use crate::node::{Node, NodeData}; -use blitz_net::RequestHandler; use image::DynamicImage; use selectors::context::QuirksMode; use std::{ @@ -29,8 +28,9 @@ pub(crate) struct CssHandler { pub source_url: Url, pub guard: SharedRwLock, } -impl RequestHandler for CssHandler { - fn bytes(self, bytes: &[u8]) -> Resource { +impl RequestHandler for CssHandler { + type Data = Resource; + fn bytes(self: Box, bytes: &[u8], callback: SharedCallback) { let css = String::from_utf8(bytes.into()).expect("Invalid UTF8"); let escaped_css = html_escape::decode_html_entities(&css); let sheet = Stylesheet::from_str( @@ -44,7 +44,10 @@ impl RequestHandler for CssHandler { QuirksMode::NoQuirks, AllowImportRules::Yes, ); - Resource::Css(self.node, DocumentStyleSheet(ServoArc::new(sheet))) + callback.call(Resource::Css( + self.node, + DocumentStyleSheet(ServoArc::new(sheet)), + )) } } pub(crate) struct ImageHandler(usize); @@ -53,15 +56,17 @@ impl ImageHandler { Self(node_id) } } -impl RequestHandler for ImageHandler { - fn bytes(self, bytes: &[u8]) -> Resource { +impl RequestHandler for ImageHandler { + type Data = Resource; + fn bytes(self: Box, bytes: &[u8], 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() { - return Resource::Image(self.0, Arc::new(image)); + callback.call(Resource::Image(self.0, Arc::new(image))); + return; }; // Try parse SVG @@ -78,7 +83,7 @@ impl RequestHandler for ImageHandler { }; let tree = Tree::from_data(bytes, &options).unwrap(); - Resource::Svg(self.0, Box::new(tree)) + callback.call(Resource::Svg(self.0, Box::new(tree))); } } @@ -147,6 +152,7 @@ pub fn walk_tree(indent: usize, node: &Node) { } } +use blitz_traits::net::{RequestHandler, SharedCallback}; use peniko::Color as PenikoColor; pub trait ToPenikoColor { diff --git a/packages/net/Cargo.toml b/packages/net/Cargo.toml index 4b1b8198..bdcad152 100644 --- a/packages/net/Cargo.toml +++ b/packages/net/Cargo.toml @@ -4,19 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tokio = { workspace = true, optional = true } -reqwest = { version = "0.12.7", optional = true } -winit = { version = "0.30.5", optional = true } -futures-util = { version = "0.3.30", optional = true } -ureq = { version = "2.10.1", optional = true } - -http = "1.1.0" -url = "2.5.2" +blitz-traits = { path = "../traits" } +tokio = { workspace = true } +reqwest = { version = "0.12.7" } data-url = "0.3.1" -tracing = "0.1.40" thiserror = "1.0.63" - -[features] -default = [] -blocking = ["dep:ureq"] -non_blocking = ["dep:tokio", "dep:winit", "dep:futures-util", "dep:reqwest"] \ No newline at end of file diff --git a/packages/net/src/lib.rs b/packages/net/src/lib.rs index acc64dc9..1fb6fc75 100644 --- a/packages/net/src/lib.rs +++ b/packages/net/src/lib.rs @@ -1,42 +1,94 @@ -mod provider; +use blitz_traits::net::{BoxedHandler, Callback, NetProvider, SharedCallback, Url}; +use data_url::DataUrl; +use reqwest::Client; +use std::sync::Arc; +use thiserror::Error; +use tokio::runtime::Handle; -use std::ops::Deref; -use url::Url; - -pub use http::Method; - -pub use provider::*; - -#[cfg(any(feature = "blocking", feature = "non_blocking"))] const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"; -pub trait NetProvider { - fn fetch(&self, url: Url, handler: H) - where - H: RequestHandler; +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(&decoded.0, callback); + } + "file" => { + let file_content = std::fs::read(url.path())?; + handler.bytes(&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 D -where - P: NetProvider, - D: Deref, -{ - fn fetch(&self, url: Url, handler: H) - where - H: RequestHandler, - { - self.deref().fetch(url, handler) +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(Self::fetch_inner(client, url, callback, handler)), + ); } } -pub trait RequestHandler: Send + Sync + 'static { - fn bytes(self, bytes: &[u8]) -> T; - fn method(&self) -> Method { - Method::GET +#[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), +} + +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +pub struct MpscCallback(UnboundedSender); +impl MpscCallback { + pub fn new() -> (UnboundedReceiver, Self) { + let (send, recv) = unbounded_channel(); + (recv, Self(send)) } } -impl T + Sync + Send + 'static, T> RequestHandler for F { - fn bytes(self, bytes: &[u8]) -> T { - self(bytes) +impl Callback for MpscCallback { + type Data = T; + fn call(self: Arc, data: Self::Data) { + let _ = self.0.send(data); } } diff --git a/packages/net/src/provider/blocking.rs b/packages/net/src/provider/blocking.rs deleted file mode 100644 index ac4094dd..00000000 --- a/packages/net/src/provider/blocking.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::{NetProvider, RequestHandler, USER_AGENT}; -use std::cell::RefCell; -use std::io::Read; -use thiserror::Error; -use url::Url; - -const FILE_SIZE_LIMIT: u64 = 1_000_000_000; // 1GB - -pub struct SyncProvider(pub RefCell>); -impl SyncProvider { - pub fn new() -> Self { - Self(RefCell::new(Vec::new())) - } - fn fetch_inner>( - &self, - url: Url, - handler: &H, - ) -> Result, SyncProviderError> { - Ok(match url.scheme() { - "data" => { - let data_url = data_url::DataUrl::process(url.as_str())?; - let decoded = data_url.decode_to_vec()?; - decoded.0 - } - "file" => { - let file_content = std::fs::read(url.path())?; - file_content - } - _ => { - let response = ureq::request(handler.method().as_str(), url.as_str()) - .set("User-Agent", USER_AGENT) - .call()?; - - let len: usize = response - .header("Content-Length") - .and_then(|c| c.parse().ok()) - .unwrap_or(0); - let mut bytes: Vec = Vec::with_capacity(len); - response - .into_reader() - .take(FILE_SIZE_LIMIT) - .read_to_end(&mut bytes)?; - bytes - } - }) - } -} -impl NetProvider for SyncProvider { - fn fetch(&self, url: Url, handler: H) - where - H: RequestHandler, - { - let res = match self.fetch_inner(url, &handler) { - Ok(v) => v, - Err(e) => { - tracing::error!("{e}"); - return; - } - }; - self.0.borrow_mut().push(handler.bytes(&res)); - } -} - -#[derive(Error, Debug)] -enum SyncProviderError { - #[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}")] - Ureq(#[from] ureq::Error), -} diff --git a/packages/net/src/provider/dummy.rs b/packages/net/src/provider/dummy.rs deleted file mode 100644 index 335609d0..00000000 --- a/packages/net/src/provider/dummy.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::{NetProvider, RequestHandler}; -use url::Url; - -pub struct DummyProvider; -impl NetProvider for DummyProvider { - fn fetch(&self, _url: Url, _handler: H) - where - H: RequestHandler, - { - } -} diff --git a/packages/net/src/provider/mod.rs b/packages/net/src/provider/mod.rs deleted file mode 100644 index 35883b72..00000000 --- a/packages/net/src/provider/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[cfg(feature = "blocking")] -mod blocking; -mod dummy; -#[cfg(feature = "non_blocking")] -mod non_blocking; - -#[cfg(feature = "non_blocking")] -pub use non_blocking::AsyncProvider; - -#[cfg(feature = "blocking")] -pub use blocking::SyncProvider; - -pub use dummy::DummyProvider; diff --git a/packages/net/src/provider/non_blocking.rs b/packages/net/src/provider/non_blocking.rs deleted file mode 100644 index 715800ad..00000000 --- a/packages/net/src/provider/non_blocking.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::{NetProvider, RequestHandler, USER_AGENT}; -use data_url::DataUrl; -use futures_util::{stream::FuturesUnordered, StreamExt}; -use reqwest::Client; -use std::{sync::Arc, time::Duration}; -use thiserror::Error; -use tokio::{ - runtime::{Handle, Runtime}, - sync::Mutex, - task::JoinHandle, -}; -use url::Url; -use winit::{event_loop::EventLoopProxy, window::WindowId}; - -type TaskHandle = JoinHandle>; - -pub struct AsyncProvider { - rt: Handle, - client: Client, - futures: Mutex>>, -} -impl AsyncProvider { - pub fn new(rt: &Runtime) -> Self { - Self { - rt: rt.handle().clone(), - client: Client::new(), - futures: Mutex::new(FuturesUnordered::new()), - } - } - pub fn resolve + Send>( - self: Arc, - event_loop_proxy: EventLoopProxy

, - window_id: WindowId, - ) { - self.rt.clone().spawn(async move { - let mut interval = tokio::time::interval(Duration::from_millis(100)); - - 'thread: loop { - interval.tick().await; - while let Some(ir) = self.futures.lock().await.next().await { - match ir { - Ok(Ok(t)) => { - let e = event_loop_proxy.send_event((window_id, t).into()); - if e.is_err() { - break 'thread; - } - } - Ok(Err(e)) => { - tracing::error!("Fetch failed with {e:?}") - } - Err(e) => { - tracing::error!("Fetch thread failed with {e}") - } - } - } - } - }); - } -} -impl AsyncProvider { - async fn fetch_inner>( - client: Client, - url: Url, - handler: H, - ) -> Result { - match url.scheme() { - "data" => { - let data_url = DataUrl::process(url.as_str())?; - let decoded = data_url.decode_to_vec()?; - Ok(handler.bytes(&decoded.0)) - } - "file" => { - let file_content = std::fs::read(url.path())?; - Ok(handler.bytes(&file_content)) - } - _ => { - let start = tokio::time::Instant::now(); - - let response = client - .request(handler.method(), url.clone()) - .header("User-Agent", USER_AGENT) - .send() - .await?; - let res = handler.bytes(&response.bytes().await?); - tracing::info!( - "Loaded {} in: {}ms", - url.as_str(), - start.elapsed().as_millis() - ); - Ok(res) - } - } - } -} - -impl NetProvider for AsyncProvider { - fn fetch(&self, url: Url, handler: H) - where - H: RequestHandler, - { - let client = self.client.clone(); - let join = self.rt.spawn(Self::fetch_inner(client, url, handler)); - self.futures.blocking_lock().push(join); - } -} - -#[derive(Error, Debug)] -enum AsyncProviderError { - #[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), -} diff --git a/packages/traits/Cargo.toml b/packages/traits/Cargo.toml new file mode 100644 index 00000000..9495bbde --- /dev/null +++ b/packages/traits/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "blitz-traits" +version = "0.1.0" +edition = "2021" + +[dependencies] +http = "1.1.0" +url = "2.5.2" \ 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..c3fe711b --- /dev/null +++ b/packages/traits/src/net.rs @@ -0,0 +1,42 @@ +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 { + type Data; + fn fetch(&self, url: Url, handler: BoxedHandler); +} + +pub trait RequestHandler: Send + Sync + 'static { + type Data; + fn bytes(self: Box, bytes: &[u8], 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) {} +} +pub struct DummyCallback(PhantomData); +impl Callback for DummyCallback { + type Data = T; + fn call(self: Arc, _data: Self::Data) {} +} From 54a916740aa5c0b1d9539721e5dd5fe5fe13a049 Mon Sep 17 00:00:00 2001 From: koko Date: Sat, 14 Sep 2024 01:06:21 +0200 Subject: [PATCH 16/24] fmt, clippy and some git weirdness --- Cargo.toml | 12 +-- packages/dom/src/htmlsink.rs | 144 ++++++++++++++++++----------------- packages/dom/src/stylo.rs | 13 ++++ packages/net/Cargo.toml | 11 --- packages/net/src/lib.rs | 94 ----------------------- packages/traits/Cargo.toml | 7 -- packages/traits/src/lib.rs | 1 - packages/traits/src/net.rs | 42 ---------- 8 files changed, 93 insertions(+), 231 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 92435c1a..e88517aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,12 +7,12 @@ members = ["packages/blitz", "packages/dom", "packages/dioxus-blitz", "packages/ resolver = "2" [workspace.dependencies] -style = { git = "https://github.com/dioxuslabs/stylo", branch = "enable-table-moz-center-style-adjust" } -style_config = { git = "https://github.com/dioxuslabs/stylo", branch = "enable-table-moz-center-style-adjust" } -style_traits = { git = "https://github.com/dioxuslabs/stylo", branch = "enable-table-moz-center-style-adjust" } -style_dom = { git = "https://github.com/dioxuslabs/stylo", package = "dom", branch = "enable-table-moz-center-style-adjust" } -selectors = { git = "https://github.com/dioxuslabs/stylo", branch = "enable-table-moz-center-style-adjust" } -html5ever = "0.27" # needs to match stylo markup5ever version +style = { git = "https://github.com/dioxuslabs/stylo", branch = "blitz" } +style_config = { git = "https://github.com/dioxuslabs/stylo", branch = "blitz" } +style_traits = { git = "https://github.com/dioxuslabs/stylo", branch = "blitz" } +style_dom = { git = "https://github.com/dioxuslabs/stylo", package = "dom", branch = "blitz" } +selectors = { git = "https://github.com/dioxuslabs/stylo", branch = "blitz" } +html5ever = "0.29" # needs to match stylo markup5ever version taffy = { git = "https://github.com/dioxuslabs/taffy", rev = "950a0eb1322f15e5d1083f4793b55d52061718de" } parley = { git = "https://github.com/nicoburns/parley", rev = "029bf1df3e1829935fa6d25b875d3138f79a62c1" } dioxus = { git = "https://github.com/dioxuslabs/dioxus", rev = "a3aa6ae771a2d0a4d8cb6055c41efc0193b817ef" } diff --git a/packages/dom/src/htmlsink.rs b/packages/dom/src/htmlsink.rs index 2c9b7327..d5466236 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -1,15 +1,17 @@ -use crate::node::{Attribute, ElementNodeData, Node, NodeData}; use crate::util::{CssHandler, ImageHandler, Resource}; +use std::borrow::Cow; +use std::cell::{Cell, Ref, RefCell, RefMut}; +use std::collections::HashSet; + +use crate::node::{Attribute, ElementNodeData, Node, NodeData}; use crate::Document; use blitz_traits::net::SharedProvider; use html5ever::local_name; use html5ever::{ tendril::{StrTendril, TendrilSink}, tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink}, - ExpandedName, QualName, + QualName, }; -use std::borrow::Cow; -use std::collections::HashSet; /// Convert an html5ever Attribute which uses tendril for its value to a blitz Attribute /// which uses String. @@ -21,15 +23,15 @@ fn html5ever_to_blitz_attr(attr: html5ever::Attribute) -> Attribute { } pub struct DocumentHtmlParser<'a> { - doc: &'a mut Document, + doc: RefCell<&'a mut Document>, - style_nodes: Vec, + style_nodes: RefCell>, /// Errors that occurred during parsing. - pub errors: Vec>, + pub errors: RefCell>>, /// The document's quirks mode. - pub quirks_mode: QuirksMode, + pub quirks_mode: Cell, net_provider: SharedProvider, } @@ -37,10 +39,10 @@ pub struct DocumentHtmlParser<'a> { impl<'a> DocumentHtmlParser<'a> { pub fn new(doc: &mut Document, net_provider: SharedProvider) -> DocumentHtmlParser { DocumentHtmlParser { - doc, - style_nodes: Vec::new(), - errors: Vec::new(), - quirks_mode: QuirksMode::NoQuirks, + doc: RefCell::new(doc), + style_nodes: RefCell::new(Vec::new()), + errors: RefCell::new(Vec::new()), + quirks_mode: Cell::new(QuirksMode::NoQuirks), net_provider, } } @@ -57,27 +59,27 @@ impl<'a> DocumentHtmlParser<'a> { .unwrap() } - fn create_node(&mut self, node_data: NodeData) -> usize { - self.doc.create_node(node_data) + fn create_node(&self, node_data: NodeData) -> usize { + self.doc.borrow_mut().create_node(node_data) } - fn create_text_node(&mut self, text: &str) -> usize { - self.doc.create_text_node(text) + fn create_text_node(&self, text: &str) -> usize { + self.doc.borrow_mut().create_text_node(text) } - fn node(&self, id: usize) -> &Node { - &self.doc.nodes[id] + fn node(&self, id: usize) -> Ref { + Ref::map(self.doc.borrow(), |doc| &doc.nodes[id]) } - fn node_mut(&mut self, id: usize) -> &mut Node { - &mut self.doc.nodes[id] + fn node_mut(&self, id: usize) -> RefMut { + RefMut::map(self.doc.borrow_mut(), |doc| &mut doc.nodes[id]) } - fn try_append_text_to_text_node(&mut self, node_id: Option, text: &str) -> bool { + fn try_append_text_to_text_node(&self, node_id: Option, text: &str) -> bool { let Some(node_id) = node_id else { return false; }; - let node = self.node_mut(node_id); + let mut node = self.node_mut(node_id); match node.text_data_mut() { Some(data) => { @@ -88,41 +90,42 @@ impl<'a> DocumentHtmlParser<'a> { } } - fn last_child(&mut self, parent_id: usize) -> Option { + fn last_child(&self, parent_id: usize) -> Option { self.node(parent_id).children.last().copied() } - fn load_linked_stylesheet(&mut self, target_id: usize) { + fn load_linked_stylesheet(&self, target_id: usize) { let node = self.node(target_id); let rel_attr = node.attr(local_name!("rel")); let href_attr = node.attr(local_name!("href")); if let (Some("stylesheet"), Some(href)) = (rel_attr, href_attr) { - let url = self.doc.resolve_url(href); + let url = self.doc.borrow().resolve_url(href); + let guard = self.doc.borrow().guard.clone(); self.net_provider.fetch( url.clone(), Box::new(CssHandler { node: target_id, source_url: url, - guard: self.doc.guard.clone(), + guard, }), ); } } - fn load_image(&mut self, target_id: usize) { + fn load_image(&self, target_id: usize) { let node = self.node(target_id); if let Some(raw_src) = node.attr(local_name!("src")) { if !raw_src.is_empty() { - let src = self.doc.resolve_url(raw_src); + let src = self.doc.borrow().resolve_url(raw_src); self.net_provider .fetch(src, Box::new(ImageHandler::new(target_id))); } } } - fn process_button_input(&mut self, target_id: usize) { + fn process_button_input(&self, target_id: usize) { let node = self.node(target_id); let Some(data) = node.element_data() else { return; @@ -150,47 +153,52 @@ impl<'b> TreeSink for DocumentHtmlParser<'b> { // we use the ID of the nodes in the tree as the handle type Handle = usize; + type ElemName<'a> = Ref<'a, QualName> where Self: 'a; + fn finish(self) -> Self::Output { + let doc = self.doc.into_inner(); + // Add inline stylesheets (