Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checkbox inputs #125

Merged
merged 11 commits into from
Sep 6, 2024
80 changes: 80 additions & 0 deletions examples/form.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! Drive the renderer from Dioxus

use dioxus::prelude::*;

fn main() {
dioxus_blitz::launch(app);
}

fn app() -> Element {
let mut checkbox_checked = use_signal(|| false);

rsx! {
div {
class: "container",
style { {CSS} }
form {
div {
input {
type: "checkbox",
id: "check1",
name: "check1",
value: "check1",
checked: "{checkbox_checked}",
oninput: move |ev| {
dbg!(ev);
checkbox_checked.set(!checkbox_checked());
},
}
label {
r#for: "check1",
"Checkbox 1 (controlled)"
}
}
div {
label {
input {
type: "checkbox",
name: "check2",
value: "check2",
}
"Checkbox 2 (uncontrolled)"
}
}
}
div { "Checkbox 1 checked: {checkbox_checked}" }
}
}
}

const CSS: &str = r#"

.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
}


form {
margin: 12px 0;
display: block;
}

form > div {
margin: 8px 0;
}

label {
display: inline-block;
}

input {
/* Should be accent-color */
color: #0000cc;
}

"#;
61 changes: 60 additions & 1 deletion packages/blitz/src/renderer/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -362,6 +363,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 {
Expand Down Expand Up @@ -441,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()
Expand Down Expand Up @@ -1091,4 +1093,61 @@ impl ElementCx<'_> {
) {
unimplemented!()
}

fn draw_input(&self, scene: &mut Scene) {
if self.element.local_name() == "input"
&& matches!(self.element.attr(local_name!("type")), Some("checkbox"))
{
let checked: bool = self
.element
.attr(local_name!("checked"))
.and_then(|c| c.parse().ok())
.unwrap_or_default();
Comment on lines +1101 to +1105
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checked is a boolean attribute, so this should just check for the presence of the checked attribute in the array. If it is present then checked is true else false (even the empty string or "false" counts as true).

See: https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML


// TODO this should be coming from css accent-color, but I couldn't find how to retrieve it
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accent-color can be enabled here: https://github.com/servo/stylo/blob/main/style/properties/longhands/inherited_ui.mako.rs#L94 by changing engines="gecko" to engines="gecko servo".

Such a change could be submitted to the DioxusLabs fork of Stylo. We are currently using the enable-table-moz-center-style-adjust branch. So changes should be based on that branch (we should probably be more principled about using a blitz branch or similar).

let accent_color = self.style.get_inherited_text().color.as_vello();

let scale = self
.frame
.outer_rect
.width()
.min(self.frame.outer_rect.height())
/ 16.0;

let frame = self.frame.outer_rect.to_rounded_rect(scale * 2.0);

if checked {
scene.fill(Fill::NonZero, self.transform, accent_color, None, &frame);

//Tick code derived from masonry
let mut path = BezPath::new();
path.move_to((2.0, 9.0));
path.line_to((6.0, 13.0));
path.line_to((14.0, 2.0));

path.apply_affine(Affine::scale(scale));

let style = Stroke {
width: 2.0 * scale,
join: Join::Round,
miter_limit: 10.0,
start_cap: Cap::Round,
end_cap: Cap::Round,
dash_pattern: Default::default(),
dash_offset: 0.0,
};

scene.stroke(&style, self.transform, Color::WHITE, None, &path);
} else {
scene.fill(Fill::NonZero, self.transform, Color::WHITE, None, &frame);
scene.stroke(
&Stroke::default(),
self.transform,
accent_color,
None,
&frame,
);
}
}
}
}
1 change: 1 addition & 0 deletions packages/dioxus-blitz/src/accessibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down
137 changes: 124 additions & 13 deletions packages/dioxus-blitz/src/documents/dioxus_document.rs
Original file line number Diff line number Diff line change
@@ -1,17 +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 dioxus::{
dioxus_core::{
AttributeValue, ElementId, Template, TemplateAttribute, TemplateNode, VirtualDom,
WriteMutations,
},
html::FormValue,
prelude::{set_event_converter, PlatformEventData},
};
use futures_util::{pin_mut, FutureExt};
Expand All @@ -21,7 +22,7 @@ use style::{
properties::{style_structs::Font, ComputedValues},
};

use super::event_handler::{NativeClickData, NativeConverter};
use super::event_handler::{NativeClickData, NativeConverter, NativeFormData};

type NodeId = usize;

Expand Down Expand Up @@ -111,17 +112,41 @@ impl DocumentLike for DioxusDocument {
continue;
};

for attr in element.attrs() {
if attr.name.local.as_ref() == "data-dioxus-id" {
if let Ok(value) = attr.value.parse::<usize>() {
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;
}
}
}
Expand All @@ -134,6 +159,80 @@ impl DocumentLike for DioxusDocument {
}

impl DioxusDocument {
pub fn click_event_data(&self) -> Rc<PlatformEventData> {
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<PlatformEventData> {
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::<String, FormValue>::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);
Expand Down Expand Up @@ -190,6 +289,18 @@ impl DioxusDocument {
// dbg!(writer.state);
}

fn dioxus_id(element_node_data: &ElementNodeData) -> Option<ElementId> {
Some(ElementId(
element_node_data
.attrs
.iter()
.find(|attr| *attr.name.local == *"data-dioxus-id")?
.value
.parse::<usize>()
.ok()?,
))
}

// pub fn apply_mutations(&mut self) {
// // Apply the mutations to the actual dom
// let mut writer = MutationWriter {
Expand Down
Loading