From 25f9fdd574840b36c3facc82d4eabcb6c5563b8f Mon Sep 17 00:00:00 2001 From: Matt Hunzinger Date: Mon, 1 Jul 2024 16:52:08 -0400 Subject: [PATCH 1/7] Copy over the todomvc example and get it running --- examples/assets/todomvc.css | 373 ++++++++++++++++++++++++++++++++++++ examples/todomvc.rs | 258 +++++++++++++++++++++++++ 2 files changed, 631 insertions(+) create mode 100644 examples/assets/todomvc.css create mode 100644 examples/todomvc.rs diff --git a/examples/assets/todomvc.css b/examples/assets/todomvc.css new file mode 100644 index 000000000..c928424af --- /dev/null +++ b/examples/assets/todomvc.css @@ -0,0 +1,373 @@ +html, +body, pre { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + text-align: center; + border: none; + /* Mobile Safari */ + opacity: 0; + position: absolute; +} + +.toggle-all+label { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -13px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all+label:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked+label:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; + /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle+label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked+label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ + +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + .filters { + bottom: 10px; + } +} \ No newline at end of file diff --git a/examples/todomvc.rs b/examples/todomvc.rs new file mode 100644 index 000000000..893711687 --- /dev/null +++ b/examples/todomvc.rs @@ -0,0 +1,258 @@ +//! The typical TodoMVC app, implemented in Dioxus. + +use dioxus::prelude::*; +use std::collections::HashMap; + +fn main() { + dioxus_blitz::launch(app) +} + +#[derive(PartialEq, Eq, Clone, Copy)] +enum FilterState { + All, + Active, + Completed, +} + +#[derive(Debug, PartialEq, Eq)] +struct TodoItem { + id: u32, + checked: bool, + contents: String, +} + +fn app() -> Element { + // We store the todos in a HashMap in a Signal. + // Each key is the id of the todo, and the value is the todo itself. + let mut todos = use_signal(HashMap::::new); + + let filter = use_signal(|| FilterState::All); + + // We use a simple memoized signal to calculate the number of active todos. + // Whenever the todos change, the active_todo_count will be recalculated. + let active_todo_count = + use_memo(move || todos.read().values().filter(|item| !item.checked).count()); + + // We use a memoized signal to filter the todos based on the current filter state. + // Whenever the todos or filter change, the filtered_todos will be recalculated. + // Note that we're only storing the IDs of the todos, not the todos themselves. + let filtered_todos = use_memo(move || { + let mut filtered_todos = todos + .read() + .iter() + .filter(|(_, item)| match filter() { + FilterState::All => true, + FilterState::Active => !item.checked, + FilterState::Completed => item.checked, + }) + .map(|f| *f.0) + .collect::>(); + + filtered_todos.sort_unstable(); + + filtered_todos + }); + + // Toggle all the todos to the opposite of the current state. + // If all todos are checked, uncheck them all. If any are unchecked, check them all. + let toggle_all = move |_| { + let check = active_todo_count() != 0; + for (_, item) in todos.write().iter_mut() { + item.checked = check; + } + }; + + rsx!(body { + style { {include_str!("./assets/todomvc.css")} } + section { class: "todoapp", + TodoHeader { todos } + section { class: "main", + if !todos.read().is_empty() { + input { + id: "toggle-all", + class: "toggle-all", + r#type: "checkbox", + onchange: toggle_all, + checked: active_todo_count() == 0 + } + label { r#for: "toggle-all" } + } + + // Render the todos using the filtered_todos signal + // We pass the ID into the TodoEntry component so it can access the todo from the todos signal. + // Since we store the todos in a signal too, we also need to send down the todo list + ul { class: "todo-list", + for id in filtered_todos() { + TodoEntry { key: "{id}", id, todos } + } + } + + // We only show the footer if there are todos. + if !todos.read().is_empty() { + ListFooter { active_todo_count, todos, filter } + } + } + } + + // A simple info footer + footer { class: "info", + p { "Double-click to edit a todo" } + p { + "Created by " + a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" } + } + p { + "Part of " + a { href: "http://todomvc.com", "TodoMVC" } + } + } + }) +} + +#[component] +fn TodoHeader(mut todos: Signal>) -> Element { + let mut draft = use_signal(|| "".to_string()); + let mut todo_id = use_signal(|| 0); + + let onkeydown = move |evt: KeyboardEvent| { + if evt.key() == Key::Enter && !draft.read().is_empty() { + let id = todo_id(); + let todo = TodoItem { + id, + checked: false, + contents: draft.to_string(), + }; + todos.write().insert(id, todo); + todo_id += 1; + draft.set("".to_string()); + } + }; + + rsx! { + header { class: "header", + h1 { "todos" } + input { + class: "new-todo", + placeholder: "What needs to be done?", + value: "{draft}", + autofocus: "true", + oninput: move |evt| draft.set(evt.value()), + onkeydown + } + } + } +} + +/// A single todo entry +/// This takes the ID of the todo and the todos signal as props +/// We can use these together to memoize the todo contents and checked state +#[component] +fn TodoEntry(mut todos: Signal>, id: u32) -> Element { + let mut is_editing = use_signal(|| false); + + // To avoid re-rendering this component when the todo list changes, we isolate our reads to memos + // This way, the component will only re-render when the contents of the todo change, or when the editing state changes. + // This does involve taking a local clone of the todo contents, but it allows us to prevent this component from re-rendering + let checked = use_memo(move || todos.read().get(&id).unwrap().checked); + let contents = use_memo(move || todos.read().get(&id).unwrap().contents.clone()); + + rsx! { + li { + // Dioxus lets you use if statements in rsx to conditionally render attributes + // These will get merged into a single class attribute + class: if checked() { "completed" }, + class: if is_editing() { "editing" }, + + // Some basic controls for the todo + div { class: "view", + input { + class: "toggle", + r#type: "checkbox", + id: "cbg-{id}", + checked: "{checked}", + oninput: move |evt| todos.write().get_mut(&id).unwrap().checked = evt.checked() + } + label { + r#for: "cbg-{id}", + ondoubleclick: move |_| is_editing.set(true), + prevent_default: "onclick", + "{contents}" + } + button { + class: "destroy", + onclick: move |_| { + todos.write().remove(&id); + }, + prevent_default: "onclick" + } + } + + // Only render the actual input if we're editing + if is_editing() { + input { + class: "edit", + value: "{contents}", + oninput: move |evt| todos.write().get_mut(&id).unwrap().contents = evt.value(), + autofocus: "true", + onfocusout: move |_| is_editing.set(false), + onkeydown: move |evt| { + match evt.key() { + Key::Enter | Key::Escape | Key::Tab => is_editing.set(false), + _ => {} + } + } + } + } + } + } +} + +#[component] +fn ListFooter( + mut todos: Signal>, + active_todo_count: ReadOnlySignal, + mut filter: Signal, +) -> Element { + // We use a memoized signal to calculate whether we should show the "Clear completed" button. + // This will recompute whenever the todos change, and if the value is true, the button will be shown. + let show_clear_completed = use_memo(move || todos.read().values().any(|todo| todo.checked)); + + rsx! { + footer { class: "footer", + span { class: "todo-count", + strong { "{active_todo_count} " } + span { + match active_todo_count() { + 1 => "item", + _ => "items", + }, + " left" + } + } + ul { class: "filters", + for (state , state_text , url) in [ + (FilterState::All, "All", "#/"), + (FilterState::Active, "Active", "#/active"), + (FilterState::Completed, "Completed", "#/completed"), + ] { + li { + a { + href: url, + class: if filter() == state { "selected" }, + onclick: move |_| filter.set(state), + prevent_default: "onclick", + {state_text} + } + } + } + } + if show_clear_completed() { + button { + class: "clear-completed", + onclick: move |_| todos.write().retain(|_, todo| !todo.checked), + "Clear completed" + } + } + } + } +} \ No newline at end of file From 72b392f2f36a7d740d0854070bab95b78715305d Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 1 Oct 2024 23:15:40 +0100 Subject: [PATCH 2/7] todomvc example tweaks --- examples/assets/todomvc.css | 2 +- examples/todomvc.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/assets/todomvc.css b/examples/assets/todomvc.css index c928424af..45f3626bb 100644 --- a/examples/assets/todomvc.css +++ b/examples/assets/todomvc.css @@ -73,7 +73,7 @@ body { font-size: 100px; font-weight: 100; text-align: center; - color: rgba(175, 47, 47, 0.15); + color: rgba(175, 47, 47, 1.0); -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 893711687..74fa4a173 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -133,6 +133,7 @@ fn TodoHeader(mut todos: Signal>) -> Element { h1 { "todos" } input { class: "new-todo", + r#type: "text", placeholder: "What needs to be done?", value: "{draft}", autofocus: "true", @@ -191,6 +192,7 @@ fn TodoEntry(mut todos: Signal>, id: u32) -> Element { if is_editing() { input { class: "edit", + r#type: "text", value: "{contents}", oninput: move |evt| todos.write().get_mut(&id).unwrap().contents = evt.value(), autofocus: "true", @@ -255,4 +257,4 @@ fn ListFooter( } } } -} \ No newline at end of file +} From 89f572c79ff1812fb5ca172703255acf0c42134e Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 1 Oct 2024 23:16:28 +0100 Subject: [PATCH 3/7] Render body background color for whole window if html element doesn't have a background color --- packages/blitz/src/renderer.rs | 21 +++++++++++--- packages/blitz/src/renderer/render.rs | 42 ++++++++++++++++++++++++++- packages/dioxus-blitz/src/window.rs | 20 ++++++++++--- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/packages/blitz/src/renderer.rs b/packages/blitz/src/renderer.rs index bb05d2c6e..8ee17159d 100644 --- a/packages/blitz/src/renderer.rs +++ b/packages/blitz/src/renderer.rs @@ -122,7 +122,14 @@ where }; } - pub fn render(&mut self, doc: &Document, scale: f64, devtools: Devtools) { + pub fn render( + &mut self, + doc: &Document, + scale: f64, + width: u32, + height: u32, + devtools: Devtools, + ) { let RenderState::Active(state) = &mut self.render_state else { return; }; @@ -144,7 +151,7 @@ where }; // Regenerate the vello scene - render::generate_vello_scene(&mut self.scene, doc, scale, devtools); + render::generate_vello_scene(&mut self.scene, doc, scale, width, height, devtools); state .renderer @@ -191,9 +198,15 @@ pub async fn render_to_buffer(dom: &Document, viewport: Viewport) -> Vec { .expect("Got non-Send/Sync error from creating renderer"); let mut scene = Scene::new(); - generate_vello_scene(&mut scene, dom, viewport.scale_f64(), Devtools::default()); - let (width, height) = viewport.window_size; + generate_vello_scene( + &mut scene, + dom, + viewport.scale_f64(), + width, + height, + Devtools::default(), + ); let size = Extent3d { width, diff --git a/packages/blitz/src/renderer/render.rs b/packages/blitz/src/renderer/render.rs index 68492c96a..b186ef15b 100644 --- a/packages/blitz/src/renderer/render.rs +++ b/packages/blitz/src/renderer/render.rs @@ -71,6 +71,8 @@ pub fn generate_vello_scene( scene: &mut Scene, dom: &Document, scale: f64, + width: u32, + height: u32, devtool_config: Devtools, ) { CLIPS_USED.store(0, atomic::Ordering::SeqCst); @@ -79,6 +81,8 @@ pub fn generate_vello_scene( let generator = VelloSceneGenerator { dom, scale, + width, + height, devtools: devtool_config, }; generator.generate_vello_scene(scene); @@ -97,6 +101,8 @@ pub struct VelloSceneGenerator<'dom> { /// Input parameters (read only) for generating the Scene dom: &'dom Document, scale: f64, + width: u32, + height: u32, devtools: Devtools, } @@ -121,9 +127,43 @@ impl<'dom> VelloSceneGenerator<'dom> { // 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(); + + let root_element = self.dom.as_ref().root_element(); + let root_id = root_element.id; + let bg_width = (self.width as f32).max(root_element.final_layout.size.width); + let bg_height = (self.height as f32).max(root_element.final_layout.size.height); + + let background_color = { + let html_color = root_element + .primary_styles() + .unwrap() + .clone_background_color(); + if html_color == GenericColor::TRANSPARENT_BLACK { + root_element + .children + .iter() + .find_map(|id| { + self.dom.as_ref().get_node(*id).filter(|node| { + node.raw_dom_data + .is_element_with_tag_name(&local_name!("body")) + }) + }) + .and_then(|body| body.primary_styles()) + .map(|style| style.clone_background_color()) + } else { + Some(html_color) + } + }; + + if let Some(bg_color) = background_color { + let bg_color = bg_color.as_vello(); + let rect = Rect::from_origin_size((0.0, 0.0), (bg_width as f64, bg_height as f64)); + scene.fill(Fill::NonZero, Affine::IDENTITY, bg_color, None, &rect); + } + self.render_element( scene, - self.dom.as_ref().root_element().id, + root_id, Point { x: -viewport_scroll.x, y: -viewport_scroll.y, diff --git a/packages/dioxus-blitz/src/window.rs b/packages/dioxus-blitz/src/window.rs index d5e52be77..040eebdbc 100644 --- a/packages/dioxus-blitz/src/window.rs +++ b/packages/dioxus-blitz/src/window.rs @@ -132,8 +132,14 @@ impl View { }; // Render - self.renderer - .render(self.dom.as_ref(), self.viewport.scale_f64(), self.devtools); + let (width, height) = self.viewport.window_size; + self.renderer.render( + self.dom.as_ref(), + self.viewport.scale_f64(), + width, + height, + self.devtools, + ); // Set waker self.waker = Some(create_waker(&self.event_loop_proxy, self.window_id())); @@ -173,8 +179,14 @@ impl View { pub fn redraw(&mut self) { self.dom.as_mut().resolve(); - self.renderer - .render(self.dom.as_ref(), self.viewport.scale_f64(), self.devtools); + let (width, height) = self.viewport.window_size; + self.renderer.render( + self.dom.as_ref(), + self.viewport.scale_f64(), + width, + height, + self.devtools, + ); } pub fn window_id(&self) -> WindowId { From 564d5c1eccd134ce700b422ca800b8caa27f0739 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 2 Oct 2024 10:45:23 +0100 Subject: [PATCH 4/7] Implement layout sizing for text inputs --- packages/dom/src/layout/mod.rs | 36 ++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/dom/src/layout/mod.rs b/packages/dom/src/layout/mod.rs index 000d96f63..35d1beac3 100644 --- a/packages/dom/src/layout/mod.rs +++ b/packages/dom/src/layout/mod.rs @@ -106,6 +106,17 @@ impl LayoutPartialTree for Document { compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| { let node = &mut tree.nodes[node_id.into()]; + let resolved_line_height = node.primary_styles().map(|style| { + use style::values::computed::font::LineHeight; + + let font_size = style.clone_font_size().used_size().px(); + match style.clone_line_height() { + LineHeight::Normal => font_size * 1.2, + LineHeight::Number(num) => font_size * num.0, + LineHeight::Length(value) => value.0.px(), + } + }); + match &mut node.raw_dom_data { NodeData::Text(data) => { // With the new "inline context" architecture all text nodes should be wrapped in an "inline layout context" @@ -142,6 +153,15 @@ impl LayoutPartialTree for Document { return taffy::LayoutOutput::HIDDEN; } + // Perform text layout for text inputs + if inputs.run_mode == taffy::RunMode::PerformLayout { + if let Some(input_data) = element_data.text_input_data_mut() { + input_data + .editor + .rebuild(&mut tree.font_ctx, &mut tree.layout_ctx); + } + } + // todo: need to handle shadow roots by actually descending into them if *element_data.name.local == *"input" { match element_data.attr(local_name!("type")) { @@ -173,16 +193,20 @@ impl LayoutPartialTree for Document { }, ); } + None | Some("text" | "password" | "email") => { + return compute_leaf_layout( + inputs, + &node.style, + |_known_size, _available_space| taffy::Size { + width: 300.0, + height: resolved_line_height.unwrap_or(16.0), + }, + ); + } _ => {} } } - if let Some(input_data) = element_data.text_input_data_mut() { - input_data - .editor - .rebuild(&mut tree.font_ctx, &mut tree.layout_ctx); - } - if *element_data.name.local == *"img" { // Get width and height attributes on image element // From b07aa4fb67f1a60fe3852d4991e622911729e840 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 2 Oct 2024 11:04:29 +0100 Subject: [PATCH 5/7] Fix hit testing for text inputs with padding/border --- packages/dom/src/document.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/dom/src/document.rs b/packages/dom/src/document.rs index 79ffe7083..0f2c222d2 100644 --- a/packages/dom/src/document.rs +++ b/packages/dom/src/document.rs @@ -135,6 +135,10 @@ impl DocumentLike for Document { assert!(hit.node_id == event.target); let node = &mut self.nodes[hit.node_id]; + let content_box_offset = taffy::Point { + x: node.final_layout.padding.left + node.final_layout.border.left, + y: node.final_layout.padding.top + node.final_layout.border.top, + }; let Some(el) = node.raw_dom_data.downcast_element_mut() else { return true; }; @@ -147,8 +151,8 @@ impl DocumentLike for Document { 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(); + let x = (hit.x - content_box_offset.x) as f64 * self.viewport.scale_f64(); + let y = (hit.y - content_box_offset.y) as f64 * self.viewport.scale_f64(); text_input_data.editor.pointer_down( kurbo::Point { x, y }, mods, From 3d437bb44d4a641255c2419768fefdc30cbbd806 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 2 Oct 2024 11:14:57 +0100 Subject: [PATCH 6/7] Run default actions even if there is a dioxus event handler --- packages/dioxus-blitz/src/documents/dioxus_document.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/dioxus-blitz/src/documents/dioxus_document.rs b/packages/dioxus-blitz/src/documents/dioxus_document.rs index 40d38b23b..793e7833d 100644 --- a/packages/dioxus-blitz/src/documents/dioxus_document.rs +++ b/packages/dioxus-blitz/src/documents/dioxus_document.rs @@ -101,6 +101,8 @@ impl DocumentLike for DioxusDocument { set_event_converter(Box::new(NativeConverter {})); + let mut handled = false; + if matches!(event.data, EventData::Click { .. }) { // look for the data-dioxus-id attribute on the element // todo: we might need to walk upwards to find the first element with a data-dioxus-id attribute @@ -127,7 +129,8 @@ impl DocumentLike for DioxusDocument { let form_data = self.input_event_form_data(&chain, element); self.vdom.handle_event("input", form_data, id, true); } - return true; + handled = true; + // return true; } //Clicking labels triggers click, and possibly input event, of bound input @@ -150,7 +153,8 @@ impl DocumentLike for DioxusDocument { let form_data = self.input_event_form_data(&chain, element_data); self.vdom.handle_event("input", form_data, dioxus_id, true); } - return true; + handled = true; + // return true; } } } @@ -158,7 +162,7 @@ impl DocumentLike for DioxusDocument { self.inner.as_mut().handle_event(event); - false + handled } } From dba02d8a15b1362a24c2e024c06d487d10f4b828 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 2 Oct 2024 12:43:27 +0100 Subject: [PATCH 7/7] Set line height on text inputs (bump parley for editor line height support) --- Cargo.toml | 2 +- packages/dom/src/layout/construct.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c876260be..468dc72ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ style_dom = { git = "https://github.com/dioxuslabs/stylo", package = "dom", bran selectors = { git = "https://github.com/dioxuslabs/stylo", branch = "blitz" } html5ever = "0.29" # needs to match stylo markup5ever version taffy = { git = "https://github.com/dioxuslabs/taffy", rev = "950a0eb1322f15e5d1083f4793b55d52061718de" } -parley = { git = "https://github.com/nicoburns/parley", rev = "029bf1df3e1829935fa6d25b875d3138f79a62c1" } +parley = { git = "https://github.com/nicoburns/parley", rev = "186b6e991d08731c0588dc0b247564cbba1c0435" } 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"] } diff --git a/packages/dom/src/layout/construct.rs b/packages/dom/src/layout/construct.rs index 097c91035..a35504f84 100644 --- a/packages/dom/src/layout/construct.rs +++ b/packages/dom/src/layout/construct.rs @@ -506,6 +506,9 @@ fn create_text_editor(doc: &mut Document, input_element_id: usize, is_multiline: text_input_data .editor .set_text_size(parley_style.font_size * doc.viewport.scale()); + text_input_data + .editor + .set_line_height(parley_style.line_height); text_input_data.editor.set_brush(parley_style.brush); element.node_specific_data = NodeSpecificData::TextInput(text_input_data); }