diff --git a/Cargo.toml b/Cargo.toml index 9f708a42..3608fd38 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/form.rs b/examples/form.rs new file mode 100644 index 00000000..f9dbc307 --- /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: Some("").filter(|_| 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/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 { 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/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/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 07079dea..6e0c2568 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}, @@ -71,7 +72,6 @@ pub fn generate_vello_scene( dom, scale, devtools: devtool_config, - scroll_offset: dom.scroll_offset, }; generator.generate_vello_scene(scene); @@ -90,7 +90,6 @@ pub struct VelloSceneGenerator<'dom> { dom: &'dom Document, scale: f64, devtools: Devtools, - scroll_offset: f64, } impl<'dom> VelloSceneGenerator<'dom> { @@ -113,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, }, ); @@ -164,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; @@ -314,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 @@ -354,14 +354,31 @@ 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); cx.stroke_frame(scene); + 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: -element.scroll_offset.x, + y: -element.scroll_offset.y, + }); 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 { @@ -441,7 +458,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() @@ -931,6 +948,106 @@ 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; + + // 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 { + 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_outset_shadow { + scene.pop_layer(); + CLIP_DEPTH.fetch_sub(1, atomic::Ordering::SeqCst); + } + } + + 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(); @@ -1091,4 +1208,69 @@ 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 = 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 = if disabled { + peniko::Color { + r: 209, + g: 209, + b: 209, + a: 255, + } + } else { + self.style.get_inherited_text().color.as_vello() + }; + + 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); + + 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::translate(Vec2 { x: 2.0, y: 1.0 }).then_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 ce9ef5b8..85667867 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -1,18 +1,18 @@ //! 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 super::event_handler::{NativeClickData, NativeConverter}; use dioxus::{ dioxus_core::{ AttributeValue, ElementId, Template, TemplateAttribute, TemplateNode, VirtualDom, WriteMutations, }, + html::FormValue, prelude::{set_event_converter, PlatformEventData}, }; use futures_util::{pin_mut, FutureExt}; @@ -23,6 +23,8 @@ use style::{ stylesheets::Origin, }; +use super::event_handler::{NativeClickData, NativeConverter, NativeFormData}; + type NodeId = usize; fn qual_name(local_name: &str, namespace: Option<&str>) -> QualName { @@ -111,17 +113,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 +160,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 +290,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/dioxus-blitz/src/lib.rs b/packages/dioxus-blitz/src/lib.rs index d7984b17..bdc250fb 100644 --- a/packages/dioxus-blitz/src/lib.rs +++ b/packages/dioxus-blitz/src/lib.rs @@ -23,13 +23,18 @@ mod accessibility; use crate::application::Application; use crate::window::View; +use blitz_dom::util::Resource; use blitz_dom::{DocumentLike, HtmlDocument}; use blitz_net::AsyncProvider; use dioxus::prelude::{ComponentFunction, Element, VirtualDom}; use std::sync::Arc; use tokio::runtime::Runtime; use url::Url; -use winit::event_loop::{ControlFlow, EventLoop}; +use winit::{ + dpi::LogicalSize, + event_loop::{ControlFlow, EventLoop}, + window::Window, +}; pub use crate::documents::DioxusDocument; pub use crate::waker::BlitzEvent; @@ -65,16 +70,14 @@ pub fn launch_cfg_with_props( let vdom = VirtualDom::new_with_props(root, props); let document = DioxusDocument::new(vdom); + // Turn on the runtime and enter it let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap(); - let net = Arc::new(AsyncProvider::new(&rt)); - - let window = WindowConfig::new(document, 800.0, 600.0, net); - - launch_with_window(window, rt) + let _guard = rt.enter(); + launch_with_document(document, rt, None) } pub fn launch_url(url: &str) { @@ -113,16 +116,29 @@ pub fn launch_static_html_cfg(html: &str, cfg: Config) { .unwrap(); let _guard = rt.enter(); + let net = AsyncProvider::new(&rt); - let net_provider = Arc::new(AsyncProvider::new(&rt)); + let document = HtmlDocument::from_html(html, cfg.base_url, cfg.stylesheets, &net); + launch_with_document(document, rt, Some(Arc::new(net))); +} + +fn launch_with_document( + doc: impl DocumentLike, + rt: Runtime, + net: Option>>, +) { + 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, net); - let document = HtmlDocument::from_html( - html, - cfg.base_url, - cfg.stylesheets, - Arc::clone(&net_provider), - ); - let window = WindowConfig::new(document, 800.0, 600.0, net_provider); launch_with_window(window, rt) } diff --git a/packages/dioxus-blitz/src/window.rs b/packages/dioxus-blitz/src/window.rs index 90bcef3b..ccf3c1c3 100644 --- a/packages/dioxus-blitz/src/window.rs +++ b/packages/dioxus-blitz/src/window.rs @@ -9,29 +9,29 @@ use winit::keyboard::PhysicalKey; #[allow(unused)] use wgpu::rwh::HasWindowHandle; -#[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))] -use crate::menu::init_menu; use blitz_dom::util::Resource; use blitz_net::AsyncProvider; 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}; use winit::{event::Modifiers, event::WindowEvent, keyboard::KeyCode, window::Window}; +#[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))] +use crate::menu::init_menu; + pub struct WindowConfig { doc: Doc, attributes: WindowAttributes, - net: Arc>, + net: Option>>, } impl WindowConfig { - pub fn new(doc: Doc, width: f32, height: f32, net: Arc>) -> Self { + pub fn new(doc: Doc, net: Option>>) -> Self { WindowConfig { doc, - attributes: Window::default_attributes().with_inner_size(LogicalSize { width, height }), + attributes: Window::default_attributes(), net, } } @@ -39,7 +39,7 @@ impl WindowConfig { pub fn with_attributes( doc: Doc, attributes: WindowAttributes, - net: Arc>, + net: Option>>, ) -> Self { WindowConfig { doc, @@ -87,7 +87,9 @@ impl View { ) -> Self { let winit_window = Arc::from(event_loop.create_window(config.attributes).unwrap()); - Arc::clone(&config.net).resolve(proxy.clone(), winit_window.id()); + if let Some(net) = config.net { + net.resolve(proxy.clone(), winit_window.id()); + } // TODO: make this conditional on text input focus winit_window.set_ime_allowed(true); @@ -203,8 +205,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: ({}, {})",); @@ -408,14 +411,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(); } @@ -426,6 +431,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/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 5cdfd321..6bdfc80f 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::{ImageData, NodeSpecificData, TextBrush}; -use crate::{Node, NodeData, TextNodeData, Viewport}; +use crate::node::{Attribute, ImageData, 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 crate::util::Resource; @@ -76,12 +77,12 @@ pub struct Document { /// We pin the tree to a guarantee to the nodes it creates that the tree is stable in memory. /// /// There is no way to create the tree - publicly or privately - that would invalidate that invariant. - pub nodes: Box>, + pub(crate) nodes: Box>, pub(crate) guard: SharedRwLock, /// The styling engine of firefox - pub stylist: Stylist, + pub(crate) stylist: Stylist, // caching for the stylist pub(crate) snapshots: SnapshotMap, @@ -97,6 +98,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) ua_stylesheets: HashMap, pub(crate) nodes_to_stylesheet: BTreeMap, @@ -110,9 +114,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, } @@ -127,22 +128,48 @@ 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; + }; + + 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 + { 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); + } + } } } EventData::KeyPress { event, mods } => { @@ -202,6 +229,7 @@ impl Document { snapshots, nodes_to_id, viewport, + viewport_scroll: kurbo::Point::ZERO, base_url: None, // quadtree: Quadtree::new(20), ua_stylesheets: HashMap::new(), @@ -211,7 +239,6 @@ impl Document { hover_node_id: None, focus_node_id: None, - scroll_offset: 0.0, changed: HashSet::new(), }; @@ -247,6 +274,69 @@ 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 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(), + }) + } + } + pub fn root_node(&self) -> &Node { &self.nodes[0] } @@ -704,7 +794,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 { @@ -742,12 +831,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) @@ -784,29 +873,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/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, + } + }, + ); + } + _ => {} } } 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; } diff --git a/packages/dom/src/stylo.rs b/packages/dom/src/stylo.rs index 87c0bfd4..7ea9e0eb 100644 --- a/packages/dom/src/stylo.rs +++ b/packages/dom/src/stylo.rs @@ -7,8 +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, NamespaceConstraint}, matching::{ElementSelectorFlags, MatchingContext, VisitedHandlingMode}, sink::Push, Element, OpaqueElement, @@ -21,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, @@ -350,22 +354,42 @@ impl<'a> selectors::Element for BlitzNode<'a> { fn attr_matches( &self, - _ns: &selectors::attr::NamespaceConstraint< - &::NamespaceUrl, - >, - local_name: &::LocalName, - _operation: &selectors::attr::AttrSelectorOperation< - &::AttrValue, - >, + _ns: &NamespaceConstraint<&GenericAtomIdent>, + local_name: &GenericAtomIdent, + operation: &AttrSelectorOperation<&AtomString>, ) -> 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(