From 5a01d899cd7db075f662cb2d859744d59324e73f Mon Sep 17 00:00:00 2001 From: Simon Corsin Date: Thu, 23 Jul 2026 12:21:15 -0500 Subject: [PATCH] Add hierarchical color palettes and theme-aware assets --- .../src/valdi/valdi_core/src/Asset.ts | 16 + .../valdi/valdi_core/src/ValdiRuntime.d.ts | 6 +- .../valdi_tsx/src/NativeTemplateElements.d.ts | 5 + valdi/src/valdi/android/NativeBridge.cpp | 7 +- valdi/src/valdi/ios/SCValdiRuntime.mm | 2 +- .../runtime/Attributes/AssetAttributes.cpp | 13 +- .../runtime/Attributes/AttributeHandler.cpp | 1 + .../valdi/runtime/Attributes/AttributeIds.cpp | 1 + .../valdi/runtime/Attributes/AttributeIds.hpp | 1 + .../AttributesBindingContextImpl.cpp | 52 ++-- .../AttributesBindingContextImpl.hpp | 6 +- .../runtime/Attributes/AttributesManager.cpp | 22 +- .../runtime/Attributes/AttributesManager.hpp | 9 +- .../Attributes/DefaultAttributeProcessors.cpp | 256 +++++++++++++--- .../Attributes/DefaultAttributeProcessors.hpp | 18 +- .../runtime/Attributes/DefaultAttributes.cpp | 1 + .../runtime/Attributes/ValueConverters.cpp | 19 +- .../runtime/Attributes/ValueConverters.hpp | 4 +- .../runtime/Attributes/ViewNodeAttribute.cpp | 12 +- .../runtime/Attributes/ViewNodeAttribute.hpp | 3 + .../Attributes/ViewNodeAttributesApplier.cpp | 31 +- .../Attributes/ViewNodeAttributesApplier.hpp | 3 + .../runtime/Context/ViewManagerContext.cpp | 4 +- .../runtime/Context/ViewManagerContext.hpp | 4 +- valdi/src/valdi/runtime/Context/ViewNode.cpp | 116 +++++++- valdi/src/valdi/runtime/Context/ViewNode.hpp | 19 +- .../valdi/runtime/Context/ViewNodeTree.cpp | 5 + .../runtime/JavaScript/JavaScriptRuntime.cpp | 57 +++- .../runtime/JavaScript/JavaScriptRuntime.hpp | 8 +- .../AttributedTextNativeModuleFactory.cpp | 10 +- .../AttributedTextNativeModuleFactory.hpp | 8 +- .../runtime/Rendering/ViewNodeRenderer.cpp | 6 +- .../runtime/Resources/DirectionalAsset.cpp | 11 +- .../runtime/Resources/DirectionalAsset.hpp | 2 +- .../Resources/PlatformSpecificAsset.cpp | 34 ++- .../Resources/PlatformSpecificAsset.hpp | 2 +- .../valdi/runtime/Resources/ThemableAsset.cpp | 75 +++++ .../valdi/runtime/Resources/ThemableAsset.hpp | 53 ++++ valdi/src/valdi/runtime/Runtime.cpp | 29 +- valdi/src/valdi/runtime/Runtime.hpp | 10 +- valdi/src/valdi/runtime/RuntimeManager.cpp | 84 ++---- valdi/src/valdi/runtime/RuntimeManager.hpp | 8 +- valdi/test/benchmark/ViewNode_benchmark.cpp | 2 +- valdi/test/integration/Runtime_tests.cpp | 281 ++++++++++++++++-- valdi/test/runtime/AttributeParser_tests.cpp | 4 +- .../runtime/AttributeProcessors_tests.cpp | 229 +++++++------- .../runtime/ColorPaletteManager_tests.cpp | 89 ++++++ valdi/test/utils/ViewNodeTestsUtils.cpp | 13 +- .../test/src/ColorPaletteOverrideTest.tsx | 49 +++ .../modules/test/src/ColorPaletteTest.tsx | 41 ++- .../modules/test/src/ThemableAsset.tsx | 53 ++++ .../cpp/Attributes/AttributeUtils.cpp | 29 +- .../cpp/Attributes/AttributeUtils.hpp | 2 + .../cpp/Attributes/ColorPalette.cpp | 66 +++- .../cpp/Attributes/ColorPalette.hpp | 44 ++- .../src/valdi_core/cpp/Resources/Asset.cpp | 11 +- .../src/valdi_core/cpp/Resources/Asset.hpp | 25 +- 57 files changed, 1550 insertions(+), 421 deletions(-) create mode 100644 valdi/src/valdi/runtime/Resources/ThemableAsset.cpp create mode 100644 valdi/src/valdi/runtime/Resources/ThemableAsset.hpp create mode 100644 valdi/test/runtime/ColorPaletteManager_tests.cpp create mode 100644 valdi/testdata/resources/modules/test/src/ColorPaletteOverrideTest.tsx create mode 100644 valdi/testdata/resources/modules/test/src/ThemableAsset.tsx diff --git a/src/valdi_modules/src/valdi/valdi_core/src/Asset.ts b/src/valdi_modules/src/valdi/valdi_core/src/Asset.ts index 32e27005..aa0b5564 100644 --- a/src/valdi_modules/src/valdi/valdi_core/src/Asset.ts +++ b/src/valdi_modules/src/valdi/valdi_core/src/Asset.ts @@ -73,6 +73,10 @@ export type PlatformAssetOverrides = { android?: string | Asset; }; +export type ThemableAssetMap = { + [colorPaletteName: string]: string | Asset; +}; + /** * Make a platform specific Asset from a default asset `defaultAsset` and * iOS and/or Android assets in `platformAssetOverrides`. The iOS asset will be @@ -97,6 +101,18 @@ export function makePlatformSpecificAsset( return runtime.makePlatformSpecificAsset(defaultAsset, platformAssetOverrides); } +/** + * Make a themable Asset from an object keyed by color palette name. + * The asset matching the resolved color palette of the element will be + * rendered. If no asset matches the resolved color palette, nothing is rendered. + * @param assetsByColorPalette an object containing asset overrides keyed by color palette name + * @returns an Asset that can be used as a src attribute, and will use the + * asset matching the resolved color palette. + */ +export function makeThemableAsset(assetsByColorPalette: ThemableAssetMap): Asset { + return runtime.makeThemableAsset(assetsByColorPalette); +} + /** * Callback called whenever an asset has finished loading. */ diff --git a/src/valdi_modules/src/valdi/valdi_core/src/ValdiRuntime.d.ts b/src/valdi_modules/src/valdi/valdi_core/src/ValdiRuntime.d.ts index f9b26acf..40acd082 100644 --- a/src/valdi_modules/src/valdi/valdi_core/src/ValdiRuntime.d.ts +++ b/src/valdi_modules/src/valdi/valdi_core/src/ValdiRuntime.d.ts @@ -2,7 +2,7 @@ import { RuntimeBase } from 'coreutils/src/RuntimeBase'; import { ElementFrame } from 'valdi_tsx/src/Geometry'; import { NativeNode } from 'valdi_tsx/src/NativeNode'; import { NativeView } from 'valdi_tsx/src/NativeView'; -import { Asset, PlatformAssetOverrides } from './Asset'; +import { Asset, PlatformAssetOverrides, ThemableAssetMap } from './Asset'; import { ElementId } from './IRenderedElement'; import { IRootComponentsManager } from './IRootComponentsManager'; import { RenderRequest } from './RenderRequest'; @@ -163,6 +163,7 @@ export interface ValdiRuntime extends RuntimeBase { makeAssetFromBytes(bytes: ArrayBuffer | Uint8Array): Asset; makeDirectionalAsset(ltrAsset: string | Asset, rtlAsset: string | Asset): Asset; makePlatformSpecificAsset(defaultAsset: string | Asset, platformAssetOverrides: PlatformAssetOverrides): Asset; + makeThemableAsset(assetsByColorPalette: ThemableAssetMap): Asset; getAssets(catalogPath: string): AssetEntry[]; addAssetLoadObserver( asset: string | Asset, @@ -173,7 +174,8 @@ export interface ValdiRuntime extends RuntimeBase { ): () => void; getLoadedAssetMetadata(loadedAsset: LoadedAsset): LoadedAssetMetadata | undefined; - setColorPalette(colorPalette: ColorPalette): void; + configureColorPalette(name: string, colorPalette: ColorPalette): void; + setActiveColorPalette(name: string): void; outputLog(type: number, content: string): void; diff --git a/src/valdi_modules/src/valdi/valdi_tsx/src/NativeTemplateElements.d.ts b/src/valdi_modules/src/valdi/valdi_tsx/src/NativeTemplateElements.d.ts index 1a77455f..7d4ae7da 100644 --- a/src/valdi_modules/src/valdi/valdi_tsx/src/NativeTemplateElements.d.ts +++ b/src/valdi_modules/src/valdi/valdi_tsx/src/NativeTemplateElements.d.ts @@ -75,6 +75,11 @@ interface LayoutAttributes { */ lazyLayout?: boolean; + /** + * Overrides the configured color palette for this element and descendants. + */ + colorPaletteName?: string; + /** * @experimental This feature is experimental and may change in future releases. * diff --git a/valdi/src/valdi/android/NativeBridge.cpp b/valdi/src/valdi/android/NativeBridge.cpp index 784c3d8b..1504f3a3 100644 --- a/valdi/src/valdi/android/NativeBridge.cpp +++ b/valdi/src/valdi/android/NativeBridge.cpp @@ -1865,9 +1865,10 @@ jlong ValdiAndroid::NativeBridge::createViewFactory( // NOLINT auto attributes = viewManagerContext->getAttributesManager().getAttributesForClass(cppViewClassName); if (hasBindAttributes == JNI_TRUE) { - Valdi::AttributesBindingContextImpl bindingContext(viewManagerContext->getAttributesManager().getAttributeIds(), - viewManagerContext->getAttributesManager().getColorPalette(), - runtimeManagerWrapper->getRuntimeManager().getLogger()); + Valdi::AttributesBindingContextImpl bindingContext( + viewManagerContext->getAttributesManager().getAttributeIds(), + viewManagerContext->getAttributesManager().getColorPaletteManager(), + runtimeManagerWrapper->getRuntimeManager().getLogger()); auto wrapper = Valdi::makeShared(androidViewManager, bindingContext); auto ptr = reinterpret_cast(Valdi::unsafeBridgeCast(wrapper.get())); diff --git a/valdi/src/valdi/ios/SCValdiRuntime.mm b/valdi/src/valdi/ios/SCValdiRuntime.mm index cd723439..4609fcd0 100644 --- a/valdi/src/valdi/ios/SCValdiRuntime.mm +++ b/valdi/src/valdi/ios/SCValdiRuntime.mm @@ -454,7 +454,7 @@ - (void)setPerformHapticFeedbackFunctionBlock:(void (^)(NSString *))block auto attributes = attributesManager.getAttributesForClass(viewClassName); if (attributesBinder) { - Valdi::AttributesBindingContextImpl bindingContext(attributesManager.getAttributeIds(), attributesManager.getColorPalette(), _runtime->getLogger()); + Valdi::AttributesBindingContextImpl bindingContext(attributesManager.getAttributeIds(), attributesManager.getColorPaletteManager(), _runtime->getLogger()); SCValdiAttributesBinder *wrapper = [[SCValdiAttributesBinder alloc] initWithNativeAttributesBindingContext:(SCValdiAttributesBinderNative *)&bindingContext fontManager:_fontManager]; diff --git a/valdi/src/valdi/runtime/Attributes/AssetAttributes.cpp b/valdi/src/valdi/runtime/Attributes/AssetAttributes.cpp index ef6ba28d..d62915ed 100644 --- a/valdi/src/valdi/runtime/Attributes/AssetAttributes.cpp +++ b/valdi/src/valdi/runtime/Attributes/AssetAttributes.cpp @@ -46,7 +46,10 @@ class AssetMeasureDelegate : public DefaultMeasureDelegate { return Size(); } - asset = asset->withDirection(isRightToLeft); + asset = asset->withConfiguration(AssetConfiguration(nullptr, std::nullopt, isRightToLeft)); + if (asset == nullptr) { + return Size(); + } double maxWidth = widthMode == MeasureMode::MeasureModeUnspecified ? -1 : static_cast(width); double maxHeight = heightMode == MeasureMode::MeasureModeUnspecified ? -1 : static_cast(height); @@ -89,8 +92,8 @@ class AssetSrcAttributeHandlerDelegate : public AttributeHandlerDelegate { if (!result) { return result.moveError(); } - asset = result.value()->withPlatform(viewNode.getPlatformType()); - asset = asset->withDirection(viewNode.isRightToLeft()); + asset = result.value()->withConfiguration(AssetConfiguration( + viewNode.getResolvedColorPalette(), viewNode.getPlatformType(), viewNode.isRightToLeft())); } setAsset(viewTransactionScope, viewNode, view, asset, callback, associatedData, flipOnRtl); @@ -233,8 +236,12 @@ void AssetAttributes::bind(AttributeHandlerById& attributes, Ref(_assetOutputType), true); + auto& srcOnLoadHandler = attributes[_attributeIds.getIdForName(srcOnLoad)]; + srcOnLoadHandler.setShouldReevaluateOnColorPaletteChange(true); + auto& srcHandler = attributes[srcAttributeId]; srcHandler.appendPostprocessor(postprocessAsset); + srcHandler.setShouldReevaluateOnColorPaletteChange(true); if (_assetOutputType != snap::valdi_core::AssetOutputType::Lottie) { auto& filterHandler = attributes[filterAttributeId]; diff --git a/valdi/src/valdi/runtime/Attributes/AttributeHandler.cpp b/valdi/src/valdi/runtime/Attributes/AttributeHandler.cpp index 4ec24f26..06bf1070 100644 --- a/valdi/src/valdi/runtime/Attributes/AttributeHandler.cpp +++ b/valdi/src/valdi/runtime/Attributes/AttributeHandler.cpp @@ -221,6 +221,7 @@ AttributeHandler AttributeHandler::withDelegate(const Ref preprocessString(const Value& value) { return ValueConverter::toString(value).map(); } +static Result postprocessColor(ViewNode& viewNode, const Value& value) { + if (!value.isString()) { + return value; + } + + const auto& colorPalette = viewNode.getResolvedColorPalette(); + if (colorPalette == nullptr) { + return Error("ViewNode has no resolved ColorPalette"); + } + return ValueConverter::toColor(*colorPalette, value); +} + AttributesBindingContextImpl::AttributesBindingContextImpl(AttributeIds& attributeIds, - const Ref& colorPalette, + const Ref& colorPaletteManager, ILogger& logger) - : _attributeIds(attributeIds), _colorPalette(colorPalette), _logger(logger) {} + : _attributeIds(attributeIds), _colorPaletteManager(colorPaletteManager), _logger(logger) {} AttributesBindingContextImpl::~AttributesBindingContextImpl() = default; @@ -113,17 +125,19 @@ AttributeId AttributesBindingContextImpl::bindTextAttribute(const StringBox& att bool invalidateLayoutOnChange, const Ref& delegate) { auto& registeredHandler = registerHandler(attribute, invalidateLayoutOnChange, delegate); - registeredHandler.appendPreprocessor( - [colorPalette = _colorPalette, logger = &_logger](const Value& value) -> Result { - if (value.isString()) { - return value; - } - // strict parsing for non production build - auto strict = !snap::kIsAppstoreBuild; - return TextAttributeValueParser::parse(*colorPalette, value, *logger, strict); - }, - false); - registeredHandler.setEnablePreprocessorCache(true); + registeredHandler.appendPostprocessor([logger = &_logger](ViewNode& viewNode, const Value& value) -> Result { + if (value.isString()) { + return value; + } + // strict parsing for non production build + auto strict = !snap::kIsAppstoreBuild; + const auto& colorPalette = viewNode.getResolvedColorPalette(); + if (colorPalette == nullptr) { + return Error("ViewNode has no resolved ColorPalette"); + } + return TextAttributeValueParser::parse(*colorPalette, value, *logger, strict); + }); + registeredHandler.setShouldReevaluateOnColorPaletteChange(true); return registeredHandler.getId(); } @@ -261,16 +275,8 @@ const AttributeHandlerById& AttributesBindingContextImpl::getHandlers() const { } void AttributesBindingContextImpl::registerColorPreprocessor(AttributeHandler& handler) { - handler.appendPreprocessor( - [colorPalette = _colorPalette](const Value& value) -> Result { - auto color = ValueConverter::toColor(*colorPalette, value); - if (!color) { - return color.moveError(); - } - - return Value(color.value().value); - }, - false); + handler.appendPreprocessor(&ValueConverter::toColorValue, false); + handler.appendPostprocessor(&postprocessColor); handler.setShouldReevaluateOnColorPaletteChange(true); } diff --git a/valdi/src/valdi/runtime/Attributes/AttributesBindingContextImpl.hpp b/valdi/src/valdi/runtime/Attributes/AttributesBindingContextImpl.hpp index 4cb4c38c..78a5d347 100644 --- a/valdi/src/valdi/runtime/Attributes/AttributesBindingContextImpl.hpp +++ b/valdi/src/valdi/runtime/Attributes/AttributesBindingContextImpl.hpp @@ -21,7 +21,9 @@ class ILogger; class AttributesBindingContextImpl : public AttributesBindingContext { public: - AttributesBindingContextImpl(AttributeIds& attributeIds, const Ref& colorPalette, ILogger& logger); + AttributesBindingContextImpl(AttributeIds& attributeIds, + const Ref& colorPaletteManager, + ILogger& logger); ~AttributesBindingContextImpl() override; void registerPreprocessor(const Valdi::StringBox& attribute, @@ -83,7 +85,7 @@ class AttributesBindingContextImpl : public AttributesBindingContext { private: AttributeIds& _attributeIds; - Ref _colorPalette; + Ref _colorPaletteManager; ILogger& _logger; AttributeHandlerById _handlers; Ref _defaultDelegate; diff --git a/valdi/src/valdi/runtime/Attributes/AttributesManager.cpp b/valdi/src/valdi/runtime/Attributes/AttributesManager.cpp index cd0f57ee..d972e757 100644 --- a/valdi/src/valdi/runtime/Attributes/AttributesManager.cpp +++ b/valdi/src/valdi/runtime/Attributes/AttributesManager.cpp @@ -20,21 +20,22 @@ namespace Valdi { AttributesManager::AttributesManager(IViewManager& viewManager, AttributeIds& attributeIds, - const Ref& colorPalette, + const Ref& colorPaletteManager, ILogger& logger, std::shared_ptr yogaConfig) : _viewManager(viewManager), _attributeIds(attributeIds), _logger(logger), _yogaConfig(std::move(yogaConfig)), - _colorPalette(colorPalette) {} + _colorPaletteManager(colorPaletteManager) {} void AttributesManager::registerPreprocessor(AttributeId attributeId, - Result (*preprocessor)(const Ref&, const Value&)) { - registerPreprocessor(attributeId, - [colorPalette = _colorPalette, preprocessor](const Value& value) -> Result { - return preprocessor(colorPalette, value); - }); + Result (*preprocessor)(const Ref&, + const Value&)) { + registerPreprocessor( + attributeId, [colorPaletteManager = _colorPaletteManager, preprocessor](const Value& value) -> Result { + return preprocessor(colorPaletteManager, value); + }); } void AttributesManager::registerPreprocessor(AttributeId attributeId, @@ -88,7 +89,6 @@ SharedBoundAttributes AttributesManager::lockFreeGetAttributesForClass(const Str scrollAttributesBound); _boundAttributesByClass[className] = boundAttributes; - return boundAttributes; } @@ -129,7 +129,7 @@ void AttributesManager::populateAttributeHandlerById(AttributeHandlerById& attri } if (!isLayoutClass) { - AttributesBindingContextImpl binder(_attributeIds, _colorPalette, _logger); + AttributesBindingContextImpl binder(_attributeIds, _colorPaletteManager, _logger); _viewManager.bindAttributes(className, binder); if (binder.getDefaultDelegate() != nullptr) { @@ -196,8 +196,8 @@ AttributeIds& AttributesManager::getAttributeIds() const { return _attributeIds; } -const Ref& AttributesManager::getColorPalette() const { - return _colorPalette; +const Ref& AttributesManager::getColorPaletteManager() const { + return _colorPaletteManager; } const StringBox& AttributesManager::getLayoutPlaceholderClassName() { diff --git a/valdi/src/valdi/runtime/Attributes/AttributesManager.hpp b/valdi/src/valdi/runtime/Attributes/AttributesManager.hpp index 8b61f6d3..d1b92585 100644 --- a/valdi/src/valdi/runtime/Attributes/AttributesManager.hpp +++ b/valdi/src/valdi/runtime/Attributes/AttributesManager.hpp @@ -30,25 +30,24 @@ class AttributesManager { public: AttributesManager(IViewManager& viewManager, AttributeIds& attributeIds, - const Ref& colorPalette, + const Ref& colorPaletteManager, ILogger& logger, std::shared_ptr yogaConfig); void registerPreprocessor(AttributeId attributeId, const AttributePreprocessor& preprocessor); void registerPreprocessor(AttributeId attributeId, - Result (*preprocessor)(const Ref&, const Value&)); + Result (*preprocessor)(const Ref&, const Value&)); void registerPostprocessor(AttributeId attributeId, Result (*postprocessor)(ViewNode& viewNode, const Value& value)); SharedBoundAttributes getAttributesForClass(const StringBox& className) noexcept; FlatMap getAllBoundAttributes() const; - IViewManager& getViewManager() const; ILogger& getLogger() const; YGConfig* getYogaConfig() const; AttributeIds& getAttributeIds() const; - const Ref& getColorPalette() const; + const Ref& getColorPaletteManager() const; static const StringBox& getLayoutPlaceholderClassName(); static const StringBox& getDeferredClassName(); @@ -61,7 +60,7 @@ class AttributesManager { FlatMap _boundAttributesByClass; FlatMap> _preprocessors; FlatMap> _postprocessors; - Ref _colorPalette; + Ref _colorPaletteManager; mutable std::mutex _mutex; SharedBoundAttributes lockFreeGetAttributesForClass(const StringBox& className) noexcept; diff --git a/valdi/src/valdi/runtime/Attributes/DefaultAttributeProcessors.cpp b/valdi/src/valdi/runtime/Attributes/DefaultAttributeProcessors.cpp index ff1827e6..ec22f05e 100644 --- a/valdi/src/valdi/runtime/Attributes/DefaultAttributeProcessors.cpp +++ b/valdi/src/valdi/runtime/Attributes/DefaultAttributeProcessors.cpp @@ -15,6 +15,7 @@ #include "valdi/runtime/Context/ViewNode.hpp" #include +#include namespace Valdi { @@ -23,7 +24,7 @@ static Error parseError(AttributeParser& parser, std::string_view name) { return parser.getError(); } -Result preprocessBorder(const Ref& colorPalette, const Value& in) { +Result preprocessBorder(const Value& in) { auto stringBox = in.toStringBox(); AttributeParser parser(stringBox.toStringView()); @@ -44,7 +45,7 @@ Result preprocessBorder(const Ref& colorPalette, const Valu return parseError(parser, "border style"); } - auto color = parser.parseColor(*colorPalette); + auto color = parser.parseColorValue(); if (!color) { return parseError(parser, "border color"); } @@ -55,7 +56,7 @@ Result preprocessBorder(const Ref& colorPalette, const Valu return parser.getError(); } - border = ValueArray::make({Value(borderWidth.value().value), Value(color.value().value)}); + border = ValueArray::make({Value(borderWidth.value().value), color.value()}); } else { border = ValueArray::make({Value(borderWidth.value().value)}); @@ -64,7 +65,7 @@ Result preprocessBorder(const Ref& colorPalette, const Valu return Value(border); } -Result preprocessBoxShadow(const Ref& colorPalette, const Value& in) { +Result preprocessBoxShadow(const Value& in) { auto stringBox = in.toStringBox(); static StringBox none = STRING_LITERAL("none"); if (stringBox == none) { @@ -90,7 +91,7 @@ Result preprocessBoxShadow(const Ref& colorPalette, const V return parseError(parser, "boxShadow blur"); } - auto color = parser.parseColor(*colorPalette); + auto color = parser.parseColorValue(); if (!color) { return parseError(parser, "boxShadow color"); } @@ -102,12 +103,12 @@ Result preprocessBoxShadow(const Ref& colorPalette, const V Value(hOffset.value().value), Value(vOffset.value().value), Value(blur.value().value), - Value(color.value().value)}); + color.value()}); return Value(boxShadow); } -Result preprocessTextShadow(const Ref& colorPalette, const Value& in) { +Result preprocessTextShadow(const Value& in) { auto stringBox = in.toStringBox(); static StringBox none = STRING_LITERAL("none"); if (stringBox == none) { @@ -116,7 +117,7 @@ Result preprocessTextShadow(const Ref& colorPalette, const AttributeParser parser(stringBox.toStringView()); - auto color = parser.parseColor(*colorPalette); + auto color = parser.parseColorValue(); if (!color) { return parseError(parser, "Failed to parse text shadow color: "); } @@ -142,11 +143,8 @@ Result preprocessTextShadow(const Ref& colorPalette, const return parser.getError(); } - const auto textShadow = ValueArray::make({Value(color.value().value), - Value(radius.value()), - Value(opacity.value()), - Value(hOffset.value()), - Value(vOffset.value())}); + const auto textShadow = ValueArray::make( + {color.value(), Value(radius.value()), Value(opacity.value()), Value(hOffset.value()), Value(vOffset.value())}); return Value(textShadow); } @@ -173,7 +171,7 @@ static LinearGradientAngle angleRadToAngleEnum(double angleRad) { return static_cast(angleEnum); } -Result preprocessGradient(const Ref& colorPalette, const Value& in) { +Result preprocessGradient(const Value& in) { auto stringBox = in.toStringBox(); Ref colorArray; @@ -211,12 +209,12 @@ Result preprocessGradient(const Ref& colorPalette, const Va if (shouldParseColorComponents) { while (!parser.isAtEnd()) { parser.tryParseWhitespaces(); - auto color = parser.parseColor(*colorPalette); + auto color = parser.parseColorValue(); if (!color) { return parseError(parser, "gradient color"); } - colors.emplace(color.value().value); + colors.emplace(color.value()); parser.tryParseWhitespaces(); @@ -254,12 +252,12 @@ Result preprocessGradient(const Ref& colorPalette, const Va } else { parser.tryParseWhitespaces(); - auto singleColor = parser.parseColor(*colorPalette); + auto singleColor = parser.parseColorValue(); if (!singleColor) { return parser.getError(); } - colorArray = ValueArray::make({Value(singleColor.value().value)}); + colorArray = ValueArray::make({singleColor.value()}); locationArray = ValueArray::make(0); } @@ -274,7 +272,7 @@ Result preprocessGradient(const Ref& colorPalette, const Va return Value(gradient); } -Result preprocessBorderRadius(const Ref& /*colorPalette*/, const Value& in) { +Result preprocessBorderRadius(const Value& in) { auto borderRadius = ValueConverter::toBorderValues(in); if (!borderRadius) { return borderRadius.moveError(); @@ -283,8 +281,97 @@ Result preprocessBorderRadius(const Ref& /*colorPalette*/, return Value(borderRadius.value()); } +static Result resolveColorValue(const ColorPalette& colorPalette, const Value& in) { + return ValueConverter::toColor(colorPalette, in); +} + +static Result resolveColorValue(ViewNode& viewNode, const Value& in) { + if (!in.isString()) { + return in; + } + + const auto& colorPalette = viewNode.getResolvedColorPalette(); + if (colorPalette == nullptr) { + return Error("ViewNode has no resolved ColorPalette"); + } + return resolveColorValue(*colorPalette, in); +} + +static Result resolveColorAtIndex(ViewNode& viewNode, const Value& in, size_t colorIndex) { + if (!in.isArray()) { + return in; + } + + const auto* array = in.getArray(); + if (array->size() <= colorIndex || (*array)[colorIndex].isUndefined()) { + return in; + } + + auto resolvedColor = resolveColorValue(viewNode, (*array)[colorIndex]); + if (!resolvedColor) { + return resolvedColor.moveError(); + } + + auto out = array->clone(); + out->emplace(colorIndex, resolvedColor.moveValue()); + return Value(out); +} + +static Result> resolveColorAtIndexInArray(ViewNode& viewNode, const Value& in, size_t colorIndex) { + const auto* array = in.getArray(); + if (array == nullptr) { + return Error("Invalid array value"); + } + + auto out = array->clone(); + if (out->size() > colorIndex && !(*out)[colorIndex].isUndefined()) { + auto resolvedColor = resolveColorValue(viewNode, (*out)[colorIndex]); + if (!resolvedColor) { + return resolvedColor.moveError(); + } + out->emplace(colorIndex, resolvedColor.moveValue()); + } + + return out; +} + +Result postprocessBorder(ViewNode& viewNode, const Value& in) { + constexpr size_t kBorderColorIndex = 1; + return resolveColorAtIndex(viewNode, in, kBorderColorIndex); +} + +static Result postprocessBoxShadow(bool isRightToLeft, Ref boxShadow) { + if (boxShadow->size() != 5) { + return Error("Invalid boxShadow value"); + } + + if (!isRightToLeft) { + return Value(boxShadow); + } + + constexpr size_t kHOffsetIndex = 1; + + auto hOffset = (*boxShadow)[kHOffsetIndex].toDouble(); + if (hOffset != 0.0) { + boxShadow->emplace(kHOffsetIndex, Value(hOffset * -1)); + } + + return Value(boxShadow); +} + Result postprocessBoxShadow(ViewNode& viewNode, const Value& in) { - return postprocessBoxShadow(viewNode.isRightToLeft(), in); + constexpr size_t kBoxShadowColorIndex = 4; + auto resolvedBoxShadow = resolveColorAtIndexInArray(viewNode, in, kBoxShadowColorIndex); + if (!resolvedBoxShadow) { + return resolvedBoxShadow.moveError(); + } + + return postprocessBoxShadow(viewNode.isRightToLeft(), resolvedBoxShadow.moveValue()); +} + +Result postprocessTextShadow(ViewNode& viewNode, const Value& in) { + constexpr size_t kTextShadowColorIndex = 0; + return resolveColorAtIndex(viewNode, in, kTextShadowColorIndex); } Result postprocessBoxShadow(bool isRightToLeft, const Value& in) { @@ -315,14 +402,106 @@ Result postprocessBoxShadow(bool isRightToLeft, const Value& in) { constexpr size_t kAngleIndex = 2; -Result makeBackgroundWithAngle(const ValueArray* background, LinearGradientAngle angle) { - auto newBackground = background->clone(); - newBackground->emplace(kAngleIndex, Value(angle)); - return Value(newBackground); +static std::optional flippedGradientAngle(LinearGradientAngle angle) { + switch (angle) { + case LinearGradientAngleTopBottom: + case LinearGradientAngleBottomTop: + return std::nullopt; + case LinearGradientAngleTopRightBottomLeft: + return LinearGradientAngleTopLeftBottomRight; + case LinearGradientAngleRightLeft: + return LinearGradientAngleLeftRight; + case LinearGradientAngleBottomRightTopLeft: + return LinearGradientAngleBottomLeftTopRight; + case LinearGradientAngleBottomLeftTopRight: + return LinearGradientAngleBottomRightTopLeft; + case LinearGradientAngleLeftRight: + return LinearGradientAngleRightLeft; + case LinearGradientAngleTopLeftBottomRight: + return LinearGradientAngleTopRightBottomLeft; + } + + return std::nullopt; +} + +static Result postprocessGradient(bool isRightToLeft, Ref background) { + if (background->size() != 4) { + return Error("Invalid background value"); + } + + if (!isRightToLeft) { + return Value(background); + } + + auto angle = static_cast((*background)[kAngleIndex].toInt()); + auto flippedAngle = flippedGradientAngle(angle); + if (flippedAngle) { + background->emplace(kAngleIndex, Value(flippedAngle.value())); + } + + return Value(background); } Result postprocessGradient(ViewNode& viewNode, const Value& in) { - return postprocessGradient(viewNode.isRightToLeft(), in); + if (!in.isArray()) { + return in; + } + + const auto* background = in.getArray(); + if (background->size() != 4) { + return Error("Invalid background value"); + } + + constexpr size_t kColorsIndex = 0; + const auto* colors = (*background)[kColorsIndex].getArray(); + if (colors == nullptr) { + return Error("Invalid background colors value"); + } + + auto resolvedColors = colors->clone(); + for (size_t i = 0; i < colors->size(); ++i) { + auto resolvedColor = resolveColorValue(viewNode, (*colors)[i]); + if (!resolvedColor) { + return resolvedColor.moveError(); + } + resolvedColors->emplace(i, resolvedColor.moveValue()); + } + + auto resolvedBackground = background->clone(); + resolvedBackground->emplace(kColorsIndex, Value(resolvedColors)); + + return postprocessGradient(viewNode.isRightToLeft(), std::move(resolvedBackground)); +} + +Result postprocessGradient(bool isRightToLeft, const ColorPalette& colorPalette, const Value& in) { + if (!in.isArray()) { + return in; + } + + const auto* background = in.getArray(); + if (background->size() != 4) { + return Error("Invalid background value"); + } + + constexpr size_t kColorsIndex = 0; + const auto* colors = (*background)[kColorsIndex].getArray(); + if (colors == nullptr) { + return Error("Invalid background colors value"); + } + + auto resolvedColors = colors->clone(); + for (size_t i = 0; i < colors->size(); ++i) { + auto resolvedColor = resolveColorValue(colorPalette, (*colors)[i]); + if (!resolvedColor) { + return resolvedColor.moveError(); + } + resolvedColors->emplace(i, resolvedColor.moveValue()); + } + + auto resolvedBackground = background->clone(); + resolvedBackground->emplace(kColorsIndex, Value(resolvedColors)); + + return postprocessGradient(isRightToLeft, std::move(resolvedBackground)); } Result postprocessGradient(bool isRightToLeft, const Value& in) { @@ -336,27 +515,14 @@ Result postprocessGradient(bool isRightToLeft, const Value& in) { } auto angle = static_cast((*background)[kAngleIndex].toInt()); - - switch (angle) { - case LinearGradientAngleTopBottom: - return in; - case LinearGradientAngleTopRightBottomLeft: - return makeBackgroundWithAngle(background, LinearGradientAngleTopLeftBottomRight); - case LinearGradientAngleRightLeft: - return makeBackgroundWithAngle(background, LinearGradientAngleLeftRight); - case LinearGradientAngleBottomRightTopLeft: - return makeBackgroundWithAngle(background, LinearGradientAngleBottomLeftTopRight); - case LinearGradientAngleBottomTop: - return in; - case LinearGradientAngleBottomLeftTopRight: - return makeBackgroundWithAngle(background, LinearGradientAngleBottomRightTopLeft); - case LinearGradientAngleLeftRight: - return makeBackgroundWithAngle(background, LinearGradientAngleRightLeft); - case LinearGradientAngleTopLeftBottomRight: - return makeBackgroundWithAngle(background, LinearGradientAngleTopRightBottomLeft); + auto flippedAngle = flippedGradientAngle(angle); + if (!flippedAngle) { + return in; } - return in; + auto flippedBackground = background->clone(); + flippedBackground->emplace(kAngleIndex, Value(flippedAngle.value())); + return Value(flippedBackground); } Result postprocessBorderRadius(ViewNode& viewNode, const Value& in) { @@ -403,7 +569,9 @@ void registerDefaultProcessors(AttributesManager& attributesManager) { attributesManager.registerPreprocessor(textGradientAttributeId, &preprocessGradient); attributesManager.registerPreprocessor(maskImageAttributeId, &preprocessGradient); + attributesManager.registerPostprocessor(borderAttributeId, &postprocessBorder); attributesManager.registerPostprocessor(boxShadowAttributeId, &postprocessBoxShadow); + attributesManager.registerPostprocessor(textShadowAttributeId, &postprocessTextShadow); attributesManager.registerPostprocessor(backgroundAttributeId, &postprocessGradient); attributesManager.registerPostprocessor(borderRadiusAttributeId, &postprocessBorderRadius); attributesManager.registerPostprocessor(textGradientAttributeId, &postprocessGradient); diff --git a/valdi/src/valdi/runtime/Attributes/DefaultAttributeProcessors.hpp b/valdi/src/valdi/runtime/Attributes/DefaultAttributeProcessors.hpp index 819c55eb..db66fe06 100644 --- a/valdi/src/valdi/runtime/Attributes/DefaultAttributeProcessors.hpp +++ b/valdi/src/valdi/runtime/Attributes/DefaultAttributeProcessors.hpp @@ -12,15 +12,23 @@ namespace Valdi { -Result preprocessBorder(const Ref& colorPalette, const Value& in); -Result preprocessBoxShadow(const Ref& colorPalette, const Value& in); -Result preprocessTextShadow(const Ref& colorPalette, const Value& in); -Result preprocessBorderRadius(const Ref& /*colorPalette*/, const Value& in); -Result preprocessGradient(const Ref& colorPalette, const Value& in); +class ColorPalette; +Result preprocessBorder(const Value& in); +Result preprocessBoxShadow(const Value& in); +Result preprocessTextShadow(const Value& in); +Result preprocessBorderRadius(const Value& in); +Result preprocessGradient(const Value& in); + +Result postprocessBorder(ViewNode& viewNode, const Value& in); +Result postprocessBoxShadow(ViewNode& viewNode, const Value& in); +Result postprocessTextShadow(ViewNode& viewNode, const Value& in); +Result postprocessGradient(ViewNode& viewNode, const Value& in); +Result postprocessBorderRadius(ViewNode& viewNode, const Value& in); Result postprocessBoxShadow(bool isRightToLeft, const Value& in); Result postprocessBorderRadius(bool isRightToLeft, const Value& in); Result postprocessGradient(bool isRightToLeft, const Value& in); +Result postprocessGradient(bool isRightToLeft, const ColorPalette& colorPalette, const Value& in); void registerDefaultProcessors(AttributesManager& attributesManager); diff --git a/valdi/src/valdi/runtime/Attributes/DefaultAttributes.cpp b/valdi/src/valdi/runtime/Attributes/DefaultAttributes.cpp index 1c7ad13c..6c5971ef 100644 --- a/valdi/src/valdi/runtime/Attributes/DefaultAttributes.cpp +++ b/valdi/src/valdi/runtime/Attributes/DefaultAttributes.cpp @@ -46,6 +46,7 @@ void DefaultAttributes::bind(AttributeHandlerById& attributes) { binder.bindViewNodeFloat("estimatedWidth", &ViewNode::setEstimatedWidth); binder.bindViewNodeFloat("estimatedHeight", &ViewNode::setEstimatedHeight); + binder.bindViewNodeString("colorPaletteName", &ViewNode::setColorPaletteName); binder.bind( "limitToViewport", diff --git a/valdi/src/valdi/runtime/Attributes/ValueConverters.cpp b/valdi/src/valdi/runtime/Attributes/ValueConverters.cpp index 7cf62db1..a7636079 100644 --- a/valdi/src/valdi/runtime/Attributes/ValueConverters.cpp +++ b/valdi/src/valdi/runtime/Attributes/ValueConverters.cpp @@ -64,13 +64,26 @@ std::optional ensureParserAtEnd(AttributeParser& parser, std::optional&& r return std::move(result); } -Result ValueConverter::toColor(const ColorPalette& colorPalette, const Value& value) { +Result ValueConverter::toColor(const ColorPalette& colorPalette, const Value& value) { + if (value.isString()) { + auto colorName = value.toStringBox(); + auto color = colorPalette.getColorForName(colorName); + if (!color) { + return Error(STRING_FORMAT("Invalid color name '{}'", colorName)); + } + return Value(color.value().value); + } + + return value; +} + +Result ValueConverter::toColorValue(const Value& value) { if (value.isNumber()) { - return Color(value.toInt()); + return Value(value.toLong()); } else if (value.isString()) { auto strBox = value.toStringBox(); AttributeParser parser(strBox.toStringView()); - auto color = ensureParserAtEnd(parser, parser.parseColor(colorPalette)); + auto color = ensureParserAtEnd(parser, parser.parseColorValue()); if (!color) { return parser.getError(); } diff --git a/valdi/src/valdi/runtime/Attributes/ValueConverters.hpp b/valdi/src/valdi/runtime/Attributes/ValueConverters.hpp index e053a0a1..518dcc1e 100644 --- a/valdi/src/valdi/runtime/Attributes/ValueConverters.hpp +++ b/valdi/src/valdi/runtime/Attributes/ValueConverters.hpp @@ -24,7 +24,9 @@ struct ValueConverter { [[nodiscard]] static Result toInt(const Value& value); - [[nodiscard]] static Result toColor(const ColorPalette& colorPalette, const Value& value); + [[nodiscard]] static Result toColor(const ColorPalette& colorPalette, const Value& value); + + [[nodiscard]] static Result toColorValue(const Value& value); [[nodiscard]] static Result toDouble(const Value& value); diff --git a/valdi/src/valdi/runtime/Attributes/ViewNodeAttribute.cpp b/valdi/src/valdi/runtime/Attributes/ViewNodeAttribute.cpp index b7506d71..dc8fd09d 100644 --- a/valdi/src/valdi/runtime/Attributes/ViewNodeAttribute.cpp +++ b/valdi/src/valdi/runtime/Attributes/ViewNodeAttribute.cpp @@ -21,7 +21,8 @@ ViewNodeAttribute::ViewNodeAttribute(const AttributeHandler* handler) : _handler(handler), _handlerNeedsView(handler->requiresView()), _handlerCanAffectLayout(handler->shouldInvalidateLayoutOnChange()), - _handlerIsCompositePart(handler->isCompositePart()) {} + _handlerIsCompositePart(handler->isCompositePart()), + _handlerShouldReevaluateOnColorChange(handler->shouldReevaluateOnColorPaletteChange()) {} ViewNodeAttribute::~ViewNodeAttribute() { if (_hasSingleAttribute) { @@ -327,6 +328,10 @@ bool ViewNodeAttribute::isCompositePart() const { return _handlerIsCompositePart; } +bool ViewNodeAttribute::shouldReevaluateOnColorChange() const { + return _handlerShouldReevaluateOnColorChange; +} + const CompositeAttribute* ViewNodeAttribute::getCompositeAttribute() const { return _handler->getCompositeAttribute().get(); } @@ -432,6 +437,10 @@ void ViewNodeAttribute::markDirty() { } } +void ViewNodeAttribute::markAppliedValueDirty() { + _appliedValueDirty = true; +} + Ref ViewNodeAttribute::copy() { auto copy = makeShared(_handler); @@ -452,6 +461,7 @@ void ViewNodeAttribute::setHandler(const AttributeHandler* handler) { _handlerNeedsView = handler->requiresView(); _handlerCanAffectLayout = handler->shouldInvalidateLayoutOnChange(); _handlerIsCompositePart = handler->isCompositePart(); + _handlerShouldReevaluateOnColorChange = handler->shouldReevaluateOnColorPaletteChange(); } } // namespace Valdi diff --git a/valdi/src/valdi/runtime/Attributes/ViewNodeAttribute.hpp b/valdi/src/valdi/runtime/Attributes/ViewNodeAttribute.hpp index 9855cbcb..00a14ece 100644 --- a/valdi/src/valdi/runtime/Attributes/ViewNodeAttribute.hpp +++ b/valdi/src/valdi/runtime/Attributes/ViewNodeAttribute.hpp @@ -41,6 +41,7 @@ class ViewNodeAttribute : public SimpleRefCountable { const Ref& animator); void markDirty(); + void markAppliedValueDirty(); /** Prepare this attribute for an animation. @@ -85,6 +86,7 @@ class ViewNodeAttribute : public SimpleRefCountable { Returns whether this represents a composite attribute part. */ bool isCompositePart() const; + bool shouldReevaluateOnColorChange() const; /** Whether this attribute can affect the layout calculation. @@ -137,6 +139,7 @@ class ViewNodeAttribute : public SimpleRefCountable { bool _handlerNeedsView = false; bool _handlerCanAffectLayout = false; bool _handlerIsCompositePart = false; + bool _handlerShouldReevaluateOnColorChange = false; bool _appliedValueDirty = false; bool _hasAppliedValue = false; diff --git a/valdi/src/valdi/runtime/Attributes/ViewNodeAttributesApplier.cpp b/valdi/src/valdi/runtime/Attributes/ViewNodeAttributesApplier.cpp index 047351c6..f34ce5cd 100644 --- a/valdi/src/valdi/runtime/Attributes/ViewNodeAttributesApplier.cpp +++ b/valdi/src/valdi/runtime/Attributes/ViewNodeAttributesApplier.cpp @@ -107,6 +107,10 @@ void ViewNodeAttributesApplier::reapplyAttribute(ViewTransactionScope& viewTrans } } +void ViewNodeAttributesApplier::invalidateColorAttributes() { + _colorAttributesInvalidated = true; +} + bool ViewNodeAttributesApplier::removeAllAttributesForOwner(ViewTransactionScope& viewTransactionScope, const AttributeOwner* owner, const Ref& animator) { @@ -265,6 +269,11 @@ void ViewNodeAttributesApplier::flush(ViewTransactionScope& viewTransactionScope return; } + if (_colorAttributesInvalidated) { + _colorAttributesInvalidated = false; + updateInvalidatedColorAttributes(viewTransactionScope); + } + while (!_dirtyCompositeAttributes.empty()) { auto dirtyCompositeAttribute = *_dirtyCompositeAttributes.begin(); _dirtyCompositeAttributes.erase(_dirtyCompositeAttributes.begin()); @@ -274,7 +283,25 @@ void ViewNodeAttributesApplier::flush(ViewTransactionScope& viewTransactionScope } bool ViewNodeAttributesApplier::needsFlush() const { - return !_dirtyCompositeAttributes.empty(); + return _colorAttributesInvalidated || !_dirtyCompositeAttributes.empty(); +} + +void ViewNodeAttributesApplier::updateInvalidatedColorAttributes(ViewTransactionScope& viewTransactionScope) { + for (const auto& it : _attributes) { + if (!it.second->shouldReevaluateOnColorChange()) { + continue; + } + + auto id = it.first; + auto attribute = it.second; + attribute->markAppliedValueDirty(); + + if (attribute->isCompositePart()) { + _dirtyCompositeAttributes[attribute->getCompositeAttribute()->getAttributeId()] = nullptr; + } else { + updateAttribute(viewTransactionScope, id, *attribute, nullptr, /* justAddedView */ false); + } + } } void ViewNodeAttributesApplier::updateCompositeAttribute(ViewTransactionScope& viewTransactionScope, @@ -453,6 +480,7 @@ void ViewNodeAttributesApplier::setBoundAttributes(Ref boundAtt if (_boundAttributes == nullptr) { // Clear state so flush() / emplaceAttribute() are never called with null _boundAttributes. _dirtyCompositeAttributes.clear(); + _colorAttributesInvalidated = false; _attributes.clear(); } else if (VALDI_UNLIKELY(hadAttributes)) { updateAttributeHandlers(); @@ -493,6 +521,7 @@ void ViewNodeAttributesApplier::updateAttributeHandlers() { void ViewNodeAttributesApplier::destroy() { _viewNode = nullptr; _dirtyCompositeAttributes.clear(); + _colorAttributesInvalidated = false; _attributes.clear(); } diff --git a/valdi/src/valdi/runtime/Attributes/ViewNodeAttributesApplier.hpp b/valdi/src/valdi/runtime/Attributes/ViewNodeAttributesApplier.hpp index e5253e28..bef2f707 100644 --- a/valdi/src/valdi/runtime/Attributes/ViewNodeAttributesApplier.hpp +++ b/valdi/src/valdi/runtime/Attributes/ViewNodeAttributesApplier.hpp @@ -53,6 +53,7 @@ class ViewNodeAttributesApplier : public AttributesApplier, public AttributeOwne const Ref& animator) override; void reapplyAttribute(ViewTransactionScope& viewTransactionScope, AttributeId id) override; + void invalidateColorAttributes(); void flush(ViewTransactionScope& viewTransactionScope) override; bool needsFlush() const override; @@ -101,6 +102,7 @@ class ViewNodeAttributesApplier : public AttributesApplier, public AttributeOwne FlatMap> _dirtyCompositeAttributes; bool _hasView = false; + bool _colorAttributesInvalidated = false; void updateCompositeAttribute(ViewTransactionScope& viewTransactionScope, AttributeId compositeId, @@ -129,6 +131,7 @@ class ViewNodeAttributesApplier : public AttributesApplier, public AttributeOwne StringBox getAttributeName(AttributeId id) const; void updateAttributeHandlers(); + void updateInvalidatedColorAttributes(ViewTransactionScope& viewTransactionScope); const AttributeHandler* getAttributeHandler(AttributeId id) const; }; diff --git a/valdi/src/valdi/runtime/Context/ViewManagerContext.cpp b/valdi/src/valdi/runtime/Context/ViewManagerContext.cpp index abb5d61a..550a6be7 100644 --- a/valdi/src/valdi/runtime/Context/ViewManagerContext.cpp +++ b/valdi/src/valdi/runtime/Context/ViewManagerContext.cpp @@ -15,13 +15,13 @@ namespace Valdi { ViewManagerContext::ViewManagerContext(IViewManager& viewManager, AttributeIds& attributeIds, - const Ref& colorPalette, + const Ref& colorPaletteManager, const Shared& yogaConfig, bool enablePreloading, const Ref& mainThreadManager, ILogger& logger) : _viewManager(viewManager), - _attributesManager(viewManager, attributeIds, colorPalette, logger, yogaConfig), + _attributesManager(viewManager, attributeIds, colorPaletteManager, logger, yogaConfig), _mainThreadManager(mainThreadManager) { Valdi::registerDefaultProcessors(_attributesManager); diff --git a/valdi/src/valdi/runtime/Context/ViewManagerContext.hpp b/valdi/src/valdi/runtime/Context/ViewManagerContext.hpp index 37eef1a7..1f4dd360 100644 --- a/valdi/src/valdi/runtime/Context/ViewManagerContext.hpp +++ b/valdi/src/valdi/runtime/Context/ViewManagerContext.hpp @@ -20,7 +20,7 @@ class GlobalViewFactories; class DispatchQueue; class ViewPreloader; class MainThreadManager; -class ColorPalette; +class ColorPaletteManager; using ViewPoolsStats = FlatMap; @@ -28,7 +28,7 @@ class ViewManagerContext : public SimpleRefCountable { public: ViewManagerContext(IViewManager& viewManager, AttributeIds& attributeIds, - const Ref& colorPalette, + const Ref& colorPaletteManager, const Shared& yogaConfig, bool enablePreloading, const Ref& mainThreadManager, diff --git a/valdi/src/valdi/runtime/Context/ViewNode.cpp b/valdi/src/valdi/runtime/Context/ViewNode.cpp index 2635e75b..7834698b 100644 --- a/valdi/src/valdi/runtime/Context/ViewNode.cpp +++ b/valdi/src/valdi/runtime/Context/ViewNode.cpp @@ -158,12 +158,17 @@ constexpr size_t kHasChildWithAccessibilityId = 26; constexpr size_t kCanAlwaysScrollHorizontal = 27; constexpr size_t kCanAlwaysScrollVertical = 28; constexpr size_t kAccessibilityTreeNeedsUpdate = 29; +constexpr size_t kHasOveriddenColorPalette = 30; -ViewNode::ViewNode(YGConfig* yogaConfig, AttributeIds& attributeIds, ILogger& logger) +ViewNode::ViewNode(YGConfig* yogaConfig, + AttributeIds& attributeIds, + const Ref& colorPalette, + ILogger& logger) : _yogaNode(yogaConfig != nullptr ? Yoga::createNode(yogaConfig) : nullptr), _attributeIds(attributeIds), _logger(logger), - _attributesApplier(this) { + _attributesApplier(this), + _colorPalette(colorPalette) { if (_yogaNode != nullptr) { setupYogaNode(_yogaNode, this); } @@ -195,6 +200,49 @@ ViewNodeTree* ViewNode::getViewNodeTree() const { return _viewNodeTree; } +void ViewNode::setColorPaletteName(ViewTransactionScope& viewTransactionScope, const StringBox& colorPaletteName) { + if (colorPaletteName.isEmpty() && !hasOveriddenColorPalette()) { + return; + } + + Ref colorPalette; + if (colorPaletteName.isEmpty()) { + colorPalette = getParentResolvedColorPalette(); + setHasOveriddenColorPalette(false); + } else { + SC_ASSERT(_viewNodeTree != nullptr, "Cannot resolve color palette without a ViewNodeTree"); + colorPalette = + _viewNodeTree->getViewManagerContext()->getAttributesManager().getColorPaletteManager()->getColorPalette( + colorPaletteName); + setHasOveriddenColorPalette(true); + } + + if (!setResolvedColorPalette(colorPalette)) { + return; + } + + invalidateColorAttributes(viewTransactionScope, false); + propagateInheritedColorPalette(viewTransactionScope, _colorPalette); +} + +void ViewNode::setInheritedColorPalette(ViewTransactionScope& viewTransactionScope, + const Ref& colorPalette) { + if (hasOveriddenColorPalette()) { + return; + } + + if (!setResolvedColorPalette(colorPalette)) { + return; + } + + invalidateColorAttributes(viewTransactionScope, true); + propagateInheritedColorPalette(viewTransactionScope, colorPalette); +} + +const Ref& ViewNode::getResolvedColorPalette() const { + return _colorPalette; +} + const Ref& ViewNode::getView() const { return _view; } @@ -1535,6 +1583,9 @@ void ViewNode::insertChildAt(ViewTransactionScope& viewTransactionScope, const R if (getChildCount() > kMaxChildrenBeforeIndexing && _childrenIndexer == nullptr) { _childrenIndexer = std::make_unique(this); } + if (_colorPalette != nullptr) { + child->setInheritedColorPalette(viewTransactionScope, _colorPalette); + } setCalculatedViewportHasChildNeedsUpdate(); @@ -1602,7 +1653,7 @@ Size ViewNode::onMeasure(float width, MeasureMode widthMode, float height, Measu Ref ViewNode::makePlaceholderViewNode(ViewTransactionScope& viewTransactionScope, const Ref& placeholderView) { - auto viewNode = Valdi::makeShared(nullptr, _attributeIds, _logger); + auto viewNode = Valdi::makeShared(nullptr, _attributeIds, _colorPalette, _logger); viewNode->setViewNodeTree(_viewNodeTree); viewNode->setViewFactory(viewTransactionScope, _viewFactory); viewNode->_emittingViewNode = strongSmallRef(this); @@ -3025,6 +3076,65 @@ void ViewNode::reapplyAttributesRecursive(ViewTransactionScope& viewTransactionS } } +bool ViewNode::hasOveriddenColorPalette() const { + return _flags[kHasOveriddenColorPalette]; +} + +void ViewNode::setHasOveriddenColorPalette(bool hasOveriddenColorPalette) { + _flags[kHasOveriddenColorPalette] = hasOveriddenColorPalette; +} + +bool ViewNode::setResolvedColorPalette(const Ref& colorPalette) { + if (_colorPalette == colorPalette) { + return false; + } + + _colorPalette = colorPalette; + return true; +} + +Ref ViewNode::getParentResolvedColorPalette() const { + auto parent = getParent(); + if (parent != nullptr) { + return parent->getResolvedColorPalette(); + } + + if (_viewNodeTree == nullptr) { + return nullptr; + } + + const auto& viewManagerContext = _viewNodeTree->getViewManagerContext(); + if (viewManagerContext == nullptr) { + return nullptr; + } + + return viewManagerContext->getAttributesManager().getColorPaletteManager()->getActiveColorPalette(); +} + +void ViewNode::invalidateColorAttributes(ViewTransactionScope& viewTransactionScope, bool shouldApply) { + _attributesApplier.invalidateColorAttributes(); + if (shouldApply && _colorPalette != nullptr) { + _attributesApplier.flush(viewTransactionScope); + } +} + +void ViewNode::propagateInheritedColorPalette(ViewTransactionScope& viewTransactionScope, + const Ref& colorPalette) { + for (auto* child : *this) { + child->setInheritedColorPalette(viewTransactionScope, colorPalette); + } +} + +void ViewNode::onColorPaletteMutated(ViewTransactionScope& viewTransactionScope, const ColorPalette& colorPalette) { + if (_colorPalette != nullptr && _colorPalette.get() == &colorPalette) { + invalidateColorAttributes(viewTransactionScope, true); + } + + for (auto* child : *this) { + child->onColorPaletteMutated(viewTransactionScope, colorPalette); + } +} + void ViewNode::notifyAttributeFailed(AttributeId attributeId, const Error& error) { _attributesApplier.onApplyAttributeFailed(attributeId, error); } diff --git a/valdi/src/valdi/runtime/Context/ViewNode.hpp b/valdi/src/valdi/runtime/Context/ViewNode.hpp index 1423cbae..3bc3ce0f 100644 --- a/valdi/src/valdi/runtime/Context/ViewNode.hpp +++ b/valdi/src/valdi/runtime/Context/ViewNode.hpp @@ -59,6 +59,7 @@ class BoundAttributes; class AttributeOwner; class ViewNodesFrameObserver; class Metrics; +class ColorPalette; class ViewNode; class ViewNodeIterator { @@ -151,7 +152,7 @@ enum SimplifiedScrollDirection { class ViewNode : public SharedPtrRefCountable { public: - ViewNode(YGConfig* yogaConfig, AttributeIds& attributeIds, ILogger& logger); + ViewNode(YGConfig* yogaConfig, AttributeIds& attributeIds, const Ref& colorPalette, ILogger& logger); ~ViewNode() override; @@ -322,6 +323,11 @@ class ViewNode : public SharedPtrRefCountable { void reapplyAttributesRecursive(ViewTransactionScope& viewTransactionScope, const std::vector& attributes, bool invalidateMeasure); + void onColorPaletteMutated(ViewTransactionScope& viewTransactionScope, const ColorPalette& colorPalette); + + void setColorPaletteName(ViewTransactionScope& viewTransactionScope, const StringBox& colorPaletteName); + void setInheritedColorPalette(ViewTransactionScope& viewTransactionScope, const Ref& colorPalette); + const Ref& getResolvedColorPalette() const; void notifyAttributeFailed(AttributeId attributeId, const Error& error); @@ -653,13 +659,14 @@ class ViewNode : public SharedPtrRefCountable { float _stickyCachedParentH = 0.0f; float _stickyCachedChildH = 0.0f; - std::bitset<30> _flags; + std::bitset<31> _flags; ViewNodeTree* _viewNodeTree = nullptr; Ref _view; Ref _viewFactory; Ref _assetHandler; + Ref _colorPalette; Ref _onViewCreatedCallback; Ref _onViewDestroyedCallback; @@ -759,6 +766,14 @@ class ViewNode : public SharedPtrRefCountable { const Ref& resolveAnimator(const Ref& parentAnimator) const; + bool hasOveriddenColorPalette() const; + void setHasOveriddenColorPalette(bool hasOveriddenColorPalette); + bool setResolvedColorPalette(const Ref& colorPalette); + Ref getParentResolvedColorPalette() const; + void invalidateColorAttributes(ViewTransactionScope& viewTransactionScope, bool shouldApply); + void propagateInheritedColorPalette(ViewTransactionScope& viewTransactionScope, + const Ref& colorPalette); + ViewNodeScrollState& getOrCreateScrollState(); ViewNodeAccessibilityState& getOrCreateAccessibilityState(); diff --git a/valdi/src/valdi/runtime/Context/ViewNodeTree.cpp b/valdi/src/valdi/runtime/Context/ViewNodeTree.cpp index 86d087ef..98bcfa63 100644 --- a/valdi/src/valdi/runtime/Context/ViewNodeTree.cpp +++ b/valdi/src/valdi/runtime/Context/ViewNodeTree.cpp @@ -553,6 +553,11 @@ void ViewNodeTree::setRootViewNode(Ref rootViewNode, bool useDefaultVi if (_rootViewNode != nullptr) { _rootViewNode->removeFromParent(viewTransactionScope); + if (_viewManagerContext != nullptr) { + _rootViewNode->setInheritedColorPalette( + viewTransactionScope, + _viewManagerContext->getAttributesManager().getColorPaletteManager()->getActiveColorPalette()); + } if (useDefaultViewFactory) { SC_ASSERT_NOTNULL(_viewManager); _rootViewNode->setViewFactory(viewTransactionScope, diff --git a/valdi/src/valdi/runtime/JavaScript/JavaScriptRuntime.cpp b/valdi/src/valdi/runtime/JavaScript/JavaScriptRuntime.cpp index 0b1acfeb..dc707e1c 100644 --- a/valdi/src/valdi/runtime/JavaScript/JavaScriptRuntime.cpp +++ b/valdi/src/valdi/runtime/JavaScript/JavaScriptRuntime.cpp @@ -28,6 +28,7 @@ #include "valdi/runtime/JavaScript/ValueFunctionWithJSValue.hpp" #include "valdi/runtime/Resources/DirectionalAsset.hpp" #include "valdi/runtime/Resources/PlatformSpecificAsset.hpp" +#include "valdi/runtime/Resources/ThemableAsset.hpp" #include "valdi/runtime/ValdiRuntimeTweaks.hpp" #include "valdi_core/JSRuntimeNativeObjectsManager.hpp" #include "valdi_core/cpp/Constants.hpp" @@ -1813,6 +1814,31 @@ JSValueRef JavaScriptRuntime::runtimeMakePlatformSpecificAsset(JSFunctionNativeC return makeWrappedObject(callContext.getContext(), asset, callContext.getExceptionTracker(), false); } +JSValueRef JavaScriptRuntime::runtimeMakeThemableAsset(JSFunctionNativeCallContext& callContext) { + auto assetsByColorPaletteValue = callContext.getParameterAsValue(0); + CHECK_CALL_CONTEXT(callContext); + + auto assetsByColorPaletteMap = assetsByColorPaletteValue.getMapRef(); + if (assetsByColorPaletteMap == nullptr || assetsByColorPaletteMap->empty()) { + return callContext.throwError(Error("Invalid themable assets object specified")); + } + + FlatMap> assetsByColorPalette; + for (const auto& assetByColorPalette : *assetsByColorPaletteMap) { + auto asset = AssetResolver::resolve(_resourceManager, assetByColorPalette.second); + if (asset == nullptr) { + return callContext.throwError( + Error("Themable assets can only be created from URL or Valdi assets")); + } + + assetsByColorPalette[assetByColorPalette.first] = asset; + } + + auto asset = makeShared(std::move(assetsByColorPalette)); + + return makeWrappedObject(callContext.getContext(), asset, callContext.getExceptionTracker(), false); +} + JSValueRef JavaScriptRuntime::runtimeGetLoadedAssetMetadata(JSFunctionNativeCallContext& callContext) { auto loadedAsset = castOrNull(callContext.getParameterAsWrappedObject(0)); CHECK_CALL_CONTEXT(callContext); @@ -1937,16 +1963,35 @@ JSValueRef JavaScriptRuntime::runtimeGetAssets(JSFunctionNativeCallContext& call return assetsArray; } -JSValueRef JavaScriptRuntime::runtimeSetColorPalette(JSFunctionNativeCallContext& callContext) { +JSValueRef JavaScriptRuntime::runtimeConfigureColorPalette(JSFunctionNativeCallContext& callContext) { + if (!_isWorker) { + auto name = callContext.getParameterAsString(0); + CHECK_CALL_CONTEXT(callContext); + auto colorPaletteMap = callContext.getParameterAsValue(1); + CHECK_CALL_CONTEXT(callContext); + + dispatchOnMainThread([weakSelf = weakRef(this), name, colorPaletteMap = std::move(colorPaletteMap)]() { + auto self = weakSelf.lock(); + if (self != nullptr) { + if (auto listener = self->getListener()) { + listener->configureColorPalette(name, colorPaletteMap); + } + } + }); + } + return callContext.getContext().newUndefined(); +} + +JSValueRef JavaScriptRuntime::runtimeSetActiveColorPalette(JSFunctionNativeCallContext& callContext) { if (!_isWorker) { - auto colorPaletteMap = callContext.getParameterAsValue(0); + auto name = callContext.getParameterAsString(0); CHECK_CALL_CONTEXT(callContext); - dispatchOnMainThread([weakSelf = weakRef(this), colorPaletteMap = std::move(colorPaletteMap)]() { + dispatchOnMainThread([weakSelf = weakRef(this), name]() { auto self = weakSelf.lock(); if (self != nullptr) { if (auto listener = self->getListener()) { - listener->updateColorPalette(colorPaletteMap); + listener->setActiveColorPalette(name); } } }); @@ -2425,8 +2470,10 @@ void JavaScriptRuntime::buildContext(Valdi::IJavaScriptContext& context, JS_BIND(context, exceptionTracker, runtimeObject, "makeDirectionalAsset", runtimeMakeDirectionalAsset); JS_BIND(context, exceptionTracker, runtimeObject, "makePlatformSpecificAsset", runtimeMakePlatformSpecificAsset); + JS_BIND(context, exceptionTracker, runtimeObject, "makeThemableAsset", runtimeMakeThemableAsset); JS_BIND(context, exceptionTracker, runtimeObject, "getLoadedAssetMetadata", runtimeGetLoadedAssetMetadata); - JS_BIND(context, exceptionTracker, runtimeObject, "setColorPalette", runtimeSetColorPalette); + JS_BIND(context, exceptionTracker, runtimeObject, "configureColorPalette", runtimeConfigureColorPalette); + JS_BIND(context, exceptionTracker, runtimeObject, "setActiveColorPalette", runtimeSetActiveColorPalette); JS_BIND(context, exceptionTracker, runtimeObject, "onMainThreadIdle", runtimeOnMainThreadIdle); JS_BIND(context, exceptionTracker, runtimeObject, "createWorker", runtimeCreateWorker); diff --git a/valdi/src/valdi/runtime/JavaScript/JavaScriptRuntime.hpp b/valdi/src/valdi/runtime/JavaScript/JavaScriptRuntime.hpp index 470f366b..9d8f5d7f 100644 --- a/valdi/src/valdi/runtime/JavaScript/JavaScriptRuntime.hpp +++ b/valdi/src/valdi/runtime/JavaScript/JavaScriptRuntime.hpp @@ -100,7 +100,9 @@ class IJavaScriptRuntimeListener { bool createIfNeeded, Function&)>&& function) = 0; - virtual void updateColorPalette(const Value& colorPaletteMap) = 0; + virtual void configureColorPalette(const StringBox& name, const Value& colorPaletteMap) = 0; + + virtual void setActiveColorPalette(const StringBox& name) = 0; virtual void onUncaughtJsError(const StringBox& moduleName, const Error& error) = 0; @@ -477,8 +479,10 @@ class JavaScriptRuntime : public JavaScriptTaskScheduler, JSValueRef runtimeGetAssets(JSFunctionNativeCallContext& callContext); JSValueRef runtimeMakeDirectionalAsset(JSFunctionNativeCallContext& callContext); JSValueRef runtimeMakePlatformSpecificAsset(JSFunctionNativeCallContext& callContext); + JSValueRef runtimeMakeThemableAsset(JSFunctionNativeCallContext& callContext); JSValueRef runtimeGetLoadedAssetMetadata(JSFunctionNativeCallContext& callContext); - JSValueRef runtimeSetColorPalette(JSFunctionNativeCallContext& callContext); + JSValueRef runtimeConfigureColorPalette(JSFunctionNativeCallContext& callContext); + JSValueRef runtimeSetActiveColorPalette(JSFunctionNativeCallContext& callContext); JSValueRef runtimeTakeElementSnapshot(JSFunctionNativeCallContext& callContext); JSValueRef runtimeGetNativeNodeForElementId(JSFunctionNativeCallContext& callContext); diff --git a/valdi/src/valdi/runtime/JavaScript/Modules/AttributedTextNativeModuleFactory.cpp b/valdi/src/valdi/runtime/JavaScript/Modules/AttributedTextNativeModuleFactory.cpp index d3cf9fa2..938b82cc 100644 --- a/valdi/src/valdi/runtime/JavaScript/Modules/AttributedTextNativeModuleFactory.cpp +++ b/valdi/src/valdi/runtime/JavaScript/Modules/AttributedTextNativeModuleFactory.cpp @@ -5,9 +5,9 @@ namespace Valdi { -AttributedTextNativeModuleFactory::AttributedTextNativeModuleFactory(const Ref& colorPalette, - ILogger& logger) - : _colorPalette(colorPalette), _logger(logger) {} +AttributedTextNativeModuleFactory::AttributedTextNativeModuleFactory( + const Ref& colorPaletteManager, ILogger& logger) + : _colorPaletteManager(colorPaletteManager), _logger(logger) {} AttributedTextNativeModuleFactory::~AttributedTextNativeModuleFactory() = default; @@ -25,7 +25,7 @@ Value AttributedTextNativeModuleFactory::loadModule() { Value AttributedTextNativeModuleFactory::makeNativeAttributedText(const ValueFunctionCallContext& callContext) const { auto value = callContext.getParameter(0); - auto result = TextAttributeValueParser::parse(*_colorPalette, value, _logger, true); + auto result = TextAttributeValueParser::parse(*_colorPaletteManager->getActiveColorPalette(), value, _logger, true); if (!result) { callContext.getExceptionTracker().onError(result.moveError()); return Value::undefined(); @@ -34,4 +34,4 @@ Value AttributedTextNativeModuleFactory::makeNativeAttributedText(const ValueFun return result.value(); } -} // namespace Valdi \ No newline at end of file +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/JavaScript/Modules/AttributedTextNativeModuleFactory.hpp b/valdi/src/valdi/runtime/JavaScript/Modules/AttributedTextNativeModuleFactory.hpp index adacef7f..115a5df9 100644 --- a/valdi/src/valdi/runtime/JavaScript/Modules/AttributedTextNativeModuleFactory.hpp +++ b/valdi/src/valdi/runtime/JavaScript/Modules/AttributedTextNativeModuleFactory.hpp @@ -5,23 +5,23 @@ namespace Valdi { -class ColorPalette; +class ColorPaletteManager; class ILogger; class ValueFunctionCallContext; class AttributedTextNativeModuleFactory : public Valdi::SharedPtrRefCountable, public snap::valdi_core::ModuleFactory { public: - AttributedTextNativeModuleFactory(const Ref& colorPalette, ILogger& logger); + AttributedTextNativeModuleFactory(const Ref& colorPaletteManager, ILogger& logger); ~AttributedTextNativeModuleFactory() override; StringBox getModulePath() override; Value loadModule() override; private: - Ref _colorPalette; + Ref _colorPaletteManager; ILogger& _logger; Value makeNativeAttributedText(const ValueFunctionCallContext& callContext) const; }; -} // namespace Valdi \ No newline at end of file +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/Rendering/ViewNodeRenderer.cpp b/valdi/src/valdi/runtime/Rendering/ViewNodeRenderer.cpp index 1314a68e..33009a7a 100644 --- a/valdi/src/valdi/runtime/Rendering/ViewNodeRenderer.cpp +++ b/valdi/src/valdi/runtime/Rendering/ViewNodeRenderer.cpp @@ -54,8 +54,10 @@ void ViewNodeRenderer::visit(RenderRequestEntries::CreateElement& entry) { return; } - auto viewNode = - Valdi::makeShared(_attributesManager.getYogaConfig(), _attributesManager.getAttributeIds(), _logger); + auto viewNode = Valdi::makeShared(_attributesManager.getYogaConfig(), + _attributesManager.getAttributeIds(), + _attributesManager.getColorPaletteManager()->getActiveColorPalette(), + _logger); viewNode->setViewFactory(_viewTransactionScope, _viewNodeTree.getOrCreateViewFactory(entry.getViewClassName())); viewNode->setRawId(entry.getElementId()); diff --git a/valdi/src/valdi/runtime/Resources/DirectionalAsset.cpp b/valdi/src/valdi/runtime/Resources/DirectionalAsset.cpp index 7f4ccb77..cc92a12d 100644 --- a/valdi/src/valdi/runtime/Resources/DirectionalAsset.cpp +++ b/valdi/src/valdi/runtime/Resources/DirectionalAsset.cpp @@ -29,8 +29,15 @@ double DirectionalAsset::getHeight() const { return _ltrAsset->getHeight(); } -Ref DirectionalAsset::withDirection(bool rightToLeft) { - return rightToLeft ? _rtlAsset : _ltrAsset; +Ref DirectionalAsset::withConfiguration(const AssetConfiguration& configuration) { + if (!configuration.rightToLeft.has_value()) { + return strongSmallRef(this); + } + + auto asset = configuration.rightToLeft.value() ? _rtlAsset : _ltrAsset; + auto remainingConfiguration = configuration; + remainingConfiguration.rightToLeft = std::nullopt; + return asset->withConfiguration(remainingConfiguration); } void DirectionalAsset::addLoadObserver(const std::shared_ptr& observer, diff --git a/valdi/src/valdi/runtime/Resources/DirectionalAsset.hpp b/valdi/src/valdi/runtime/Resources/DirectionalAsset.hpp index 714b4643..18d022c4 100644 --- a/valdi/src/valdi/runtime/Resources/DirectionalAsset.hpp +++ b/valdi/src/valdi/runtime/Resources/DirectionalAsset.hpp @@ -28,7 +28,7 @@ class DirectionalAsset : public Asset { double getWidth() const final; double getHeight() const final; - Ref withDirection(bool rightToLeft) final; + Ref withConfiguration(const AssetConfiguration& configuration) final; void addLoadObserver(const std::shared_ptr& observer, snap::valdi_core::AssetOutputType outputType, diff --git a/valdi/src/valdi/runtime/Resources/PlatformSpecificAsset.cpp b/valdi/src/valdi/runtime/Resources/PlatformSpecificAsset.cpp index cdbc80ea..00fb7ed1 100644 --- a/valdi/src/valdi/runtime/Resources/PlatformSpecificAsset.cpp +++ b/valdi/src/valdi/runtime/Resources/PlatformSpecificAsset.cpp @@ -36,24 +36,38 @@ double PlatformSpecificAsset::getHeight() const { return _defaultAsset->getHeight(); } -Ref PlatformSpecificAsset::withPlatform(PlatformType platformType) { - switch (platformType) { +Ref PlatformSpecificAsset::withConfiguration(const AssetConfiguration& configuration) { + if (!configuration.platformType.has_value()) { + return strongSmallRef(this); + } + + Ref asset; + switch (configuration.platformType.value()) { case PlatformTypeAndroid: if (_androidAsset == nullptr) { - return _defaultAsset; + asset = _defaultAsset; + } else { + asset = _androidAsset; } - return _androidAsset; + break; case PlatformTypeIOS: case PlatformTypeMacOS: case PlatformTypeWeb: case PlatformTypeLinux: if (_iOSAsset == nullptr) { - return _defaultAsset; + asset = _defaultAsset; + } else { + asset = _iOSAsset; } - return _iOSAsset; + break; default: - return _defaultAsset; + asset = _defaultAsset; + break; } + + auto remainingConfiguration = configuration; + remainingConfiguration.platformType = std::nullopt; + return asset->withConfiguration(remainingConfiguration); } void PlatformSpecificAsset::addLoadObserver(const std::shared_ptr& observer, @@ -64,7 +78,7 @@ void PlatformSpecificAsset::addLoadObserver(const std::shared_ptr withPlatform(PlatformType platformType) final; + Ref withConfiguration(const AssetConfiguration& configuration) final; void addLoadObserver(const std::shared_ptr& observer, snap::valdi_core::AssetOutputType outputType, diff --git a/valdi/src/valdi/runtime/Resources/ThemableAsset.cpp b/valdi/src/valdi/runtime/Resources/ThemableAsset.cpp new file mode 100644 index 00000000..e5ee716e --- /dev/null +++ b/valdi/src/valdi/runtime/Resources/ThemableAsset.cpp @@ -0,0 +1,75 @@ +// +// ThemableAsset.cpp +// valdi +// + +#include "valdi/runtime/Resources/ThemableAsset.hpp" +#include "valdi_core/cpp/Attributes/ColorPalette.hpp" + +namespace Valdi { + +ThemableAsset::ThemableAsset(FlatMap> assetsByColorPalette) + : _assetsByColorPalette(std::move(assetsByColorPalette)) { + if (!_assetsByColorPalette.empty()) { + _representativeAsset = _assetsByColorPalette.begin()->second; + } +} + +ThemableAsset::~ThemableAsset() = default; + +bool ThemableAsset::canBeMeasured() const { + return _representativeAsset != nullptr && _representativeAsset->canBeMeasured(); +} + +StringBox ThemableAsset::getIdentifier() { + return _representativeAsset != nullptr ? _representativeAsset->getIdentifier() : StringBox(); +} + +double ThemableAsset::getWidth() const { + return _representativeAsset != nullptr ? _representativeAsset->getWidth() : 0.0; +} + +double ThemableAsset::getHeight() const { + return _representativeAsset != nullptr ? _representativeAsset->getHeight() : 0.0; +} + +Ref ThemableAsset::withConfiguration(const AssetConfiguration& configuration) { + if (configuration.colorPalette == nullptr) { + return strongSmallRef(this); + } + + const auto& it = _assetsByColorPalette.find(configuration.colorPalette->getName()); + if (it == _assetsByColorPalette.end()) { + return nullptr; + } + + auto remainingConfiguration = configuration; + remainingConfiguration.colorPalette = nullptr; + return it->second->withConfiguration(remainingConfiguration); +} + +void ThemableAsset::addLoadObserver(const std::shared_ptr& /*observer*/, + snap::valdi_core::AssetOutputType /*outputType*/, + int32_t /*preferredWidth*/, + int32_t /*preferredHeight*/, + const Valdi::Value& /*associatedData*/) { + // No op. ThemableAsset should be resolved through withConfiguration before rendering. +} + +void ThemableAsset::removeLoadObserver( + const std::shared_ptr& /*observer*/) { + // No op. ThemableAsset should be resolved through withConfiguration before rendering. +} + +void ThemableAsset::updateLoadObserverPreferredSize( + const std::shared_ptr& /*observer*/, + int32_t /*preferredWidth*/, + int32_t /*preferredHeight*/) { + // No op. ThemableAsset should be resolved through withConfiguration before rendering. +} + +std::optional ThemableAsset::getResolvedLocation() const { + return _representativeAsset != nullptr ? _representativeAsset->getResolvedLocation() : std::nullopt; +} + +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/Resources/ThemableAsset.hpp b/valdi/src/valdi/runtime/Resources/ThemableAsset.hpp new file mode 100644 index 00000000..20511419 --- /dev/null +++ b/valdi/src/valdi/runtime/Resources/ThemableAsset.hpp @@ -0,0 +1,53 @@ +// +// ThemableAsset.hpp +// valdi +// + +#pragma once + +#include "valdi_core/cpp/Resources/Asset.hpp" +#include "valdi_core/cpp/Utils/FlatMap.hpp" +#include "valdi_core/cpp/Utils/StringBox.hpp" + +namespace Valdi { + +class ColorPalette; + +/** + A ThemableAsset holds assets keyed by color palette name and resolves to the + asset matching the view node's resolved color palette. + */ +class ThemableAsset : public Asset { +public: + explicit ThemableAsset(FlatMap> assetsByColorPalette); + ~ThemableAsset() override; + + bool canBeMeasured() const final; + + StringBox getIdentifier() final; + + double getWidth() const final; + double getHeight() const final; + + Ref withConfiguration(const AssetConfiguration& configuration) final; + + void addLoadObserver(const std::shared_ptr& observer, + snap::valdi_core::AssetOutputType outputType, + int32_t preferredWidth, + int32_t preferredHeight, + const Valdi::Value& associatedData) final; + + void removeLoadObserver(const std::shared_ptr& observer) final; + + void updateLoadObserverPreferredSize(const std::shared_ptr& observer, + int32_t preferredWidth, + int32_t preferredHeight) final; + + std::optional getResolvedLocation() const final; + +private: + FlatMap> _assetsByColorPalette; + Ref _representativeAsset; +}; + +} // namespace Valdi diff --git a/valdi/src/valdi/runtime/Runtime.cpp b/valdi/src/valdi/runtime/Runtime.cpp index abccb844..4b243aa0 100644 --- a/valdi/src/valdi/runtime/Runtime.cpp +++ b/valdi/src/valdi/runtime/Runtime.cpp @@ -123,7 +123,7 @@ Runtime::Runtime(AttributeIds& attributeIds, const Shared& resourceLoader, const Ref& assetLoaderManager, const Holder>& requestManager, - const Ref& colorPalette, + const Ref& colorPaletteManager, const Ref& diskCache, const std::shared_ptr& yogaConfig, const Shared& runtimeMessageHandler, @@ -146,7 +146,7 @@ Runtime::Runtime(AttributeIds& attributeIds, _contextManager(logger, this), _viewNodeManager(*mainThreadManager, *logger), _mainThreadManager(mainThreadManager), - _colorPalette(colorPalette), + _colorPaletteManager(colorPaletteManager), _diskCache(diskCache), _userSession(userSession), _requestManager(requestManager), @@ -213,7 +213,8 @@ void Runtime::postInit() { _diskCache, _workerQueue, _userSession, _keychain, *_logger, disablePersistentStoreEncryption())); } registerNativeModuleFactory(makeShared().toShared()); - registerNativeModuleFactory(makeShared(_colorPalette, *_logger).toShared()); + registerNativeModuleFactory( + makeShared(_colorPaletteManager, *_logger).toShared()); registerJavaScriptModuleFactory(makeShared(*_resourceManager, _workerQueue, *_logger)); registerJavaScriptModuleFactory(makeShared()); @@ -781,30 +782,24 @@ void Runtime::registerJavaScriptModuleFactory(const Ref _javaScriptRuntime->registerJavaScriptModuleFactory(moduleFactory); } -void Runtime::updateColorPalette(const Value& colorPaletteMap) { +void Runtime::configureColorPalette(const StringBox& name, const Value& colorPaletteMap) { if (colorPaletteMap.isMap()) { FlatMap colors; for (const auto& it : *colorPaletteMap.getMap()) { - auto colorResult = ValueConverter::toColor(*_colorPalette, it.second); - if (!colorResult) { - VALDI_ERROR(*_logger, "Failed to parse color '{}': {}", it.first, colorResult.error()); + auto colorValue = ValueConverter::toColorValue(it.second); + if (!colorValue) { + VALDI_ERROR(*_logger, "Failed to parse color '{}': {}", it.first, colorValue.error()); continue; } - - colors[it.first] = colorResult.value(); + colors[it.first] = Color(colorValue.value().toLong()); } - _colorPalette->updateColors(colors); + _colorPaletteManager->configureColorPalette(name, colors); } } -Value Runtime::getColorPalette() { - auto valueMap = Valdi::makeShared(); - for (const auto& [name, color] : _colorPalette->getColors()) { - (*valueMap)[name] = Valdi::Value(color.value); - } - - return Valdi::Value(std::move(valueMap)); +void Runtime::setActiveColorPalette(const StringBox& name) { + _colorPaletteManager->setActiveColorPalette(name); } void Runtime::onUncaughtJsError(const StringBox& moduleName, const Error& error) { diff --git a/valdi/src/valdi/runtime/Runtime.hpp b/valdi/src/valdi/runtime/Runtime.hpp index 6e79df37..d70efc30 100644 --- a/valdi/src/valdi/runtime/Runtime.hpp +++ b/valdi/src/valdi/runtime/Runtime.hpp @@ -66,7 +66,7 @@ class RenderViewNodeRequest; using SharedRuntime = Ref; class ViewManagerContext; -class ColorPalette; +class ColorPaletteManager; class AssetLoaderManager; class ITweakValueProvider; class JavaScriptANRDetector; @@ -89,7 +89,7 @@ class Runtime final : public IDebuggerServiceListener, IJavaScriptRuntimeListene const Shared& resourceLoader, const Ref& assetLoaderManager, const Holder>& requestManager, - const Ref& colorPalette, + const Ref& colorPaletteManager, const Ref& diskCache, const std::shared_ptr& yogaConfig, const Shared& runtimeMessageHandler, @@ -266,8 +266,8 @@ class Runtime final : public IDebuggerServiceListener, IJavaScriptRuntimeListene void emitInitMetrics(); - void updateColorPalette(const Value& colorPaletteMap) override; - Value getColorPalette(); + void configureColorPalette(const StringBox& name, const Value& colorPaletteMap) override; + void setActiveColorPalette(const StringBox& name) override; const Ref& getDiskCache() const; @@ -314,7 +314,7 @@ class Runtime final : public IDebuggerServiceListener, IJavaScriptRuntimeListene ContextManager _contextManager; ViewNodeTreeManager _viewNodeManager; Ref _mainThreadManager; - Ref _colorPalette; + Ref _colorPaletteManager; Ref _diskCache; SharedAtomicObject _userSession; const Holder> _requestManager; diff --git a/valdi/src/valdi/runtime/RuntimeManager.cpp b/valdi/src/valdi/runtime/RuntimeManager.cpp index 714d2078..d06638a4 100644 --- a/valdi/src/valdi/runtime/RuntimeManager.cpp +++ b/valdi/src/valdi/runtime/RuntimeManager.cpp @@ -127,7 +127,7 @@ RuntimeManager::RuntimeManager(const Ref& mainThreadDispa _diskCache(diskCache), _keychain(std::move(keychain)), _runtimeMessageHandler(runtimeMessageHandler), - _colorPalette(makeShared()), + _colorPaletteManager(makeShared()), _platformType(platformType), _jsThreadQoS(jsThreadQoS), _debuggerServiceEnabled(_debuggerService != nullptr) { @@ -138,7 +138,7 @@ RuntimeManager::RuntimeManager(const Ref& mainThreadDispa _anrDetector->setListener(makeShared(runtimeMessageHandler)); } - _colorPalette->setListener(this); + _colorPaletteManager->setListener(this); } RuntimeManager::~RuntimeManager() { @@ -185,7 +185,7 @@ SharedRuntime RuntimeManager::createRuntime(const Shared& resou resourceLoader, _assetLoaderManager, _requestManager, - _colorPalette, + _colorPaletteManager, _diskCache, _yogaConfig, _runtimeMessageHandler, @@ -260,7 +260,7 @@ Ref RuntimeManager::createViewManagerContext( auto viewManagerContext = makeShared( viewManager, _attributeIds, - _colorPalette, + _colorPaletteManager, _yogaConfig, enablePreloading, mainThreadManagerOverride != nullptr ? mainThreadManagerOverride : _mainThreadManager, @@ -459,60 +459,28 @@ void RuntimeManager::setApplicationId(const StringBox& applicationId) { } } -void RuntimeManager::onColorPaletteUpdated(const ColorPalette& /*colorPalette*/) { - FlatSet attributesToReapply; - - // Clear the cache for all color attributes - for (const auto& viewManagerContext : _viewManagerContexts) { - for (const auto& it : viewManagerContext->getAttributesManager().getAllBoundAttributes()) { - for (const auto& handlerIt : it.second->getHandlers()) { - auto* handler = it.second->getAttributeHandlerForId(handlerIt.first); - - if (handler->shouldReevaluateOnColorPaletteChange()) { - handler->clearPreprocessorCache(); - attributesToReapply.insert(handlerIt.first); - } - } - } - } - - // Reapply all the color attributes - auto allAttributes = makeShared>(); - allAttributes->insert(allAttributes->end(), attributesToReapply.begin(), attributesToReapply.end()); - +void RuntimeManager::onColorPaletteManagerUpdated(const ColorPaletteManager& colorPaletteManager, + const ColorPalette& colorPalette, + bool activeColorPaletteChanged) { #if VALDI_DEBUG_TREE_UPDATES - std::string applyTrigger = "apply_attributes"; - if (!_viewManagerContexts.empty() && !attributesToReapply.empty()) { - const auto& attributeIds = _viewManagerContexts.front()->getAttributesManager().getAttributeIds(); - std::string names; - size_t count = 0; - constexpr size_t kMaxNames = 12; - constexpr size_t kMaxLen = 80; - for (AttributeId id : attributesToReapply) { - if (count >= kMaxNames || (count > 0 && names.size() >= kMaxLen)) { - names += ",..."; - break; - } - if (count != 0) { - names += ","; - } - names += attributeIds.getNameForId(id).slowToString(); - ++count; - } - if (!names.empty()) { - applyTrigger += ":"; - applyTrigger += names; - } - } + std::string applyTrigger = "apply_color_attributes"; for (const auto& runtime : getAllRuntimes()) { for (const auto& tree : runtime->getViewNodeTreeManager().getAllRootViewNodeTrees()) { tree->scheduleExclusiveUpdate( - [treePtr = tree.get(), allAttributes]() { + [treePtr = tree.get(), + colorPaletteRef = activeColorPaletteChanged ? colorPaletteManager.getActiveColorPalette() : nullptr, + colorPalettePtr = &colorPalette, + activeColorPaletteChanged]() { auto rootViewNode = treePtr->getRootViewNode(); if (rootViewNode != nullptr) { - rootViewNode->reapplyAttributesRecursive( - treePtr->getCurrentViewTransactionScope(), *allAttributes, false); + if (activeColorPaletteChanged) { + rootViewNode->setInheritedColorPalette(treePtr->getCurrentViewTransactionScope(), + colorPaletteRef); + } else { + rootViewNode->onColorPaletteMutated(treePtr->getCurrentViewTransactionScope(), + *colorPalettePtr); + } } }, Valdi::DispatchFunction(), @@ -523,11 +491,19 @@ void RuntimeManager::onColorPaletteUpdated(const ColorPalette& /*colorPalette*/) for (const auto& runtime : getAllRuntimes()) { for (const auto& tree : runtime->getViewNodeTreeManager().getAllRootViewNodeTrees()) { tree->scheduleExclusiveUpdate( - [treePtr = tree.get(), allAttributes]() { + [treePtr = tree.get(), + colorPaletteRef = activeColorPaletteChanged ? colorPaletteManager.getActiveColorPalette() : nullptr, + colorPalettePtr = &colorPalette, + activeColorPaletteChanged]() { auto rootViewNode = treePtr->getRootViewNode(); if (rootViewNode != nullptr) { - rootViewNode->reapplyAttributesRecursive( - treePtr->getCurrentViewTransactionScope(), *allAttributes, false); + if (activeColorPaletteChanged) { + rootViewNode->setInheritedColorPalette(treePtr->getCurrentViewTransactionScope(), + colorPaletteRef); + } else { + rootViewNode->onColorPaletteMutated(treePtr->getCurrentViewTransactionScope(), + *colorPalettePtr); + } } }, Valdi::DispatchFunction()); diff --git a/valdi/src/valdi/runtime/RuntimeManager.hpp b/valdi/src/valdi/runtime/RuntimeManager.hpp index 861ffc6e..d7d7c3a0 100644 --- a/valdi/src/valdi/runtime/RuntimeManager.hpp +++ b/valdi/src/valdi/runtime/RuntimeManager.hpp @@ -61,7 +61,7 @@ class IRuntimeManagerListener : public SimpleRefCountable { virtual void onRuntimeCreated(Runtime& runtime) = 0; }; -class RuntimeManager : public ValdiObject, protected ColorPaletteListener { +class RuntimeManager : public ValdiObject, protected ColorPaletteManagerListener { public: RuntimeManager(const Ref& mainThreadDispatcher, IJavaScriptBridge* jsBridge, @@ -163,7 +163,9 @@ class RuntimeManager : public ValdiObject, protected ColorPaletteListener { VALDI_CLASS_HEADER(RuntimeManager) protected: - void onColorPaletteUpdated(const ColorPalette& colorPalette) override; + void onColorPaletteManagerUpdated(const ColorPaletteManager& colorPaletteManager, + const ColorPalette& colorPalette, + bool activeColorPaletteChanged) override; private: std::shared_ptr _initStopWatch; @@ -194,7 +196,7 @@ class RuntimeManager : public ValdiObject, protected ColorPaletteListener { std::vector _registeredTypeConverters; std::vector> _listeners; - Ref _colorPalette; + Ref _colorPaletteManager; Ref _attributionResolver; Holder> _userSession; Ref _runtimeTweaks; diff --git a/valdi/test/benchmark/ViewNode_benchmark.cpp b/valdi/test/benchmark/ViewNode_benchmark.cpp index 5ef29897..b29a542e 100644 --- a/valdi/test/benchmark/ViewNode_benchmark.cpp +++ b/valdi/test/benchmark/ViewNode_benchmark.cpp @@ -23,7 +23,7 @@ struct Dependencies { Dependencies() : mainQueue(), mainThreadManager(mainQueue), viewManager(), attributeIds() { viewManagerContext = makeShared(viewManager, attributeIds, - makeShared(), + makeShared(), Yoga::createConfig(0), true, mainThreadManager, diff --git a/valdi/test/integration/Runtime_tests.cpp b/valdi/test/integration/Runtime_tests.cpp index 6fbb3373..0507cfde 100644 --- a/valdi/test/integration/Runtime_tests.cpp +++ b/valdi/test/integration/Runtime_tests.cpp @@ -5319,6 +5319,106 @@ TEST_P(RuntimeFixture, supportsPlatformSpecificAsset) { ASSERT_EQ(iOSAssetUrl, asset->getIdentifier()); } +TEST_P(RuntimeFixture, supportsThemableAsset) { + auto lightAssetUrl = STRING_LITERAL("file://light.png"); + auto darkAssetUrl = STRING_LITERAL("file://dark.png"); + wrapper.diskCache->store(Path(URL(lightAssetUrl).getPath()), BytesView()).ensureSuccess(); + wrapper.diskCache->store(Path(URL(darkAssetUrl).getPath()), BytesView()).ensureSuccess(); + + auto viewModel = Value() + .setMapValue("lightAsset", Value(lightAssetUrl)) + .setMapValue("darkAsset", Value(darkAssetUrl)) + .setMapValue("includeDarkAsset", Value(true)); + + auto tree = + wrapper.createViewNodeTreeAndContext(STRING_LITERAL("ThemableAsset@test/src/ThemableAsset"), viewModel, Value()); + + tree->setLayoutSpecs(Size(1.0f, 1.0f), LayoutDirectionLTR); + + wrapper.waitUntilAllUpdatesCompleted(); + + auto rootNode = tree->getRootViewNode(); + ASSERT_TRUE(rootNode != nullptr); + + auto rootAsset = getSrcAssetFromNode(*rootNode->getChildAt(0)); + ASSERT_TRUE(rootAsset != nullptr); + ASSERT_EQ(lightAssetUrl, rootAsset->getIdentifier()); + + auto overriddenAsset = getSrcAssetFromNode(*rootNode->getChildAt(1)->getChildAt(0)); + ASSERT_TRUE(overriddenAsset != nullptr); + ASSERT_EQ(darkAssetUrl, overriddenAsset->getIdentifier()); + + auto nestedAsset = getSrcAssetFromNode(*rootNode->getChildAt(2)); + ASSERT_TRUE(nestedAsset != nullptr); + ASSERT_EQ(lightAssetUrl, nestedAsset->getIdentifier()); +} + +TEST_P(RuntimeFixture, canSwitchActiveColorPaletteForThemableAsset) { + auto lightAssetUrl = STRING_LITERAL("file://light.png"); + auto darkAssetUrl = STRING_LITERAL("file://dark.png"); + wrapper.diskCache->store(Path(URL(lightAssetUrl).getPath()), BytesView()).ensureSuccess(); + wrapper.diskCache->store(Path(URL(darkAssetUrl).getPath()), BytesView()).ensureSuccess(); + + auto viewModel = Value() + .setMapValue("lightAsset", Value(lightAssetUrl)) + .setMapValue("darkAsset", Value(darkAssetUrl)) + .setMapValue("includeDarkAsset", Value(true)); + + auto tree = + wrapper.createViewNodeTreeAndContext(STRING_LITERAL("ThemableAsset@test/src/ThemableAsset"), viewModel, Value()); + + tree->setLayoutSpecs(Size(1.0f, 1.0f), LayoutDirectionLTR); + + wrapper.waitUntilAllUpdatesCompleted(); + + wrapper.runtime->getJavaScriptRuntime()->callComponentFunction(tree->getContext(), + STRING_LITERAL("setDarkColorPalette")); + + wrapper.flushQueues(); + + auto rootNode = tree->getRootViewNode(); + ASSERT_TRUE(rootNode != nullptr); + + auto asset = getSrcAssetFromNode(*rootNode->getChildAt(0)); + ASSERT_TRUE(asset != nullptr); + ASSERT_EQ(darkAssetUrl, asset->getIdentifier()); + + auto nestedAsset = getSrcAssetFromNode(*rootNode->getChildAt(2)); + ASSERT_TRUE(nestedAsset != nullptr); + ASSERT_EQ(darkAssetUrl, nestedAsset->getIdentifier()); +} + +TEST_P(RuntimeFixture, missingThemableAssetPaletteClearsAsset) { + auto lightAssetUrl = STRING_LITERAL("file://light.png"); + auto darkAssetUrl = STRING_LITERAL("file://dark.png"); + wrapper.diskCache->store(Path(URL(lightAssetUrl).getPath()), BytesView()).ensureSuccess(); + wrapper.diskCache->store(Path(URL(darkAssetUrl).getPath()), BytesView()).ensureSuccess(); + + auto viewModel = Value() + .setMapValue("lightAsset", Value(lightAssetUrl)) + .setMapValue("darkAsset", Value(darkAssetUrl)) + .setMapValue("includeDarkAsset", Value(false)); + + auto tree = + wrapper.createViewNodeTreeAndContext(STRING_LITERAL("ThemableAsset@test/src/ThemableAsset"), viewModel, Value()); + + tree->setLayoutSpecs(Size(1.0f, 1.0f), LayoutDirectionLTR); + + wrapper.waitUntilAllUpdatesCompleted(); + + wrapper.runtime->getJavaScriptRuntime()->callComponentFunction(tree->getContext(), + STRING_LITERAL("setDarkColorPalette")); + + wrapper.flushQueues(); + + auto rootNode = tree->getRootViewNode(); + ASSERT_TRUE(rootNode != nullptr); + + ASSERT_EQ(nullptr, getSrcAssetFromNode(*rootNode->getChildAt(0))); + ASSERT_EQ(nullptr, getSrcAssetFromNode(*rootNode->getChildAt(1)->getChildAt(0))); + ASSERT_EQ(nullptr, getSrcAssetFromNode(*rootNode->getChildAt(2))); +} + TEST_P(RuntimeFixture, canHotReloadAsset) { auto assets = registerAssets(wrapper); @@ -7549,6 +7649,19 @@ TEST_P(RuntimeFixture, supportsNotifyWithUncaughtErrorHandler) { ASSERT_TRUE(result.isError()); } +static DummyView makeColorPaletteTestView(int64_t borderColor, int64_t backgroundColor) { + return DummyView("SCValdiView") + .addAttribute("border", Value(ValueArray::make({Value(1.0), Value(borderColor)}))) + .addChild(DummyView("SCValdiView") + .addAttribute("background", + Value(ValueArray::make({ + Value(ValueArray::make({Value(backgroundColor)})), + Value(ValueArray::make({})), + Value(static_cast(0)), + Value(false), + })))); +} + TEST_P(RuntimeFixture, supportsCustomColorPalette) { auto tree = wrapper.createViewNodeTreeAndContext(STRING_LITERAL("ColorPaletteTest@test/src/ColorPaletteTest"), Value(makeShared()), @@ -7556,20 +7669,10 @@ TEST_P(RuntimeFixture, supportsCustomColorPalette) { wrapper.waitUntilAllUpdatesCompleted(); - ASSERT_EQ(DummyView("SCValdiView") - .addAttribute("border", Value(ValueArray::make({Value(1.0), Value(65535)}))) - .addChild(DummyView("SCValdiView") - .addAttribute("background", - Value(ValueArray::make({ - Value(ValueArray::make({Value(8388863)})), - Value(ValueArray::make({})), - Value(static_cast(0)), - Value(false), - })))), - getRootView(tree)); + ASSERT_EQ(makeColorPaletteTestView(65535, 8388863), getRootView(tree)); } -TEST_P(RuntimeFixture, canUpdateCustomColorPalette) { +TEST_P(RuntimeFixture, canSwitchActiveCustomColorPalette) { auto tree = wrapper.createViewNodeTreeAndContext(STRING_LITERAL("ColorPaletteTest@test/src/ColorPaletteTest"), Value(makeShared()), Value::undefined()); @@ -7577,22 +7680,150 @@ TEST_P(RuntimeFixture, canUpdateCustomColorPalette) { wrapper.waitUntilAllUpdatesCompleted(); wrapper.runtime->getJavaScriptRuntime()->callComponentFunction(tree->getContext(), - STRING_LITERAL("updateColorPalette")); + STRING_LITERAL("setDarkColorPalette")); wrapper.flushQueues(); - ASSERT_EQ( - DummyView("SCValdiView") - .addAttribute("border", Value(ValueArray::make({Value(1.0), Value(static_cast(4278190335))}))) - .addChild(DummyView("SCValdiView") - .addAttribute("background", - Value(ValueArray::make({ - Value(ValueArray::make({Value(static_cast(4294902015))})), - Value(ValueArray::make({})), - Value(static_cast(0)), - Value(false), - })))), - getRootView(tree)); + ASSERT_EQ(makeColorPaletteTestView(4278190335, 4294902015), getRootView(tree)); +} + +TEST_P(RuntimeFixture, doesNotReapplyCustomColorPaletteWhenInactivePaletteChanges) { + auto tree = wrapper.createViewNodeTreeAndContext(STRING_LITERAL("ColorPaletteTest@test/src/ColorPaletteTest"), + Value(makeShared()), + Value::undefined()); + + wrapper.waitUntilAllUpdatesCompleted(); + + wrapper.runtime->getJavaScriptRuntime()->callComponentFunction(tree->getContext(), + STRING_LITERAL("updateDarkColorPalette")); + + wrapper.flushQueues(); + + ASSERT_EQ(makeColorPaletteTestView(65535, 8388863), getRootView(tree)); +} + +TEST_P(RuntimeFixture, reappliesCustomColorPaletteWhenActivePaletteChanges) { + auto tree = wrapper.createViewNodeTreeAndContext(STRING_LITERAL("ColorPaletteTest@test/src/ColorPaletteTest"), + Value(makeShared()), + Value::undefined()); + + wrapper.waitUntilAllUpdatesCompleted(); + + wrapper.runtime->getJavaScriptRuntime()->callComponentFunction(tree->getContext(), + STRING_LITERAL("updateLightColorPalette")); + + wrapper.flushQueues(); + + ASSERT_EQ(makeColorPaletteTestView(4278190335, 4294902015), getRootView(tree)); +} + +static DummyView makeColorPaletteOverrideTestView(int64_t rootBackgroundColor, + int64_t overrideBackgroundColor, + int64_t overrideChildBackgroundColor, + int64_t siblingBackgroundColor) { + auto makeBackground = [](int64_t color) { + return Value(ValueArray::make({ + Value(ValueArray::make({Value(color)})), + Value(ValueArray::make({})), + Value(static_cast(0)), + Value(false), + })); + }; + + return DummyView("SCValdiView") + .addAttribute("background", makeBackground(rootBackgroundColor)) + .addChild( + DummyView("SCValdiView") + .addAttribute("background", makeBackground(overrideBackgroundColor)) + .addChild( + DummyView("SCValdiView").addAttribute("background", makeBackground(overrideChildBackgroundColor)))) + .addChild(DummyView("SCValdiView").addAttribute("background", makeBackground(siblingBackgroundColor))); +} + +TEST_P(RuntimeFixture, supportsPerViewNodeColorPaletteOverride) { + auto tree = wrapper.createViewNodeTreeAndContext( + STRING_LITERAL("ColorPaletteOverrideTest@test/src/ColorPaletteOverrideTest"), + Value(makeShared()), + Value::undefined()); + + wrapper.waitUntilAllUpdatesCompleted(); + + ASSERT_EQ(makeColorPaletteOverrideTestView(65535, 4278190335, 4294902015, 8388863), getRootView(tree)); +} + +TEST_P(RuntimeFixture, switchingActiveColorPaletteDoesNotChangeOverriddenSubtree) { + auto tree = wrapper.createViewNodeTreeAndContext( + STRING_LITERAL("ColorPaletteOverrideTest@test/src/ColorPaletteOverrideTest"), + Value(makeShared()), + Value::undefined()); + + wrapper.waitUntilAllUpdatesCompleted(); + + wrapper.runtime->getJavaScriptRuntime()->callComponentFunction(tree->getContext(), + STRING_LITERAL("setDarkActiveColorPalette")); + + wrapper.flushQueues(); + + ASSERT_EQ(makeColorPaletteOverrideTestView(4278190335, 4278190335, 4294902015, 4294902015), getRootView(tree)); +} + +TEST_P(RuntimeFixture, clearingRootColorPaletteOverrideFallsBackToActivePalette) { + auto tree = wrapper.createViewNodeTreeAndContext( + STRING_LITERAL("ColorPaletteOverrideTest@test/src/ColorPaletteOverrideTest"), + Value(makeShared()), + Value::undefined()); + + wrapper.waitUntilAllUpdatesCompleted(); + + auto root = tree->getRootViewNode(); + ASSERT_NE(nullptr, root); + ASSERT_EQ(STRING_LITERAL("light"), root->getResolvedColorPalette()->getName()); + + tree->scheduleExclusiveUpdate([&]() { + root->setColorPaletteName(tree->getCurrentViewTransactionScope(), STRING_LITERAL("dark")); + }); + wrapper.flushQueues(); + ASSERT_EQ(STRING_LITERAL("dark"), root->getResolvedColorPalette()->getName()); + + tree->scheduleExclusiveUpdate([&]() { + root->setColorPaletteName(tree->getCurrentViewTransactionScope(), StringBox()); + }); + wrapper.flushQueues(); + + ASSERT_NE(nullptr, root->getResolvedColorPalette()); + ASSERT_EQ(STRING_LITERAL("light"), root->getResolvedColorPalette()->getName()); +} + +TEST_P(RuntimeFixture, mutatingOverriddenColorPaletteUpdatesOverriddenSubtree) { + auto tree = wrapper.createViewNodeTreeAndContext( + STRING_LITERAL("ColorPaletteOverrideTest@test/src/ColorPaletteOverrideTest"), + Value(makeShared()), + Value::undefined()); + + wrapper.waitUntilAllUpdatesCompleted(); + + wrapper.runtime->getJavaScriptRuntime()->callComponentFunction(tree->getContext(), + STRING_LITERAL("updateDarkColorPalette")); + + wrapper.flushQueues(); + + ASSERT_EQ(makeColorPaletteOverrideTestView(65535, 255, 4294967295, 8388863), getRootView(tree)); +} + +TEST_P(RuntimeFixture, mutatingUnusedColorPaletteDoesNotChangeRenderedAttributes) { + auto tree = wrapper.createViewNodeTreeAndContext( + STRING_LITERAL("ColorPaletteOverrideTest@test/src/ColorPaletteOverrideTest"), + Value(makeShared()), + Value::undefined()); + + wrapper.waitUntilAllUpdatesCompleted(); + + wrapper.runtime->getJavaScriptRuntime()->callComponentFunction(tree->getContext(), + STRING_LITERAL("updateUnusedColorPalette")); + + wrapper.flushQueues(); + + ASSERT_EQ(makeColorPaletteOverrideTestView(65535, 4278190335, 4294902015, 8388863), getRootView(tree)); } static Result postprocessArrayValueToLength(ViewNode& viewNode, const Value& in) { diff --git a/valdi/test/runtime/AttributeParser_tests.cpp b/valdi/test/runtime/AttributeParser_tests.cpp index 8103801d..82fd4edb 100644 --- a/valdi/test/runtime/AttributeParser_tests.cpp +++ b/valdi/test/runtime/AttributeParser_tests.cpp @@ -15,7 +15,7 @@ using namespace Valdi; namespace ValdiTest { TEST(AttributeParser, canParseColor) { - ColorPalette colorPalette; + ColorPalette colorPalette(STRING_LITERAL("test")); AttributeParser parser("red rgba(0, 255, 0, 1.0) #0000ff #0000ff10 #f5a rgba(50, 25, 100, 0.5) rgba(25%, 10%, 5%, " "0.1) rgba(255, 255, 255, 5%)"); @@ -86,7 +86,7 @@ TEST(AttributeParser, canParseColor) { } TEST(AttributeParser, failsToParseColorOnColorPaletteMismatch) { - ColorPalette colorPalette; + ColorPalette colorPalette(STRING_LITERAL("test")); AttributeParser parser("reddish"); diff --git a/valdi/test/runtime/AttributeProcessors_tests.cpp b/valdi/test/runtime/AttributeProcessors_tests.cpp index 929b763e..bc6da57b 100644 --- a/valdi/test/runtime/AttributeProcessors_tests.cpp +++ b/valdi/test/runtime/AttributeProcessors_tests.cpp @@ -1,89 +1,115 @@ #include "valdi/runtime/Attributes/DefaultAttributeProcessors.hpp" #include "valdi/runtime/Attributes/ValueConverters.hpp" -#include "valdi_core/cpp/Attributes/AttributeUtils.hpp" +#include "valdi_core/cpp/Attributes/ColorPalette.hpp" #include "valdi_core/cpp/Utils/StringCache.hpp" #include "valdi_core/cpp/Utils/ValueArray.hpp" #include "gtest/gtest.h" #include +#include using namespace Valdi; namespace ValdiTest { -static auto kColorPalette = makeShared(); - -static Value makeGradientValue(std::vector colors, std::vector locations, int32_t angle, bool radial) { +template +static Value makeGradientValueImpl(const ColorsT& colors, + std::initializer_list locations, + int32_t angle, + bool radial) { auto outColors = ValueArray::make(colors.size()); auto outLocations = ValueArray::make(locations.size()); - for (size_t i = 0; i < colors.size(); i++) { - outColors->emplace(i, Value(colors[i].value)); + size_t i = 0; + for (const auto& color : colors) { + outColors->emplace(i++, Value(color)); } - for (size_t i = 0; i < locations.size(); i++) { - outLocations->emplace(i, Value(locations[i])); + + i = 0; + for (auto location : locations) { + outLocations->emplace(i++, Value(location)); } return Value(ValueArray::make({Value(outColors), Value(outLocations), Value(angle), Value(radial)})); } +static Value makeGradientValue(std::initializer_list colors, + std::initializer_list locations, + int32_t angle, + bool radial) { + return makeGradientValueImpl(colors, locations, angle, radial); +} + +static Value makeGradientValue(std::initializer_list colors, + std::initializer_list locations, + int32_t angle, + bool radial) { + return makeGradientValueImpl(colors, locations, angle, radial); +} + +static Ref makeTestColorPalette() { + auto colorPalette = makeShared(STRING_LITERAL("default")); + colorPalette->updateColors({ + {STRING_LITERAL("primary"), Color(0x11223344)}, + {STRING_LITERAL("secondary"), Color(0x55667788)}, + }); + return colorPalette; +} + TEST(AttributeProcessor, canParseSimpleBackground) { - auto result = preprocessGradient(kColorPalette, Value(STRING_LITERAL("red"))); + auto colorPalette = makeTestColorPalette(); - ASSERT_TRUE(result.success()) << result.description(); + auto result = preprocessGradient(Value(STRING_LITERAL("red"))); - ASSERT_EQ(makeGradientValue({Color::rgba(255, 0, 0, 1.0)}, {}, 0, false), result.value()); + ASSERT_TRUE(result.success()) << result.description(); - auto ltrResult = postprocessGradient(false, result.value()); - ASSERT_TRUE(ltrResult) << ltrResult.description(); - ASSERT_EQ(result.value(), ltrResult.value()); + ASSERT_EQ(makeGradientValue({STRING_LITERAL("red")}, {}, 0, false), result.value()); - auto rtlResult = postprocessGradient(true, result.value()); - ASSERT_TRUE(rtlResult) << rtlResult.description(); - ASSERT_EQ(result.value(), rtlResult.value()); + auto postprocessed = postprocessGradient(false, *colorPalette, result.value()); + ASSERT_TRUE(postprocessed) << postprocessed.description(); + ASSERT_EQ(makeGradientValue({0xFF0000FF}, {}, 0, false), postprocessed.value()); } TEST(AttributeProcessor, failsOnTrailingInvalidKeyword) { - auto result = preprocessGradient(kColorPalette, Value(STRING_LITERAL("red wtf"))); + auto result = preprocessGradient(Value(STRING_LITERAL("red wtf"))); ASSERT_FALSE(result.success()) << result.description(); } TEST(AttributeProcessor, canParseSimpleLinearGradient) { - auto result = preprocessGradient(kColorPalette, Value(STRING_LITERAL("linear-gradient(blue, white, red)"))); + auto colorPalette = makeTestColorPalette(); + + auto result = preprocessGradient(Value(STRING_LITERAL("linear-gradient(blue, white, red)"))); ASSERT_TRUE(result.success()) << result.description(); ASSERT_EQ(makeGradientValue( { - Color::rgba(0, 0, 255, 1.0), - Color::rgba(255, 255, 255, 1.0), - Color::rgba(255, 0, 0, 1.0), + STRING_LITERAL("blue"), + STRING_LITERAL("white"), + STRING_LITERAL("red"), }, {}, 0, false), result.value()); - auto ltrResult = postprocessGradient(false, result.value()); - ASSERT_TRUE(ltrResult) << ltrResult.description(); - ASSERT_EQ(result.value(), ltrResult.value()); - - auto rtlResult = postprocessGradient(true, result.value()); - ASSERT_TRUE(rtlResult) << rtlResult.description(); - ASSERT_EQ(result.value(), rtlResult.value()); + auto postprocessed = postprocessGradient(false, *colorPalette, result.value()); + ASSERT_TRUE(postprocessed) << postprocessed.description(); + ASSERT_EQ(makeGradientValue({0x0000FFFF, 0xFFFFFFFF, 0xFF0000FF}, {}, 0, false), postprocessed.value()); } TEST(AttributeProcessor, canParseLinearGradientWithLocations) { - auto result = - preprocessGradient(kColorPalette, Value(STRING_LITERAL("linear-gradient(blue 0, white 0.25, red 0.75)"))); + auto colorPalette = makeTestColorPalette(); + + auto result = preprocessGradient(Value(STRING_LITERAL("linear-gradient(blue 0, white 0.25, red 0.75)"))); ASSERT_TRUE(result.success()) << result.description(); ASSERT_EQ(makeGradientValue( { - Color::rgba(0, 0, 255, 1.0), - Color::rgba(255, 255, 255, 1.0), - Color::rgba(255, 0, 0, 1.0), + STRING_LITERAL("blue"), + STRING_LITERAL("white"), + STRING_LITERAL("red"), }, { 0.0, @@ -94,125 +120,99 @@ TEST(AttributeProcessor, canParseLinearGradientWithLocations) { false), result.value()); - auto ltrResult = postprocessGradient(false, result.value()); - ASSERT_TRUE(ltrResult) << ltrResult.description(); - ASSERT_EQ(result.value(), ltrResult.value()); - - auto rtlResult = postprocessGradient(true, result.value()); - ASSERT_TRUE(rtlResult) << rtlResult.description(); - ASSERT_EQ(result.value(), rtlResult.value()); + auto postprocessed = postprocessGradient(false, *colorPalette, result.value()); + ASSERT_TRUE(postprocessed) << postprocessed.description(); + ASSERT_EQ(makeGradientValue({0x0000FFFF, 0xFFFFFFFF, 0xFF0000FF}, {0.0, 0.25, 0.75}, 0, false), + postprocessed.value()); } TEST(AttributeProcessor, failsWhenLinearGradientWithLocationsIsNotBalanced) { - auto result = preprocessGradient(kColorPalette, Value(STRING_LITERAL("linear-gradient(blue 0, white 0.25, red)"))); + auto result = preprocessGradient(Value(STRING_LITERAL("linear-gradient(blue 0, white 0.25, red)"))); ASSERT_FALSE(result.success()) << result.description(); } TEST(AttributeProcessor, canParseAngleInLinearGradient) { - auto result = preprocessGradient(kColorPalette, Value(STRING_LITERAL("linear-gradient(45deg, blue, white, red)"))); + auto colorPalette = makeTestColorPalette(); + + auto result = preprocessGradient(Value(STRING_LITERAL("linear-gradient(45deg, blue, white, red)"))); ASSERT_TRUE(result.success()) << result.description(); ASSERT_EQ(makeGradientValue( { - Color::rgba(0, 0, 255, 1.0), - Color::rgba(255, 255, 255, 1.0), - Color::rgba(255, 0, 0, 1.0), + STRING_LITERAL("blue"), + STRING_LITERAL("white"), + STRING_LITERAL("red"), }, {}, 1, false), result.value()); - auto ltrResult = postprocessGradient(false, result.value()); - ASSERT_TRUE(ltrResult) << ltrResult.description(); - ASSERT_EQ(result.value(), ltrResult.value()); - - auto rtlResult = postprocessGradient(true, result.value()); - ASSERT_TRUE(rtlResult) << rtlResult.description(); - ASSERT_EQ(makeGradientValue( - { - Color::rgba(0, 0, 255, 1.0), - Color::rgba(255, 255, 255, 1.0), - Color::rgba(255, 0, 0, 1.0), - }, - {}, - 7, - false), - rtlResult.value()); + auto postprocessed = postprocessGradient(true, *colorPalette, result.value()); + ASSERT_TRUE(postprocessed) << postprocessed.description(); + ASSERT_EQ(makeGradientValue({0x0000FFFF, 0xFFFFFFFF, 0xFF0000FF}, {}, 7, false), postprocessed.value()); } TEST(AttributeProcessor, canParseAngleAsRadInLinearGradient) { - auto result = preprocessGradient(kColorPalette, Value(STRING_LITERAL("linear-gradient(1.6rad, blue, white, red)"))); + auto colorPalette = makeTestColorPalette(); + + auto result = preprocessGradient(Value(STRING_LITERAL("linear-gradient(1.6rad, blue, white, red)"))); ASSERT_TRUE(result.success()) << result.description(); ASSERT_EQ(makeGradientValue( { - Color::rgba(0, 0, 255, 1.0), - Color::rgba(255, 255, 255, 1.0), - Color::rgba(255, 0, 0, 1.0), + STRING_LITERAL("blue"), + STRING_LITERAL("white"), + STRING_LITERAL("red"), }, {}, 2, false), result.value()); - auto ltrResult = postprocessGradient(false, result.value()); - ASSERT_TRUE(ltrResult) << ltrResult.description(); - ASSERT_EQ(result.value(), ltrResult.value()); - - auto rtlResult = postprocessGradient(true, result.value()); - ASSERT_TRUE(rtlResult) << rtlResult.description(); - ASSERT_EQ(makeGradientValue( - { - Color::rgba(0, 0, 255, 1.0), - Color::rgba(255, 255, 255, 1.0), - Color::rgba(255, 0, 0, 1.0), - }, - {}, - 6, - false), - rtlResult.value()); + auto postprocessed = postprocessGradient(true, *colorPalette, result.value()); + ASSERT_TRUE(postprocessed) << postprocessed.description(); + ASSERT_EQ(makeGradientValue({0x0000FFFF, 0xFFFFFFFF, 0xFF0000FF}, {}, 6, false), postprocessed.value()); } TEST(AttributeProcessor, canParseSimpleRadialGradient) { - auto result = preprocessGradient(kColorPalette, Value(STRING_LITERAL("radial-gradient(blue, white, red)"))); + auto colorPalette = makeTestColorPalette(); + + auto result = preprocessGradient(Value(STRING_LITERAL("radial-gradient(blue, white, red)"))); ASSERT_TRUE(result.success()) << result.description(); ASSERT_EQ(makeGradientValue( { - Color::rgba(0, 0, 255, 1.0), - Color::rgba(255, 255, 255, 1.0), - Color::rgba(255, 0, 0, 1.0), + STRING_LITERAL("blue"), + STRING_LITERAL("white"), + STRING_LITERAL("red"), }, {}, 0, true), result.value()); - auto ltrResult = postprocessGradient(false, result.value()); - ASSERT_TRUE(ltrResult) << ltrResult.description(); - ASSERT_EQ(result.value(), ltrResult.value()); - - auto rtlResult = postprocessGradient(true, result.value()); - ASSERT_TRUE(rtlResult) << rtlResult.description(); - ASSERT_EQ(result.value(), rtlResult.value()); + auto postprocessed = postprocessGradient(false, *colorPalette, result.value()); + ASSERT_TRUE(postprocessed) << postprocessed.description(); + ASSERT_EQ(makeGradientValue({0x0000FFFF, 0xFFFFFFFF, 0xFF0000FF}, {}, 0, true), postprocessed.value()); } TEST(AttributeProcessor, canParseRadialGradientWithLocations) { - auto result = - preprocessGradient(kColorPalette, Value(STRING_LITERAL("radial-gradient(blue 0, white 0.25, red 0.75)"))); + auto colorPalette = makeTestColorPalette(); + + auto result = preprocessGradient(Value(STRING_LITERAL("radial-gradient(blue 0, white 0.25, red 0.75)"))); ASSERT_TRUE(result.success()) << result.description(); ASSERT_EQ(makeGradientValue( { - Color::rgba(0, 0, 255, 1.0), - Color::rgba(255, 255, 255, 1.0), - Color::rgba(255, 0, 0, 1.0), + STRING_LITERAL("blue"), + STRING_LITERAL("white"), + STRING_LITERAL("red"), }, { 0.0, @@ -223,24 +223,20 @@ TEST(AttributeProcessor, canParseRadialGradientWithLocations) { true), result.value()); - auto ltrResult = postprocessGradient(false, result.value()); - ASSERT_TRUE(ltrResult) << ltrResult.description(); - ASSERT_EQ(result.value(), ltrResult.value()); - - auto rtlResult = postprocessGradient(true, result.value()); - ASSERT_TRUE(rtlResult) << rtlResult.description(); - ASSERT_EQ(result.value(), rtlResult.value()); + auto postprocessed = postprocessGradient(false, *colorPalette, result.value()); + ASSERT_TRUE(postprocessed) << postprocessed.description(); + ASSERT_EQ(makeGradientValue({0x0000FFFF, 0xFFFFFFFF, 0xFF0000FF}, {0.0, 0.25, 0.75}, 0, true), + postprocessed.value()); } TEST(AttributeProcessor, failsWhenRadialGradientWithLocationsIsNotBalanced) { - auto result = preprocessGradient(kColorPalette, Value(STRING_LITERAL("radial-gradient(blue 0, white 0.25, red)"))); + auto result = preprocessGradient(Value(STRING_LITERAL("radial-gradient(blue 0, white 0.25, red)"))); ASSERT_FALSE(result.success()) << result.description(); } TEST(AttributeProcessor, failsWhenRadialGradientHasAngle) { - auto result = - preprocessGradient(kColorPalette, Value(STRING_LITERAL("radial-gradient(45deg, blue 0, white 0.25, red)"))); + auto result = preprocessGradient(Value(STRING_LITERAL("radial-gradient(45deg, blue 0, white 0.25, red)"))); ASSERT_FALSE(result.success()) << result.description(); } @@ -317,7 +313,7 @@ TEST(AttributeProcessor, failsParseBorderRadiusWithInvalidValues) { } TEST(AttributeProcessor, flipsHorizontalBordersOnRTL) { - auto result = preprocessBorderRadius(nullptr, Value(STRING_LITERAL("42 12 100 1337"))); + auto result = preprocessBorderRadius(Value(STRING_LITERAL("42 12 100 1337"))); ASSERT_TRUE(result) << result.description(); auto borderRadius = result.value().getTypedRef(); @@ -348,4 +344,19 @@ TEST(AttributeProcessor, flipsHorizontalBordersOnRTL) { ASSERT_EQ(borderRadius->getBottomRight(), rtlBorderRadius->getBottomLeft()); } +TEST(AttributeProcessor, postprocessGradientResolvesColors) { + auto colorPalette = makeTestColorPalette(); + + auto result = preprocessGradient(Value(STRING_LITERAL("linear-gradient(45deg, primary, secondary)"))); + ASSERT_TRUE(result.success()) << result.description(); + + ASSERT_EQ(makeGradientValue({STRING_LITERAL("primary"), STRING_LITERAL("secondary")}, {}, 1, false), + result.value()); + + auto postprocessed = postprocessGradient(false, *colorPalette, result.value()); + ASSERT_TRUE(postprocessed.success()) << postprocessed.description(); + + ASSERT_EQ(makeGradientValue({0x11223344, 0x55667788}, {}, 1, false), postprocessed.value()); +} + } // namespace ValdiTest diff --git a/valdi/test/runtime/ColorPaletteManager_tests.cpp b/valdi/test/runtime/ColorPaletteManager_tests.cpp new file mode 100644 index 00000000..5c2ef1b0 --- /dev/null +++ b/valdi/test/runtime/ColorPaletteManager_tests.cpp @@ -0,0 +1,89 @@ +#include "valdi_core/cpp/Attributes/ColorPalette.hpp" +#include "valdi_core/cpp/Utils/StringCache.hpp" +#include "gtest/gtest.h" + +using namespace Valdi; + +namespace ValdiTest { + +class TestColorPaletteManagerListener : public ColorPaletteManagerListener { +public: + void onColorPaletteManagerUpdated(const ColorPaletteManager& colorPaletteManager, + const ColorPalette& colorPalette, + bool activeColorPaletteChanged) override { + updateCount++; + lastActiveColorPaletteName = colorPaletteManager.getActiveColorPalette()->getName(); + lastUpdatedColorPaletteName = colorPalette.getName(); + lastActiveColorPaletteChanged = activeColorPaletteChanged; + } + + int updateCount = 0; + StringBox lastActiveColorPaletteName; + StringBox lastUpdatedColorPaletteName; + bool lastActiveColorPaletteChanged = false; +}; + +TEST(ColorPaletteManager, defaultsToDefaultPaletteWithCssColors) { + ColorPaletteManager manager; + + ASSERT_EQ(STRING_LITERAL("default"), manager.getActiveColorPalette()->getName()); + ASSERT_EQ(Color::rgba(255, 0, 0, 1.0), + manager.getActiveColorPalette()->getColorForName(STRING_LITERAL("red")).value()); +} + +TEST(ColorPaletteManager, notifiesWhenConfiguringInactivePaletteWithChangedValuesOnly) { + ColorPaletteManager manager; + TestColorPaletteManagerListener listener; + manager.setListener(&listener); + + manager.configureColorPalette(STRING_LITERAL("dark"), {{STRING_LITERAL("background"), Color::rgba(0, 0, 0, 1.0)}}); + manager.configureColorPalette(STRING_LITERAL("dark"), {{STRING_LITERAL("background"), Color::rgba(0, 0, 0, 1.0)}}); + + ASSERT_EQ(1, listener.updateCount); + ASSERT_EQ(STRING_LITERAL("default"), manager.getActiveColorPalette()->getName()); + ASSERT_EQ(STRING_LITERAL("dark"), listener.lastUpdatedColorPaletteName); + ASSERT_FALSE(listener.lastActiveColorPaletteChanged); +} + +TEST(ColorPaletteManager, notifiesWhenConfiguringActivePaletteWithChangedValuesOnly) { + ColorPaletteManager manager; + TestColorPaletteManagerListener listener; + manager.setListener(&listener); + + manager.configureColorPalette(STRING_LITERAL("default"), + {{STRING_LITERAL("background"), Color::rgba(255, 255, 255, 1.0)}}); + manager.configureColorPalette(STRING_LITERAL("default"), + {{STRING_LITERAL("background"), Color::rgba(255, 255, 255, 1.0)}}); + + ASSERT_EQ(1, listener.updateCount); + ASSERT_EQ(STRING_LITERAL("default"), listener.lastUpdatedColorPaletteName); + ASSERT_FALSE(listener.lastActiveColorPaletteChanged); + ASSERT_EQ(Color::rgba(255, 255, 255, 1.0), + manager.getActiveColorPalette()->getColorForName(STRING_LITERAL("background")).value()); +} + +TEST(ColorPaletteManager, notifiesWhenActivePaletteChangesOnly) { + ColorPaletteManager manager; + TestColorPaletteManagerListener listener; + manager.setListener(&listener); + + manager.setActiveColorPalette(STRING_LITERAL("dark")); + manager.setActiveColorPalette(STRING_LITERAL("dark")); + + ASSERT_EQ(1, listener.updateCount); + ASSERT_EQ(STRING_LITERAL("dark"), listener.lastActiveColorPaletteName); + ASSERT_EQ(STRING_LITERAL("dark"), listener.lastUpdatedColorPaletteName); + ASSERT_TRUE(listener.lastActiveColorPaletteChanged); +} + +TEST(ColorPaletteManager, createsDefaultInitializedPaletteWhenActivatingUnknownName) { + ColorPaletteManager manager; + + manager.setActiveColorPalette(STRING_LITERAL("dark")); + + ASSERT_EQ(STRING_LITERAL("dark"), manager.getActiveColorPalette()->getName()); + ASSERT_EQ(Color::rgba(255, 0, 0, 1.0), + manager.getActiveColorPalette()->getColorForName(STRING_LITERAL("red")).value()); +} + +} // namespace ValdiTest diff --git a/valdi/test/utils/ViewNodeTestsUtils.cpp b/valdi/test/utils/ViewNodeTestsUtils.cpp index ebbddd6e..e0c57d78 100644 --- a/valdi/test/utils/ViewNodeTestsUtils.cpp +++ b/valdi/test/utils/ViewNodeTestsUtils.cpp @@ -13,8 +13,11 @@ static Ref makeMainThreadManager(const Ref()), _mainThreadManager(makeMainThreadManager(_mainQueue->createMainThreadDispatcher())), - _attributesManager( - _viewManager, _attributeIds, makeShared(), ConsoleLogger::getLogger(), Yoga::createConfig(0)), + _attributesManager(_viewManager, + _attributeIds, + makeShared(), + ConsoleLogger::getLogger(), + Yoga::createConfig(0)), _viewTransactionScope(makeShared(&_viewManager, nullptr, false)) { _mainThreadManager->markCurrentThreadIsMainThread(); @@ -49,8 +52,10 @@ ViewTransactionScope& ViewNodeTestsDependencies::getViewTransactionScope() { } Ref ViewNodeTestsDependencies::createNode(const char* viewClassName) { - auto viewNode = Valdi::makeShared( - _attributesManager.getYogaConfig(), _attributesManager.getAttributeIds(), _attributesManager.getLogger()); + auto viewNode = Valdi::makeShared(_attributesManager.getYogaConfig(), + _attributesManager.getAttributeIds(), + _attributesManager.getColorPaletteManager()->getActiveColorPalette(), + _attributesManager.getLogger()); viewNode->setViewNodeTree(_tree.get()); viewNode->setViewFactory(*_viewTransactionScope, _viewFactories->getViewFactory(STRING_LITERAL(viewClassName))); diff --git a/valdi/testdata/resources/modules/test/src/ColorPaletteOverrideTest.tsx b/valdi/testdata/resources/modules/test/src/ColorPaletteOverrideTest.tsx new file mode 100644 index 00000000..740d3ea2 --- /dev/null +++ b/valdi/testdata/resources/modules/test/src/ColorPaletteOverrideTest.tsx @@ -0,0 +1,49 @@ +import { Component } from 'valdi_core/src/Component'; +import { ValdiRuntime } from 'valdi_core/src/ValdiRuntime'; + +declare const runtime: ValdiRuntime; + +export class ColorPaletteOverrideTest extends Component { + onCreate() { + runtime.configureColorPalette('light', { + background: 'rgba(0, 0, 255, 1)', + foreground: 'rgba(0, 128, 0, 1)', + }); + runtime.configureColorPalette('dark', { + background: 'rgba(255, 0, 0, 1)', + foreground: 'rgba(255, 255, 0, 1)', + }); + runtime.configureColorPalette('unused', { + background: 'rgba(0, 0, 0, 1)', + foreground: 'rgba(255, 255, 255, 1)', + }); + runtime.setActiveColorPalette('light'); + } + + onRender() { + + + + + + ; + } + + setDarkActiveColorPalette() { + runtime.setActiveColorPalette('dark'); + } + + updateDarkColorPalette() { + runtime.configureColorPalette('dark', { + background: 'rgba(0, 0, 0, 1)', + foreground: 'rgba(255, 255, 255, 1)', + }); + } + + updateUnusedColorPalette() { + runtime.configureColorPalette('unused', { + background: 'rgba(255, 255, 255, 1)', + foreground: 'rgba(0, 0, 0, 1)', + }); + } +} diff --git a/valdi/testdata/resources/modules/test/src/ColorPaletteTest.tsx b/valdi/testdata/resources/modules/test/src/ColorPaletteTest.tsx index 801d7bff..0fc707d3 100644 --- a/valdi/testdata/resources/modules/test/src/ColorPaletteTest.tsx +++ b/valdi/testdata/resources/modules/test/src/ColorPaletteTest.tsx @@ -1,27 +1,42 @@ -import { Component } from "valdi_core/src/Component"; -import { ValdiRuntime } from "valdi_core/src/ValdiRuntime"; +import { Component } from 'valdi_core/src/Component'; +import { ValdiRuntime } from 'valdi_core/src/ValdiRuntime'; declare const runtime: ValdiRuntime; export class ColorPaletteTest extends Component { - onCreate() { - runtime.setColorPalette({ - background: 'blue', - foreground: 'green' + runtime.configureColorPalette('light', { + background: 'rgba(0, 0, 255, 1)', + foreground: 'rgba(0, 128, 0, 1)', + }); + runtime.configureColorPalette('dark', { + background: 'rgba(255, 0, 0, 1)', + foreground: 'rgba(255, 255, 0, 1)', }); + runtime.setActiveColorPalette('light'); } onRender() { - - + + ; + } + + setDarkColorPalette() { + runtime.setActiveColorPalette('dark'); + } + + updateDarkColorPalette() { + runtime.configureColorPalette('dark', { + background: 'rgba(0, 0, 0, 1)', + foreground: 'rgba(255, 255, 255, 1)', + }); } - updateColorPalette() { - runtime.setColorPalette({ - background: 'red', - foreground: 'yellow' + updateLightColorPalette() { + runtime.configureColorPalette('light', { + background: 'rgba(255, 0, 0, 1)', + foreground: 'rgba(255, 255, 0, 1)', }); } -} \ No newline at end of file +} diff --git a/valdi/testdata/resources/modules/test/src/ThemableAsset.tsx b/valdi/testdata/resources/modules/test/src/ThemableAsset.tsx new file mode 100644 index 00000000..e9adc0f5 --- /dev/null +++ b/valdi/testdata/resources/modules/test/src/ThemableAsset.tsx @@ -0,0 +1,53 @@ +import { Asset, ThemableAssetMap, makeDirectionalAsset, makeThemableAsset } from 'valdi_core/src/Asset'; +import { Component } from 'valdi_core/src/Component'; +import { ValdiRuntime } from 'valdi_core/src/ValdiRuntime'; + +declare const runtime: ValdiRuntime; + +interface ViewModel { + lightAsset: string | Asset; + darkAsset: string | Asset; + includeDarkAsset: boolean; +} + +export class ThemableAsset extends Component { + private asset?: Asset; + private nestedAsset?: Asset; + + onCreate() { + runtime.configureColorPalette('light', { + background: 'rgba(0, 0, 255, 1)', + }); + runtime.configureColorPalette('dark', { + background: 'rgba(255, 0, 0, 1)', + }); + runtime.setActiveColorPalette('light'); + } + + onViewModelUpdate(): void { + const assetsByColorPalette: ThemableAssetMap = { + light: this.viewModel.lightAsset, + }; + + if (this.viewModel.includeDarkAsset) { + assetsByColorPalette.dark = this.viewModel.darkAsset; + } + + this.asset = makeThemableAsset(assetsByColorPalette); + this.nestedAsset = makeDirectionalAsset(this.asset, this.viewModel.lightAsset); + } + + onRender() { + + + + + + + ; + } + + setDarkColorPalette() { + runtime.setActiveColorPalette('dark'); + } +} diff --git a/valdi_core/src/valdi_core/cpp/Attributes/AttributeUtils.cpp b/valdi_core/src/valdi_core/cpp/Attributes/AttributeUtils.cpp index 903944dd..49e37b55 100644 --- a/valdi_core/src/valdi_core/cpp/Attributes/AttributeUtils.cpp +++ b/valdi_core/src/valdi_core/cpp/Attributes/AttributeUtils.cpp @@ -47,6 +47,24 @@ std::optional AttributeParser::parseColorComponent(bool isAlpha) { } std::optional AttributeParser::parseColor(const ColorPalette& colorPalette) { + auto colorValue = parseColorValue(); + if (!colorValue) { + return std::nullopt; + } + + if (colorValue->isNumber()) { + return Color(colorValue->toLong()); + } + + auto colorName = colorValue->toStringBox(); + auto color = colorPalette.getColorForName(colorName); + if (!color) { + setErrorAtCurrentPosition(Error(STRING_FORMAT("Invalid color name '{}'", colorName))); + } + return color; +} + +std::optional AttributeParser::parseColorValue() { tryParseWhitespaces(); if (tryParse("rgba(")) { @@ -87,7 +105,7 @@ std::optional AttributeParser::parseColor(const ColorPalette& colorPalett return std::nullopt; } - return Color(r.value(), g.value(), b.value(), a.value()); + return Value(Color(r.value(), g.value(), b.value(), a.value()).value); } else { if (tryParse('#')) { auto currentPosition = position(); @@ -111,19 +129,14 @@ std::optional AttributeParser::parseColor(const ColorPalette& colorPalett color = 0xFF | color << 8; } - return Color(color); + return Value(color); } else { - // Skip until the end of non-whitespace and then check if this is a color keyword tryParseWhitespaces(); auto keyword = parseIdentifier(); if (!keyword || keyword.value().empty()) { return std::nullopt; } - auto color = colorPalette.getColorForName(StringCache::getGlobal().makeString(keyword.value())); - if (!color) { - setErrorAtCurrentPosition(Error(STRING_FORMAT("Invalid color name '{}'", keyword.value()))); - } - return color; + return Value(StringCache::getGlobal().makeString(keyword.value())); } } } diff --git a/valdi_core/src/valdi_core/cpp/Attributes/AttributeUtils.hpp b/valdi_core/src/valdi_core/cpp/Attributes/AttributeUtils.hpp index 703f1719..b297f3bc 100644 --- a/valdi_core/src/valdi_core/cpp/Attributes/AttributeUtils.hpp +++ b/valdi_core/src/valdi_core/cpp/Attributes/AttributeUtils.hpp @@ -12,6 +12,7 @@ #include "valdi_core/cpp/Utils/Result.hpp" #include "valdi_core/cpp/Utils/StringBox.hpp" #include "valdi_core/cpp/Utils/TextParser.hpp" +#include "valdi_core/cpp/Utils/Value.hpp" #include namespace Valdi { @@ -48,6 +49,7 @@ class AttributeParser : public TextParser { explicit AttributeParser(std::string_view str); std::optional parseColor(const ColorPalette& colorPalette); + std::optional parseColorValue(); std::optional parseAngle(); std::optional parseDimension(); diff --git a/valdi_core/src/valdi_core/cpp/Attributes/ColorPalette.cpp b/valdi_core/src/valdi_core/cpp/Attributes/ColorPalette.cpp index 2f16a454..d6964e8e 100644 --- a/valdi_core/src/valdi_core/cpp/Attributes/ColorPalette.cpp +++ b/valdi_core/src/valdi_core/cpp/Attributes/ColorPalette.cpp @@ -176,7 +176,7 @@ std::vector> getDefaultColors() { }; } -ColorPalette::ColorPalette() { +ColorPalette::ColorPalette(const StringBox& name) : _name(name) { for (const auto& it : getDefaultColors()) { setColorForName(StringCache::getGlobal().makeString(it.first), it.second); } @@ -184,7 +184,11 @@ ColorPalette::ColorPalette() { ColorPalette::~ColorPalette() = default; -void ColorPalette::updateColors(const FlatMap& colors) { +const StringBox& ColorPalette::getName() const { + return _name; +} + +bool ColorPalette::updateColors(const FlatMap& colors) { auto changed = false; for (const auto& it : colors) { @@ -193,11 +197,7 @@ void ColorPalette::updateColors(const FlatMap& colors) { } } - if (changed) { - if (_listener != nullptr) { - _listener->onColorPaletteUpdated(*this); - } - } + return changed; } bool ColorPalette::setColorForName(const StringBox& name, Color color) { @@ -226,8 +226,58 @@ const FlatMap& ColorPalette::getColors() const { return _colorByName; } -void ColorPalette::setListener(ColorPaletteListener* listener) { +ColorPaletteManager::ColorPaletteManager() : _activeColorPalette(makeShared(STRING_LITERAL("default"))) { + _colorPaletteByName[_activeColorPalette->getName()] = _activeColorPalette; +} + +ColorPaletteManager::~ColorPaletteManager() = default; + +const Ref& ColorPaletteManager::getActiveColorPalette() const { + return _activeColorPalette; +} + +const Ref& ColorPaletteManager::getColorPalette(const StringBox& name) { + return getOrCreateColorPalette(name); +} + +const FlatMap>& ColorPaletteManager::getColorPalettes() const { + return _colorPaletteByName; +} + +void ColorPaletteManager::configureColorPalette(const StringBox& name, const FlatMap& colors) { + const auto& colorPalette = getOrCreateColorPalette(name); + if (colorPalette->updateColors(colors)) { + notifyListener(*colorPalette, false); + } +} + +void ColorPaletteManager::setActiveColorPalette(const StringBox& name) { + if (name == _activeColorPalette->getName()) { + return; + } + + _activeColorPalette = getOrCreateColorPalette(name); + notifyListener(*_activeColorPalette, true); +} + +void ColorPaletteManager::setListener(ColorPaletteManagerListener* listener) { _listener = listener; } +const Ref& ColorPaletteManager::getOrCreateColorPalette(const StringBox& name) { + auto it = _colorPaletteByName.find(name); + if (it != _colorPaletteByName.end()) { + return it->second; + } + + _colorPaletteByName[name] = makeShared(name); + return _colorPaletteByName[name]; +} + +void ColorPaletteManager::notifyListener(const ColorPalette& colorPalette, bool activeColorPaletteChanged) { + if (_listener != nullptr) { + _listener->onColorPaletteManagerUpdated(*this, colorPalette, activeColorPaletteChanged); + } +} + } // namespace Valdi diff --git a/valdi_core/src/valdi_core/cpp/Attributes/ColorPalette.hpp b/valdi_core/src/valdi_core/cpp/Attributes/ColorPalette.hpp index 36da8547..9535bd3b 100644 --- a/valdi_core/src/valdi_core/cpp/Attributes/ColorPalette.hpp +++ b/valdi_core/src/valdi_core/cpp/Attributes/ColorPalette.hpp @@ -63,29 +63,55 @@ std::ostream& operator<<(std::ostream& os, const Color& value); class ColorPalette; -class ColorPaletteListener { +class ColorPaletteManager; + +class ColorPaletteManagerListener { public: - virtual ~ColorPaletteListener() = default; - virtual void onColorPaletteUpdated(const ColorPalette& colorPalette) = 0; + virtual ~ColorPaletteManagerListener() = default; + virtual void onColorPaletteManagerUpdated(const ColorPaletteManager& colorPaletteManager, + const ColorPalette& colorPalette, + bool activeColorPaletteChanged) = 0; }; -class ColorPalette : public SharedPtrRefCountable { +class ColorPalette : public SimpleRefCountable { public: - ColorPalette(); + explicit ColorPalette(const StringBox& name); ~ColorPalette() override; + const StringBox& getName() const; std::optional getColorForName(const StringBox& name) const; const FlatMap& getColors() const; - void updateColors(const FlatMap& colors); - - void setListener(ColorPaletteListener* listener); + bool updateColors(const FlatMap& colors); private: + StringBox _name; FlatMap _colorByName; - ColorPaletteListener* _listener = nullptr; bool setColorForName(const StringBox& name, Color color); }; +class ColorPaletteManager : public SharedPtrRefCountable { +public: + ColorPaletteManager(); + ~ColorPaletteManager() override; + + const Ref& getActiveColorPalette() const; + const Ref& getColorPalette(const StringBox& name); + const FlatMap>& getColorPalettes() const; + + void configureColorPalette(const StringBox& name, const FlatMap& colors); + void setActiveColorPalette(const StringBox& name); + + void setListener(ColorPaletteManagerListener* listener); + +private: + FlatMap> _colorPaletteByName; + Ref _activeColorPalette; + ColorPaletteManagerListener* _listener = nullptr; + + const Ref& getOrCreateColorPalette(const StringBox& name); + void notifyListener(const ColorPalette& colorPalette, bool activeColorPaletteChanged); +}; + } // namespace Valdi diff --git a/valdi_core/src/valdi_core/cpp/Resources/Asset.cpp b/valdi_core/src/valdi_core/cpp/Resources/Asset.cpp index 8f7e57e3..87ecbe16 100644 --- a/valdi_core/src/valdi_core/cpp/Resources/Asset.cpp +++ b/valdi_core/src/valdi_core/cpp/Resources/Asset.cpp @@ -13,6 +13,11 @@ Asset::Asset() = default; Asset::~Asset() = default; +AssetConfiguration::AssetConfiguration(Ref colorPalette, + std::optional platformType, + std::optional rightToLeft) + : colorPalette(std::move(colorPalette)), platformType(platformType), rightToLeft(rightToLeft) {} + bool Asset::needResolve() const { return !getResolvedLocation().has_value(); } @@ -44,11 +49,7 @@ StringBox Asset::getResolvedURL() const { return resolvedLocation.value().getUrl(); } -Ref Asset::withDirection(bool /*rightToLeft*/) { - return strongSmallRef(this); -} - -Ref Asset::withPlatform(PlatformType /*platformType*/) { +Ref Asset::withConfiguration(const AssetConfiguration& /*configuration*/) { return strongSmallRef(this); } diff --git a/valdi_core/src/valdi_core/cpp/Resources/Asset.hpp b/valdi_core/src/valdi_core/cpp/Resources/Asset.hpp index 0e15b856..25b00e41 100644 --- a/valdi_core/src/valdi_core/cpp/Resources/Asset.hpp +++ b/valdi_core/src/valdi_core/cpp/Resources/Asset.hpp @@ -8,12 +8,24 @@ #pragma once #include "valdi_core/Asset.hpp" +#include "valdi_core/cpp/Attributes/ColorPalette.hpp" #include "valdi_core/cpp/Context/PlatformType.hpp" #include "valdi_core/cpp/Resources/AssetLocation.hpp" #include "valdi_core/cpp/Utils/ValdiObject.hpp" +#include namespace Valdi { +struct AssetConfiguration { + AssetConfiguration(Ref colorPalette, + std::optional platformType, + std::optional rightToLeft); + + Ref colorPalette; + std::optional platformType; + std::optional rightToLeft; +}; + class Asset : public ValdiObject, public snap::valdi_core::Asset { public: Asset(); @@ -26,18 +38,7 @@ class Asset : public ValdiObject, public snap::valdi_core::Asset { virtual bool canBeMeasured() const = 0; - /** - Return an LTR or RTL representation of the asset. - If the asset is not directional, this will return "this". - */ - virtual Ref withDirection(bool rightToLeft); - - /** - Return a platform specific asset depending on - the platform it will be rendered upon. - If the asset is not platform specific, this will return "this". - */ - virtual Ref withPlatform(PlatformType platformType); + virtual Ref withConfiguration(const AssetConfiguration& configuration); virtual double getWidth() const = 0; virtual double getHeight() const = 0;