From 8969635b9a6f11f4a676b57ef318bb1764cb52da Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Mon, 26 Aug 2024 11:46:16 +1000 Subject: [PATCH 01/11] Begin implementing input type checkbox --- examples/interactive.rs | 3 +++ packages/blitz/src/renderer/render.rs | 37 +++++++++++++++++++++++++++ packages/dom/src/layout/construct.rs | 21 +++++++++++++++ packages/dom/src/node.rs | 17 ++++++++++++ 4 files changed, 78 insertions(+) diff --git a/examples/interactive.rs b/examples/interactive.rs index ee5c15f8..2a25baeb 100644 --- a/examples/interactive.rs +++ b/examples/interactive.rs @@ -31,6 +31,9 @@ fn app() -> Element { onclick: move |_| { count.set(0) }, "Reset" } + form { + input { type: "checkbox", id: "check1", name: "check1", value: "Check1", style: "width:20px;" } + } } } } diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index 07079dea..b5078f6f 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -362,6 +362,7 @@ impl<'dom> VelloSceneGenerator<'dom> { cx.stroke_devtools(scene); cx.draw_image(scene); cx.draw_svg(scene); + cx.draw_input(scene); // Render the text in text inputs if let Some(input_data) = cx.text_input { @@ -1091,4 +1092,40 @@ impl ElementCx<'_> { ) { unimplemented!() } + + fn draw_input(&self, scene: &mut Scene) { + if let Some(checked) = self.element.element_data().and_then(|d| d.checkbox_data()) { + println!("Drawing input"); + let checkbox_frame = Rect { + x0: self.frame.outer_rect.x0, + y0: self.frame.outer_rect.y0, + x1: 32.0, + y1: 32.0, + }; + scene.fill( + Fill::NonZero, + self.transform, + Color::WHITE, + None, + &checkbox_frame, + ); + scene.stroke( + &Stroke::default(), + self.transform, + Color::BLACK, + None, + &checkbox_frame, + ); + if checked { + let fragment = vello_svg::render_tree(&usvg::Tree::from_str( + r##" + + +"##, + &usvg::Options::default(), + ).unwrap()); + scene.append(&fragment, Some(self.transform)); + } + } + } } diff --git a/packages/dom/src/layout/construct.rs b/packages/dom/src/layout/construct.rs index bf62b0a3..5c782a58 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 matches!(type_attr, Some("checkbox")) { + create_checkbox_data(doc, container_node_id); + return; } } @@ -303,6 +306,24 @@ fn create_text_editor(doc: &mut Document, input_element_id: usize, is_multiline: } } +fn create_checkbox_data(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(_) + ) { + element.node_specific_data = NodeSpecificData::CheckboxInput( + element + .attr(local_name!("checked")) + .unwrap_or("false") + .parse() + .unwrap_or(false), + ); + } +} + 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 36c657b4..f97957cf 100644 --- a/packages/dom/src/node.rs +++ b/packages/dom/src/node.rs @@ -388,6 +388,20 @@ impl ElementNodeData { } } + pub fn checkbox_data(&self) -> Option { + match self.node_specific_data { + NodeSpecificData::CheckboxInput(data) => Some(data), + _ => None, + } + } + + pub fn checkbox_data_mut(&mut self) -> Option<&mut bool> { + match self.node_specific_data { + NodeSpecificData::CheckboxInput(ref mut data) => Some(data), + _ => None, + } + } + pub fn inline_layout_data(&self) -> Option<&TextLayout> { match self.node_specific_data { NodeSpecificData::InlineRoot(ref data) => Some(data), @@ -500,6 +514,8 @@ pub enum NodeSpecificData { TableRoot(Arc), /// Parley text editor (text inputs) TextInput(TextInputData), + /// Checkbox (checked) + CheckboxInput(bool), /// No data (for nodes that don't need any node-specific data) None, } @@ -512,6 +528,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"), } } From d6cc3e7ebf0f49b41ce55b1c3a60a290faa6e6cf Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Thu, 29 Aug 2024 14:07:54 +1000 Subject: [PATCH 02/11] Use checked attribute --- examples/interactive.rs | 14 ++++++- packages/blitz/src/renderer/render.rs | 27 ++++++------ packages/dioxus-blitz/src/accessibility.rs | 1 + .../src/documents/dioxus_document.rs | 14 ++++--- .../src/documents/event_handler.rs | 21 ++++++++-- packages/dom/src/document.rs | 42 +++++++++++++++---- packages/dom/src/events.rs | 4 +- packages/dom/src/layout/construct.rs | 21 ---------- packages/dom/src/layout/mod.rs | 19 +++++++-- packages/dom/src/node.rs | 17 -------- 10 files changed, 105 insertions(+), 75 deletions(-) diff --git a/examples/interactive.rs b/examples/interactive.rs index 2a25baeb..956a334d 100644 --- a/examples/interactive.rs +++ b/examples/interactive.rs @@ -8,6 +8,7 @@ fn main() { fn app() -> Element { let mut count = use_signal(|| 0); + let mut checkbox_checked = use_signal(|| false); rsx! { div { @@ -32,7 +33,18 @@ fn app() -> Element { "Reset" } form { - input { type: "checkbox", id: "check1", name: "check1", value: "Check1", style: "width:20px;" } + input { + type: "checkbox", + id: "check1", + name: "check1", + value: "check1", + checked: "{checkbox_checked}", + oninput: move |_| { + println!("checkbox changed"); + checkbox_checked.set(!checkbox_checked()) + }, + onclick: move |_| { println!("checkbox clicked") }, + } } } } diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index b5078f6f..9564e3a5 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -1094,35 +1094,34 @@ impl ElementCx<'_> { } fn draw_input(&self, scene: &mut Scene) { - if let Some(checked) = self.element.element_data().and_then(|d| d.checkbox_data()) { - println!("Drawing input"); - let checkbox_frame = Rect { - x0: self.frame.outer_rect.x0, - y0: self.frame.outer_rect.y0, - x1: 32.0, - y1: 32.0, - }; + if self.element.local_name() == "input" + && matches!(self.element.attr(local_name!("type")), Some("checkbox")) + { + let checked: bool = self + .element + .attr(local_name!("checked")) + .and_then(|c| c.parse().ok()) + .unwrap_or_default(); scene.fill( Fill::NonZero, self.transform, Color::WHITE, None, - &checkbox_frame, + &self.frame.outer_rect, ); scene.stroke( &Stroke::default(), self.transform, Color::BLACK, None, - &checkbox_frame, + &self.frame.outer_rect, ); if checked { - let fragment = vello_svg::render_tree(&usvg::Tree::from_str( + let fragment = vello_svg::render_tree(&usvg::Tree::from_str(&format!( r##" - + -"##, - &usvg::Options::default(), +"##, self.frame.outer_rect.width(), self.frame.outer_rect.height()),&usvg::Options::default() ).unwrap()); scene.append(&fragment, Some(self.transform)); } diff --git a/packages/dioxus-blitz/src/accessibility.rs b/packages/dioxus-blitz/src/accessibility.rs index 0d08825f..8787a2b5 100644 --- a/packages/dioxus-blitz/src/accessibility.rs +++ b/packages/dioxus-blitz/src/accessibility.rs @@ -71,6 +71,7 @@ impl AccessibilityState { let ty = element_data.attr(local_name!("type")).unwrap_or("text"); match ty { "number" => Role::NumberInput, + "checkbox" => Role::CheckBox, _ => Role::TextInput, } } diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index 94433fe3..7ff1a753 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -3,8 +3,8 @@ use std::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, NodeData, QualName, TextNodeData, Viewport, DEFAULT_CSS, }; use dioxus::{ @@ -21,7 +21,7 @@ use style::{ properties::{style_structs::Font, ComputedValues}, }; -use super::event_handler::{NativeClickData, NativeConverter}; +use super::event_handler::{NativeClickData, NativeConverter, NativeFormData}; type NodeId = usize; @@ -110,7 +110,6 @@ 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::() { @@ -120,6 +119,11 @@ impl DocumentLike for DioxusDocument { let data = Rc::new(PlatformEventData::new(Box::new(NativeClickData {}))); self.vdom.handle_event(event.name(), data, id, true); + if *element.name.local == *"input" { + let data = + Rc::new(PlatformEventData::new(Box::new(NativeFormData {}))); + self.vdom.handle_event("input", data, id, true); + } return true; } } @@ -127,7 +131,7 @@ impl DocumentLike for DioxusDocument { } } - self.inner.as_mut().handle_event(event); + self.inner.as_mut().handle_event(event.clone()); false } diff --git a/packages/dioxus-blitz/src/documents/event_handler.rs b/packages/dioxus-blitz/src/documents/event_handler.rs index 28d7aafd..31eb1408 100644 --- a/packages/dioxus-blitz/src/documents/event_handler.rs +++ b/packages/dioxus-blitz/src/documents/event_handler.rs @@ -1,4 +1,7 @@ -use dioxus::prelude::{HtmlEventConverter, PlatformEventData}; +use dioxus::{ + html::{HasFileData, HasFormData}, + prelude::{HtmlEventConverter, PlatformEventData}, +}; #[derive(Clone)] pub struct NativeClickData {} @@ -68,8 +71,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 +128,14 @@ impl HtmlEventConverter for NativeConverter { todo!() } } + +#[derive(Clone)] +pub struct NativeFormData {} + +impl HasFormData for NativeFormData { + fn as_any(&self) -> &dyn std::any::Any { + self as &dyn std::any::Any + } +} + +impl HasFileData for NativeFormData {} diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 8e2d3aaa..617d1169 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -1,7 +1,8 @@ use crate::events::{EventData, HitResult, RendererEvent}; -use crate::node::TextBrush; +use crate::node::{Attribute, NodeSpecificData, TextBrush}; use crate::{Node, NodeData, TextNodeData, Viewport}; use app_units::Au; +use html5ever::{local_name, namespace_url, ns, QualName}; use peniko::kurbo; // use quadtree_rs::Quadtree; use parley::editor::{PointerButton, TextEvent}; @@ -124,20 +125,45 @@ impl DocumentLike for Document { assert!(hit.node_id == event.target); let node = &mut self.nodes[hit.node_id]; - let text_input_data = node - .raw_dom_data - .downcast_element_mut() - .and_then(|el| el.text_input_data_mut()); - if text_input_data.is_some() { + let Some(el) = node.raw_dom_data.downcast_element_mut() else { + return true; + }; + + if let NodeSpecificData::TextInput(ref mut text_input_data) = + el.node_specific_data + { let x = hit.x as f64 * self.viewport.scale_f64(); let y = hit.y as f64 * self.viewport.scale_f64(); - text_input_data.unwrap().editor.pointer_down( + text_input_data.editor.pointer_down( kurbo::Point { x, y }, mods, PointerButton::Primary, ); - println!("Clicked {}", hit.node_id); + self.set_focus_to(hit.node_id); + } else if *el.name.local == *"input" + && matches!(el.attr(local_name!("type")), Some("checkbox")) + { + let checked_attr_opt = el + .attrs + .iter_mut() + .find(|attr| attr.name.local == local_name!("checked")); + + let checked_attr = if let Some(attr) = checked_attr_opt { + attr + } else { + let attr = Attribute { + name: QualName::new(None, ns!(html), local_name!("checked")), + value: String::from("false"), + }; + el.attrs.push(attr); + el.attrs + .iter_mut() + .find(|attr| attr.name.local == local_name!("checked")) + .unwrap() + }; + let checked = checked_attr.value.parse().unwrap_or(false); + checked_attr.value = (!checked).to_string(); self.set_focus_to(hit.node_id); } } diff --git a/packages/dom/src/events.rs b/packages/dom/src/events.rs index a72733a3..9193b4c9 100644 --- a/packages/dom/src/events.rs +++ b/packages/dom/src/events.rs @@ -4,7 +4,7 @@ pub struct EventListener { pub name: String, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct RendererEvent { pub target: usize, pub data: EventData, @@ -17,7 +17,7 @@ impl RendererEvent { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum EventData { Click { x: f32, y: f32, mods: Modifiers }, KeyPress { event: KeyEvent, mods: Modifiers }, diff --git a/packages/dom/src/layout/construct.rs b/packages/dom/src/layout/construct.rs index 5c782a58..bf62b0a3 100644 --- a/packages/dom/src/layout/construct.rs +++ b/packages/dom/src/layout/construct.rs @@ -44,9 +44,6 @@ pub(crate) fn collect_layout_children( ) { create_text_editor(doc, container_node_id, false); return; - } else if matches!(type_attr, Some("checkbox")) { - create_checkbox_data(doc, container_node_id); - return; } } @@ -306,24 +303,6 @@ fn create_text_editor(doc: &mut Document, input_element_id: usize, is_multiline: } } -fn create_checkbox_data(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(_) - ) { - element.node_specific_data = NodeSpecificData::CheckboxInput( - element - .attr(local_name!("checked")) - .unwrap_or("false") - .parse() - .unwrap_or(false), - ); - } -} - pub(crate) fn build_inline_layout( doc: &mut Document, inline_context_root_node_id: usize, diff --git a/packages/dom/src/layout/mod.rs b/packages/dom/src/layout/mod.rs index 501a47a3..946f7e4b 100644 --- a/packages/dom/src/layout/mod.rs +++ b/packages/dom/src/layout/mod.rs @@ -144,10 +144,21 @@ 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; + } + //Checkboxes have a fixed size + //TODO size should depend on css pseudoselector too + Some("checkbox") => { + return taffy::LayoutOutput::from_outer_size(taffy::Size { + width: 16.0, + height: 16.0, + }); + } + _ => {} } } diff --git a/packages/dom/src/node.rs b/packages/dom/src/node.rs index f97957cf..36c657b4 100644 --- a/packages/dom/src/node.rs +++ b/packages/dom/src/node.rs @@ -388,20 +388,6 @@ impl ElementNodeData { } } - pub fn checkbox_data(&self) -> Option { - match self.node_specific_data { - NodeSpecificData::CheckboxInput(data) => Some(data), - _ => None, - } - } - - pub fn checkbox_data_mut(&mut self) -> Option<&mut bool> { - match self.node_specific_data { - NodeSpecificData::CheckboxInput(ref mut data) => Some(data), - _ => None, - } - } - pub fn inline_layout_data(&self) -> Option<&TextLayout> { match self.node_specific_data { NodeSpecificData::InlineRoot(ref data) => Some(data), @@ -514,8 +500,6 @@ pub enum NodeSpecificData { TableRoot(Arc), /// Parley text editor (text inputs) TextInput(TextInputData), - /// Checkbox (checked) - CheckboxInput(bool), /// No data (for nodes that don't need any node-specific data) None, } @@ -528,7 +512,6 @@ 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"), } } From 061115923fd210691fbec5d0584066e1aaf153bb Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Thu, 29 Aug 2024 18:52:10 +1000 Subject: [PATCH 03/11] Handle label clicking to associated "for" checkbox --- examples/interactive.rs | 16 +++++++-- .../src/documents/dioxus_document.rs | 34 +++++++++++++++++++ packages/dom/src/default.css | 5 +++ packages/dom/src/layout/mod.rs | 27 +++++++++++---- 4 files changed, 74 insertions(+), 8 deletions(-) diff --git a/examples/interactive.rs b/examples/interactive.rs index 956a334d..aa2f1b2f 100644 --- a/examples/interactive.rs +++ b/examples/interactive.rs @@ -36,7 +36,6 @@ fn app() -> Element { input { type: "checkbox", id: "check1", - name: "check1", value: "check1", checked: "{checkbox_checked}", oninput: move |_| { @@ -44,7 +43,12 @@ fn app() -> Element { checkbox_checked.set(!checkbox_checked()) }, onclick: move |_| { println!("checkbox clicked") }, - } + } + label { + class: "label", + r#for: "check1", + "Check" + } } } } @@ -123,5 +127,13 @@ const CSS: &str = r#" background-color: white; } +form { + margin: 12px 0; +} + +.label { + display: inline-block; + background: white; +} "#; diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index 7ff1a753..e417362a 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -110,8 +110,10 @@ impl DocumentLike for DioxusDocument { continue; }; + for attr in element.attrs() { if attr.name.local.as_ref() == "data-dioxus-id" { + dbg!(node, element); if let Ok(value) = attr.value.parse::() { let id = ElementId(value); // let data = dioxus::html::EventData::Mouse() @@ -128,6 +130,19 @@ impl DocumentLike for DioxusDocument { } } } + + //Clicking labels triggers click of associated checkbox + if *element.name.local == *"label" { + if let Some(target_element_id) = self.for_target_element_id(element) { + let data = Rc::new(PlatformEventData::new(Box::new(NativeClickData {}))); + self.vdom + .handle_event(event.name(), data, target_element_id, true); + let data = Rc::new(PlatformEventData::new(Box::new(NativeFormData {}))); + self.vdom + .handle_event("input", data, target_element_id, true); + return true; + } + } } } @@ -194,6 +209,25 @@ impl DioxusDocument { // dbg!(writer.state); } + /// Find the element id referenced by the "for" attribute of a given label element + fn for_target_element_id(&self, label_element: &ElementNodeData) -> Option { + let target_element_dom_id = label_element.attr(local_name!("for"))?; + let element_id = self.inner.tree().into_iter().find_map(|(_id, node)| { + let element_data = node.element_data()?; + let id = element_data.id.as_ref()?; + if *id != Atom::from(target_element_dom_id) { + return None; + } + element_data.attrs + .iter() + .find(|attr| attr.name.local.as_ref() == "data-dioxus-id")? + .value + .parse::() + .ok() + })?; + Some(ElementId(element_id)) + } + // pub fn apply_mutations(&mut self) { // // Apply the mutations to the actual dom // let mut writer = MutationWriter { 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/layout/mod.rs b/packages/dom/src/layout/mod.rs index 946f7e4b..73f05236 100644 --- a/packages/dom/src/layout/mod.rs +++ b/packages/dom/src/layout/mod.rs @@ -150,13 +150,28 @@ impl LayoutPartialTree for Document { node.style.display = Display::None; return taffy::LayoutOutput::HIDDEN; } - //Checkboxes have a fixed size - //TODO size should depend on css pseudoselector too Some("checkbox") => { - return taffy::LayoutOutput::from_outer_size(taffy::Size { - width: 16.0, - height: 16.0, - }); + return compute_leaf_layout( + inputs, + &node.style, + |_known_size, _available_space| { + let width = node + .style + .size + .width + .resolve_or_zero(inputs.parent_size.width); + let height = node + .style + .size + .height + .resolve_or_zero(inputs.parent_size.height); + let min_size = width.min(height); + taffy::Size { + width: min_size, + height: min_size, + } + }, + ); } _ => {} } From 1c0c92d691a68e40660c4c208cd99faf3e9bd510 Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Thu, 29 Aug 2024 19:12:27 +1000 Subject: [PATCH 04/11] Clean up click and input event processing --- .../src/documents/dioxus_document.rs | 79 +++++++++++-------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index e417362a..e951125b 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -113,33 +113,33 @@ impl DocumentLike for DioxusDocument { for attr in element.attrs() { if attr.name.local.as_ref() == "data-dioxus-id" { - dbg!(node, element); if let Ok(value) = attr.value.parse::() { let id = ElementId(value); // let data = dioxus::html::EventData::Mouse() - - let data = - Rc::new(PlatformEventData::new(Box::new(NativeClickData {}))); - self.vdom.handle_event(event.name(), data, id, true); - if *element.name.local == *"input" { - let data = - Rc::new(PlatformEventData::new(Box::new(NativeFormData {}))); - self.vdom.handle_event("input", data, id, true); + //TODO Check for other clickable inputs here, eg radio + let click_is_input_event = *element.name.local == *"input" + && element.attr(local_name!("type")) == Some("checkbox"); + self.handle_click_event(id); + if click_is_input_event { + self.handle_input_event(id); } return true; } } } - //Clicking labels triggers click of associated checkbox + //Clicking labels triggers click, and possibly input event, of associated input if *element.name.local == *"label" { - if let Some(target_element_id) = self.for_target_element_id(element) { - let data = Rc::new(PlatformEventData::new(Box::new(NativeClickData {}))); - self.vdom - .handle_event(event.name(), data, target_element_id, true); - let data = Rc::new(PlatformEventData::new(Box::new(NativeFormData {}))); - self.vdom - .handle_event("input", data, target_element_id, true); + if let Some((target_element, target_element_id)) = + self.for_target_input_element(element) + { + //TODO Check for other clickable inputs here, eg radio + let click_is_input_event = + target_element.attr(local_name!("type")) == Some("checkbox"); + self.handle_click_event(target_element_id); + if click_is_input_event { + self.handle_input_event(target_element_id); + } return true; } } @@ -153,6 +153,16 @@ impl DocumentLike for DioxusDocument { } impl DioxusDocument { + pub fn handle_click_event(&mut self, id: ElementId) { + let data = Rc::new(PlatformEventData::new(Box::new(NativeClickData {}))); + self.vdom.handle_event("click", data, id, true); + } + + pub fn handle_input_event(&mut self, id: ElementId) { + let data = Rc::new(PlatformEventData::new(Box::new(NativeFormData {}))); + self.vdom.handle_event("input", data, id, true); + } + pub fn new(vdom: VirtualDom) -> Self { let viewport = Viewport::new(0, 0, 1.0); let mut doc = Document::new(viewport); @@ -210,22 +220,29 @@ impl DioxusDocument { } /// Find the element id referenced by the "for" attribute of a given label element - fn for_target_element_id(&self, label_element: &ElementNodeData) -> Option { + fn for_target_input_element( + &self, + label_element: &ElementNodeData, + ) -> Option<(&ElementNodeData, ElementId)> { let target_element_dom_id = label_element.attr(local_name!("for"))?; - let element_id = self.inner.tree().into_iter().find_map(|(_id, node)| { + self.inner.tree().into_iter().find_map(|(_id, node)| { let element_data = node.element_data()?; - let id = element_data.id.as_ref()?; - if *id != Atom::from(target_element_dom_id) { - return None; - } - element_data.attrs - .iter() - .find(|attr| attr.name.local.as_ref() == "data-dioxus-id")? - .value - .parse::() - .ok() - })?; - Some(ElementId(element_id)) + let id = element_data.id.as_ref()?; + if *id != Atom::from(target_element_dom_id) { + return None; + } + if *element_data.name.local != *"input" { + return None; + } + let element_id = element_data + .attrs + .iter() + .find(|attr| *attr.name.local == *"data-dioxus-id")? + .value + .parse::() + .ok()?; + Some((element_data, ElementId(element_id))) + }) } // pub fn apply_mutations(&mut self) { From e0f504166cee55b7c814a478800d4d53aeb8a130 Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Sat, 31 Aug 2024 23:20:27 +1000 Subject: [PATCH 05/11] Support labels in html documents, and propagating to them from dioxus documents --- examples/form.rs | 73 +++++++++++++ examples/interactive.rs | 27 ----- .../src/documents/dioxus_document.rs | 62 +++++------ packages/dom/src/document.rs | 101 ++++++++++++++---- packages/dom/src/events.rs | 4 +- 5 files changed, 179 insertions(+), 88 deletions(-) create mode 100644 examples/form.rs diff --git a/examples/form.rs b/examples/form.rs new file mode 100644 index 00000000..dd66e33b --- /dev/null +++ b/examples/form.rs @@ -0,0 +1,73 @@ +//! 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", + value: "check1", + checked: "{checkbox_checked}", + oninput: move |_| { + checkbox_checked.set(!checkbox_checked()) + }, + } + label { + r#for: "check1", + "Checkbox 1 (controlled)" + } + } + div { + label { + input { + type: "checkbox", + id: "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; +} + +"#; diff --git a/examples/interactive.rs b/examples/interactive.rs index aa2f1b2f..ee5c15f8 100644 --- a/examples/interactive.rs +++ b/examples/interactive.rs @@ -8,7 +8,6 @@ fn main() { fn app() -> Element { let mut count = use_signal(|| 0); - let mut checkbox_checked = use_signal(|| false); rsx! { div { @@ -32,24 +31,6 @@ fn app() -> Element { onclick: move |_| { count.set(0) }, "Reset" } - form { - input { - type: "checkbox", - id: "check1", - value: "check1", - checked: "{checkbox_checked}", - oninput: move |_| { - println!("checkbox changed"); - checkbox_checked.set(!checkbox_checked()) - }, - onclick: move |_| { println!("checkbox clicked") }, - } - label { - class: "label", - r#for: "check1", - "Check" - } - } } } } @@ -127,13 +108,5 @@ const CSS: &str = r#" background-color: white; } -form { - margin: 12px 0; -} - -.label { - display: inline-block; - background: white; -} "#; diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index e951125b..d751a833 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -111,31 +111,31 @@ 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() - //TODO Check for other clickable inputs here, eg radio - let click_is_input_event = *element.name.local == *"input" - && element.attr(local_name!("type")) == Some("checkbox"); - self.handle_click_event(id); - if click_is_input_event { - self.handle_input_event(id); - } - return true; - } + if let Some(id) = DioxusDocument::dioxus_id(element) { + // let data = dioxus::html::EventData::Mouse() + //TODO Check for other clickable inputs here, eg radio + let click_is_input_event = *element.name.local == *"input" + && element.attr(local_name!("type")) == Some("checkbox"); + self.handle_click_event(id); + if click_is_input_event { + self.handle_input_event(id); } + return true; } - //Clicking labels triggers click, and possibly input event, of associated input + //Clicking labels triggers click, and possibly input event, of bound input if *element.name.local == *"label" { - if let Some((target_element, target_element_id)) = - self.for_target_input_element(element) + let bound_input_elements = self.inner.label_bound_input_elements(*node); + if let Some((target_element_data, target_element_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)) + }) { //TODO Check for other clickable inputs here, eg radio let click_is_input_event = - target_element.attr(local_name!("type")) == Some("checkbox"); + target_element_data.attr(local_name!("type")) == Some("checkbox"); self.handle_click_event(target_element_id); if click_is_input_event { self.handle_input_event(target_element_id); @@ -146,7 +146,7 @@ impl DocumentLike for DioxusDocument { } } - self.inner.as_mut().handle_event(event.clone()); + self.inner.as_mut().handle_event(event); false } @@ -219,30 +219,16 @@ impl DioxusDocument { // dbg!(writer.state); } - /// Find the element id referenced by the "for" attribute of a given label element - fn for_target_input_element( - &self, - label_element: &ElementNodeData, - ) -> Option<(&ElementNodeData, ElementId)> { - let target_element_dom_id = label_element.attr(local_name!("for"))?; - self.inner.tree().into_iter().find_map(|(_id, node)| { - let element_data = node.element_data()?; - let id = element_data.id.as_ref()?; - if *id != Atom::from(target_element_dom_id) { - return None; - } - if *element_data.name.local != *"input" { - return None; - } - let element_id = element_data + 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()?; - Some((element_data, ElementId(element_id))) - }) + .ok()?, + )) } // pub fn apply_mutations(&mut self) { diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 617d1169..1e5a7ead 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -1,6 +1,6 @@ use crate::events::{EventData, HitResult, RendererEvent}; use crate::node::{Attribute, NodeSpecificData, TextBrush}; -use crate::{Node, NodeData, TextNodeData, Viewport}; +use crate::{ElementNodeData, Node, NodeData, TextNodeData, Viewport}; use app_units::Au; use html5ever::{local_name, namespace_url, ns, QualName}; use peniko::kurbo; @@ -14,6 +14,7 @@ use style::selector_parser::ServoElementSnapshot; use style::servo::media_queries::FontMetricsProvider; use style::servo_arc::Arc as ServoArc; use style::values::computed::ui::CursorKind; +use style::Atom; use style::{ dom::{TDocument, TNode}, media_queries::{Device, MediaList}, @@ -144,28 +145,24 @@ impl DocumentLike for Document { } else if *el.name.local == *"input" && matches!(el.attr(local_name!("type")), Some("checkbox")) { - let checked_attr_opt = el - .attrs - .iter_mut() - .find(|attr| attr.name.local == local_name!("checked")); - - let checked_attr = if let Some(attr) = checked_attr_opt { - attr - } else { - let attr = Attribute { - name: QualName::new(None, ns!(html), local_name!("checked")), - value: String::from("false"), - }; - el.attrs.push(attr); - el.attrs - .iter_mut() - .find(|attr| attr.name.local == local_name!("checked")) - .unwrap() - }; - let checked = checked_attr.value.parse().unwrap_or(false); - checked_attr.value = (!checked).to_string(); + 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 == *"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 } => { @@ -269,6 +266,68 @@ impl Document { .or(self.try_root_element().map(|el| el.id)) } + /// Find the label's bound input: + /// the element id referenced by the "for" attribute of a given label element + /// or the first input element which is nested in the label + 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 != *"input" { + return None; + } + let id = element_data.id.as_ref()?; + if *id == Atom::from(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 == *"input" { + Some(node) + } else { + None + } + }) + .collect() + } + } + + pub fn toggle_checkbox(el: &mut ElementNodeData) { + let checked_attr_opt = el + .attrs + .iter_mut() + .find(|attr| attr.name.local == local_name!("checked")); + + let checked_attr = if let Some(attr) = checked_attr_opt { + attr + } else { + let attr = Attribute { + name: QualName::new(None, ns!(html), local_name!("checked")), + value: String::from("false"), + }; + el.attrs.push(attr); + el.attrs + .iter_mut() + .find(|attr| attr.name.local == local_name!("checked")) + .unwrap() + }; + let checked = checked_attr.value.parse().unwrap_or(false); + checked_attr.value = (!checked).to_string(); + } + pub fn root_node(&self) -> &Node { &self.nodes[0] } diff --git a/packages/dom/src/events.rs b/packages/dom/src/events.rs index 9193b4c9..a72733a3 100644 --- a/packages/dom/src/events.rs +++ b/packages/dom/src/events.rs @@ -4,7 +4,7 @@ pub struct EventListener { pub name: String, } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct RendererEvent { pub target: usize, pub data: EventData, @@ -17,7 +17,7 @@ impl RendererEvent { } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum EventData { Click { x: f32, y: f32, mods: Modifiers }, KeyPress { event: KeyEvent, mods: Modifiers }, From 712b129939bfabe9e568f07a3d6f444fa697b427 Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Sat, 31 Aug 2024 23:30:47 +1000 Subject: [PATCH 06/11] Add some comment about label_bound_input_elements returning vec --- .../src/documents/dioxus_document.rs | 16 ++++++++-------- packages/dom/src/document.rs | 5 ++++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index d751a833..9970a029 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -114,10 +114,10 @@ impl DocumentLike for DioxusDocument { if let Some(id) = DioxusDocument::dioxus_id(element) { // let data = dioxus::html::EventData::Mouse() //TODO Check for other clickable inputs here, eg radio - let click_is_input_event = *element.name.local == *"input" + let triggers_input_event = *element.name.local == *"input" && element.attr(local_name!("type")) == Some("checkbox"); self.handle_click_event(id); - if click_is_input_event { + if triggers_input_event { self.handle_input_event(id); } return true; @@ -126,7 +126,7 @@ impl DocumentLike for DioxusDocument { //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); - if let Some((target_element_data, target_element_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)?; @@ -134,11 +134,11 @@ impl DocumentLike for DioxusDocument { }) { //TODO Check for other clickable inputs here, eg radio - let click_is_input_event = - target_element_data.attr(local_name!("type")) == Some("checkbox"); - self.handle_click_event(target_element_id); - if click_is_input_event { - self.handle_input_event(target_element_id); + let triggers_input_event = + element_data.attr(local_name!("type")) == Some("checkbox"); + self.handle_click_event(dioxus_id); + if triggers_input_event { + self.handle_input_event(dioxus_id); } return true; } diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 1e5a7ead..35b0a463 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -266,9 +266,12 @@ impl Document { .or(self.try_root_element().map(|el| el.id)) } - /// Find the label's bound input: + /// 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(); From 92c3d5a9a08f4a1853bd429ecd67bbde6b6d31d6 Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Sun, 1 Sep 2024 00:07:23 +1000 Subject: [PATCH 07/11] Use text color to render checkboxes, use bezpath to render tick --- examples/form.rs | 5 ++ packages/blitz/src/renderer/render.rs | 74 +++++++++++++++++++-------- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/examples/form.rs b/examples/form.rs index dd66e33b..d877c44e 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -70,4 +70,9 @@ label { display: inline-block; } +input { + /* Should be accent-color */ + color: #0000cc; +} + "#; diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index 9564e3a5..621c30d3 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}, @@ -1102,28 +1103,59 @@ impl ElementCx<'_> { .attr(local_name!("checked")) .and_then(|c| c.parse().ok()) .unwrap_or_default(); - scene.fill( - Fill::NonZero, - self.transform, - Color::WHITE, - None, - &self.frame.outer_rect, - ); - scene.stroke( - &Stroke::default(), - self.transform, - Color::BLACK, - None, - &self.frame.outer_rect, - ); + + // TODO this should be coming from css accent-color, but I couldn't find how to retrieve it + let accent_color = self.style.get_inherited_text().color.as_vello(); + if checked { - let fragment = vello_svg::render_tree(&usvg::Tree::from_str(&format!( - r##" - - -"##, self.frame.outer_rect.width(), self.frame.outer_rect.height()),&usvg::Options::default() - ).unwrap()); - scene.append(&fragment, Some(self.transform)); + scene.fill( + Fill::NonZero, + self.transform, + accent_color, + None, + &self.frame.outer_rect, + ); + + //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)); + + let path_scale = self + .frame + .outer_rect + .width() + .min(self.frame.outer_rect.height()) + / 16.0; + path.apply_affine(Affine::scale(path_scale)); + + let style = Stroke { + width: 2.0 * path_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, + &self.frame.outer_rect, + ); + scene.stroke( + &Stroke::default(), + self.transform, + accent_color, + None, + &self.frame.outer_rect, + ); } } } From cd6d244fa419e8c2c06838b84104d20895c7e29c Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Sun, 1 Sep 2024 19:32:32 +1000 Subject: [PATCH 08/11] Send filled in FormData event --- examples/form.rs | 8 +- .../src/documents/dioxus_document.rs | 100 +++++++++++++++--- .../src/documents/event_handler.rs | 19 +++- packages/dom/src/document.rs | 8 +- 4 files changed, 110 insertions(+), 25 deletions(-) diff --git a/examples/form.rs b/examples/form.rs index d877c44e..b7a6c93b 100644 --- a/examples/form.rs +++ b/examples/form.rs @@ -18,10 +18,12 @@ fn app() -> Element { input { type: "checkbox", id: "check1", + name: "check1", value: "check1", checked: "{checkbox_checked}", - oninput: move |_| { - checkbox_checked.set(!checkbox_checked()) + oninput: move |ev| { + dbg!(ev); + checkbox_checked.set(!checkbox_checked()); }, } label { @@ -33,7 +35,7 @@ fn app() -> Element { label { input { type: "checkbox", - id: "check2", + name: "check2", value: "check2", } "Checkbox 2 (uncontrolled)" diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index 9970a029..002257bf 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -1,10 +1,10 @@ //! Integration between Dioxus and Blitz -use std::rc::Rc; +use std::{collections::HashMap, rc::Rc}; use blitz_dom::{ events::EventData, local_name, namespace_url, node::Attribute, ns, Atom, Document, - DocumentLike, ElementNodeData, NodeData, QualName, TextNodeData, Viewport, DEFAULT_CSS, + DocumentLike, ElementNodeData, Node, NodeData, QualName, TextNodeData, Viewport, DEFAULT_CSS, }; use dioxus::{ @@ -12,6 +12,7 @@ use dioxus::{ AttributeValue, ElementId, Template, TemplateAttribute, TemplateNode, VirtualDom, WriteMutations, }, + html::FormValue, prelude::{set_event_converter, PlatformEventData}, }; use futures_util::{pin_mut, FutureExt}; @@ -113,12 +114,14 @@ impl DocumentLike for DioxusDocument { if let Some(id) = DioxusDocument::dioxus_id(element) { // let data = dioxus::html::EventData::Mouse() - //TODO Check for other clickable inputs here, eg radio - let triggers_input_event = *element.name.local == *"input" + 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"); - self.handle_click_event(id); if triggers_input_event { - self.handle_input_event(id); + let form_data = self.input_event_form_data(&chain, element); + self.vdom.handle_event("input", form_data, id, true); } return true; } @@ -126,6 +129,7 @@ impl DocumentLike for DioxusDocument { //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()?; @@ -133,12 +137,14 @@ impl DocumentLike for DioxusDocument { Some((target_element_data, dioxus_id)) }) { - //TODO Check for other clickable inputs here, eg radio + 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"); - self.handle_click_event(dioxus_id); if triggers_input_event { - self.handle_input_event(dioxus_id); + let form_data = self.input_event_form_data(&chain, element_data); + self.vdom.handle_event("input", form_data, dioxus_id, true); } return true; } @@ -153,14 +159,78 @@ impl DocumentLike for DioxusDocument { } impl DioxusDocument { - pub fn handle_click_event(&mut self, id: ElementId) { - let data = Rc::new(PlatformEventData::new(Box::new(NativeClickData {}))); - self.vdom.handle_event("click", data, id, true); + 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: &Vec, + 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))) } - pub fn handle_input_event(&mut self, id: ElementId) { - let data = Rc::new(PlatformEventData::new(Box::new(NativeFormData {}))); - self.vdom.handle_event("input", data, id, true); + /// 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 { diff --git a/packages/dioxus-blitz/src/documents/event_handler.rs b/packages/dioxus-blitz/src/documents/event_handler.rs index 31eb1408..2b1621f8 100644 --- a/packages/dioxus-blitz/src/documents/event_handler.rs +++ b/packages/dioxus-blitz/src/documents/event_handler.rs @@ -1,5 +1,7 @@ +use std::collections::HashMap; + use dioxus::{ - html::{HasFileData, HasFormData}, + html::{FormValue, HasFileData, HasFormData}, prelude::{HtmlEventConverter, PlatformEventData}, }; @@ -129,13 +131,24 @@ impl HtmlEventConverter for NativeConverter { } } -#[derive(Clone)] -pub struct NativeFormData {} +#[derive(Clone, Debug)] +pub struct NativeFormData { + pub value: String, + pub values: HashMap, +} impl HasFormData for NativeFormData { fn as_any(&self) -> &dyn std::any::Any { self as &dyn std::any::Any } + + fn value(&self) -> String { + self.value.clone() + } + + fn values(&self) -> HashMap { + self.values.clone() + } } impl HasFileData for NativeFormData {} diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 35b0a463..6ca8270a 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -142,14 +142,14 @@ impl DocumentLike for Document { ); self.set_focus_to(hit.node_id); - } else if *el.name.local == *"input" + } 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 == *"label" { + 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) @@ -280,7 +280,7 @@ impl Document { .into_iter() .filter_map(|(_id, node)| { let element_data = node.element_data()?; - if *element_data.name.local != *"input" { + if element_data.name.local != local_name!("input") { return None; } let id = element_data.id.as_ref()?; @@ -298,7 +298,7 @@ impl Document { .filter_map(|child_id| { let node = self.get_node(*child_id)?; let element_data = node.element_data()?; - if *element_data.name.local == *"input" { + if element_data.name.local == local_name!("input") { Some(node) } else { None From 4b8a8f0b4004f2f54aff2c18b355750a64f78fb9 Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Sun, 1 Sep 2024 19:43:50 +1000 Subject: [PATCH 09/11] Round corners of checkboxes --- packages/blitz/src/renderer/render.rs | 37 ++++++++++----------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index 621c30d3..c5465755 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -1107,14 +1107,17 @@ impl ElementCx<'_> { // TODO this should be coming from css accent-color, but I couldn't find how to retrieve it let accent_color = self.style.get_inherited_text().color.as_vello(); + let scale = self + .frame + .outer_rect + .width() + .min(self.frame.outer_rect.height()) + / 16.0; + + let frame = self.frame.outer_rect.to_rounded_rect(scale * 2.0); + if checked { - scene.fill( - Fill::NonZero, - self.transform, - accent_color, - None, - &self.frame.outer_rect, - ); + scene.fill(Fill::NonZero, self.transform, accent_color, None, &frame); //Tick code derived from masonry let mut path = BezPath::new(); @@ -1122,16 +1125,10 @@ impl ElementCx<'_> { path.line_to((6.0, 13.0)); path.line_to((14.0, 2.0)); - let path_scale = self - .frame - .outer_rect - .width() - .min(self.frame.outer_rect.height()) - / 16.0; - path.apply_affine(Affine::scale(path_scale)); + path.apply_affine(Affine::scale(scale)); let style = Stroke { - width: 2.0 * path_scale, + width: 2.0 * scale, join: Join::Round, miter_limit: 10.0, start_cap: Cap::Round, @@ -1142,19 +1139,13 @@ impl ElementCx<'_> { scene.stroke(&style, self.transform, Color::WHITE, None, &path); } else { - scene.fill( - Fill::NonZero, - self.transform, - Color::WHITE, - None, - &self.frame.outer_rect, - ); + scene.fill(Fill::NonZero, self.transform, Color::WHITE, None, &frame); scene.stroke( &Stroke::default(), self.transform, accent_color, None, - &self.frame.outer_rect, + &frame, ); } } From 6c271aea9d70d96c9f310b13d18623c62588436e Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Mon, 2 Sep 2024 22:22:34 +1000 Subject: [PATCH 10/11] Fix clippy warnings --- packages/dioxus-blitz/src/documents/dioxus_document.rs | 2 +- packages/dom/src/document.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index 002257bf..f5239163 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -167,7 +167,7 @@ impl DioxusDocument { /// Currently only cares about input checkboxes pub fn input_event_form_data( &self, - parent_chain: &Vec, + parent_chain: &[usize], element_node_data: &ElementNodeData, ) -> Rc { let parent_form = parent_chain.iter().find_map(|id| { diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 6ca8270a..66f8a2ea 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -14,7 +14,6 @@ use style::selector_parser::ServoElementSnapshot; use style::servo::media_queries::FontMetricsProvider; use style::servo_arc::Arc as ServoArc; use style::values::computed::ui::CursorKind; -use style::Atom; use style::{ dom::{TDocument, TNode}, media_queries::{Device, MediaList}, @@ -284,7 +283,7 @@ impl Document { return None; } let id = element_data.id.as_ref()?; - if *id == Atom::from(target_element_dom_id) { + if *id == *target_element_dom_id { Some(node) } else { None From 3d0ccf1acc56826d4e424732d11c0954033e175e Mon Sep 17 00:00:00 2001 From: Christopher Fraser Date: Wed, 4 Sep 2024 20:41:26 +1000 Subject: [PATCH 11/11] Add missing lifetime that was elided to clear clippy nightly --- packages/blitz/src/renderer/render.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index c5465755..d017e415 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -443,7 +443,7 @@ impl<'dom> VelloSceneGenerator<'dom> { } } - fn element_cx<'w>(&'w self, element: &'w Node, location: Point) -> ElementCx { + fn element_cx<'w>(&'w self, element: &'w Node, location: Point) -> ElementCx<'w> { let style = element .stylo_element_data .borrow()