diff --git a/crates/grida-canvas/src/htmlcss/collect.rs b/crates/grida-canvas/src/htmlcss/collect.rs index a9d5750dc..2ad820e71 100644 --- a/crates/grida-canvas/src/htmlcss/collect.rs +++ b/crates/grida-canvas/src/htmlcss/collect.rs @@ -9,7 +9,7 @@ use crate::cg::prelude::*; use super::style::GradientStop; use csscascade::adapter::{self, HtmlElement}; -use csscascade::dom::{DemoDom, DemoNodeData}; +use csscascade::dom::{DemoDom, DemoNode, DemoNodeData}; use style::color::{AbsoluteColor, ColorSpace}; use style::dom::TElement; @@ -150,6 +150,36 @@ fn to_roman(mut n: i32) -> String { result } +// ─── HTML attribute helpers ───────────────────────────────────────── + +/// Get an HTML attribute value from a DOM node. +fn get_element_attr(node: &DemoNode, name: &str) -> Option { + match &node.data { + DemoNodeData::Element(data) => data + .attrs + .iter() + .find(|a| a.name.local.as_ref().eq_ignore_ascii_case(name)) + .map(|a| a.value.to_string()), + _ => None, + } +} + +/// Check if an HTML attribute is present (boolean attribute like `checked`, `disabled`). +fn has_element_attr(node: &DemoNode, name: &str) -> bool { + get_element_attr(node, name).is_some() +} + +/// Collect the concatenated text content of a DOM node's children (shallow). +fn collect_text_content(dom: &DemoDom, node: &DemoNode) -> String { + let mut text = String::new(); + for child_id in &node.children { + if let DemoNodeData::Text(t) = &dom.node(*child_id).data { + text.push_str(t); + } + } + text +} + fn collect_element(element: HtmlElement) -> StyledElement { collect_element_with_counter(element, &mut None) } @@ -214,10 +244,13 @@ fn collect_element_with_counter( None }; - // Collect children, merging consecutive inline content into InlineGroups + // ── Widget detection (form controls) ── let dom = adapter::dom(); let node_data = dom.node(element.node_id()); + let is_void_widget = detect_widget(&tag, node_data, dom, &mut el); + + // Collect children, merging consecutive inline content into InlineGroups let mut pending_inline: Vec = Vec::new(); let parent_text_align = el.font.text_align; let parent_font = el.font.clone(); @@ -234,40 +267,46 @@ fn collect_element_with_counter( })); } - for child_id in &node_data.children { - let child_node = dom.node(*child_id); - match &child_node.data { - DemoNodeData::Text(text) => { - let processed = process_whitespace(text, parent_white_space); - if !processed.is_empty() { - pending_inline.push(InlineRunItem::Text(TextRun { - text: processed, - font: parent_font.clone(), - color: parent_color, - decoration: None, - })); - } - } - DemoNodeData::Element(_) => { - let child_el = HtmlElement::from_node_id(*child_id); - let child = collect_element_with_counter(child_el, &mut child_counter); - if child.display == types::Display::None { - continue; + // Void widget elements () have no DOM children to collect. + if !is_void_widget { + for child_id in &node_data.children { + let child_node = dom.node(*child_id); + match &child_node.data { + DemoNodeData::Text(text) => { + let processed = process_whitespace(text, parent_white_space); + if !processed.is_empty() { + pending_inline.push(InlineRunItem::Text(TextRun { + text: processed, + font: parent_font.clone(), + color: parent_color, + decoration: None, + })); + } } + DemoNodeData::Element(_) => { + let child_el = HtmlElement::from_node_id(*child_id); + let child = collect_element_with_counter(child_el, &mut child_counter); + if child.display == types::Display::None { + continue; + } - if child.display == types::Display::Inline - || child.display == types::Display::InlineBlock - { - // Flatten inline element's content into the pending items - // (Chromium: InlineItemsBuilder flattens DOM → kOpenTag/kText/kCloseTag) - collect_inline_items(&child, &mut pending_inline); - } else { - // Block child — flush pending inline content first - flush_inline_group(&mut pending_inline, parent_text_align, &mut el.children); - el.children.push(StyledNode::Element(child)); + // Widgets with intrinsic sizes need their own Taffy node + // for sizing to work — don't flatten them into inline groups. + let is_inline = child.display == types::Display::Inline + || child.display == types::Display::InlineBlock; + if is_inline && !child.widget.is_widget() { + collect_inline_items(&child, &mut pending_inline); + } else { + flush_inline_group( + &mut pending_inline, + parent_text_align, + &mut el.children, + ); + el.children.push(StyledNode::Element(child)); + } } + _ => {} } - _ => {} // comments, doctypes } } @@ -277,6 +316,320 @@ fn collect_element_with_counter( el } +// ─── Widget (form control) detection ──────────────────────────────── + +/// Chromium placeholder color (#757575). +const PLACEHOLDER_COLOR: CGColor = CGColor { + r: 117, + g: 117, + b: 117, + a: 255, +}; + +/// Detect form control elements and populate `StyledElement::widget`. +/// +/// Returns `true` for void elements (like ``) whose DOM children +/// should be skipped. +fn detect_widget(tag: &str, node_data: &DemoNode, dom: &DemoDom, el: &mut StyledElement) -> bool { + match tag { + "input" => { + detect_input_widget(node_data, el); + true // is a void element + } + "textarea" => { + detect_textarea_widget(node_data, dom, el); + true // skip text children — value already injected + } + "select" => { + detect_select_widget(node_data, dom, el); + true // skip