Skip to content
Merged
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
14 changes: 14 additions & 0 deletions Libraries/LibWeb/CSS/StyleComputer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,20 @@ GC::Ptr<DOM::Element> 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);
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/CSS/StyleComputer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<DOM::Element> 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
Expand Down
5 changes: 5 additions & 0 deletions Libraries/LibWeb/CSS/StyleEngineBridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ Vector<StyleNodeID> StyleEngine::take_deferred_element_initial_features()
return nodes;
}

HashTable<StyleNodeID> 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);
Expand Down
14 changes: 12 additions & 2 deletions Libraries/LibWeb/CSS/StyleEngineBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<StyleNodeID> 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<StyleNodeID> take_deferred_element_initial_features();
HashTable<StyleNodeID> take_elements_awaiting_first_style_computation();
[[nodiscard]] bool resize_parsed_substitution_cache(u64 bytes);

void set_element_parts(StyleNodeID node, ReadonlySpan<StyleAtomID> names, ReadonlySpan<StyleNodeID> hosts);
Expand Down Expand Up @@ -223,6 +232,7 @@ class WEB_API StyleEngine {

HashTable<FlatPtr> m_atoms;
HashTable<StyleNodeID> m_nodes_with_pending_initial_features;
HashTable<StyleNodeID> m_nodes_awaiting_first_style_computation;
size_t m_element_match_capacity { 64 };

u32 m_declaration_block_version { 1 };
Expand Down
4 changes: 4 additions & 0 deletions Libraries/LibWeb/CSS/UpdateStyle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Libraries/LibWeb/DOM/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTML::HTMLInputElement>(*this) && has_attribute(HTML::AttributeNames::placeholder)) {
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(*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<HTML::HTMLTextAreaElement>(*this) && has_attribute(HTML::AttributeNames::placeholder)) {
Expand Down
3 changes: 3 additions & 0 deletions Libraries/LibWeb/DOM/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,8 @@ class WEB_API Element

virtual void initialize_element() { }

void prepare_for_style_computation(Badge<CSS::StyleComputer>) { prepare_for_style_computation(); }

protected:
Element(Document&, DOM::QualifiedName);

Expand All @@ -791,6 +793,7 @@ class WEB_API Element
MUST_UPCALL virtual void attribute_changed(Utf16FlyString const& local_name, Optional<Utf16String> const& old_value, Optional<Utf16String> const& value, Optional<Utf16FlyString> const& namespace_);

virtual void computed_properties_changed() { }
virtual void prepare_for_style_computation() { }

virtual void visit_edges(Cell::Visitor&) override;

Expand Down
20 changes: 14 additions & 6 deletions Libraries/LibWeb/HTML/HTMLInputElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -799,10 +799,11 @@ WebIDL::ExceptionOr<void> 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();
}

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -1064,7 +1065,7 @@ Utf16String HTMLInputElement::placeholder() const
// https://html.spec.whatwg.org/multipage/input.html#attr-input-placeholder
Optional<Utf16String> 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 {};
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions Libraries/LibWeb/HTML/HTMLInputElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Internals::Internals>) { create_shadow_tree_if_needed(); }

Utf16String placeholder() const;
Optional<Utf16String> placeholder_value() const;

Expand Down Expand Up @@ -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<CSS::StyleProperty>&) const override;
Expand Down
8 changes: 8 additions & 0 deletions Libraries/LibWeb/Internals/Internals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
#include <LibWeb/HTML/EventLoop/TaskQueue.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/HTML/LocalNavigable.h>
#include <LibWeb/HTML/LocalTraversableNavigable.h>
Expand Down Expand Up @@ -972,8 +973,15 @@ GC::Ref<WebIDL::Promise> Internals::flush_session_history_traversal_queue()
return promise;
}

bool Internals::has_shadow_root(GC::Ref<DOM::Element> element)
{
return element->shadow_root() != nullptr;
}

GC::Ptr<DOM::ShadowRoot> Internals::get_shadow_root(GC::Ref<DOM::Element> element)
{
if (auto* input = as_if<HTML::HTMLInputElement>(*element))
input->ensure_user_agent_shadow_tree({});
return element->shadow_root();
}

Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/Internals/Internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<DOM::Element>);
GC::Ptr<DOM::ShadowRoot> get_shadow_root(GC::Ref<DOM::Element>);

void handle_sdl_input_events();
Expand Down
2 changes: 2 additions & 0 deletions Libraries/LibWeb/Internals/Internals.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions Tests/LibWeb/Text/input/DOM/input-shadow-tree-materialization.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
test(() => {
const container = document.createElement("div");
container.innerHTML = '<input type="email">';
const parsedInput = container.firstElementChild;
println(`detached parsed input: ${internals.hasShadowRoot(parsedInput)}`);

const changedInput = document.createElement("input");
changedInput.type = "file";
println(`detached type change: ${internals.hasShadowRoot(changedInput)}`);

const numberInput = document.createElement("input");
numberInput.type = "number";
numberInput.value = "1";
println(`detached number value change: ${internals.hasShadowRoot(numberInput)}`);

const imageContainer = document.createElement("div");
imageContainer.innerHTML = '<input type="image" alt="Parsed image">';
println(`detached parsed image input: ${internals.hasShadowRoot(imageContainer.firstElementChild)}`);

const changedImageInput = document.createElement("input");
changedImageInput.type = "image";
changedImageInput.alt = "Changed image";
println(`detached image alt change: ${internals.hasShadowRoot(changedImageInput)}`);

document.body.appendChild(parsedInput);
getComputedStyle(parsedInput).display;
println(`after style computation: ${internals.hasShadowRoot(parsedInput)}`);

const oldShadowRoot = internals.getShadowRoot(parsedInput);
parsedInput.type = "color";
println(`materialized type change: ${oldShadowRoot !== internals.getShadowRoot(parsedInput)}`);
});
</script>
4 changes: 3 additions & 1 deletion Tests/LibWeb/Text/input/css/style-engine/has-mutations.html
Original file line number Diff line number Diff line change
Expand Up @@ -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: "" } })));

Expand Down
21 changes: 21 additions & 0 deletions Tests/LibWeb/Text/input/input-placeholder-shown-before-style.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<script src="include.js"></script>
<input id="parsed-input" placeholder="Parsed input">
<script>
test(() => {
println(`parsed input: ${document.querySelector("#parsed-input").matches(":placeholder-shown")}`);

const appendedInput = document.createElement("input");
appendedInput.placeholder = "Appended input";
document.body.appendChild(appendedInput);
println(`appended input: ${appendedInput.matches(":placeholder-shown")}`);
println(`shadow before value: ${internals.hasShadowRoot(appendedInput)}`);

appendedInput.value = "value";
println(`non-empty value: ${appendedInput.matches(":placeholder-shown")}`);
println(`shadow after value: ${internals.hasShadowRoot(appendedInput)}`);

appendedInput.value = "";
println(`cleared value: ${appendedInput.matches(":placeholder-shown")}`);
});
</script>
Loading