From 58472314f917773912d7643fb3c158fb1053b0e7 Mon Sep 17 00:00:00 2001 From: Sepcnt <30561671+sepcnt@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:27:27 +0800 Subject: [PATCH 1/3] Port full gradient support to blitz (#130) --- examples/gradient.rs | 95 ++++-- packages/blitz/src/renderer/render.rs | 408 +++++++++++++++++++++----- 2 files changed, 408 insertions(+), 95 deletions(-) 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..f07894d7 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,24 +1286,189 @@ 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) { From 0562690341b3123da5c93446b3fa99dc7a43fcb1 Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Tue, 10 Sep 2024 20:54:30 +1000 Subject: [PATCH 2/3] Checkbox internal state (#131) * Set checkbox internal state instead of attribute * Move checked state to NodeSpecificData instead of simply reflecting in checked attribute --- examples/form.rs | 9 ++- packages/blitz/src/renderer/render.rs | 8 ++- .../src/documents/dioxus_document.rs | 62 ++++++++++++++++--- packages/dom/src/document.rs | 26 ++------ packages/dom/src/layout/construct.rs | 17 +++++ packages/dom/src/node.rs | 17 +++++ packages/dom/src/stylo.rs | 6 +- 7 files changed, 110 insertions(+), 35 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/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index f07894d7..b46a8ba5 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -1475,7 +1475,13 @@ impl ElementCx<'_> { 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 f5239163..f933a4db 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::{ @@ -188,7 +191,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")) @@ -202,13 +208,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))) } @@ -561,8 +568,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 @@ -668,6 +678,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 866d4684..1a8e0dac 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -1,8 +1,8 @@ use crate::events::{EventData, HitResult, RendererEvent}; -use crate::node::{Attribute, NodeSpecificData, TextBrush}; +use crate::node::{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 parley::editor::{PointerButton, TextEvent}; @@ -313,24 +313,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 { 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 ac376599d0a1819f07092fc0c4a7da770004175f Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Thu, 12 Sep 2024 12:55:02 +0100 Subject: [PATCH 3/3] Upgrade stylo and html5ever (#132) --- Cargo.toml | 12 +-- packages/dom/src/htmlsink.rs | 137 ++++++++++++++++++----------------- packages/dom/src/stylo.rs | 13 ++++ 3 files changed, 89 insertions(+), 73 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 17fa840d..d2e7847b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,12 +7,12 @@ members = ["packages/blitz", "packages/dom", "packages/dioxus-blitz"] 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 08668d65..819c7c89 100644 --- a/packages/dom/src/htmlsink.rs +++ b/packages/dom/src/htmlsink.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::cell::{Cell, Ref, RefCell, RefMut}; use std::collections::HashSet; use std::sync::Arc; @@ -9,7 +10,7 @@ use html5ever::local_name; use html5ever::{ tendril::{StrTendril, TendrilSink}, tree_builder::{ElementFlags, NodeOrText, QuirksMode, TreeSink}, - ExpandedName, QualName, + QualName, }; /// Convert an html5ever Attribute which uses tendril for its value to a blitz Attribute @@ -22,24 +23,24 @@ 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, } impl<'a> DocumentHtmlParser<'a> { pub fn new(doc: &mut Document) -> 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), } } @@ -51,27 +52,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) => { @@ -82,33 +83,34 @@ 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); match crate::util::fetch_string(url.as_str()) { Ok(css) => { let css = html_escape::decode_html_entities(&css); - self.doc.add_stylesheet(&css); + self.doc.borrow_mut().add_stylesheet(&css); } Err(_) => eprintln!("Error fetching stylesheet {}", url), } } } - 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); + drop(node); // FIXME: Image fetching should not be a synchronous network request during parsing let image_result = crate::util::fetch_image(src.as_str()); @@ -134,7 +136,7 @@ impl<'a> DocumentHtmlParser<'a> { } } - 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; @@ -162,47 +164,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 (