diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 81adfe53559b1..4d60e1d47412c 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -214,6 +214,20 @@ GC::Ptr StyleComputer::element_for_style_node(StyleNodeID style_no return m_style_nodes[style_node_id.value()]; } +void StyleComputer::prepare_elements_for_style_computation() +{ + for (;;) { + auto elements = m_style_engine.take_elements_awaiting_first_style_computation(); + if (elements.is_empty()) + break; + for (auto style_node : elements) { + auto element = element_for_style_node(style_node); + if (element && element->is_connected()) + element->prepare_for_style_computation({}); + } + } +} + void StyleComputer::visit_edges(Visitor& visitor) { Base::visit_edges(visitor); diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index c7ffba4cb6a43..db56891f34432 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -350,6 +350,7 @@ class WEB_API StyleComputer final : public GC::Cell { void register_style_node(StyleNodeID style_node_id, DOM::Element&); void unregister_style_node(StyleNodeID style_node_id); [[nodiscard]] GC::Ptr element_for_style_node(StyleNodeID style_node_id) const; + void prepare_elements_for_style_computation(); // Style scopes are numbered per document, with zero naming the document's own scope. A scope is // never reused, so a sheet detached with an identity that has been retired detaches nothing diff --git a/Libraries/LibWeb/CSS/StyleEngineBridge.cpp b/Libraries/LibWeb/CSS/StyleEngineBridge.cpp index 99acc5b351272..0a19eecc8639a 100644 --- a/Libraries/LibWeb/CSS/StyleEngineBridge.cpp +++ b/Libraries/LibWeb/CSS/StyleEngineBridge.cpp @@ -72,6 +72,11 @@ Vector StyleEngine::take_deferred_element_initial_features() return nodes; } +HashTable StyleEngine::take_elements_awaiting_first_style_computation() +{ + return move(m_nodes_awaiting_first_style_computation); +} + bool StyleEngine::resize_parsed_substitution_cache(u64 bytes) { return StyleEngineFFI::style_engine_resize_parsed_substitution_cache(m_impl, bytes); diff --git a/Libraries/LibWeb/CSS/StyleEngineBridge.h b/Libraries/LibWeb/CSS/StyleEngineBridge.h index 4b4582edb1913..f6afe79a044eb 100644 --- a/Libraries/LibWeb/CSS/StyleEngineBridge.h +++ b/Libraries/LibWeb/CSS/StyleEngineBridge.h @@ -50,10 +50,19 @@ class WEB_API StyleEngine { // Identity 0 is never returned; it means "no node". StyleNodeID allocate_style_node(); void allocate_style_nodes(Span nodes); - void defer_element_initial_features(StyleNodeID style_node) { m_nodes_with_pending_initial_features.set(style_node); } - void cancel_deferred_element_initial_features(StyleNodeID style_node) { m_nodes_with_pending_initial_features.remove(style_node); } + void defer_element_initial_features(StyleNodeID style_node) + { + m_nodes_with_pending_initial_features.set(style_node); + m_nodes_awaiting_first_style_computation.set(style_node); + } + void cancel_deferred_element_initial_features(StyleNodeID style_node) + { + m_nodes_with_pending_initial_features.remove(style_node); + m_nodes_awaiting_first_style_computation.remove(style_node); + } [[nodiscard]] bool has_deferred_element_initial_features(StyleNodeID style_node) const { return m_nodes_with_pending_initial_features.contains(style_node); } Vector take_deferred_element_initial_features(); + HashTable take_elements_awaiting_first_style_computation(); [[nodiscard]] bool resize_parsed_substitution_cache(u64 bytes); void set_element_parts(StyleNodeID node, ReadonlySpan names, ReadonlySpan hosts); @@ -223,6 +232,7 @@ class WEB_API StyleEngine { HashTable m_atoms; HashTable m_nodes_with_pending_initial_features; + HashTable m_nodes_awaiting_first_style_computation; size_t m_element_match_capacity { 64 }; u32 m_declaration_block_version { 1 }; diff --git a/Libraries/LibWeb/CSS/UpdateStyle.cpp b/Libraries/LibWeb/CSS/UpdateStyle.cpp index d3ab026576977..45730effebda3 100644 --- a/Libraries/LibWeb/CSS/UpdateStyle.cpp +++ b/Libraries/LibWeb/CSS/UpdateStyle.cpp @@ -454,6 +454,10 @@ static void update_style(DOM::Document& document) // Fetch the viewport rect once, instead of repeatedly, during style computation. document.update_style_computer_viewport_rect(); + // An element may have rendering-only descendants that must join the transaction which first styles it. Prepare + // those descendants before selector inputs cross the transaction boundary. + document.style_computer().prepare_elements_for_style_computation(); + // Media rules are evaluated before the transaction boundary below, because evaluating them is // itself a source of inputs: a rule that starts or stops applying publishes its activation. A // transaction taken ahead of that would leave those inputs for the next flush, so the flush that made diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index c5c5dc8344eea..a745b6c829d78 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -2690,7 +2690,7 @@ bool Element::matches_placeholder_shown_pseudo_class() const // - input elements that have a placeholder attribute whose value is currently being presented to the user. if (is(*this) && has_attribute(HTML::AttributeNames::placeholder)) { auto const& input_element = static_cast(*this); - return input_element.placeholder_element() && input_element.placeholder_value().has_value(); + return input_element.placeholder_value().has_value(); } // - textarea elements that have a placeholder attribute whose value is currently being presented to the user. if (is(*this) && has_attribute(HTML::AttributeNames::placeholder)) { diff --git a/Libraries/LibWeb/DOM/Element.h b/Libraries/LibWeb/DOM/Element.h index e99790019e7f7..fb7f66f779112 100644 --- a/Libraries/LibWeb/DOM/Element.h +++ b/Libraries/LibWeb/DOM/Element.h @@ -777,6 +777,8 @@ class WEB_API Element virtual void initialize_element() { } + void prepare_for_style_computation(Badge) { prepare_for_style_computation(); } + protected: Element(Document&, DOM::QualifiedName); @@ -791,6 +793,7 @@ class WEB_API Element MUST_UPCALL virtual void attribute_changed(Utf16FlyString const& local_name, Optional const& old_value, Optional const& value, Optional const& namespace_); virtual void computed_properties_changed() { } + virtual void prepare_for_style_computation() { } virtual void visit_edges(Cell::Visitor&) override; diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 68118da106d9a..db21ad96e2a06 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -799,10 +799,11 @@ WebIDL::ExceptionOr HTMLInputElement::set_value(Utf16View value) if (m_text_node) { m_text_node->set_data(m_value); update_placeholder_visibility(); - - set_the_selection_range(m_text_node->length(), m_text_node->length()); } + if (selection_or_range_applies()) + set_the_selection_range(m_value.length_in_code_units(), m_value.length_in_code_units()); + update_shadow_tree(); } @@ -963,7 +964,7 @@ void HTMLInputElement::update_text_input_shadow_tree() { update_placeholder_visibility(); - if (m_type == TypeAttributeState::Number) { + if (m_type == TypeAttributeState::Number && m_up_button_element && m_down_button_element) { // The `textfield` appearance is used to hide the stepper buttons. if (auto style = computed_style(); style && style->appearance() == CSS::Appearance::Textfield) { m_up_button_element->set_inline_style(stepper_button_style_when_hidden()); @@ -1064,7 +1065,7 @@ Utf16String HTMLInputElement::placeholder() const // https://html.spec.whatwg.org/multipage/input.html#attr-input-placeholder Optional HTMLInputElement::placeholder_value() const { - if (!m_text_node || !m_text_node->data().is_empty()) + if (!relevant_value().is_empty()) return {}; if (!is_allowed_to_have_placeholder(type_state())) return {}; @@ -1175,6 +1176,9 @@ void HTMLInputElement::remove_image_button_alt_text_shadow_tree() void HTMLInputElement::update_image_button_alt_text_shadow_tree() { + if (!shadow_root() && !has_style()) + return; + auto alt_text = get_attribute_value(HTML::AttributeNames::alt); if (type_state() != TypeAttributeState::ImageButton || !renders_as_alt_text() || alt_text.is_empty()) { remove_image_button_alt_text_shadow_tree(); @@ -1745,9 +1749,11 @@ void HTMLInputElement::type_attribute_changed(TypeAttributeState old_state, Type CSS::Invalidation::invalidate_style_after_default_state_change(*this, was_default); CSS::Invalidation::invalidate_style_after_read_write_state_change(*this, was_read_write); clear_element_reference_pseudo_elements(); + auto should_materialize_shadow_tree = shadow_root() || has_style(); set_shadow_root(nullptr); m_image_button_alt_text_node = nullptr; - create_shadow_tree_if_needed(); + if (should_materialize_shadow_tree) + create_shadow_tree_if_needed(); // 5. Signal a type change for the element. (The Radio Button state uses this, in particular.) signal_a_type_change(); @@ -2164,7 +2170,9 @@ void HTMLInputElement::clear_algorithm() void HTMLInputElement::form_associated_element_was_inserted() { - create_shadow_tree_if_needed(); + // NB: The user-agent shadow tree is rendering state. It is created when a connected control first participates in + // a style update, before computed properties are assigned. Creating it in the insertion steps would also + // materialize controls in detached and short-lived trees. if (is_connected()) { // https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio) diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.h b/Libraries/LibWeb/HTML/HTMLInputElement.h index c1737d2b8f9c8..04c7ae5005da0 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -106,6 +106,8 @@ class WEB_API HTMLInputElement final void commit_pending_changes(); bool has_uncommitted_changes() { return m_has_uncommitted_changes; } + void ensure_user_agent_shadow_tree(Badge) { create_shadow_tree_if_needed(); } + Utf16String placeholder() const; Optional placeholder_value() const; @@ -281,6 +283,7 @@ class WEB_API HTMLInputElement final void type_attribute_changed(TypeAttributeState old_state, TypeAttributeState new_state); virtual void computed_properties_changed() override; + virtual void prepare_for_style_computation() override { create_shadow_tree_if_needed(); } virtual bool is_presentational_hint(Utf16FlyString const&) const override; virtual void apply_presentational_hints(Vector&) const override; diff --git a/Libraries/LibWeb/Internals/Internals.cpp b/Libraries/LibWeb/Internals/Internals.cpp index d221019b6e8de..6d68b28ec3b6d 100644 --- a/Libraries/LibWeb/Internals/Internals.cpp +++ b/Libraries/LibWeb/Internals/Internals.cpp @@ -62,6 +62,7 @@ #include #include #include +#include #include #include #include @@ -972,8 +973,15 @@ GC::Ref Internals::flush_session_history_traversal_queue() return promise; } +bool Internals::has_shadow_root(GC::Ref element) +{ + return element->shadow_root() != nullptr; +} + GC::Ptr Internals::get_shadow_root(GC::Ref element) { + if (auto* input = as_if(*element)) + input->ensure_user_agent_shadow_tree({}); return element->shadow_root(); } diff --git a/Libraries/LibWeb/Internals/Internals.h b/Libraries/LibWeb/Internals/Internals.h index b2df9d5c9e051..c30963daed54d 100644 --- a/Libraries/LibWeb/Internals/Internals.h +++ b/Libraries/LibWeb/Internals/Internals.h @@ -148,6 +148,7 @@ class WEB_API Internals final : public InternalsBase { bool has_html_parser_end_state(DOM::Document& document) { return document.has_html_parser_end_state(); } void clobber_next_navigation_with_a_traversal(); + bool has_shadow_root(GC::Ref); GC::Ptr get_shadow_root(GC::Ref); void handle_sdl_input_events(); diff --git a/Libraries/LibWeb/Internals/Internals.idl b/Libraries/LibWeb/Internals/Internals.idl index e2634eadcac82..71c463a007ed4 100644 --- a/Libraries/LibWeb/Internals/Internals.idl +++ b/Libraries/LibWeb/Internals/Internals.idl @@ -129,6 +129,8 @@ interface Internals { // re-stamping the next navigation's ongoing navigation with a traversal during its unload check. undefined clobberNextNavigationWithATraversal(); + boolean hasShadowRoot(Element element); + // Returns the shadow root of the element, if it has one, even if it's not normally accessible to JS. ShadowRoot? getShadowRoot(Element element); diff --git a/Tests/LibWeb/Text/expected/DOM/input-shadow-tree-materialization.txt b/Tests/LibWeb/Text/expected/DOM/input-shadow-tree-materialization.txt new file mode 100644 index 0000000000000..42457b478154a --- /dev/null +++ b/Tests/LibWeb/Text/expected/DOM/input-shadow-tree-materialization.txt @@ -0,0 +1,7 @@ +detached parsed input: false +detached type change: false +detached number value change: false +detached parsed image input: false +detached image alt change: false +after style computation: true +materialized type change: true diff --git a/Tests/LibWeb/Text/expected/css/style-engine/has-mutations.txt b/Tests/LibWeb/Text/expected/css/style-engine/has-mutations.txt index 0eb3f294d18df..80bd7ec2f8473 100644 --- a/Tests/LibWeb/Text/expected/css/style-engine/has-mutations.txt +++ b/Tests/LibWeb/Text/expected/css/style-engine/has-mutations.txt @@ -20,5 +20,5 @@ the first alternative of a listed argument: [listed-alternatives, listed-hit] the second alternative of a listed argument: [listed-alternatives-2, listed-hit-2] an argument that names no fact at all: [anything, anything-child] an argument that names a position: [positional-last] -an argument that names a constraint: [constrained, constrained-input, div, div, div] +an argument that names a constraint: [constrained, constrained-input] a sibling seam closing over a departure: [seam-after] diff --git a/Tests/LibWeb/Text/expected/input-placeholder-shown-before-style.txt b/Tests/LibWeb/Text/expected/input-placeholder-shown-before-style.txt new file mode 100644 index 0000000000000..8d63d27c2c5ca --- /dev/null +++ b/Tests/LibWeb/Text/expected/input-placeholder-shown-before-style.txt @@ -0,0 +1,6 @@ +parsed input: true +appended input: true +shadow before value: false +non-empty value: false +shadow after value: false +cleared value: true diff --git a/Tests/LibWeb/Text/input/DOM/input-shadow-tree-materialization.html b/Tests/LibWeb/Text/input/DOM/input-shadow-tree-materialization.html new file mode 100644 index 0000000000000..e096604d1fc88 --- /dev/null +++ b/Tests/LibWeb/Text/input/DOM/input-shadow-tree-materialization.html @@ -0,0 +1,36 @@ + + + diff --git a/Tests/LibWeb/Text/input/css/style-engine/has-mutations.html b/Tests/LibWeb/Text/input/css/style-engine/has-mutations.html index afce26fb913df..756957b789cbb 100644 --- a/Tests/LibWeb/Text/input/css/style-engine/has-mutations.html +++ b/Tests/LibWeb/Text/input/css/style-engine/has-mutations.html @@ -124,7 +124,9 @@ plan("an argument that names a position", () => document.getElementById("positional").appendChild(make("span", "positional-last"))); - // A control arrives already required, which no attribute the engine indexes says. + // This control is already required before the mutation, so no indexed attribute mutation represents that + // initial state. Rendering-only user-agent descendants remain unmaterialized until style is computed, so this + // mutation publishes only author nodes. plan("an argument that names a constraint", () => document.getElementById("constrained").appendChild(make("input", "constrained-input", { attributes: { required: "" } }))); diff --git a/Tests/LibWeb/Text/input/input-placeholder-shown-before-style.html b/Tests/LibWeb/Text/input/input-placeholder-shown-before-style.html new file mode 100644 index 0000000000000..9b14a3f3e2058 --- /dev/null +++ b/Tests/LibWeb/Text/input/input-placeholder-shown-before-style.html @@ -0,0 +1,21 @@ + + + +