diff --git a/bridge/bindings/qjs/converter_impl.h b/bridge/bindings/qjs/converter_impl.h index 94a1cd9912..840a0486b7 100644 --- a/bridge/bindings/qjs/converter_impl.h +++ b/bridge/bindings/qjs/converter_impl.h @@ -35,6 +35,7 @@ #include "core/css/computed_css_style_declaration.h" #include "core/css/legacy/legacy_computed_css_style_declaration.h" +#include "foundation/utility/make_visitor.h" namespace webf { @@ -637,56 +638,6 @@ struct Converter -struct Converter { - using ImplType = ElementStyle; - - static ElementStyle FromValue(JSContext* ctx, JSValue value, ExceptionState& exception_state) { - auto ectx = ExecutingContext::From(ctx); - if (ectx->isBlinkEnabled()) { - if (JS_IsNull(value)) { - return static_cast(nullptr); - } - - return Converter::FromValue(ctx, value, exception_state); - } else { - if (JS_IsNull(value)) { - return static_cast(nullptr); - } - - return Converter::FromValue(ctx, value, exception_state); - } - } - - static ElementStyle ArgumentsValue(ExecutingContext* context, - JSValue value, - uint32_t argv_index, - ExceptionState& exception_state) { - if (context->isBlinkEnabled()) { - if (JS_IsNull(value)) { - return static_cast(nullptr); - } - - return Converter::ArgumentsValue(context, value, argv_index, exception_state); - } else { - if (JS_IsNull(value)) { - return static_cast(nullptr); - } - - return Converter::ArgumentsValue(context, value, argv_index, exception_state); - } - } - - static JSValue ToValue(JSContext* ctx, ElementStyle value) { - return std::visit(MakeVisitor([&ctx](auto* style) { - if (style == nullptr) - return JS_NULL; - return Converter>>::ToValue(ctx, style); - }), - value); - } -}; - template <> struct Converter { using ImplType = WindowComputedStyle; diff --git a/bridge/core/api/element.cc b/bridge/core/api/element.cc index ca51e175e3..0dc5f62c82 100644 --- a/bridge/core/api/element.cc +++ b/bridge/core/api/element.cc @@ -11,24 +11,20 @@ #include "core/css/legacy/legacy_inline_css_style_declaration.h" #include "core/dom/container_node.h" #include "core/dom/element.h" -#include "foundation/utility/make_visitor.h" namespace webf { WebFValue ElementPublicMethods::Style(Element* ptr) { auto* element = static_cast(ptr); MemberMutationScope member_mutation_scope{element->GetExecutingContext()}; - auto style = element->style(); + auto* style_declaration = element->style(); + if (!style_declaration) { + return WebFValue::Null(); + } - return std::visit( - MakeVisitor( - [&](legacy::LegacyInlineCssStyleDeclaration* styleDeclaration) { - WebFValueStatus* status_block = styleDeclaration->KeepAlive(); - return WebFValue( - styleDeclaration, styleDeclaration->legacyCssStyleDeclarationPublicMethods(), status_block); - }, - [](auto&&) { return WebFValue::Null(); }), - style); + WebFValueStatus* status_block = style_declaration->KeepAlive(); + return WebFValue( + style_declaration, style_declaration->legacyCssStyleDeclarationPublicMethods(), status_block); } void ElementPublicMethods::ToBlob(Element* ptr, diff --git a/bridge/core/css/blink_inline_style_validation_test.cc b/bridge/core/css/blink_inline_style_validation_test.cc index 20f06a29bc..eed4660d13 100644 --- a/bridge/core/css/blink_inline_style_validation_test.cc +++ b/bridge/core/css/blink_inline_style_validation_test.cc @@ -26,7 +26,7 @@ std::string CommandArg01ToUTF8(const UICommandItem& item) { return String(utf16, static_cast(item.args_01_length)).ToUTF8String(); } -std::string SharedNativeStringToUTF8(const SharedNativeString* s) { +std::string SharedNativeStringToUTF8(const webf::SharedNativeString* s) { if (!s || !s->string() || s->length() == 0) { return ""; } @@ -53,7 +53,7 @@ bool HasSetStyleWithKeyValue(ExecutingContext* context, const std::string& key, auto* items = static_cast(pack->data); for (int64_t i = 0; i < pack->length; ++i) { const UICommandItem& item = items[i]; - if (item.type == static_cast(UICommand::kSetStyle)) { + if (item.type == static_cast(UICommand::kSetInlineStyle)) { if (CommandArg01ToUTF8(item) != key) { continue; } @@ -79,34 +79,7 @@ bool HasSetStyleWithKeyValue(ExecutingContext* context, const std::string& key, if (item.string_01 < 0) { value_text = getValueName(static_cast(-item.string_01 - 1)); } else { - auto* value_ptr = reinterpret_cast(static_cast(item.string_01)); - value_text = SharedNativeStringToUTF8(value_ptr); - } - if (value_text == value) { - return true; - } - } - return false; -} - -bool HasSetStyleByIdWithKeyValue(ExecutingContext* context, const std::string& key, const std::string& value) { - const CSSPropertyID expected_property_id = CssPropertyID(context, ConvertCamelCaseToKebabCase(key)); - auto* pack = static_cast(context->uiCommandBuffer()->data()); - auto* items = static_cast(pack->data); - for (int64_t i = 0; i < pack->length; ++i) { - const UICommandItem& item = items[i]; - if (item.type != static_cast(UICommand::kSetStyleById)) { - continue; - } - if (item.args_01_length != static_cast(expected_property_id)) { - continue; - } - - std::string value_text; - if (item.string_01 < 0) { - value_text = getValueName(static_cast(-item.string_01 - 1)); - } else { - auto* value_ptr = reinterpret_cast(static_cast(item.string_01)); + auto* value_ptr = reinterpret_cast(static_cast(item.string_01)); value_text = SharedNativeStringToUTF8(value_ptr); } if (value_text == value) { @@ -118,7 +91,7 @@ bool HasSetStyleByIdWithKeyValue(ExecutingContext* context, const std::string& k } // namespace -TEST(BlinkCSSStyleDeclarationValidation, RejectsInvalidFontSize) { +TEST(BlinkCSSStyleDeclarationValidation, ForwardsInvalidFontSizeToDart) { bool static errorCalled = false; webf::WebFPage::consoleMessageHandler = [](void*, const std::string&, int) {}; @@ -137,15 +110,15 @@ TEST(BlinkCSSStyleDeclarationValidation, RejectsInvalidFontSize) { const char* set_valid = "document.body.style.fontSize = '18px';"; env->page()->evaluateScript(set_valid, strlen(set_valid), "vm://", 0); TEST_runLoop(context); - EXPECT_TRUE(HasSetStyleByIdWithKeyValue(context, "fontSize", "18px")); + EXPECT_TRUE(HasSetStyleWithKeyValue(context, "fontSize", "18px")); context->uiCommandBuffer()->clear(); - // Invalid font-size should be rejected on the native (Blink) CSS side and - // thus should not be forwarded to Dart. + // Inline style is legacy-only even when Blink CSS is enabled; invalid values + // are forwarded to Dart for validation/handling. const char* set_invalid = "document.body.style.fontSize = '-1px';"; env->page()->evaluateScript(set_invalid, strlen(set_invalid), "vm://", 0); TEST_runLoop(context); - EXPECT_FALSE(HasSetStyleByIdWithKeyValue(context, "fontSize", "-1px")); + EXPECT_TRUE(HasSetStyleWithKeyValue(context, "fontSize", "-1px")); EXPECT_EQ(errorCalled, false); } diff --git a/bridge/core/css/css_properties.json5 b/bridge/core/css/css_properties.json5 index 6b7a0747f8..c3326dad34 100644 --- a/bridge/core/css/css_properties.json5 +++ b/bridge/core/css/css_properties.json5 @@ -1103,7 +1103,7 @@ }, { name: "list-style-type", - // Parsing and initial value only – forwarded to Dart via UICommand kSetStyle + // Parsing and initial value only – forwarded to Dart via UICommand kSetInlineStyle // We don't currently store this on ComputedStyle; values are emitted from the inline // property set during style resolution. field_template: "keyword", @@ -1151,7 +1151,7 @@ // CSS Counters { name: "counter-reset", - // Parsing and initial value only – forwarded to Dart via UICommand kSetStyle + // Parsing and initial value only – forwarded to Dart via UICommand kSetInlineStyle property_methods: [ "ParseSingleValue", "InitialValue" diff --git a/bridge/core/css/inline_css_style_declaration_test.cc b/bridge/core/css/inline_css_style_declaration_test.cc index eb2c403f0f..8011cb48e1 100644 --- a/bridge/core/css/inline_css_style_declaration_test.cc +++ b/bridge/core/css/inline_css_style_declaration_test.cc @@ -57,7 +57,7 @@ document.body.style.setProperty('--main-color', 'lightblue'); console.assert(doc UICommandItem& last = ((UICommandItem*)p_buffer_pack->data)[commandSize - 1]; - EXPECT_EQ(last.type, (int32_t)UICommand::kSetStyle); + EXPECT_EQ(last.type, (int32_t)UICommand::kSetInlineStyle); uint16_t* last_key = (uint16_t*)last.string_01; // auto native_str = new webf::SharedNativeString(last_key, last.args_01_length); @@ -101,4 +101,4 @@ TEST(InlineCSSStyleDeclaration, setNullValue) { "console.assert(document.body.style.height === '')"; env->page()->evaluateScript(code, strlen(code), "vm://", 0); EXPECT_EQ(errorCalled, false); -} \ No newline at end of file +} diff --git a/bridge/core/css/legacy/legacy_inline_css_style_declaration.cc b/bridge/core/css/legacy/legacy_inline_css_style_declaration.cc index 08c892f636..8a42ced75e 100644 --- a/bridge/core/css/legacy/legacy_inline_css_style_declaration.cc +++ b/bridge/core/css/legacy/legacy_inline_css_style_declaration.cc @@ -4,6 +4,9 @@ */ #include "legacy_inline_css_style_declaration.h" #include "plugin_api/legacy_inline_css_style_declaration.h" +#include +#include +#include #include #include "core/dom/mutation_observer_interest_group.h" #include "core/executing_context.h" @@ -14,6 +17,7 @@ #include "core/css/css_property_value_set.h" #include "core/dom/element.h" #include "foundation/string/string_builder.h" +#include "foundation/string/string_view.h" namespace webf { namespace legacy { @@ -59,14 +63,16 @@ static std::string parseJavaScriptCSSPropertyName(std::string& propertyName) { return result; } -static std::string convertCamelCaseToKebabCase(const std::string& propertyName) { +static const std::string& convertCamelCaseToKebabCase(const std::string& propertyName) { static std::unordered_map propertyCache{}; - if (propertyCache.count(propertyName) > 0) { - return propertyCache[propertyName]; + auto it = propertyCache.find(propertyName); + if (it != propertyCache.end()) { + return it->second; } std::string result; + result.reserve(propertyName.size()); for (char c : propertyName) { if (std::isupper(c)) { result += '-'; @@ -76,8 +82,71 @@ static std::string convertCamelCaseToKebabCase(const std::string& propertyName) } } - propertyCache[propertyName] = result; - return result; + auto inserted = propertyCache.emplace(propertyName, std::move(result)); + return inserted.first->second; +} + +enum class PriorityParseResult { + kNone, + kImportant, + kInvalid, +}; + +static PriorityParseResult ParsePriority(const AtomicString& priority) { + if (priority.IsNull() || priority.empty()) { + return PriorityParseResult::kNone; + } + + std::string raw = priority.ToUTF8String(); + size_t start = 0; + size_t end = raw.size(); + while (start < end && std::isspace(static_cast(raw[start]))) { + start++; + } + while (end > start && std::isspace(static_cast(raw[end - 1]))) { + end--; + } + if (start == end) { + return PriorityParseResult::kNone; + } + + std::string_view trimmed(raw.data() + start, end - start); + if (EqualIgnoringASCIICase(trimmed, "important")) { + return PriorityParseResult::kImportant; + } + return PriorityParseResult::kInvalid; +} + +static std::pair ParseValueAndImportant(const std::string& raw) { + size_t end = raw.size(); + while (end > 0 && std::isspace(static_cast(raw[end - 1]))) { + end--; + } + + constexpr std::string_view kKeyword = "important"; + if (end < kKeyword.size()) { + return {raw, false}; + } + + size_t keyword_start = end - kKeyword.size(); + std::string_view tail(raw.data() + keyword_start, kKeyword.size()); + if (!EqualIgnoringASCIICase(tail, kKeyword)) { + return {raw, false}; + } + + size_t i = keyword_start; + while (i > 0 && std::isspace(static_cast(raw[i - 1]))) { + i--; + } + if (i == 0 || raw[i - 1] != '!') { + return {raw, false}; + } + + size_t value_end = i - 1; + while (value_end > 0 && std::isspace(static_cast(raw[value_end - 1]))) { + value_end--; + } + return {raw.substr(0, value_end), true}; } LegacyInlineCssStyleDeclaration* LegacyInlineCssStyleDeclaration::Create(ExecutingContext* context, @@ -90,7 +159,7 @@ LegacyInlineCssStyleDeclaration::LegacyInlineCssStyleDeclaration(Element* owner_ : LegacyCssStyleDeclaration(owner_element_->ctx(), nullptr), owner_element_(owner_element_) {} ScriptValue LegacyInlineCssStyleDeclaration::item(const AtomicString& key, ExceptionState& exception_state) { - if (webf::IsPrototypeMethods(key)) { + if (IsPrototypeMethods(key)) { return ScriptValue::Undefined(ctx()); } @@ -133,17 +202,26 @@ ScriptValue LegacyInlineCssStyleDeclaration::item(const AtomicString& key, Excep bool LegacyInlineCssStyleDeclaration::SetItem(const AtomicString& key, const ScriptValue& value, ExceptionState& exception_state) { - if (webf::IsPrototypeMethods(key)) { + if (IsPrototypeMethods(key)) { return false; } std::string propertyName = key.ToUTF8String(); + AtomicString value_string = value.ToLegacyDOMString(ctx()); + + // CSSOM property assignment does not accept `!important` inside the value + // string. Use setProperty(..., "important") instead. + auto [_, contains_important_suffix] = ParseValueAndImportant(value_string.ToUTF8String()); + if (contains_important_suffix) { + return true; + } + AtomicString old_style = cssText(); - bool success = InternalSetProperty(propertyName, value.ToLegacyDOMString(ctx())); - if (success) { + const bool changed = InternalSetProperty(propertyName, value_string, AtomicString::Empty()); + if (changed) { InlineStyleChanged(old_style); } - return success; + return true; } bool LegacyInlineCssStyleDeclaration::DeleteItem(const webf::AtomicString& key, webf::ExceptionState& exception_state) { @@ -172,9 +250,17 @@ void LegacyInlineCssStyleDeclaration::setProperty(const AtomicString& key, const AtomicString& priority, ExceptionState& exception_state) { std::string propertyName = key.ToUTF8String(); + AtomicString value_string = value.ToLegacyDOMString(ctx()); + + // setProperty takes priority separately; `!important` is not allowed inside |value|. + auto [_, contains_important_suffix] = ParseValueAndImportant(value_string.ToUTF8String()); + if (contains_important_suffix) { + return; + } + AtomicString old_style = cssText(); - bool success = InternalSetProperty(propertyName, value.ToLegacyDOMString(ctx())); - if (success) { + const bool changed = InternalSetProperty(propertyName, value_string, priority); + if (changed) { InlineStyleChanged(old_style); } } @@ -194,19 +280,33 @@ void LegacyInlineCssStyleDeclaration::CopyWith(LegacyInlineCssStyleDeclaration* for (auto& attr : inline_style->properties_) { properties_[attr.first] = attr.second; } + important_properties_ = inline_style->important_properties_; } AtomicString LegacyInlineCssStyleDeclaration::cssText() const { - std::string result; - size_t index = 0; + if (properties_.empty()) { + return AtomicString::Empty(); + } + + StringBuilder builder; + bool first = true; for (auto& attr : properties_) { - result += convertCamelCaseToKebabCase(attr.first) + ": " + attr.second.ToUTF8String() + ";"; - index++; - if (index < properties_.size()) { - result += " "; + if (!first) { + builder.Append(' '); } + first = false; + + const std::string& kebab_name = convertCamelCaseToKebabCase(attr.first); + builder.Append(StringView(kebab_name.c_str(), kebab_name.size())); + builder.Append(": "_s); + builder.Append(attr.second); + if (important_properties_.count(attr.first) > 0) { + builder.Append(" !important"_s); + } + builder.Append(";"_s); } - return AtomicString(result); + + return builder.ToAtomicString(); } void LegacyInlineCssStyleDeclaration::setCssText(const webf::AtomicString& value, webf::ExceptionState& exception_state) { @@ -216,6 +316,7 @@ void LegacyInlineCssStyleDeclaration::setCssText(const webf::AtomicString& value } void LegacyInlineCssStyleDeclaration::SetCSSTextInternal(const AtomicString& value) { + static const AtomicString kImportantPriority = AtomicString::CreateFromUTF8("important"); const std::string css_text = value.ToUTF8String(); InternalClearProperty(); @@ -235,7 +336,15 @@ void LegacyInlineCssStyleDeclaration::SetCSSTextInternal(const AtomicString& val css_key = trim(css_key); std::string css_value = s.substr(position + 1, s.length()); css_value = trim(css_value); - InternalSetProperty(css_key, AtomicString(css_value)); + bool important = false; + auto [stripped_value, important_from_value] = ParseValueAndImportant(css_value); + + if (important_from_value) { + important = true; + css_value = stripped_value; + } + + InternalSetProperty(css_key, AtomicString(css_value), important ? kImportantPriority : AtomicString::Empty()); } } } @@ -254,6 +363,9 @@ String LegacyInlineCssStyleDeclaration::ToString() const { builder.Append(attr.first); builder.Append(": "_s); builder.Append(attr.second); + if (important_properties_.count(attr.first) > 0) { + builder.Append(" !important"_s); + } builder.Append(";"_s); } @@ -294,21 +406,54 @@ AtomicString LegacyInlineCssStyleDeclaration::InternalGetPropertyValue(std::stri return g_empty_atom; } -bool LegacyInlineCssStyleDeclaration::InternalSetProperty(std::string& name, const AtomicString& value) { +bool LegacyInlineCssStyleDeclaration::InternalSetProperty(std::string& name, + const AtomicString& value, + const AtomicString& priority) { name = parseJavaScriptCSSPropertyName(name); - if (properties_[name] == value) { + + // An empty value removes the property. + if (value.empty()) { + auto it = properties_.find(name); + if (it == properties_.end()) { + important_properties_.erase(name); + return false; + } + + properties_.erase(it); + important_properties_.erase(name); + + std::unique_ptr args_01 = stringToNativeString(name); + GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kSetInlineStyle, std::move(args_01), + owner_element_->bindingObject(), nullptr); + return true; + } + + const PriorityParseResult parsed_priority = ParsePriority(priority); + if (parsed_priority == PriorityParseResult::kInvalid) { return false; } + const bool important = parsed_priority == PriorityParseResult::kImportant; - AtomicString old_value = properties_[name]; + auto it = properties_.find(name); + bool was_important = important_properties_.count(name) > 0; + bool value_unchanged = it != properties_.end() && it->second == value; + if (value_unchanged && was_important == important) { + return false; + } properties_[name] = value; + if (important) { + important_properties_.insert(name); + } else { + important_properties_.erase(name); + } std::unique_ptr args_01 = stringToNativeString(name); auto* payload = reinterpret_cast(dart_malloc(sizeof(NativeStyleValueWithHref))); payload->value = value.ToNativeString().release(); payload->href = nullptr; - GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kSetStyle, std::move(args_01), + payload->important = important ? 1 : 0; + GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kSetInlineStyle, std::move(args_01), owner_element_->bindingObject(), payload); return true; @@ -323,9 +468,10 @@ AtomicString LegacyInlineCssStyleDeclaration::InternalRemoveProperty(std::string AtomicString return_value = properties_[name]; properties_.erase(name); + important_properties_.erase(name); std::unique_ptr args_01 = stringToNativeString(name); - GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kSetStyle, std::move(args_01), + GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kSetInlineStyle, std::move(args_01), owner_element_->bindingObject(), nullptr); return return_value; @@ -335,6 +481,7 @@ void LegacyInlineCssStyleDeclaration::InternalClearProperty() { if (properties_.empty()) return; properties_.clear(); + important_properties_.clear(); GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kClearStyle, nullptr, owner_element_->bindingObject(), nullptr); } diff --git a/bridge/core/css/legacy/legacy_inline_css_style_declaration.h b/bridge/core/css/legacy/legacy_inline_css_style_declaration.h index 502b685333..47bfdd7452 100644 --- a/bridge/core/css/legacy/legacy_inline_css_style_declaration.h +++ b/bridge/core/css/legacy/legacy_inline_css_style_declaration.h @@ -6,6 +6,7 @@ #define BRIDGE_CSS_LEGACY_STYLE_DECLARATION_H #include +#include #include "bindings/qjs/cppgc/member.h" #include "bindings/qjs/exception_state.h" #include "bindings/qjs/script_value.h" @@ -57,10 +58,11 @@ class LegacyInlineCssStyleDeclaration : public LegacyCssStyleDeclaration { private: AtomicString InternalGetPropertyValue(std::string& name); - bool InternalSetProperty(std::string& name, const AtomicString& value); + bool InternalSetProperty(std::string& name, const AtomicString& value, const AtomicString& priority); AtomicString InternalRemoveProperty(std::string& name); void InternalClearProperty(); std::unordered_map properties_; + std::unordered_set important_properties_; Member owner_element_; }; diff --git a/bridge/core/css/legacy/legacy_inline_css_style_declaration_test.cc b/bridge/core/css/legacy/legacy_inline_css_style_declaration_test.cc index 69bcc6aac7..91a7d04c8a 100644 --- a/bridge/core/css/legacy/legacy_inline_css_style_declaration_test.cc +++ b/bridge/core/css/legacy/legacy_inline_css_style_declaration_test.cc @@ -4,6 +4,8 @@ */ #include "gtest/gtest.h" +#include "foundation/native_type.h" +#include "foundation/string/wtf_string.h" #include "webf_test_env.h" using namespace webf; @@ -57,15 +59,53 @@ document.body.style.setProperty('--main-color', 'lightblue'); console.assert(doc UICommandItem& last = ((UICommandItem*)p_buffer_pack->data)[commandSize - 1]; - EXPECT_EQ(last.type, (int32_t)UICommand::kSetStyle); - uint16_t* last_key = (uint16_t*)last.string_01; + EXPECT_EQ(last.type, (int32_t)UICommand::kSetInlineStyle); + const auto* last_key = reinterpret_cast(static_cast(last.string_01)); + EXPECT_EQ(String(last_key, static_cast(last.args_01_length)).ToUTF8String(), "--main-color"); - auto native_str = new webf::SharedNativeString(last_key, last.args_01_length); - EXPECT_STREQ(AtomicString(context->ctx(), - std::unique_ptr(static_cast(native_str))) - .ToStdString(context->ctx()) - .c_str(), - "--main-color"); + EXPECT_EQ(errorCalled, false); +} + +TEST(CSSStyleDeclaration, supportImportantInPayload) { + bool static errorCalled = false; + bool static logCalled = false; + webf::WebFPage::consoleMessageHandler = [](void* ctx, const std::string& message, int logLevel) { logCalled = true; }; + auto env = TEST_init([](double contextId, const char* errmsg) { + WEBF_LOG(VERBOSE) << errmsg; + errorCalled = true; + }); + auto context = env->page()->executingContext(); + const char* code = "document.body.style.setProperty('color', 'red', 'important');"; + env->page()->evaluateScript(code, strlen(code), "vm://", 0); + + UICommandBufferPack* p_buffer_pack = static_cast(context->uiCommandBuffer()->data()); + size_t commandSize = p_buffer_pack->length; + UICommandItem& last = ((UICommandItem*)p_buffer_pack->data)[commandSize - 1]; + + ASSERT_EQ(last.type, (int32_t)UICommand::kSetInlineStyle); + auto* payload = reinterpret_cast(static_cast(last.nativePtr2)); + ASSERT_NE(payload, nullptr); + EXPECT_EQ(payload->important, 1); + + const auto* value_chars = reinterpret_cast(payload->value->string()); + EXPECT_EQ(String(value_chars, static_cast(payload->value->length())).ToUTF8String(), "red"); + + EXPECT_EQ(errorCalled, false); +} + +TEST(CSSStyleDeclaration, assignmentDoesNotAcceptImportantValue) { + bool static errorCalled = false; + bool static logCalled = false; + webf::WebFPage::consoleMessageHandler = [](void* ctx, const std::string& message, int logLevel) { logCalled = true; }; + auto env = TEST_init([](double contextId, const char* errmsg) { + WEBF_LOG(VERBOSE) << errmsg; + errorCalled = true; + }); + auto context = env->page()->executingContext(); + const char* code = + "document.body.style.color = 'red !important';" + "console.assert(document.body.style.color === '');"; + env->page()->evaluateScript(code, strlen(code), "vm://", 0); EXPECT_EQ(errorCalled, false); } @@ -102,4 +142,4 @@ TEST(InlineCSSStyleDeclaration, setNullValue) { "console.assert(document.body.style.height === '')"; env->page()->evaluateScript(code, strlen(code), "vm://", 0); EXPECT_EQ(errorCalled, false); -} \ No newline at end of file +} diff --git a/bridge/core/css/resolver/cascade_map.cc b/bridge/core/css/resolver/cascade_map.cc index d66801c82e..c7a7897e6a 100644 --- a/bridge/core/css/resolver/cascade_map.cc +++ b/bridge/core/css/resolver/cascade_map.cc @@ -198,4 +198,4 @@ void CascadeMap::Reset() { backing_vector_.clear(); } -} // namespace webf \ No newline at end of file +} // namespace webf diff --git a/bridge/core/css/resolver/cascade_map.h b/bridge/core/css/resolver/cascade_map.h index ca77e08b05..df5a1bf085 100644 --- a/bridge/core/css/resolver/cascade_map.h +++ b/bridge/core/css/resolver/cascade_map.h @@ -279,4 +279,4 @@ inline bool CascadeMap::CascadePriorityList::IsEmpty() const { } // namespace webf -#endif // WEBF_CORE_CSS_RESOLVER_CASCADE_MAP_H_ \ No newline at end of file +#endif // WEBF_CORE_CSS_RESOLVER_CASCADE_MAP_H_ diff --git a/bridge/core/css/resolver/style_cascade.cc b/bridge/core/css/resolver/style_cascade.cc index 29991f6758..f68662718b 100644 --- a/bridge/core/css/resolver/style_cascade.cc +++ b/bridge/core/css/resolver/style_cascade.cc @@ -524,10 +524,8 @@ std::shared_ptr StyleCascade::BuildWinningPropertySe const CascadePriority* prio = map_.Find(CSSPropertyName(custom_name)); if (!prio) continue; uint32_t pos = prio->GetPosition(); - const StylePropertySet* set = nullptr; - unsigned idx = 0; CSSPropertyValueSet::PropertyReference prop_ref = CSSPropertyValueSet::PropertyReference(*result, 0); - if (!find_ref_at(pos, &set, &idx, &prop_ref)) { + if (!find_ref_at(pos, nullptr, nullptr, &prop_ref)) { continue; } const std::shared_ptr* value_ptr = prop_ref.Value(); diff --git a/bridge/core/css/resolver/style_resolver.cc b/bridge/core/css/resolver/style_resolver.cc index ccfec32004..7db4a97f0c 100644 --- a/bridge/core/css/resolver/style_resolver.cc +++ b/bridge/core/css/resolver/style_resolver.cc @@ -303,14 +303,8 @@ void StyleResolver::MatchAllRules( // Match author rules MatchAuthorRules(element, 0, collector); - // Match inline style (highest priority) - if (element.IsStyledElement()) { - auto inline_style_set = const_cast(element).EnsureMutableInlineStyle(); - if (inline_style_set && inline_style_set->PropertyCount() > 0) { - collector.AddElementStyleProperties(inline_style_set, - PropertyAllowedInMode::kAll); - } - } + // NOTE: WebF does not participate inline styles in the native (Blink) cascade. + // Inline declarations (style="" / CSSOM) are forwarded to Dart and merged there. } void StyleResolver::MatchUARules(ElementRuleCollector& collector) { diff --git a/bridge/core/css/rule_feature_set.cc b/bridge/core/css/rule_feature_set.cc index b70f07b692..e51df323d0 100644 --- a/bridge/core/css/rule_feature_set.cc +++ b/bridge/core/css/rule_feature_set.cc @@ -1599,9 +1599,18 @@ RuleFeatureSet::SelectorPreMatch RuleFeatureSet::CollectMetadataFromSelector( switch (current->GetPseudoType()) { case CSSSelector::kPseudoHas: break; + case CSSSelector::kPseudoFirstLetter: + metadata.uses_first_letter_rules = true; + break; case CSSSelector::kPseudoFirstLine: metadata.uses_first_line_rules = true; break; + case CSSSelector::kPseudoBefore: + metadata.uses_before_rules = true; + break; + case CSSSelector::kPseudoAfter: + metadata.uses_after_rules = true; + break; case CSSSelector::kPseudoWindowInactive: metadata.uses_window_inactive_selector = true; break; @@ -1668,6 +1677,9 @@ RuleFeatureSet::SelectorPreMatch RuleFeatureSet::CollectMetadataFromSelector( void RuleFeatureSet::FeatureMetadata::Merge(const FeatureMetadata& other) { uses_first_line_rules |= other.uses_first_line_rules; + uses_first_letter_rules |= other.uses_first_letter_rules; + uses_before_rules |= other.uses_before_rules; + uses_after_rules |= other.uses_after_rules; uses_window_inactive_selector |= other.uses_window_inactive_selector; max_direct_adjacent_selectors = std::max(max_direct_adjacent_selectors, other.max_direct_adjacent_selectors); uses_has_inside_nth |= other.uses_has_inside_nth; @@ -1675,6 +1687,9 @@ void RuleFeatureSet::FeatureMetadata::Merge(const FeatureMetadata& other) { void RuleFeatureSet::FeatureMetadata::Clear() { uses_first_line_rules = false; + uses_first_letter_rules = false; + uses_before_rules = false; + uses_after_rules = false; uses_window_inactive_selector = false; max_direct_adjacent_selectors = 0; invalidates_parts = false; @@ -1683,6 +1698,8 @@ void RuleFeatureSet::FeatureMetadata::Clear() { bool RuleFeatureSet::FeatureMetadata::operator==(const FeatureMetadata& other) const { return uses_first_line_rules == other.uses_first_line_rules && + uses_first_letter_rules == other.uses_first_letter_rules && + uses_before_rules == other.uses_before_rules && uses_after_rules == other.uses_after_rules && uses_window_inactive_selector == other.uses_window_inactive_selector && max_direct_adjacent_selectors == other.max_direct_adjacent_selectors && invalidates_parts == other.invalidates_parts && uses_has_inside_nth == other.uses_has_inside_nth; diff --git a/bridge/core/css/rule_feature_set.h b/bridge/core/css/rule_feature_set.h index d317752549..d84dd009d8 100644 --- a/bridge/core/css/rule_feature_set.h +++ b/bridge/core/css/rule_feature_set.h @@ -134,6 +134,9 @@ class RuleFeatureSet { // Member functions for accessing non-invalidation-set related features. bool UsesFirstLineRules() const { return metadata_.uses_first_line_rules; } + bool UsesFirstLetterRules() const { return metadata_.uses_first_letter_rules; } + bool UsesBeforeRules() const { return metadata_.uses_before_rules; } + bool UsesAfterRules() const { return metadata_.uses_after_rules; } bool UsesWindowInactiveSelector() const { return metadata_.uses_window_inactive_selector; } // Returns true if we have :nth-child(... of S) selectors where S contains a // :has() selector. @@ -266,6 +269,9 @@ class RuleFeatureSet { bool operator!=(const FeatureMetadata& o) const { return !(*this == o); } bool uses_first_line_rules = false; + bool uses_first_letter_rules = false; + bool uses_before_rules = false; + bool uses_after_rules = false; bool uses_window_inactive_selector = false; unsigned max_direct_adjacent_selectors = 0; bool invalidates_parts = false; diff --git a/bridge/core/css/style_engine.cc b/bridge/core/css/style_engine.cc index f54a766275..baf4ee514b 100644 --- a/bridge/core/css/style_engine.cc +++ b/bridge/core/css/style_engine.cc @@ -1115,737 +1115,757 @@ void StyleEngine::AttributeChangedForElement(const AtomicString& attribute_local void StyleEngine::RecalcStyleForSubtree(Element& root_element) { Document& document = GetDocument(); - if (!document.GetExecutingContext()->isBlinkEnabled()) { + ExecutingContext* ctx = document.GetExecutingContext(); + if (!ctx || !ctx->isBlinkEnabled()) { return; } - std::function apply_for_element = - [&](Element* element, const InheritedState& parent_state) -> InheritedState { - if (!element || !element->IsStyledElement()) { - return parent_state; - } + bool emit_before = true; + bool emit_after = true; + bool emit_first_letter = true; + bool emit_first_line = true; + if (global_rule_set_) { + const RuleFeatureSet& features = global_rule_set_->GetRuleFeatureSet(); + emit_before = features.UsesBeforeRules(); + emit_after = features.UsesAfterRules(); + emit_first_letter = features.UsesFirstLetterRules(); + emit_first_line = features.UsesFirstLineRules(); + } - StyleResolver& resolver = EnsureStyleResolver(); - StyleResolverState state(document, *element); - ElementRuleCollector collector(state, SelectorChecker::kResolvingStyle); - resolver.CollectAllRules(state, collector, /*include_smil_properties*/ false); - collector.SortAndTransferMatchedRules(); + StyleResolver& resolver = EnsureStyleResolver(); + auto* command_buffer = ctx->uiCommandBuffer(); - StyleCascade cascade(state); - for (const auto& entry : collector.GetMatchResult().GetMatchedProperties()) { - if (entry.is_inline_style) { - cascade.MutableMatchResult().AddInlineStyleProperties(entry.properties); - } else { - cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); - } + auto apply_for_element = [&](Element* element) -> bool { + if (!element || !element->IsStyledElement()) { + return false; + } + + StyleResolverState state(document, *element); + ElementRuleCollector collector(state, SelectorChecker::kResolvingStyle); + resolver.CollectAllRules(state, collector, /*include_smil_properties*/ false); + collector.SortAndTransferMatchedRules(); + + StyleCascade cascade(state); + for (const auto& entry : collector.GetMatchResult().GetMatchedProperties()) { + cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); + } + + std::shared_ptr property_set = cascade.ExportWinningPropertySet(); + + bool display_none_for_invalidation = false; + if (property_set && !property_set->IsEmpty()) { + if (const auto* display_ptr = property_set->GetPropertyCSSValue(CSSPropertyID::kDisplay); + display_ptr && *display_ptr && (*display_ptr)->IsIdentifierValue()) { + const auto& ident = To(*(*display_ptr)); + display_none_for_invalidation = ident.GetValueID() == CSSValueID::kNone; + } else { + String display_value = property_set->GetPropertyValue(CSSPropertyID::kDisplay); + display_none_for_invalidation = display_value.StripWhiteSpace().LowerASCII() == "none"; + } + } + element->SetDisplayNoneForStyleInvalidation(display_none_for_invalidation); + + if (!property_set || property_set->IsEmpty()) { + // Even if there are no element-level winners, clear any previously-sent + // sheet overrides (to avoid stale styles) and emit pseudo styles if any exist. + command_buffer->AddCommand(UICommand::kClearSheetStyle, nullptr, element->bindingObject(), nullptr); + auto emit_pseudo_if_any = [&](PseudoId pseudo_id, const char* pseudo_name) { + ElementRuleCollector pseudo_collector(state, SelectorChecker::kResolvingStyle); + pseudo_collector.SetPseudoElementStyleRequest(PseudoElementStyleRequest(pseudo_id)); + resolver.CollectAllRules(state, pseudo_collector, /*include_smil_properties*/ false); + pseudo_collector.SortAndTransferMatchedRules(); + + StyleCascade pseudo_cascade(state); + for (const auto& entry : pseudo_collector.GetMatchResult().GetMatchedProperties()) { + pseudo_cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); } - std::shared_ptr property_set = cascade.ExportWinningPropertySet(); + std::shared_ptr pseudo_set = pseudo_cascade.ExportWinningPropertySet(); + if (!pseudo_set || pseudo_set->PropertyCount() == 0) return false; - auto inline_style = const_cast(*element).EnsureMutableInlineStyle(); - if (inline_style && inline_style->PropertyCount() > 0) { - if (!property_set) { - property_set = std::make_shared(kHTMLStandardMode); + AtomicString pseudo_atom = AtomicString::CreateFromUTF8(pseudo_name); + command_buffer->AddCommand(UICommand::kClearPseudoStyle, pseudo_atom.ToNativeString(), element->bindingObject(), + nullptr); + + for (unsigned i = 0; i < pseudo_set->PropertyCount(); ++i) { + auto prop = pseudo_set->PropertyAt(i); + CSSPropertyID id = prop.Id(); + if (id == CSSPropertyID::kInvalid) { + continue; } - unsigned icount = inline_style->PropertyCount(); - for (unsigned i = 0; i < icount; ++i) { - auto in_prop = inline_style->PropertyAt(i); - CSSPropertyID id = in_prop.Id(); - if (id == CSSPropertyID::kInvalid || id == CSSPropertyID::kVariable) { - continue; - } - if (!property_set->HasProperty(id)) { - const auto* vptr = in_prop.Value(); - if (vptr && *vptr) { - property_set->SetProperty(id, *vptr, in_prop.IsImportant()); - } - } + const auto* value_ptr = prop.Value(); + if (!value_ptr || !(*value_ptr)) { + continue; + } + AtomicString prop_name = prop.Name().ToAtomicString(); + String value_string = pseudo_set->GetPropertyValueWithHint(prop_name, i); + if (value_string.IsNull()) { + value_string = (*value_ptr)->CssTextForSerialization(); + } + if (id == CSSPropertyID::kVariable && value_string.IsEmpty()) { + value_string = String(" "); } + String base_href_string = pseudo_set->GetPropertyBaseHrefWithHint(prop_name, i); + auto key_ns = prop_name.ToStylePropertyNameNativeString(); + auto* payload = + reinterpret_cast(dart_malloc(sizeof(NativePseudoStyleWithHref))); + payload->key = key_ns.release(); + payload->value = stringToNativeString(value_string).release(); + if (!base_href_string.IsEmpty()) { + payload->href = stringToNativeString(base_href_string).release(); + } else { + payload->href = nullptr; + } + command_buffer->AddCommand(UICommand::kSetPseudoStyle, pseudo_atom.ToNativeString(), element->bindingObject(), + payload); } + return true; + }; - bool display_none_for_invalidation = false; - if (property_set && !property_set->IsEmpty()) { - String display_value = property_set->GetPropertyValue(CSSPropertyID::kDisplay); - display_none_for_invalidation = display_value.StripWhiteSpace().LowerASCII() == "none"; - } - element->SetDisplayNoneForStyleInvalidation(display_none_for_invalidation); - - if (!property_set || property_set->IsEmpty()) { - // Even if there are no element-level winners, clear any previously-sent - // overrides (to avoid stale styles) and emit pseudo styles if any exist. - auto* ctx = document.GetExecutingContext(); - if (!(inline_style && inline_style->PropertyCount() > 0)) { - ctx->uiCommandBuffer()->AddCommand(UICommand::kClearStyle, nullptr, element->bindingObject(), nullptr); - } - auto emit_pseudo_if_any = [&](PseudoId pseudo_id, const char* pseudo_name) { - ElementRuleCollector pseudo_collector(state, SelectorChecker::kResolvingStyle); - pseudo_collector.SetPseudoElementStyleRequest(PseudoElementStyleRequest(pseudo_id)); - resolver.CollectAllRules(state, pseudo_collector, /*include_smil_properties*/ false); - pseudo_collector.SortAndTransferMatchedRules(); - - StyleCascade pseudo_cascade(state); - for (const auto& entry : pseudo_collector.GetMatchResult().GetMatchedProperties()) { - if (entry.is_inline_style) { - pseudo_cascade.MutableMatchResult().AddInlineStyleProperties(entry.properties); - } else { - pseudo_cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); - } - } + if (emit_before) { + (void)emit_pseudo_if_any(PseudoId::kPseudoIdBefore, "before"); + } + if (emit_after) { + (void)emit_pseudo_if_any(PseudoId::kPseudoIdAfter, "after"); + } + if (emit_first_letter) { + (void)emit_pseudo_if_any(PseudoId::kPseudoIdFirstLetter, "first-letter"); + } + if (emit_first_line) { + (void)emit_pseudo_if_any(PseudoId::kPseudoIdFirstLine, "first-line"); + } - std::shared_ptr pseudo_set = pseudo_cascade.ExportWinningPropertySet(); - if (!pseudo_set || pseudo_set->PropertyCount() == 0) return false; + return element->IsDisplayNoneForStyleInvalidation(); + } - { - auto pseudo_atom = AtomicString::CreateFromUTF8(pseudo_name); - auto pseudo_ns = pseudo_atom.ToNativeString(); - ctx->uiCommandBuffer()->AddCommand(UICommand::kClearPseudoStyle, std::move(pseudo_ns), - element->bindingObject(), nullptr); - } - for (unsigned i = 0; i < pseudo_set->PropertyCount(); ++i) { - auto prop = pseudo_set->PropertyAt(i); - CSSPropertyID id = prop.Id(); - if (id == CSSPropertyID::kInvalid) { - continue; - } - const auto* value_ptr = prop.Value(); - if (!value_ptr || !(*value_ptr)) { - continue; - } - AtomicString prop_name = prop.Name().ToAtomicString(); - String value_string = pseudo_set->GetPropertyValueWithHint(prop_name, i); - if (value_string.IsNull()) { - value_string = (*value_ptr)->CssTextForSerialization(); - } - if (id == CSSPropertyID::kVariable && value_string.IsEmpty()) { - value_string = String(" "); - } - String base_href_string = pseudo_set->GetPropertyBaseHrefWithHint(prop_name, i); - auto key_ns = prop_name.ToStylePropertyNameNativeString(); - auto* payload = - reinterpret_cast(dart_malloc(sizeof(NativePseudoStyleWithHref))); - payload->key = key_ns.release(); - payload->value = stringToNativeString(value_string).release(); - if (!base_href_string.IsEmpty()) { - payload->href = stringToNativeString(base_href_string.ToUTF8String()).release(); - } else { - payload->href = nullptr; - } - auto pseudo_atom = AtomicString::CreateFromUTF8(pseudo_name); - auto pseudo_ns = pseudo_atom.ToNativeString(); - ctx->uiCommandBuffer()->AddCommand(UICommand::kSetPseudoStyle, std::move(pseudo_ns), - element->bindingObject(), payload); - } - return true; - }; - - (void)emit_pseudo_if_any(PseudoId::kPseudoIdBefore, "before"); - (void)emit_pseudo_if_any(PseudoId::kPseudoIdAfter, "after"); - (void)emit_pseudo_if_any(PseudoId::kPseudoIdFirstLetter, "first-letter"); - (void)emit_pseudo_if_any(PseudoId::kPseudoIdFirstLine, "first-line"); - - InheritedState next_state; - next_state.inherited_values = parent_state.inherited_values; - next_state.custom_vars = parent_state.custom_vars; - return next_state; + command_buffer->AddCommand(UICommand::kClearSheetStyle, nullptr, element->bindingObject(), nullptr); + + unsigned count = property_set->PropertyCount(); + + // Pre-scan white-space longhands + bool have_ws_collapse = false; + bool have_text_wrap = false; + WhiteSpaceCollapse ws_collapse_enum = WhiteSpaceCollapse::kCollapse; + TextWrap text_wrap_enum = TextWrap::kWrap; + for (unsigned i = 0; i < count; ++i) { + auto prop = property_set->PropertyAt(i); + CSSPropertyID id = prop.Id(); + if (id == CSSPropertyID::kInvalid) continue; + const auto* value_ptr = prop.Value(); + if (!value_ptr || !(*value_ptr)) continue; + const CSSValue& value = *(*value_ptr); + if (id == CSSPropertyID::kWhiteSpaceCollapse) { + std::string sv = value.CssTextForSerialization().ToUTF8String(); + if (sv == "collapse") { + ws_collapse_enum = WhiteSpaceCollapse::kCollapse; + have_ws_collapse = true; + } else if (sv == "preserve") { + ws_collapse_enum = WhiteSpaceCollapse::kPreserve; + have_ws_collapse = true; + } else if (sv == "preserve-breaks") { + ws_collapse_enum = WhiteSpaceCollapse::kPreserveBreaks; + have_ws_collapse = true; + } else if (sv == "break-spaces") { + ws_collapse_enum = WhiteSpaceCollapse::kBreakSpaces; + have_ws_collapse = true; + } + } else if (id == CSSPropertyID::kTextWrap) { + std::string sv = value.CssTextForSerialization().ToUTF8String(); + if (sv == "wrap") { + text_wrap_enum = TextWrap::kWrap; + have_text_wrap = true; + } else if (sv == "nowrap") { + text_wrap_enum = TextWrap::kNoWrap; + have_text_wrap = true; + } else if (sv == "balance") { + text_wrap_enum = TextWrap::kBalance; + have_text_wrap = true; + } else if (sv == "pretty") { + text_wrap_enum = TextWrap::kPretty; + have_text_wrap = true; } + } + } - auto* ctx = document.GetExecutingContext(); - // Only clear when we actually have properties to apply; otherwise we - // might clear a previously-sent snapshot and leave the element with no - // inline overrides (e.g., BODY background), causing incorrect paint. - ctx->uiCommandBuffer()->AddCommand(UICommand::kClearStyle, nullptr, element->bindingObject(), nullptr); - bool cleared = true; - - if (!property_set || property_set->IsEmpty()) { - InheritedState next_state; - next_state.inherited_values = parent_state.inherited_values; - next_state.custom_vars = parent_state.custom_vars; - return next_state; + bool emit_white_space_shorthand = have_ws_collapse || have_text_wrap; + String white_space_value_str; + if (emit_white_space_shorthand) { + EWhiteSpace ws = ToWhiteSpace(ws_collapse_enum, text_wrap_enum); + switch (ws) { + case EWhiteSpace::kNormal: + white_space_value_str = String("normal"); + break; + case EWhiteSpace::kNowrap: + white_space_value_str = String("nowrap"); + break; + case EWhiteSpace::kPre: + white_space_value_str = String("pre"); + break; + case EWhiteSpace::kPreLine: + white_space_value_str = String("pre-line"); + break; + case EWhiteSpace::kPreWrap: + white_space_value_str = String("pre-wrap"); + break; + case EWhiteSpace::kBreakSpaces: + white_space_value_str = String("break-spaces"); + break; + } + } + + for (unsigned i = 0; i < count; ++i) { + auto prop = property_set->PropertyAt(i); + CSSPropertyID id = prop.Id(); + if (id == CSSPropertyID::kInvalid) continue; + if (id == CSSPropertyID::kWhiteSpaceCollapse || id == CSSPropertyID::kTextWrap) { + continue; + } + const auto* value_ptr = prop.Value(); + if (!value_ptr || !(*value_ptr)) continue; + + AtomicString prop_name = prop.Name().ToAtomicString(); + String base_href_string = property_set->GetPropertyBaseHrefWithHint(prop_name, i); + + if (id == CSSPropertyID::kVariable) { + String value_string = property_set->GetPropertyValueWithHint(prop_name, i); + if (value_string.IsNull()) { + value_string = (*value_ptr)->CssTextForSerialization(); + } + if (value_string.IsEmpty()) { + value_string = String(" "); } - unsigned count = property_set->PropertyCount(); - InheritedValueMap inherited_values(parent_state.inherited_values); - CustomVarMap custom_vars(parent_state.custom_vars); - - // Pre-scan white-space longhands - bool have_ws_collapse = false; - bool have_text_wrap = false; - WhiteSpaceCollapse ws_collapse_enum = WhiteSpaceCollapse::kCollapse; - TextWrap text_wrap_enum = TextWrap::kWrap; - for (unsigned i = 0; i < count; ++i) { - auto prop = property_set->PropertyAt(i); - CSSPropertyID id = prop.Id(); - if (id == CSSPropertyID::kInvalid) continue; - const auto* value_ptr = prop.Value(); - if (!value_ptr || !(*value_ptr)) continue; - const CSSValue& value = *(*value_ptr); - if (id == CSSPropertyID::kWhiteSpaceCollapse) { - std::string sv = value.CssTextForSerialization().ToUTF8String(); - if (sv == "collapse") { ws_collapse_enum = WhiteSpaceCollapse::kCollapse; have_ws_collapse = true; } - else if (sv == "preserve") { ws_collapse_enum = WhiteSpaceCollapse::kPreserve; have_ws_collapse = true; } - else if (sv == "preserve-breaks") { ws_collapse_enum = WhiteSpaceCollapse::kPreserveBreaks; have_ws_collapse = true; } - else if (sv == "break-spaces") { ws_collapse_enum = WhiteSpaceCollapse::kBreakSpaces; have_ws_collapse = true; } - } else if (id == CSSPropertyID::kTextWrap) { - std::string sv = value.CssTextForSerialization().ToUTF8String(); - if (sv == "wrap") { text_wrap_enum = TextWrap::kWrap; have_text_wrap = true; } - else if (sv == "nowrap") { text_wrap_enum = TextWrap::kNoWrap; have_text_wrap = true; } - else if (sv == "balance") { text_wrap_enum = TextWrap::kBalance; have_text_wrap = true; } - else if (sv == "pretty") { text_wrap_enum = TextWrap::kPretty; have_text_wrap = true; } - } + auto key_ns = prop_name.ToStylePropertyNameNativeString(); + auto* payload = reinterpret_cast(dart_malloc(sizeof(NativeStyleValueWithHref))); + payload->value = stringToNativeString(value_string).release(); + if (!base_href_string.IsEmpty()) { + payload->href = stringToNativeString(base_href_string).release(); + } else { + payload->href = nullptr; } + payload->important = prop.IsImportant() ? 1 : 0; + command_buffer->AddCommand(UICommand::kSetSheetStyle, std::move(key_ns), element->bindingObject(), payload); + continue; + } - bool emit_white_space_shorthand = have_ws_collapse || have_text_wrap; - String white_space_value_str; - if (emit_white_space_shorthand) { - EWhiteSpace ws = ToWhiteSpace(ws_collapse_enum, text_wrap_enum); - switch (ws) { - case EWhiteSpace::kNormal: white_space_value_str = String("normal"); break; - case EWhiteSpace::kNowrap: white_space_value_str = String("nowrap"); break; - case EWhiteSpace::kPre: white_space_value_str = String("pre"); break; - case EWhiteSpace::kPreLine: white_space_value_str = String("pre-line"); break; - case EWhiteSpace::kPreWrap: white_space_value_str = String("pre-wrap"); break; - case EWhiteSpace::kBreakSpaces: white_space_value_str = String("break-spaces"); break; - } + int64_t value_slot = 0; + if ((*value_ptr)->IsIdentifierValue()) { + const auto& ident = To(*(*value_ptr)); + value_slot = -static_cast(ident.GetValueID()) - 1; + } else { + String value_string = property_set->GetPropertyValueWithHint(prop_name, i); + if (value_string.IsNull()) { + value_string = (*value_ptr)->CssTextForSerialization(); + } + if (value_string.IsEmpty()) { + continue; } + auto* value_ns = stringToNativeString(value_string).release(); + value_slot = static_cast(reinterpret_cast(value_ns)); + } - for (unsigned i = 0; i < count; ++i) { - auto prop = property_set->PropertyAt(i); - CSSPropertyID id = prop.Id(); - if (id == CSSPropertyID::kInvalid) continue; - const auto* value_ptr = prop.Value(); - if (!value_ptr || !(*value_ptr)) continue; + SharedNativeString* base_href = nullptr; + if (!base_href_string.IsEmpty()) { + base_href = stringToNativeString(base_href_string).release(); + } - AtomicString prop_name = prop.Name().ToAtomicString(); - // Custom properties require sending the property name across the bridge. - if (id == CSSPropertyID::kVariable) { - String value_string = property_set->GetPropertyValueWithHint(prop_name, i); - String base_href_string = property_set->GetPropertyBaseHrefWithHint(prop_name, i); - if (value_string.IsNull()) value_string = String(""); - if (value_string.IsEmpty()) { - value_string = String(" "); - } + command_buffer->AddSheetStyleByIdCommand(element->bindingObject(), static_cast(id), value_slot, + base_href, prop.IsImportant()); + } - // Skip white-space longhands; will emit shorthand later - if (id == CSSPropertyID::kWhiteSpaceCollapse || id == CSSPropertyID::kTextWrap) { - continue; - } + if (emit_white_space_shorthand) { + CSSValueID ws_value_id = CSSValueID::kInvalid; + EWhiteSpace ws = ToWhiteSpace(ws_collapse_enum, text_wrap_enum); + switch (ws) { + case EWhiteSpace::kNormal: + ws_value_id = CSSValueID::kNormal; + break; + case EWhiteSpace::kNowrap: + ws_value_id = CSSValueID::kNowrap; + break; + case EWhiteSpace::kPre: + ws_value_id = CSSValueID::kPre; + break; + case EWhiteSpace::kPreLine: + ws_value_id = CSSValueID::kPreLine; + break; + case EWhiteSpace::kPreWrap: + ws_value_id = CSSValueID::kPreWrap; + break; + case EWhiteSpace::kBreakSpaces: + ws_value_id = CSSValueID::kBreakSpaces; + break; + } - // Already cleared above. - auto key_ns = prop_name.ToStylePropertyNameNativeString(); - auto* payload = reinterpret_cast(dart_malloc(sizeof(NativeStyleValueWithHref))); - payload->value = stringToNativeString(value_string).release(); - if (!base_href_string.IsEmpty()) { - payload->href = stringToNativeString(base_href_string.ToUTF8String()).release(); - } else { - payload->href = nullptr; - } - ctx->uiCommandBuffer()->AddCommand(UICommand::kSetStyle, std::move(key_ns), element->bindingObject(), - payload); - continue; - } + int64_t value_slot = 0; + if (ws_value_id != CSSValueID::kInvalid) { + value_slot = -static_cast(ws_value_id) - 1; + } else if (!white_space_value_str.IsEmpty()) { + auto* value_ns = stringToNativeString(white_space_value_str).release(); + value_slot = static_cast(reinterpret_cast(value_ns)); + } - // Skip white-space longhands; will emit shorthand later - if (id == CSSPropertyID::kWhiteSpaceCollapse || id == CSSPropertyID::kTextWrap) { - continue; - } + command_buffer->AddSheetStyleByIdCommand(element->bindingObject(), static_cast(CSSPropertyID::kWhiteSpace), + value_slot, nullptr, /*important*/ false); + } - int64_t value_slot = 0; - SharedNativeString* base_href = nullptr; - if ((*value_ptr)->IsIdentifierValue()) { - const auto& ident = To(*(*value_ptr)); - value_slot = -static_cast(ident.GetValueID()) - 1; - } else { - String value_string = property_set->GetPropertyValueWithHint(prop_name, i); - String base_href_string = property_set->GetPropertyBaseHrefWithHint(prop_name, i); - if (value_string.IsNull()) { - value_string = String(""); - } - if (!value_string.IsEmpty()) { - auto* value_ns = stringToNativeString(value_string).release(); - value_slot = static_cast(reinterpret_cast(value_ns)); - } - if (!base_href_string.IsEmpty()) { - base_href = stringToNativeString(base_href_string.ToUTF8String()).release(); - } - } + // Pseudo emission (only minimal content properties as in RecalcStyle) + auto send_pseudo_for = [&](PseudoId pseudo_id, const char* pseudo_name) { + ElementRuleCollector pseudo_collector(state, SelectorChecker::kResolvingStyle); + pseudo_collector.SetPseudoElementStyleRequest(PseudoElementStyleRequest(pseudo_id)); + resolver.CollectAllRules(state, pseudo_collector, /*include_smil_properties*/ false); + pseudo_collector.SortAndTransferMatchedRules(); - ctx->uiCommandBuffer()->AddStyleByIdCommand(element->bindingObject(), static_cast(id), value_slot, - base_href); - } + StyleCascade pseudo_cascade(state); + for (const auto& entry : pseudo_collector.GetMatchResult().GetMatchedProperties()) { + pseudo_cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); + } - if (emit_white_space_shorthand) { - // Already cleared above. - CSSValueID ws_value_id = CSSValueID::kInvalid; - EWhiteSpace ws = ToWhiteSpace(ws_collapse_enum, text_wrap_enum); - switch (ws) { - case EWhiteSpace::kNormal: - ws_value_id = CSSValueID::kNormal; - break; - case EWhiteSpace::kNowrap: - ws_value_id = CSSValueID::kNowrap; - break; - case EWhiteSpace::kPre: - ws_value_id = CSSValueID::kPre; - break; - case EWhiteSpace::kPreLine: - ws_value_id = CSSValueID::kPreLine; - break; - case EWhiteSpace::kPreWrap: - ws_value_id = CSSValueID::kPreWrap; - break; - case EWhiteSpace::kBreakSpaces: - ws_value_id = CSSValueID::kBreakSpaces; - break; - } + std::shared_ptr pseudo_set = pseudo_cascade.ExportWinningPropertySet(); + if (!pseudo_set || pseudo_set->PropertyCount() == 0) return; - int64_t value_slot = 0; - if (ws_value_id != CSSValueID::kInvalid) { - value_slot = -static_cast(ws_value_id) - 1; - } else if (!white_space_value_str.IsEmpty()) { - auto* value_ns = stringToNativeString(white_space_value_str).release(); - value_slot = static_cast(reinterpret_cast(value_ns)); - } + AtomicString pseudo_atom = AtomicString::CreateFromUTF8(pseudo_name); + command_buffer->AddCommand(UICommand::kClearPseudoStyle, pseudo_atom.ToNativeString(), element->bindingObject(), + nullptr); - ctx->uiCommandBuffer()->AddStyleByIdCommand(element->bindingObject(), - static_cast(CSSPropertyID::kWhiteSpace), value_slot, - nullptr); + for (unsigned i = 0; i < pseudo_set->PropertyCount(); ++i) { + auto prop = pseudo_set->PropertyAt(i); + CSSPropertyID id = prop.Id(); + if (id == CSSPropertyID::kInvalid) { + continue; + } + const auto* value_ptr = prop.Value(); + if (!value_ptr || !(*value_ptr)) { + continue; + } + AtomicString prop_name = prop.Name().ToAtomicString(); + String value_string = pseudo_set->GetPropertyValueWithHint(prop_name, i); + if (value_string.IsNull()) { + value_string = (*value_ptr)->CssTextForSerialization(); + } + if (id == CSSPropertyID::kVariable && value_string.IsEmpty()) { + value_string = String(" "); + } + String base_href_string = pseudo_set->GetPropertyBaseHrefWithHint(prop_name, i); + + auto key_ns = prop_name.ToStylePropertyNameNativeString(); + auto* payload = reinterpret_cast(dart_malloc(sizeof(NativePseudoStyleWithHref))); + payload->key = key_ns.release(); + payload->value = stringToNativeString(value_string).release(); + if (!base_href_string.IsEmpty()) { + payload->href = stringToNativeString(base_href_string).release(); + } else { + payload->href = nullptr; } - // Pseudo emission (only minimal content properties as in RecalcStyle) - auto send_pseudo_for = [&](PseudoId pseudo_id, const char* pseudo_name) { - ElementRuleCollector pseudo_collector(state, SelectorChecker::kResolvingStyle); - pseudo_collector.SetPseudoElementStyleRequest(PseudoElementStyleRequest(pseudo_id)); - resolver.CollectAllRules(state, pseudo_collector, /*include_smil_properties*/ false); - pseudo_collector.SortAndTransferMatchedRules(); - - StyleCascade pseudo_cascade(state); - for (const auto& entry : pseudo_collector.GetMatchResult().GetMatchedProperties()) { - if (entry.is_inline_style) { - pseudo_cascade.MutableMatchResult().AddInlineStyleProperties(entry.properties); - } else { - pseudo_cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); - } - } - - std::shared_ptr pseudo_set = pseudo_cascade.ExportWinningPropertySet(); - if (!pseudo_set || pseudo_set->PropertyCount() == 0) return; - { - auto pseudo_atom = AtomicString::CreateFromUTF8(pseudo_name); - auto pseudo_ns = pseudo_atom.ToNativeString(); - ctx->uiCommandBuffer()->AddCommand(UICommand::kClearPseudoStyle, std::move(pseudo_ns), - element->bindingObject(), nullptr); - } - for (unsigned i = 0; i < pseudo_set->PropertyCount(); ++i) { - auto prop = pseudo_set->PropertyAt(i); - CSSPropertyID id = prop.Id(); - if (id == CSSPropertyID::kInvalid) { - continue; - } - const auto* value_ptr = prop.Value(); - if (!value_ptr || !(*value_ptr)) { - continue; - } - AtomicString prop_name = prop.Name().ToAtomicString(); - String value_string = pseudo_set->GetPropertyValueWithHint(prop_name, i); - if (value_string.IsNull()) { - value_string = (*value_ptr)->CssTextForSerialization(); - } - if (id == CSSPropertyID::kVariable && value_string.IsEmpty()) { - value_string = String(" "); - } - String base_href_string = pseudo_set->GetPropertyBaseHrefWithHint(prop_name, i); - - auto key_ns = prop_name.ToStylePropertyNameNativeString(); - auto* payload = - reinterpret_cast(dart_malloc(sizeof(NativePseudoStyleWithHref))); - payload->key = key_ns.release(); - payload->value = stringToNativeString(value_string).release(); - if (!base_href_string.IsEmpty()) { - payload->href = stringToNativeString(base_href_string.ToUTF8String()).release(); - } else { - payload->href = nullptr; - } + command_buffer->AddCommand(UICommand::kSetPseudoStyle, pseudo_atom.ToNativeString(), element->bindingObject(), + payload); + } + }; - auto pseudo_atom = AtomicString::CreateFromUTF8(pseudo_name); - auto pseudo_ns = pseudo_atom.ToNativeString(); - ctx->uiCommandBuffer()->AddCommand(UICommand::kSetPseudoStyle, std::move(pseudo_ns), - element->bindingObject(), payload); - } - }; + if (emit_before) { + send_pseudo_for(PseudoId::kPseudoIdBefore, "before"); + } + if (emit_after) { + send_pseudo_for(PseudoId::kPseudoIdAfter, "after"); + } + if (emit_first_letter) { + send_pseudo_for(PseudoId::kPseudoIdFirstLetter, "first-letter"); + } + if (emit_first_line) { + send_pseudo_for(PseudoId::kPseudoIdFirstLine, "first-line"); + } - send_pseudo_for(PseudoId::kPseudoIdBefore, "before"); - send_pseudo_for(PseudoId::kPseudoIdAfter, "after"); - send_pseudo_for(PseudoId::kPseudoIdFirstLetter, "first-letter"); - send_pseudo_for(PseudoId::kPseudoIdFirstLine, "first-line"); + return element->IsDisplayNoneForStyleInvalidation(); + }; - InheritedState next_state; - next_state.inherited_values = std::move(inherited_values); - next_state.custom_vars = std::move(custom_vars); - return next_state; - }; + std::vector stack; + stack.reserve(64); + stack.push_back(&root_element); + while (!stack.empty()) { + Node* node = stack.back(); + stack.pop_back(); + if (!node) { + continue; + } - std::function walk = - [&](Node* node, const InheritedState& inherited_state) { - if (!node) return; - InheritedState current_state = inherited_state; - if (node->IsElementNode()) { - current_state = apply_for_element(static_cast(node), inherited_state); - } - for (Node* child = node->firstChild(); child; child = child->nextSibling()) { - walk(child, current_state); - } - }; + if (node->IsElementNode()) { + auto* element = static_cast(node); + if (apply_for_element(element)) { + continue; + } + } - InheritedState root_state; - walk(&root_element, root_state); + for (Node* child = node->lastChild(); child; child = child->previousSibling()) { + stack.push_back(child); + } + } } void StyleEngine::RecalcStyleForElementOnly(Element& element) { Document& document = GetDocument(); - if (!document.GetExecutingContext()->isBlinkEnabled()) { + ExecutingContext* ctx = document.GetExecutingContext(); + if (!ctx || !ctx->isBlinkEnabled()) { return; } - // Reuse the same per-element styling logic as RecalcStyleForSubtree, but do - // not recurse into children and ignore inherited/custom-var accumulation. - std::function apply_for_element = - [&](Element* el, const InheritedState& parent_state) -> InheritedState { - if (!el || !el->IsStyledElement()) { - return parent_state; - } + bool emit_before = true; + bool emit_after = true; + bool emit_first_letter = true; + bool emit_first_line = true; + if (global_rule_set_) { + const RuleFeatureSet& features = global_rule_set_->GetRuleFeatureSet(); + emit_before = features.UsesBeforeRules(); + emit_after = features.UsesAfterRules(); + emit_first_letter = features.UsesFirstLetterRules(); + emit_first_line = features.UsesFirstLineRules(); + } - StyleResolver& resolver = EnsureStyleResolver(); - StyleResolverState state(document, *el); - ElementRuleCollector collector(state, SelectorChecker::kResolvingStyle); - resolver.CollectAllRules(state, collector, /*include_smil_properties*/ false); - collector.SortAndTransferMatchedRules(); + // Reuse the same per-element styling logic as RecalcStyleForSubtree, but do + // not recurse into children. + StyleResolver& resolver = EnsureStyleResolver(); + auto* command_buffer = ctx->uiCommandBuffer(); - StyleCascade cascade(state); - for (const auto& entry : collector.GetMatchResult().GetMatchedProperties()) { - if (entry.is_inline_style) { - cascade.MutableMatchResult().AddInlineStyleProperties(entry.properties); - } else { - cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); - } - } + auto apply_for_element = [&](Element* el) { + if (!el || !el->IsStyledElement()) { + return; + } - std::shared_ptr property_set = cascade.ExportWinningPropertySet(); + StyleResolverState state(document, *el); + ElementRuleCollector collector(state, SelectorChecker::kResolvingStyle); + resolver.CollectAllRules(state, collector, /*include_smil_properties*/ false); + collector.SortAndTransferMatchedRules(); - auto inline_style = const_cast(*el).EnsureMutableInlineStyle(); - if (inline_style && inline_style->PropertyCount() > 0) { - if (!property_set) { - property_set = std::make_shared(kHTMLStandardMode); - } - unsigned icount = inline_style->PropertyCount(); - for (unsigned i = 0; i < icount; ++i) { - auto in_prop = inline_style->PropertyAt(i); - CSSPropertyID id = in_prop.Id(); - if (id == CSSPropertyID::kInvalid || id == CSSPropertyID::kVariable) { - continue; - } - if (!property_set->HasProperty(id)) { - const auto* vptr = in_prop.Value(); - if (vptr && *vptr) { - property_set->SetProperty(id, *vptr, in_prop.IsImportant()); - } - } - } - } + StyleCascade cascade(state); + for (const auto& entry : collector.GetMatchResult().GetMatchedProperties()) { + cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); + } - bool display_none_for_invalidation = false; - if (property_set && !property_set->IsEmpty()) { - String display_value = property_set->GetPropertyValue(CSSPropertyID::kDisplay); - display_none_for_invalidation = display_value.StripWhiteSpace().LowerASCII() == "none"; - } - el->SetDisplayNoneForStyleInvalidation(display_none_for_invalidation); + std::shared_ptr property_set = cascade.ExportWinningPropertySet(); - auto* ctx = document.GetExecutingContext(); + bool display_none_for_invalidation = false; + if (property_set && !property_set->IsEmpty()) { + if (const auto* display_ptr = property_set->GetPropertyCSSValue(CSSPropertyID::kDisplay); + display_ptr && *display_ptr && (*display_ptr)->IsIdentifierValue()) { + const auto& ident = To(*(*display_ptr)); + display_none_for_invalidation = ident.GetValueID() == CSSValueID::kNone; + } else { + String display_value = property_set->GetPropertyValue(CSSPropertyID::kDisplay); + display_none_for_invalidation = display_value.StripWhiteSpace().LowerASCII() == "none"; + } + } + el->SetDisplayNoneForStyleInvalidation(display_none_for_invalidation); - if (!property_set || property_set->IsEmpty()) { - if (!(inline_style && inline_style->PropertyCount() > 0)) { - ctx->uiCommandBuffer()->AddCommand(UICommand::kClearStyle, nullptr, el->bindingObject(), nullptr); - } + if (!property_set || property_set->IsEmpty()) { + command_buffer->AddCommand(UICommand::kClearSheetStyle, nullptr, el->bindingObject(), nullptr); - auto emit_pseudo_if_any = [&](PseudoId pseudo_id, const char* pseudo_name) { - ElementRuleCollector pseudo_collector(state, SelectorChecker::kResolvingStyle); - pseudo_collector.SetPseudoElementStyleRequest(PseudoElementStyleRequest(pseudo_id)); - resolver.CollectAllRules(state, pseudo_collector, /*include_smil_properties*/ false); - pseudo_collector.SortAndTransferMatchedRules(); - - StyleCascade pseudo_cascade(state); - for (const auto& entry : pseudo_collector.GetMatchResult().GetMatchedProperties()) { - if (entry.is_inline_style) { - pseudo_cascade.MutableMatchResult().AddInlineStyleProperties(entry.properties); - } else { - pseudo_cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); - } - } + auto emit_pseudo_if_any = [&](PseudoId pseudo_id, const char* pseudo_name) { + ElementRuleCollector pseudo_collector(state, SelectorChecker::kResolvingStyle); + pseudo_collector.SetPseudoElementStyleRequest(PseudoElementStyleRequest(pseudo_id)); + resolver.CollectAllRules(state, pseudo_collector, /*include_smil_properties*/ false); + pseudo_collector.SortAndTransferMatchedRules(); - std::shared_ptr pseudo_set = pseudo_cascade.ExportWinningPropertySet(); - if (!pseudo_set || pseudo_set->PropertyCount() == 0) return false; - - auto pseudo_atom = AtomicString::CreateFromUTF8(pseudo_name); - auto pseudo_ns = pseudo_atom.ToNativeString(); - ctx->uiCommandBuffer()->AddCommand(UICommand::kClearPseudoStyle, std::move(pseudo_ns), - el->bindingObject(), nullptr); - for (unsigned i = 0; i < pseudo_set->PropertyCount(); ++i) { - auto prop = pseudo_set->PropertyAt(i); - CSSPropertyID id = prop.Id(); - AtomicString prop_name = prop.Name().ToAtomicString(); - String value_string = pseudo_set->GetPropertyValueWithHint(prop_name, i); - if (value_string.IsNull()) value_string = String(""); - if (id == CSSPropertyID::kVariable && value_string.IsEmpty()) { - value_string = String(" "); - } - String base_href_string = pseudo_set->GetPropertyBaseHrefWithHint(prop_name, i); - auto key_ns = prop_name.ToStylePropertyNameNativeString(); - auto* payload = - reinterpret_cast(dart_malloc(sizeof(NativePseudoStyleWithHref))); - payload->key = key_ns.release(); - payload->value = stringToNativeString(value_string).release(); - if (!base_href_string.IsEmpty()) { - payload->href = stringToNativeString(base_href_string.ToUTF8String()).release(); - } else { - payload->href = nullptr; - } - - auto pseudo_atom2 = AtomicString::CreateFromUTF8(pseudo_name); - auto pseudo_ns2 = pseudo_atom2.ToNativeString(); - ctx->uiCommandBuffer()->AddCommand(UICommand::kSetPseudoStyle, std::move(pseudo_ns2), - el->bindingObject(), payload); - } - return true; - }; - - (void)emit_pseudo_if_any(PseudoId::kPseudoIdBefore, "before"); - (void)emit_pseudo_if_any(PseudoId::kPseudoIdAfter, "after"); - (void)emit_pseudo_if_any(PseudoId::kPseudoIdFirstLetter, "first-letter"); - (void)emit_pseudo_if_any(PseudoId::kPseudoIdFirstLine, "first-line"); - - InheritedState next_state; - next_state.inherited_values = parent_state.inherited_values; - next_state.custom_vars = parent_state.custom_vars; - return next_state; + StyleCascade pseudo_cascade(state); + for (const auto& entry : pseudo_collector.GetMatchResult().GetMatchedProperties()) { + pseudo_cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); } - // We have properties to apply. Clear existing overrides and emit winners. - ctx->uiCommandBuffer()->AddCommand(UICommand::kClearStyle, nullptr, el->bindingObject(), nullptr); + std::shared_ptr pseudo_set = pseudo_cascade.ExportWinningPropertySet(); + if (!pseudo_set || pseudo_set->PropertyCount() == 0) return false; - unsigned count = property_set->PropertyCount(); - InheritedValueMap inherited_values(parent_state.inherited_values); - CustomVarMap custom_vars(parent_state.custom_vars); + AtomicString pseudo_atom = AtomicString::CreateFromUTF8(pseudo_name); + command_buffer->AddCommand(UICommand::kClearPseudoStyle, pseudo_atom.ToNativeString(), el->bindingObject(), + nullptr); - bool have_ws_collapse = false; - bool have_text_wrap = false; - WhiteSpaceCollapse ws_collapse_enum = WhiteSpaceCollapse::kCollapse; - TextWrap text_wrap_enum = TextWrap::kWrap; - for (unsigned i = 0; i < count; ++i) { - auto prop = property_set->PropertyAt(i); + for (unsigned i = 0; i < pseudo_set->PropertyCount(); ++i) { + auto prop = pseudo_set->PropertyAt(i); CSSPropertyID id = prop.Id(); - if (id == CSSPropertyID::kInvalid) continue; + if (id == CSSPropertyID::kInvalid) { + continue; + } const auto* value_ptr = prop.Value(); - if (!value_ptr || !(*value_ptr)) continue; - const CSSValue& value = *(*value_ptr); - if (id == CSSPropertyID::kWhiteSpaceCollapse) { - std::string sv = value.CssTextForSerialization().ToUTF8String(); - if (sv == "collapse") { ws_collapse_enum = WhiteSpaceCollapse::kCollapse; have_ws_collapse = true; } - else if (sv == "preserve") { ws_collapse_enum = WhiteSpaceCollapse::kPreserve; have_ws_collapse = true; } - else if (sv == "preserve-breaks") { ws_collapse_enum = WhiteSpaceCollapse::kPreserveBreaks; have_ws_collapse = true; } - else if (sv == "break-spaces") { ws_collapse_enum = WhiteSpaceCollapse::kBreakSpaces; have_ws_collapse = true; } - } else if (id == CSSPropertyID::kTextWrap) { - std::string sv = value.CssTextForSerialization().ToUTF8String(); - if (sv == "wrap") { text_wrap_enum = TextWrap::kWrap; have_text_wrap = true; } - else if (sv == "nowrap") { text_wrap_enum = TextWrap::kNoWrap; have_text_wrap = true; } - else if (sv == "balance") { text_wrap_enum = TextWrap::kBalance; have_text_wrap = true; } - else if (sv == "pretty") { text_wrap_enum = TextWrap::kPretty; have_text_wrap = true; } + if (!value_ptr || !(*value_ptr)) { + continue; } - } - - bool emit_white_space_shorthand = have_ws_collapse || have_text_wrap; - String white_space_value_str; - if (emit_white_space_shorthand) { - EWhiteSpace ws = ToWhiteSpace(ws_collapse_enum, text_wrap_enum); - switch (ws) { - case EWhiteSpace::kNormal: white_space_value_str = String("normal"); break; - case EWhiteSpace::kNowrap: white_space_value_str = String("nowrap"); break; - case EWhiteSpace::kPre: white_space_value_str = String("pre"); break; - case EWhiteSpace::kPreLine: white_space_value_str = String("pre-line"); break; - case EWhiteSpace::kPreWrap: white_space_value_str = String("pre-wrap"); break; - case EWhiteSpace::kBreakSpaces: white_space_value_str = String("break-spaces"); break; + AtomicString prop_name = prop.Name().ToAtomicString(); + String value_string = pseudo_set->GetPropertyValueWithHint(prop_name, i); + if (value_string.IsNull()) { + value_string = (*value_ptr)->CssTextForSerialization(); + } + if (id == CSSPropertyID::kVariable && value_string.IsEmpty()) { + value_string = String(" "); } + String base_href_string = pseudo_set->GetPropertyBaseHrefWithHint(prop_name, i); + auto key_ns = prop_name.ToStylePropertyNameNativeString(); + auto* payload = + reinterpret_cast(dart_malloc(sizeof(NativePseudoStyleWithHref))); + payload->key = key_ns.release(); + payload->value = stringToNativeString(value_string).release(); + if (!base_href_string.IsEmpty()) { + payload->href = stringToNativeString(base_href_string).release(); + } else { + payload->href = nullptr; + } + command_buffer->AddCommand(UICommand::kSetPseudoStyle, pseudo_atom.ToNativeString(), el->bindingObject(), + payload); } + return true; + }; - for (unsigned i = 0; i < count; ++i) { - auto prop = property_set->PropertyAt(i); - CSSPropertyID id = prop.Id(); - if (id == CSSPropertyID::kInvalid) continue; - const auto* value_ptr = prop.Value(); - if (!value_ptr || !(*value_ptr)) continue; + if (emit_before) { + (void)emit_pseudo_if_any(PseudoId::kPseudoIdBefore, "before"); + } + if (emit_after) { + (void)emit_pseudo_if_any(PseudoId::kPseudoIdAfter, "after"); + } + if (emit_first_letter) { + (void)emit_pseudo_if_any(PseudoId::kPseudoIdFirstLetter, "first-letter"); + } + if (emit_first_line) { + (void)emit_pseudo_if_any(PseudoId::kPseudoIdFirstLine, "first-line"); + } - AtomicString prop_name = prop.Name().ToAtomicString(); - // Custom properties require sending the property name across the bridge. - if (id == CSSPropertyID::kVariable) { - String value_string = property_set->GetPropertyValueWithHint(prop_name, i); - String base_href_string = property_set->GetPropertyBaseHrefWithHint(prop_name, i); - if (value_string.IsNull()) value_string = String(""); - if (value_string.IsEmpty()) { - value_string = String(" "); - } + return; + } - if (id == CSSPropertyID::kWhiteSpaceCollapse || id == CSSPropertyID::kTextWrap) { - continue; - } + command_buffer->AddCommand(UICommand::kClearSheetStyle, nullptr, el->bindingObject(), nullptr); + + unsigned count = property_set->PropertyCount(); + + bool have_ws_collapse = false; + bool have_text_wrap = false; + WhiteSpaceCollapse ws_collapse_enum = WhiteSpaceCollapse::kCollapse; + TextWrap text_wrap_enum = TextWrap::kWrap; + for (unsigned i = 0; i < count; ++i) { + auto prop = property_set->PropertyAt(i); + CSSPropertyID id = prop.Id(); + if (id == CSSPropertyID::kInvalid) continue; + const auto* value_ptr = prop.Value(); + if (!value_ptr || !(*value_ptr)) continue; + const CSSValue& value = *(*value_ptr); + if (id == CSSPropertyID::kWhiteSpaceCollapse) { + std::string sv = value.CssTextForSerialization().ToUTF8String(); + if (sv == "collapse") { + ws_collapse_enum = WhiteSpaceCollapse::kCollapse; + have_ws_collapse = true; + } else if (sv == "preserve") { + ws_collapse_enum = WhiteSpaceCollapse::kPreserve; + have_ws_collapse = true; + } else if (sv == "preserve-breaks") { + ws_collapse_enum = WhiteSpaceCollapse::kPreserveBreaks; + have_ws_collapse = true; + } else if (sv == "break-spaces") { + ws_collapse_enum = WhiteSpaceCollapse::kBreakSpaces; + have_ws_collapse = true; + } + } else if (id == CSSPropertyID::kTextWrap) { + std::string sv = value.CssTextForSerialization().ToUTF8String(); + if (sv == "wrap") { + text_wrap_enum = TextWrap::kWrap; + have_text_wrap = true; + } else if (sv == "nowrap") { + text_wrap_enum = TextWrap::kNoWrap; + have_text_wrap = true; + } else if (sv == "balance") { + text_wrap_enum = TextWrap::kBalance; + have_text_wrap = true; + } else if (sv == "pretty") { + text_wrap_enum = TextWrap::kPretty; + have_text_wrap = true; + } + } + } - auto key_ns = prop_name.ToStylePropertyNameNativeString(); - auto* payload = reinterpret_cast(dart_malloc(sizeof(NativeStyleValueWithHref))); - payload->value = stringToNativeString(value_string).release(); - if (!base_href_string.IsEmpty()) { - payload->href = stringToNativeString(base_href_string.ToUTF8String()).release(); - } else { - payload->href = nullptr; - } - ctx->uiCommandBuffer()->AddCommand(UICommand::kSetStyle, std::move(key_ns), el->bindingObject(), payload); - continue; - } + bool emit_white_space_shorthand = have_ws_collapse || have_text_wrap; + String white_space_value_str; + if (emit_white_space_shorthand) { + EWhiteSpace ws = ToWhiteSpace(ws_collapse_enum, text_wrap_enum); + switch (ws) { + case EWhiteSpace::kNormal: + white_space_value_str = String("normal"); + break; + case EWhiteSpace::kNowrap: + white_space_value_str = String("nowrap"); + break; + case EWhiteSpace::kPre: + white_space_value_str = String("pre"); + break; + case EWhiteSpace::kPreLine: + white_space_value_str = String("pre-line"); + break; + case EWhiteSpace::kPreWrap: + white_space_value_str = String("pre-wrap"); + break; + case EWhiteSpace::kBreakSpaces: + white_space_value_str = String("break-spaces"); + break; + } + } - if (id == CSSPropertyID::kWhiteSpaceCollapse || id == CSSPropertyID::kTextWrap) { - continue; - } + for (unsigned i = 0; i < count; ++i) { + auto prop = property_set->PropertyAt(i); + CSSPropertyID id = prop.Id(); + if (id == CSSPropertyID::kInvalid) continue; + if (id == CSSPropertyID::kWhiteSpaceCollapse || id == CSSPropertyID::kTextWrap) { + continue; + } + const auto* value_ptr = prop.Value(); + if (!value_ptr || !(*value_ptr)) continue; - int64_t value_slot = 0; - SharedNativeString* base_href = nullptr; + AtomicString prop_name = prop.Name().ToAtomicString(); + String base_href_string = property_set->GetPropertyBaseHrefWithHint(prop_name, i); - if ((*value_ptr)->IsIdentifierValue()) { - const auto& ident = To(*(*value_ptr)); - value_slot = -static_cast(ident.GetValueID()) - 1; - } else { - String value_string = property_set->GetPropertyValueWithHint(prop_name, i); - String base_href_string = property_set->GetPropertyBaseHrefWithHint(prop_name, i); - if (value_string.IsNull()) value_string = String(""); + if (id == CSSPropertyID::kVariable) { + String value_string = property_set->GetPropertyValueWithHint(prop_name, i); + if (value_string.IsNull()) { + value_string = (*value_ptr)->CssTextForSerialization(); + } + if (value_string.IsEmpty()) { + value_string = String(" "); + } - if (!value_string.IsEmpty()) { - auto* value_ns = stringToNativeString(value_string).release(); - value_slot = static_cast(reinterpret_cast(value_ns)); - } - if (!base_href_string.IsEmpty()) { - base_href = stringToNativeString(base_href_string.ToUTF8String()).release(); - } - } + auto key_ns = prop_name.ToStylePropertyNameNativeString(); + auto* payload = reinterpret_cast(dart_malloc(sizeof(NativeStyleValueWithHref))); + payload->value = stringToNativeString(value_string).release(); + if (!base_href_string.IsEmpty()) { + payload->href = stringToNativeString(base_href_string).release(); + } else { + payload->href = nullptr; + } + payload->important = prop.IsImportant() ? 1 : 0; + command_buffer->AddCommand(UICommand::kSetSheetStyle, std::move(key_ns), el->bindingObject(), payload); + continue; + } - ctx->uiCommandBuffer()->AddStyleByIdCommand(el->bindingObject(), static_cast(id), value_slot, - base_href); + int64_t value_slot = 0; + if ((*value_ptr)->IsIdentifierValue()) { + const auto& ident = To(*(*value_ptr)); + value_slot = -static_cast(ident.GetValueID()) - 1; + } else { + String value_string = property_set->GetPropertyValueWithHint(prop_name, i); + if (value_string.IsNull()) { + value_string = (*value_ptr)->CssTextForSerialization(); + } + if (value_string.IsEmpty()) { + continue; } + auto* value_ns = stringToNativeString(value_string).release(); + value_slot = static_cast(reinterpret_cast(value_ns)); + } - if (emit_white_space_shorthand) { - CSSValueID ws_value_id = CSSValueID::kInvalid; - EWhiteSpace ws = ToWhiteSpace(ws_collapse_enum, text_wrap_enum); - switch (ws) { - case EWhiteSpace::kNormal: - ws_value_id = CSSValueID::kNormal; - break; - case EWhiteSpace::kNowrap: - ws_value_id = CSSValueID::kNowrap; - break; - case EWhiteSpace::kPre: - ws_value_id = CSSValueID::kPre; - break; - case EWhiteSpace::kPreLine: - ws_value_id = CSSValueID::kPreLine; - break; - case EWhiteSpace::kPreWrap: - ws_value_id = CSSValueID::kPreWrap; - break; - case EWhiteSpace::kBreakSpaces: - ws_value_id = CSSValueID::kBreakSpaces; - break; - } + SharedNativeString* base_href = nullptr; + if (!base_href_string.IsEmpty()) { + base_href = stringToNativeString(base_href_string).release(); + } - int64_t value_slot = 0; - if (ws_value_id != CSSValueID::kInvalid) { - value_slot = -static_cast(ws_value_id) - 1; - } else if (!white_space_value_str.IsEmpty()) { - auto* value_ns = stringToNativeString(white_space_value_str).release(); - value_slot = static_cast(reinterpret_cast(value_ns)); - } + command_buffer->AddSheetStyleByIdCommand(el->bindingObject(), static_cast(id), value_slot, base_href, + prop.IsImportant()); + } - ctx->uiCommandBuffer()->AddStyleByIdCommand(el->bindingObject(), - static_cast(CSSPropertyID::kWhiteSpace), value_slot, - nullptr); - } + if (emit_white_space_shorthand) { + CSSValueID ws_value_id = CSSValueID::kInvalid; + EWhiteSpace ws = ToWhiteSpace(ws_collapse_enum, text_wrap_enum); + switch (ws) { + case EWhiteSpace::kNormal: + ws_value_id = CSSValueID::kNormal; + break; + case EWhiteSpace::kNowrap: + ws_value_id = CSSValueID::kNowrap; + break; + case EWhiteSpace::kPre: + ws_value_id = CSSValueID::kPre; + break; + case EWhiteSpace::kPreLine: + ws_value_id = CSSValueID::kPreLine; + break; + case EWhiteSpace::kPreWrap: + ws_value_id = CSSValueID::kPreWrap; + break; + case EWhiteSpace::kBreakSpaces: + ws_value_id = CSSValueID::kBreakSpaces; + break; + } - auto send_pseudo_for = [&](PseudoId pseudo_id, const char* pseudo_name) { - ElementRuleCollector pseudo_collector(state, SelectorChecker::kResolvingStyle); - pseudo_collector.SetPseudoElementStyleRequest(PseudoElementStyleRequest(pseudo_id)); - resolver.CollectAllRules(state, pseudo_collector, /*include_smil_properties*/ false); - pseudo_collector.SortAndTransferMatchedRules(); - - StyleCascade pseudo_cascade(state); - for (const auto& entry : pseudo_collector.GetMatchResult().GetMatchedProperties()) { - if (entry.is_inline_style) { - pseudo_cascade.MutableMatchResult().AddInlineStyleProperties(entry.properties); - } else { - pseudo_cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); - } - } + int64_t value_slot = 0; + if (ws_value_id != CSSValueID::kInvalid) { + value_slot = -static_cast(ws_value_id) - 1; + } else if (!white_space_value_str.IsEmpty()) { + auto* value_ns = stringToNativeString(white_space_value_str).release(); + value_slot = static_cast(reinterpret_cast(value_ns)); + } - std::shared_ptr pseudo_set = pseudo_cascade.ExportWinningPropertySet(); - if (!pseudo_set || pseudo_set->PropertyCount() == 0) return; - - auto pseudo_atom = AtomicString::CreateFromUTF8(pseudo_name); - auto pseudo_ns = pseudo_atom.ToNativeString(); - ctx->uiCommandBuffer()->AddCommand(UICommand::kClearPseudoStyle, std::move(pseudo_ns), - el->bindingObject(), nullptr); - for (unsigned i = 0; i < pseudo_set->PropertyCount(); ++i) { - auto prop = pseudo_set->PropertyAt(i); - CSSPropertyID id = prop.Id(); - if (id == CSSPropertyID::kInvalid) { - continue; - } - const auto* value_ptr = prop.Value(); - if (!value_ptr || !(*value_ptr)) { - continue; - } - AtomicString prop_name = prop.Name().ToAtomicString(); - String value_string = pseudo_set->GetPropertyValueWithHint(prop_name, i); - if (value_string.IsNull()) { - value_string = (*value_ptr)->CssTextForSerialization(); - } - if (id == CSSPropertyID::kVariable && value_string.IsEmpty()) { - value_string = String(" "); - } - String base_href_string = pseudo_set->GetPropertyBaseHrefWithHint(prop_name, i); - - auto key_ns = prop_name.ToStylePropertyNameNativeString(); - auto* payload = - reinterpret_cast(dart_malloc(sizeof(NativePseudoStyleWithHref))); - payload->key = key_ns.release(); - payload->value = stringToNativeString(value_string).release(); - if (!base_href_string.IsEmpty()) { - payload->href = stringToNativeString(base_href_string.ToUTF8String()).release(); - } else { - payload->href = nullptr; - } + command_buffer->AddSheetStyleByIdCommand(el->bindingObject(), static_cast(CSSPropertyID::kWhiteSpace), + value_slot, nullptr, /*important*/ false); + } - auto pseudo_atom2 = AtomicString::CreateFromUTF8(pseudo_name); - auto pseudo_ns2 = pseudo_atom2.ToNativeString(); - ctx->uiCommandBuffer()->AddCommand(UICommand::kSetPseudoStyle, std::move(pseudo_ns2), - el->bindingObject(), payload); - } - }; + auto send_pseudo_for = [&](PseudoId pseudo_id, const char* pseudo_name) { + ElementRuleCollector pseudo_collector(state, SelectorChecker::kResolvingStyle); + pseudo_collector.SetPseudoElementStyleRequest(PseudoElementStyleRequest(pseudo_id)); + resolver.CollectAllRules(state, pseudo_collector, /*include_smil_properties*/ false); + pseudo_collector.SortAndTransferMatchedRules(); - send_pseudo_for(PseudoId::kPseudoIdBefore, "before"); - send_pseudo_for(PseudoId::kPseudoIdAfter, "after"); - send_pseudo_for(PseudoId::kPseudoIdFirstLetter, "first-letter"); - send_pseudo_for(PseudoId::kPseudoIdFirstLine, "first-line"); + StyleCascade pseudo_cascade(state); + for (const auto& entry : pseudo_collector.GetMatchResult().GetMatchedProperties()) { + pseudo_cascade.MutableMatchResult().AddMatchedProperties(entry.properties, entry.origin, entry.layer_level); + } - InheritedState next_state; - next_state.inherited_values = std::move(inherited_values); - next_state.custom_vars = std::move(custom_vars); - return next_state; - }; + std::shared_ptr pseudo_set = pseudo_cascade.ExportWinningPropertySet(); + if (!pseudo_set || pseudo_set->PropertyCount() == 0) return; + + AtomicString pseudo_atom = AtomicString::CreateFromUTF8(pseudo_name); + command_buffer->AddCommand(UICommand::kClearPseudoStyle, pseudo_atom.ToNativeString(), el->bindingObject(), + nullptr); + + for (unsigned i = 0; i < pseudo_set->PropertyCount(); ++i) { + auto prop = pseudo_set->PropertyAt(i); + CSSPropertyID id = prop.Id(); + if (id == CSSPropertyID::kInvalid) { + continue; + } + const auto* value_ptr = prop.Value(); + if (!value_ptr || !(*value_ptr)) { + continue; + } + AtomicString prop_name = prop.Name().ToAtomicString(); + String value_string = pseudo_set->GetPropertyValueWithHint(prop_name, i); + if (value_string.IsNull()) { + value_string = (*value_ptr)->CssTextForSerialization(); + } + if (id == CSSPropertyID::kVariable && value_string.IsEmpty()) { + value_string = String(" "); + } + String base_href_string = pseudo_set->GetPropertyBaseHrefWithHint(prop_name, i); + + auto key_ns = prop_name.ToStylePropertyNameNativeString(); + auto* payload = reinterpret_cast(dart_malloc(sizeof(NativePseudoStyleWithHref))); + payload->key = key_ns.release(); + payload->value = stringToNativeString(value_string).release(); + if (!base_href_string.IsEmpty()) { + payload->href = stringToNativeString(base_href_string).release(); + } else { + payload->href = nullptr; + } + + command_buffer->AddCommand(UICommand::kSetPseudoStyle, pseudo_atom.ToNativeString(), el->bindingObject(), + payload); + } + }; + + if (emit_before) { + send_pseudo_for(PseudoId::kPseudoIdBefore, "before"); + } + if (emit_after) { + send_pseudo_for(PseudoId::kPseudoIdAfter, "after"); + } + if (emit_first_letter) { + send_pseudo_for(PseudoId::kPseudoIdFirstLetter, "first-letter"); + } + if (emit_first_line) { + send_pseudo_for(PseudoId::kPseudoIdFirstLine, "first-line"); + } + }; - InheritedState empty_state; - apply_for_element(&element, empty_state); + apply_for_element(&element); } void StyleEngine::RecalcStyle(StyleRecalcChange change, const StyleRecalcContext& style_recalc_context) { @@ -1975,15 +1995,15 @@ void StyleEngine::RecalcStyle(StyleRecalcChange change, const StyleRecalcContext Element* element = static_cast(node); if (element->NeedsStyleRecalc()) { StyleChangeType change_type = element->GetStyleChangeType(); - if (change_type == kInlineIndependentStyleChange) { - // Inline-only independent style changes affect this element but not - // its descendants. Recompute style just for this element to keep - // selector results in sync with the rest of the pipeline. + if (change_type == kInlineIndependentStyleChange || change_type == kLocalStyleChange) { + // Inline-only independent and local style changes only require rule + // matching for this element. Descendant style effects are handled via + // selector-based invalidation or Dart-side inheritance/var() updates. RecalcStyleForElementOnly(*element); element->ClearNeedsStyleRecalc(); } else { - // For local or subtree changes, recompute styles for this element and - // its descendants and then clear dirty bits in that subtree. + // For subtree changes, recompute styles for this element and its + // descendants and then clear dirty bits in that subtree. RecalcStyleForSubtree(*element); clear_flags_for_subtree(element); return; diff --git a/bridge/core/css/style_engine.h b/bridge/core/css/style_engine.h index 6f5834acde..6fc5aa2b19 100644 --- a/bridge/core/css/style_engine.h +++ b/bridge/core/css/style_engine.h @@ -138,7 +138,8 @@ class StyleEngine final { void RecalcStyleForSubtree(Element& root); // Recalculate styles for a single element only, without recursing into its // descendants. This is used for fine-grained style change types such as - // kInlineIndependentStyleChange where descendants are not affected. + // kInlineIndependentStyleChange and kLocalStyleChange where only the element + // itself needs rule matching (descendant effects are handled separately). void RecalcStyleForElementOnly(Element& element); // Returns true if there is a pending style invalidation root tracked by diff --git a/bridge/core/dom/element.cc b/bridge/core/dom/element.cc index 48274b73a2..f6f64d93e6 100644 --- a/bridge/core/dom/element.cc +++ b/bridge/core/dom/element.cc @@ -435,32 +435,14 @@ Element* Element::insertAdjacentElement(const AtomicString& position, } } -ElementStyle Element::style() { - if (GetExecutingContext()->isBlinkEnabled()) { - if (!IsStyledElement()) { - return static_cast(nullptr); - } - return inlineStyleForBlink(); - } - +legacy::LegacyInlineCssStyleDeclaration* Element::style() { if (!IsStyledElement()) { - return static_cast(nullptr); + return nullptr; } legacy::LegacyCssStyleDeclaration& style = EnsureElementRareData().EnsureLegacyInlineCSSStyleDeclaration(this); return To(&style); } -InlineCssStyleDeclaration* Element::inlineStyleForBlink() { - if (!IsStyledElement()) - return nullptr; - // Provide Blink inline style declaration when Blink engine is enabled; otherwise return nullptr. - if (!GetExecutingContext()->isBlinkEnabled()) { - return nullptr; - } - CSSStyleDeclaration& decl = EnsureElementRareData().EnsureInlineCSSStyleDeclaration(this); - return To(&decl); -} - DOMTokenList* Element::classList() { ElementRareDataVector& rare_data = EnsureElementRareData(); if (rare_data.GetClassList() == nullptr) { @@ -509,23 +491,11 @@ void Element::CloneNonAttributePropertiesFrom(const Element& other, CloneChildre // Clone the inline style from the legacy style declaration if (other.IsStyledElement() && this->IsStyledElement()) { // Get the source element's style - auto other_style = const_cast(other).style(); - auto this_style = this->style(); - - std::visit(MakeVisitorWithUnreachableWildcard( - [&](legacy::LegacyInlineCssStyleDeclaration* other_style, - legacy::LegacyInlineCssStyleDeclaration* this_style) { - if (other_style && !other_style->cssText().empty()) { - // Get or create this element's style and copy the CSS text - if (this_style) { - this_style->CopyWith(other_style); - } - } - }, - [&](InlineCssStyleDeclaration* other_style, InlineCssStyleDeclaration* this_style) { - // todo: - }), - other_style, this_style); + auto* other_style = const_cast(other).style(); + auto* this_style = style(); + if (other_style && this_style && !other_style->cssText().empty()) { + this_style->CopyWith(other_style); + } } // Also clone the inline style from element_data_ if it exists @@ -880,12 +850,10 @@ void Element::SynchronizeStyleAttributeInternal() { assert(GetElementData()->style_attribute_is_dirty()); GetElementData()->SetStyleAttributeIsDirty(false); - std::visit(MakeVisitor([&](auto&& inline_style) { - SetAttributeInternal(html_names::kStyleAttr, inline_style->cssText(), - AttributeModificationReason::kBySynchronizationOfLazyAttribute, - ASSERT_NO_EXCEPTION()); - }), - style()); + auto* inline_style = style(); + DCHECK(inline_style); + SetAttributeInternal(html_names::kStyleAttr, inline_style->cssText(), + AttributeModificationReason::kBySynchronizationOfLazyAttribute, ASSERT_NO_EXCEPTION()); } void Element::SetAttributeInternal(const webf::AtomicString& name, @@ -934,8 +902,11 @@ void Element::SynchronizeAttribute(const AtomicString& name) { void Element::InvalidateStyleAttribute(bool only_changed_independent_properties) { if (GetExecutingContext()->isBlinkEnabled()) { - DCHECK(HasElementData()); - GetElementData()->SetStyleAttributeIsDirty(true); + UniqueElementData& data = EnsureUniqueElementData(); + data.SetStyleAttributeIsDirty(true); + // Inline style is legacy-only in Blink mode; ensure native inline style + // snapshots do not participate in the native cascade. + data.inline_style_ = nullptr; SetNeedsStyleRecalc(only_changed_independent_properties ? kInlineIndependentStyleChange : kLocalStyleChange, StyleChangeReasonForTracing::Create(style_change_reason::kInlineCSSStyleMutated)); // Mirror Blink: treat inline style mutation as a style-attribute change @@ -1011,12 +982,8 @@ void Element::StyleAttributeChanged(const AtomicString& new_style_string, if (new_style_string.IsNull()) { EnsureUniqueElementData().inline_style_ = nullptr; - if (GetExecutingContext()->isBlinkEnabled()) { - // Clear all inline styles on Dart side when style attribute is removed. - if (InActiveDocument()) { - GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kClearStyle, nullptr, bindingObject(), nullptr); - } - } + auto&& legacy_inline_style = EnsureElementRareData().EnsureLegacyInlineCSSStyleDeclaration(this); + To(legacy_inline_style).SetCSSTextInternal(AtomicString::Empty()); } else { SetInlineStyleFromString(new_style_string); } @@ -1033,101 +1000,27 @@ void Element::StyleAttributeChanged(const AtomicString& new_style_string, SetNeedsStyleRecalc( kLocalStyleChange, StyleChangeReasonForTracing::Create(style_change_reason::kStyleAttributeChange)); + + // Schedule selector invalidations for style-attribute changes. This is + // required for rules like `[style] .child` where a mutation on this element + // can affect matching on descendants/siblings. + StyleEngine& engine = GetDocument().EnsureStyleEngine(); + engine.AttributeChangedForElement(html_names::kStyleAttr, *this); } } void Element::SetInlineStyleFromString(const webf::AtomicString& new_style_string) { - if (GetExecutingContext()->isBlinkEnabled()) { - DCHECK(IsStyledElement()); - std::shared_ptr inline_style = EnsureUniqueElementData().inline_style_; - - // Avoid redundant work if we're using shared attribute data with already - // parsed inline style. - if (inline_style && !GetElementData()->IsUnique()) { - return; - } - - // We reconstruct the property set instead of mutating if there is no CSSOM - // wrapper. This makes wrapperless property sets immutable and so cacheable. - if (inline_style && !inline_style->IsMutable()) { - inline_style = nullptr; - } - - if (!inline_style) { - inline_style = CSSParser::ParseInlineStyleDeclaration(new_style_string.ToUTF8String(), this); - } else { - DCHECK(inline_style->IsMutable()); - static_cast(const_cast(inline_style.get())) - ->ParseDeclarationList(new_style_string, GetDocument().ElementSheet().Contents()); - } - - // Persist the parsed inline style back to the element so CSSOM accessors - // (style(), cssText(), getPropertyValue(), serialization) reflect updates. - EnsureUniqueElementData().inline_style_ = inline_style; - - // Emit declared style updates to Dart as raw CSS strings (no C++ evaluation). - // This keeps values like calc(), var(), and viewport units intact for Dart-side evaluation. - if (inline_style && InActiveDocument()) { - unsigned count = inline_style->PropertyCount(); - // Always clear existing inline styles before applying new set to avoid stale properties. - GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kClearStyle, nullptr, bindingObject(), nullptr); - for (unsigned i = 0; i < count; ++i) { - auto property = inline_style->PropertyAt(i); - CSSPropertyID id = property.Id(); - if (id == CSSPropertyID::kInvalid) { - continue; - } - const auto* value_ptr = property.Value(); - if (!value_ptr || !(*value_ptr)) { - // Skip parse-error or missing values; they should not be forwarded to Dart. - continue; - } - AtomicString prop_name = property.Name().ToAtomicString(); - if (id == CSSPropertyID::kVariable) { - String value_string = inline_style->GetPropertyValueWithHint(prop_name, i); - if (value_string.IsNull()) { - value_string = (*value_ptr)->CssTextForSerialization(); - } - if (value_string.IsEmpty()) { - value_string = String(" "); - } - - // Normalize CSS property names (e.g. background-color, text-align) to the - // camelCase form expected by the Dart style engine before sending them - // across the bridge. Custom properties starting with '--' are preserved - // verbatim by ToStylePropertyNameNativeString(). - std::unique_ptr args_01 = prop_name.ToStylePropertyNameNativeString(); - auto* payload = reinterpret_cast(dart_malloc(sizeof(NativeStyleValueWithHref))); - payload->value = stringToNativeString(value_string).release(); - payload->href = nullptr; - GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kSetStyle, std::move(args_01), bindingObject(), - payload); - continue; - } - - int64_t value_slot = 0; - if ((*value_ptr)->IsIdentifierValue()) { - const auto& ident = To(*(*value_ptr)); - value_slot = -static_cast(ident.GetValueID()) - 1; - } else { - String value_string = inline_style->GetPropertyValueWithHint(prop_name, i); - if (value_string.IsNull()) { - value_string = (*value_ptr)->CssTextForSerialization(); - } - if (!value_string.IsEmpty()) { - auto* value_ns = stringToNativeString(value_string).release(); - value_slot = static_cast(reinterpret_cast(value_ns)); - } - } + DCHECK(IsStyledElement()); - GetExecutingContext()->uiCommandBuffer()->AddStyleByIdCommand(bindingObject(), static_cast(id), - value_slot, nullptr); - } - } - } else { - auto&& legacy_inline_style = EnsureElementRareData().EnsureLegacyInlineCSSStyleDeclaration(this); - To(legacy_inline_style).SetCSSTextInternal(new_style_string); + // Inline style is treated as legacy-only even when Blink CSS is enabled. + // Forward raw inline declarations to Dart and keep the native style engine + // focused on non-inline (sheet) styles. + if (GetExecutingContext()->isBlinkEnabled() && HasElementData()) { + EnsureUniqueElementData().inline_style_ = nullptr; } + + auto&& legacy_inline_style = EnsureElementRareData().EnsureLegacyInlineCSSStyleDeclaration(this); + To(legacy_inline_style).SetCSSTextInternal(new_style_string); } String Element::outerHTML() { diff --git a/bridge/core/dom/element.d.ts b/bridge/core/dom/element.d.ts index 88a1b1f907..3e0f34d755 100644 --- a/bridge/core/dom/element.d.ts +++ b/bridge/core/dom/element.d.ts @@ -2,14 +2,11 @@ import {Node} from "./node"; import {Document} from "./document"; import {ScrollToOptions} from "./scroll_to_options"; import { ElementAttributes } from './legacy/element_attributes'; -import {CSSStyleDeclaration} from "../css/css_style_declaration"; +import {LegacyInlineCssStyleDeclaration} from "../css/legacy/legacy_inline_css_style_declaration"; import {ParentNode} from "./parent_node"; import {ChildNode} from "./child_node"; import {Blob} from "../fileapi/blob"; -// Forward-decl for std::variant ElementStyle -declare class ElementStyleVariant{} - interface Element extends Node, ParentNode, ChildNode { id: string; className: string; @@ -17,7 +14,7 @@ interface Element extends Node, ParentNode, ChildNode { readonly dataset: DOMStringMap; name: DartImpl; readonly attributes: ElementAttributes; - readonly style: ElementStyleVariant; + readonly style: LegacyInlineCssStyleDeclaration; readonly clientHeight: SupportAsync>>; readonly clientLeft: SupportAsync>>; readonly clientTop: SupportAsync>>; diff --git a/bridge/core/dom/element.h b/bridge/core/dom/element.h index 50ff53d33f..5a7c96a5f2 100644 --- a/bridge/core/dom/element.h +++ b/bridge/core/dom/element.h @@ -10,7 +10,6 @@ #include "bindings/qjs/cppgc/garbage_collected.h" #include "bindings/qjs/script_promise.h" #include "container_node.h" -#include "core/css/inline_css_style_declaration.h" #include "core/css/legacy/legacy_inline_css_style_declaration.h" #include "core/dom/attribute_collection.h" #include "core/dom/element_rare_data_vector.h" @@ -22,7 +21,6 @@ #include "parent_node.h" #include "plugin_api/element.h" #include "qjs_scroll_to_options.h" -#include "foundation/utility/make_visitor.h" namespace webf { @@ -30,6 +28,7 @@ class ShadowRoot; class StyleScopeData; class StyleRecalcChange; class StyleRecalcContext; +class MutableCSSPropertyValueSet; enum class ElementFlags { kTabIndexWasSetExplicitly = 1 << 0, @@ -46,8 +45,6 @@ enum class ElementFlags { using ScrollOffset = gfx::Vector2dF; -using ElementStyle = std::variant; - class Element : public ContainerNode { DEFINE_WRAPPERTYPEINFO(); @@ -194,10 +191,8 @@ class Element : public ContainerNode { Element* insertAdjacentElement(const AtomicString& position, Element* element, ExceptionState& exception_state); - // InlineCssStyleDeclaration* style(); - ElementStyle style(); - // Blink-only inline style accessor (not exposed to legacy bindings). - InlineCssStyleDeclaration* inlineStyleForBlink(); + // CSSOM inline style (legacy-only). + legacy::LegacyInlineCssStyleDeclaration* style(); DOMTokenList* classList(); DOMStringMap* dataset(); diff --git a/bridge/core/dom/legacy/element_attributes.cc b/bridge/core/dom/legacy/element_attributes.cc index 63f5ac39b5..9115a3d046 100644 --- a/bridge/core/dom/legacy/element_attributes.cc +++ b/bridge/core/dom/legacy/element_attributes.cc @@ -10,7 +10,6 @@ #include "core/html/custom/widget_element.h" #include "foundation/native_value_converter.h" #include "foundation/string/string_builder.h" -#include "foundation/utility/make_visitor.h" #include "html_names.h" namespace webf { @@ -107,6 +106,8 @@ void ElementAttributes::removeAttribute(const AtomicString& name, ExceptionState std::unique_ptr args_01 = name.ToNativeString(); GetExecutingContext()->uiCommandBuffer()->AddCommand(UICommand::kRemoveAttribute, std::move(args_01), element_->bindingObject(), nullptr); + + element_->DidModifyAttribute(name, old_value, AtomicString::Null(), Element::AttributeModificationReason::kDirectly); } void ElementAttributes::CopyWith(ElementAttributes* attributes) { @@ -133,11 +134,10 @@ String ElementAttributes::ToString() { } else { if (element_ != nullptr) { builder.Append("\""_s); - std::visit(MakeVisitor([&](auto* style) { - if (style != nullptr) { - builder.Append(style->ToString()); - } - }), element_->style()); + auto* style = element_->style(); + if (style != nullptr) { + builder.Append(style->ToString()); + } builder.Append("\""_s); } } diff --git a/bridge/core/frame/window.cc b/bridge/core/frame/window.cc index afea358a48..fea3455782 100644 --- a/bridge/core/frame/window.cc +++ b/bridge/core/frame/window.cc @@ -316,8 +316,7 @@ legacy::LegacyComputedCssStyleDeclaration* Window::getComputedStyle(Element* ele // When Blink CSS engine is enabled, ensure any pending selector-based // invalidations are applied before querying the Dart-side computed style. // This keeps window.getComputedStyle() in sync with the latest styles - // produced by the native StyleEngine, including background-clip:text - // gradients that are resolved via Blink and forwarded as inline styles. + // produced by the native StyleEngine and forwarded to Dart as sheet styles. ExecutingContext* context = GetExecutingContext(); if (context && context->isBlinkEnabled()) { Document* doc = context->document(); diff --git a/bridge/foundation/native_type.h b/bridge/foundation/native_type.h index 7d03432ad2..afaa196783 100644 --- a/bridge/foundation/native_type.h +++ b/bridge/foundation/native_type.h @@ -80,12 +80,13 @@ struct NativeMap : public DartReadable { uint32_t length{0}; }; -// Combined style value + base href payload for UICommand::kSetStyle. +// Combined style value + base href payload for UICommand::kSetInlineStyle and UICommand::kSetSheetStyle. // - |value| holds the serialized CSS value (NativeString*). // - |href| holds an optional base href (NativeString*), or nullptr if absent. struct NativeStyleValueWithHref : public DartReadable { SharedNativeString* value{nullptr}; SharedNativeString* href{nullptr}; + int32_t important{0}; // 0 = not important, 1 = important }; // Combined style property/value id + base href payload for UICommand::kSetStyleById. diff --git a/bridge/foundation/shared_ui_command.cc b/bridge/foundation/shared_ui_command.cc index 268844002a..08a0b0683c 100644 --- a/bridge/foundation/shared_ui_command.cc +++ b/bridge/foundation/shared_ui_command.cc @@ -91,6 +91,30 @@ void SharedUICommand::AddStyleByIdCommand(void* native_binding_object, ui_command_sync_strategy_->RecordStyleByIdCommand(item, request_ui_update); } +void SharedUICommand::AddSheetStyleByIdCommand(void* native_binding_object, + int32_t property_id, + int64_t value_slot, + SharedNativeString* base_href, + bool important, + bool request_ui_update) { + if (!context_->isDedicated()) { + std::lock_guard lock(read_buffer_mutex_); + read_buffer_->AddSheetStyleByIdCommand(native_binding_object, property_id, value_slot, base_href, important, + request_ui_update); + return; + } + + UICommandItem item{}; + item.type = static_cast(UICommand::kSetSheetStyleById); + uint32_t encoded_property_id = + static_cast(property_id) | (important ? 0x80000000u : 0u); + item.args_01_length = static_cast(encoded_property_id); + item.string_01 = value_slot; + item.nativePtr = static_cast(reinterpret_cast(native_binding_object)); + item.nativePtr2 = static_cast(reinterpret_cast(base_href)); + ui_command_sync_strategy_->RecordStyleByIdCommand(item, request_ui_update); +} + void* SharedUICommand::data() { std::lock_guard lock(read_buffer_mutex_); diff --git a/bridge/foundation/shared_ui_command.h b/bridge/foundation/shared_ui_command.h index b3b4f26720..bf932d5d41 100644 --- a/bridge/foundation/shared_ui_command.h +++ b/bridge/foundation/shared_ui_command.h @@ -46,6 +46,15 @@ class SharedUICommand : public DartReadable { SharedNativeString* base_href, bool request_ui_update = true); + // Fast-path for UICommand::kSetSheetStyleById without allocating a payload struct. + // See UICommandBuffer::AddSheetStyleByIdCommand for the data encoding. + void AddSheetStyleByIdCommand(void* native_binding_object, + int32_t property_id, + int64_t value_slot, + SharedNativeString* base_href, + bool important, + bool request_ui_update = true); + void ConfigureSyncCommandBufferSize(size_t size); void* data(); diff --git a/bridge/foundation/shared_ui_command_test.cc b/bridge/foundation/shared_ui_command_test.cc index 34190f89e5..f4fa1bb5a3 100644 --- a/bridge/foundation/shared_ui_command_test.cc +++ b/bridge/foundation/shared_ui_command_test.cc @@ -438,7 +438,7 @@ TEST_F(SharedUICommandTest, SyncStrategyIntegration) { // Add commands that go to waiting queue shared_command_->AddCommand(UICommand::kCreateElement, CreateSharedString("div"), nullptr, nullptr); - shared_command_->AddCommand(UICommand::kSetStyle, CreateSharedString("color:blue"), nullptr, nullptr); + shared_command_->AddCommand(UICommand::kSetInlineStyle, CreateSharedString("color:blue"), nullptr, nullptr); // In non-dedicated mode, commands go directly to read buffer // In dedicated mode, they would go to waiting queue @@ -523,7 +523,7 @@ TEST_F(SharedUICommandTest, CommandCategorizationSync) { // Test waiting queue commands shared_command_->AddCommand(UICommand::kSetAttribute, CreateSharedString("attr"), nullptr, nullptr); - shared_command_->AddCommand(UICommand::kSetStyle, CreateSharedString("style"), nullptr, nullptr); + shared_command_->AddCommand(UICommand::kSetInlineStyle, CreateSharedString("style"), nullptr, nullptr); shared_command_->AddCommand(UICommand::kDisposeBindingObject, nullptr, nullptr, nullptr); // These should be in waiting queue until we force sync @@ -538,4 +538,4 @@ TEST_F(SharedUICommandTest, CommandCategorizationSync) { EXPECT_GE(pack2->length, 3); } dart_free(pack2); -} \ No newline at end of file +} diff --git a/bridge/foundation/ui_command_buffer.cc b/bridge/foundation/ui_command_buffer.cc index 04126be63b..70fd3f9908 100644 --- a/bridge/foundation/ui_command_buffer.cc +++ b/bridge/foundation/ui_command_buffer.cc @@ -34,12 +34,15 @@ UICommandKind GetKindFromUICommand(UICommand command) { case UICommand::kAddEvent: case UICommand::kRemoveEvent: return UICommandKind::kEvent; - case UICommand::kSetStyle: + case UICommand::kSetInlineStyle: case UICommand::kSetStyleById: case UICommand::kSetPseudoStyle: case UICommand::kRemovePseudoStyle: case UICommand::kClearPseudoStyle: case UICommand::kClearStyle: + case UICommand::kClearSheetStyle: + case UICommand::kSetSheetStyle: + case UICommand::kSetSheetStyleById: return UICommandKind::kStyleUpdate; case UICommand::kSetAttribute: case UICommand::kRemoveAttribute: @@ -96,6 +99,24 @@ void UICommandBuffer::AddStyleByIdCommand(void* nativePtr, addCommand(item, request_ui_update); } +void UICommandBuffer::AddSheetStyleByIdCommand(void* nativePtr, + int32_t property_id, + int64_t value_slot, + SharedNativeString* base_href, + bool important, + bool request_ui_update) { + UICommandItem item{}; + item.type = static_cast(UICommand::kSetSheetStyleById); + uint32_t encoded_property_id = + static_cast(property_id) | (important ? 0x80000000u : 0u); + item.args_01_length = static_cast(encoded_property_id); + item.string_01 = value_slot; + item.nativePtr = static_cast(reinterpret_cast(nativePtr)); + item.nativePtr2 = static_cast(reinterpret_cast(base_href)); + updateFlags(UICommand::kSetSheetStyleById); + addCommand(item, request_ui_update); +} + void UICommandBuffer::updateFlags(UICommand command) { UICommandKind type = GetKindFromUICommand(command); kind_flag = kind_flag | type; diff --git a/bridge/foundation/ui_command_buffer.h b/bridge/foundation/ui_command_buffer.h index 9d249c075e..6d04fa4be6 100644 --- a/bridge/foundation/ui_command_buffer.h +++ b/bridge/foundation/ui_command_buffer.h @@ -40,7 +40,7 @@ enum class UICommand { kAddEvent, kRemoveNode, kInsertAdjacentNode, - kSetStyle, + kSetInlineStyle, kSetPseudoStyle, kClearStyle, kSetAttribute, @@ -65,7 +65,13 @@ enum class UICommand { kRemoveIntersectionObserver, kDisconnectIntersectionObserver, // Append-only: set inline style using CSSPropertyID/CSSValueID integers (Blink mode fast-path). - kSetStyleById + kSetStyleById, + // Append-only: clear non-inline (sheet) styles emitted from native Blink engine. + kClearSheetStyle, + // Append-only: set non-inline (sheet) style property/value emitted from native Blink engine. + kSetSheetStyle, + // Append-only: set non-inline (sheet) style using CSSPropertyID/CSSValueID ints (Blink mode fast-path). + kSetSheetStyleById }; #define MAXIMUM_UI_COMMAND_SIZE 2048 @@ -108,6 +114,19 @@ class UICommandBuffer { int64_t value_slot, SharedNativeString* base_href, bool request_ui_update = true); + // Fast-path for UICommand::kSetSheetStyleById without allocating a payload struct. + // Encoding: + // - args_01_length: property id (CSSPropertyID integer value), with !important encoded in the MSB: + // encoded = property_id | (important ? 0x80000000 : 0). + // - string_01: either a pointer to a NativeString (SharedNativeString*) holding the value (>= 0), + // or a negative immediate CSSValueID: -(value_id + 1). + // - nativePtr2: optional base href NativeString (SharedNativeString*) pointer (may be nullptr). + virtual void AddSheetStyleByIdCommand(void* nativePtr, + int32_t property_id, + int64_t value_slot, + SharedNativeString* base_href, + bool important, + bool request_ui_update = true); UICommandItem* data(); uint32_t kindFlag(); int64_t size(); diff --git a/bridge/foundation/ui_command_ring_buffer_test.cc b/bridge/foundation/ui_command_ring_buffer_test.cc index 0f5d729848..7d1f414996 100644 --- a/bridge/foundation/ui_command_ring_buffer_test.cc +++ b/bridge/foundation/ui_command_ring_buffer_test.cc @@ -181,7 +181,7 @@ TEST_F(UICommandRingBufferTest, CommandBatchingStrategy) { // Test split on special commands package.Clear(); - package.AddCommand(UICommandItem(static_cast(UICommand::kSetStyle), nullptr, nullptr, nullptr)); + package.AddCommand(UICommandItem(static_cast(UICommand::kSetInlineStyle), nullptr, nullptr, nullptr)); EXPECT_TRUE(package.ShouldSplit(UICommand::kStartRecordingCommand)); EXPECT_TRUE(package.ShouldSplit(UICommand::kFinishRecordingCommand)); EXPECT_TRUE(package.ShouldSplit(UICommand::kAsyncCaller)); @@ -234,4 +234,4 @@ TEST_F(UICommandRingBufferTest, StressTestHighVolume) { EXPECT_TRUE(buffer.Empty()); } -} // namespace webf \ No newline at end of file +} // namespace webf diff --git a/bridge/foundation/ui_command_strategy.cc b/bridge/foundation/ui_command_strategy.cc index fdc2ea1de2..549f12cd2f 100644 --- a/bridge/foundation/ui_command_strategy.cc +++ b/bridge/foundation/ui_command_strategy.cc @@ -98,8 +98,11 @@ void UICommandSyncStrategy::RecordUICommand(UICommand type, break; } - case UICommand::kSetStyle: + case UICommand::kSetInlineStyle: case UICommand::kSetStyleById: + case UICommand::kClearSheetStyle: + case UICommand::kSetSheetStyle: + case UICommand::kSetSheetStyleById: case UICommand::kSetPseudoStyle: case UICommand::kRemovePseudoStyle: case UICommand::kClearPseudoStyle: diff --git a/bridge/foundation/ui_command_strategy_test.cc b/bridge/foundation/ui_command_strategy_test.cc index ed0844c318..03c80ca5e2 100644 --- a/bridge/foundation/ui_command_strategy_test.cc +++ b/bridge/foundation/ui_command_strategy_test.cc @@ -96,7 +96,7 @@ TEST_F(UICommandSyncStrategyTest, WaitingQueueCommands) { CreateSharedString("div"), &obj1, nullptr, true); EXPECT_EQ(strategy_->GetWaitingCommandsCount(), 1); - strategy_->RecordUICommand(UICommand::kSetStyle, + strategy_->RecordUICommand(UICommand::kSetInlineStyle, CreateSharedString("color:red"), &obj1, nullptr, true); EXPECT_EQ(strategy_->GetWaitingCommandsCount(), 2); @@ -155,7 +155,7 @@ TEST_F(UICommandSyncStrategyTest, ResetClearsEverything) { // Add various commands strategy_->RecordUICommand(UICommand::kCreateElement, CreateSharedString("div"), &obj1, nullptr, true); - strategy_->RecordUICommand(UICommand::kSetStyle, + strategy_->RecordUICommand(UICommand::kSetInlineStyle, CreateSharedString("width:100px"), &obj1, nullptr, true); strategy_->RecordUICommand(UICommand::kInsertAdjacentNode, CreateSharedString("beforeend"), &obj1, &obj2, true); @@ -192,7 +192,7 @@ TEST_F(UICommandSyncStrategyTest, CommandCategorization) { EXPECT_EQ(strategy_->GetWaitingCommandsCount(), 1); // 3. Simple waiting queue commands - strategy_->RecordUICommand(UICommand::kSetStyle, + strategy_->RecordUICommand(UICommand::kSetInlineStyle, CreateSharedString("margin:10px"), &obj, nullptr, true); EXPECT_EQ(strategy_->GetWaitingCommandsCount(), 2); @@ -250,7 +250,7 @@ TEST_F(UICommandSyncStrategyTest, IntegrationWithSharedUICommand) { CreateSharedString("integration"), &obj, nullptr, true); // The command should be recorded by the strategy - shared_command_->AddCommand(UICommand::kSetStyle, + shared_command_->AddCommand(UICommand::kSetInlineStyle, CreateSharedString("display:block"), &obj, nullptr, true); // Trigger sync with a finish command @@ -265,4 +265,4 @@ TEST_F(UICommandSyncStrategyTest, IntegrationWithSharedUICommand) { EXPECT_GE(pack->length, 0); dart_free(pack); -} \ No newline at end of file +} diff --git a/bridge/test/css_unittests.cmake b/bridge/test/css_unittests.cmake index 57db2ce331..bd048f54c9 100644 --- a/bridge/test/css_unittests.cmake +++ b/bridge/test/css_unittests.cmake @@ -11,6 +11,7 @@ list(APPEND WEBF_CSS_UNIT_TEST_SOURCE ./core/css/resolver/style_cascade_test.cc ./core/css/resolver/selector_specificity_test.cc ./core/css/inline_style_test.cc + ./core/css/blink_inline_style_validation_test.cc ./core/css/selector_test.cc ./core/css/css_initial_test.cc ./core/css/css_selector_test.cc diff --git a/bridge/test/webf_test_context.cc b/bridge/test/webf_test_context.cc index 1ccdb305de..115a0523f4 100644 --- a/bridge/test/webf_test_context.cc +++ b/bridge/test/webf_test_context.cc @@ -279,7 +279,7 @@ static JSValue syncThreadBuffer(JSContext* ctx, JSValueConst this_val, int argc, // NOTE: if we snapshot after document.body.appendChild, we may not get all styles as // the style recalc cannot catch up the UICommand flush. This is a work-ground of the issue. // `__webf_sync_buffer__` is used by the snapshot test harness to flush pending UI commands. - // Ensure declared-value styles are recalculated and emitted as `kSetStyle` commands before syncing. + // Ensure declared-value styles are recalculated and emitted as `kSetInlineStyle` commands before syncing. if (context != nullptr && context->isBlinkEnabled()) { if (auto* document = context->document()) { MemberMutationScope scope{context}; diff --git a/integration_tests/snapshots/css/css-flexbox/relayout-align-to-stretch.ts.f379be341.png b/integration_tests/snapshots/css/css-flexbox/relayout-align-to-stretch.ts.f379be341.png index 97480c237b..aa9ea13096 100644 Binary files a/integration_tests/snapshots/css/css-flexbox/relayout-align-to-stretch.ts.f379be341.png and b/integration_tests/snapshots/css/css-flexbox/relayout-align-to-stretch.ts.f379be341.png differ diff --git a/integration_tests/specs/css/css-inline/important-semantics.ts b/integration_tests/specs/css/css-inline/important-semantics.ts new file mode 100644 index 0000000000..07628a6a82 --- /dev/null +++ b/integration_tests/specs/css/css-inline/important-semantics.ts @@ -0,0 +1,129 @@ +describe('important semantics', () => { + function addStyle(text: string) { + const style = document.createElement('style'); + style.textContent = text; + document.head.appendChild(style); + return style; + } + + it('stylesheet important overrides inline normal', async () => { + const style = addStyle('.important-inline { color: rgb(0, 128, 0) !important; }'); + const target = document.createElement('div'); + target.className = 'important-inline'; + target.setAttribute('style', 'color: rgb(255, 0, 0);'); + target.textContent = 'inline normal vs stylesheet important'; + document.body.appendChild(target); + + await waitForFrame(); + + const color = getComputedStyle(target).color; + expect(color.indexOf('0, 128, 0') >= 0 || color === 'green').toBeTrue(); + + style.remove(); + target.remove(); + await waitForFrame(); + }); + + it('inline important overrides stylesheet important', async () => { + const style = addStyle('.important-inline-win { color: rgb(0, 128, 0) !important; }'); + const target = document.createElement('div'); + target.className = 'important-inline-win'; + target.setAttribute('style', 'color: rgb(255, 0, 0) !important;'); + target.textContent = 'inline important vs stylesheet important'; + document.body.appendChild(target); + + await waitForFrame(); + + const color = getComputedStyle(target).color; + expect(color.indexOf('255, 0, 0') >= 0 || color === 'red').toBeTrue(); + + style.remove(); + target.remove(); + await waitForFrame(); + }); + + it('inline important via CSSOM setProperty overrides stylesheet important', async () => { + const style = addStyle('.important-inline-cssom { color: rgb(0, 128, 0) !important; }'); + const target = document.createElement('div'); + target.className = 'important-inline-cssom'; + target.textContent = 'cssom setProperty important'; + document.body.appendChild(target); + + target.style.setProperty('color', 'rgb(255, 0, 0)', 'important'); + await waitForFrame(); + + const color = getComputedStyle(target).color; + expect(color.indexOf('255, 0, 0') >= 0 || color === 'red').toBeTrue(); + + style.remove(); + target.remove(); + await waitForFrame(); + }); + + it('inline important via cssText overrides stylesheet important', async () => { + const style = addStyle('.important-inline-text { color: rgb(0, 128, 0) !important; }'); + const target = document.createElement('div'); + target.className = 'important-inline-text'; + target.textContent = 'cssText important'; + document.body.appendChild(target); + + target.style.cssText = 'color: rgb(255, 0, 0) !important;'; + await waitForFrame(); + + const color = getComputedStyle(target).color; + expect(color.indexOf('255, 0, 0') >= 0 || color === 'red').toBeTrue(); + + style.remove(); + target.remove(); + await waitForFrame(); + }); + + it('clearing inline important restores stylesheet important', async () => { + const style = addStyle('.important-inline-clear { color: rgb(0, 128, 0) !important; }'); + const target = document.createElement('div'); + target.className = 'important-inline-clear'; + target.textContent = 'clear inline important'; + document.body.appendChild(target); + + target.setAttribute('style', 'color: rgb(255, 0, 0) !important;'); + await waitForFrame(); + + let color = getComputedStyle(target).color; + expect(color.indexOf('255, 0, 0') >= 0 || color === 'red').toBeTrue(); + + target.removeAttribute('style'); + await waitForFrame(); + + color = getComputedStyle(target).color; + expect(color.indexOf('0, 128, 0') >= 0 || color === 'green').toBeTrue(); + + style.remove(); + target.remove(); + await waitForFrame(); + }); + + it('stylesheet important beats later non-important and yields to later important', async () => { + const styleA = addStyle('.sheet-important { color: rgb(0, 0, 255) !important; }'); + const styleB = addStyle('.sheet-important { color: rgb(255, 0, 0); }'); + const target = document.createElement('div'); + target.className = 'sheet-important'; + target.textContent = 'sheet important ordering'; + document.body.appendChild(target); + + await waitForFrame(); + + let color = getComputedStyle(target).color; + expect(color.indexOf('0, 0, 255') >= 0 || color === 'blue').toBeTrue(); + + styleB.textContent = '.sheet-important { color: rgb(255, 0, 0) !important; }'; + await waitForFrame(); + + color = getComputedStyle(target).color; + expect(color.indexOf('255, 0, 0') >= 0 || color === 'red').toBeTrue(); + + styleA.remove(); + styleB.remove(); + target.remove(); + await waitForFrame(); + }); +}); diff --git a/integration_tests/specs/css/css-transforms/translate.ts b/integration_tests/specs/css/css-transforms/translate.ts index f672220597..a631d94d00 100644 --- a/integration_tests/specs/css/css-transforms/translate.ts +++ b/integration_tests/specs/css/css-transforms/translate.ts @@ -220,6 +220,7 @@ describe('Transform translate', () => { ] ); BODY.appendChild(div); + await waitForFrame(); expect(div.clientWidth === div.scrollWidth).toBe(true); }); diff --git a/webf/lib/src/bridge/native_types.dart b/webf/lib/src/bridge/native_types.dart index 782450ab70..d675e50c23 100644 --- a/webf/lib/src/bridge/native_types.dart +++ b/webf/lib/src/bridge/native_types.dart @@ -47,12 +47,14 @@ final class NativeMap extends Struct { external int length; } -// Combined style value + base href payload for UICommandType.setStyle. +// Combined style value + base href payload for UICommandType.setInlineStyle and UICommandType.setSheetStyle. // - |value| is the CSS value as NativeString. // - |href| is an optional base href as NativeString (may be nullptr). final class NativeStyleValueWithHref extends Struct { external Pointer value; external Pointer href; + @Int32() + external int important; // 0 = not important, 1 = important } // Combined style property/value id + base href payload for UICommandType.setStyleById. diff --git a/webf/lib/src/bridge/to_native.dart b/webf/lib/src/bridge/to_native.dart index f136506111..0b894bbcc3 100644 --- a/webf/lib/src/bridge/to_native.dart +++ b/webf/lib/src/bridge/to_native.dart @@ -810,7 +810,7 @@ enum UICommandType { addEvent, removeNode, insertAdjacentNode, - setStyle, + setInlineStyle, setPseudoStyle, clearStyle, setAttribute, @@ -835,6 +835,12 @@ enum UICommandType { disconnectIntersectionObserver, // Append-only: set inline style using Blink CSSPropertyID/CSSValueID ints. setStyleById, + // Append-only: clear non-inline (sheet) styles emitted from native Blink engine. + clearSheetStyle, + // Append-only: set non-inline (sheet) style property/value emitted from native Blink engine. + setSheetStyle, + // Append-only: set non-inline (sheet) style using Blink CSSPropertyID/CSSValueID ints. + setSheetStyleById, } final class UICommandItem extends Struct { diff --git a/webf/lib/src/bridge/ui_command.dart b/webf/lib/src/bridge/ui_command.dart index 3fc68a514b..23770d2540 100644 --- a/webf/lib/src/bridge/ui_command.dart +++ b/webf/lib/src/bridge/ui_command.dart @@ -87,7 +87,8 @@ List nativeUICommandToDartFFI(double contextId) { // Extract type command.type = UICommandType.values[commandItem.type]; - if (command.type == UICommandType.setStyleById) { + if (command.type == UICommandType.setStyleById || + command.type == UICommandType.setSheetStyleById) { command.args = ''; command.nativePtr = commandItem.nativePtr != 0 ? Pointer.fromAddress(commandItem.nativePtr) : nullptr; command.nativePtr2 = commandItem.nativePtr2 != 0 ? Pointer.fromAddress(commandItem.nativePtr2) : nullptr; @@ -136,15 +137,17 @@ void execUICommands(WebFViewController view, List commands) { if (enableWebFCommandLog) { String printMsg; switch(command.type) { - case UICommandType.setStyle: + case UICommandType.setInlineStyle: String? valueLog; String? baseHrefLog; + int? importantLog; if (command.nativePtr2 != nullptr) { try { final Pointer payload = command.nativePtr2.cast(); final Pointer valuePtr = payload.ref.value; final Pointer hrefPtr = payload.ref.href; + importantLog = payload.ref.important; if (valuePtr != nullptr) { valueLog = nativeStringToString(valuePtr); } @@ -154,10 +157,11 @@ void execUICommands(WebFViewController view, List commands) { } catch (_) { valueLog = ''; baseHrefLog = ''; + importantLog = null; } } printMsg = - 'nativePtr: ${command.nativePtr} type: ${command.type} key: ${command.args} value: $valueLog baseHref: ${baseHrefLog ?? 'null'}'; + 'nativePtr: ${command.nativePtr} type: ${command.type} key: ${command.args} value: $valueLog important: ${importantLog ?? 0} baseHref: ${baseHrefLog ?? 'null'}'; break; case UICommandType.setStyleById: final String keyLog = blinkStylePropertyNameFromId(command.stylePropertyId); @@ -183,6 +187,33 @@ void execUICommands(WebFViewController view, List commands) { printMsg = 'nativePtr: ${command.nativePtr} type: ${command.type} propertyId: ${command.stylePropertyId} key: $keyLog value: ${valueLog ?? ''} baseHref: ${baseHrefLog ?? 'null'}'; break; + case UICommandType.setSheetStyleById: + final int encoded = command.stylePropertyId; + final bool important = (encoded & 0x80000000) != 0; + final int propertyId = encoded & 0x7fffffff; + final String keyLog = blinkStylePropertyNameFromId(propertyId); + String? valueLog; + String? baseHrefLog; + final int slot = command.styleValueSlot; + if (slot < 0) { + valueLog = blinkKeywordFromValueId(-slot - 1); + } else if (slot > 0) { + try { + valueLog = nativeStringToString(Pointer.fromAddress(slot)); + } catch (_) { + valueLog = ''; + } + } + if (command.nativePtr2 != nullptr) { + try { + baseHrefLog = nativeStringToString(command.nativePtr2.cast()); + } catch (_) { + baseHrefLog = ''; + } + } + printMsg = + 'nativePtr: ${command.nativePtr} type: ${command.type} propertyId: $propertyId important: ${important ? 1 : 0} key: $keyLog value: ${valueLog ?? ''} baseHref: ${baseHrefLog ?? 'null'}'; + break; case UICommandType.setPseudoStyle: if (command.nativePtr2 != nullptr) { try { @@ -298,14 +329,16 @@ void execUICommands(WebFViewController view, List commands) { case UICommandType.cloneNode: view.cloneNode(nativePtr.cast(), command.nativePtr2.cast()); break; - case UICommandType.setStyle: + case UICommandType.setInlineStyle: String value = ''; String? baseHref; + bool important = false; if (command.nativePtr2 != nullptr) { final Pointer payload = command.nativePtr2.cast(); final Pointer valuePtr = payload.ref.value; final Pointer hrefPtr = payload.ref.href; + important = payload.ref.important == 1; if (valuePtr != nullptr) { final Pointer nativeValue = valuePtr.cast(); value = nativeStringToString(nativeValue); @@ -320,7 +353,34 @@ void execUICommands(WebFViewController view, List commands) { malloc.free(payload); } - view.setInlineStyle(nativePtr, command.args, value, baseHref: baseHref); + view.setInlineStyle(nativePtr, command.args, value, baseHref: baseHref, important: important); + pendingStylePropertiesTargets[nativePtr.address] = true; + break; + case UICommandType.setSheetStyle: + String value = ''; + String? baseHref; + bool important = false; + if (command.nativePtr2 != nullptr) { + final Pointer payload = + command.nativePtr2.cast(); + final Pointer valuePtr = payload.ref.value; + final Pointer hrefPtr = payload.ref.href; + important = payload.ref.important == 1; + if (valuePtr != nullptr) { + final Pointer nativeValue = valuePtr.cast(); + value = nativeStringToString(nativeValue); + freeNativeString(nativeValue); + } + if (hrefPtr != nullptr) { + final Pointer nativeHref = hrefPtr.cast(); + final String raw = nativeStringToString(nativeHref); + freeNativeString(nativeHref); + baseHref = raw.isEmpty ? null : raw; + } + malloc.free(payload); + } + + view.setSheetStyle(nativePtr, command.args, value, baseHref: baseHref, important: important); pendingStylePropertiesTargets[nativePtr.address] = true; break; case UICommandType.setStyleById: @@ -349,6 +409,35 @@ void execUICommands(WebFViewController view, List commands) { view.setInlineStyle(nativePtr, key, value, baseHref: baseHref); pendingStylePropertiesTargets[nativePtr.address] = true; break; + case UICommandType.setSheetStyleById: + final int encoded = command.stylePropertyId; + final bool important = (encoded & 0x80000000) != 0; + final int propertyId = encoded & 0x7fffffff; + final String key = blinkStylePropertyNameFromId(propertyId); + if (key.isEmpty) break; + + String value = ''; + String? baseHref; + + final int slot = command.styleValueSlot; + if (slot < 0) { + value = blinkKeywordFromValueId(-slot - 1); + } else if (slot > 0) { + final Pointer nativeValue = Pointer.fromAddress(slot); + value = nativeStringToString(nativeValue); + freeNativeString(nativeValue); + } + + if (command.nativePtr2 != nullptr) { + final Pointer nativeHref = command.nativePtr2.cast(); + final String raw = nativeStringToString(nativeHref); + freeNativeString(nativeHref); + baseHref = raw.isEmpty ? null : raw; + } + + view.setSheetStyle(nativePtr, key, value, baseHref: baseHref, important: important); + pendingStylePropertiesTargets[nativePtr.address] = true; + break; case UICommandType.setPseudoStyle: if (command.nativePtr2 != nullptr) { final keyValue = nativePairToPairRecord(command.nativePtr2.cast()); @@ -372,6 +461,10 @@ void execUICommands(WebFViewController view, List commands) { view.clearInlineStyle(nativePtr); pendingStylePropertiesTargets[nativePtr.address] = true; break; + case UICommandType.clearSheetStyle: + view.clearSheetStyle(nativePtr); + pendingStylePropertiesTargets[nativePtr.address] = true; + break; case UICommandType.setPseudoStyle: if (command.nativePtr2 != nullptr) { final Pointer payload = diff --git a/webf/lib/src/css/animation.dart b/webf/lib/src/css/animation.dart index 9aad89f985..dc1fd67b20 100644 --- a/webf/lib/src/css/animation.dart +++ b/webf/lib/src/css/animation.dart @@ -583,7 +583,7 @@ class KeyframeEffect extends AnimationEffect { final selected = progress < 0.5 ? start : end; // Fallback path uses CSSStyleDeclaration to expand shorthands when needed. // This keeps layered values (e.g., background-position lists) in sync. - renderStyle.target.style.setProperty(property, selected?.toString() ?? ''); + renderStyle.target.style.enqueueInlineProperty(property, selected?.toString() ?? ''); return selected; } diff --git a/webf/lib/src/css/computed_style_declaration.dart b/webf/lib/src/css/computed_style_declaration.dart index 1f0332e663..4f4ee07305 100644 --- a/webf/lib/src/css/computed_style_declaration.dart +++ b/webf/lib/src/css/computed_style_declaration.dart @@ -99,11 +99,11 @@ class ComputedCSSStyleDeclaration extends CSSStyleDeclaration { return _valueForPropertyInStyle(propertyID, needUpdateStyle: true); } - @override void setProperty( String propertyName, String? value, { bool? isImportant, + PropertyType? propertyType, String? baseHref, bool validate = true, }) { diff --git a/webf/lib/src/css/css_animation.dart b/webf/lib/src/css/css_animation.dart index b7ceb9493a..5185b2d8c9 100644 --- a/webf/lib/src/css/css_animation.dart +++ b/webf/lib/src/css/css_animation.dart @@ -96,7 +96,7 @@ mixin CSSAnimationMixin on RenderStyle { } final Map _runningAnimation = {}; - final Map _cacheOriginProperties = {}; + final Map _cacheOriginProperties = {}; String _getSingleString(List list, int index) { return list[index]; @@ -140,7 +140,7 @@ mixin CSSAnimationMixin on RenderStyle { final styles = getAnimationInitStyle(keyframes); styles.forEach((property, value) { - String? originStyle = target.inlineStyle[property]; + InlineStyleEntry? originStyle = target.inlineStyle[property]; if (originStyle != null) { _cacheOriginProperties.putIfAbsent(property, () => originStyle); } @@ -265,8 +265,9 @@ mixin CSSAnimationMixin on RenderStyle { AnimationEffect? effect = animation.effect; if (effect != null && effect is KeyframeEffect) { for (var property in effect.properties) { - if (_cacheOriginProperties.containsKey(property)) { - target.setInlineStyle(property, _cacheOriginProperties[property]!); + InlineStyleEntry? origin = _cacheOriginProperties[property]; + if (origin != null) { + target.setInlineStyle(property, origin.value, important: origin.important); } _cacheOriginProperties.remove(property); } diff --git a/webf/lib/src/css/element_rule_collector.dart b/webf/lib/src/css/element_rule_collector.dart index 612a91fdc5..1097867c4d 100644 --- a/webf/lib/src/css/element_rule_collector.dart +++ b/webf/lib/src/css/element_rule_collector.dart @@ -6,6 +6,7 @@ * Copyright (C) 2022-2024 The WebF authors. All rights reserved. */ +import 'dart:collection'; import 'package:webf/css.dart'; import 'package:webf/dom.dart'; import 'package:webf/src/foundation/debug_flags.dart'; @@ -92,7 +93,7 @@ class ElementRuleCollector { // Deduplicate while preserving order. final List list = []; - final Set seen = {}; + final Set seen = HashSet.identity(); for (final CSSRule r in candidates) { if (r is CSSStyleRule && !seen.contains(r)) { seen.add(r); @@ -260,7 +261,7 @@ class ElementRuleCollector { CSSStyleDeclaration collectionFromRuleSet(RuleSet ruleSet, Element element) { final rules = matchedRules(ruleSet, element); - CSSStyleDeclaration declaration = CSSStyleDeclaration(); + CSSStyleDeclaration declaration = CSSStyleDeclaration.sheet(); if (rules.isEmpty) { return declaration; } diff --git a/webf/lib/src/css/parser/parser.dart b/webf/lib/src/css/parser/parser.dart index b20ddfc36b..cbd901fa47 100644 --- a/webf/lib/src/css/parser/parser.dart +++ b/webf/lib/src/css/parser/parser.dart @@ -93,8 +93,8 @@ class CSSParser { return CSSStyleSheet(rules); } - Map parseInlineStyle() { - Map style = {}; + Map parseInlineStyle() { + Map style = {}; do { if (TokenKind.isIdentifier(_peekToken.kind)) { var propertyIdent = camelize(identifier().name); @@ -121,7 +121,18 @@ class CSSParser { } } var expr = processExpr(); - style[propertyIdent] = expr; + if (expr != null) { + final bool importantPriority = _maybeEat(TokenKind.IMPORTANT); + final int trailingToken = _peek(); + final bool hasUnexpectedTrailingToken = trailingToken != TokenKind.SEMICOLON && + trailingToken != TokenKind.RBRACE && + trailingToken != TokenKind.END_OF_FILE; + if ((importantPriority && trailingToken == TokenKind.IMPORTANT) || hasUnexpectedTrailingToken) { + _skipToDeclarationEnd(); + } else { + style[propertyIdent] = InlineStyleEntry(expr, important: importantPriority); + } + } } else if (_peekToken.kind == TokenKind.VAR_DEFINITION) { _next(); } else if (_peekToken.kind == TokenKind.DIRECTIVE_INCLUDE) { @@ -667,7 +678,7 @@ class CSSParser { List processDeclarations({bool checkBrace = true}) { if (checkBrace) _eat(TokenKind.LBRACE); - var declaration = CSSStyleDeclaration(); + var declaration = CSSStyleDeclaration.sheet(); List list = [declaration]; do { var selectorGroup = _nestedSelector(); @@ -1132,7 +1143,13 @@ class CSSParser { return; } - style.setProperty(propertyIdent, expr, isImportant: importantPriority, baseHref: href); + style.setProperty( + propertyIdent, + expr.toString(), + isImportant: importantPriority, + propertyType: PropertyType.sheet, + baseHref: href, + ); } } else if (_peekToken.kind == TokenKind.VAR_DEFINITION) { _next(); diff --git a/webf/lib/src/css/rule_set.dart b/webf/lib/src/css/rule_set.dart index cd2fb2b9bd..6dfd88da9d 100644 --- a/webf/lib/src/css/rule_set.dart +++ b/webf/lib/src/css/rule_set.dart @@ -35,6 +35,10 @@ class RuleSet { final Map keyframesRules = {}; + // Fast flag to avoid expensive pseudo-element matching for every element + // when the active RuleSet contains no pseudo-element selectors at all. + bool hasPseudoElementSelectors = false; + int _lastPosition = 0; void addRules(List rules, { required String? baseHref }) { @@ -70,6 +74,7 @@ class RuleSet { tagRules.clear(); universalRules.clear(); pseudoRules.clear(); + hasPseudoElementSelectors = false; } // indexed by selectorText @@ -85,6 +90,7 @@ class RuleSet { // Invalid selector like `P:first-line.three`; drop this rule. return; } + hasPseudoElementSelectors = true; break; } } diff --git a/webf/lib/src/css/style_declaration.dart b/webf/lib/src/css/style_declaration.dart index f0888fa7bf..b5a0a4fdcc 100644 --- a/webf/lib/src/css/style_declaration.dart +++ b/webf/lib/src/css/style_declaration.dart @@ -80,10 +80,34 @@ RegExp _kebabCaseReg = RegExp(r'[A-Z]'); final LinkedLruHashMap> _cachedExpandedShorthand = LinkedLruHashMap(maximumSize: 500); class CSSPropertyValue { - String? baseHref; - String value; + final String value; + final String? baseHref; + final bool important; + final PropertyType propertyType; + + const CSSPropertyValue( + this.value, { + this.baseHref, + this.important = false, + this.propertyType = PropertyType.inline, + }); - CSSPropertyValue(this.value, {this.baseHref}); + @override + String toString() { + return value; + } +} + +enum PropertyType { + inline, + sheet, +} + +class InlineStyleEntry { + final String value; + final bool important; + + const InlineStyleEntry(this.value, {this.important = false}); @override String toString() { @@ -106,70 +130,67 @@ class CSSPropertyValue { /// 3. Via [Window.getComputedStyle()], which exposes the [CSSStyleDeclaration] /// object as a read-only interface. class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBindingObject { - Element? target; + final PropertyType _defaultPropertyType; - // TODO(yuanyan): defaultStyle should be longhand properties. - Map? defaultStyle; - StyleChangeListener? onStyleChanged; - StyleFlushedListener? onStyleFlushed; + CSSStyleDeclaration([super.context]) : _defaultPropertyType = PropertyType.inline; - CSSStyleDeclaration? _pseudoBeforeStyle; - CSSStyleDeclaration? get pseudoBeforeStyle => _pseudoBeforeStyle; - set pseudoBeforeStyle(CSSStyleDeclaration? newStyle) { - _pseudoBeforeStyle = newStyle; - target?.markBeforePseudoElementNeedsUpdate(); - } + CSSStyleDeclaration.sheet([super.context]) : _defaultPropertyType = PropertyType.sheet; - CSSStyleDeclaration? _pseudoAfterStyle; - CSSStyleDeclaration? get pseudoAfterStyle => _pseudoAfterStyle; - set pseudoAfterStyle(CSSStyleDeclaration? newStyle) { - _pseudoAfterStyle = newStyle; - target?.markAfterPseudoElementNeedsUpdate(); + /// An empty style declaration. + static CSSStyleDeclaration empty = CSSStyleDeclaration(); + + final Map _properties = {}; + + CSSPropertyValue? _getEffectivePropertyValueEntry(String propertyName) => _properties[propertyName]; + + void _setStagedPropertyValue(String propertyName, CSSPropertyValue value) { + _properties[propertyName] = value; } - // ::first-letter pseudo style (applies to the first typographic letter) - CSSStyleDeclaration? _pseudoFirstLetterStyle; - CSSStyleDeclaration? get pseudoFirstLetterStyle => _pseudoFirstLetterStyle; - set pseudoFirstLetterStyle(CSSStyleDeclaration? newStyle) { - _pseudoFirstLetterStyle = newStyle; - // Trigger a layout rebuild so IFC can re-shape text for first-letter styling - target?.markFirstLetterPseudoNeedsUpdate(); + List _effectivePropertyNamesSnapshot() => _properties.keys.toList(growable: false); + + static bool _samePropertyValue(CSSPropertyValue? a, CSSPropertyValue? b) { + if (identical(a, b)) return true; + if (a == null && b == null) return true; + if (a == null || b == null) return false; + return a.value == b.value && + a.baseHref == b.baseHref && + a.important == b.important && + a.propertyType == b.propertyType; } - // ::first-line pseudo style (applies to only the first formatted line) - CSSStyleDeclaration? _pseudoFirstLineStyle; - CSSStyleDeclaration? get pseudoFirstLineStyle => _pseudoFirstLineStyle; - set pseudoFirstLineStyle(CSSStyleDeclaration? newStyle) { - _pseudoFirstLineStyle = newStyle; - target?.markFirstLinePseudoNeedsUpdate(); + static int _cascadePriority(CSSPropertyValue value) { + int priority = value.important ? 2 : 0; + if (value.propertyType == PropertyType.inline) priority += 1; + return priority; } - CSSStyleDeclaration([super.context]); + /// Textual representation of the declaration block. + /// Setting this attribute changes the style. + String get cssText { + if (length == 0) return EMPTY_STRING; - // ignore: prefer_initializing_formals - CSSStyleDeclaration.computedStyle(this.target, this.defaultStyle, this.onStyleChanged, [this.onStyleFlushed]); + final StringBuffer css = StringBuffer(); + bool first = true; - /// An empty style declaration. - static CSSStyleDeclaration empty = CSSStyleDeclaration(); + for (final MapEntry entry in this) { + final String property = entry.key; + final CSSPropertyValue value = entry.value; - /// When some property changed, corresponding [StyleChangeListener] will be - /// invoked in synchronous. - final List _styleChangeListeners = []; + if (!first) css.write(' '); + first = false; - final Map _properties = {}; - Map _pendingProperties = {}; - final Map _importants = {}; - final Map _sheetStyle = {}; + css + ..write(_kebabize(property)) + ..write(': ') + ..write(value.value); + if (value.important) { + css.write(' !important'); + } + css.write(';'); + } - /// Textual representation of the declaration block. - /// Setting this attribute changes the style. - String get cssText { - String css = EMPTY_STRING; - _properties.forEach((property, value) { - if (css.isNotEmpty) css += ' '; - css += '${_kebabize(property)}: $value ${_importants.containsKey(property) ? '!important' : ''};'; - }); - return css; + return css.toString(); } /// Whether the given property is marked as `!important` on this declaration. @@ -177,11 +198,7 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding /// Exposed for components (e.g., CSS variable resolver) that need to /// preserve importance when updating dependent properties. bool isImportant(String propertyName) { - return _importants[propertyName] == true; - } - - bool get hasInheritedPendingProperty { - return _pendingProperties.keys.any((key) => isInheritedPropertyString(_kebabize(key))); + return _getEffectivePropertyValueEntry(propertyName)?.important ?? false; } // @TODO: Impl the cssText setter. @@ -199,17 +216,17 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding /// value is a String containing the value of the property. /// If not set, returns the empty string. String getPropertyValue(String propertyName) { - // Get the latest pending value first. - return _pendingProperties[propertyName]?.value ?? _properties[propertyName]?.value ?? EMPTY_STRING; + return _getEffectivePropertyValueEntry(propertyName)?.value ?? EMPTY_STRING; } /// Returns the baseHref associated with a property value if available. String? getPropertyBaseHref(String propertyName) { - return _pendingProperties[propertyName]?.baseHref ?? _properties[propertyName]?.baseHref; + return _getEffectivePropertyValueEntry(propertyName)?.baseHref; } /// Removes a property from the CSS declaration. void removeProperty(String propertyName, [bool? isImportant]) { + propertyName = propertyName.trim(); switch (propertyName) { case PADDING: return CSSStyleProperty.removeShorthandPadding(this, isImportant); @@ -269,47 +286,22 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding case ANIMATION: return CSSStyleProperty.removeShorthandAnimation(this, isImportant); } - - String present = EMPTY_STRING; - if (isImportant == true) { - _importants.remove(propertyName); - // Fallback to css style. - String? value = _sheetStyle[propertyName]; - if (!isNullOrEmptyValue(value)) { - present = value!; - } - } else if (isImportant == false) { - _sheetStyle.remove(propertyName); - } - - // Fallback to default style (UA / element default). - if (isNullOrEmptyValue(present) && defaultStyle != null && defaultStyle!.containsKey(propertyName)) { - present = defaultStyle![propertyName]; - } - - // If there is still no value, fall back to the CSS initial value for - // this property. To preserve inheritance semantics, we only do this for - // non-inherited properties. For inherited ones we prefer leaving the - // value empty so [RenderStyle] can pull from the parent instead. - if (isNullOrEmptyValue(present) && cssInitialValues.containsKey(propertyName)) { - final String kebabName = _kebabize(propertyName); - final bool isInherited = isInheritedPropertyString(kebabName); - if (!isInherited) { - present = cssInitialValues[propertyName]; - } - } - - // Update removed value by flush pending properties. - _pendingProperties[propertyName] = CSSPropertyValue(present); + _properties.remove(propertyName); } void _expandShorthand( String propertyName, String normalizedValue, bool? isImportant, { + PropertyType? propertyType, String? baseHref, bool validate = true, }) { + // Mirror setProperty()'s resolution rules so expanded longhands inherit + // the same origin + importance as the originating shorthand. + PropertyType resolvedType = propertyType ?? _defaultPropertyType; + bool resolvedImportant = isImportant == true; + Map longhandProperties; String cacheKey = '$propertyName:$normalizedValue'; if (_cachedExpandedShorthand.containsKey(cacheKey)) { @@ -339,8 +331,13 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding // comma-separated value so layered painters can retrieve per-layer positions. CSSStyleProperty.setShorthandBackgroundPosition(longhandProperties, normalizedValue); // Preserve original list for layered backgrounds (not consumed by renderStyle). - // Store directly to pending map during expansion to avoid recursive shorthand handling. - _pendingProperties[BACKGROUND_POSITION] = CSSPropertyValue(normalizedValue, baseHref: baseHref); + // Store directly during expansion to avoid recursive shorthand handling. + _setStagedPropertyValue(BACKGROUND_POSITION, CSSPropertyValue( + normalizedValue, + baseHref: baseHref, + important: resolvedImportant, + propertyType: resolvedType, + )); break; case BORDER_RADIUS: CSSStyleProperty.setShorthandBorderRadius(longhandProperties, normalizedValue); @@ -412,11 +409,18 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding } if (longhandProperties.isNotEmpty) { - longhandProperties.forEach((String propertyName, String? value) { + longhandProperties.forEach((String propertyName, String? value) { // Preserve the baseHref from the originating declaration so any // url(...) in expanded longhands (e.g., background-image) resolve // relative to the stylesheet that contained the shorthand. - setProperty(propertyName, value, isImportant: isImportant, baseHref: baseHref, validate: validate); + setProperty( + propertyName, + value, + isImportant: resolvedImportant ? true : null, + propertyType: resolvedType, + baseHref: baseHref, + validate: validate, + ); }); } } @@ -614,13 +618,312 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding String propertyName, String? value, { bool? isImportant, + PropertyType? propertyType, + String? baseHref, + bool validate = true, + }) { + propertyName = propertyName.trim(); + + // Null or empty value means should be removed. + if (CSSStyleDeclaration.isNullOrEmptyValue(value)) { + removeProperty(propertyName, isImportant); + return; + } + + final String rawValue = value.toString(); + final bool isCustomProperty = CSSVariable.isCSSSVariableProperty(propertyName); + String normalizedValue = isCustomProperty ? rawValue : _toLowerCase(propertyName, rawValue.trim()); + + if (validate && !_isValidValue(propertyName, normalizedValue)) return; + + if (_cssShorthandProperty[propertyName] != null) { + return _expandShorthand(propertyName, normalizedValue, isImportant, + propertyType: propertyType, baseHref: baseHref, validate: validate); + } + + PropertyType resolvedType = propertyType ?? _defaultPropertyType; + bool resolvedImportant = isImportant == true; + + final CSSPropertyValue? existing = _properties[propertyName]; + if (existing != null) { + final bool existingImportant = existing.important; + if (existingImportant && !resolvedImportant) { + return; + } + if (existingImportant == resolvedImportant) { + if (existing.propertyType == PropertyType.inline && resolvedType == PropertyType.sheet) { + return; + } + } + } + + if (existing != null && + existing.value == normalizedValue && + existing.important == resolvedImportant && + existing.propertyType == resolvedType && + (!CSSVariable.isCSSVariableValue(normalizedValue))) { + return; + } + + _properties[propertyName] = CSSPropertyValue( + normalizedValue, + baseHref: baseHref, + important: resolvedImportant, + propertyType: resolvedType, + ); + } + + // Inserts the style of the given Declaration into the current Declaration. + void union(CSSStyleDeclaration declaration) { + for (final MapEntry entry in declaration) { + final String propertyName = entry.key; + final CSSPropertyValue otherValue = entry.value; + final CSSPropertyValue? currentValue = _getEffectivePropertyValueEntry(propertyName); + if (currentValue != null) { + final int otherPriority = _cascadePriority(otherValue); + final int currentPriority = _cascadePriority(currentValue); + if (otherPriority < currentPriority) continue; + } + if (!identical(currentValue, otherValue)) { + _setStagedPropertyValue(propertyName, otherValue); + } + } + } + + // Merge the difference between the declarations and return the updated status + bool merge(CSSStyleDeclaration other) { + final List properties = _effectivePropertyNamesSnapshot(); + bool updateStatus = false; + + // Apply updates + additions based on `other`. + for (final MapEntry entry in other) { + final String propertyName = entry.key; + final CSSPropertyValue otherValue = entry.value; + final CSSPropertyValue? prevValue = _getEffectivePropertyValueEntry(propertyName); + + // Mirror previous behavior: if the property does not exist on `this`, + // stage the incoming value directly (even when empty). + if (prevValue == null) { + _setStagedPropertyValue(propertyName, otherValue); + updateStatus = true; + continue; + } + + if (isNullOrEmptyValue(prevValue) && isNullOrEmptyValue(otherValue)) { + continue; + } else if (!isNullOrEmptyValue(prevValue) && isNullOrEmptyValue(otherValue)) { + // Remove property. + removeProperty(propertyName, prevValue.important ? true : null); + updateStatus = true; + } else if (!_samePropertyValue(prevValue, otherValue)) { + _setStagedPropertyValue(propertyName, otherValue); + updateStatus = true; + } + } + + // Remove properties missing in `other`. + for (final String propertyName in properties) { + final CSSPropertyValue? prevValue = _getEffectivePropertyValueEntry(propertyName); + if (isNullOrEmptyValue(prevValue)) continue; + if (other._getEffectivePropertyValueEntry(propertyName) != null) continue; + + removeProperty(propertyName, prevValue?.important == true ? true : null); + updateStatus = true; + } + + return updateStatus; + } + + operator [](String property) => getPropertyValue(property); + operator []=(String property, value) { + setProperty(property, value?.toString()); + } + + /// Check a css property is valid. + @override + bool contains(Object? property) { + if (property != null && property is String) { + return getPropertyValue(property).isNotEmpty; + } + return super.contains(property); + } + + void reset() { + _properties.clear(); + } + + @override + Future dispose() async { + super.dispose(); + reset(); + } + + static bool isNullOrEmptyValue(value) { + if (value == null) return true; + if (value is CSSPropertyValue) { + return value.value == EMPTY_STRING; + } + return value == EMPTY_STRING; + } + + @override + String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) => 'CSSStyleDeclaration($cssText)'; + + @override + int get hashCode => cssText.hashCode; + + @override + bool operator ==(Object other) { + return hashCode == other.hashCode; + } + + + @override + Iterator> get iterator { + return _properties.entries.iterator; + } +} + +class ElementCSSStyleDeclaration extends CSSStyleDeclaration{ + Element? target; + + // TODO(yuanyan): defaultStyle should be longhand properties. + Map? defaultStyle; + StyleChangeListener? onStyleChanged; + StyleFlushedListener? onStyleFlushed; + + Map _pendingProperties = {}; + + @override + CSSPropertyValue? _getEffectivePropertyValueEntry(String propertyName) { + return _pendingProperties[propertyName] ?? super._getEffectivePropertyValueEntry(propertyName); + } + + @override + void _setStagedPropertyValue(String propertyName, CSSPropertyValue value) { + _pendingProperties[propertyName] = value; + } + + @override + List _effectivePropertyNamesSnapshot() { + if (_pendingProperties.isEmpty) return _properties.keys.toList(growable: false); + if (_properties.isEmpty) return _pendingProperties.keys.toList(growable: false); + + final List keys = _properties.keys.toList(growable: true); + for (final String key in _pendingProperties.keys) { + if (!_properties.containsKey(key)) keys.add(key); + } + return keys; + } + + bool get hasInheritedPendingProperty { + return _pendingProperties.keys.any((key) => isInheritedPropertyString(_kebabize(key))); + } + + CSSStyleDeclaration? _pseudoBeforeStyle; + CSSStyleDeclaration? get pseudoBeforeStyle => _pseudoBeforeStyle; + set pseudoBeforeStyle(CSSStyleDeclaration? newStyle) { + _pseudoBeforeStyle = newStyle; + target?.markBeforePseudoElementNeedsUpdate(); + } + + CSSStyleDeclaration? _pseudoAfterStyle; + CSSStyleDeclaration? get pseudoAfterStyle => _pseudoAfterStyle; + set pseudoAfterStyle(CSSStyleDeclaration? newStyle) { + _pseudoAfterStyle = newStyle; + target?.markAfterPseudoElementNeedsUpdate(); + } + + // ::first-letter pseudo style (applies to the first typographic letter) + CSSStyleDeclaration? _pseudoFirstLetterStyle; + CSSStyleDeclaration? get pseudoFirstLetterStyle => _pseudoFirstLetterStyle; + set pseudoFirstLetterStyle(CSSStyleDeclaration? newStyle) { + _pseudoFirstLetterStyle = newStyle; + // Trigger a layout rebuild so IFC can re-shape text for first-letter styling + target?.markFirstLetterPseudoNeedsUpdate(); + } + + // ::first-line pseudo style (applies to only the first formatted line) + CSSStyleDeclaration? _pseudoFirstLineStyle; + CSSStyleDeclaration? get pseudoFirstLineStyle => _pseudoFirstLineStyle; + set pseudoFirstLineStyle(CSSStyleDeclaration? newStyle) { + _pseudoFirstLineStyle = newStyle; + target?.markFirstLinePseudoNeedsUpdate(); + } + + /// When some property changed, corresponding [StyleChangeListener] will be + /// invoked in synchronous. + final List _styleChangeListeners = []; + + ElementCSSStyleDeclaration([super.context]); + + // ignore: prefer_initializing_formals + ElementCSSStyleDeclaration.computedStyle(this.target, this.defaultStyle, this.onStyleChanged, [this.onStyleFlushed]); + + void enqueueInlineProperty( + String propertyName, + String? value, { + bool? isImportant, + String? baseHref, + bool validate = true, + }) { + setProperty( + propertyName, + value, + isImportant: isImportant, + propertyType: PropertyType.inline, + baseHref: baseHref, + validate: validate, + ); + } + + void enqueueSheetProperty( + String propertyName, + String? value, { + bool? isImportant, + String? baseHref, + bool validate = true, + }) { + setProperty( + propertyName, + value, + isImportant: isImportant, + propertyType: PropertyType.sheet, + baseHref: baseHref, + validate: validate, + ); + } + + @override + void setProperty( + String propertyName, + String? value, { + bool? isImportant, + PropertyType? propertyType, String? baseHref, bool validate = true, }) { propertyName = propertyName.trim(); // Null or empty value means should be removed. - if (isNullOrEmptyValue(value)) { + if (CSSStyleDeclaration.isNullOrEmptyValue(value)) { + final PropertyType resolvedType = propertyType ?? _defaultPropertyType; + + // Clearing an inline declaration should never clobber an already-staged + // stylesheet value (e.g. during style recomputation where inlineStyle may + // transiently carry empty entries). If the current winner isn't inline, + // treat this as a no-op. + if (resolvedType == PropertyType.inline) { + final CSSPropertyValue? existing = _getEffectivePropertyValueEntry(propertyName); + if (existing != null && existing.propertyType != PropertyType.inline) { + final CSSPropertyValue? staged = _pendingProperties[propertyName]; + if (staged != null && staged.propertyType == PropertyType.inline) { + _pendingProperties.remove(propertyName); + } + return; + } + } + removeProperty(propertyName, isImportant); return; } @@ -632,27 +935,181 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding if (validate && !_isValidValue(propertyName, normalizedValue)) return; if (_cssShorthandProperty[propertyName] != null) { - return _expandShorthand(propertyName, normalizedValue, isImportant, baseHref: baseHref, validate: validate); + return _expandShorthand( + propertyName, + normalizedValue, + isImportant, + propertyType: propertyType, + baseHref: baseHref, + validate: validate, + ); } - // From style sheet mark the property important as false. - if (isImportant == false) { - _sheetStyle[propertyName] = normalizedValue; + PropertyType resolvedType = propertyType ?? _defaultPropertyType; + bool resolvedImportant = isImportant == true; + + final CSSPropertyValue? existing = _pendingProperties[propertyName] ?? _properties[propertyName]; + if (existing != null) { + final bool existingImportant = existing.important; + if (existingImportant && !resolvedImportant) { + return; + } + if (existingImportant == resolvedImportant) { + if (existing.propertyType == PropertyType.inline && resolvedType == PropertyType.sheet) { + return; + } + } } - // If the important property is already set, we should ignore it. - if (isImportant != true && _importants[propertyName] == true) { + if (existing != null && + existing.value == normalizedValue && + existing.important == resolvedImportant && + existing.propertyType == resolvedType && + (!CSSVariable.isCSSVariableValue(normalizedValue))) { return; } - if (isImportant == true) { - _importants[propertyName] = true; + _pendingProperties[propertyName] = CSSPropertyValue( + normalizedValue, + baseHref: baseHref, + important: resolvedImportant, + propertyType: resolvedType, + ); + } + + @override + int get length { + int total = _properties.length; + for (final String key in _pendingProperties.keys) { + if (!_properties.containsKey(key)) total++; + } + return total; + } + + @override + String item(int index) { + if (index < _properties.length) { + return _properties.keys.elementAt(index); + } + int remaining = index - _properties.length; + for (final String key in _pendingProperties.keys) { + if (_properties.containsKey(key)) continue; + if (remaining == 0) return key; + remaining--; + } + throw RangeError.index(index, this, 'index', null, length); + } + + @override + Iterator> get iterator { + if (_pendingProperties.isEmpty) return _properties.entries.iterator; + if (_properties.isEmpty) return _pendingProperties.entries.iterator; + return _PendingPropertiesIterator(_properties, _pendingProperties); + } + + @override + void removeProperty(String propertyName, [bool? isImportant]) { + propertyName = propertyName.trim(); + switch (propertyName) { + case PADDING: + return CSSStyleProperty.removeShorthandPadding(this, isImportant); + case MARGIN: + return CSSStyleProperty.removeShorthandMargin(this, isImportant); + case INSET: + return CSSStyleProperty.removeShorthandInset(this, isImportant); + case BACKGROUND: + return CSSStyleProperty.removeShorthandBackground(this, isImportant); + case BACKGROUND_POSITION: + return CSSStyleProperty.removeShorthandBackgroundPosition(this, isImportant); + case BORDER_RADIUS: + return CSSStyleProperty.removeShorthandBorderRadius(this, isImportant); + case GRID_TEMPLATE: + return CSSStyleProperty.removeShorthandGridTemplate(this, isImportant); + case GRID: + return CSSStyleProperty.removeShorthandGrid(this, isImportant); + case PLACE_CONTENT: + return CSSStyleProperty.removeShorthandPlaceContent(this, isImportant); + case PLACE_ITEMS: + return CSSStyleProperty.removeShorthandPlaceItems(this, isImportant); + case PLACE_SELF: + return CSSStyleProperty.removeShorthandPlaceSelf(this, isImportant); + case OVERFLOW: + return CSSStyleProperty.removeShorthandOverflow(this, isImportant); + case FONT: + return CSSStyleProperty.removeShorthandFont(this, isImportant); + case FLEX: + return CSSStyleProperty.removeShorthandFlex(this, isImportant); + case FLEX_FLOW: + return CSSStyleProperty.removeShorthandFlexFlow(this, isImportant); + case GAP: + return CSSStyleProperty.removeShorthandGap(this, isImportant); + case GRID_ROW: + return CSSStyleProperty.removeShorthandGridRow(this, isImportant); + case GRID_COLUMN: + return CSSStyleProperty.removeShorthandGridColumn(this, isImportant); + case GRID_AREA: + return CSSStyleProperty.removeShorthandGridArea(this, isImportant); + case BORDER: + case BORDER_TOP: + case BORDER_RIGHT: + case BORDER_BOTTOM: + case BORDER_LEFT: + case BORDER_INLINE_START: + case BORDER_INLINE_END: + case BORDER_BLOCK_START: + case BORDER_BLOCK_END: + case BORDER_COLOR: + case BORDER_STYLE: + case BORDER_WIDTH: + return CSSStyleProperty.removeShorthandBorder(this, propertyName, isImportant); + case TRANSITION: + return CSSStyleProperty.removeShorthandTransition(this, isImportant); + case TEXT_DECORATION: + return CSSStyleProperty.removeShorthandTextDecoration(this, isImportant); + case ANIMATION: + return CSSStyleProperty.removeShorthandAnimation(this, isImportant); + } + + String present = EMPTY_STRING; + + // Fallback to default style (UA / element default). + final dynamic defaultValue = defaultStyle?[propertyName]; + if (CSSStyleDeclaration.isNullOrEmptyValue(present) && !CSSStyleDeclaration.isNullOrEmptyValue(defaultValue)) { + present = defaultValue.toString(); + } + + // If there is still no value, fall back to the CSS initial value for + // this property. To preserve inheritance semantics, we only do this for + // non-inherited properties. For inherited ones we prefer leaving the + // value empty so [RenderStyle] can pull from the parent instead. + if (CSSStyleDeclaration.isNullOrEmptyValue(present) && cssInitialValues.containsKey(propertyName)) { + final String kebabName = _kebabize(propertyName); + final bool isInherited = isInheritedPropertyString(kebabName); + if (!isInherited) { + present = cssInitialValues[propertyName]; + } } - String? prevValue = getPropertyValue(propertyName); - if (normalizedValue == prevValue && (!CSSVariable.isCSSVariableValue(normalizedValue))) return; + // Update removed value by flush pending properties. + _pendingProperties[propertyName] = CSSPropertyValue( + present, + important: false, + propertyType: PropertyType.sheet, + ); + } + + @override + void reset() { + super.reset(); + _pendingProperties.clear(); + } - _pendingProperties[propertyName] = CSSPropertyValue(normalizedValue, baseHref: baseHref); + void addStyleChangeListener(StyleChangeListener listener) { + _styleChangeListeners.add(listener); + } + + void removeStyleChangeListener(StyleChangeListener listener) { + _styleChangeListeners.remove(listener); } void flushDisplayProperties() { @@ -660,8 +1117,7 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding // If style target element not exists, no need to do flush operation. if (target == null) return; - if (_pendingProperties.containsKey(DISPLAY) && - target.isConnected) { + if (_pendingProperties.containsKey(DISPLAY) && target.isConnected) { CSSPropertyValue? prevValue = _properties[DISPLAY]; CSSPropertyValue currentValue = _pendingProperties[DISPLAY]!; _properties[DISPLAY] = currentValue; @@ -677,8 +1133,7 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding if (target == null) return; // Display change from none to other value that the renderBoxModel is null. - if (_pendingProperties.containsKey(DISPLAY) && - target.isConnected) { + if (_pendingProperties.containsKey(DISPLAY) && target.isConnected) { CSSPropertyValue? prevValue = _properties[DISPLAY]; CSSPropertyValue currentValue = _pendingProperties[DISPLAY]!; _properties[DISPLAY] = currentValue; @@ -694,32 +1149,42 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding // Reset first avoid set property in flush stage. _pendingProperties = {}; - List propertyNames = pendingProperties.keys.toList(); - for (String propertyName in _propertyOrders) { - int index = propertyNames.indexOf(propertyName); - if (index > -1) { - propertyNames.removeAt(index); - propertyNames.insert(0, propertyName); + final List pendingKeys = pendingProperties.keys.toList(growable: false); + final Set remainingKeys = pendingKeys.toSet(); + + // Keep ordering behavior consistent with previous implementation: + // 1. Move properties in `_propertyOrders` to the front. + // 2. Preserve pending insertion order for the rest. + final List reorderedKeys = []; + for (final String propertyName in _propertyOrders.reversed) { + if (remainingKeys.remove(propertyName)) { + reorderedKeys.add(propertyName); } } - - Map prevValues = {}; - for (String propertyName in propertyNames) { - // Update the prevValue to currentValue. - prevValues[propertyName] = _properties[propertyName]; - _properties[propertyName] = pendingProperties[propertyName]!; + for (final String propertyName in pendingKeys) { + if (remainingKeys.contains(propertyName)) { + reorderedKeys.add(propertyName); + } } - propertyNames.sort((left, right) { - final isVariableLeft = CSSVariable.isCSSSVariableProperty(left) ? 1 : 0; - final isVariableRight = CSSVariable.isCSSSVariableProperty(right) ? 1 : 0; - if (isVariableLeft == 1 || isVariableRight == 1) { - return isVariableRight - isVariableLeft; + // Stable partition: CSS variables should be flushed first. + final List propertyNames = []; + for (final String propertyName in reorderedKeys) { + if (CSSVariable.isCSSSVariableProperty(propertyName)) { + propertyNames.add(propertyName); } - return 0; - }); - + } + for (final String propertyName in reorderedKeys) { + if (!CSSVariable.isCSSSVariableProperty(propertyName)) { + propertyNames.add(propertyName); + } + } + final Map prevValues = {}; + for (final MapEntry entry in pendingProperties.entries) { + prevValues[entry.key] = _properties[entry.key]; + _properties[entry.key] = entry.value; + } for (String propertyName in propertyNames) { CSSPropertyValue? prevValue = prevValues[propertyName]; @@ -728,32 +1193,72 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding } onStyleFlushed?.call(propertyNames); + } + + void _emitPropertyChanged(String property, String? original, String present, {String? baseHref}) { + if (original == present && (!CSSVariable.isCSSVariableValue(present))) return; + if (onStyleChanged != null) { + onStyleChanged!(property, original, present, baseHref: baseHref); + } + + for (int i = 0; i < _styleChangeListeners.length; i++) { + StyleChangeListener listener = _styleChangeListeners[i]; + listener(property, original, present, baseHref: baseHref); + } } // Set a style property on a pseudo element (before/after/first-letter/first-line) for this element. - // Values set here are treated as inline on the pseudo element and marked important - // to override stylesheet rules when applicable. + // Pseudo elements don't have inline styles; this stores the resolved pseudo styles + // (from the native bridge and/or stylesheet matching) for the UI layer. void setPseudoProperty(String type, String propertyName, String value, {String? baseHref, bool validate = true}) { switch (type) { case 'before': - pseudoBeforeStyle ??= CSSStyleDeclaration(); - pseudoBeforeStyle!.setProperty(propertyName, value, isImportant: true, baseHref: baseHref, validate: validate); + pseudoBeforeStyle ??= CSSStyleDeclaration.sheet(); + pseudoBeforeStyle!.setProperty( + propertyName, + value, + isImportant: true, + propertyType: PropertyType.sheet, + baseHref: baseHref, + validate: validate, + ); target?.markBeforePseudoElementNeedsUpdate(); break; case 'after': - pseudoAfterStyle ??= CSSStyleDeclaration(); - pseudoAfterStyle!.setProperty(propertyName, value, isImportant: true, baseHref: baseHref, validate: validate); + pseudoAfterStyle ??= CSSStyleDeclaration.sheet(); + pseudoAfterStyle!.setProperty( + propertyName, + value, + isImportant: true, + propertyType: PropertyType.sheet, + baseHref: baseHref, + validate: validate, + ); target?.markAfterPseudoElementNeedsUpdate(); break; case 'first-letter': - pseudoFirstLetterStyle ??= CSSStyleDeclaration(); - pseudoFirstLetterStyle!.setProperty(propertyName, value, isImportant: true, baseHref: baseHref, validate: validate); + pseudoFirstLetterStyle ??= CSSStyleDeclaration.sheet(); + pseudoFirstLetterStyle!.setProperty( + propertyName, + value, + isImportant: true, + propertyType: PropertyType.sheet, + baseHref: baseHref, + validate: validate, + ); target?.markFirstLetterPseudoNeedsUpdate(); break; case 'first-line': - pseudoFirstLineStyle ??= CSSStyleDeclaration(); - pseudoFirstLineStyle!.setProperty(propertyName, value, isImportant: true, baseHref: baseHref, validate: validate); + pseudoFirstLineStyle ??= CSSStyleDeclaration.sheet(); + pseudoFirstLineStyle!.setProperty( + propertyName, + value, + isImportant: true, + propertyType: PropertyType.sheet, + baseHref: baseHref, + validate: validate, + ); target?.markFirstLinePseudoNeedsUpdate(); break; } @@ -763,35 +1268,26 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding void removePseudoProperty(String type, String propertyName) { switch (type) { case 'before': - if (pseudoBeforeStyle != null) { - // Remove the inline override; fall back to stylesheet value if present. - pseudoBeforeStyle!.removeProperty(propertyName, true); - } + pseudoBeforeStyle?.removeProperty(propertyName, true); target?.markBeforePseudoElementNeedsUpdate(); break; case 'after': - if (pseudoAfterStyle != null) { - pseudoAfterStyle!.removeProperty(propertyName, true); - } + pseudoAfterStyle?.removeProperty(propertyName, true); target?.markAfterPseudoElementNeedsUpdate(); break; case 'first-letter': - if (pseudoFirstLetterStyle != null) { - pseudoFirstLetterStyle!.removeProperty(propertyName, true); - } + pseudoFirstLetterStyle?.removeProperty(propertyName, true); target?.markFirstLetterPseudoNeedsUpdate(); break; case 'first-line': - if (pseudoFirstLineStyle != null) { - pseudoFirstLineStyle!.removeProperty(propertyName, true); - } + pseudoFirstLineStyle?.removeProperty(propertyName, true); target?.markFirstLinePseudoNeedsUpdate(); break; } } void clearPseudoStyle(String type) { - switch(type) { + switch (type) { case 'before': pseudoBeforeStyle = null; target?.markBeforePseudoElementNeedsUpdate(); @@ -811,31 +1307,6 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding } } - // Inserts the style of the given Declaration into the current Declaration. - void union(CSSStyleDeclaration declaration) { - Map properties = {} - ..addAll(_properties) - ..addAll(_pendingProperties); - - for (String propertyName in declaration._pendingProperties.keys) { - bool currentIsImportant = _importants[propertyName] ?? false; - bool otherIsImportant = declaration._importants[propertyName] ?? false; - CSSPropertyValue? currentValue = properties[propertyName]; - CSSPropertyValue? otherValue = declaration._pendingProperties[propertyName]; - if ((otherIsImportant || !currentIsImportant) && currentValue != otherValue) { - // Add property. - if (otherValue != null) { - _pendingProperties[propertyName] = otherValue; - } else { - _pendingProperties.remove(propertyName); - } - if (otherIsImportant) { - _importants[propertyName] = true; - } - } - } - } - void handlePseudoRules(Element parentElement, List rules) { if (rules.isEmpty) return; @@ -877,7 +1348,7 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding firstLineRules.sort(sortRules); if (beforeRules.isNotEmpty) { - pseudoBeforeStyle ??= CSSStyleDeclaration(); + pseudoBeforeStyle ??= CSSStyleDeclaration.sheet(); // Merge all the rules for (CSSStyleRule rule in beforeRules) { pseudoBeforeStyle!.union(rule.declaration); @@ -888,7 +1359,7 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding } if (afterRules.isNotEmpty) { - pseudoAfterStyle ??= CSSStyleDeclaration(); + pseudoAfterStyle ??= CSSStyleDeclaration.sheet(); for (CSSStyleRule rule in afterRules) { pseudoAfterStyle!.union(rule.declaration); } @@ -898,7 +1369,7 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding } if (firstLetterRules.isNotEmpty) { - pseudoFirstLetterStyle ??= CSSStyleDeclaration(); + pseudoFirstLetterStyle ??= CSSStyleDeclaration.sheet(); for (CSSStyleRule rule in firstLetterRules) { pseudoFirstLetterStyle!.union(rule.declaration); } @@ -908,7 +1379,7 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding } if (firstLineRules.isNotEmpty) { - pseudoFirstLineStyle ??= CSSStyleDeclaration(); + pseudoFirstLineStyle ??= CSSStyleDeclaration.sheet(); for (CSSStyleRule rule in firstLineRules) { pseudoFirstLineStyle!.union(rule.declaration); } @@ -918,104 +1389,37 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding } } - // Merge the difference between the declarations and return the updated status + @override bool merge(CSSStyleDeclaration other) { - Map properties = {} - ..addAll(_properties) - ..addAll(_pendingProperties); - bool updateStatus = false; - for (String propertyName in properties.keys) { - CSSPropertyValue? prevValue = properties[propertyName]; - CSSPropertyValue? currentValue = other._pendingProperties[propertyName]; - bool currentImportant = other._importants[propertyName] ?? false; + final bool updateStatus = super.merge(other); - if (isNullOrEmptyValue(prevValue) && isNullOrEmptyValue(currentValue)) { - continue; - } else if (!isNullOrEmptyValue(prevValue) && isNullOrEmptyValue(currentValue)) { - // Remove property. - removeProperty(propertyName, currentImportant); - updateStatus = true; - } else if (prevValue != currentValue) { - // Update property. - setProperty(propertyName, currentValue?.value, isImportant: currentImportant, baseHref: currentValue?.baseHref); - updateStatus = true; - } - } - - for (String propertyName in other._pendingProperties.keys) { - CSSPropertyValue? prevValue = properties[propertyName]; - CSSPropertyValue? currentValue = other._pendingProperties[propertyName]; - bool currentImportant = other._importants[propertyName] ?? false; - - if (isNullOrEmptyValue(prevValue) && !isNullOrEmptyValue(currentValue)) { - // Add property. - setProperty(propertyName, currentValue?.value, isImportant: currentImportant, baseHref: currentValue?.baseHref); - updateStatus = true; - } - } + if (other is! ElementCSSStyleDeclaration) return updateStatus; + bool pseudoUpdated = false; // Merge pseudo-element styles. Ensure target side is initialized so rules from // 'other' are not dropped when this side is null. if (other.pseudoBeforeStyle != null) { - pseudoBeforeStyle ??= CSSStyleDeclaration(); + pseudoBeforeStyle ??= CSSStyleDeclaration.sheet(); pseudoBeforeStyle!.merge(other.pseudoBeforeStyle!); + pseudoUpdated = true; } if (other.pseudoAfterStyle != null) { - pseudoAfterStyle ??= CSSStyleDeclaration(); + pseudoAfterStyle ??= CSSStyleDeclaration.sheet(); pseudoAfterStyle!.merge(other.pseudoAfterStyle!); + pseudoUpdated = true; } if (other.pseudoFirstLetterStyle != null) { - pseudoFirstLetterStyle ??= CSSStyleDeclaration(); + pseudoFirstLetterStyle ??= CSSStyleDeclaration.sheet(); pseudoFirstLetterStyle!.merge(other.pseudoFirstLetterStyle!); + pseudoUpdated = true; } if (other.pseudoFirstLineStyle != null) { - pseudoFirstLineStyle ??= CSSStyleDeclaration(); + pseudoFirstLineStyle ??= CSSStyleDeclaration.sheet(); pseudoFirstLineStyle!.merge(other.pseudoFirstLineStyle!); + pseudoUpdated = true; } - return updateStatus; - } - - operator [](String property) => getPropertyValue(property); - operator []=(String property, value) { - setProperty(property, value); - } - - /// Check a css property is valid. - @override - bool contains(Object? property) { - if (property != null && property is String) { - return getPropertyValue(property).isNotEmpty; - } - return super.contains(property); - } - - void addStyleChangeListener(StyleChangeListener listener) { - _styleChangeListeners.add(listener); - } - - void removeStyleChangeListener(StyleChangeListener listener) { - _styleChangeListeners.remove(listener); - } - - void _emitPropertyChanged(String property, String? original, String present, {String? baseHref}) { - if (original == present && (!CSSVariable.isCSSVariableValue(present))) return; - - if (onStyleChanged != null) { - onStyleChanged!(property, original, present, baseHref: baseHref); - } - - for (int i = 0; i < _styleChangeListeners.length; i++) { - StyleChangeListener listener = _styleChangeListeners[i]; - listener(property, original, present, baseHref: baseHref); - } - } - - void reset() { - _properties.clear(); - _pendingProperties.clear(); - _importants.clear(); - _sheetStyle.clear(); + return updateStatus || pseudoUpdated; } @override @@ -1023,28 +1427,49 @@ class CSSStyleDeclaration extends DynamicBindingObject with StaticDefinedBinding super.dispose(); target = null; _styleChangeListeners.clear(); - reset(); + _pseudoBeforeStyle = null; + _pseudoAfterStyle = null; + _pseudoFirstLetterStyle = null; + _pseudoFirstLineStyle = null; } +} - static bool isNullOrEmptyValue(value) { - return value == null || value == EMPTY_STRING; - } +class _PendingPropertiesIterator implements Iterator> { + final Map _properties; + final Map _pendingProperties; + final Iterator> _propertiesIterator; + final Iterator> _pendingIterator; - @override - String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) => 'CSSStyleDeclaration($cssText)'; + bool _iteratingProperties = true; + late MapEntry _current; + + _PendingPropertiesIterator(this._properties, this._pendingProperties) + : _propertiesIterator = _properties.entries.iterator, + _pendingIterator = _pendingProperties.entries.iterator; @override - int get hashCode => cssText.hashCode; + MapEntry get current => _current; @override - bool operator ==(Object other) { - return hashCode == other.hashCode; - } + bool moveNext() { + if (_iteratingProperties) { + while (_propertiesIterator.moveNext()) { + final MapEntry entry = _propertiesIterator.current; + final CSSPropertyValue? pendingValue = _pendingProperties[entry.key]; + _current = pendingValue == null ? entry : MapEntry(entry.key, pendingValue); + return true; + } + _iteratingProperties = false; + } + while (_pendingIterator.moveNext()) { + final MapEntry entry = _pendingIterator.current; + if (_properties.containsKey(entry.key)) continue; + _current = entry; + return true; + } - @override - Iterator> get iterator { - return _properties.entries.followedBy(_pendingProperties.entries).iterator; + return false; } } diff --git a/webf/lib/src/devtools/cdp_service/modules/css.dart b/webf/lib/src/devtools/cdp_service/modules/css.dart index c9452d5362..06e2e78f1c 100644 --- a/webf/lib/src/devtools/cdp_service/modules/css.dart +++ b/webf/lib/src/devtools/cdp_service/modules/css.dart @@ -569,12 +569,14 @@ class InspectCSSModule extends UIInspectorModule { if (colon <= 0) continue; final String name = decl.substring(0, colon).trim(); String value = decl.substring(colon + 1).trim(); - // Drop optional trailing !important marker – inline styles already have highest priority - if (value.endsWith('!important')) { + bool important = false; + final String lower = value.toLowerCase(); + if (lower.endsWith('!important')) { value = value.substring(0, value.length - '!important'.length).trim(); + important = true; } if (name.isEmpty) continue; - element.setInlineStyle(camelize(name), value); + element.setInlineStyle(camelize(name), value, important: important); element.recalculateStyle(); } } @@ -652,13 +654,15 @@ class InspectCSSModule extends UIInspectorModule { static CSSStyle? buildInlineStyle(Element element) { List cssProperties = []; String cssText = ''; - element.inlineStyle.forEach((key, value) { + element.inlineStyle.forEach((key, entry) { String kebabName = kebabize(key); - String propertyValue = value.toString(); - String cssText0 = '$kebabName: $propertyValue'; + String propertyValue = entry.value; + String importantSuffix = entry.important ? ' !important' : ''; + String cssText0 = '$kebabName: $propertyValue$importantSuffix'; CSSProperty cssProperty = CSSProperty( name: kebabName, - value: value, + value: propertyValue, + important: entry.important, range: SourceRange( startLine: 0, startColumn: cssText.length, diff --git a/webf/lib/src/dom/element.dart b/webf/lib/src/dom/element.dart index 4905c53498..20ac4651e4 100644 --- a/webf/lib/src/dom/element.dart +++ b/webf/lib/src/dom/element.dart @@ -134,13 +134,18 @@ abstract class Element extends ContainerNode final Map attributes = {}; /// The style of the element, not inline style. - late CSSStyleDeclaration style; + late ElementCSSStyleDeclaration style; /// The default user-agent style. Map get defaultStyle => {}; - /// The inline style is a map of style property name to style property value. - final Map inlineStyle = {}; + /// The inline style is a map of style property name to value/importance. + final Map inlineStyle = {}; + + // When Blink CSS is enabled, declared-value styles are computed on the native + // side and pushed over the bridge. These are non-inline stylesheet results + // and are used as the element's sheet style snapshot during recalc. + CSSStyleDeclaration? _sheetStyle; /// The StatefulElements that holding the reference of this elements @flutter.protected @@ -255,8 +260,7 @@ abstract class Element extends ContainerNode Element(BindingContext? context) : super(NodeType.ELEMENT_NODE, context) { // Init style and add change listener. - style = CSSStyleDeclaration.computedStyle( - this, defaultStyle, _onStyleChanged, _onStyleFlushed); + style = ElementCSSStyleDeclaration.computedStyle(this, defaultStyle, _onStyleChanged, _onStyleFlushed); // Init render style. renderStyle = CSSRenderStyle(target: this); @@ -1474,7 +1478,8 @@ abstract class Element extends ContainerNode propertyHandler.deleter!(); } - if (hasAttribute(qualifiedName)) { + final bool hasTargetAttribute = hasAttribute(qualifiedName); + if (hasTargetAttribute) { attributes.remove(qualifiedName); final isNeedRecalculate = _checkRecalculateStyle([qualifiedName]); if (DebugFlags.enableCssBatchRecalc) { @@ -1500,6 +1505,16 @@ abstract class Element extends ContainerNode // Mark semantics dirty for accessibility-relevant attributes. _markSemanticsDirtyIfNeeded(qualifiedName); + } else if (qualifiedName == _styleProperty) { + // Style changes from native may not populate the attribute map; still + // trigger recalc so stylesheet cascade is restored after inline removal. + final isNeedRecalculate = _checkRecalculateStyle([qualifiedName]); + if (DebugFlags.enableCssBatchRecalc) { + ownerDocument.markElementStyleDirty(this, + reason: 'batch:remove:$qualifiedName'); + } else { + recalculateStyle(rebuildNested: isNeedRecalculate); + } } } @@ -1696,27 +1711,37 @@ abstract class Element extends ContainerNode } } - void applyDefaultStyle(CSSStyleDeclaration style) { + void applyDefaultStyle(ElementCSSStyleDeclaration style) { if (defaultStyle.isNotEmpty) { defaultStyle.forEach((propertyName, value) { if (style.contains(propertyName) == false) { - style.setProperty(propertyName, value); + style.enqueueSheetProperty(propertyName, value.toString()); } }); } } - void applyInlineStyle(CSSStyleDeclaration style) { + void applyInlineStyle(ElementCSSStyleDeclaration style) { if (inlineStyle.isNotEmpty) { - inlineStyle.forEach((propertyName, value) { - // Force inline style to be applied as important priority. - style.setProperty(propertyName, value, isImportant: true); + inlineStyle.forEach((propertyName, entry) { + if (entry.value.isEmpty) return; + style.enqueueInlineProperty(propertyName, entry.value, + isImportant: entry.important ? true : null); }); } } - void _applySheetStyle(CSSStyleDeclaration style) { - CSSStyleDeclaration matchRule = _collectMatchedRulesWithCache(); + void _applySheetStyle(ElementCSSStyleDeclaration style) { + final bool enableBlink = ownerDocument.ownerView.enableBlink; + if (enableBlink) { + final CSSStyleDeclaration? sheetStyle = _sheetStyle; + if (sheetStyle != null && sheetStyle.isNotEmpty) { + style.union(sheetStyle); + } + return; + } + + final CSSStyleDeclaration matchRule = _collectMatchedRulesWithCache(); style.union(matchRule); } @@ -1982,31 +2007,223 @@ abstract class Element extends ContainerNode // Set inline style property. void setInlineStyle(String property, String value, - {String? baseHref, bool fromNative = false}) { + {String? baseHref, bool fromNative = false, bool important = false}) { final bool enableBlink = ownerDocument.ownerView.enableBlink; - final bool validate = !(fromNative && enableBlink); - // Current only for mark property is setting by inline style. - inlineStyle[property] = value; + // Inline styles are merged on the Dart side (even in Blink mode), so keep + // Dart-side validation enabled for inline declarations. + final bool validateInline = true; + // Sheet styles pushed from native Blink are already validated; avoid + // re-validating them on the Dart side. + final bool validateSheet = !(fromNative && enableBlink); + final InlineStyleEntry? previousEntry = inlineStyle[property]; + + bool derivedImportant = important; + String derivedValue = value; + + // Legacy native bridge encodes CSSOM priority by appending `!important` + // into the value string (for older versions where UICommandType.setInlineStyle + // doesn't carry a priority field). Decode it back to the structured `important` flag so + // Dart-side parsing/cascade works as expected. + if (fromNative && !derivedImportant) { + int end = derivedValue.length; + while (end > 0 && derivedValue.codeUnitAt(end - 1) <= 0x20) { + end--; + } + + const String keyword = 'important'; + if (end >= keyword.length) { + final int keywordStart = end - keyword.length; + if (derivedValue.substring(keywordStart, end).toLowerCase() == keyword) { + int i = keywordStart; + while (i > 0 && derivedValue.codeUnitAt(i - 1) <= 0x20) { + i--; + } + if (i > 0 && derivedValue.codeUnitAt(i - 1) == 0x21) { + derivedImportant = true; + derivedValue = derivedValue.substring(0, i - 1).trimRight(); + } + } + } + } + + InlineStyleEntry entry = + InlineStyleEntry(derivedValue, important: derivedImportant); + if (fromNative && !derivedImportant) { + entry = _normalizeInlineStyleEntryFromNative(entry); + } // recalculate matching styles for element when inline styles are removed. - if (value.isEmpty) { - style.removeProperty(property, true); - // When Blink CSS is enabled, style cascading and validation happen on - // the native side. Avoid expensive Dart-side recalculation here. + if (entry.value.isEmpty) { + inlineStyle.remove(property); + final bool? wasImportant = + (previousEntry?.important ?? entry.important) ? true : null; + style.removeProperty(property, wasImportant); + if (fromNative && enableBlink) { + final CSSStyleDeclaration? sheetStyle = _sheetStyle; + if (sheetStyle != null) { + final String sheetValue = sheetStyle.getPropertyValue(property); + if (sheetValue.isNotEmpty) { + style.enqueueSheetProperty( + property, + sheetValue, + isImportant: sheetStyle.isImportant(property) ? true : null, + baseHref: sheetStyle.getPropertyBaseHref(property), + validate: validateSheet, + ); + } + } + } + // When Blink CSS is enabled, non-inline cascading happens on the native + // side. Avoid expensive Dart-side full recalculation here. if (!(fromNative && enableBlink)) { recalculateStyle(); } + return; } else { - style.setProperty(property, value, - isImportant: true, baseHref: baseHref, validate: validate); + // Current only for mark property is setting by inline style. + inlineStyle[property] = entry; + if (previousEntry?.important == true && !entry.important) { + style.removeProperty(property, true); + } + style.enqueueInlineProperty(property, entry.value, + isImportant: entry.important ? true : null, baseHref: baseHref, validate: validateInline); } } - void clearInlineStyle() { - for (var key in inlineStyle.keys) { - style.removeProperty(key, true); + // Set non-inline (sheet) style property pushed from native Blink style engine. + void setSheetStyle(String property, String value, + {String? baseHref, bool fromNative = false, bool important = false}) { + final bool enableBlink = ownerDocument.ownerView.enableBlink; + final bool validate = !(fromNative && enableBlink); + final bool previousImportant = _sheetStyle?.isImportant(property) ?? false; + + bool derivedImportant = important; + String derivedValue = value; + + // Compatibility: allow `!important` suffix in the value string. + if (fromNative && !derivedImportant) { + int end = derivedValue.length; + while (end > 0 && derivedValue.codeUnitAt(end - 1) <= 0x20) { + end--; + } + + const String keyword = 'important'; + if (end >= keyword.length) { + final int keywordStart = end - keyword.length; + if (derivedValue.substring(keywordStart, end).toLowerCase() == keyword) { + int i = keywordStart; + while (i > 0 && derivedValue.codeUnitAt(i - 1) <= 0x20) { + i--; + } + if (i > 0 && derivedValue.codeUnitAt(i - 1) == 0x21) { + derivedImportant = true; + derivedValue = derivedValue.substring(0, i - 1).trimRight(); + } + } + } + } + + InlineStyleEntry entry = InlineStyleEntry(derivedValue, important: derivedImportant); + if (fromNative && !derivedImportant) { + entry = _normalizeInlineStyleEntryFromNative(entry); + } + + // Removing a sheet declaration. + if (entry.value.isEmpty) { + _sheetStyle?.removeProperty(property); + if (_sheetStyle?.isEmpty == true) { + _sheetStyle = null; + } + + // If there is an inline declaration, re-enqueue it so we don't lose it + // after clearing a previously-winning sheet `!important`. + final InlineStyleEntry? inlineEntry = inlineStyle[property]; + if (inlineEntry != null && inlineEntry.value.isNotEmpty) { + style.enqueueInlineProperty( + property, + inlineEntry.value, + isImportant: inlineEntry.important ? true : null, + validate: validate, + ); + } else { + style.removeProperty(property, previousImportant ? true : null); + } + return; } + + (_sheetStyle ??= CSSStyleDeclaration.sheet()).setProperty( + property, + entry.value, + isImportant: entry.important ? true : null, + propertyType: PropertyType.sheet, + baseHref: baseHref, + validate: validate, + ); + + // If importance was downgraded, clear the previous important value so + // subsequent sheet updates are not blocked by stale `!important`. + if (previousImportant && !entry.important) { + style.removeProperty(property, true); + } + style.enqueueSheetProperty( + property, + entry.value, + isImportant: entry.important ? true : null, + baseHref: baseHref, + validate: validate, + ); + } + + void clearInlineStyle() { + if (inlineStyle.isEmpty) return; + + final bool enableBlink = ownerDocument.ownerView.enableBlink; + final Map removedEntries = Map.from(inlineStyle); inlineStyle.clear(); + + if (!enableBlink) { + recalculateStyle(); + return; + } + + // Blink mode expects native-side cascade to follow with computed updates. + // Clear any stale inline overrides immediately. + for (final entry in removedEntries.entries) { + style.removeProperty(entry.key, entry.value.important ? true : null); + } + + final CSSStyleDeclaration? sheetStyle = _sheetStyle; + if (sheetStyle != null && sheetStyle.isNotEmpty) { + style.union(sheetStyle); + } + } + + // Clear declared-value styles pushed from the native Blink engine. + void clearSheetStyle() { + final CSSStyleDeclaration? removedSheetStyle = _sheetStyle; + if (removedSheetStyle == null || removedSheetStyle.isEmpty) return; + _sheetStyle = null; + + for (final entry in removedSheetStyle) { + style.removeProperty(entry.key, entry.value.important ? true : null); + } + + // Re-apply inline declarations after removing sheet overrides so inline + // styles remain effective (especially when a sheet `!important` was + // previously winning). + if (inlineStyle.isNotEmpty) { + final bool enableBlink = ownerDocument.ownerView.enableBlink; + final bool validate = !enableBlink; + inlineStyle.forEach((propertyName, inlineEntry) { + if (inlineEntry.value.isEmpty) return; + style.enqueueInlineProperty( + propertyName, + inlineEntry.value, + isImportant: inlineEntry.important ? true : null, + validate: validate, + ); + }); + } } // Set pseudo element (::before, ::after, ::first-letter, ::first-line) style. @@ -2028,32 +2245,34 @@ abstract class Element extends ContainerNode style.clearPseudoStyle(type); } - void _applyPseudoStyle(CSSStyleDeclaration style) { - List pseudoRules = - _elementRuleCollector.matchedPseudoRules(ownerDocument.ruleSet, this); + void _applyPseudoStyle(ElementCSSStyleDeclaration style) { + final RuleSet ruleSet = ownerDocument.ruleSet; + if (!ruleSet.hasPseudoElementSelectors) return; + + final List pseudoRules = _elementRuleCollector.matchedPseudoRules(ruleSet, this); style.handlePseudoRules(this, pseudoRules); } - void applyStyle(CSSStyleDeclaration style) { + void applyStyle(ElementCSSStyleDeclaration style) { // Apply default style. applyDefaultStyle(style); // Init display from style directly cause renderStyle is not flushed yet. renderStyle.initDisplay(style); applyAttributeStyle(style); - applyInlineStyle(style); _applySheetStyle(style); + applyInlineStyle(style); _applyPseudoStyle(style); } - void applyAttributeStyle(CSSStyleDeclaration style) { + void applyAttributeStyle(ElementCSSStyleDeclaration style) { // Map the dir attribute to CSS direction so inline layout picks up RTL/LTR hints. final String? dirAttr = attributes['dir']; if (dirAttr != null) { final String normalized = dirAttr.trim().toLowerCase(); final TextDirection? resolved = CSSTextMixin.resolveDirection(normalized); if (resolved != null) { - style.setProperty(DIRECTION, normalized); + style.enqueueSheetProperty(DIRECTION, normalized); } } } @@ -2078,13 +2297,14 @@ abstract class Element extends ContainerNode renderStyle.display != CSSDisplay.none || shouldUpdateCSSVariables) { // Diff style. - CSSStyleDeclaration newStyle = CSSStyleDeclaration(); + ElementCSSStyleDeclaration newStyle = ElementCSSStyleDeclaration(); applyStyle(newStyle); - var hasInheritedPendingProperty = false; - if (style.merge(newStyle)) { - hasInheritedPendingProperty = style.hasInheritedPendingProperty; - style.flushPendingProperties(); - } + style.merge(newStyle); + final bool hasInheritedPendingProperty = style.hasInheritedPendingProperty; + // Always flush after a recalc. Pending properties may have already been + // queued (e.g. via setInlineStyle) and merge() can be a no-op when the + // queued values match the freshly computed ones. + style.flushPendingProperties(); if (rebuildNested || hasInheritedPendingProperty) { // Update children style. @@ -2097,15 +2317,56 @@ abstract class Element extends ContainerNode } void _removeInlineStyle() { - inlineStyle.forEach((String property, _) { - _removeInlineStyleProperty(property); + inlineStyle.forEach((String property, InlineStyleEntry entry) { + _removeInlineStyleProperty(property, entry.important ? true : null); }); inlineStyle.clear(); style.flushPendingProperties(); } - void _removeInlineStyleProperty(String property) { - style.removeProperty(property, true); + void _removeInlineStyleProperty(String property, bool? important) { + style.removeProperty(property, important); + } + + InlineStyleEntry _normalizeInlineStyleEntryFromNative(InlineStyleEntry entry) { + final String raw = entry.value; + int depth = 0; + String? quote; + int importantIndex = -1; + for (int i = 0; i < raw.length; i++) { + final String ch = raw[i]; + if (quote != null) { + if (ch == quote) quote = null; + continue; + } + if (ch == '"' || ch == '\'') { + quote = ch; + continue; + } + if (ch == '(') { + depth++; + continue; + } + if (ch == ')') { + if (depth > 0) depth--; + continue; + } + if (depth == 0 && ch == '!' && i + 10 <= raw.length) { + final String suffix = raw.substring(i + 1, i + 10).toLowerCase(); + if (suffix == 'important') { + final String trailing = raw.substring(i + 10).trim(); + if (trailing.isEmpty) { + importantIndex = i; + } + break; + } + } + } + if (importantIndex >= 0) { + final String stripped = raw.substring(0, importantIndex).trimRight(); + return InlineStyleEntry(stripped, important: true); + } + return entry; } // The Element.getBoundingClientRect() method returns a DOMRect object providing information diff --git a/webf/lib/src/html/form/base_input.dart b/webf/lib/src/html/form/base_input.dart index 6ac5892622..b921350a7d 100644 --- a/webf/lib/src/html/form/base_input.dart +++ b/webf/lib/src/html/form/base_input.dart @@ -202,13 +202,13 @@ mixin BaseInputElement on WidgetElement implements FormElementBase { case 'checkbox': { _checkboxDefaultStyle.forEach((key, value) { - style.setProperty(key, value); + style.enqueueSheetProperty(key, value.toString()); }); break; } default: _inputDefaultStyle.forEach((key, value) { - style.setProperty(key, value); + style.enqueueSheetProperty(key, value.toString()); }); break; } diff --git a/webf/lib/src/html/grouping_content.dart b/webf/lib/src/html/grouping_content.dart index 880c804f7e..babef0f512 100644 --- a/webf/lib/src/html/grouping_content.dart +++ b/webf/lib/src/html/grouping_content.dart @@ -127,12 +127,12 @@ class LIElement extends Element { // For the default outside position, markers are painted by renderer // as separate marker boxes and must not participate in IFC. @override - void applyStyle(CSSStyleDeclaration style) { + void applyStyle(ElementCSSStyleDeclaration style) { // 1) Apply element default styles (UA defaults). if (defaultStyle.isNotEmpty) { defaultStyle.forEach((propertyName, value) { if (style.contains(propertyName) == false) { - style.setProperty(propertyName, value); + style.enqueueSheetProperty(propertyName, value.toString()); } }); } @@ -143,21 +143,24 @@ class LIElement extends Element { // 4) Attribute styles (none for LI currently but keep for completeness). applyAttributeStyle(style); - // 5) Inline styles (highest priority among author styles). - if (inlineStyle.isNotEmpty) { - inlineStyle.forEach((propertyName, value) { - style.setProperty(propertyName, value, isImportant: true); - }); - } - - // 6) Stylesheet rules matching this element. + // 5) Stylesheet rules matching this element. final ElementRuleCollector collector = ElementRuleCollector(); final CSSStyleDeclaration matchRule = collector.collectionFromRuleSet(ownerDocument.ruleSet, this); style.union(matchRule); + // 6) Inline styles (highest priority among author styles). + if (inlineStyle.isNotEmpty) { + inlineStyle.forEach((propertyName, entry) { + style.enqueueInlineProperty(propertyName, entry.value, isImportant: entry.important ? true : null); + }); + } + // 7) Pseudo rules (::before/::after) from stylesheets to override defaults. - final List pseudoRules = collector.matchedPseudoRules(ownerDocument.ruleSet, this); - style.handlePseudoRules(this, pseudoRules); + final RuleSet ruleSet = ownerDocument.ruleSet; + if (ruleSet.hasPseudoElementSelectors) { + final List pseudoRules = collector.matchedPseudoRules(ruleSet, this); + style.handlePseudoRules(this, pseudoRules); + } // 8) List marker generation based on list-style-type String getProp(CSSStyleDeclaration s, String camel, String kebab) { @@ -237,7 +240,7 @@ class LIElement extends Element { } void ensurePseudo() { - style.pseudoBeforeStyle ??= CSSStyleDeclaration(); + style.pseudoBeforeStyle ??= CSSStyleDeclaration.sheet(); } String effectiveListStylePosition() { diff --git a/webf/lib/src/html/svg.dart b/webf/lib/src/html/svg.dart index 0213ee233c..a81d26d65a 100644 --- a/webf/lib/src/html/svg.dart +++ b/webf/lib/src/html/svg.dart @@ -50,8 +50,10 @@ class FlutterSvgElement extends WidgetElement { } @override - void setInlineStyle(String property, String value, {String? baseHref, bool fromNative = false}) { - super.setInlineStyle(property, value, baseHref: baseHref, fromNative: fromNative); + void setInlineStyle(String property, String value, + {String? baseHref, bool fromNative = false, bool important = false}) { + super.setInlineStyle(property, value, + baseHref: baseHref, fromNative: fromNative, important: important); _notifyAncestorSvgToRebuild(); } @@ -106,8 +108,10 @@ class FlutterSVGChildElement extends dom.Element { } @override - void setInlineStyle(String property, String value, {String? baseHref, bool fromNative = false}) { - super.setInlineStyle(property, value, baseHref: baseHref, fromNative: fromNative); + void setInlineStyle(String property, String value, + {String? baseHref, bool fromNative = false, bool important = false}) { + super.setInlineStyle(property, value, + baseHref: baseHref, fromNative: fromNative, important: important); _notifyRootSvgToRebuild(); } diff --git a/webf/lib/src/launcher/view_controller.dart b/webf/lib/src/launcher/view_controller.dart index e726972f61..924a7dea4b 100644 --- a/webf/lib/src/launcher/view_controller.dart +++ b/webf/lib/src/launcher/view_controller.dart @@ -670,8 +670,8 @@ class WebFViewController with Diagnosticable implements WidgetsBindingObserver { if (originalTarget is Element) { Element newElement = newTarget as Element; // Copy inline style. - originalTarget.inlineStyle.forEach((key, value) { - newElement.setInlineStyle(key, value); + originalTarget.inlineStyle.forEach((key, entry) { + newElement.setInlineStyle(key, entry.value, important: entry.important); }); // Copy element attributes. originalTarget.attributes.forEach((key, value) { @@ -833,13 +833,23 @@ class WebFViewController with Diagnosticable implements WidgetsBindingObserver { context2d?.requestPaint(); } - void setInlineStyle(Pointer selfPtr, String key, String value, {String? baseHref}) { + void setInlineStyle(Pointer selfPtr, String key, String value, {String? baseHref, bool important = false}) { assert(hasBindingObject(selfPtr), 'id: $selfPtr key: $key value: $value'); Node? target = getBindingObject(selfPtr); if (target == null) return; if (target is Element) { - target.setInlineStyle(key, value, baseHref: baseHref, fromNative: true); + target.setInlineStyle(key, value, baseHref: baseHref, fromNative: true, important: important); + } + } + + void setSheetStyle(Pointer selfPtr, String key, String value, {String? baseHref, bool important = false}) { + assert(hasBindingObject(selfPtr), 'id: $selfPtr key: $key value: $value'); + Node? target = getBindingObject(selfPtr); + if (target == null) return; + + if (target is Element) { + target.setSheetStyle(key, value, baseHref: baseHref, fromNative: true, important: important); } } @@ -853,6 +863,16 @@ class WebFViewController with Diagnosticable implements WidgetsBindingObserver { } } + void clearSheetStyle(Pointer selfPtr) { + assert(hasBindingObject(selfPtr), 'id: $selfPtr'); + Node? target = getBindingObject(selfPtr); + if (target == null) return; + + if (target is Element) { + target.clearSheetStyle(); + } + } + void setPseudoStyle(Pointer selfPtr, String args, String key, String value, {String? baseHref}) { assert(hasBindingObject(selfPtr), 'id: $selfPtr'); Node? target = getBindingObject(selfPtr); diff --git a/webf/lib/src/widget/widget_element.dart b/webf/lib/src/widget/widget_element.dart index 0940997087..8f03fa3ce8 100644 --- a/webf/lib/src/widget/widget_element.dart +++ b/webf/lib/src/widget/widget_element.dart @@ -116,9 +116,11 @@ abstract class WidgetElement extends dom.Element { void didAttachRenderer([Element? flutterWidgetElement]) {} @override - void setInlineStyle(String property, String value, {String? baseHref, bool fromNative = false}) { + void setInlineStyle(String property, String value, + {String? baseHref, bool fromNative = false, bool important = false}) { bool shouldRebuild = shouldElementRebuild(property, style.getPropertyValue(property), value); - super.setInlineStyle(property, value, baseHref: baseHref, fromNative: fromNative); + super.setInlineStyle(property, value, + baseHref: baseHref, fromNative: fromNative, important: important); if (state != null && shouldRebuild) { state!.requestUpdateState(); } diff --git a/webf/test/local_http_server.dart b/webf/test/local_http_server.dart index bfe95efcbe..fafdf8fccf 100644 --- a/webf/test/local_http_server.dart +++ b/webf/test/local_http_server.dart @@ -6,6 +6,7 @@ */ import 'dart:convert'; +import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; @@ -33,14 +34,21 @@ class LocalHttpServer { static String basePath = 'assets'; - final int port = _randomPort(); + int port = _randomPort(); ServerSocket? _server; Uri getUri([String? path]) { return Uri.http('${InternetAddress.loopbackIPv4.host}:$port', path ?? ''); } - void _startServer() { + static bool _isAddressInUse(SocketException error) { + final int? code = error.osError?.errorCode; + if (code == 48 || code == 98) return true; // macOS/Linux EADDRINUSE + final String message = error.message.toLowerCase(); + return message.contains('address already in use'); + } + + void _startServer([int attempt = 0]) { ServerSocket.bind(InternetAddress.loopbackIPv4, port).then((ServerSocket server) { _server = server; server.listen((Socket socket) { @@ -101,6 +109,13 @@ class LocalHttpServer { print('$error $stackTrace'); }); }); + }).catchError((Object error, StackTrace stackTrace) { + if (error is SocketException && _isAddressInUse(error) && attempt < 20) { + port = _randomPort(); + _startServer(attempt + 1); + return; + } + Zone.current.handleUncaughtError(error, stackTrace); }); } diff --git a/webf/test/src/css/background_shorthand_clip_text_test.dart b/webf/test/src/css/background_shorthand_clip_text_test.dart index 9cc51f1d5d..fed018c70c 100644 --- a/webf/test/src/css/background_shorthand_clip_text_test.dart +++ b/webf/test/src/css/background_shorthand_clip_text_test.dart @@ -30,4 +30,3 @@ void main() { }); }); } - diff --git a/webf/test/src/css/css_wide_keywords_inherit_test.dart b/webf/test/src/css/css_wide_keywords_inherit_test.dart index 34923524c7..6065ab4b29 100644 --- a/webf/test/src/css/css_wide_keywords_inherit_test.dart +++ b/webf/test/src/css/css_wide_keywords_inherit_test.dart @@ -21,4 +21,3 @@ void main() { expect(style.getPropertyValue(LEFT), INHERIT); }); } - diff --git a/webf/test/src/css/inline_style_important_from_native_test.dart b/webf/test/src/css/inline_style_important_from_native_test.dart new file mode 100644 index 0000000000..9cc639472c --- /dev/null +++ b/webf/test/src/css/inline_style_important_from_native_test.dart @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webf/dom.dart' as dom; +import 'package:webf/webf.dart'; + +import '../../setup.dart'; +import '../widget/test_utils.dart'; + +void main() { + setUpAll(() { + setupTest(); + }); + + setUp(() { + WebFControllerManager.instance.initialize( + WebFControllerManagerConfig( + maxAliveInstances: 5, + maxAttachedInstances: 5, + enableDevTools: false, + ), + ); + }); + + tearDown(() async { + WebFControllerManager.instance.disposeAll(); + await Future.delayed(const Duration(milliseconds: 100)); + }); + + testWidgets('native inline value "!important" is decoded as priority', + (WidgetTester tester) async { + final prepared = await WebFWidgetTestUtils.prepareCustomWidgetTest( + tester: tester, + controllerName: 'native-important-${DateTime.now().millisecondsSinceEpoch}', + createController: () => WebFController( + enableBlink: false, + viewportWidth: 360, + viewportHeight: 640, + ), + bundle: WebFBundle.fromContent( + ''' + + + + + +
test
+ + +''', + url: 'test://native-inline-important/', + contentType: htmlContentType, + ), + ); + + final dom.Element target = prepared.getElementById('t'); + + expect(target.renderStyle.color.value.value, equals(0xFF008000)); + + target.setInlineStyle('color', 'rgb(255, 0, 0) !important', fromNative: true); + target.style.flushPendingProperties(); + + expect(target.inlineStyle['color']?.important, isTrue); + expect(target.inlineStyle['color']?.value, equals('rgb(255, 0, 0)')); + expect(target.renderStyle.color.value.value, equals(0xFFFF0000)); + }); +} + diff --git a/webf/test/src/css/inline_style_important_upgrade_same_value_test.dart b/webf/test/src/css/inline_style_important_upgrade_same_value_test.dart new file mode 100644 index 0000000000..68244d0fae --- /dev/null +++ b/webf/test/src/css/inline_style_important_upgrade_same_value_test.dart @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webf/dom.dart' as dom; +import 'package:webf/webf.dart'; + +import '../../setup.dart'; +import '../widget/test_utils.dart'; + +void main() { + setUpAll(() { + setupTest(); + }); + + setUp(() { + WebFControllerManager.instance.initialize( + WebFControllerManagerConfig( + maxAliveInstances: 5, + maxAttachedInstances: 5, + enableDevTools: false, + ), + ); + }); + + tearDown(() async { + WebFControllerManager.instance.disposeAll(); + await Future.delayed(const Duration(milliseconds: 100)); + }); + + testWidgets('inline importance upgrade applies even when value unchanged', (WidgetTester tester) async { + final String inlineBaseHref = 'test://inline-important-upgrade-${DateTime.now().millisecondsSinceEpoch}/'; + + final prepared = await WebFWidgetTestUtils.prepareCustomWidgetTest( + tester: tester, + controllerName: 'inline-important-upgrade-${DateTime.now().millisecondsSinceEpoch}', + createController: () => WebFController( + enableBlink: false, + viewportWidth: 360, + viewportHeight: 640, + ), + bundle: WebFBundle.fromContent( + ''' + + + + + +
test
+ + +''', + url: 'test://inline-important-upgrade/', + contentType: htmlContentType, + ), + ); + + final dom.Element target = prepared.getElementById('t'); + + expect(target.renderStyle.color.value.value, equals(0xFFFF0000)); + expect(target.style.getPropertyBaseHref('color'), isNull); + + target.setInlineStyle('color', 'rgb(255, 0, 0)'); + target.style.flushPendingProperties(); + + expect(target.inlineStyle['color']?.important, isFalse); + expect(target.style.getPropertyBaseHref('color'), isNull); + + target.setInlineStyle( + 'color', + 'rgb(255, 0, 0)', + important: true, + baseHref: inlineBaseHref, + ); + target.style.flushPendingProperties(); + + expect(target.inlineStyle['color']?.important, isTrue); + expect(target.style.getPropertyBaseHref('color'), equals(inlineBaseHref)); + }); +} + diff --git a/webf/test/src/css/inline_style_remove_fallback_test.dart b/webf/test/src/css/inline_style_remove_fallback_test.dart new file mode 100644 index 0000000000..33063fbcfa --- /dev/null +++ b/webf/test/src/css/inline_style_remove_fallback_test.dart @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022-present The WebF authors. All rights reserved. + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webf/dom.dart' as dom; +import 'package:webf/webf.dart'; + +import '../../setup.dart'; +import '../widget/test_utils.dart'; + +void main() { + setUpAll(() { + setupTest(); + }); + + setUp(() { + WebFControllerManager.instance.initialize( + WebFControllerManagerConfig( + maxAliveInstances: 5, + maxAttachedInstances: 5, + enableDevTools: false, + ), + ); + }); + + tearDown(() async { + WebFControllerManager.instance.disposeAll(); + await Future.delayed(const Duration(milliseconds: 100)); + }); + + testWidgets('Removing inline style falls back to stylesheet value', (WidgetTester tester) async { + final prepared = await WebFWidgetTestUtils.prepareCustomWidgetTest( + tester: tester, + controllerName: 'inline-style-remove-fallback-${DateTime.now().millisecondsSinceEpoch}', + createController: () => WebFController( + enableBlink: false, + viewportWidth: 360, + viewportHeight: 640, + ), + bundle: WebFBundle.fromContent( + ''' + + + + + +
test
+ + +''', + url: 'test://inline-style-remove-fallback/', + contentType: htmlContentType, + ), + ); + + final dom.Element target = prepared.getElementById('t'); + expect(target.renderStyle.color.value.value, equals(0xFFFF0000)); + + target.setInlineStyle('color', 'rgb(0, 0, 255)', fromNative: true); + target.style.flushPendingProperties(); + + expect(target.inlineStyle['color']?.value, equals('rgb(0, 0, 255)')); + expect(target.renderStyle.color.value.value, equals(0xFF0000FF)); + + target.setInlineStyle('color', '', fromNative: true); + target.style.flushPendingProperties(); + + expect(target.inlineStyle.containsKey('color'), isFalse); + expect(target.renderStyle.color.value.value, equals(0xFFFF0000)); + }); + + testWidgets('Clearing inline styles falls back to stylesheet value', (WidgetTester tester) async { + final prepared = await WebFWidgetTestUtils.prepareCustomWidgetTest( + tester: tester, + controllerName: 'inline-style-clear-fallback-${DateTime.now().millisecondsSinceEpoch}', + createController: () => WebFController( + enableBlink: false, + viewportWidth: 360, + viewportHeight: 640, + ), + bundle: WebFBundle.fromContent( + ''' + + + + + +
test
+ + +''', + url: 'test://inline-style-clear-fallback/', + contentType: htmlContentType, + ), + ); + + final dom.Element target = prepared.getElementById('t'); + expect(target.renderStyle.color.value.value, equals(0xFFFF0000)); + + target.setInlineStyle('color', 'rgb(0, 0, 255)', fromNative: true); + target.style.flushPendingProperties(); + expect(target.renderStyle.color.value.value, equals(0xFF0000FF)); + + target.clearInlineStyle(); + target.style.flushPendingProperties(); + + expect(target.inlineStyle.isEmpty, isTrue); + expect(target.renderStyle.color.value.value, equals(0xFFFF0000)); + }); +} + diff --git a/webf/test/src/css/inset_shorthand_test.dart b/webf/test/src/css/inset_shorthand_test.dart index ae0fb34053..d0b4d3898f 100644 --- a/webf/test/src/css/inset_shorthand_test.dart +++ b/webf/test/src/css/inset_shorthand_test.dart @@ -54,7 +54,7 @@ void main() { }); test('removes to initial longhands', () { - final CSSStyleDeclaration style = CSSStyleDeclaration(); + final ElementCSSStyleDeclaration style = ElementCSSStyleDeclaration(); style.setProperty(INSET, '20px'); style.removeProperty(INSET); @@ -76,4 +76,3 @@ void main() { }); }); } - diff --git a/webf/test/src/css/style_declaration_merge_union_test.dart b/webf/test/src/css/style_declaration_merge_union_test.dart new file mode 100644 index 0000000000..3d3adf94b0 --- /dev/null +++ b/webf/test/src/css/style_declaration_merge_union_test.dart @@ -0,0 +1,120 @@ +import 'package:test/test.dart'; +import 'package:webf/css.dart'; + +void main() { + group('CSSStyleDeclaration.union', () { + test('inline wins over sheet when same importance', () { + final ElementCSSStyleDeclaration target = ElementCSSStyleDeclaration(); + target.enqueueInlineProperty(COLOR, 'red'); + + final CSSStyleDeclaration sheet = CSSStyleDeclaration.sheet(); + sheet.setProperty(COLOR, 'blue'); + + target.union(sheet); + expect(target.getPropertyValue(COLOR), 'red'); + }); + + test('sheet overrides sheet when same importance', () { + final CSSStyleDeclaration a = CSSStyleDeclaration.sheet(); + a.setProperty(COLOR, 'red'); + + final CSSStyleDeclaration b = CSSStyleDeclaration.sheet(); + b.setProperty(COLOR, 'blue'); + + a.union(b); + expect(a.getPropertyValue(COLOR), 'blue'); + }); + + test('important beats non-important', () { + final CSSStyleDeclaration a = CSSStyleDeclaration.sheet(); + a.setProperty(COLOR, 'red', isImportant: true); + + final CSSStyleDeclaration b = CSSStyleDeclaration.sheet(); + b.setProperty(COLOR, 'blue'); + + a.union(b); + expect(a.getPropertyValue(COLOR), 'red'); + }); + + test('important overrides non-important', () { + final CSSStyleDeclaration a = CSSStyleDeclaration.sheet(); + a.setProperty(COLOR, 'red'); + + final CSSStyleDeclaration b = CSSStyleDeclaration.sheet(); + b.setProperty(COLOR, 'blue', isImportant: true); + + a.union(b); + expect(a.getPropertyValue(COLOR), 'blue'); + }); + + test('important inline wins over important sheet', () { + final ElementCSSStyleDeclaration target = ElementCSSStyleDeclaration(); + target.enqueueInlineProperty(COLOR, 'red', isImportant: true); + + final CSSStyleDeclaration sheet = CSSStyleDeclaration.sheet(); + sheet.setProperty(COLOR, 'blue', isImportant: true); + + target.union(sheet); + expect(target.getPropertyValue(COLOR), 'red'); + }); + }); + + group('CSSStyleDeclaration.merge', () { + test('updates changed property values', () { + final CSSStyleDeclaration a = CSSStyleDeclaration.sheet(); + a.setProperty(COLOR, 'red'); + + final CSSStyleDeclaration b = CSSStyleDeclaration.sheet(); + b.setProperty(COLOR, 'blue'); + + expect(a.merge(b), isTrue); + expect(a.getPropertyValue(COLOR), 'blue'); + }); + + test('removes properties missing from other', () { + final CSSStyleDeclaration a = CSSStyleDeclaration.sheet(); + a.setProperty(COLOR, 'red'); + + final CSSStyleDeclaration b = CSSStyleDeclaration.sheet(); + + expect(a.merge(b), isTrue); + expect(a.getPropertyValue(COLOR), EMPTY_STRING); + expect(a.length, 0); + }); + + test('adds properties present only on other', () { + final CSSStyleDeclaration a = CSSStyleDeclaration.sheet(); + + final CSSStyleDeclaration b = CSSStyleDeclaration.sheet(); + b.setProperty(COLOR, 'blue'); + + expect(a.merge(b), isTrue); + expect(a.getPropertyValue(COLOR), 'blue'); + }); + + test('returns false when no effective changes', () { + final CSSStyleDeclaration a = CSSStyleDeclaration.sheet(); + a.setProperty(COLOR, 'red'); + + final CSSStyleDeclaration b = CSSStyleDeclaration.sheet(); + b.setProperty(COLOR, 'red'); + + expect(a.merge(b), isFalse); + }); + + test('stages empty values when other explicitly clears a missing property', () { + final ElementCSSStyleDeclaration current = ElementCSSStyleDeclaration(); + + final ElementCSSStyleDeclaration other = ElementCSSStyleDeclaration(); + other.removeProperty(COLOR); + + expect(current.length, 0); + expect(other.length, 1); + + expect(current.merge(other), isTrue); + expect(current.getPropertyValue(COLOR), EMPTY_STRING); + expect(current.length, 1); + }); + }); +} + diff --git a/webf/test/src/css/style_inline_parser.dart b/webf/test/src/css/style_inline_parser.dart index 1e2de916f8..c88c5b0bc7 100644 --- a/webf/test/src/css/style_inline_parser.dart +++ b/webf/test/src/css/style_inline_parser.dart @@ -5,15 +5,24 @@ import 'package:webf/css.dart'; import 'package:test/test.dart'; -Map parseInlineStyle(String style) { +Map parseInlineStyle(String style) { return CSSParser(style).parseInlineStyle(); } void main() { group('CSSStyleRuleParser', () { test('0', () { - Map style = parseInlineStyle('color : red; background: red;'); - expect(style['color'], 'red'); + Map style = parseInlineStyle('color : red; background: red;'); + expect(style['color']?.value, 'red'); + expect(style['color']?.important, isFalse); + }); + + test('important', () { + Map style = + parseInlineStyle('color: red !important; background: red;'); + expect(style['color']?.value, 'red'); + expect(style['color']?.important, isTrue); + expect(style['background']?.important, isFalse); }); }); } diff --git a/webf/test/src/rendering/css_sizing_test.dart b/webf/test/src/rendering/css_sizing_test.dart index 98ce37b327..5aeae8ece4 100644 --- a/webf/test/src/rendering/css_sizing_test.dart +++ b/webf/test/src/rendering/css_sizing_test.dart @@ -57,7 +57,7 @@ void main() { expect(div.offsetWidth, equals(100.0)); // Change width dynamically - div.style.setProperty('width', '200px'); + div.style.enqueueInlineProperty('width', '200px'); await tester.pump(); expect(div.offsetWidth, equals(200.0)); @@ -234,7 +234,7 @@ void main() { expect(div.offsetHeight, equals(150.0)); // Change height dynamically - div.style.setProperty('height', '250px'); + div.style.enqueueInlineProperty('height', '250px'); await tester.pump(); expect(div.offsetHeight, equals(250.0)); @@ -774,8 +774,8 @@ void main() { expect(box.renderStyle.height.value, equals(100.0)); // Update styles - box.style.setProperty('width', '200px'); - box.style.setProperty('height', '150px'); + box.style.enqueueInlineProperty('width', '200px'); + box.style.enqueueInlineProperty('height', '150px'); box.style.flushPendingProperties(); await tester.pump(); await tester.pump();