From 2082a737d9088a940b9971b13ee4d9db7278ccdb Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Fri, 30 Aug 2024 04:46:27 +1000 Subject: [PATCH 01/13] Implement box-shadow drawing (#122) * Implement box-shadow drawing (no inset support yet) * Remove stray dbg, add warning print when trying to render inset * change shadow to rgba(0,0,0,0.6) from black * Add support for inset shadows * update cargo.toml formatting * Update vello version in dependencies, remove patch --- Cargo.toml | 6 +- examples/tailwind.rs | 8 +++ packages/blitz/Cargo.toml | 2 +- packages/blitz/src/renderer.rs | 3 +- packages/blitz/src/renderer/render.rs | 83 +++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 55060ab5..17fa840d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,14 +15,14 @@ selectors = { git = "https://github.com/dioxuslabs/stylo", branch = "enable-tabl html5ever = "0.27" # 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"} +dioxus = { git = "https://github.com/dioxuslabs/dioxus", rev = "a3aa6ae771a2d0a4d8cb6055c41efc0193b817ef" } dioxus-ssr = { git = "https://github.com/dioxuslabs/dioxus", rev = "a3aa6ae771a2d0a4d8cb6055c41efc0193b817ef" } tokio = { version = "1.25.0", features = ["full"] } tracing = "0.1.40" -vello = { version = "0.2", features = ["wgpu"] } +vello = { git = "https://github.com/linebender/vello", rev = "aaa9f5f2d0f21f3d038501ea0cf32c989d97aab3", package = "vello", features = [ "wgpu" ] } peniko = { version = "0.1" } # fello = { git = "https://github.com/linebender/vello" } -wgpu = "0.20" +wgpu = "22.1.0" # This is a "virtual package" # It is not meant to be published, but is used so "cargo run --example XYZ" works properly diff --git a/examples/tailwind.rs b/examples/tailwind.rs index 807606d9..5a783194 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind.rs @@ -16,6 +16,7 @@ fn app() -> Element { for _row in 0..3 { div { class: "flex flex-row", div { id: "cool", "h123456789asdjkahskj\nhiiiii" } + div { id: "cool-inset", "h123456789asdjkahskj\nhiiiii" } p { class: "cool", "hi" } for x in 1..=9 { div { class: "bg-red-{x}00 border", "{x}" } @@ -30,6 +31,13 @@ p.cool { background-color: purple; } #cool { background-color: blue; font-size: 32px; + box-shadow: 16px 16px 16px rgba(0,0,0,0.6); +} +#cool-inset { + margin-top: 16px; + background-color: purple; + font-size: 32px; + box-shadow: inset 16px 16px 16px rgba(255,255,255,0.6); } .bg-red-100 { background-color: rgb(254 226 226); } .bg-red-200 { background-color: rgb(254 202 202); } diff --git a/packages/blitz/Cargo.toml b/packages/blitz/Cargo.toml index 03170b6a..924893a4 100644 --- a/packages/blitz/Cargo.toml +++ b/packages/blitz/Cargo.toml @@ -17,5 +17,5 @@ vello = { workspace = true } wgpu = { workspace = true } raw-window-handle = "0.6.0" image = "0.25" -vello_svg = { git = "https://github.com/DioxusLabs/vello_svg", rev = "6a8bf4abd1ad053431253964d1b62ad4427f4311" } +vello_svg = { git = "https://github.com/cfraz89/vello_svg", rev = "fc29d4ebf8d6aaee980b203f39ef2c73fe43c017" } futures-intrusive = "0.5.0" diff --git a/packages/blitz/src/renderer.rs b/packages/blitz/src/renderer.rs index 073ad310..bb05d2c6 100644 --- a/packages/blitz/src/renderer.rs +++ b/packages/blitz/src/renderer.rs @@ -140,6 +140,7 @@ where width: state.surface.config.width, height: state.surface.config.height, antialiasing_method: vello::AaConfig::Msaa16, + debug: vello::DebugLayers::none(), }; // Regenerate the vello scene @@ -215,7 +216,7 @@ pub async fn render_to_buffer(dom: &Document, viewport: Viewport) -> Vec { width, height, antialiasing_method: vello::AaConfig::Area, - // debug: vello::DebugLayers::none(), + debug: vello::DebugLayers::none(), }; renderer .render_to_texture(device, queue, &scene, &view, &render_params) diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index 07079dea..b05ed6db 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -357,7 +357,9 @@ impl<'dom> VelloSceneGenerator<'dom> { let cx = self.element_cx(element, location); cx.stroke_effects(scene); cx.stroke_outline(scene); + cx.draw_outset_box_shadow(scene); cx.stroke_frame(scene); + cx.draw_inset_box_shadow(scene); cx.stroke_border(scene); cx.stroke_devtools(scene); cx.draw_image(scene); @@ -931,6 +933,87 @@ impl ElementCx<'_> { // fn draw_image_frame(&self, scene: &mut Scene) {} + fn draw_outset_box_shadow(&self, scene: &mut Scene) { + let box_shadow = &self.style.get_effects().box_shadow.0; + for shadow in box_shadow.iter().filter(|s| !s.inset) { + let shadow_color = shadow.base.color.as_vello(); + if shadow_color != Color::TRANSPARENT { + let transform = self.transform.then_translate(Vec2 { + x: shadow.base.horizontal.px() as f64, + y: shadow.base.vertical.px() as f64, + }); + + //TODO draw shadows with matching individual radii instead of averaging + let radius = (self.frame.border_top_left_radius_height + + self.frame.border_bottom_left_radius_width + + self.frame.border_bottom_left_radius_height + + self.frame.border_bottom_left_radius_width + + self.frame.border_bottom_right_radius_height + + self.frame.border_bottom_right_radius_width + + self.frame.border_top_right_radius_height + + self.frame.border_top_right_radius_width) + / 8.0; + + // Fill the color + scene.draw_blurred_rounded_rect( + transform, + self.frame.outer_rect, + shadow_color, + radius, + shadow.base.blur.px() as f64, + ); + } + } + } + + fn draw_inset_box_shadow(&self, scene: &mut Scene) { + let box_shadow = &self.style.get_effects().box_shadow.0; + let has_inset_shadow = box_shadow.iter().any(|s| s.inset); + if has_inset_shadow { + CLIPS_WANTED.fetch_add(1, atomic::Ordering::SeqCst); + let clips_available = CLIPS_USED.load(atomic::Ordering::SeqCst) <= CLIP_LIMIT; + if clips_available { + scene.push_layer(Mix::Clip, 1.0, self.transform, &self.frame.frame()); + CLIPS_USED.fetch_add(1, atomic::Ordering::SeqCst); + let depth = CLIP_DEPTH.fetch_add(1, atomic::Ordering::SeqCst) + 1; + CLIP_DEPTH_USED.fetch_max(depth, atomic::Ordering::SeqCst); + } + } + for shadow in box_shadow.iter().filter(|s| s.inset) { + let shadow_color = shadow.base.color.as_vello(); + if shadow_color != Color::TRANSPARENT { + let transform = self.transform.then_translate(Vec2 { + x: shadow.base.horizontal.px() as f64, + y: shadow.base.vertical.px() as f64, + }); + + //TODO draw shadows with matching individual radii instead of averaging + let radius = (self.frame.border_top_left_radius_height + + self.frame.border_bottom_left_radius_width + + self.frame.border_bottom_left_radius_height + + self.frame.border_bottom_left_radius_width + + self.frame.border_bottom_right_radius_height + + self.frame.border_bottom_right_radius_width + + self.frame.border_top_right_radius_height + + self.frame.border_top_right_radius_width) + / 8.0; + + // Fill the color + scene.draw_blurred_rounded_rect( + transform, + self.frame.outer_rect, + shadow_color, + radius, + shadow.base.blur.px() as f64, + ); + } + } + if has_inset_shadow { + scene.pop_layer(); + CLIP_DEPTH.fetch_sub(1, atomic::Ordering::SeqCst); + } + } + fn draw_solid_frame(&self, scene: &mut Scene) { let background_color = &self.style.get_background().background_color; let bg_color = background_color.as_vello(); From b7c50e9b5d714d1572551f6041058b9eb0a6de2f Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 5 Sep 2024 22:53:38 +0100 Subject: [PATCH 02/13] Don't render box shadows underneath the element with the shadow --- .../src/renderer/multicolor_rounded_rect.rs | 45 +++++++++++++++++++ packages/blitz/src/renderer/render.rs | 19 ++++++++ 2 files changed, 64 insertions(+) diff --git a/packages/blitz/src/renderer/multicolor_rounded_rect.rs b/packages/blitz/src/renderer/multicolor_rounded_rect.rs index 32ac6263..a108416f 100644 --- a/packages/blitz/src/renderer/multicolor_rounded_rect.rs +++ b/packages/blitz/src/renderer/multicolor_rounded_rect.rs @@ -208,6 +208,38 @@ impl ElementFrame { } } + /// Construct a bezpath drawing the frame + pub fn shadow_clip(&self) -> BezPath { + let mut path = BezPath::new(); + self.shadow_clip_shape(&mut path); + path + } + + fn shadow_clip_shape(&self, path: &mut BezPath) { + use Corner::*; + + for corner in [TopLeft, TopRight, BottomRight, BottomLeft] { + path.insert_point(self.shadow_clip_corner(corner, 100.0)); + } + + if self.is_sharp(TopLeft) { + path.move_to(self.corner(TopLeft, ArcSide::Outer)); + } else { + const TOLERANCE: f64 = 0.1; + let arc = self.full_arc(TopLeft, ArcSide::Outer, Direction::Anticlockwise); + let elements = arc.path_elements(TOLERANCE); + path.extend(elements); + } + + for corner in [/*TopLeft, */ BottomLeft, BottomRight, TopRight] { + if self.is_sharp(corner) { + path.insert_point(self.corner(corner, ArcSide::Outer)); + } else { + path.insert_arc(self.full_arc(corner, ArcSide::Outer, Direction::Anticlockwise)); + } + } + } + fn corner(&self, corner: Corner, side: ArcSide) -> Point { let Rect { x0, y0, x1, y1 } = self.outer_rect; @@ -241,6 +273,19 @@ impl ElementFrame { Point { x, y } } + fn shadow_clip_corner(&self, corner: Corner, offset: f64) -> Point { + let Rect { x0, y0, x1, y1 } = self.outer_rect; + + let (x, y) = match corner { + Corner::TopLeft => (x0 - offset, y0 - offset), + Corner::TopRight => (x1 + offset, y0 - offset), + Corner::BottomRight => (x1 + offset, y1 + offset), + Corner::BottomLeft => (x0 - offset, y1 + offset), + }; + + Point { x, y } + } + /// Check if the corner width is smaller than the radius. /// If it is, we need to fill in the gap with an arc fn corner_needs_infill(&self, corner: Corner) -> bool { diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index b05ed6db..7306623d 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -935,6 +935,20 @@ impl ElementCx<'_> { fn draw_outset_box_shadow(&self, scene: &mut Scene) { let box_shadow = &self.style.get_effects().box_shadow.0; + + // TODO: Only apply clip if element has transparency + let has_outset_shadow = box_shadow.iter().any(|s| !s.inset); + if has_outset_shadow { + CLIPS_WANTED.fetch_add(1, atomic::Ordering::SeqCst); + let clips_available = CLIPS_USED.load(atomic::Ordering::SeqCst) <= CLIP_LIMIT; + if clips_available { + scene.push_layer(Mix::Clip, 1.0, self.transform, &self.frame.shadow_clip()); + CLIPS_USED.fetch_add(1, atomic::Ordering::SeqCst); + let depth = CLIP_DEPTH.fetch_add(1, atomic::Ordering::SeqCst) + 1; + CLIP_DEPTH_USED.fetch_max(depth, atomic::Ordering::SeqCst); + } + } + for shadow in box_shadow.iter().filter(|s| !s.inset) { let shadow_color = shadow.base.color.as_vello(); if shadow_color != Color::TRANSPARENT { @@ -964,6 +978,11 @@ impl ElementCx<'_> { ); } } + + if has_outset_shadow { + scene.pop_layer(); + CLIP_DEPTH.fetch_sub(1, atomic::Ordering::SeqCst); + } } fn draw_inset_box_shadow(&self, scene: &mut Scene) { From f6aab19b260a6ca7a5deb0688528d72b4d1ee89f Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 6 Sep 2024 01:02:23 +0100 Subject: [PATCH 03/13] Fix clippy --- packages/blitz/src/renderer/render.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index 7306623d..c1a08c5a 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -443,7 +443,7 @@ impl<'dom> VelloSceneGenerator<'dom> { } } - fn element_cx<'w>(&'w self, element: &'w Node, location: Point) -> ElementCx { + fn element_cx<'w>(&'w self, element: &'w Node, location: Point) -> ElementCx<'w> { let style = element .stylo_element_data .borrow() From f3045d15098e003f102f37791663aab6b10871a0 Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Fri, 6 Sep 2024 20:56:26 +1000 Subject: [PATCH 04/13] Checkbox inputs (#125) * Begin implementing input type checkbox * Use checked attribute * Handle label clicking to associated "for" checkbox * Clean up click and input event processing * Support labels in html documents, and propagating to them from dioxus documents * Add some comment about label_bound_input_elements returning vec * Use text color to render checkboxes, use bezpath to render tick * Send filled in FormData event * Round corners of checkboxes * Fix clippy warnings * Add missing lifetime that was elided to clear clippy nightly --- examples/form.rs | 80 ++++++++++ packages/blitz/src/renderer/render.rs | 59 ++++++++ packages/dioxus-blitz/src/accessibility.rs | 1 + .../src/documents/dioxus_document.rs | 137 ++++++++++++++++-- .../src/documents/event_handler.rs | 34 ++++- packages/dom/src/default.css | 5 + packages/dom/src/document.rs | 105 ++++++++++++-- packages/dom/src/layout/mod.rs | 34 ++++- 8 files changed, 426 insertions(+), 29 deletions(-) create mode 100644 examples/form.rs diff --git a/examples/form.rs b/examples/form.rs new file mode 100644 index 00000000..b7a6c93b --- /dev/null +++ b/examples/form.rs @@ -0,0 +1,80 @@ +//! Drive the renderer from Dioxus + +use dioxus::prelude::*; + +fn main() { + dioxus_blitz::launch(app); +} + +fn app() -> Element { + let mut checkbox_checked = use_signal(|| false); + + rsx! { + div { + class: "container", + style { {CSS} } + form { + div { + input { + type: "checkbox", + id: "check1", + name: "check1", + value: "check1", + checked: "{checkbox_checked}", + oninput: move |ev| { + dbg!(ev); + checkbox_checked.set(!checkbox_checked()); + }, + } + label { + r#for: "check1", + "Checkbox 1 (controlled)" + } + } + div { + label { + input { + type: "checkbox", + name: "check2", + value: "check2", + } + "Checkbox 2 (uncontrolled)" + } + } + } + div { "Checkbox 1 checked: {checkbox_checked}" } + } + } +} + +const CSS: &str = r#" + +.container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; + width: 100vw; +} + + +form { + margin: 12px 0; + display: block; +} + +form > div { + margin: 8px 0; +} + +label { + display: inline-block; +} + +input { + /* Should be accent-color */ + color: #0000cc; +} + +"#; diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index c1a08c5a..44f134f1 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -40,6 +40,7 @@ use style::{ use image::{imageops::FilterType, DynamicImage}; use parley::layout::PositionedLayoutItem; use taffy::prelude::Layout; +use vello::kurbo::{BezPath, Cap, Join}; use vello::{ kurbo::{Affine, Point, Rect, Shape, Stroke, Vec2}, peniko::{self, Brush, Color, Fill, Mix}, @@ -364,6 +365,7 @@ impl<'dom> VelloSceneGenerator<'dom> { cx.stroke_devtools(scene); cx.draw_image(scene); cx.draw_svg(scene); + cx.draw_input(scene); // Render the text in text inputs if let Some(input_data) = cx.text_input { @@ -1193,4 +1195,61 @@ impl ElementCx<'_> { ) { unimplemented!() } + + fn draw_input(&self, scene: &mut Scene) { + if self.element.local_name() == "input" + && matches!(self.element.attr(local_name!("type")), Some("checkbox")) + { + let checked: bool = self + .element + .attr(local_name!("checked")) + .and_then(|c| c.parse().ok()) + .unwrap_or_default(); + + // TODO this should be coming from css accent-color, but I couldn't find how to retrieve it + let accent_color = self.style.get_inherited_text().color.as_vello(); + + let scale = self + .frame + .outer_rect + .width() + .min(self.frame.outer_rect.height()) + / 16.0; + + let frame = self.frame.outer_rect.to_rounded_rect(scale * 2.0); + + if checked { + scene.fill(Fill::NonZero, self.transform, accent_color, None, &frame); + + //Tick code derived from masonry + let mut path = BezPath::new(); + path.move_to((2.0, 9.0)); + path.line_to((6.0, 13.0)); + path.line_to((14.0, 2.0)); + + path.apply_affine(Affine::scale(scale)); + + let style = Stroke { + width: 2.0 * scale, + join: Join::Round, + miter_limit: 10.0, + start_cap: Cap::Round, + end_cap: Cap::Round, + dash_pattern: Default::default(), + dash_offset: 0.0, + }; + + scene.stroke(&style, self.transform, Color::WHITE, None, &path); + } else { + scene.fill(Fill::NonZero, self.transform, Color::WHITE, None, &frame); + scene.stroke( + &Stroke::default(), + self.transform, + accent_color, + None, + &frame, + ); + } + } + } } diff --git a/packages/dioxus-blitz/src/accessibility.rs b/packages/dioxus-blitz/src/accessibility.rs index 0d08825f..8787a2b5 100644 --- a/packages/dioxus-blitz/src/accessibility.rs +++ b/packages/dioxus-blitz/src/accessibility.rs @@ -71,6 +71,7 @@ impl AccessibilityState { let ty = element_data.attr(local_name!("type")).unwrap_or("text"); match ty { "number" => Role::NumberInput, + "checkbox" => Role::CheckBox, _ => Role::TextInput, } } diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index 94433fe3..f5239163 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -1,10 +1,10 @@ //! Integration between Dioxus and Blitz -use std::rc::Rc; +use std::{collections::HashMap, rc::Rc}; use blitz_dom::{ - events::EventData, namespace_url, node::Attribute, ns, Atom, Document, DocumentLike, - ElementNodeData, NodeData, QualName, TextNodeData, Viewport, DEFAULT_CSS, + events::EventData, local_name, namespace_url, node::Attribute, ns, Atom, Document, + DocumentLike, ElementNodeData, Node, NodeData, QualName, TextNodeData, Viewport, DEFAULT_CSS, }; use dioxus::{ @@ -12,6 +12,7 @@ use dioxus::{ AttributeValue, ElementId, Template, TemplateAttribute, TemplateNode, VirtualDom, WriteMutations, }, + html::FormValue, prelude::{set_event_converter, PlatformEventData}, }; use futures_util::{pin_mut, FutureExt}; @@ -21,7 +22,7 @@ use style::{ properties::{style_structs::Font, ComputedValues}, }; -use super::event_handler::{NativeClickData, NativeConverter}; +use super::event_handler::{NativeClickData, NativeConverter, NativeFormData}; type NodeId = usize; @@ -111,17 +112,41 @@ impl DocumentLike for DioxusDocument { continue; }; - for attr in element.attrs() { - if attr.name.local.as_ref() == "data-dioxus-id" { - if let Ok(value) = attr.value.parse::() { - let id = ElementId(value); - // let data = dioxus::html::EventData::Mouse() + if let Some(id) = DioxusDocument::dioxus_id(element) { + // let data = dioxus::html::EventData::Mouse() + self.vdom + .handle_event("click", self.click_event_data(), id, true); + //TODO Check for other inputs which trigger input event on click here, eg radio + let triggers_input_event = element.name.local == local_name!("input") + && element.attr(local_name!("type")) == Some("checkbox"); + if triggers_input_event { + let form_data = self.input_event_form_data(&chain, element); + self.vdom.handle_event("input", form_data, id, true); + } + return true; + } - let data = - Rc::new(PlatformEventData::new(Box::new(NativeClickData {}))); - self.vdom.handle_event(event.name(), data, id, true); - return true; + //Clicking labels triggers click, and possibly input event, of bound input + if *element.name.local == *"label" { + let bound_input_elements = self.inner.label_bound_input_elements(*node); + //Filter down bound elements to those which have dioxus id + if let Some((element_data, dioxus_id)) = + bound_input_elements.into_iter().find_map(|n| { + let target_element_data = n.element_data()?; + let dioxus_id = DioxusDocument::dioxus_id(target_element_data)?; + Some((target_element_data, dioxus_id)) + }) + { + self.vdom + .handle_event("click", self.click_event_data(), dioxus_id, true); + //TODO Check for other inputs which trigger input event on click here, eg radio + let triggers_input_event = + element_data.attr(local_name!("type")) == Some("checkbox"); + if triggers_input_event { + let form_data = self.input_event_form_data(&chain, element_data); + self.vdom.handle_event("input", form_data, dioxus_id, true); } + return true; } } } @@ -134,6 +159,80 @@ impl DocumentLike for DioxusDocument { } impl DioxusDocument { + pub fn click_event_data(&self) -> Rc { + Rc::new(PlatformEventData::new(Box::new(NativeClickData {}))) + } + + /// Generate the FormData from an input event + /// Currently only cares about input checkboxes + pub fn input_event_form_data( + &self, + parent_chain: &[usize], + element_node_data: &ElementNodeData, + ) -> Rc { + let parent_form = parent_chain.iter().find_map(|id| { + let node = self.inner.get_node(*id)?; + let element_data = node.element_data()?; + if element_data.name.local == local_name!("form") { + Some(node) + } else { + None + } + }); + let values = if let Some(parent_form) = parent_form { + let mut values = HashMap::::new(); + for form_input in self.input_descendents(parent_form).into_iter() { + // Match html behaviour here. To be included in values: + // - input must have a name + // - if its an input, we only include it if checked + // - 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") + { + let value = form_input + .attr(local_name!("value")) + .unwrap_or("on") + .to_string(); + values.insert(name.to_string(), FormValue(vec![value])); + } + } + } + values + } else { + Default::default() + }; + let form_data = NativeFormData { + value: element_node_data + .attr(local_name!("value")) + .unwrap_or_default() + .to_string(), + values, + }; + Rc::new(PlatformEventData::new(Box::new(form_data))) + } + + /// Collect all the inputs which are descendents of a given node + fn input_descendents(&self, node: &Node) -> Vec<&Node> { + node.children + .iter() + .flat_map(|id| { + let mut res = Vec::<&Node>::new(); + let Some(n) = self.inner.get_node(*id) else { + return res; + }; + let Some(element_data) = n.element_data() else { + return res; + }; + if element_data.name.local == local_name!("input") { + res.push(n); + } + res.extend(self.input_descendents(n).iter()); + res + }) + .collect() + } + pub fn new(vdom: VirtualDom) -> Self { let viewport = Viewport::new(0, 0, 1.0); let mut doc = Document::new(viewport); @@ -190,6 +289,18 @@ impl DioxusDocument { // dbg!(writer.state); } + fn dioxus_id(element_node_data: &ElementNodeData) -> Option { + Some(ElementId( + element_node_data + .attrs + .iter() + .find(|attr| *attr.name.local == *"data-dioxus-id")? + .value + .parse::() + .ok()?, + )) + } + // pub fn apply_mutations(&mut self) { // // Apply the mutations to the actual dom // let mut writer = MutationWriter { diff --git a/packages/dioxus-blitz/src/documents/event_handler.rs b/packages/dioxus-blitz/src/documents/event_handler.rs index 28d7aafd..2b1621f8 100644 --- a/packages/dioxus-blitz/src/documents/event_handler.rs +++ b/packages/dioxus-blitz/src/documents/event_handler.rs @@ -1,4 +1,9 @@ -use dioxus::prelude::{HtmlEventConverter, PlatformEventData}; +use std::collections::HashMap; + +use dioxus::{ + html::{FormValue, HasFileData, HasFormData}, + prelude::{HtmlEventConverter, PlatformEventData}, +}; #[derive(Clone)] pub struct NativeClickData {} @@ -68,8 +73,9 @@ impl HtmlEventConverter for NativeConverter { todo!() } - fn convert_form_data(&self, _event: &PlatformEventData) -> dioxus::prelude::FormData { - todo!() + fn convert_form_data(&self, event: &PlatformEventData) -> dioxus::prelude::FormData { + let o = event.downcast::().unwrap().clone(); + dioxus::prelude::FormData::from(o) } fn convert_image_data(&self, _event: &PlatformEventData) -> dioxus::prelude::ImageData { @@ -124,3 +130,25 @@ impl HtmlEventConverter for NativeConverter { todo!() } } + +#[derive(Clone, Debug)] +pub struct NativeFormData { + pub value: String, + pub values: HashMap, +} + +impl HasFormData for NativeFormData { + fn as_any(&self) -> &dyn std::any::Any { + self as &dyn std::any::Any + } + + fn value(&self) -> String { + self.value.clone() + } + + fn values(&self) -> HashMap { + self.values.clone() + } +} + +impl HasFileData for NativeFormData {} diff --git a/packages/dom/src/default.css b/packages/dom/src/default.css index c7c050dd..0544e8f5 100644 --- a/packages/dom/src/default.css +++ b/packages/dom/src/default.css @@ -42,6 +42,11 @@ input { display: inline-block; } +input[type="checkbox"] { + width: 14px; + height: 14px; + margin: 3px 3px 3px 4px; +} /* To ensure http://www.w3.org/TR/REC-html40/struct/dirlang.html#style-bidi: * * "When a block element that does not have a dir attribute is transformed to diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 8e2d3aaa..66f8a2ea 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -1,7 +1,8 @@ use crate::events::{EventData, HitResult, RendererEvent}; -use crate::node::TextBrush; -use crate::{Node, NodeData, TextNodeData, Viewport}; +use crate::node::{Attribute, NodeSpecificData, TextBrush}; +use crate::{ElementNodeData, Node, NodeData, TextNodeData, Viewport}; use app_units::Au; +use html5ever::{local_name, namespace_url, ns, QualName}; use peniko::kurbo; // use quadtree_rs::Quadtree; use parley::editor::{PointerButton, TextEvent}; @@ -124,21 +125,42 @@ impl DocumentLike for Document { assert!(hit.node_id == event.target); let node = &mut self.nodes[hit.node_id]; - let text_input_data = node - .raw_dom_data - .downcast_element_mut() - .and_then(|el| el.text_input_data_mut()); - if text_input_data.is_some() { + let Some(el) = node.raw_dom_data.downcast_element_mut() else { + return true; + }; + + if let NodeSpecificData::TextInput(ref mut text_input_data) = + el.node_specific_data + { let x = hit.x as f64 * self.viewport.scale_f64(); let y = hit.y as f64 * self.viewport.scale_f64(); - text_input_data.unwrap().editor.pointer_down( + text_input_data.editor.pointer_down( kurbo::Point { x, y }, mods, PointerButton::Primary, ); - println!("Clicked {}", hit.node_id); self.set_focus_to(hit.node_id); + } else if el.name.local == local_name!("input") + && matches!(el.attr(local_name!("type")), Some("checkbox")) + { + Document::toggle_checkbox(el); + self.set_focus_to(hit.node_id); + } + // Clicking labels triggers click, and possibly input event, of associated input + else if el.name.local == local_name!("label") { + let node_id = node.id; + if let Some(target_node_id) = self + .label_bound_input_elements(node_id) + .first() + .map(|n| n.id) + { + let target_node = self.get_node_mut(target_node_id).unwrap(); + if let Some(target_element) = target_node.element_data_mut() { + Document::toggle_checkbox(target_element); + } + self.set_focus_to(node_id); + } } } } @@ -243,6 +265,71 @@ impl Document { .or(self.try_root_element().map(|el| el.id)) } + /// Find the label's bound input elements: + /// the element id referenced by the "for" attribute of a given label element + /// or the first input element which is nested in the label + /// Note that although there should only be one bound element, + /// we return all possibilities instead of just the first + /// in order to allow the caller to decide which one is correct + pub fn label_bound_input_elements(&self, label_node_id: usize) -> Vec<&Node> { + let label_node = self.get_node(label_node_id).unwrap(); + let label_element = label_node.element_data().unwrap(); + if let Some(target_element_dom_id) = label_element.attr(local_name!("for")) { + self.tree() + .into_iter() + .filter_map(|(_id, node)| { + let element_data = node.element_data()?; + if element_data.name.local != local_name!("input") { + return None; + } + let id = element_data.id.as_ref()?; + if *id == *target_element_dom_id { + Some(node) + } else { + None + } + }) + .collect() + } else { + label_node + .children + .iter() + .filter_map(|child_id| { + let node = self.get_node(*child_id)?; + let element_data = node.element_data()?; + if element_data.name.local == local_name!("input") { + Some(node) + } else { + None + } + }) + .collect() + } + } + + pub fn toggle_checkbox(el: &mut ElementNodeData) { + let checked_attr_opt = el + .attrs + .iter_mut() + .find(|attr| attr.name.local == local_name!("checked")); + + let checked_attr = if let Some(attr) = checked_attr_opt { + attr + } else { + let attr = Attribute { + name: QualName::new(None, ns!(html), local_name!("checked")), + value: String::from("false"), + }; + el.attrs.push(attr); + el.attrs + .iter_mut() + .find(|attr| attr.name.local == local_name!("checked")) + .unwrap() + }; + let checked = checked_attr.value.parse().unwrap_or(false); + checked_attr.value = (!checked).to_string(); + } + pub fn root_node(&self) -> &Node { &self.nodes[0] } diff --git a/packages/dom/src/layout/mod.rs b/packages/dom/src/layout/mod.rs index 501a47a3..73f05236 100644 --- a/packages/dom/src/layout/mod.rs +++ b/packages/dom/src/layout/mod.rs @@ -144,10 +144,36 @@ impl LayoutPartialTree for Document { // todo: need to handle shadow roots by actually descending into them if *element_data.name.local == *"input" { - // if the input type is hidden, hide it - if let Some("hidden") = element_data.attr(local_name!("type")) { - node.style.display = Display::None; - return taffy::LayoutOutput::HIDDEN; + match element_data.attr(local_name!("type")) { + // if the input type is hidden, hide it + Some("hidden") => { + node.style.display = Display::None; + return taffy::LayoutOutput::HIDDEN; + } + Some("checkbox") => { + return compute_leaf_layout( + inputs, + &node.style, + |_known_size, _available_space| { + let width = node + .style + .size + .width + .resolve_or_zero(inputs.parent_size.width); + let height = node + .style + .size + .height + .resolve_or_zero(inputs.parent_size.height); + let min_size = width.min(height); + taffy::Size { + width: min_size, + height: min_size, + } + }, + ); + } + _ => {} } } From 8aedc3c27e1eff13294be577b7835999125d938e Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 6 Sep 2024 11:56:53 +0100 Subject: [PATCH 05/13] Enable checkbox lists in markdown example --- examples/markdown.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/markdown.rs b/examples/markdown.rs index fa7b3b46..ebe1f933 100644 --- a/examples/markdown.rs +++ b/examples/markdown.rs @@ -39,7 +39,7 @@ fn main() { .tagfilter(false) .table(true) .autolink(true) - .tasklist(false) + .tasklist(true) .superscript(false) .header_ids(None) .footnotes(false) @@ -69,6 +69,8 @@ fn main() { body_html ); + println!("{html}"); + dioxus_blitz::launch_static_html_cfg( &html, Config { From d924e925f6eeb703b4dbfa96785fa20ba22d895d Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 6 Sep 2024 12:27:13 +0100 Subject: [PATCH 06/13] Fix attribute selector matching --- packages/dom/src/stylo.rs | 43 +++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/dom/src/stylo.rs b/packages/dom/src/stylo.rs index 87c0bfd4..bdb61155 100644 --- a/packages/dom/src/stylo.rs +++ b/packages/dom/src/stylo.rs @@ -9,6 +9,7 @@ use crate::node::NodeData; use atomic_refcell::{AtomicRef, AtomicRefMut}; use html5ever::{local_name, LocalName, Namespace}; use selectors::{ + attr::{AttrSelectorOperation, AttrSelectorOperator}, matching::{ElementSelectorFlags, MatchingContext, VisitedHandlingMode}, sink::Push, Element, OpaqueElement, @@ -354,18 +355,42 @@ impl<'a> selectors::Element for BlitzNode<'a> { &::NamespaceUrl, >, local_name: &::LocalName, - _operation: &selectors::attr::AttrSelectorOperation< - &::AttrValue, - >, + operation: &AttrSelectorOperation<&::AttrValue>, ) -> bool { // println!("attr matches {}", self.id); - let mut has_attr = false; - self.each_attr_name(|f| { - if f.as_ref() == local_name.as_ref() { - has_attr = true; + + let Some(attr_value) = self.raw_dom_data.attr(local_name.0.clone()) else { + return false; + }; + + match operation { + AttrSelectorOperation::Exists => true, + AttrSelectorOperation::WithValue { + operator, + case_sensitivity: _, + value, + } => { + let value = value.as_ref(); + + // TODO: case sensitivity + return match operator { + AttrSelectorOperator::Equal => attr_value == value, + AttrSelectorOperator::Includes => attr_value + .split_ascii_whitespace() + .any(|word| word == value), + AttrSelectorOperator::DashMatch => { + // Represents elements with an attribute name of attr whose value can be exactly value + // or can begin with value immediately followed by a hyphen, - (U+002D) + attr_value.starts_with(value) + && (attr_value.len() == value.len() + || attr_value.chars().nth(value.len()) == Some('-')) + } + AttrSelectorOperator::Prefix => attr_value.starts_with(value), + AttrSelectorOperator::Substring => attr_value.contains(value), + AttrSelectorOperator::Suffix => attr_value.ends_with(value), + }; } - }); - has_attr + } } fn match_non_ts_pseudo_class( From 6b3af4d3aa1dd1a1adec4d9bacfea1e6400eed67 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 6 Sep 2024 12:32:44 +0100 Subject: [PATCH 07/13] Simplify function signature of attr_matches function --- packages/dom/src/stylo.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/dom/src/stylo.rs b/packages/dom/src/stylo.rs index bdb61155..7ea9e0eb 100644 --- a/packages/dom/src/stylo.rs +++ b/packages/dom/src/stylo.rs @@ -7,9 +7,11 @@ use crate::node::Node; use crate::node::NodeData; use atomic_refcell::{AtomicRef, AtomicRefMut}; +use html5ever::LocalNameStaticSet; +use html5ever::NamespaceStaticSet; use html5ever::{local_name, LocalName, Namespace}; use selectors::{ - attr::{AttrSelectorOperation, AttrSelectorOperator}, + attr::{AttrSelectorOperation, AttrSelectorOperator, NamespaceConstraint}, matching::{ElementSelectorFlags, MatchingContext, VisitedHandlingMode}, sink::Push, Element, OpaqueElement, @@ -22,6 +24,7 @@ use style::selector_parser::PseudoElement; use style::stylesheets::layer_rule::LayerOrder; use style::values::computed::Percentage; use style::values::specified::box_::DisplayOutside; +use style::values::AtomString; use style::CaseSensitivityExt; use style::{ animation::DocumentAnimationSet, @@ -351,14 +354,10 @@ impl<'a> selectors::Element for BlitzNode<'a> { fn attr_matches( &self, - _ns: &selectors::attr::NamespaceConstraint< - &::NamespaceUrl, - >, - local_name: &::LocalName, - operation: &AttrSelectorOperation<&::AttrValue>, + _ns: &NamespaceConstraint<&GenericAtomIdent>, + local_name: &GenericAtomIdent, + operation: &AttrSelectorOperation<&AtomString>, ) -> bool { - // println!("attr matches {}", self.id); - let Some(attr_value) = self.raw_dom_data.attr(local_name.0.clone()) else { return false; }; From 24a61d6fe6e51440b01d7c1b51338b73a0864732 Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Fri, 6 Sep 2024 22:57:13 +1000 Subject: [PATCH 08/13] Non-root scrolling (#121) * Implement scrolling in nodes, remove window-level scrolling * Implement scrolling bubbling up to root element * Tweak scroll demo for visibility * Trim example * Fix trailing whitespace in window.rs * Partially bubble parent scrolling, delegate root scrolling to viewport * Move scroll out of viewport into viewport_scroll property, so it isn't reset on resize of viewport * Update comments * Update formatting * format files * Fix hover/click of scrolled elements/viewport --- examples/scroll.rs | 55 ++++++++++++++++ packages/blitz/src/renderer/render.rs | 31 ++++++--- packages/dioxus-blitz/src/window.rs | 22 ++++--- packages/dom/src/document.rs | 91 +++++++++++++++++++-------- packages/dom/src/node.rs | 13 +++- 5 files changed, 166 insertions(+), 46 deletions(-) create mode 100644 examples/scroll.rs diff --git a/examples/scroll.rs b/examples/scroll.rs new file mode 100644 index 00000000..7e8cd2e1 --- /dev/null +++ b/examples/scroll.rs @@ -0,0 +1,55 @@ +// Example: scrolling. +// Creates a scrollable element to demo being able to scroll elements when their content size +// exceeds their layout size +use dioxus::prelude::*; + +fn root() -> Element { + let css = r#" + .scrollable { + background-color: green; + overflow: scroll; + height: 200px; + } + + .gap { + height: 300px; + margin: 8px; + background: #11ff11; + display: flex; + align-items: center; + color: white; + } + + .gap:hover { + background: red; + } + + .not-scrollable { + background-color: yellow; + padding-top: 16px; + padding-bottom: 16px; + "#; + + rsx! { + style { {css} } + div { class: "not-scrollable", "Not scrollable" } + div { class: "scrollable", + div { + "Scroll me" + } + div { + class: "gap", + onclick: |_| println!("Gap clicked!"), + "gap" + } + div { + "Hello" + } + } + div { class: "not-scrollable", "Not scrollable" } + } +} + +fn main() { + dioxus_blitz::launch(root); +} diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index 44f134f1..3c4f629b 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -72,7 +72,6 @@ pub fn generate_vello_scene( dom, scale, devtools: devtool_config, - scroll_offset: dom.scroll_offset, }; generator.generate_vello_scene(scene); @@ -91,7 +90,6 @@ pub struct VelloSceneGenerator<'dom> { dom: &'dom Document, scale: f64, devtools: Devtools, - scroll_offset: f64, } impl<'dom> VelloSceneGenerator<'dom> { @@ -114,12 +112,13 @@ impl<'dom> VelloSceneGenerator<'dom> { pub fn generate_vello_scene(&self, scene: &mut Scene) { // Simply render the document (the root element (note that this is not the same as the root node))) scene.reset(); + let viewport_scroll = self.dom.as_ref().viewport_scroll(); self.render_element( scene, self.dom.as_ref().root_element().id, Point { - x: 0.0, - y: self.scroll_offset, + x: -viewport_scroll.x, + y: -viewport_scroll.y, }, ); @@ -165,8 +164,6 @@ impl<'dom> VelloSceneGenerator<'dom> { abs_y += y; } - abs_y += self.scroll_offset as f32; - // Hack: scale factor let abs_x = f64::from(abs_x) * scale; let abs_y = f64::from(abs_y) * scale; @@ -315,8 +312,10 @@ impl<'dom> VelloSceneGenerator<'dom> { // TODO: account for overflow_x vs overflow_y let styles = &element.primary_styles().unwrap(); - let overflow = styles.get_box().overflow_x; - let should_clip = !matches!(overflow, Overflow::Visible); + let overflow_x = styles.get_box().overflow_x; + let overflow_y = styles.get_box().overflow_y; + let should_clip = + !matches!(overflow_x, Overflow::Visible) || !matches!(overflow_y, Overflow::Visible); let clips_available = CLIPS_USED.load(atomic::Ordering::SeqCst) <= CLIP_LIMIT; // Apply padding/border offset to inline root @@ -355,7 +354,7 @@ impl<'dom> VelloSceneGenerator<'dom> { CLIP_DEPTH_USED.fetch_max(depth, atomic::Ordering::SeqCst); } - let cx = self.element_cx(element, location); + let mut cx = self.element_cx(element, location); cx.stroke_effects(scene); cx.stroke_outline(scene); cx.draw_outset_box_shadow(scene); @@ -363,6 +362,20 @@ impl<'dom> VelloSceneGenerator<'dom> { cx.draw_inset_box_shadow(scene); cx.stroke_border(scene); cx.stroke_devtools(scene); + + // Now that background has been drawn, offset pos and cx in order to draw our contents scrolled + let pos = Point { + x: pos.x - element.scroll_offset.x, + y: pos.y - element.scroll_offset.y, + }; + cx.pos = Point { + x: cx.pos.x - element.scroll_offset.x, + y: cx.pos.y - element.scroll_offset.y, + }; + cx.transform = cx.transform.then_translate(Vec2 { + x: cx.transform.translation().x - element.scroll_offset.x, + y: cx.transform.translation().y - element.scroll_offset.y, + }); cx.draw_image(scene); cx.draw_svg(scene); cx.draw_input(scene); diff --git a/packages/dioxus-blitz/src/window.rs b/packages/dioxus-blitz/src/window.rs index 74c883f4..b9ac65e9 100644 --- a/packages/dioxus-blitz/src/window.rs +++ b/packages/dioxus-blitz/src/window.rs @@ -190,8 +190,9 @@ impl View { } pub fn mouse_move(&mut self, x: f32, y: f32) -> bool { - let dom_x = x / self.viewport.zoom(); - let dom_y = (y - self.dom.as_ref().scroll_offset as f32) / self.viewport.zoom(); + let viewport_scroll = self.dom.as_ref().viewport_scroll(); + let dom_x = x + viewport_scroll.x as f32 / self.viewport.zoom(); + let dom_y = y + viewport_scroll.y as f32 / self.viewport.zoom(); // println!("Mouse move: ({}, {})", x, y); // println!("Unscaled: ({}, {})",); @@ -391,14 +392,16 @@ impl View { } } WindowEvent::MouseWheel { delta, .. } => { - match delta { - winit::event::MouseScrollDelta::LineDelta(_, y) => { - self.dom.as_mut().scroll_by(y as f64 * 20.0) - } - winit::event::MouseScrollDelta::PixelDelta(offsets) => { - self.dom.as_mut().scroll_by(offsets.y) - } + let (scroll_x, scroll_y)= match delta { + winit::event::MouseScrollDelta::LineDelta(x, y) => (x as f64 * 20.0, y as f64 * 20.0), + winit::event::MouseScrollDelta::PixelDelta(offsets) => (offsets.x, offsets.y) }; + + if let Some(hover_node_id)= self.dom.as_ref().get_hover_node_id() { + self.dom.as_mut().scroll_node_by(hover_node_id, scroll_x, scroll_y); + } else { + self.dom.as_mut().scroll_viewport_by(scroll_x, scroll_y); + } self.request_redraw(); } @@ -409,6 +412,7 @@ impl View { WindowEvent::Focused(_) => {} // Touch and motion events + // Todo implement touch scrolling WindowEvent::Touch(_) => {} WindowEvent::TouchpadPressure { .. } => {} WindowEvent::AxisMotion { .. } => {} diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 66f8a2ea..6bc4c583 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -96,6 +96,9 @@ pub struct Document { // Viewport details such as the dimensions, HiDPI scale, and zoom factor, pub(crate) viewport: Viewport, + // Scroll within our viewport + pub(crate) viewport_scroll: kurbo::Point, + pub(crate) stylesheets: HashMap, /// A Parley font context @@ -108,9 +111,6 @@ pub struct Document { /// The node which is currently focussed (if any) pub(crate) focus_node_id: Option, - // TODO: move to nodes - pub scroll_offset: f64, - pub changed: HashSet, } @@ -221,6 +221,7 @@ impl Document { snapshots, nodes_to_id, viewport, + viewport_scroll: kurbo::Point::ZERO, base_url: None, // quadtree: Quadtree::new(20), stylesheets: HashMap::new(), @@ -229,7 +230,6 @@ impl Document { hover_node_id: None, focus_node_id: None, - scroll_offset: 0.0, changed: HashSet::new(), }; @@ -746,7 +746,6 @@ impl Document { self.stylist.set_device(device, &guards) }; self.stylist.force_stylesheet_origins_dirty(origins); - self.clamp_scroll(); } pub fn stylist_device(&mut self) -> &Device { @@ -784,12 +783,12 @@ impl Document { height: AvailableSpace::Definite(size.height.to_f32_px()), }; - let root_node_id = taffy::NodeId::from(self.root_element().id); + let root_element_id = taffy::NodeId::from(self.root_element().id); // println!("\n\nRESOLVE LAYOUT\n===========\n"); - taffy::compute_root_layout(self, root_node_id, available_space); - taffy::round_layout(self, root_node_id); + taffy::compute_root_layout(self, root_element_id, available_space); + taffy::round_layout(self, root_element_id); // println!("\n\n"); // taffy::print_tree(self, root_node_id) @@ -826,29 +825,71 @@ impl Document { Some(cursor) } - pub fn scroll_by(&mut self, px: f64) { - // Invert scrolling on macos - #[cfg(target_os = "macos")] - { - self.scroll_offset += px; + /// Scroll a node by given x and y + /// Will bubble scrolling up to parent node once it can no longer scroll further + /// If we're already at the root node, bubbles scrolling up to the viewport + pub fn scroll_node_by(&mut self, node_id: usize, x: f64, y: f64) { + let Some(node) = self.nodes.get_mut(node_id) else { + return; + }; + + let new_x = node.scroll_offset.x - x; + let new_y = node.scroll_offset.y - y; + + let mut bubble_x = 0.0; + let mut bubble_y = 0.0; + + let scroll_width = node.final_layout.scroll_width() as f64; + let scroll_height = node.final_layout.scroll_height() as f64; + + // If we're past our scroll bounds, transfer remainder of scrolling to parent/viewport + if new_x < 0.0 { + bubble_x = -new_x; + node.scroll_offset.x = 0.0; + } else if new_x > scroll_width { + bubble_x = scroll_width - new_x; + node.scroll_offset.x = scroll_width; + } else { + node.scroll_offset.x = new_x; } - #[cfg(not(target_os = "macos"))] - { - self.scroll_offset -= px; + + if new_y < 0.0 { + bubble_y = -new_y; + node.scroll_offset.y = 0.0; + } else if new_y > scroll_height { + bubble_y = scroll_height - new_y; + node.scroll_offset.y = scroll_height; + } else { + node.scroll_offset.y = new_y; } - self.clamp_scroll(); + if bubble_x != 0.0 || bubble_y != 0.0 { + if let Some(parent) = node.parent { + self.scroll_node_by(parent, bubble_x, bubble_y); + } else { + self.scroll_viewport_by(bubble_x, bubble_y); + } + } } - /// Clamp scroll offset - fn clamp_scroll(&mut self) { - let content_height = self.root_element().final_layout.size.height as f64; - let viewport_height = self.stylist_device().au_viewport_size().height.to_f64_px(); + /// Scroll the viewport by the given values + pub fn scroll_viewport_by(&mut self, x: f64, y: f64) { + let content_size = self.root_element().final_layout.size; + let new_scroll = (self.viewport_scroll.x - x, self.viewport_scroll.y - y); + let window_width = self.viewport.window_size.0 as f64 / self.viewport.scale() as f64; + let window_height = self.viewport.window_size.1 as f64 / self.viewport.scale() as f64; + self.viewport_scroll.x = f64::max( + 0.0, + f64::min(new_scroll.0, content_size.width as f64 - window_width), + ); + self.viewport_scroll.y = f64::max( + 0.0, + f64::min(new_scroll.1, content_size.height as f64 - window_height), + ) + } - self.scroll_offset = self - .scroll_offset - .max(-(content_height - viewport_height)) - .min(0.0); + pub fn viewport_scroll(&self) -> kurbo::Point { + self.viewport_scroll } pub fn visit(&self, mut visit: F) diff --git a/packages/dom/src/node.rs b/packages/dom/src/node.rs index 36c657b4..870ba44b 100644 --- a/packages/dom/src/node.rs +++ b/packages/dom/src/node.rs @@ -1,6 +1,7 @@ use atomic_refcell::{AtomicRef, AtomicRefCell}; use html5ever::{local_name, LocalName, QualName}; use image::DynamicImage; +use peniko::kurbo; use selectors::matching::QuirksMode; use slab::Slab; use std::cell::RefCell; @@ -76,6 +77,7 @@ pub struct Node { pub unrounded_layout: Layout, pub final_layout: Layout, pub listeners: Vec, + pub scroll_offset: kurbo::Point, // Flags pub is_inline_root: bool, @@ -108,6 +110,7 @@ impl Node { unrounded_layout: Layout::new(), final_layout: Layout::new(), listeners: Default::default(), + scroll_offset: kurbo::Point::ZERO, is_inline_root: false, is_table_root: false, } @@ -779,11 +782,15 @@ impl Node { /// TODO: z-index /// (If multiple children are positioned at the position then a random one will be recursed into) pub fn hit(&self, x: f32, y: f32) -> Option { - let x = x - self.final_layout.location.x; - let y = y - self.final_layout.location.y; + let x = x - self.final_layout.location.x + self.scroll_offset.x as f32; + let y = y - self.final_layout.location.y + self.scroll_offset.y as f32; let size = self.final_layout.size; - if x < 0.0 || x > size.width || y < 0.0 || y > size.height { + if x < 0.0 + || x > size.width + self.scroll_offset.x as f32 + || y < 0.0 + || y > size.height + self.scroll_offset.y as f32 + { return None; } From 9e7b74313f12ab89c787d198cd568a1f5f70bfbb Mon Sep 17 00:00:00 2001 From: Matt Hunzinger Date: Fri, 6 Sep 2024 13:31:16 -0400 Subject: [PATCH 09/13] Skip setting default window size on mobile (#129) * Skip setting default window size on mobile * Fix default height --- packages/dioxus-blitz/src/lib.rs | 26 ++++++++++++++++++++++---- packages/dioxus-blitz/src/window.rs | 5 ++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/dioxus-blitz/src/lib.rs b/packages/dioxus-blitz/src/lib.rs index 19a00312..f53914fd 100644 --- a/packages/dioxus-blitz/src/lib.rs +++ b/packages/dioxus-blitz/src/lib.rs @@ -24,7 +24,11 @@ mod accessibility; use blitz_dom::{DocumentLike, HtmlDocument}; use dioxus::prelude::{ComponentFunction, Element, VirtualDom}; use url::Url; -use winit::event_loop::{ControlFlow, EventLoop}; +use winit::{ + dpi::LogicalSize, + event_loop::{ControlFlow, EventLoop}, + window::Window, +}; use crate::application::Application; use crate::window::View; @@ -62,9 +66,8 @@ 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) + launch_with_document(document) } pub fn launch_url(url: &str) { @@ -97,7 +100,22 @@ 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_document(document) +} + +fn launch_with_document(doc: impl DocumentLike) { + let mut window_attrs = Window::default_attributes(); + if !cfg!(all(target_os = "android", target_os = "ios")) { + window_attrs.inner_size = Some( + LogicalSize { + width: 800., + height: 600., + } + .into(), + ); + } + let window = WindowConfig::new(doc); + launch_with_window(window) } diff --git a/packages/dioxus-blitz/src/window.rs b/packages/dioxus-blitz/src/window.rs index b9ac65e9..05a34695 100644 --- a/packages/dioxus-blitz/src/window.rs +++ b/packages/dioxus-blitz/src/window.rs @@ -11,7 +11,6 @@ use wgpu::rwh::HasWindowHandle; use std::sync::Arc; use std::task::Waker; -use winit::dpi::LogicalSize; use winit::event::{ElementState, MouseButton}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; use winit::window::{WindowAttributes, WindowId}; @@ -26,10 +25,10 @@ pub struct WindowConfig { } impl WindowConfig { - pub fn new(doc: Doc, width: f32, height: f32) -> Self { + pub fn new(doc: Doc) -> Self { WindowConfig { doc, - attributes: Window::default_attributes().with_inner_size(LogicalSize { width, height }), + attributes: Window::default_attributes(), } } From f98e4029ea9aca17f092084b6a49c62c5f077b7a Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Fri, 6 Sep 2024 21:25:58 +0100 Subject: [PATCH 10/13] Fix checkbox positioning (don't apply transform twice when rendering) --- packages/blitz/src/renderer/render.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index 3c4f629b..b3cea7bf 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -373,8 +373,8 @@ impl<'dom> VelloSceneGenerator<'dom> { y: cx.pos.y - element.scroll_offset.y, }; cx.transform = cx.transform.then_translate(Vec2 { - x: cx.transform.translation().x - element.scroll_offset.x, - y: cx.transform.translation().y - element.scroll_offset.y, + x: -element.scroll_offset.x, + y: -element.scroll_offset.y, }); cx.draw_image(scene); cx.draw_svg(scene); From bea7717257700d0e50e9b35588cb2a96ce86981d Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sat, 7 Sep 2024 11:45:54 +0100 Subject: [PATCH 11/13] Determine checkbox checked status based on presence of checked attribute (ignoring value) --- packages/blitz/src/renderer/render.rs | 6 +---- packages/dom/src/document.rs | 32 +++++++++++++-------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index b3cea7bf..9e60e623 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -1213,11 +1213,7 @@ impl ElementCx<'_> { if self.element.local_name() == "input" && matches!(self.element.attr(local_name!("type")), Some("checkbox")) { - let checked: bool = self - .element - .attr(local_name!("checked")) - .and_then(|c| c.parse().ok()) - .unwrap_or_default(); + let checked = self.element.attr(local_name!("checked")).is_some(); // TODO this should be coming from css accent-color, but I couldn't find how to retrieve it let accent_color = self.style.get_inherited_text().color.as_vello(); diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 6bc4c583..9bf00f3a 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -308,26 +308,24 @@ impl Document { } pub fn toggle_checkbox(el: &mut ElementNodeData) { - let checked_attr_opt = el + let is_checked = el .attrs - .iter_mut() - .find(|attr| attr.name.local == local_name!("checked")); + .iter() + .any(|attr| attr.name.local == local_name!("checked")); - let checked_attr = if let Some(attr) = checked_attr_opt { - attr - } else { - let attr = Attribute { - name: QualName::new(None, ns!(html), local_name!("checked")), - value: String::from("false"), - }; - el.attrs.push(attr); + if is_checked { el.attrs - .iter_mut() - .find(|attr| attr.name.local == local_name!("checked")) - .unwrap() - }; - let checked = checked_attr.value.parse().unwrap_or(false); - checked_attr.value = (!checked).to_string(); + .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(), + }) + } } pub fn root_node(&self) -> &Node { From 00427a89fd51024da7df1bb93ce4a928531b7f9a Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sat, 7 Sep 2024 11:46:13 +0100 Subject: [PATCH 12/13] Support disabled attribute on checkboxes --- packages/blitz/src/renderer/render.rs | 18 +++++++++++++++--- packages/dom/src/document.rs | 5 +++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index 9e60e623..6e0c2568 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -1214,15 +1214,27 @@ impl ElementCx<'_> { && matches!(self.element.attr(local_name!("type")), Some("checkbox")) { let checked = self.element.attr(local_name!("checked")).is_some(); + 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 - let accent_color = self.style.get_inherited_text().color.as_vello(); + let accent_color = if disabled { + peniko::Color { + r: 209, + g: 209, + b: 209, + a: 255, + } + } else { + self.style.get_inherited_text().color.as_vello() + }; - let scale = self + let scale = (self .frame .outer_rect .width() .min(self.frame.outer_rect.height()) + - 4.0) + .max(0.0) / 16.0; let frame = self.frame.outer_rect.to_rounded_rect(scale * 2.0); @@ -1236,7 +1248,7 @@ impl ElementCx<'_> { path.line_to((6.0, 13.0)); path.line_to((14.0, 2.0)); - path.apply_affine(Affine::scale(scale)); + path.apply_affine(Affine::translate(Vec2 { x: 2.0, y: 1.0 }).then_scale(scale)); let style = Stroke { width: 2.0 * scale, diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 9bf00f3a..866d4684 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -129,6 +129,11 @@ impl DocumentLike for Document { return true; }; + let disabled = el.attr(local_name!("disabled")).is_some(); + if disabled { + return true; + } + if let NodeSpecificData::TextInput(ref mut text_input_data) = el.node_specific_data { From 8a5bd351338cb56da5c3235d0805a2558b76ceb7 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sat, 7 Sep 2024 13:08:24 +0100 Subject: [PATCH 13/13] Fix form example --- examples/form.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/form.rs b/examples/form.rs index b7a6c93b..f9dbc307 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -20,7 +20,7 @@ fn app() -> Element { id: "check1", name: "check1", value: "check1", - checked: "{checkbox_checked}", + checked: Some("").filter(|_| checkbox_checked()), oninput: move |ev| { dbg!(ev); checkbox_checked.set(!checkbox_checked());