From 98bad27d091f33e6eb3d4f2e2b0321d0a5b66c90 Mon Sep 17 00:00:00 2001 From: koko Date: Wed, 11 Sep 2024 01:00:24 +0200 Subject: [PATCH] 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,