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

Implement form submissions #188

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ image = { version = "0.25", default-features = false }
woff = "0.3"
woff2 = "0.3"
html-escape = "0.2.13"
percent-encoding = "2.3.1"

# Other dependencies
rustc-hash = "1.1.0"
Expand Down
8 changes: 5 additions & 3 deletions apps/readme/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod readme_application;

use blitz_html::HtmlDocument;
use blitz_net::Provider;
use blitz_traits::navigation::NavigationProvider;
use blitz_traits::navigation::{NavigationOptions, NavigationProvider};
use markdown::{markdown_to_html, BLITZ_MD_STYLES, GITHUB_MD_STYLES};
use notify::{Error as NotifyError, Event as NotifyEvent, RecursiveMode, Watcher as _};
use readme_application::{ReadmeApplication, ReadmeEvent};
Expand All @@ -27,8 +27,10 @@ struct ReadmeNavigationProvider {
}

impl NavigationProvider for ReadmeNavigationProvider {
fn navigate_new_page(&self, url: String) {
let _ = self.proxy.send_event(BlitzShellEvent::Navigate(url));
fn navigate_to(&self, opts: NavigationOptions) {
let _ = self
.proxy
.send_event(BlitzShellEvent::Navigate(Box::new(opts)));
}
}

Expand Down
4 changes: 2 additions & 2 deletions apps/readme/src/readme_application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ impl ApplicationHandler<BlitzShellEvent> for ReadmeApplication {
self.reload_document(true);
}
}
BlitzShellEvent::Navigate(url) => {
self.raw_url = url;
BlitzShellEvent::Navigate(opts) => {
self.raw_url = opts.url.into();
self.reload_document(false);
}
event => self.inner.user_event(event_loop, event),
Expand Down
1 change: 1 addition & 0 deletions packages/blitz-dom/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ usvg = { workspace = true, optional = true }
woff = { workspace = true, optional = true }
woff2 = { workspace = true, optional = true }
html-escape = { workspace = true }
percent-encoding = { workspace = true }

# IO & Networking
url = { workspace = true, features = ["serde"] }
Expand Down
37 changes: 10 additions & 27 deletions packages/blitz-dom/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::events::handle_event;
use crate::layout::construct::collect_layout_children;
use crate::node::{ImageData, NodeSpecificData, Status, TextBrush};
use crate::stylo_to_cursor_icon::stylo_to_cursor_icon;
use crate::util::{resolve_url, ImageType};
use crate::util::{resolve_url, AncestorTraverser, ImageType, TreeTraverser};
use crate::{ElementNodeData, Node, NodeData, TextNodeData};
use app_units::Au;
use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
Expand All @@ -24,7 +24,7 @@ use style::values::GenericAtomIdent;
use crate::net::{Resource, StylesheetLoader};
use selectors::{matching::QuirksMode, Element};
use slab::Slab;
use std::collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use std::collections::{BTreeMap, Bound, HashMap, HashSet};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use style::media_queries::MediaType;
Expand Down Expand Up @@ -126,6 +126,8 @@ pub struct BaseDocument {

pub changed: HashSet<usize>,

pub controls_to_form: HashMap<usize, usize>,

/// Network provider. Can be used to fetch assets.
pub net_provider: SharedProvider<Resource>,

Expand Down Expand Up @@ -211,6 +213,7 @@ impl BaseDocument {
focus_node_id: None,
active_node_id: None,
changed: HashSet::new(),
controls_to_form: HashMap::new(),
net_provider: Arc::new(DummyNetProvider::default()),
navigation_provider: Arc::new(DummyNavigationProvider {}),
};
Expand Down Expand Up @@ -628,6 +631,7 @@ impl BaseDocument {
Resource::Font(bytes) => {
self.font_ctx.collection.register_fonts(bytes.to_vec());
}
_ => {}
}
}

Expand Down Expand Up @@ -1177,35 +1181,14 @@ impl BaseDocument {
where
F: FnMut(usize, &Node),
{
let mut stack = VecDeque::new();
stack.push_front(0);

while let Some(node_key) = stack.pop_back() {
let node = &self.nodes[node_key];
visit(node_key, node);

for &child_key in &node.children {
stack.push_front(child_key);
}
}
TreeTraverser::new(self).for_each(|node_id| visit(node_id, &self.nodes[node_id]));
}

/// Collect the nodes into a chain by traversing upwards
pub fn node_chain(&self, node_id: usize) -> Vec<usize> {
let mut next_node_id = Some(node_id);
let mut chain = Vec::with_capacity(16);

while let Some(node_id) = next_node_id {
let node = &self.tree()[node_id];

if node.is_element() {
chain.push(node_id);
}

next_node_id = node.parent;
}

chain
AncestorTraverser::new(self, node_id)
.filter(|ancestor_id| self.nodes[*ancestor_id].is_element())
.collect()
}
}

Expand Down
62 changes: 54 additions & 8 deletions packages/blitz-dom/src/events/keyboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::{
};
use blitz_traits::BlitzKeyEvent;
use keyboard_types::{Key, Modifiers};
use markup5ever::local_name;
use parley::{FontContext, LayoutContext};

pub(crate) fn handle_keypress(doc: &mut BaseDocument, target: usize, event: BlitzKeyEvent) {
Expand All @@ -13,14 +14,18 @@ pub(crate) fn handle_keypress(doc: &mut BaseDocument, target: usize, event: Blit
}

let node = &mut doc.nodes[node_id];
let text_input_data = node
.data
.downcast_element_mut()
.and_then(|el| el.text_input_data_mut());
let Some(element_data) = node.element_data_mut() else {
return;
};

if let Some(input_data) = text_input_data {
if let Some(input_data) = element_data.text_input_data_mut() {
println!("Sent text event to {}", node_id);
apply_keypress_event(input_data, &mut doc.font_ctx, &mut doc.layout_ctx, event);
let implicit_submission =
apply_keypress_event(input_data, &mut doc.font_ctx, &mut doc.layout_ctx, event);

if implicit_submission {
implicit_form_submission(doc, target);
}
}
}
}
Expand All @@ -35,10 +40,10 @@ pub(crate) fn apply_keypress_event(
font_ctx: &mut FontContext,
layout_ctx: &mut LayoutContext<TextBrush>,
event: BlitzKeyEvent,
) {
) -> bool {
// Do nothing if it is a keyup event
if !event.state.is_pressed() {
return;
return false;
}

let mods = event.modifiers;
Expand Down Expand Up @@ -165,9 +170,50 @@ pub(crate) fn apply_keypress_event(
Key::Enter => {
if is_multiline {
driver.insert_or_replace_selection("\n");
} else {
return true;
}
}
Key::Character(s) => driver.insert_or_replace_selection(&s),
_ => {}
};
false
}

/// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#field-that-blocks-implicit-submission
fn implicit_form_submission(doc: &BaseDocument, text_target: usize) {
let Some(form_owner_id) = doc.controls_to_form.get(&text_target) else {
return;
};
if doc
.controls_to_form
.iter()
.filter(|(_control_id, form_id)| *form_id == form_owner_id)
.filter_map(|(control_id, _)| doc.nodes[*control_id].element_data())
.filter(|element_data| {
element_data.attr(local_name!("type")).is_some_and(|t| {
matches!(
t,
"text"
| "search"
| "email"
| "url"
| "tel"
| "password"
| "date"
| "month"
| "week"
| "time"
| "datetime-local"
| "number"
)
})
})
.count()
> 1
{
return;
}

doc.submit_form(*form_owner_id, *form_owner_id);
}
30 changes: 20 additions & 10 deletions packages/blitz-dom/src/events/mouse.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use blitz_traits::{HitResult, MouseEventButtons};
use blitz_traits::{navigation::NavigationOptions, HitResult, MouseEventButtons};
use markup5ever::local_name;

use crate::{node::NodeSpecificData, util::resolve_url, BaseDocument, Node};
Expand Down Expand Up @@ -99,10 +99,14 @@ pub(crate) fn handle_click(doc: &mut BaseDocument, _target: usize, x: f32, y: f3
let mut maybe_hit = doc.hit(x, y);

while let Some(hit) = maybe_hit {
let node = &mut doc.nodes[hit.node_id];
let node_id = hit.node_id;
let maybe_element = {
let node = &mut doc.nodes[node_id];
node.data.downcast_element_mut()
};

let Some(el) = node.data.downcast_element_mut() else {
maybe_hit = parent_hit(node, x, y);
let Some(el) = maybe_element else {
maybe_hit = parent_hit(&doc.nodes[node_id], x, y);
continue;
};

Expand All @@ -117,20 +121,18 @@ pub(crate) fn handle_click(doc: &mut BaseDocument, _target: usize, x: f32, y: f3
&& matches!(el.attr(local_name!("type")), Some("checkbox"))
{
BaseDocument::toggle_checkbox(el);
doc.set_focus_to(hit.node_id);
doc.set_focus_to(node_id);
return;
} else if el.name.local == local_name!("input")
&& matches!(el.attr(local_name!("type")), Some("radio"))
{
let node_id = node.id;
let radio_set = el.attr(local_name!("name")).unwrap().to_string();
BaseDocument::toggle_radio(doc, radio_set, node_id);
BaseDocument::set_focus_to(doc, hit.node_id);
BaseDocument::set_focus_to(doc, node_id);
return;
}
// 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) = doc
.label_bound_input_elements(node_id)
.first()
Expand All @@ -146,7 +148,8 @@ pub(crate) fn handle_click(doc: &mut BaseDocument, _target: usize, x: f32, y: f3
} else if el.name.local == local_name!("a") {
if let Some(href) = el.attr(local_name!("href")) {
if let Some(url) = resolve_url(&doc.base_url, href) {
doc.navigation_provider.navigate_new_page(url.into());
doc.navigation_provider
.navigate_to(NavigationOptions::new(url, doc.id()));
} else {
println!(
"{href} is not parseable as a url. : {base_url:?}",
Expand All @@ -157,10 +160,17 @@ pub(crate) fn handle_click(doc: &mut BaseDocument, _target: usize, x: f32, y: f3
} else {
println!("Clicked link without href: {:?}", el.attrs());
}
} else if el.name.local == local_name!("input")
&& el.attr(local_name!("type")) == Some("submit")
|| el.name.local == local_name!("button")
{
if let Some(form_owner) = doc.controls_to_form.get(&node_id) {
doc.submit_form(*form_owner, node_id);
}
}

// No match. Recurse up to parent.
maybe_hit = parent_hit(&doc.nodes[hit.node_id], x, y)
maybe_hit = parent_hit(&doc.nodes[node_id], x, y)
}

// If nothing is matched then clear focus
Expand Down
Loading