render: size native input controls

This commit is contained in:
SGavrl committed 2026-07-23 21:30:35 +00:00
1 parent eb133de419
commit 40a34639ca
2 files changed
+179 -2

No files matched your search

+153 -2
View File
@@ -222,6 +222,7 @@ fn cascade_walk(
matcher: &mut obscura_dom::selector::Matcher,
styles: &mut HashMap<NodeId, crate::LayoutStyle>,
parent_props: &std::rc::Rc<HashMap<String, String>>,
quirks_mode: bool,
) {
let Some(node) = tree.get_node(id) else { return };
let is_element = node.is_element();
@@ -230,6 +231,45 @@ fn cascade_walk(
let mut this_props = parent_props.clone();
if let Some(elem) = node.as_element() {
let mut style = crate::style::ua_style(elem.local.as_ref());
if quirks_mode && elem.local.as_ref() == "form" {
// Legacy HTML/quirks rendering keeps one em after forms. Standards
// mode does not; Hacker News and many old document templates omit
// a doctype and rely on this spacing.
style.margin_relative[2] = Some(crate::Dimension::Em(1.0));
}
if elem.local.as_ref() == "input" {
let input_type = node
.get_attribute("type")
.unwrap_or("text")
.trim()
.to_ascii_lowercase();
if quirks_mode {
style.box_sizing = crate::BoxSizing::BorderBox;
}
match input_type.as_str() {
"checkbox" | "radio" => {
style.margin = crate::Edges {
top: 3.0,
right: 3.0,
bottom: 3.0,
left: 4.0,
};
style.padding = crate::Edges::default();
style.border = crate::Edges::default();
}
"range" | "color" => {
style.margin = crate::Edges {
top: 2.0,
right: 2.0,
bottom: 2.0,
left: 2.0,
};
style.padding = crate::Edges::default();
style.border = crate::Edges::default();
}
_ => {}
}
}
// UA rule `[hidden]:not([hidden=until-found]) { display: none }`.
// Applied before the author cascade so a matching author `display`
// still wins (UA origin, per the HTML rendering spec). Sites ship
@@ -271,7 +311,15 @@ fn cascade_walk(
matcher.push_ancestor(tree, id);
}
for cid in tree.children(id) {
cascade_walk(tree, cid, sheet, matcher, styles, &this_props);
cascade_walk(
tree,
cid,
sheet,
matcher,
styles,
&this_props,
quirks_mode,
);
}
if is_element {
matcher.pop_ancestor();
@@ -320,7 +368,23 @@ pub fn layout_dom_with_images(
// into children, pop on the way back out. This is what lets descendant
// combinators (".mw-body .firstHeading") fast-reject via the filter
// instead of falling back to the always-true "can't reject" case.
cascade_walk(tree, tree.document(), &sheet, &mut matcher, &mut styles, &root_props);
let quirks_mode = !tree
.descendants(tree.document())
.into_iter()
.any(|id| {
tree.get_node(id).map_or(false, |node| {
matches!(node.data, obscura_dom::tree::NodeData::Doctype { .. })
})
});
cascade_walk(
tree,
tree.document(),
&sheet,
&mut matcher,
&mut styles,
&root_props,
quirks_mode,
);
if timing {
let (r, i, c, l, u) = sheet.debug_stats();
eprintln!("[timing] parse+index={:?} cascade={:?} rules={} id_keys={} class_keys={} local_keys={} universal={}", t_parse, t1.elapsed(), r, i, c, l, u);
@@ -638,6 +702,93 @@ pub fn layout_dom_with_images(
}
}
// Resolve native input-control intrinsic border-box geometry after
// inheritance and author cascading. Text-like inputs use the HTML
// `size` attribute (20 by default) and the control's own computed
// font; author CSS widths/heights remain authoritative. Without this
// replaced-control sizing, every input is an empty auto-sized leaf
// (0px tall and often stretched to its container).
for (&id, style) in styles.iter_mut() {
let Some(node) = tree.get_node(id) else { continue };
let Some(element) = node.as_element() else { continue };
if element.local.as_ref() != "input" {
continue;
}
let input_type = node
.get_attribute("type")
.unwrap_or("text")
.trim()
.to_ascii_lowercase();
if input_type == "hidden" {
style.display = crate::Display::None;
continue;
}
let font_size = style.font_size.unwrap_or(13.333_333).max(1.0);
let horizontal_edges = style.padding.left
+ style.padding.right
+ style.border.left
+ style.border.right;
let vertical_edges = style.padding.top
+ style.padding.bottom
+ style.border.top
+ style.border.bottom;
let default_height =
crate::inline::used_line_height(style).max(1.0) + vertical_edges;
let (intrinsic_width, intrinsic_height) = match input_type.as_str() {
"checkbox" | "radio" => (13.0, 13.0),
"range" => (129.0, 16.0),
"color" => (50.0, 27.0),
"file" => (253.0, default_height.max(22.0)),
"submit" | "reset" | "button" => {
let fallback = match input_type.as_str() {
"reset" => "Reset",
"button" => "",
_ => "Submit Query",
};
let label = node.get_attribute("value").unwrap_or(fallback);
(
(label.chars().count() as f32 * font_size * 0.55
+ font_size * 1.5
+ horizontal_edges)
.max(20.0),
default_height,
)
}
"image" => (horizontal_edges, vertical_edges),
_ => {
let size = node
.get_attribute("size")
.and_then(|value| value.parse::<u32>().ok())
.filter(|&value| value > 0)
.unwrap_or(20) as f32;
(
size * font_size * 0.6
+ font_size * 0.675
+ horizontal_edges,
default_height,
)
}
};
if style.width == crate::Dimension::Auto {
let declared_width = if style.box_sizing == crate::BoxSizing::ContentBox {
(intrinsic_width - horizontal_edges).max(0.0)
} else {
intrinsic_width
};
style.width = crate::Dimension::Px(declared_width);
}
if style.height == crate::Dimension::Auto {
let declared_height = if style.box_sizing == crate::BoxSizing::ContentBox {
(intrinsic_height - vertical_edges).max(0.0)
} else {
intrinsic_height
};
style.height = crate::Dimension::Px(declared_height);
}
}
resolve_grid_areas(tree, root_id, &mut styles);
// Blockify grid items (CSS Display 3): a direct child of a grid
+26
View File
@@ -107,6 +107,32 @@ pub fn ua_style(tag: &str) -> LayoutStyle {
} else if tag == "a" {
style.color = Some([0, 0, 238, 255]); // blue
style.underline = Some(true); // UA default: links are underlined
} else if tag == "input" {
// Native text controls are atomic inline-level boxes with their own
// platform font and intrinsic border-box dimensions; they do not
// inherit the page's font shorthand by default. The declared CSS box
// remains content-box in standards mode and switches in quirks mode.
// Size-dependent geometry is resolved after cascading, once the input
// type and `size` attribute are available (dom::layout_dom).
style.display = Display::Inline;
style.is_inline_block = true;
style.font_size = Some(13.333_333);
style.font_family = Some("arial".to_string());
style.line_height = Some(crate::LineHeight::Normal);
style.padding = Edges {
top: 1.0,
right: 2.0,
bottom: 1.0,
left: 2.0,
};
style.border = Edges {
top: 2.0,
right: 2.0,
bottom: 2.0,
left: 2.0,
};
style.border_color = Some([118, 118, 118, 255]);
style.background_color = Some([255, 255, 255, 255]);
} else if tag == "table" || tag == "tbody" {
style.display = Display::Flex;
style.internal_flex_container = true;