From 0442a243768576c8bf052ffd5853ac1db4f13cc5 Mon Sep 17 00:00:00 2001 From: Sasi Shanmugarajah Date: Tue, 7 Feb 2023 08:25:06 -0500 Subject: [PATCH] APL-CORE: February 2023 Release of APL 2023.1 compliant core engine (2023.1.0) For more details on this release refer to CHANGELOG.md To learn about APL see: https://developer.amazon.com/docs/alexa-presentation-language/understand-apl.html --- CHANGELOG.md | 12 + README.md | 25 +- aplcore/CMakeLists.txt | 6 +- aplcore/include/apl/action/arrayaction.h | 4 +- aplcore/include/apl/action/sequentialaction.h | 2 +- .../apl/animation/easingapproximation.h | 16 +- aplcore/include/apl/animation/easinggrammar.h | 1 - aplcore/include/apl/apl.h | 11 +- aplcore/include/apl/audio/audioplayer.h | 4 +- aplcore/include/apl/command/corecommand.h | 7 +- aplcore/include/apl/common.h | 17 +- aplcore/include/apl/component/component.h | 13 +- .../include/apl/component/componentpropdef.h | 4 +- .../apl/component/containercomponent.h | 2 +- aplcore/include/apl/component/corecomponent.h | 20 +- .../include/apl/component/edittextcomponent.h | 2 +- .../include/apl/component/imagecomponent.h | 2 +- .../apl/component/mediacomponenttrait.h | 1 + .../apl/component/scrollviewcomponent.h | 2 +- aplcore/include/apl/component/selector.h | 76 ++ .../include/apl/component/yogaproperties.h | 2 +- aplcore/include/apl/content/aplversion.h | 8 +- aplcore/include/apl/content/content.h | 3 +- .../apl/content/extensioncommanddefinition.h | 18 +- .../apl/content/extensionfilterdefinition.h | 10 +- aplcore/include/apl/content/rootconfig.h | 21 +- .../apl/datagrammar/databindingerrors.h | 5 +- .../apl/datagrammar/databindinggrammar.h | 2 +- .../apl/datagrammar/databindingrules.h | 7 +- .../dynamicindexlistdatasourceprovider.h | 1 - .../datasource/dynamiclistdatasourcecommon.h | 2 +- .../dynamiclistdatasourceprovider.h | 7 +- .../dynamictokenlistdatasourceprovider.h | 1 - .../offsetindexdatasourceconnection.h | 2 + aplcore/include/apl/engine/builder.h | 3 +- aplcore/include/apl/engine/context.h | 21 +- aplcore/include/apl/engine/evaluate.h | 1 - aplcore/include/apl/engine/event.h | 2 +- aplcore/include/apl/engine/eventmanager.h | 71 ++ aplcore/include/apl/engine/eventpublisher.h | 49 + aplcore/include/apl/engine/info.h | 3 + aplcore/include/apl/engine/layoutmanager.h | 2 +- aplcore/include/apl/engine/propdef.h | 142 +-- .../include/apl/engine/queueeventmanager.h | 49 + .../include/apl/engine/recalculatetarget.h | 20 + aplcore/include/apl/engine/rootcontext.h | 42 +- aplcore/include/apl/engine/rootcontextdata.h | 17 +- .../include/apl/extension/extensionclient.h | 6 +- .../apl/extension/extensioncomponent.h | 2 +- aplcore/include/apl/graphic/graphic.h | 17 +- aplcore/include/apl/graphic/graphicelement.h | 41 +- .../apl/graphic/graphicelementcontainer.h | 8 +- .../include/apl/graphic/graphicelementgroup.h | 11 +- .../include/apl/graphic/graphicelementpath.h | 13 +- .../include/apl/graphic/graphicelementtext.h | 11 +- aplcore/include/apl/media/mediaobject.h | 3 +- aplcore/include/apl/media/mediaplayer.h | 13 +- aplcore/include/apl/media/mediatrack.h | 61 + aplcore/include/apl/primitives/filter.h | 5 +- aplcore/include/apl/primitives/header.h | 29 + aplcore/include/apl/primitives/mediasource.h | 10 +- .../apl/primitives/symbolreferencemap.h | 2 +- aplcore/include/apl/primitives/timegrammar.h | 8 +- aplcore/include/apl/primitives/urlrequest.h | 8 +- aplcore/include/apl/scenegraph/common.h | 2 + .../include/apl/scenegraph/graphicfragment.h | 105 ++ aplcore/include/apl/scenegraph/layer.h | 45 +- .../include/apl/scenegraph/modifiednodelist.h | 46 - aplcore/include/apl/scenegraph/node.h | 80 +- .../apl/scenegraph/scenegraphupdates.h | 11 +- aplcore/include/apl/scenegraph/textlayout.h | 7 + .../apl/touch/gestures/scrollgesture.h | 2 +- aplcore/include/apl/utils/identifier.h | 55 + .../include/apl/utils/synchronizedweakcache.h | 109 ++ aplcore/src/action/CMakeLists.txt | 6 +- aplcore/src/action/animatedscrollaction.cpp | 2 - aplcore/src/action/arrayaction.cpp | 5 +- aplcore/src/action/autopageaction.cpp | 5 +- aplcore/src/action/controlmediaaction.cpp | 1 + aplcore/src/action/extensioneventaction.cpp | 1 + aplcore/src/action/playmediaaction.cpp | 1 + aplcore/src/action/scrolltoaction.cpp | 3 +- aplcore/src/action/sequentialaction.cpp | 7 +- aplcore/src/action/setpageaction.cpp | 3 +- aplcore/src/action/speakitemaction.cpp | 10 +- aplcore/src/animation/coreeasing.cpp | 30 +- aplcore/src/animation/easing.cpp | 27 +- aplcore/src/animation/easingapproximation.cpp | 41 +- aplcore/src/command/CMakeLists.txt | 2 +- aplcore/src/command/corecommand.cpp | 23 +- aplcore/src/command/sendeventcommand.cpp | 9 +- aplcore/src/component/CMakeLists.txt | 3 +- aplcore/src/component/actionablecomponent.cpp | 6 +- aplcore/src/component/component.cpp | 1 + aplcore/src/component/containercomponent.cpp | 3 +- aplcore/src/component/corecomponent.cpp | 21 +- aplcore/src/component/edittextcomponent.cpp | 11 +- aplcore/src/component/framecomponent.cpp | 4 +- .../src/component/gridsequencecomponent.cpp | 5 +- aplcore/src/component/imagecomponent.cpp | 5 +- aplcore/src/component/mediacomponenttrait.cpp | 1 + .../multichildscrollablecomponent.cpp | 3 +- aplcore/src/component/pagercomponent.cpp | 6 +- aplcore/src/component/scrollablecomponent.cpp | 7 +- aplcore/src/component/scrollviewcomponent.cpp | 3 +- aplcore/src/component/selector.cpp | 472 +++++++ aplcore/src/component/sequencecomponent.cpp | 3 +- aplcore/src/component/textcomponent.cpp | 5 +- aplcore/src/component/textmeasurement.cpp | 4 + aplcore/src/component/touchablecomponent.cpp | 7 +- .../src/component/touchwrappercomponent.cpp | 3 +- .../src/component/vectorgraphiccomponent.cpp | 84 +- aplcore/src/component/videocomponent.cpp | 8 +- aplcore/src/component/yogaproperties.cpp | 1 + aplcore/src/content/CMakeLists.txt | 2 + aplcore/src/content/aplversion.cpp | 3 +- aplcore/src/content/content.cpp | 27 +- .../content/extensioncommanddefinition.cpp | 42 + .../src/content/extensionfilterdefinition.cpp | 32 + aplcore/src/content/rootconfig.cpp | 12 +- aplcore/src/content/viewport.cpp | 3 +- aplcore/src/datagrammar/bytecodeoptimizer.cpp | 2 +- aplcore/src/datagrammar/functions.cpp | 4 +- .../dynamiclistdatasourceprovider.cpp | 5 + aplcore/src/engine/CMakeLists.txt | 6 +- aplcore/src/engine/binding.cpp | 3 +- aplcore/src/engine/builder.cpp | 33 +- aplcore/src/engine/context.cpp | 63 +- aplcore/src/engine/evaluate.cpp | 1 - aplcore/src/engine/event.cpp | 3 +- aplcore/src/engine/info.cpp | 3 +- aplcore/src/engine/keyboardmanager.cpp | 4 +- aplcore/src/engine/layoutmanager.cpp | 1 + aplcore/src/engine/parameterarray.cpp | 1 - aplcore/src/engine/propdef.cpp | 127 +- aplcore/src/engine/properties.cpp | 56 +- aplcore/src/engine/resources.cpp | 5 +- aplcore/src/engine/rootcontext.cpp | 61 +- aplcore/src/engine/rootcontextdata.cpp | 16 +- aplcore/src/extension/extensionclient.cpp | 9 +- aplcore/src/extension/extensioncomponent.cpp | 3 +- aplcore/src/extension/extensionmanager.cpp | 3 +- aplcore/src/extension/extensionmediator.cpp | 5 +- aplcore/src/graphic/graphic.cpp | 68 +- aplcore/src/graphic/graphicbuilder.cpp | 10 +- aplcore/src/graphic/graphicelement.cpp | 140 +- .../src/graphic/graphicelementcontainer.cpp | 35 +- aplcore/src/graphic/graphicelementgroup.cpp | 160 ++- aplcore/src/graphic/graphicelementpath.cpp | 133 +- aplcore/src/graphic/graphicelementtext.cpp | 130 +- aplcore/src/graphic/graphicfilter.cpp | 2 + aplcore/src/livedata/layoutrebuilder.cpp | 5 +- aplcore/src/media/CMakeLists.txt | 1 + aplcore/src/media/coremediamanager.cpp | 6 +- aplcore/src/media/mediatrack.cpp | 24 + aplcore/src/media/mediautils.cpp | 23 +- aplcore/src/primitives/CMakeLists.txt | 8 +- aplcore/src/primitives/filter.cpp | 10 +- aplcore/src/primitives/functions.cpp | 7 +- aplcore/src/primitives/mediasource.cpp | 50 +- aplcore/src/primitives/rect.cpp | 4 +- aplcore/src/primitives/styledtext.cpp | 7 +- aplcore/src/primitives/styledtextstate.cpp | 8 +- aplcore/src/primitives/timegrammar.cpp | 2 + aplcore/src/primitives/transform2d.cpp | 2 +- aplcore/src/primitives/unicode.cpp | 2 +- aplcore/src/scenegraph/CMakeLists.txt | 2 +- aplcore/src/scenegraph/builder.cpp | 3 +- aplcore/src/scenegraph/graphicfragment.cpp | 330 +++++ aplcore/src/scenegraph/layer.cpp | 59 +- aplcore/src/scenegraph/modifiednodelist.cpp | 62 - aplcore/src/scenegraph/node.cpp | 145 +-- aplcore/src/scenegraph/pathbounds.cpp | 87 +- aplcore/src/scenegraph/pathparser.cpp | 2 +- aplcore/src/scenegraph/scenegraphupdates.cpp | 43 +- aplcore/src/touch/gesture.cpp | 7 +- .../src/touch/gestures/doublepressgesture.cpp | 6 +- aplcore/src/touch/gestures/flinggesture.cpp | 8 +- .../src/touch/gestures/longpressgesture.cpp | 6 +- .../src/touch/gestures/pagerflinggesture.cpp | 10 +- aplcore/src/touch/gestures/scrollgesture.cpp | 8 +- .../src/touch/gestures/swipeawaygesture.cpp | 16 +- aplcore/src/touch/gestures/tapgesture.cpp | 5 +- aplcore/src/touch/pointermanager.cpp | 3 +- aplcore/src/touch/utils/autoscroller.cpp | 4 +- aplcore/src/touch/utils/pagemovehandler.cpp | 2 +- aplcore/src/utils/stickyfunctions.cpp | 1 + bin/apl-header-inclusion-validation.sh | 3 +- doc/scenegraph.md | 2 +- unit/CMakeLists.txt | 6 +- .../unittest_easing_approximation.cpp | 44 +- .../unittest_sequencer_preservation.cpp | 52 + unit/component/CMakeLists.txt | 1 + .../unittest_edit_text_component.cpp | 1 + unit/component/unittest_properties.cpp | 16 +- unit/component/unittest_selector.cpp | 523 ++++++++ unit/datagrammar/unittest_grammar.cpp | 14 +- unit/datasource/unittest_dynamicindexlist.cpp | 1 + unit/engine/CMakeLists.txt | 1 + unit/engine/unittest_builder.cpp | 90 +- unit/engine/unittest_builder_bind.cpp | 79 ++ unit/engine/unittest_context.cpp | 33 +- unit/engine/unittest_dependant.cpp | 3 +- unit/engine/unittest_hover.cpp | 19 +- unit/engine/unittest_keyboard_manager.cpp | 7 +- unit/engine/unittest_layouts.cpp | 790 ++++++------ unit/engine/unittest_queue_event_manager.cpp | 88 ++ unit/engine/unittest_resources.cpp | 2 + .../extension/unittest_extension_mediator.cpp | 6 +- unit/graphic/unittest_graphic_bind.cpp | 115 ++ unit/graphic/unittest_graphic_data.cpp | 77 ++ unit/media/CMakeLists.txt | 1 + unit/media/unittest_fake_player.cpp | 24 +- unit/media/unittest_media_manager.cpp | 38 +- unit/media/unittest_media_utils.cpp | 101 ++ unit/primitives/unittest_transform.cpp | 6 +- unit/scenegraph/CMakeLists.txt | 1 + unit/scenegraph/test_sg.cpp | 47 +- unit/scenegraph/test_sg.h | 31 +- unit/scenegraph/unittest_sg_graphic.cpp | 1016 ++++++++++----- .../unittest_sg_graphic_component.cpp | 257 +++- .../scenegraph/unittest_sg_graphic_layers.cpp | 1128 +++++++++++++++++ unit/scenegraph/unittest_sg_layer.cpp | 46 +- unit/scenegraph/unittest_sg_node.cpp | 8 +- unit/scenegraph/unittest_sg_nodebounds.cpp | 54 +- unit/scenegraph/unittest_sg_path.cpp | 11 +- unit/scenegraph/unittest_sg_pathbounds.cpp | 31 + unit/scenegraph/unittest_sg_pathparser.cpp | 30 +- unit/test_comparisons.h | 116 ++ unit/testeventloop.cpp | 1 + unit/testeventloop.h | 83 +- unit/utils/CMakeLists.txt | 3 +- unit/utils/unittest_synchronizedweakcache.cpp | 96 ++ unit/utils/unittest_weakcache.cpp | 41 + 234 files changed, 7564 insertions(+), 2050 deletions(-) create mode 100644 aplcore/include/apl/component/selector.h create mode 100644 aplcore/include/apl/engine/eventmanager.h create mode 100644 aplcore/include/apl/engine/eventpublisher.h create mode 100644 aplcore/include/apl/engine/queueeventmanager.h create mode 100644 aplcore/include/apl/media/mediatrack.h create mode 100644 aplcore/include/apl/primitives/header.h create mode 100644 aplcore/include/apl/scenegraph/graphicfragment.h delete mode 100644 aplcore/include/apl/scenegraph/modifiednodelist.h create mode 100644 aplcore/include/apl/utils/identifier.h create mode 100644 aplcore/include/apl/utils/synchronizedweakcache.h create mode 100644 aplcore/src/component/selector.cpp create mode 100644 aplcore/src/content/extensioncommanddefinition.cpp create mode 100644 aplcore/src/content/extensionfilterdefinition.cpp create mode 100644 aplcore/src/media/mediatrack.cpp create mode 100644 aplcore/src/scenegraph/graphicfragment.cpp delete mode 100644 aplcore/src/scenegraph/modifiednodelist.cpp create mode 100644 unit/component/unittest_selector.cpp create mode 100644 unit/engine/unittest_queue_event_manager.cpp create mode 100644 unit/media/unittest_media_utils.cpp create mode 100644 unit/scenegraph/unittest_sg_graphic_layers.cpp create mode 100644 unit/test_comparisons.h create mode 100644 unit/utils/unittest_synchronizedweakcache.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 372f912..da8cdce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [2023.1] + +### Added + +- Three Porter-Duff operations to the Blend filter +- Selector syntax for choosing the target of a command +- TextTrack property for closed captions + +### Changed + +- Bug fixes. + ## [2022.2.1] ### Changed diff --git a/README.md b/README.md index 551586d..5e75167 100644 --- a/README.md +++ b/README.md @@ -182,8 +182,31 @@ following: apl-check-core ``` +Mac Note: The CMake build generates the file `CMakeCache.txt` which contains paths to the system SDKROOT and build tools. +New installations of XCode or Mac Command Line tools often modify the "Active Developer Directory" changing the location +of the SDK. This may result in build failures. Most often this issue can be characterized by a failure to find the +`SYSROOT` path. +An example of the path from `CMakeCache.txt` may look like: +`CMAKE_OSX_SYSROOT:PATH=/Library/Developer/CommandLineTools/SDKs/MacOSX13.0.sdk` +The "Active Developer Directory" can be identified as follows: +```shell +> xcode-select -p +/Library/Developer/CommandLineTools +``` + + +To resolve, any of the following options are possible +- To use the `SYSROOT` derived from the new "Active Developer Directory": +Delete the build folder and regenerate the build. A "clean" build is not sufficient for `CmakeCache.txt` +to be recreated. +- To modify the "Active Developer Directory" to the desired SDK and tools paths +Use `xcode-select` to set the new path or reset to the default. +- To specify the explicit location of the SDK and tools rather than use the "Active Developer Directory" +Modify the `CMakeCache.txt` SDK and tool paths. This option is not recommended, as it must be repeated any time the +build folder is absent. + # Building APL Core + Tests (Windows) -To build `apl.lib` and tests in in a Windows or UWP environment, do the +To build `apl.lib` and tests in a Windows or UWP environment, do the following: ``` diff --git a/aplcore/CMakeLists.txt b/aplcore/CMakeLists.txt index 7a11bae..3e706bd 100644 --- a/aplcore/CMakeLists.txt +++ b/aplcore/CMakeLists.txt @@ -83,12 +83,12 @@ add_subdirectory(src/livedata) add_subdirectory(src/media) add_subdirectory(src/primitives) add_subdirectory(src/scaling) -if(ENABLE_SCENEGRAPH) - add_subdirectory(src/scenegraph) -endif(ENABLE_SCENEGRAPH) add_subdirectory(src/time) add_subdirectory(src/touch) add_subdirectory(src/utils) +if(ENABLE_SCENEGRAPH) + add_subdirectory(src/scenegraph) +endif(ENABLE_SCENEGRAPH) set( PUBLIC_HEADER_LIST diff --git a/aplcore/include/apl/action/arrayaction.h b/aplcore/include/apl/action/arrayaction.h index 4dc9e2f..22360e8 100644 --- a/aplcore/include/apl/action/arrayaction.h +++ b/aplcore/include/apl/action/arrayaction.h @@ -17,8 +17,8 @@ #define _APL_ACTION_ARRAY_ACTION_H #include "apl/action/action.h" -#include "apl/command/command.h" #include "apl/command/arraycommand.h" +#include "apl/command/command.h" namespace apl { @@ -41,7 +41,7 @@ class ArrayAction : public Action { const std::shared_ptr mCommand; const bool mFastMode; - int mNextIndex; + size_t mNextIndex; CommandPtr mCurrentCommand; ActionPtr mCurrentAction; diff --git a/aplcore/include/apl/action/sequentialaction.h b/aplcore/include/apl/action/sequentialaction.h index 2b78a47..f61c711 100644 --- a/aplcore/include/apl/action/sequentialaction.h +++ b/aplcore/include/apl/action/sequentialaction.h @@ -43,7 +43,7 @@ class SequentialAction : public Action { bool mFastMode; bool mStateFinally; - int mNextIndex; + size_t mNextIndex; int mRepeatCounter; CommandPtr mCurrentCommand; diff --git a/aplcore/include/apl/animation/easingapproximation.h b/aplcore/include/apl/animation/easingapproximation.h index 084c984..9766798 100644 --- a/aplcore/include/apl/animation/easingapproximation.h +++ b/aplcore/include/apl/animation/easingapproximation.h @@ -29,7 +29,7 @@ class EasingApproximation : public EasingSegment::Data, public: /** - * Create an easing curve approximation. + * Create an easing curve approximation or return an equivalent one from cache if possible. * @param dof The number of entries in the start, tout, tin, and end arrays * @param start An array of starting values * @param tout An array of tangent-out values (relative to the start value) @@ -38,12 +38,12 @@ class EasingApproximation : public EasingSegment::Data, * @param blockCount The total number of subsegments to create. * @return A pointer to the easing approximation. */ - static std::shared_ptr create(int dof, - const float *start, - const float *tout, - const float *tin, - const float *end, - int blockCount); + static std::shared_ptr getOrCreate(int dof, + const float *start, + const float *tout, + const float *tin, + const float *end, + int blockCount); // Private constructor - use the "create" method instead. EasingApproximation(int dof, @@ -53,6 +53,8 @@ class EasingApproximation : public EasingSegment::Data, mData(std::move(data)), mCumulative(std::move(cumulative)) {} + ~EasingApproximation(); + /** * Calculate a position along the easing curve. * @param percentage The percentage of the length of the path. diff --git a/aplcore/include/apl/animation/easinggrammar.h b/aplcore/include/apl/animation/easinggrammar.h index cf60e5d..25b8965 100644 --- a/aplcore/include/apl/animation/easinggrammar.h +++ b/aplcore/include/apl/animation/easinggrammar.h @@ -18,7 +18,6 @@ #include "apl/animation/coreeasing.h" #include "apl/datagrammar/grammarpolyfill.h" -#include "apl/utils/log.h" #include "apl/utils/stringfunctions.h" namespace apl { diff --git a/aplcore/include/apl/apl.h b/aplcore/include/apl/apl.h index 18bd5e0..76204d3 100644 --- a/aplcore/include/apl/apl.h +++ b/aplcore/include/apl/apl.h @@ -24,14 +24,13 @@ #include "rapidjson/document.h" -#include "apl/apl_config.h" +#include "apl/buildTimeConstants.h" #include "apl/common.h" #include "apl/action/action.h" #include "apl/audio/audioplayer.h" #include "apl/audio/audioplayerfactory.h" #include "apl/audio/speechmark.h" -#include "apl/buildTimeConstants.h" #include "apl/component/component.h" #include "apl/component/textmeasurement.h" #include "apl/content/configurationchange.h" @@ -69,6 +68,10 @@ #include "apl/primitives/styledtext.h" #include "apl/primitives/transform2d.h" #include "apl/scaling/metricstransform.h" +#include "apl/touch/pointerevent.h" +#include "apl/utils/localemethods.h" +#include "apl/utils/log.h" +#include "apl/utils/session.h" #ifdef SCENEGRAPH #include "apl/scenegraph/accessibility.h" #include "apl/scenegraph/edittextconfig.h" @@ -87,10 +90,6 @@ #include "apl/scenegraph/textmeasurement.h" #include "apl/scenegraph/textproperties.h" #endif // SCENEGRAPH -#include "apl/touch/pointerevent.h" -#include "apl/utils/localemethods.h" -#include "apl/utils/log.h" -#include "apl/utils/session.h" #ifdef ALEXAEXTENSIONS #include "apl/extension/extensionmediator.h" diff --git a/aplcore/include/apl/audio/audioplayer.h b/aplcore/include/apl/audio/audioplayer.h index d205258..f105193 100644 --- a/aplcore/include/apl/audio/audioplayer.h +++ b/aplcore/include/apl/audio/audioplayer.h @@ -21,9 +21,11 @@ #include #include "apl/common.h" + #include "apl/audio/audiostate.h" #include "apl/audio/speechmark.h" -#include "apl/media/mediaplayer.h" +#include "apl/engine/event.h" +#include "apl/media/mediatrack.h" namespace apl { diff --git a/aplcore/include/apl/command/corecommand.h b/aplcore/include/apl/command/corecommand.h index 7b6d595..56bce08 100644 --- a/aplcore/include/apl/command/corecommand.h +++ b/aplcore/include/apl/command/corecommand.h @@ -17,12 +17,12 @@ #define _APL_COMMAND_CORE_COMMAND_H #include "apl/command/command.h" -#include "apl/utils/bimap.h" -#include "apl/primitives/objectbag.h" -#include "apl/engine/context.h" #include "apl/component/corecomponent.h" +#include "apl/engine/context.h" #include "apl/engine/event.h" #include "apl/engine/propdef.h" +#include "apl/primitives/objectbag.h" +#include "apl/utils/bimap.h" namespace apl { @@ -115,6 +115,7 @@ class CoreCommand : public Command { std::string mTargetId; rapidjson::Document mFrozenEventContext; bool mFrozen = false; + bool mMissingTargetId = false; }; } // namespace apl diff --git a/aplcore/include/apl/common.h b/aplcore/include/apl/common.h index 5b70dcd..7c51621 100644 --- a/aplcore/include/apl/common.h +++ b/aplcore/include/apl/common.h @@ -20,6 +20,8 @@ #include #include +#include "apl/apl_config.h" + namespace apl { /** @@ -57,13 +59,14 @@ class CoreComponent; class DataSource; class DataSourceProvider; class Easing; +class EventManager; class ExtensionClient; class ExtensionCommandDefinition; class ExtensionComponent; class ExtensionMediator; +class Graphic; class GraphicContent; class GraphicElement; -class Graphic; class GraphicPattern; class LiveArray; class LiveMap; @@ -84,17 +87,17 @@ class Timers; class UIDObject; using ActionPtr = std::shared_ptr; -using AudioPlayerPtr = std::shared_ptr; using AudioPlayerFactoryPtr = std::shared_ptr; +using AudioPlayerPtr = std::shared_ptr; using CommandPtr = std::shared_ptr; -using ConstCommandPtr = std::shared_ptr; using ComponentPtr = std::shared_ptr; +using ConstCommandPtr = std::shared_ptr; +using ConstContextPtr = std::shared_ptr; using ContentPtr = std::shared_ptr; using ContextPtr = std::shared_ptr; -using ConstContextPtr = std::shared_ptr; using CoreComponentPtr = std::shared_ptr; -using DataSourcePtr = std::shared_ptr; using DataSourceProviderPtr = std::shared_ptr; +using DataSourcePtr = std::shared_ptr; using EasingPtr = std::shared_ptr; using ExtensionClientPtr = std::shared_ptr; using ExtensionCommandDefinitionPtr = std::shared_ptr; @@ -102,8 +105,8 @@ using ExtensionComponentPtr = std::shared_ptr; using ExtensionMediatorPtr = std::shared_ptr; using GraphicContentPtr = std::shared_ptr; using GraphicElementPtr = std::shared_ptr; -using GraphicPtr = std::shared_ptr; using GraphicPatternPtr = std::shared_ptr; +using GraphicPtr = std::shared_ptr; using LiveArrayPtr = std::shared_ptr; using LiveMapPtr = std::shared_ptr; using LiveObjectPtr = std::shared_ptr; @@ -122,8 +125,8 @@ using TextMeasurementPtr = std::shared_ptr; using TimersPtr = std::shared_ptr; // Convenience templates for creating sets of weak and strong pointers -template using WeakPtrSet = std::set, std::owner_less>>; template using SharedPtrSet = std::set, std::owner_less>>; +template using WeakPtrSet = std::set, std::owner_less>>; } // namespace apl diff --git a/aplcore/include/apl/component/component.h b/aplcore/include/apl/component/component.h index 0a644d7..be32689 100644 --- a/aplcore/include/apl/component/component.h +++ b/aplcore/include/apl/component/component.h @@ -21,16 +21,17 @@ #include #include "apl/common.h" -#include "componentproperties.h" -#include "apl/utils/counter.h" + +#include "apl/component/componentproperties.h" #include "apl/engine/propertymap.h" +#include "apl/engine/state.h" #include "apl/engine/uidobject.h" #include "apl/primitives/rect.h" -#include "apl/engine/state.h" +#include "apl/utils/counter.h" #include "apl/utils/deprecated.h" #include "apl/utils/noncopyable.h" -#include "apl/utils/visitor.h" #include "apl/utils/userdata.h" +#include "apl/utils/visitor.h" namespace apl { @@ -295,8 +296,8 @@ class Component : public UIDObject, * @param error Error message used when the state is kResourceError */ virtual void updateResourceState(const ExtensionComponentResourceState& state, - int errorCode = 0, - const std::string& error = ""); + int errorCode = 0, + const std::string& error = ""); /* * @return The number of children displayed. diff --git a/aplcore/include/apl/component/componentpropdef.h b/aplcore/include/apl/component/componentpropdef.h index 9859531..66b5979 100644 --- a/aplcore/include/apl/component/componentpropdef.h +++ b/aplcore/include/apl/component/componentpropdef.h @@ -16,9 +16,11 @@ #ifndef _APL_COMPONENT_PROP_DEF_H #define _APL_COMPONENT_PROP_DEF_H -#include "component.h" +#include "apl/component/component.h" #include "apl/engine/propdef.h" +#include + namespace apl { using Trigger = void (*)(Component&); diff --git a/aplcore/include/apl/component/containercomponent.h b/aplcore/include/apl/component/containercomponent.h index d414855..4b69e90 100644 --- a/aplcore/include/apl/component/containercomponent.h +++ b/aplcore/include/apl/component/containercomponent.h @@ -16,7 +16,7 @@ #ifndef _APL_CONTAINER_COMPONENT_H #define _APL_CONTAINER_COMPONENT_H -#include "corecomponent.h" +#include "apl/component/corecomponent.h" namespace apl { diff --git a/aplcore/include/apl/component/corecomponent.h b/aplcore/include/apl/component/corecomponent.h index b38b38e..fd72c41 100644 --- a/aplcore/include/apl/component/corecomponent.h +++ b/aplcore/include/apl/component/corecomponent.h @@ -18,22 +18,24 @@ #include #include - #include + #include "apl/apl_config.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/common.h" -#endif // SCENEGRAPH + #include "apl/component/component.h" #include "apl/component/textmeasurement.h" -#include "apl/engine/properties.h" #include "apl/engine/context.h" +#include "apl/engine/properties.h" #include "apl/engine/recalculatetarget.h" #include "apl/focus/focusdirection.h" #include "apl/primitives/keyboard.h" #include "apl/primitives/size.h" #include "apl/primitives/transform2d.h" +#ifdef SCENEGRAPH +#include "apl/scenegraph/common.h" +#endif // SCENEGRAPH + namespace apl { class ComponentPropDef; @@ -644,10 +646,10 @@ class CoreComponent : public Component, bool isDisplayable() const; /** - * Calculate real opacity of component. - * @param parentRealOpacity parent component real opacity. - * @return component cumulative opacity. - */ + * Calculate real opacity of component. + * @param parentRealOpacity parent component real opacity. + * @return component cumulative opacity. + */ float calculateRealOpacity(float parentRealOpacity) const; /** diff --git a/aplcore/include/apl/component/edittextcomponent.h b/aplcore/include/apl/component/edittextcomponent.h index 9bcacea..afbaed1 100644 --- a/aplcore/include/apl/component/edittextcomponent.h +++ b/aplcore/include/apl/component/edittextcomponent.h @@ -21,8 +21,8 @@ #include "apl/apl_config.h" #include "apl/component/actionablecomponent.h" #ifdef SCENEGRAPH -#include "apl/scenegraph/textproperties.h" #include "apl/scenegraph/edittext.h" +#include "apl/scenegraph/textproperties.h" #include "apl/utils/principal_ptr.h" #endif // SCENEGRAPH diff --git a/aplcore/include/apl/component/imagecomponent.h b/aplcore/include/apl/component/imagecomponent.h index 3ce4f1f..e9bd1b0 100644 --- a/aplcore/include/apl/component/imagecomponent.h +++ b/aplcore/include/apl/component/imagecomponent.h @@ -16,7 +16,7 @@ #ifndef _APL_IMAGE_COMPONENT_H #define _APL_IMAGE_COMPONENT_H -#include "mediacomponenttrait.h" +#include "apl/component/mediacomponenttrait.h" namespace apl { diff --git a/aplcore/include/apl/component/mediacomponenttrait.h b/aplcore/include/apl/component/mediacomponenttrait.h index 02720dd..70bd243 100644 --- a/aplcore/include/apl/component/mediacomponenttrait.h +++ b/aplcore/include/apl/component/mediacomponenttrait.h @@ -19,6 +19,7 @@ #include "apl/component/componenttrait.h" #include "apl/engine/event.h" #include "apl/media/mediaobject.h" +#include "apl/primitives/urlrequest.h" namespace apl { diff --git a/aplcore/include/apl/component/scrollviewcomponent.h b/aplcore/include/apl/component/scrollviewcomponent.h index 9cb4e3b..b2b0df8 100644 --- a/aplcore/include/apl/component/scrollviewcomponent.h +++ b/aplcore/include/apl/component/scrollviewcomponent.h @@ -16,7 +16,7 @@ #ifndef _APL_SCROLL_VIEW_COMPONENT_H #define _APL_SCROLL_VIEW_COMPONENT_H -#include "scrollablecomponent.h" +#include "apl/component/scrollablecomponent.h" namespace apl { diff --git a/aplcore/include/apl/component/selector.h b/aplcore/include/apl/component/selector.h new file mode 100644 index 0000000..cf87b5d --- /dev/null +++ b/aplcore/include/apl/component/selector.h @@ -0,0 +1,76 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef _APL_SELECTOR_H +#define _APL_SELECTOR_H + +#include "apl/common.h" + +namespace apl { + +/** + * Represent a selector that identifies and returns a component (or nullptr) from a hierarchy + * of components + * + * This grammar parses component selectors and returns a Selector object. The grammar + * is defined as: + * + * componentId ::= element? modifier* + * element ::= uid | id | ":source" | ":root" + * modifier ::= modifierType "(" arg? ")" + * modifierType ::= ":parent" | ":child" | ":find": | ":next" | ":previous" + * arg ::= number | "id=" id | "type=" type + * uid ::= ":" [0-9]+ + * id ::= [-_a-zA-Z0-9]+ + * number ::= "0" | "-"? [1-9][0-9]* + * type ::= STRING + * + * Note that the 'id' syntax is more permissive than what is allowed in the APL specification. + * This is to support backwards compatibility. + * + * Valid examples of this grammar: + * + * FOO # The first component with id=FOO + * :1003 # The component with unique ID ":1003" + * :source # The component that issued the command + * :root # The top of the component hierarchy + * :source:child(2) # The third child of the source component + * FOO:child(-1) # The last child of the FOO component + * FOO:parent() # The parent of FOO + * FOO:child(id=BAR) # The first direct child of FOO where id=BAR + * FOO:find(id=BAR) # The first descendant of FOO where id=BAR + * FOO:child(type=Text) # The first direct child of FOO where type=Text + * FOO:parent:child(id=BAR) # The first sibling of FOO where id=BAR + * FOO:next(id=BAR) # The first sibling searching forwards of FOO where id=Bar + * FOO:parent(2) # The grandparent of FOO + * FOO:previous(1) # The sibling immediately before FOO + */ +class Selector { +public: + /** + * Parse a 'componentId' string and return the matching component + * @param string The string to parse + * @param context The current data-binding context + * @param source The starting component (may be nullptr) + * @return The target component (may be nullptr) + */ + static CoreComponentPtr resolve(const std::string& string, + const ContextPtr& context, + const CoreComponentPtr& source=nullptr); +}; + +} // namespace apl + +#endif diff --git a/aplcore/include/apl/component/yogaproperties.h b/aplcore/include/apl/component/yogaproperties.h index 8c1b6fe..854174f 100644 --- a/aplcore/include/apl/component/yogaproperties.h +++ b/aplcore/include/apl/component/yogaproperties.h @@ -18,7 +18,7 @@ #include -#include "componentproperties.h" +#include "apl/component/componentproperties.h" namespace apl { diff --git a/aplcore/include/apl/content/aplversion.h b/aplcore/include/apl/content/aplversion.h index 48a4d25..ac383e6 100644 --- a/aplcore/include/apl/content/aplversion.h +++ b/aplcore/include/apl/content/aplversion.h @@ -36,6 +36,7 @@ class APLVersion { kAPLVersion19 = 0x1U << 9, /// Support version 1.9 kAPLVersion20221 = 0x1U << 10, /// Support version 2022.1 kAPLVersion20222 = 0x1U << 11, /// Support version 2022.2 + kAPLVersion20231 = 0x1U << 12, /// Support version 2023.1 kAPLVersion10to11 = kAPLVersion10 | kAPLVersion11, /// Convenience ranges from 1.0 to latest, kAPLVersion10to12 = kAPLVersion10to11 | kAPLVersion12, kAPLVersion10to13 = kAPLVersion10to12 | kAPLVersion13, @@ -47,9 +48,10 @@ class APLVersion { kAPLVersion10to19 = kAPLVersion10to18 | kAPLVersion19, kAPLVersion10to20221 = kAPLVersion10to19 | kAPLVersion20221, kAPLVersion20221to20222 = kAPLVersion10to20221 | kAPLVersion20222, - kAPLVersionLatest = kAPLVersion20221to20222, /// Support the most recent engine version - kAPLVersionDefault = kAPLVersion20221to20222, /// Default value - kAPLVersionReported = kAPLVersion20222, /// Default reported version + kAPLVersion20222to20231 = kAPLVersion20221to20222 | kAPLVersion20231, + kAPLVersionLatest = kAPLVersion20222to20231, /// Support the most recent engine version + kAPLVersionDefault = kAPLVersion20222to20231, /// Default value + kAPLVersionReported = kAPLVersion20231, /// Default reported version kAPLVersionAny = 0xffffffff, /// Support any versions in the list }; diff --git a/aplcore/include/apl/content/content.h b/aplcore/include/apl/content/content.h index 6b37d99..5fe1a5c 100644 --- a/aplcore/include/apl/content/content.h +++ b/aplcore/include/apl/content/content.h @@ -19,12 +19,11 @@ #include #include -#include "apl/apl_config.h" #include "apl/common.h" -#include "apl/utils/counter.h" #include "apl/content/package.h" #include "apl/content/settings.h" #include "apl/engine/properties.h" +#include "apl/utils/counter.h" #include "apl/utils/session.h" namespace apl { diff --git a/aplcore/include/apl/content/extensioncommanddefinition.h b/aplcore/include/apl/content/extensioncommanddefinition.h index 49e9fc5..e83c68f 100644 --- a/aplcore/include/apl/content/extensioncommanddefinition.h +++ b/aplcore/include/apl/content/extensioncommanddefinition.h @@ -18,8 +18,6 @@ #include "apl/content/extensionproperty.h" -#include "apl/utils/log.h" - namespace apl { /** @@ -121,13 +119,7 @@ class ExtensionCommandDefinition { * @param prop Extension property definition. * @return This object for chaining. */ - ExtensionCommandDefinition& property(const std::string& name, ExtensionProperty&& prop) { - if (name == "when" || name == "type") - LOG(LogLevel::kWarn) << "Unable to register property '" << name << "' in custom command " << mName; - else - mPropertyMap.emplace(name, std::move(prop)); - return *this; - } + ExtensionCommandDefinition& property(const std::string& name, ExtensionProperty&& prop); /** * Add a named array-ified property. The property will be converted into an array of values. The names "when" @@ -136,13 +128,7 @@ class ExtensionCommandDefinition { * @param required If true and the property is not provided, the command will not execute. * @return This object for chaining. */ - ExtensionCommandDefinition& arrayProperty(std::string property, bool required) { - if (property == "when" || property == "type") - LOG(LogLevel::kWarn) << "Unable to register array-ified property '" << property << "' in custom command " << mName; - else - mPropertyMap.emplace(property, ExtensionProperty{kBindingTypeArray, Object::EMPTY_ARRAY(), required}); - return *this; - } + ExtensionCommandDefinition& arrayProperty(std::string property, bool required); /** * @return The URI of the extension diff --git a/aplcore/include/apl/content/extensionfilterdefinition.h b/aplcore/include/apl/content/extensionfilterdefinition.h index ab782d2..fd43e02 100644 --- a/aplcore/include/apl/content/extensionfilterdefinition.h +++ b/aplcore/include/apl/content/extensionfilterdefinition.h @@ -16,8 +16,8 @@ #ifndef _APL_EXTENSION_FILTER_DEFINITION_H #define _APL_EXTENSION_FILTER_DEFINITION_H -#include "apl/primitives/object.h" #include "apl/engine/binding.h" +#include "apl/primitives/object.h" namespace apl { @@ -107,13 +107,7 @@ class ExtensionFilterDefinition { * @param prop Extension property definition. * @return This object for chaining. */ - ExtensionFilterDefinition& property(const std::string& name, Property&& prop) { - if (name == "when" || name == "type" || name == "source" || name == "destination") - LOG(LogLevel::kWarn) << "Unable to register property '" << name << "' in custom filter extension " << mName; - else - mPropertyMap.emplace(name, std::move(prop)); - return *this; - } + ExtensionFilterDefinition& property(const std::string& name, Property&& prop); /** * @return The URI of the extension diff --git a/aplcore/include/apl/content/rootconfig.h b/aplcore/include/apl/content/rootconfig.h index a64daae..17828b3 100644 --- a/aplcore/include/apl/content/rootconfig.h +++ b/aplcore/include/apl/content/rootconfig.h @@ -20,11 +20,8 @@ #include #include -#include "apl/apl_config.h" #include "apl/common.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/common.h" -#endif // SCENEGRAPH + #include "apl/component/componentproperties.h" #include "apl/content/aplversion.h" #include "apl/content/extensioncommanddefinition.h" @@ -39,6 +36,10 @@ #include "apl/utils/deprecated.h" #include "apl/utils/stringfunctions.h" +#ifdef SCENEGRAPH +#include "apl/scenegraph/common.h" +#endif // SCENEGRAPH + #ifdef ALEXAEXTENSIONS #include #endif @@ -96,6 +97,8 @@ class RootConfig { kExperimentalFeatureFocusEditTextOnTap, /// Send event when core assumes keyboard input is required kExperimentalFeatureRequestKeyboard, + /// AVG should use layers for parameterized elements + kExperimentalFeatureGraphicLayers, }; /** @@ -903,12 +906,12 @@ class RootConfig { /** * @return The configured media manager object */ - MediaManagerPtr getMediaManager() const { return mMediaManager; } + MediaManagerPtr getMediaManager() const { return mMediaManager; } - /** - * @return The configured media player factory - */ - MediaPlayerFactoryPtr getMediaPlayerFactory() const { return mMediaPlayerFactory; } + /** + * @return The configured media player factory + */ + MediaPlayerFactoryPtr getMediaPlayerFactory() const { return mMediaPlayerFactory; } /** * @return The configured audio player factory diff --git a/aplcore/include/apl/datagrammar/databindingerrors.h b/aplcore/include/apl/datagrammar/databindingerrors.h index 226b0d2..d707e1d 100644 --- a/aplcore/include/apl/datagrammar/databindingerrors.h +++ b/aplcore/include/apl/datagrammar/databindingerrors.h @@ -18,8 +18,9 @@ #include -#include "grammarerror.h" -#include "databindingrules.h" +#include "apl/datagrammar/databindingrules.h" +#include "apl/datagrammar/grammarerror.h" +#include "apl/utils/log.h" namespace apl { namespace datagrammar { diff --git a/aplcore/include/apl/datagrammar/databindinggrammar.h b/aplcore/include/apl/datagrammar/databindinggrammar.h index 08d2eb1..71bf55c 100644 --- a/aplcore/include/apl/datagrammar/databindinggrammar.h +++ b/aplcore/include/apl/datagrammar/databindinggrammar.h @@ -96,7 +96,7 @@ struct number : sor>, // INTEGER seq>, // . DIGITS+ number_int> {}; // INTEGER -struct symbol : seq< not_at, alpha, star > {}; +struct symbol : seq< not_at, identifier> {}; // Inline Arrays (e.g., [1,2,3]) struct array_comma : one<','> {}; diff --git a/aplcore/include/apl/datagrammar/databindingrules.h b/aplcore/include/apl/datagrammar/databindingrules.h index c79e289..74c1d3d 100644 --- a/aplcore/include/apl/datagrammar/databindingrules.h +++ b/aplcore/include/apl/datagrammar/databindingrules.h @@ -19,12 +19,11 @@ #define _APL_DATA_BINDING_RULES_H #include -#include "databindinggrammar.h" -#include "bytecodeassembler.h" - -#include "apl/primitives/object.h" +#include "apl/datagrammar/bytecodeassembler.h" +#include "apl/datagrammar/databindinggrammar.h" #include "apl/primitives/dimension.h" +#include "apl/primitives/object.h" #include "apl/utils/stringfunctions.h" #ifdef APL_CORE_UWP diff --git a/aplcore/include/apl/datasource/dynamicindexlistdatasourceprovider.h b/aplcore/include/apl/datasource/dynamicindexlistdatasourceprovider.h index dc2ff48..53e00a1 100644 --- a/aplcore/include/apl/datasource/dynamicindexlistdatasourceprovider.h +++ b/aplcore/include/apl/datasource/dynamicindexlistdatasourceprovider.h @@ -16,7 +16,6 @@ #ifndef _APL_DYNAMIC_INDEX_LIST_DATA_SOURCE_PROVIDER_H #define _APL_DYNAMIC_INDEX_LIST_DATA_SOURCE_PROVIDER_H -#include "apl/apl.h" #include "apl/datasource/dynamiclistdatasourceprovider.h" namespace apl { diff --git a/aplcore/include/apl/datasource/dynamiclistdatasourcecommon.h b/aplcore/include/apl/datasource/dynamiclistdatasourcecommon.h index fd3d031..d960c81 100644 --- a/aplcore/include/apl/datasource/dynamiclistdatasourcecommon.h +++ b/aplcore/include/apl/datasource/dynamiclistdatasourcecommon.h @@ -16,7 +16,7 @@ #ifndef _APL_DYNAMIC_LIST_DATA_SOURCE_COMMON_H #define _APL_DYNAMIC_LIST_DATA_SOURCE_COMMON_H -#include "apl/apl.h" +#include "apl/common.h" namespace apl { diff --git a/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h b/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h index 0ccf351..21973b6 100644 --- a/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h +++ b/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h @@ -16,9 +16,12 @@ #ifndef _APL_DYNAMIC_LIST_DATA_SOURCE_PROVIDER_H #define _APL_DYNAMIC_LIST_DATA_SOURCE_PROVIDER_H -#include "apl/apl.h" -#include "apl/datasource/offsetindexdatasourceconnection.h" +#include "apl/common.h" +#include "apl/datasource/datasourceprovider.h" #include "apl/datasource/dynamiclistdatasourcecommon.h" +#include "apl/datasource/offsetindexdatasourceconnection.h" +#include "apl/primitives/object.h" +#include "apl/utils/noncopyable.h" namespace apl { diff --git a/aplcore/include/apl/datasource/dynamictokenlistdatasourceprovider.h b/aplcore/include/apl/datasource/dynamictokenlistdatasourceprovider.h index 8fc4617..8086568 100644 --- a/aplcore/include/apl/datasource/dynamictokenlistdatasourceprovider.h +++ b/aplcore/include/apl/datasource/dynamictokenlistdatasourceprovider.h @@ -16,7 +16,6 @@ #ifndef _APL_DYNAMIC_TOKEN_LIST_DATA_SOURCE_PROVIDER_H #define _APL_DYNAMIC_TOKEN_LIST_DATA_SOURCE_PROVIDER_H -#include "apl/apl.h" #include "apl/datasource/dynamiclistdatasourceprovider.h" namespace apl { diff --git a/aplcore/include/apl/datasource/offsetindexdatasourceconnection.h b/aplcore/include/apl/datasource/offsetindexdatasourceconnection.h index f8e79f5..217faa8 100644 --- a/aplcore/include/apl/datasource/offsetindexdatasourceconnection.h +++ b/aplcore/include/apl/datasource/offsetindexdatasourceconnection.h @@ -16,6 +16,8 @@ #ifndef _APL_OFFSET_INDEX_DATA_SOURCE_PROVIDER_H #define _APL_OFFSET_INDEX_DATA_SOURCE_PROVIDER_H +#include + #include "apl/datasource/datasourceconnection.h" namespace apl { diff --git a/aplcore/include/apl/engine/builder.h b/aplcore/include/apl/engine/builder.h index 96c679e..89544a2 100644 --- a/aplcore/include/apl/engine/builder.h +++ b/aplcore/include/apl/engine/builder.h @@ -75,7 +75,8 @@ class Builder { bool fullBuild, bool useDirtyFlag); - CoreComponentPtr expandLayout(const ContextPtr& context, + CoreComponentPtr expandLayout(const std::string& name, + const ContextPtr& context, Properties& properties, const rapidjson::Value& layout, const CoreComponentPtr& parent, diff --git a/aplcore/include/apl/engine/context.h b/aplcore/include/apl/engine/context.h index 80a653a..0d5e7b3 100644 --- a/aplcore/include/apl/engine/context.h +++ b/aplcore/include/apl/engine/context.h @@ -16,15 +16,14 @@ #ifndef _APL_CONTEXT_H #define _APL_CONTEXT_H -#include -#include #include -#include #include +#include +#include #include -#include "apl/apl_config.h" #include "apl/common.h" + #include "apl/component/componentproperties.h" #include "apl/engine/contextobject.h" #include "apl/engine/jsonresource.h" @@ -33,15 +32,16 @@ #include "apl/engine/styleinstance.h" #include "apl/primitives/object.h" #include "apl/primitives/textmeasurerequest.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/common.h" -#endif // SCENEGRAPH #include "apl/utils/counter.h" #include "apl/utils/localemethods.h" #include "apl/utils/lrucache.h" #include "apl/utils/noncopyable.h" #include "apl/utils/path.h" +#ifdef SCENEGRAPH +#include "apl/scenegraph/common.h" +#endif // SCENEGRAPH + namespace apl { class Metrics; @@ -122,7 +122,7 @@ class Context : public RecalculateTarget, * @return The context. */ static ContextPtr createRootEvaluationContext(const Metrics& metrics, - const std::shared_ptr& core); + const std::shared_ptr& core); /** * Create a "clean" context. This shares the same root data, but does not contain any @@ -512,6 +512,11 @@ class Context : public RecalculateTarget, */ ComponentPtr findComponentById(const std::string& id) const; + /** + * @return The top component + */ + ComponentPtr topComponent() const; + /** * @return The current theme */ diff --git a/aplcore/include/apl/engine/evaluate.h b/aplcore/include/apl/engine/evaluate.h index 90c4332..ce1def8 100644 --- a/aplcore/include/apl/engine/evaluate.h +++ b/aplcore/include/apl/engine/evaluate.h @@ -18,7 +18,6 @@ #include "apl/utils/bimap.h" #include "apl/primitives/object.h" -#include "apl/primitives/dimension.h" namespace apl { diff --git a/aplcore/include/apl/engine/event.h b/aplcore/include/apl/engine/event.h index 936d4f0..fe6df9c 100644 --- a/aplcore/include/apl/engine/event.h +++ b/aplcore/include/apl/engine/event.h @@ -19,9 +19,9 @@ #include #include "apl/action/action.h" +#include "apl/command/commandproperties.h" #include "apl/component/component.h" #include "apl/primitives/objectbag.h" -#include "apl/command/commandproperties.h" namespace apl { diff --git a/aplcore/include/apl/engine/eventmanager.h b/aplcore/include/apl/engine/eventmanager.h new file mode 100644 index 0000000..d6f0a04 --- /dev/null +++ b/aplcore/include/apl/engine/eventmanager.h @@ -0,0 +1,71 @@ +/** +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. +*/ + +#ifndef APL_EVENT_MANAGER_H +#define APL_EVENT_MANAGER_H + +#include + +#include "apl/engine/event.h" +#include "apl/engine/eventpublisher.h" + +namespace apl +{ + +class EventManager; + +using EventManagerPtr = std::shared_ptr; + +/** + * Read-Write interface for publishing and consuming events. + */ +class EventManager : public EventPublisher +{ +public: + virtual ~EventManager() {}; + + /** + * Discard all pending, published events. + */ + virtual void clear() = 0; + + /** + * Determine if any published events are pending. + * + * @return true iff there are no pending events + */ + virtual bool empty() const = 0; + + /** + * Return the next pending published event. Check empty first. + * @return A reference to the next event + */ + virtual Event& front() = 0; + + /** + * Return the next pending published event. Check empty first. + * @return A reference to the next event if it exists; ??? otherwise. + */ + virtual const Event& front() const = 0; + + /** + * Removes the next pending event. Check empty first. + */ + virtual void pop() = 0; +}; + +} // namespace apl + +#endif // APL_EVENT_MANAGER_H diff --git a/aplcore/include/apl/engine/eventpublisher.h b/aplcore/include/apl/engine/eventpublisher.h new file mode 100644 index 0000000..942d834 --- /dev/null +++ b/aplcore/include/apl/engine/eventpublisher.h @@ -0,0 +1,49 @@ +/** +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. + */ + +#ifndef APL_EVENT_PUBLISHER_H +#define APL_EVENT_PUBLISHER_H + +#include "apl/engine/event.h" + +namespace apl +{ + +/** + * Write-only interface for publishing events from within a document. + */ +class EventPublisher +{ +public: + virtual ~EventPublisher() {}; + + /** + * Pushes event for publication. + * + * @param event The Event to publish + */ + virtual void push(const Event& event) = 0; + + /** + * Pushes event for publication. + * + * @param event The Event to publish + */ + virtual void push(Event&& event) = 0; +}; + +} // namespace apl + +#endif // APL_EVENT_PUBLISHER_H diff --git a/aplcore/include/apl/engine/info.h b/aplcore/include/apl/engine/info.h index d8e0d0c..2a59b76 100644 --- a/aplcore/include/apl/engine/info.h +++ b/aplcore/include/apl/engine/info.h @@ -16,9 +16,12 @@ #ifndef _APL_INFO_H #define _APL_INFO_H +#include #include #include +#include "apl/common.h" + namespace apl { class RootContextData; diff --git a/aplcore/include/apl/engine/layoutmanager.h b/aplcore/include/apl/engine/layoutmanager.h index d9be218..34d14c2 100644 --- a/aplcore/include/apl/engine/layoutmanager.h +++ b/aplcore/include/apl/engine/layoutmanager.h @@ -20,9 +20,9 @@ #include #include "apl/common.h" +#include "apl/component/componentproperties.h" #include "apl/primitives/object.h" #include "apl/primitives/size.h" -#include "apl/component/componentproperties.h" namespace apl { diff --git a/aplcore/include/apl/engine/propdef.h b/aplcore/include/apl/engine/propdef.h index 33e40be..f5c8f66 100644 --- a/aplcore/include/apl/engine/propdef.h +++ b/aplcore/include/apl/engine/propdef.h @@ -18,18 +18,7 @@ #include "apl/engine/arrayify.h" #include "apl/engine/properties.h" - -#include "apl/animation/easing.h" -#include "apl/graphic/graphicfilter.h" -#include "apl/graphic/graphicpattern.h" -#include "apl/primitives/filter.h" -#include "apl/primitives/gradient.h" -#include "apl/primitives/mediasource.h" -#include "apl/primitives/object.h" -#include "apl/primitives/styledtext.h" -#include "apl/primitives/transform.h" -#include "apl/primitives/urlrequest.h" - +#include "apl/primitives/dimension.h" #include "apl/utils/bimap.h" namespace apl { @@ -114,9 +103,33 @@ inline Object asNonAutoRelativeDimension(const Context& context, const Object& o return object.asNonAutoRelativeDimension(context); } -inline Object asColor(const Context& context, const Object& object) { - return object.asColor(context); -} +extern Object asStyledText(const Context& context, const Object& object); + +extern Object asColor(const Context& context, const Object& object); + +extern Object asFilterArray(const Context& context, const Object& object); + +extern Object asGraphicFilterArray(const Context& context, const Object& object); + +extern Object asGradient(const Context& context, const Object& object); + +extern Object asFill(const Context& context, const Object& object); + +extern Object asVectorGraphicSource(const Context& context, const Object& object); + +extern Object asImageSourceArray(const Context& context, const Object& object); + +extern Object asMediaSourceArray(const Context& context, const Object& object); + +extern Object asFilteredText(const Context& context, const Object& object); + +extern Object asTransformOrArray(const Context& context, const Object& object); + +extern Object asEasing(const Context& context, const Object& object); + +extern Object asGraphicPattern(const Context& context, const Object& object); + +extern Object asAvgGradient(const Context& context, const Object& object); inline Object asOpacity(const Context&, const Object& object) { double value = object.asNumber(); @@ -125,31 +138,12 @@ inline Object asOpacity(const Context&, const Object& object) { return value; } +extern Object asPaddingArray(const Context& context, const Object& object); + inline Object asCommand(const Context& context, const Object& object) { return Object(arrayify(context, object)); } - -inline Object asFilterArray(const Context& context, const Object& object) { - std::vector data; - for (auto& m : arrayify(context, object)) { - auto f = Filter::create(context, m); - if (f.is()) - data.push_back(std::move(f)); - } - return Object(std::move(data)); -} - -inline Object asGraphicFilterArray(const Context& context, const Object& object) { - std::vector data; - for (auto& m : arrayify(context, object)) { - auto f = GraphicFilter::create(context, m); - if (f.is()) - data.push_back(std::move(f)); - } - return Object(std::move(data)); -} - inline Object asStringOrArray(const Context& context, const Object& object) { std::vector data; for (auto& m : arrayify(context, object)) @@ -174,49 +168,6 @@ inline Object asMapped(const Context& context, const Object& object, return defvalue; } -inline Object asGradient(const Context& context, const Object& object) { - return Gradient::create(context, object); -} - -inline Object asFill(const Context& context, const Object& object) { - auto gradient = asGradient(context, object); - return gradient.is() ? gradient : asColor(context, object); -} - -inline Object asVectorGraphicSource(const Context& context, const Object& object) { - return object.isString() ? object : URLRequest::create(context, object); -} - -inline Object asImageSourceArray(const Context& context, const Object& object) { - std::vector data; - for (auto& m : arrayify(context, object)) { - auto request = m.isString() ? m : URLRequest::create(context, m); - if (!request.isNull()) - data.emplace_back(std::move(request)); - } - - if (data.empty()) - return ""; - - // In case of URLs the spec enforces an array of elements. - // We don't want to break runtimes that are not using urls and depend - // on the old logic that evaluates the strings via arrayify - if (data.size() == 1 && data.front().isString()) - return data.front(); - - return Object(std::move(data)); -} - -inline Object asMediaSourceArray(const Context& context, const Object& object) { - std::vector data; - for (auto& m : arrayify(context, object)) { - auto ms = MediaSource::create(context, m); - if (ms.is()) - data.push_back(std::move(ms)); - } - return Object(std::move(data)); -} - inline Object asDashArray(const Context& context, const Object& object) { std::vector data = arrayify(context, object); auto size = data.size(); @@ -227,43 +178,10 @@ inline Object asDashArray(const Context& context, const Object& object) { return Object(std::move(data)); } -inline Object asStyledText(const Context& context, const Object& object) { - return StyledText::create(context, object); -} - -inline Object asFilteredText(const Context& context, const Object& object) { - return StyledText::create(context, object).getText(); -} - -inline Object asTransformOrArray(const Context& context, const Object& object) { - if (object.is()) - return object; - - return evaluateRecursive(context, arrayify(context, object)); -} - -inline Object asEasing(const Context& context, const Object& object) { - if (object.is()) - return object; - - return Easing::parse(context.session(), object.asString()); -} - inline Object asDeepArray(const Context &context, const Object &object) { return evaluateRecursive(context, arrayify(context, object)); } -inline Object asGraphicPattern(const Context& context, const Object& object) { - return GraphicPattern::create(context, object); -} - -inline Object asAvgGradient(const Context& context, const Object& object) { - return Gradient::createAVG(context, object); -} - -Object asPaddingArray(const Context& context, const Object& object); - - /** * Flags that specify how the property definition will be used. */ diff --git a/aplcore/include/apl/engine/queueeventmanager.h b/aplcore/include/apl/engine/queueeventmanager.h new file mode 100644 index 0000000..f496f4b --- /dev/null +++ b/aplcore/include/apl/engine/queueeventmanager.h @@ -0,0 +1,49 @@ +/** +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. + */ + +#ifndef APL_QUEUE_EVENT_MANAGER_H +#define APL_QUEUE_EVENT_MANAGER_H + +#include +#include + +#include "apl/engine/eventmanager.h" + +namespace apl +{ + +/** + * An EventManager that delegates all operations to an encapsulated std::queue. + */ +class QueueEventManager : public EventManager +{ +public: + ~QueueEventManager() override = default; + + void clear() override { events = std::queue(); } + bool empty() const override { return events.empty(); } + const Event& front() const override { return events.front(); } + Event& front() override { return events.front(); } + void pop() override { events.pop(); } + void push(const Event& event) override { events.push(event); } + void push(Event&& event) override { events.push(event); } + +private: + std::queue events; +}; + +} // namespace apl + +#endif // APL_QUEUE_EVENT_MANAGER_H diff --git a/aplcore/include/apl/engine/recalculatetarget.h b/aplcore/include/apl/engine/recalculatetarget.h index f3dff4d..a6ee23d 100644 --- a/aplcore/include/apl/engine/recalculatetarget.h +++ b/aplcore/include/apl/engine/recalculatetarget.h @@ -61,6 +61,14 @@ class RecalculateTarget { it++; } + /** + * Return true if there is at least one upstream that can change this target + * @return True if there is at least one upstream dependant + */ + bool hasUpstream() { + return !mUpstream.empty(); + } + /** * Return true if this key is driven by one or more upstream dependants * @param key The key @@ -70,6 +78,18 @@ class RecalculateTarget { return mUpstream.find(key) != mUpstream.end(); } + /** + * Return true if at least one of these keys is driven by one or more upstream dependents + * @param keys The vector of keys + * @return True if there is at least one upstream dependant that matches + */ + bool hasUpstream(const std::vector& keys) { + for (const auto& m : keys) + if (mUpstream.find(m) != mUpstream.end()) + return true; + return false; + } + /** * Return how many upstream dependants are connected to this key. * @param key The key diff --git a/aplcore/include/apl/engine/rootcontext.h b/aplcore/include/apl/engine/rootcontext.h index 3ff5a7a..6e2a5d5 100644 --- a/aplcore/include/apl/engine/rootcontext.h +++ b/aplcore/include/apl/engine/rootcontext.h @@ -19,22 +19,20 @@ #include #include -#include "apl/apl_config.h" +#include "apl/common.h" #include "apl/content/configurationchange.h" +#include "apl/content/rootconfig.h" #include "apl/content/settings.h" -#include "apl/common.h" #include "apl/document/displaystate.h" #include "apl/engine/event.h" #include "apl/engine/info.h" -#include "apl/content/rootconfig.h" #include "apl/focus/focusdirection.h" -#include "apl/media/mediaobject.h" +#include "apl/primitives/keyboard.h" +#include "apl/utils/noncopyable.h" +#include "apl/utils/userdata.h" #ifdef SCENEGRAPH #include "apl/scenegraph/common.h" #endif // SCENEGRAPH -#include "apl/utils/noncopyable.h" -#include "apl/utils/userdata.h" -#include "apl/primitives/keyboard.h" #ifdef ALEXAEXTENSIONS #include "apl/extension/extensionmediator.h" @@ -42,6 +40,7 @@ namespace apl { +class EventManager; class Metrics; class RootConfig; class RootContextData; @@ -124,6 +123,21 @@ class RootContext : public std::enable_shared_from_this, const RootConfig& config, std::function callback); + /** + * Construct a top-level root context. + * @param metrics Display metrics + * @param content Content to display + * @param config Configuration information + * @param callback Pre-layout callback + * @param eventManager Manages published events + * @return A pointer to the root context. + */ + static RootContextPtr create(const Metrics& metrics, + const ContentPtr& content, + const RootConfig& config, + std::function callback, + const std::shared_ptr& eventManager); + /** * Notify core of a configuration change. Internally this method will trigger the "onConfigChange" * event handler in the APL document. A common behavior in the onConfigChange event handler is to @@ -181,6 +195,18 @@ class RootContext : public std::enable_shared_from_this, */ RootContext(const Metrics& metrics, const ContentPtr& content, const RootConfig& config); + /** + * Public constructor. Use the ::create method instead. + * @param metrics Display metrics + * @param content Processed APL content data + * @param config Configuration information + * @param eventManager Manages published events + */ + RootContext(const Metrics& metrics, + const ContentPtr& content, + const RootConfig& config, + const std::shared_ptr& eventManager); + ~RootContext() override; /** @@ -528,7 +554,7 @@ class RootContext : public std::enable_shared_from_this, */ DisplayState getDisplayState() const { return mDisplayState; } - void init(const Metrics& metrics, const RootConfig& config, bool reinflation); + void init(const Metrics& metrics, const RootConfig& config, bool reinflation, const std::shared_ptr& eventManager); bool setup(const CoreComponentPtr& top); bool verifyAPLVersionCompatibility(const std::vector>& ordered, const APLVersion& compatibilityVersion); diff --git a/aplcore/include/apl/engine/rootcontextdata.h b/aplcore/include/apl/engine/rootcontextdata.h index 5e96348..0ce7447 100644 --- a/aplcore/include/apl/engine/rootcontextdata.h +++ b/aplcore/include/apl/engine/rootcontextdata.h @@ -18,12 +18,8 @@ #include #include -#include #include "apl/apl_config.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/common.h" -#endif // SCENEGRAPH #include "apl/content/content.h" #include "apl/content/metrics.h" @@ -31,6 +27,7 @@ #include "apl/content/settings.h" #include "apl/datasource/datasourceconnection.h" #include "apl/engine/event.h" +#include "apl/engine/queueeventmanager.h" #include "apl/engine/hovermanager.h" #include "apl/engine/jsonresource.h" #include "apl/engine/keyboardmanager.h" @@ -41,13 +38,17 @@ #include "apl/focus/focusmanager.h" #include "apl/livedata/livedatamanager.h" #include "apl/media/mediamanager.h" -#include "apl/primitives/textmeasurerequest.h" #include "apl/primitives/size.h" +#include "apl/primitives/textmeasurerequest.h" #include "apl/time/sequencer.h" #include "apl/touch/pointermanager.h" #include "apl/utils/counter.h" #include "apl/utils/lrucache.h" +#ifdef SCENEGRAPH +#include "apl/scenegraph/common.h" +#endif // SCENEGRAPH + namespace apl { class RootContextData : public Counter { @@ -62,13 +63,15 @@ class RootContextData : public Counter { * @param settings Document settings * @param session Session information for logging messages and warnings * @param extensions Mapping of requested extensions NAME -> URI + * @param eventManager Responsible for managing all published events. */ RootContextData(const Metrics& metrics, const RootConfig& config, RuntimeState runtimeState, const SettingsPtr& settings, const SessionPtr& session, - const std::vector>& extensions); + const std::vector>& extensions, + const EventManagerPtr& eventManager = std::make_shared()); ~RootContextData(); @@ -168,7 +171,7 @@ class RootContextData : public Counter { LayoutDirection getLayoutDirection() const { return mLayoutDirection; } bool getReinflationFlag() const { return mRuntimeState.getReinflation(); } - std::queue events; + const EventManagerPtr events; #ifdef ALEXAEXTENSIONS std::queue extesnionEvents; #endif diff --git a/aplcore/include/apl/extension/extensionclient.h b/aplcore/include/apl/extension/extensionclient.h index 582f934..cd8206f 100644 --- a/aplcore/include/apl/extension/extensionclient.h +++ b/aplcore/include/apl/extension/extensionclient.h @@ -17,14 +17,14 @@ #define _APL_EXTENSION_CLIENT_H #include "apl/common.h" -#include "apl/content/jsondata.h" -#include "apl/content/extensionproperty.h" #include "apl/content/extensioncomponentdefinition.h" +#include "apl/content/extensionproperty.h" +#include "apl/content/jsondata.h" #include "apl/engine/event.h" +#include "apl/livedata/livedataobjectwatcher.h" #include "apl/utils/counter.h" #include "apl/utils/noncopyable.h" #include "apl/utils/session.h" -#include "apl/livedata/livedataobjectwatcher.h" namespace apl { diff --git a/aplcore/include/apl/extension/extensioncomponent.h b/aplcore/include/apl/extension/extensioncomponent.h index 4bc50d4..b6e336b 100644 --- a/aplcore/include/apl/extension/extensioncomponent.h +++ b/aplcore/include/apl/extension/extensioncomponent.h @@ -16,9 +16,9 @@ #ifndef _APL_EXTENSION_COMPONENT_H #define _APL_EXTENSION_COMPONENT_H +#include "apl/component/componentpropdef.h" #include "apl/component/touchablecomponent.h" #include "apl/content/extensioncomponentdefinition.h" -#include "apl/component/componentpropdef.h" namespace apl { diff --git a/aplcore/include/apl/graphic/graphic.h b/aplcore/include/apl/graphic/graphic.h index cdaa7d7..de0bb86 100644 --- a/aplcore/include/apl/graphic/graphic.h +++ b/aplcore/include/apl/graphic/graphic.h @@ -177,14 +177,10 @@ class Graphic : public UIDObject, } #ifdef SCENEGRAPH - sg::NodePtr getSceneGraph(sg::SceneGraphUpdates& sceneGraph); - - /** - * Update the scene graph with any changes - * @param sceneGraph The scene graph to update - * @return True if the scene graph needs to be redrawn - */ - bool updateSceneGraph(sg::SceneGraphUpdates& sceneGraph); + sg::GraphicFragmentPtr getSceneGraph(bool allowLayers, + sg::SceneGraphUpdates& sceneGraph, + const sg::LayerPtr& containingLayer = nullptr); + void updateSceneGraph(sg::SceneGraphUpdates& sceneGraph); #endif // SCENEGRAPH class ObjectType final : public PointerHolderObjectType {}; @@ -221,6 +217,11 @@ class Graphic : public UIDObject, std::weak_ptr mComponent; std::shared_ptr mStyles; + +#ifdef SCENEGRAPH + sg::GraphicFragmentPtr mSceneGraphFragment; + bool mHasSceneGraph = false; +#endif }; } // namespace apl diff --git a/aplcore/include/apl/graphic/graphicelement.h b/aplcore/include/apl/graphic/graphicelement.h index ceb1bea..5d76764 100644 --- a/aplcore/include/apl/graphic/graphicelement.h +++ b/aplcore/include/apl/graphic/graphicelement.h @@ -32,7 +32,7 @@ #include "apl/graphic/graphicproperties.h" #ifdef SCENEGRAPH -#include "apl/scenegraph/modifiednodelist.h" +#include "apl/scenegraph/common.h" #endif // SCENEGRAPH namespace apl { @@ -106,6 +106,18 @@ class GraphicElement : public UIDObject, */ bool isDirty(GraphicPropertyKey key) const { return mDirtyProperties.find(key) != mDirtyProperties.end(); } + /** + * Check to see if any of these graphic properties have been marked as dirty. + * @param keys The vector of properties to check. + * @return True if at least one of them is dirty + */ + bool isDirty(std::vector keys) const { + for (const auto& m : keys) + if (mDirtyProperties.find(m) != mDirtyProperties.end()) + return true; + return false; + } + /** * @return The set of properties which are marked as dirty for this element. */ @@ -139,23 +151,25 @@ class GraphicElement : public UIDObject, /** * Do any VectorGraphic or context dependent clean-up. */ - void release(); + virtual void release(); #ifdef SCENEGRAPH /** - * @return The current scene graph + * Ensure that a scene graph has been constructed for this element. */ - sg::NodePtr getSceneGraph(sg::SceneGraphUpdates& sceneGraph); + virtual sg::GraphicFragmentPtr buildSceneGraph(bool allowLayers, + sg::SceneGraphUpdates& sceneGraph) = 0; /** - * @return The first scene graph node + * Store a reference to the layer that needs to be redrawn if a content + * node in the element's scene graph needs to be redrawn */ - sg::NodePtr getSceneGraphNode() const { return mSceneGraphNode; } + void assignSceneGraphLayer(const sg::LayerPtr& containingLayer); /** * Update the scene graph based on dirty properties. */ - void updateSceneGraph(sg::ModifiedNodeList& modList); + virtual void updateSceneGraph(sg::SceneGraphUpdates& sceneGraph) = 0; #endif // SCENEGRAPH virtual std::string toDebugString() const = 0; @@ -175,8 +189,10 @@ class GraphicElement : public UIDObject, void updateTransform(const Context& context, GraphicPropertyKey inKey, GraphicPropertyKey outKey, bool useDirtyFlag); #ifdef SCENEGRAPH - virtual sg::NodePtr buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) = 0; - virtual void updateSceneGraphInternal(sg::ModifiedNodeList& modList, const sg::NodePtr& node) = 0; + sg::GraphicFragmentPtr ensureSceneGraphChildren(bool allowLayers, sg::SceneGraphUpdates& sceneGraph); + void requestRedraw(sg::SceneGraphUpdates& sceneGraph); + void requestSizeCheck(sg::SceneGraphUpdates& sceneGraph); + bool includeInSceneGraph(GraphicPropertyKey key); #endif // SCENEGRAPH static void fixFillTransform(GraphicElement& element); @@ -193,10 +209,9 @@ class GraphicElement : public UIDObject, mutable id_type mCachedTempId = 0; #ifdef SCENEGRAPH -private: - sg::NodePtr mSceneGraphNode; - sg::NodePtr mInnerSceneGraphNode; -#endif // SCENEGRAPH + sg::LayerPtr mContainingLayer; // The layer that this element will render in + sg::NodePtr mSceneGraphNode; // Rendering node used for update operations +#endif }; } // namespace apl diff --git a/aplcore/include/apl/graphic/graphicelementcontainer.h b/aplcore/include/apl/graphic/graphicelementcontainer.h index aadc733..0af4761 100644 --- a/aplcore/include/apl/graphic/graphicelementcontainer.h +++ b/aplcore/include/apl/graphic/graphicelementcontainer.h @@ -34,10 +34,10 @@ class GraphicElementContainer : public GraphicElement, } #ifdef SCENEGRAPH -protected: - sg::NodePtr buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) override; - void updateSceneGraphInternal(sg::ModifiedNodeList& sceneGraph, const sg::NodePtr& node) override; -#endif // SCENEGRAPH + sg::GraphicFragmentPtr buildSceneGraph(bool allowLayers, + sg::SceneGraphUpdates& sceneGraph) override; + void updateSceneGraph(sg::SceneGraphUpdates& sceneGraph) override {} +#endif protected: const GraphicPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/graphic/graphicelementgroup.h b/aplcore/include/apl/graphic/graphicelementgroup.h index 671162e..27672dc 100644 --- a/aplcore/include/apl/graphic/graphicelementgroup.h +++ b/aplcore/include/apl/graphic/graphicelementgroup.h @@ -33,14 +33,21 @@ class GraphicElementGroup : public GraphicElement, return std::string("GraphicElementGroup<") + mUniqueId + ">"; } + void release() override; + +#ifdef SCENEGRAPH + sg::GraphicFragmentPtr buildSceneGraph(bool allowLayers, + sg::SceneGraphUpdates& sceneGraph) override; + void updateSceneGraph(sg::SceneGraphUpdates& sceneGraph) override; +#endif + protected: const GraphicPropDefSet& propDefSet() const override; bool initialize(const GraphicPtr& graphic, const Object& json) override; void updateTransform(const Context& context, bool useDirtyFlag); #ifdef SCENEGRAPH - sg::NodePtr buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) override; - void updateSceneGraphInternal(sg::ModifiedNodeList& modList, const sg::NodePtr& node) override; + sg::LayerPtr mSceneGraphLayer; #endif // SCENEGRAPH static void fixTransform(GraphicElement& element) { diff --git a/aplcore/include/apl/graphic/graphicelementpath.h b/aplcore/include/apl/graphic/graphicelementpath.h index a30dbb4..8bbf931 100644 --- a/aplcore/include/apl/graphic/graphicelementpath.h +++ b/aplcore/include/apl/graphic/graphicelementpath.h @@ -31,14 +31,17 @@ class GraphicElementPath : public GraphicElement, return std::string("GraphicElementPath<") + mUniqueId + ">"; } + void release() override; + +#ifdef SCENEGRAPH + sg::GraphicFragmentPtr buildSceneGraph(bool allowLayers, + sg::SceneGraphUpdates& sceneGraph) override; + void updateSceneGraph(sg::SceneGraphUpdates& sceneGraph) override; +#endif + protected: const GraphicPropDefSet& propDefSet() const override; bool initialize(const GraphicPtr& graphic, const Object& json) override; - -#ifdef SCENEGRAPH - sg::NodePtr buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) override; - void updateSceneGraphInternal(sg::ModifiedNodeList& modList, const sg::NodePtr& node) override; -#endif // SCENEGRAPH }; } // namespace apl diff --git a/aplcore/include/apl/graphic/graphicelementtext.h b/aplcore/include/apl/graphic/graphicelementtext.h index 65dcf39..ac6ae4d 100644 --- a/aplcore/include/apl/graphic/graphicelementtext.h +++ b/aplcore/include/apl/graphic/graphicelementtext.h @@ -36,14 +36,19 @@ class GraphicElementText : public GraphicElement, return std::string("GraphicElementText<") + mUniqueId + ">"; } + void release() override; + +#ifdef SCENEGRAPH + sg::GraphicFragmentPtr buildSceneGraph(bool allowLayers, + sg::SceneGraphUpdates& sceneGraph) override; + void updateSceneGraph(sg::SceneGraphUpdates& sceneGraph) override; +#endif + protected: const GraphicPropDefSet& propDefSet() const override; bool initialize(const GraphicPtr& graphic, const Object& json) override; #ifdef SCENEGRAPH - sg::NodePtr buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) override; - void updateSceneGraphInternal(sg::ModifiedNodeList& modList, const sg::NodePtr& node) override; - private: void ensureSGTextLayout(); void ensureTextProperties(); diff --git a/aplcore/include/apl/media/mediaobject.h b/aplcore/include/apl/media/mediaobject.h index 973f035..81ff5e8 100644 --- a/aplcore/include/apl/media/mediaobject.h +++ b/aplcore/include/apl/media/mediaobject.h @@ -21,9 +21,10 @@ #include "apl/apl_config.h" #include "apl/common.h" + #include "apl/engine/event.h" +#include "apl/primitives/header.h" #include "apl/primitives/size.h" -#include "apl/primitives/urlrequest.h" #include "apl/utils/noncopyable.h" namespace apl { diff --git a/aplcore/include/apl/media/mediaplayer.h b/aplcore/include/apl/media/mediaplayer.h index 70a0f65..40e8eb5 100644 --- a/aplcore/include/apl/media/mediaplayer.h +++ b/aplcore/include/apl/media/mediaplayer.h @@ -17,22 +17,13 @@ #define _APL_MEDIA_PLAYER_H #include + #include "apl/media/mediaobject.h" +#include "apl/media/mediatrack.h" #include "apl/primitives/mediastate.h" namespace apl { -/** - * A description of a media track to be played by the media player. - */ -struct MediaTrack { - std::string url; // Source of the video clip - HeaderArray headers; // HeaderArray required for the track - int offset; // Starting offset within the media object, in milliseconds - int duration; // Duration from the starting offset to play. If non-positive, play the entire track - int repeatCount; // Number of times to repeat this track before moving to the next. Negative numbers repeat forever. -}; - enum MediaPlayerEventType { kMediaPlayerEventEnd, kMediaPlayerEventPause, diff --git a/aplcore/include/apl/media/mediatrack.h b/aplcore/include/apl/media/mediatrack.h new file mode 100644 index 0000000..5ac7bc2 --- /dev/null +++ b/aplcore/include/apl/media/mediatrack.h @@ -0,0 +1,61 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef _APL_MEDIA_TRACK_H +#define _APL_MEDIA_TRACK_H + +#include +#include + +#include "apl/primitives/header.h" +#include "apl/utils/bimap.h" + +namespace apl { + +/** + * The type of text track. + */ +enum TextTrackType { + kTextTrackTypeCaption, +}; + +/** + * The description of the subtitle + */ +struct TextTrack { + TextTrackType type; + std::string url; + std::string description; +}; + +using TextTrackArray = std::vector; + +/** + * A description of a media track to be played by the media player. + */ +struct MediaTrack { + std::string url; // Source of the video clip + int offset; // Starting offset within the media object, in milliseconds + int duration; // Duration from the starting offset to play. If non-positive, play the entire track + int repeatCount; // Number of times to repeat this track before moving to the next. Negative numbers repeat forever. + HeaderArray headers; // HeaderArray required for the track + TextTrackArray textTracks; // Distinct subtitle tracks to render +}; + +extern Bimap sTextTrackTypeMap; + +} // namespace apl + +#endif // _APL_MEDIA_TRACK_H diff --git a/aplcore/include/apl/primitives/filter.h b/aplcore/include/apl/primitives/filter.h index f8b9521..fc6d3af 100644 --- a/aplcore/include/apl/primitives/filter.h +++ b/aplcore/include/apl/primitives/filter.h @@ -85,7 +85,10 @@ enum BlendMode { kBlendModeHue, kBlendModeSaturation, kBlendModeColor, - kBlendModeLuminosity + kBlendModeLuminosity, + kBlendModeSourceAtop, + kBlendModeSourceIn, + kBlendModeSourceOut, }; extern Bimap sFilterTypeBimap; diff --git a/aplcore/include/apl/primitives/header.h b/aplcore/include/apl/primitives/header.h new file mode 100644 index 0000000..82554ac --- /dev/null +++ b/aplcore/include/apl/primitives/header.h @@ -0,0 +1,29 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef _APL_HEADER_H +#define _APL_HEADER_H + +#include +#include + +namespace apl { + +using HeaderItem = std::string; +using HeaderArray = std::vector; + +} // namespace apl + +#endif //_APL_HEADER_H \ No newline at end of file diff --git a/aplcore/include/apl/primitives/mediasource.h b/aplcore/include/apl/primitives/mediasource.h index c73f892..2991e52 100644 --- a/aplcore/include/apl/primitives/mediasource.h +++ b/aplcore/include/apl/primitives/mediasource.h @@ -20,6 +20,7 @@ #include "apl/primitives/objecttype.h" #include "apl/media/mediaobject.h" +#include "apl/media/mediatrack.h" #include "apl/primitives/urlrequest.h" namespace apl { @@ -47,6 +48,11 @@ class MediaSource { */ std::string getDescription() const { return mDescription; } + /** + * @return Text tracks + */ + TextTrackArray getTextTracks() const { return mTextTracks; } + /** * @return Media duration. */ @@ -89,7 +95,8 @@ class MediaSource { int duration, int repeatCount, Object entities, - int offset); + int offset, + TextTrackArray textTracks); private: URLRequest mUrlRequest; @@ -98,6 +105,7 @@ class MediaSource { int mRepeatCount; Object mEntities; int mOffset; + TextTrackArray mTextTracks; }; } // namespace apl diff --git a/aplcore/include/apl/primitives/symbolreferencemap.h b/aplcore/include/apl/primitives/symbolreferencemap.h index 3dc3e95..b7a5376 100644 --- a/aplcore/include/apl/primitives/symbolreferencemap.h +++ b/aplcore/include/apl/primitives/symbolreferencemap.h @@ -17,9 +17,9 @@ #define _APL_SYMBOL_REFERENCE_MAP_H #include +#include #include "apl/common.h" -#include "apl/utils/log.h" namespace apl { diff --git a/aplcore/include/apl/primitives/timegrammar.h b/aplcore/include/apl/primitives/timegrammar.h index 04a9838..f867f5f 100644 --- a/aplcore/include/apl/primitives/timegrammar.h +++ b/aplcore/include/apl/primitives/timegrammar.h @@ -16,15 +16,15 @@ #ifndef _APL_TIME_GRAMMAR_H #define _APL_TIME_GRAMMAR_H -#include -#include +#include #include #include -#include + +#include +#include #include "apl/datagrammar/grammarpolyfill.h" #include "apl/primitives/timefunctions.h" -#include "apl/utils/log.h" namespace apl { diff --git a/aplcore/include/apl/primitives/urlrequest.h b/aplcore/include/apl/primitives/urlrequest.h index d2fb2e6..b45675f 100644 --- a/aplcore/include/apl/primitives/urlrequest.h +++ b/aplcore/include/apl/primitives/urlrequest.h @@ -16,15 +16,13 @@ #ifndef APL_URLREQUEST_H #define APL_URLREQUEST_H -#include "apl/primitives/objecttype.h" - #include #include -namespace apl { +#include "apl/primitives/header.h" +#include "apl/primitives/objecttype.h" -using HeaderItem = std::string; -using HeaderArray = std::vector; +namespace apl { /** * URLRequest class stores the common elements required for any media source. diff --git a/aplcore/include/apl/scenegraph/common.h b/aplcore/include/apl/scenegraph/common.h index 2c908af..c695f79 100644 --- a/aplcore/include/apl/scenegraph/common.h +++ b/aplcore/include/apl/scenegraph/common.h @@ -38,6 +38,7 @@ class Accessibility; class Filter; class Layer; class Node; +class GraphicFragment; class Paint; class Path; class PathOp; @@ -50,6 +51,7 @@ using EditTextBoxPtr = std::shared_ptr; using EditTextConfigPtr = std::shared_ptr; using EditTextFactoryPtr = std::shared_ptr; using FilterPtr = std::shared_ptr; +using GraphicFragmentPtr = std::shared_ptr; using LayerPtr = std::shared_ptr; using NodePtr = std::shared_ptr; using PaintPtr = std::shared_ptr; diff --git a/aplcore/include/apl/scenegraph/graphicfragment.h b/aplcore/include/apl/scenegraph/graphicfragment.h new file mode 100644 index 0000000..3e5f0fa --- /dev/null +++ b/aplcore/include/apl/scenegraph/graphicfragment.h @@ -0,0 +1,105 @@ +/** +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. +*/ + +#ifndef _APL_SG_GRAPHIC_FRAGMENT +#define _APL_SG_GRAPHIC_FRAGMENT + +#include + +#include "apl/common.h" +#include "apl/scenegraph/common.h" + +namespace apl { + +class Object; + +namespace sg { + +/** + * When a vector graphic is first inflated into a scene graph, each graphic element returns + * a GraphicFragment containing the logic to render that graphic element. Graphic groups and + * the top-level container assemble the fragments and merge them together into a final fragment + * containing a tree of layers where each layer may have a content node. + * + * An individual fragment is either empty, contains just a Node, or contains a Layer which + * may have sub-layers and a content node. The Type of the fragment tracks if the content + * of the fragment (either the direct Node or the content node of the Layer) is fixed (i.e., + * never changes) or mutable (from a style or parameter change). The Type may also indicate + * that the Layer properties mutate (these are the layer transform, opacity, and clipping outline). + * + * The fragment also accumulates a list of GraphicElements. When a GraphicElement property + * changes it needs to know the scene graph Layer to update. As the GraphicFragments are merged + * and updated to create the final scene graph we track which elements are associated with a + * particular Node or Layer and eventually use this list to assign each GraphicElement to a specific + * Layer. + */ +class GraphicFragment { +public: + enum Type { + kEmpty, + kNodeContentFixed, + kNodeContentMutable, // Only possible in a layer-free design + kLayerFixedContentFixed, // Includes the case with no content + kLayerFixedContentMutable, + kLayerMutable + }; + + static GraphicFragmentPtr create(const GraphicElementPtr& element); + static GraphicFragmentPtr create(const GraphicElementPtr& element, const sg::NodePtr& node, + Type flags); + static GraphicFragmentPtr create(const GraphicElementPtr& element, const sg::LayerPtr& layer, + Type flags); + + GraphicFragment() = default; + bool empty() const { return !mNode && !mLayer; } + + sg::NodePtr node() const { return mNode; } + sg::LayerPtr layer() const { return mLayer; } + + bool isNode() const { return mNode != nullptr; } + bool isLayer() const { return mLayer != nullptr; } + + void setNode(const sg::NodePtr& node) { mNode = node; } + + Type getType() const { return mType; } + void setType(Type type) { mType = type; } + + GraphicFragment& operator=(const GraphicFragment&) = default; + + bool mergeWith(const GraphicFragmentPtr& otherPtr); + void addChild(const GraphicFragmentPtr& otherPtr, sg::SceneGraphUpdates& sceneGraph); + void ensureLayer(sg::SceneGraphUpdates& sceneGraph); + + void fixBoundingBox(); + void applyFilters(const Object& filters); + void clearElements() { mElements.clear(); } + void assignToLayer(const sg::LayerPtr& containingLayer); + + std::string toDebugString() const; + +private: + void addShadow(const sg::ShadowPtr& shadow); + +private: + std::vector mElements; // Elements that refer to this content + sg::NodePtr mNode; + sg::LayerPtr mLayer; + Type mType = kEmpty; +}; + +} // namespace sg +} // namespace apl + +#endif // _APL_SG_GRAPHIC_FRAGMENT diff --git a/aplcore/include/apl/scenegraph/layer.h b/aplcore/include/apl/scenegraph/layer.h index ea4936e..cfd2ee2 100644 --- a/aplcore/include/apl/scenegraph/layer.h +++ b/aplcore/include/apl/scenegraph/layer.h @@ -16,6 +16,8 @@ #ifndef _APL_LAYER_H #define _APL_LAYER_H +#include + #include "apl/common.h" #include "apl/primitives/transform2d.h" #include "apl/scenegraph/path.h" @@ -57,6 +59,8 @@ class Layer : public Counter, kFlagInteractionChanged = 1u << 11, }; + using FlagType = std::uint16_t; + // WARNING: If you changed these, update the layers.cpp file to fix the constant array enum Interaction { kInteractionNone = 0, @@ -67,30 +71,53 @@ class Layer : public Counter, kInteractionScrollVertical = 1u << 4, // Can be scrolled vertically }; + using InteractionType = std::uint8_t; + + // WARNING: If you changed these, update the layers.cpp file to fix the constant array + enum Characteristics { + kCharacteristicDoNotClipChildren = 1u << 0, // Do not clip children to the layer outline or clip path + kCharacteristicRenderOnly = 1u << 1, // This layer is only for rendering (no text input or touch events) + }; + + using CharacteristicsType = std::uint8_t; + Layer(const std::string& name, Rect bounds, float opacity, Transform2D transform); ~Layer() override; const std::string& getName() const { return mName; } - void setFlag(unsigned flag) { mFlags |= flag; } - bool isFlagSet(unsigned flag) const { return (mFlags & flag) != 0; } + void setFlag(FlagType flag) { mFlags |= flag; } + bool isFlagSet(FlagType flag) const { return (mFlags & flag) != 0; } bool anyFlagSet() const { return mFlags != 0; } void clearFlags() { mFlags = 0; } - unsigned getAndClearFlags() { auto result = mFlags; mFlags = 0; return result; } + FlagType getAndClearFlags() { auto result = mFlags; mFlags = 0; return result; } std::string debugFlagString() const; - void setInteraction(unsigned interaction) { mInteraction |= interaction; } - void updateInteraction(unsigned interaction, bool isSet); - unsigned getInteraction() const { return mInteraction; } + void setInteraction(InteractionType interaction) { mInteraction |= interaction; } + void updateInteraction(InteractionType interaction, bool isSet); + InteractionType getInteraction() const { return mInteraction; } std::string debugInteractionString() const; + void setCharacteristic(CharacteristicsType characteristic) { + mCharacteristics |= characteristic; } + CharacteristicsType getCharacteristic() const { return mCharacteristics; } + bool isCharacteristicSet(CharacteristicsType characteristic) const { return (mCharacteristics & characteristic) != 0; } + std::string debugCharacteristicString() const; + void removeAllChildren(); void appendChild(const LayerPtr& layer); + void appendChildren(const std::vector& children); const std::vector& children() const { return mChildren; } void setContent(const NodePtr& node); const NodePtr& content() const { return mContent; } + /** + * The content node does not always have the same origin as the layer. + */ + void setContentOffset(const Point& offset); + Point getContentOffset() const { return mContentOffset; } + /** * The bounds of the layer are its outline and position relative to the containing layer. */ @@ -149,6 +176,7 @@ class Layer : public Counter, std::string mName; std::vector mChildren; NodePtr mContent; + Point mContentOffset; // Local transform applied before drawing content Rect mBounds; // The bounds of the layer. The position is the top-left corner in the parent @@ -162,8 +190,9 @@ class Layer : public Counter, AccessibilityPtr mAccessibility; // Accessibility information float mOpacity = 1.0f; // Common layer opacity - unsigned mFlags = 0; - unsigned mInteraction = 0; + FlagType mFlags = 0; + InteractionType mInteraction = 0; + CharacteristicsType mCharacteristics = 0; }; diff --git a/aplcore/include/apl/scenegraph/modifiednodelist.h b/aplcore/include/apl/scenegraph/modifiednodelist.h deleted file mode 100644 index 3e45f77..0000000 --- a/aplcore/include/apl/scenegraph/modifiednodelist.h +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0/ - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -#ifndef _APL_MODIFIED_NODE_LIST_H -#define _APL_MODIFIED_NODE_LIST_H - -#include "apl/scenegraph/common.h" - -namespace apl { -namespace sg { - -/** - * Stores a simple linked list of nodes that have been modified. This is an intrusive list; - * the individual nodes have pointers to the next item in the list. We guarantee that nodes - * are only added once to the list. - * - * When the list is cleared, the flags of all the nodes in the list are cleared. - */ -class ModifiedNodeList { -public: - ModifiedNodeList(); - ~ModifiedNodeList(); - - void contentChanged(Node *node); - void clear(); - -private: - NodePtr mModified; -}; - -} // namespace sg -} // namespace apl - -#endif // _APL_MODIFIED_NODE_LIST_H diff --git a/aplcore/include/apl/scenegraph/node.h b/aplcore/include/apl/scenegraph/node.h index 6429a71..67b37ff 100644 --- a/aplcore/include/apl/scenegraph/node.h +++ b/aplcore/include/apl/scenegraph/node.h @@ -52,7 +52,6 @@ class Node : public Counter, public NonCopyable, public std::enable_shared_from_this { friend class SceneGraphUpdates; - friend class ModifiedNodeList; friend class Layer; public: @@ -79,7 +78,7 @@ class Node : public Counter, * Set the child of this node. * @param child The child */ - void setChild(const NodePtr& child); + bool setChild(const NodePtr& child); /** * Set the sibling of this node @@ -87,6 +86,15 @@ class Node : public Counter, */ NodePtr setNext(const NodePtr& sibling); + /** + * Add a sibling to the chain of siblings pointed to by this node. + * The sibling is added to the end of the chain. + * @param head The head of the sibling chain. May be null. + * @param sibling The sibling to add to the end of the chain + * @return The head of the chain + */ + static NodePtr appendSiblingToNode(const NodePtr& head, const NodePtr& sibling); + /** * Remove all children from this node */ @@ -117,29 +125,29 @@ class Node : public Counter, */ size_t childCount() const; - /** - * Override this in subclasses if needed. - * @return True if this node has changed and needs to be redrawn - */ - virtual bool needsRedraw() const; - /** * @return True if this node draws something on the screen */ virtual bool visible() const; /** - * Calculate the bounding box of this node - * @return The bounding box of this node and all of its children + * Calculate the bounding box of this node including its children and siblings + * @return The bounding box of this node, children, and siblings */ - virtual Rect boundingBox(const Transform2D& transform) const; + Rect boundingBox(const Transform2D& transform) const; /** - * Calculate the bounding box of a node and all siblings + * Calculate the bounding box of a node and all siblings. This is a convenience method that + * works on nullptr nodes. + * * @param node The node to start with * @return A rectangle that just encloses all of this node, siblings, and children */ - static Rect calculateBoundingBox(const NodePtr& node, const Transform2D& transform = Transform2D()); + static Rect calculateBoundingBox(const NodePtr& node, const Transform2D& transform = Transform2D()) { + if (!node) + return {}; + return node->boundingBox(transform); + } /** * Serialize this node @@ -150,22 +158,13 @@ class Node : public Counter, protected: explicit Node(Type type) : mType(type) {} - void setFlag(unsigned flag) { mFlags |= flag; } - bool isFlagSet(unsigned flag) const { return (mFlags & flag) != 0; } - bool anyFlagSet() const { return mFlags != 0; } - void clearFlags() { mFlags = 0; } - - enum { - kNodeFlagChildrenChanged = 1u << 0, - kNodeFlagModified = 1u << 1 - }; + virtual Rect localBoundingBox(const Transform2D& transform) const; const Type mType; NodePtr mFirstChild; NodePtr mNextSibling; NodePtr mNextModified; - unsigned mFlags = 0; }; #define NODE_SUBCLASS(TYPE_NAME, TYPE_ENUM) \ @@ -200,9 +199,10 @@ class DrawNode : public Node { bool setOp(PathOpPtr op); PathOpPtr getOp() const { return mOp; } - bool needsRedraw() const override; bool visible() const override; - Rect boundingBox(const Transform2D& transform) const override; + +protected: + Rect localBoundingBox(const Transform2D& transform) const override; private: PathPtr mPath; @@ -226,9 +226,10 @@ class TextNode : public Node { bool setRange(Range range); Range getRange() const { return mRange; } - bool needsRedraw() const override; bool visible() const override; - Rect boundingBox(const Transform2D& transform) const override; + +protected: + Rect localBoundingBox(const Transform2D& transform) const override; private: TextLayoutPtr mTextLayout; @@ -244,7 +245,9 @@ class TransformNode : public Node { bool setTransform(Transform2D transform); const Transform2D& getTransform() const { return mTransform; } - Rect boundingBox(const Transform2D& transform) const override; + +protected: + Rect localBoundingBox(const Transform2D& transform) const override; private: Transform2D mTransform; @@ -258,7 +261,9 @@ class ClipNode : public Node { bool setPath(PathPtr path); PathPtr getPath() const { return mPath; } - Rect boundingBox(const Transform2D& transform) const override; + +protected: + Rect localBoundingBox(const Transform2D& transform) const override; private: PathPtr mPath; @@ -273,7 +278,6 @@ class OpacityNode : public Node { bool setOpacity(float opacity); float getOpacity() const { return mOpacity; } - bool needsRedraw() const override; bool visible() const override; private: @@ -296,9 +300,10 @@ class ImageNode : public Node { bool setSource(Rect source); Rect getSource() const { return mSource; } - bool needsRedraw() const override; bool visible() const override; - Rect boundingBox(const Transform2D& transform) const override; + +protected: + Rect localBoundingBox(const Transform2D& transform) const override; private: FilterPtr mImage; @@ -321,9 +326,10 @@ class VideoNode : public Node { bool setScale(VideoScale scale); VideoScale getScale() const { return mScale; } - bool needsRedraw() const override; bool visible() const override; - Rect boundingBox(const Transform2D& transform) const override; + +protected: + Rect localBoundingBox(const Transform2D& transform) const override; private: MediaPlayerPtr mPlayer; @@ -339,7 +345,9 @@ class ShadowNode : public Node { bool setShadow(ShadowPtr shadow); ShadowPtr getShadow() const { return mShadow; } - Rect boundingBox(const Transform2D& transform) const override; + +protected: + Rect localBoundingBox(const Transform2D& transform) const override; private: ShadowPtr mShadow; @@ -363,9 +371,7 @@ class EditTextNode : public Node { bool setText(const std::string& text); std::string getText() const { return mText; } - bool needsRedraw() const override; bool visible() const override; - Rect boundingBox(const Transform2D& transform) const override; private: EditTextPtr mEditText; diff --git a/aplcore/include/apl/scenegraph/scenegraphupdates.h b/aplcore/include/apl/scenegraph/scenegraphupdates.h index 1679edd..01c1962 100644 --- a/aplcore/include/apl/scenegraph/scenegraphupdates.h +++ b/aplcore/include/apl/scenegraph/scenegraphupdates.h @@ -41,17 +41,18 @@ class SceneGraphUpdates { bool empty() { return mChanged.empty() && mCreated.empty(); } void changed(const LayerPtr& layer); - void changed(Layer *layer); - void created(const LayerPtr& layer); - void created(Layer *layer); + void resize(const LayerPtr& layer); + + void mapChanged(const std::function& func); - void mapChanged(std::function func); - void mapCreated(std::function func); + void fixCreatedFlags(); + void processResize(); private: std::set mChanged; std::set mCreated; + std::set mResize; }; } // namespace sg diff --git a/aplcore/include/apl/scenegraph/textlayout.h b/aplcore/include/apl/scenegraph/textlayout.h index 6534a80..e77bb6f 100644 --- a/aplcore/include/apl/scenegraph/textlayout.h +++ b/aplcore/include/apl/scenegraph/textlayout.h @@ -43,6 +43,13 @@ class TextLayout { virtual unsigned int getByteLength() const = 0; virtual Range getLineRangeFromByteRange(Range byteRange) const = 0; + + /** + * Calculate the bounding box that surrounds a range of lines in this text layout. + * If the range is empty then the bounding box surrounding all lines is returned. + * @param lineRange The range of lines to use. + * @return The bounding box of those lines or the bounding box of all lines if the range is empty. + */ virtual Rect getBoundingBoxForLines(Range lineRange) const = 0; virtual rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const { diff --git a/aplcore/include/apl/touch/gestures/scrollgesture.h b/aplcore/include/apl/touch/gestures/scrollgesture.h index 6017de5..b1d0c28 100644 --- a/aplcore/include/apl/touch/gestures/scrollgesture.h +++ b/aplcore/include/apl/touch/gestures/scrollgesture.h @@ -19,9 +19,9 @@ #include "apl/common.h" #include "apl/component/scrollablecomponent.h" #include "apl/primitives/object.h" +#include "apl/time/executionresourceholder.h" #include "apl/touch/gestures/flinggesture.h" #include "apl/touch/utils/autoscroller.h" -#include "apl/time/executionresourceholder.h" namespace apl { diff --git a/aplcore/include/apl/utils/identifier.h b/aplcore/include/apl/utils/identifier.h new file mode 100644 index 0000000..d611de7 --- /dev/null +++ b/aplcore/include/apl/utils/identifier.h @@ -0,0 +1,55 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +#ifndef _APL_IDENTIFIER_H +#define _APL_IDENTIFIER_H + +#include + +namespace apl { + +// Note: The isalpha() and isalnum() methods from may take into account locale. +// The std::regex library is slower than an explicit character-by-character check. +// A faster approach would be to unwrap a 128 or 256 byte array (or bit array) and pre-populate +// it with the valid characters. This approach uses interval checking. +inline bool isIdentifierStart(unsigned char p) +{ + return p == '_' || (p >= 'A' && p <= 'Z') || (p >= 'a' && p <= 'z'); +} + +inline bool isIdentifierTail(unsigned char p) { + return p == '_' || (p >= 'A' && p <= 'Z') || (p >= 'a' && p <= 'z') || (p >= '0' && p <= '9'); +} + +inline bool isValidIdentifier(const std::string& s) +{ + if (s.empty()) + return false; + + auto it = s.begin(); + if (!isIdentifierStart(*it)) + return false; + + while (++it != s.end()) + if (!isIdentifierTail(*it)) + return false; + + return true; +} + +} + +#endif // _APL_IDENTIFIER_H \ No newline at end of file diff --git a/aplcore/include/apl/utils/synchronizedweakcache.h b/aplcore/include/apl/utils/synchronizedweakcache.h new file mode 100644 index 0000000..ed30345 --- /dev/null +++ b/aplcore/include/apl/utils/synchronizedweakcache.h @@ -0,0 +1,109 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef _APL_SYNCHRONIZED_WEAK_CACHE_H +#define _APL_SYNCHRONIZED_WEAK_CACHE_H + +#include +#include +#include "apl/utils/weakcache.h" + +namespace apl { + +/** + * A thread-safe wrapper around a WeakCache + */ +template +class SynchronizedWeakCache { + using VPtr = std::shared_ptr; + using guard_t = std::lock_guard; + +public: + SynchronizedWeakCache() {} + + SynchronizedWeakCache(std::initializer_list> args) + : mCache(std::move(args)) {} + + /** + * Find an item in the cache and return it, if it exists. + * @param key The key to look up in the cache + * @return The value if it exists or nullptr + */ + VPtr find(K key) { + guard_t g{mMutex}; + return mCache.find(key); + } + + /** + * Insert a new item in the weak cache. The cache will automatically be cleaned prior to + * insertion if the cache has been marked as dirty. + * @param key The key to add to the cache. + * @param value The value to add + */ + void insert(K key, const VPtr& value) { + guard_t g{mMutex}; + if (mIsDirty.exchange(false)) { + mCache.clean(); + } + mCache.insert(key, value); + } + + /** + * Clean the cache - remove all expired items + */ + void clean() { + guard_t g{mMutex}; + mCache.clean(); + } + + /** + * @return The size of the cache. This method will clean the cache of invalid entries. + */ + size_t size() { + guard_t g{mMutex}; + return mCache.size(); + } + + /** + * @return True if the cache is empty. This method will clean the cache of invalid entries. + */ + bool empty() { + guard_t g{mMutex}; + return mCache.empty(); + } + + /** + * Mark cache as dirty, it will be cleaned during the next call to insert() + */ + void markDirty() { + mIsDirty.store(true); + } + + /** + * @return True if the cache is currently marked as dirty + */ + bool dirty() { + return mIsDirty.load(); + } + +private: + WeakCache mCache; + std::recursive_mutex mMutex; + std::atomic mIsDirty{false}; +}; + +} // namespace apl + +#endif // _APL_SYNCHRONIZED_WEAK_CACHE_H diff --git a/aplcore/src/action/CMakeLists.txt b/aplcore/src/action/CMakeLists.txt index 55b3dda..76c5cf4 100644 --- a/aplcore/src/action/CMakeLists.txt +++ b/aplcore/src/action/CMakeLists.txt @@ -14,14 +14,14 @@ target_sources_local(apl PRIVATE action.cpp - animateitemaction.cpp animatedscrollaction.cpp - autopageaction.cpp + animateitemaction.cpp arrayaction.cpp + autopageaction.cpp controlmediaaction.cpp delayaction.cpp - extensioneventaction.cpp documentaction.cpp + extensioneventaction.cpp openurlaction.cpp playmediaaction.cpp resourceholdingaction.cpp diff --git a/aplcore/src/action/animatedscrollaction.cpp b/aplcore/src/action/animatedscrollaction.cpp index 5e112c3..0dc90b9 100644 --- a/aplcore/src/action/animatedscrollaction.cpp +++ b/aplcore/src/action/animatedscrollaction.cpp @@ -15,8 +15,6 @@ #include "apl/action/animatedscrollaction.h" -#include "apl/animation/animatedproperty.h" -#include "apl/command/corecommand.h" #include "apl/component/scrollablecomponent.h" #include "apl/engine/rootcontext.h" #include "apl/time/sequencer.h" diff --git a/aplcore/src/action/arrayaction.cpp b/aplcore/src/action/arrayaction.cpp index 23f2873..a239f33 100644 --- a/aplcore/src/action/arrayaction.cpp +++ b/aplcore/src/action/arrayaction.cpp @@ -14,9 +14,10 @@ */ #include "apl/action/arrayaction.h" + #include "apl/action/delayaction.h" -#include "apl/command/commandfactory.h" #include "apl/command/arraycommand.h" +#include "apl/command/commandfactory.h" #include "apl/time/sequencer.h" namespace apl { @@ -36,7 +37,7 @@ ArrayAction::ArrayAction(const TimersPtr& timers, std::shared_ptrfinishAllOnTerminate()) { auto& commands = mCommand->commands(); std::vector remaining; - for (int i = mNextIndex ; i < commands.size() ; i++) + for (size_t i = mNextIndex ; i < commands.size() ; i++) remaining.push_back(commands.at(i)); auto context = mCommand->context(); diff --git a/aplcore/src/action/autopageaction.cpp b/aplcore/src/action/autopageaction.cpp index c7c6d41..4ec3f80 100644 --- a/aplcore/src/action/autopageaction.cpp +++ b/aplcore/src/action/autopageaction.cpp @@ -14,11 +14,12 @@ */ #include "apl/action/autopageaction.h" + #include "apl/command/corecommand.h" -#include "apl/time/sequencer.h" -#include "apl/content/rootconfig.h" #include "apl/component/corecomponent.h" #include "apl/component/pagercomponent.h" +#include "apl/content/rootconfig.h" +#include "apl/time/sequencer.h" namespace apl { diff --git a/aplcore/src/action/controlmediaaction.cpp b/aplcore/src/action/controlmediaaction.cpp index 5538731..c882f7a 100644 --- a/aplcore/src/action/controlmediaaction.cpp +++ b/aplcore/src/action/controlmediaaction.cpp @@ -14,6 +14,7 @@ */ #include "apl/action/controlmediaaction.h" + #include "apl/command/corecommand.h" #include "apl/component/videocomponent.h" #include "apl/media/mediaplayer.h" diff --git a/aplcore/src/action/extensioneventaction.cpp b/aplcore/src/action/extensioneventaction.cpp index 53922df..49a3b07 100644 --- a/aplcore/src/action/extensioneventaction.cpp +++ b/aplcore/src/action/extensioneventaction.cpp @@ -14,6 +14,7 @@ */ #include "apl/action/extensioneventaction.h" + #include "apl/command/extensioneventcommand.h" #include "apl/content/extensioncommanddefinition.h" #include "apl/content/rootconfig.h" diff --git a/aplcore/src/action/playmediaaction.cpp b/aplcore/src/action/playmediaaction.cpp index ef9e2b9..e5bd8ad 100644 --- a/aplcore/src/action/playmediaaction.cpp +++ b/aplcore/src/action/playmediaaction.cpp @@ -14,6 +14,7 @@ */ #include "apl/action/playmediaaction.h" + #include "apl/command/corecommand.h" #include "apl/component/videocomponent.h" #include "apl/media/mediaplayer.h" diff --git a/aplcore/src/action/scrolltoaction.cpp b/aplcore/src/action/scrolltoaction.cpp index 9f2585b..50b62b5 100644 --- a/aplcore/src/action/scrolltoaction.cpp +++ b/aplcore/src/action/scrolltoaction.cpp @@ -14,10 +14,11 @@ */ #include "apl/action/scrolltoaction.h" + #include "apl/command/corecommand.h" -#include "apl/time/sequencer.h" #include "apl/component/pagercomponent.h" #include "apl/engine/rootcontext.h" +#include "apl/time/sequencer.h" namespace apl { diff --git a/aplcore/src/action/sequentialaction.cpp b/aplcore/src/action/sequentialaction.cpp index c43a911..1a9daa3 100644 --- a/aplcore/src/action/sequentialaction.cpp +++ b/aplcore/src/action/sequentialaction.cpp @@ -14,6 +14,7 @@ */ #include "apl/action/sequentialaction.h" + #include "apl/action/delayaction.h" #include "apl/command/commandfactory.h" #include "apl/time/sequencer.h" @@ -51,15 +52,15 @@ SequentialAction::SequentialAction(const TimersPtr& timers, std::vector commands; if (!mStateFinally) { auto catchCommands = mCommand->getValue(kCommandPropertyCatch); - for (int i = 0; i < catchCommands.size(); i++) + for (size_t i = 0; i < catchCommands.size(); i++) commands.push_back(catchCommands.at(i)); auto finallyCommands = mCommand->getValue(kCommandPropertyFinally); - for (int i = 0; i < finallyCommands.size() ; i++) + for (size_t i = 0; i < finallyCommands.size() ; i++) commands.push_back(finallyCommands.at(i)); } else { auto finallyCommands = mCommand->getValue(kCommandPropertyFinally); - for (int i = mNextIndex; i < finallyCommands.size() ; i++) + for (size_t i = mNextIndex; i < finallyCommands.size() ; i++) commands.push_back(finallyCommands.at(i)); } diff --git a/aplcore/src/action/setpageaction.cpp b/aplcore/src/action/setpageaction.cpp index 20bda65..8b7dc43 100644 --- a/aplcore/src/action/setpageaction.cpp +++ b/aplcore/src/action/setpageaction.cpp @@ -14,9 +14,10 @@ */ #include "apl/action/setpageaction.h" + #include "apl/command/corecommand.h" -#include "apl/time/sequencer.h" #include "apl/component/pagercomponent.h" +#include "apl/time/sequencer.h" namespace apl { diff --git a/aplcore/src/action/speakitemaction.cpp b/aplcore/src/action/speakitemaction.cpp index b839dc4..a07dc50 100644 --- a/aplcore/src/action/speakitemaction.cpp +++ b/aplcore/src/action/speakitemaction.cpp @@ -14,19 +14,22 @@ */ #include "apl/action/speakitemaction.h" + #include "apl/action/scrolltoaction.h" #include "apl/audio/audioplayerfactory.h" #include "apl/command/commandproperties.h" #include "apl/command/corecommand.h" #include "apl/component/textcomponent.h" #include "apl/engine/rootcontext.h" +#include "apl/primitives/styledtext.h" +#include "apl/time/sequencer.h" +#include "apl/utils/make_unique.h" +#include "apl/utils/principal_ptr.h" + #ifdef SCENEGRAPH #include "apl/scenegraph/textlayout.h" #include "apl/scenegraph/textmeasurement.h" #endif // SCENEGRAPH -#include "apl/time/sequencer.h" -#include "apl/utils/make_unique.h" -#include "apl/utils/principal_ptr.h" static const bool DEBUG_SPEAK_ITEM = false; @@ -142,7 +145,6 @@ class SpeakItemActionPrivate { if (mAudioPlayer) { mAudioPlayer->setTrack(MediaTrack{ action.mSource, // URL - {}, // Headers 0, // Start 0, // Duration (play the entire track) 0, // Repeat count diff --git a/aplcore/src/animation/coreeasing.cpp b/aplcore/src/animation/coreeasing.cpp index 11216b1..07aca1c 100644 --- a/aplcore/src/animation/coreeasing.cpp +++ b/aplcore/src/animation/coreeasing.cpp @@ -19,28 +19,9 @@ #include "apl/animation/coreeasing.h" #include "apl/animation/easingapproximation.h" #include "apl/utils/stringfunctions.h" -#include "apl/utils/weakcache.h" namespace apl { -// TODO: This is not thread safe -static WeakCache sEasingAppoxCache; - -std::string -dofSig(int dof, const float* array) { - std::string result = "x" + sutil::to_string(array[0]); - for (int i = 1; i < dof; i++) - result += "," + sutil::to_string(array[i]); - return result; -} - -std::string -easingApproxSignature(int dof, const float* start, const float* tout, const float* tin, - const float* end) { - return std::to_string(dof) + dofSig(dof, start) + dofSig(dof, tout) + dofSig(dof, tin) + - dofSig(dof, end); -} - // Cubic static inline float f(float a, float b, float t) { @@ -192,16 +173,7 @@ CoreEasing::ensureApproximation(std::vector::iterator it) const float* tin = segment.tin(); const float* end = segment.end(); - auto sig = easingApproxSignature(segment.dof(), start, tout, tin, end); - auto ptr = sEasingAppoxCache.find(sig); - if (ptr) - return ptr; - - ptr = EasingApproximation::create(segment.dof(), start, tout, tin, end, EASING_POINTS); - it->data = ptr; - - sEasingAppoxCache.insert(sig, ptr); - return ptr; + return EasingApproximation::getOrCreate(segment.dof(), start, tout, tin, end, EASING_POINTS); } float diff --git a/aplcore/src/animation/easing.cpp b/aplcore/src/animation/easing.cpp index a665a6c..13f571b 100644 --- a/aplcore/src/animation/easing.cpp +++ b/aplcore/src/animation/easing.cpp @@ -16,21 +16,20 @@ #include "apl/animation/coreeasing.h" #include "apl/animation/easinggrammar.h" #include "apl/utils/session.h" -#include "apl/utils/weakcache.h" +#include "apl/utils/synchronizedweakcache.h" namespace apl { namespace pegtl = tao::TAO_PEGTL_NAMESPACE; // These easing curves are predefined and available at all times -static auto sLinear = CoreEasing::linear(); -static auto sEase = CoreEasing::bezier(0.25, 0.10, 0.25, 1); -static auto sEaseIn = CoreEasing::bezier(0.42, 0, 1, 1); -static auto sEaseOut = CoreEasing::bezier(0, 0, 0.58, 1); -static auto sEaseInOut = CoreEasing::bezier(0.42, 0, 0.58, 1); - -// TODO: This is not thread safe -static WeakCache sEasingCache = { +static const auto sLinear = CoreEasing::linear(); +static const auto sEase = CoreEasing::bezier(0.25, 0.10, 0.25, 1); +static const auto sEaseIn = CoreEasing::bezier(0.42, 0, 1, 1); +static const auto sEaseOut = CoreEasing::bezier(0, 0, 0.58, 1); +static const auto sEaseInOut = CoreEasing::bezier(0.42, 0, 0.58, 1); + +static SynchronizedWeakCache sEasingCache = { {"linear", sLinear}, {"ease", sEase}, {"ease-in", sEaseIn}, @@ -38,8 +37,6 @@ static WeakCache sEasingCache = { {"ease-in-out", sEaseInOut} }; -static bool sEasingCacheDirty = false; // Mark the cache as dirty when an easing curve is deleted - EasingPtr Easing::parse(const SessionPtr& session, const std::string& easing) { @@ -64,11 +61,6 @@ Easing::parse(const SessionPtr& session, const std::string& easing) CONSOLE(session) << "Unable to create easing curve " << easing; } else { - if (sEasingCacheDirty) { - sEasingCache.clean(); - sEasingCacheDirty = false; - } - sEasingCache.insert(s, easingCurve); return easingCurve; } @@ -93,8 +85,7 @@ Easing::has(const char *easing) } Easing::~Easing() noexcept { - sEasingCacheDirty = true; - + sEasingCache.markDirty(); } Object diff --git a/aplcore/src/animation/easingapproximation.cpp b/aplcore/src/animation/easingapproximation.cpp index 6c437d0..0e3dcc0 100644 --- a/aplcore/src/animation/easingapproximation.cpp +++ b/aplcore/src/animation/easingapproximation.cpp @@ -15,9 +15,26 @@ #include "apl/animation/easingapproximation.h" +#include "apl/utils/synchronizedweakcache.h" #include "apl/utils/log.h" namespace apl { +static SynchronizedWeakCache sEasingAppoxCache; + +std::string +dofSig(int dof, const float* array) { + std::string result = "x" + sutil::to_string(array[0]); + for (int i = 1; i < dof; i++) + result += "," + sutil::to_string(array[i]); + return result; +} + +std::string +easingApproxSignature(int dof, const float* start, const float* tout, const float* tin, + const float* end) { + return std::to_string(dof) + dofSig(dof, start) + dofSig(dof, tout) + dofSig(dof, tin) + + dofSig(dof, end); +} static float cubic( float a, float b, float c, float d, float t) { @@ -47,13 +64,17 @@ static float cubic( float a, float b, float c, float d, float t) * ... ] */ std::shared_ptr -EasingApproximation::create(int dof, - const float *start, - const float *tout, - const float *tin, - const float *end, - int divisions) +EasingApproximation::getOrCreate(int dof, + const float *start, + const float *tout, + const float *tin, + const float *end, + int divisions) { + auto sig = easingApproxSignature(dof, start, tout, tin, end); + if (auto ptr = sEasingAppoxCache.find(sig)) + return ptr; + assert(dof >= 1); assert(divisions >= 2); @@ -91,7 +112,9 @@ EasingApproximation::create(int dof, *cptr++ += cumulativeLength; } - return std::make_shared(dof, std::move(data), std::move(cumulative)); + auto ptr = std::make_shared(dof, std::move(data), std::move(cumulative)); + sEasingAppoxCache.insert(sig, ptr); + return ptr; } /** @@ -144,4 +167,8 @@ EasingApproximation::getPosition(float percentage, int coordinate) return startValue + (endValue - startValue) * segmentFraction; } +EasingApproximation::~EasingApproximation() { + sEasingAppoxCache.markDirty(); +} + } // namespace apl diff --git a/aplcore/src/command/CMakeLists.txt b/aplcore/src/command/CMakeLists.txt index 473c9e9..b3f4e4c 100644 --- a/aplcore/src/command/CMakeLists.txt +++ b/aplcore/src/command/CMakeLists.txt @@ -35,11 +35,11 @@ target_sources_local(apl scrolltoindexcommand.cpp selectcommand.cpp sendeventcommand.cpp + sequentialcommand.cpp setfocuscommand.cpp setpagecommand.cpp setstatecommand.cpp setvaluecommand.cpp - sequentialcommand.cpp speakitemcommand.cpp speaklistcommand.cpp ) diff --git a/aplcore/src/command/corecommand.cpp b/aplcore/src/command/corecommand.cpp index 412748f..93d99eb 100644 --- a/aplcore/src/command/corecommand.cpp +++ b/aplcore/src/command/corecommand.cpp @@ -16,19 +16,17 @@ #include #include "apl/command/corecommand.h" -#include "apl/component/componenteventtargetwrapper.h" - -#include "apl/utils/dump_object.h" -#include "apl/utils/session.h" #include "apl/command/animateitemcommand.h" #include "apl/command/autopagecommand.h" #include "apl/command/clearfocuscommand.h" #include "apl/command/controlmediacommand.h" +#include "apl/command/finishcommand.h" #include "apl/command/idlecommand.h" #include "apl/command/openurlcommand.h" #include "apl/command/parallelcommand.h" #include "apl/command/playmediacommand.h" +#include "apl/command/reinflatecommand.h" #include "apl/command/scrollcommand.h" #include "apl/command/scrolltocomponentcommand.h" #include "apl/command/scrolltoindexcommand.h" @@ -41,11 +39,12 @@ #include "apl/command/setvaluecommand.h" #include "apl/command/speakitemcommand.h" #include "apl/command/speaklistcommand.h" -#include "apl/command/finishcommand.h" -#include "apl/command/reinflatecommand.h" - +#include "apl/component/componenteventtargetwrapper.h" +#include "apl/component/selector.h" #include "apl/engine/layoutmanager.h" #include "apl/engine/rootcontext.h" +#include "apl/utils/dump_object.h" +#include "apl/utils/session.h" namespace apl { @@ -128,6 +127,8 @@ CoreCommand::freeze() mTargetId = mTarget->getId(); } + mMissingTargetId = mTarget && mTargetId.empty(); + // TODO: Need to see if any "better" re-evaluation required. We effectively keep copy of current // context, so any bindings may be "obsolete" after rehydration. auto event = mContext->opt("event"); @@ -160,6 +161,10 @@ CoreCommand::rehydrate(const RootContext& context) if (!mTarget) return false; } + if (mMissingTargetId) { + return false; + } + mFrozenEventContext = nullptr; mFrozen = false; return true; @@ -229,7 +234,7 @@ CoreCommand::calculateProperties() std::string id; if (p != mProperties.end()) { id = evaluate(*mContext, p->second).asString(); - mTarget = CoreComponent::cast(mContext->findComponentById(id)); + mTarget = Selector::resolve(id, mContext, mBase); } if (mTarget == nullptr && (cpd->second.flags & kPropRequired)) { @@ -238,7 +243,7 @@ CoreCommand::calculateProperties() LOG(LogLevel::kWarn).session(mContext) << "Trying to scroll to uninflated component. Flushing pending layouts."; auto& lm = mContext->layoutManager(); lm.flushLazyInflation(); - mTarget = CoreComponent::cast(mContext->findComponentById(id)); + mTarget = Selector::resolve(id, mContext, mBase); if (mTarget == nullptr) { CONSOLE(mContext) << "Illegal command " << name() << " - need to specify a target componentId"; return false; diff --git a/aplcore/src/command/sendeventcommand.cpp b/aplcore/src/command/sendeventcommand.cpp index 71f5f10..d56625d 100644 --- a/aplcore/src/command/sendeventcommand.cpp +++ b/aplcore/src/command/sendeventcommand.cpp @@ -14,9 +14,10 @@ */ #include "apl/command/sendeventcommand.h" +#include "apl/component/selector.h" #include "apl/content/rootconfig.h" -#include "apl/utils/log.h" #include "apl/utils/dump_object.h" +#include "apl/utils/log.h" #include "apl/utils/session.h" namespace apl { @@ -62,9 +63,11 @@ SendEventCommand::execute(const TimersPtr& timers, bool fastMode) { // Calculate the component map auto componentsMap = std::make_shared(); - for (auto& compId : mValues.at(kCommandPropertyComponents).getArray()) { + auto arrays = mValues.at(kCommandPropertyComponents).getArray(); + for (auto& compId : arrays) { if (compId.isString()) { - auto comp = CoreComponent::cast(mContext->findComponentById(compId.getString())); + auto selector = evaluate(*mContext, compId).asString(); + auto comp = Selector::resolve(selector, mContext, mBase); if (comp) { componentsMap->emplace(compId.getString(), comp->getValue()); } diff --git a/aplcore/src/component/CMakeLists.txt b/aplcore/src/component/CMakeLists.txt index 3745fc2..fa12064 100644 --- a/aplcore/src/component/CMakeLists.txt +++ b/aplcore/src/component/CMakeLists.txt @@ -15,9 +15,9 @@ target_sources_local(apl PRIVATE actionablecomponent.cpp component.cpp - componenteventwrapper.cpp componenteventsourcewrapper.cpp componenteventtargetwrapper.cpp + componenteventwrapper.cpp componentproperties.cpp containercomponent.cpp corecomponent.cpp @@ -30,6 +30,7 @@ target_sources_local(apl pagercomponent.cpp scrollablecomponent.cpp scrollviewcomponent.cpp + selector.cpp sequencecomponent.cpp textcomponent.cpp textmeasurement.cpp diff --git a/aplcore/src/component/actionablecomponent.cpp b/aplcore/src/component/actionablecomponent.cpp index 6cd1961..5f0e37c 100644 --- a/aplcore/src/component/actionablecomponent.cpp +++ b/aplcore/src/component/actionablecomponent.cpp @@ -14,16 +14,18 @@ */ #include "apl/component/actionablecomponent.h" + #include "apl/component/componentpropdef.h" #include "apl/content/rootconfig.h" #include "apl/engine/keyboardmanager.h" #include "apl/focus/focusmanager.h" #include "apl/primitives/keyboard.h" +#include "apl/time/sequencer.h" +#include "apl/touch/gesture.h" + #ifdef SCENEGRAPH #include "apl/scenegraph/layer.h" #endif // SCENEGRAPH -#include "apl/time/sequencer.h" -#include "apl/touch/gesture.h" namespace apl { diff --git a/aplcore/src/component/component.cpp b/aplcore/src/component/component.cpp index 3b8f1fd..cb9b202 100644 --- a/aplcore/src/component/component.cpp +++ b/aplcore/src/component/component.cpp @@ -14,6 +14,7 @@ */ #include "apl/component/component.h" + #include "apl/engine/context.h" #include "apl/utils/log.h" diff --git a/aplcore/src/component/containercomponent.cpp b/aplcore/src/component/containercomponent.cpp index b7f7bae..1e392b9 100644 --- a/aplcore/src/component/containercomponent.cpp +++ b/aplcore/src/component/containercomponent.cpp @@ -13,8 +13,9 @@ * permissions and limitations under the License. */ -#include "apl/component/componentpropdef.h" #include "apl/component/containercomponent.h" + +#include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" namespace apl { diff --git a/aplcore/src/component/corecomponent.cpp b/aplcore/src/component/corecomponent.cpp index b2bb844..2b4b8e7 100644 --- a/aplcore/src/component/corecomponent.cpp +++ b/aplcore/src/component/corecomponent.cpp @@ -13,15 +13,16 @@ * permissions and limitations under the License. */ -#include +#include "apl/component/corecomponent.h" #include +#include #include "apl/common.h" + #include "apl/component/componenteventsourcewrapper.h" #include "apl/component/componenteventtargetwrapper.h" #include "apl/component/componentpropdef.h" -#include "apl/component/corecomponent.h" #include "apl/component/yogaproperties.h" #include "apl/content/rootconfig.h" #include "apl/engine/builder.h" @@ -36,20 +37,22 @@ #include "apl/livedata/livearrayobject.h" #include "apl/primitives/accessibilityaction.h" #include "apl/primitives/keyboard.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/builder.h" -#include "apl/scenegraph/scenegraph.h" -#endif // SCENEGRAPH +#include "apl/primitives/transform.h" #include "apl/time/sequencer.h" #include "apl/time/timemanager.h" #include "apl/touch/pointerevent.h" #include "apl/utils/hash.h" #include "apl/utils/searchvisitor.h" #include "apl/utils/session.h" -#include "apl/utils/stickyfunctions.h" #include "apl/utils/stickychildrentree.h" +#include "apl/utils/stickyfunctions.h" #include "apl/utils/tracing.h" +#ifdef SCENEGRAPH +#include "apl/scenegraph/builder.h" +#include "apl/scenegraph/scenegraph.h" +#endif // SCENEGRAPH + namespace apl { const std::string VISUAL_CONTEXT_TYPE_MIXED = "mixed"; @@ -732,7 +735,6 @@ CoreComponent::getSceneGraph(sg::SceneGraphUpdates& sceneGraph) mSceneGraphLayer->appendChild(m->getSceneGraph(sceneGraph)); mSceneGraphLayer->clearFlags(); - sceneGraph.created(mSceneGraphLayer); } return mSceneGraphLayer; @@ -805,7 +807,7 @@ CoreComponent::updateSceneGraph(sg::SceneGraphUpdates& sceneGraph) layer->setFlag(sg::Layer::kFlagRedrawContent); // Transfer any layer changes to the change map - sceneGraph.changed(layer); + sceneGraph.changed(mSceneGraphLayer); // Clear all of the dirty flags on the component clearDirty(); @@ -817,6 +819,7 @@ CoreComponent::constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGraph) auto layer = sg::layer(mUniqueId, getCalculated(kPropertyBounds).get(), static_cast(getCalculated(kPropertyOpacity).asNumber()), getCalculated(kPropertyTransform).get()); + sceneGraph.created(layer); if (getCalculated(kPropertyDisabled).truthy()) layer->setInteraction(sg::Layer::kInteractionDisabled); diff --git a/aplcore/src/component/edittextcomponent.cpp b/aplcore/src/component/edittextcomponent.cpp index dec95d0..133d037 100644 --- a/aplcore/src/component/edittextcomponent.cpp +++ b/aplcore/src/component/edittextcomponent.cpp @@ -14,26 +14,27 @@ */ #include "apl/component/edittextcomponent.h" + #include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" #include "apl/content/rootconfig.h" #include "apl/engine/event.h" #include "apl/focus/focusmanager.h" #include "apl/primitives/unicode.h" +#include "apl/time/sequencer.h" +#include "apl/touch/pointerevent.h" #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" -#include "apl/scenegraph/edittextconfig.h" #include "apl/scenegraph/edittext.h" +#include "apl/scenegraph/edittextbox.h" +#include "apl/scenegraph/edittextconfig.h" #include "apl/scenegraph/edittextfactory.h" #include "apl/scenegraph/scenegraphupdates.h" -#include "apl/scenegraph/edittextbox.h" #include "apl/scenegraph/textchunk.h" #include "apl/scenegraph/textlayout.h" #include "apl/scenegraph/textmeasurement.h" #include "apl/scenegraph/utilities.h" #endif // SCENEGRAPH -#include "apl/time/sequencer.h" -#include "apl/touch/pointerevent.h" namespace apl { @@ -503,6 +504,8 @@ EditTextComponent::constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGraph) // Construct an inner layer for the actual edit text auto innerLayer = sg::layer(mUniqueId + "-innerEditText", innerBounds, 1.0, Transform2D()); + sceneGraph.created(innerLayer); + innerLayer->setContent(sg::editText(mEditText.getPtr(), mEditTextBox, mEditTextConfig, getCalculated(kPropertyText).getString())); innerLayer->clearFlags(); diff --git a/aplcore/src/component/framecomponent.cpp b/aplcore/src/component/framecomponent.cpp index 37b2e27..1aed076 100644 --- a/aplcore/src/component/framecomponent.cpp +++ b/aplcore/src/component/framecomponent.cpp @@ -13,9 +13,11 @@ * permissions and limitations under the License. */ -#include "apl/component/componentpropdef.h" #include "apl/component/framecomponent.h" + +#include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" +#include "apl/primitives/color.h" #include "apl/primitives/radii.h" #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" diff --git a/aplcore/src/component/gridsequencecomponent.cpp b/aplcore/src/component/gridsequencecomponent.cpp index 8db99c2..4ac6c8f 100644 --- a/aplcore/src/component/gridsequencecomponent.cpp +++ b/aplcore/src/component/gridsequencecomponent.cpp @@ -12,10 +12,11 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ -#include +#include "apl/component/gridsequencecomponent.h" + +#include #include "apl/component/componentpropdef.h" -#include "apl/component/gridsequencecomponent.h" #include "apl/component/yogaproperties.h" #include "apl/content/rootconfig.h" diff --git a/aplcore/src/component/imagecomponent.cpp b/aplcore/src/component/imagecomponent.cpp index 2c75347..35e4174 100644 --- a/aplcore/src/component/imagecomponent.cpp +++ b/aplcore/src/component/imagecomponent.cpp @@ -13,17 +13,18 @@ * permissions and limitations under the License. */ -#include "apl/component/componentpropdef.h" #include "apl/component/imagecomponent.h" + +#include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" #include "apl/content/rootconfig.h" #include "apl/engine/event.h" #include "apl/media/mediamanager.h" +#include "apl/time/sequencer.h" #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" #include "apl/scenegraph/scenegraph.h" #endif // SCENEGRAPH -#include "apl/time/sequencer.h" namespace apl { diff --git a/aplcore/src/component/mediacomponenttrait.cpp b/aplcore/src/component/mediacomponenttrait.cpp index 02fcf91..cdb94b9 100644 --- a/aplcore/src/component/mediacomponenttrait.cpp +++ b/aplcore/src/component/mediacomponenttrait.cpp @@ -14,6 +14,7 @@ */ #include "apl/component/mediacomponenttrait.h" + #include "apl/component/componentpropdef.h" #include "apl/content/rootconfig.h" #include "apl/media/mediamanager.h" diff --git a/aplcore/src/component/multichildscrollablecomponent.cpp b/aplcore/src/component/multichildscrollablecomponent.cpp index 648c56b..78c9189 100644 --- a/aplcore/src/component/multichildscrollablecomponent.cpp +++ b/aplcore/src/component/multichildscrollablecomponent.cpp @@ -13,8 +13,9 @@ * permissions and limitations under the License. */ -#include "apl/component/componentpropdef.h" #include "apl/component/multichildscrollablecomponent.h" + +#include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" #include "apl/content/rootconfig.h" #include "apl/engine/layoutmanager.h" diff --git a/aplcore/src/component/pagercomponent.cpp b/aplcore/src/component/pagercomponent.cpp index a453ba6..a11a19a 100644 --- a/aplcore/src/component/pagercomponent.cpp +++ b/aplcore/src/component/pagercomponent.cpp @@ -13,8 +13,9 @@ * permissions and limitations under the License. */ -#include "apl/component/componentpropdef.h" #include "apl/component/pagercomponent.h" + +#include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" #include "apl/content/rootconfig.h" #include "apl/engine/layoutmanager.h" @@ -23,10 +24,9 @@ #include "apl/livedata/livearrayobject.h" #include "apl/primitives/keyboard.h" #include "apl/time/sequencer.h" - +#include "apl/time/timemanager.h" #include "apl/touch/gestures/pagerflinggesture.h" #include "apl/touch/utils/pagemovehandler.h" -#include "apl/time/timemanager.h" namespace apl { diff --git a/aplcore/src/component/scrollablecomponent.cpp b/aplcore/src/component/scrollablecomponent.cpp index a05d233..5bc280e 100644 --- a/aplcore/src/component/scrollablecomponent.cpp +++ b/aplcore/src/component/scrollablecomponent.cpp @@ -13,15 +13,16 @@ * permissions and limitations under the License. */ +#include "apl/component/scrollablecomponent.h" + #include "apl/action/scrollaction.h" #include "apl/component/componentpropdef.h" -#include "apl/component/scrollablecomponent.h" #include "apl/component/yogaproperties.h" +#include "apl/content/rootconfig.h" #include "apl/focus/focusmanager.h" #include "apl/time/sequencer.h" -#include "apl/content/rootconfig.h" -#include "apl/touch/gestures/scrollgesture.h" #include "apl/time/timemanager.h" +#include "apl/touch/gestures/scrollgesture.h" #include "apl/utils/stickychildrentree.h" #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" diff --git a/aplcore/src/component/scrollviewcomponent.cpp b/aplcore/src/component/scrollviewcomponent.cpp index 076d82f..437656d 100644 --- a/aplcore/src/component/scrollviewcomponent.cpp +++ b/aplcore/src/component/scrollviewcomponent.cpp @@ -13,10 +13,11 @@ * permissions and limitations under the License. */ +#include "apl/component/scrollviewcomponent.h" + #include "apl/command/commandproperties.h" #include "apl/command/scrollcommand.h" #include "apl/component/componentpropdef.h" -#include "apl/component/scrollviewcomponent.h" #include "apl/component/yogaproperties.h" #include "apl/time/sequencer.h" diff --git a/aplcore/src/component/selector.cpp b/aplcore/src/component/selector.cpp new file mode 100644 index 0000000..5d8a700 --- /dev/null +++ b/aplcore/src/component/selector.cpp @@ -0,0 +1,472 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +#include + +#include "apl/component/corecomponent.h" +#include "apl/component/selector.h" +#include "apl/datagrammar/grammarpolyfill.h" +#include "apl/engine/rootcontextdata.h" +#include "apl/utils/make_unique.h" + +namespace apl { + +// ********************* Grammar definition is isolated *********************** + +namespace selectorgrammar { + +using MatchFunction = std::function; + +static CoreComponentPtr +findChildFromOffset(const CoreComponentPtr& parent, const MatchFunction& matcher, + int start, int delta) +{ + assert(parent); + const auto len = parent->getChildCount(); + for (int i = start ; i >= 0 && i < len ; i += delta) { + auto child = parent->getCoreChildAt(i); + if (matcher(child)) + return child; + } + + return nullptr; +} + +static CoreComponentPtr +findChild(const CoreComponentPtr& parent, const MatchFunction& matcher) +{ + return findChildFromOffset(parent, matcher, 0, 1); +} + +static CoreComponentPtr +findSibling(const CoreComponentPtr& component, const MatchFunction& matcher, int offset) +{ + assert(component); + auto parent = CoreComponent::cast(component->getParent()); + if (!parent) + return nullptr; + + int index; + const auto len = parent->getChildCount(); + for (index = 0 ; index < len ; index++) { + if (parent->getCoreChildAt(index) == component) + break; + } + + return findChildFromOffset(parent, matcher, index + offset, offset); +} + +static CoreComponentPtr +findNext(const CoreComponentPtr& component, const MatchFunction& matcher) +{ + return findSibling(component, matcher, 1); +} + +static CoreComponentPtr +findPrevious(const CoreComponentPtr& component, const MatchFunction& matcher) +{ + return findSibling(component, matcher, -1); +} + +/** + * Iterate up the parent components + */ +static CoreComponentPtr +findParent(const CoreComponentPtr& start, const MatchFunction& matcher) +{ + assert(start); + auto parent = CoreComponent::cast(start->getParent()); + while (parent) { + if (matcher(parent)) + return parent; + parent = CoreComponent::cast(parent->getParent()); + } + return nullptr; +} + +static CoreComponentPtr +findSearch(const CoreComponentPtr& start, const MatchFunction& matcher) +{ + assert(start); + std::vector> stack = {{start, 0}}; + + while (!stack.empty()) { + auto& back = stack.back(); + const auto len = back.first->getChildCount(); + auto index = back.second; + if (index < len) { + auto result = back.first->getCoreChildAt(index); + if (matcher(result)) + return result; + back.second += 1; + stack.emplace_back(std::pair{result, 0}); + } + else { + stack.pop_back(); + } + } + + return nullptr; +} + +enum ModifierType { + kModifierParent, + kModifierChild, + kModifierFind, + kModifierNext, + kModifierPrevious +}; + +enum MatcherType { + kMatchTypeNumeric, + kMatchTypeId, + kMatchTypeType, +}; + +using ModifierFunction = CoreComponentPtr (*)(const CoreComponentPtr&, const MatchFunction&); +static const ModifierFunction sDispatchFunctions[] = { + &findParent, + &findChild, + &findSearch, + &findNext, + &findPrevious +}; + +namespace pegtl = tao::TAO_PEGTL_NAMESPACE; +using namespace pegtl; + +static const bool DEBUG_GRAMMAR = false; + +struct ws : star {}; // White space + +struct str_child : string<'c','h','i','l','d'> {}; +struct str_find : string<'f','i','n','d'> {}; +struct str_next : string<'n','e','x','t'> {}; +struct str_parent : string<'p','a','r','e','n','t'> {}; +struct str_previous : string<'p','r','e','v','i','o','u','s'> {}; + +struct str_root : string<':','r','o','o','t'> {}; +struct str_source : string<':','s','o','u','r','c','e'> {}; + +struct str_id : string<'i','d'> {}; +struct str_type : string<'t','y','p','e'> {}; + +struct key_child : seq> {}; +struct key_find : seq> {}; +struct key_next : seq> {}; +struct key_parent : seq> {}; +struct key_previous : seq> {}; + +struct key_id : seq> {}; + +struct key_source : seq> {}; +struct key_root : seq> {}; + +struct uid : seq, plus> {}; +struct id : plus, one<'_'>>> {}; + +struct zero : if_must, not_at> {}; +struct number_int : sor, star>> {}; +struct number_sign : sor,success> {}; +struct number : seq {}; + +struct value : id {}; +struct key : sor {}; +struct key_value : if_must {}; +struct arg : sor {}; + +struct mod_type : sor {}; +struct modifier : if_must, mod_type, one<'('>, arg, one<')'>> {}; +struct modifiers : list {}; + +struct top_id : id {}; +struct element : sor {}; +struct grammar : must{}; + +// ***************** Error reporting ******************* + +template +struct selector_control : apl_control< Rule > { + static const std::string error_message; + + template + static void raise( const Input& in, fail_state& state) { + state.fail(error_message, in); + } + + template< typename Input> + static void fail( const Input& in, fail_state& state ) { + state.fail(error_message, in); + } +}; + +template<> const std::string selector_control>::error_message = "Expected a left parenthesis after the modifier name"; +template<> const std::string selector_control>::error_message = "Invalid modifier arguments"; +template<> const std::string selector_control::error_message = "Invalid modifier name"; + +// This template will catch any other parsing errors +template const std::string selector_control::error_message = "selector error matching " + pegtl::internal::demangle(); + +// **************** Actions ***********/ + +template +struct action : pegtl::nothing< Rule > { +}; + + +struct selector_state : fail_state { + selector_state(ContextPtr context, CoreComponentPtr target) + : context(std::move(context)), target(std::move(target)) {} + + ContextPtr context; + CoreComponentPtr target; + + ModifierType modifierType = kModifierChild; + MatcherType matcherType = kMatchTypeNumeric; + std::string value; + int number = 0; + bool somethingMatched = false; +}; + + +template<> struct action< number > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + std::string s(in.string()); + auto value = sutil::stoi(s); + LOGF_IF(DEBUG_GRAMMAR, "Number: '%s' -> %d", in.string().c_str(), value); + state.number = value; + } +}; + +template<> struct action< key_root > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.target = CoreComponent::cast(state.context->topComponent()); + state.somethingMatched = true; + LOG_IF(DEBUG_GRAMMAR) << "Root element"; + } +}; + +template<> struct action< key_source > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.somethingMatched = true; + LOG_IF(DEBUG_GRAMMAR) << "Source element"; + } +}; + +template<> struct action< uid > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.target = CoreComponent::cast(state.context->findComponentById(in.string())); + state.somethingMatched = true; + LOGF_IF(DEBUG_GRAMMAR, "Find UID: '%s'", in.string().c_str()); + } +}; + +template<> struct action< top_id > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.target = CoreComponent::cast(state.context->findComponentById(in.string())); + state.somethingMatched = true; + LOGF_IF(DEBUG_GRAMMAR, "Find ID: '%s'", in.string().c_str()); + } +}; + +template<> struct action< value > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.value = in.string(); + LOG_IF(DEBUG_GRAMMAR) << "Value '" << state.value << "'"; + } +}; + +template<> struct action< key_id > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.matcherType = kMatchTypeId; + LOG_IF(DEBUG_GRAMMAR) << "Match id"; + } +}; + +template<> struct action< key_type > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.matcherType = kMatchTypeType; + LOG_IF(DEBUG_GRAMMAR) << "Match type"; + } +}; + +template<> struct action< key_parent > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.modifierType = kModifierParent; + LOG_IF(DEBUG_GRAMMAR) << "Modifier parent"; + } +}; + +template<> struct action< key_child > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.modifierType = kModifierChild; + LOG_IF(DEBUG_GRAMMAR) << "Modifier child"; + } +}; + +template<> struct action< key_find > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.modifierType = kModifierFind; + LOG_IF(DEBUG_GRAMMAR) << "Modifier find"; + } +}; + +template<> struct action< key_next > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.modifierType = kModifierNext; + LOG_IF(DEBUG_GRAMMAR) << "Modifier next"; + } +}; + +template<> struct action< key_previous > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + state.modifierType = kModifierPrevious; + LOG_IF(DEBUG_GRAMMAR) << "Modifier previous"; + } +}; + +/** + * The component type can match a primitive type (like "Text", "VectorGraphic") or it can match + * the name of a layout that was inflated and created this component (like "RadioButton"). The + * data-binding context associated with the component may have "__name" and "__source" properties + * assigned. When __source=component, the __name property is the component name (like "Text"). + * When __source=layout, the __name property is the name of the layout. + * + * @param component The component whose type is being checked. + * @param type The desired type string. + * @return True if this component matches the type or if the layout wrapping this component matches + * the type. + */ +static bool matchComponentType(const CoreComponentPtr& component, const std::string& type) +{ + // The first context should have __source=component + auto context = component->getContext(); + assert(context->opt("__source").asString() == "component"); + + // Check the __name property for a quick match + auto name = context->opt("__name"); + if (name.isString() && name.getString() == type) + return true; + + // Search upwards through the context for a layout that matches. + while ((context = context->parent()) != nullptr) { + auto source = context->opt("__source"); + if (!source.isString()) + return false; + + // This context is connected to a different component. We are done searching + if (source.getString() == "component") + return false; + + // We match if a containing layout has this name + if (source.getString() == "layout" && context->opt("__name").asString() == type) + return true; + } + + return false; +} + +template<> struct action< modifier > +{ + template< typename Input > + static void apply( const Input& in, selector_state& state) { + if (!state.target) + return; + + // The :child(NUMBER) match is a special pattern + if (state.modifierType == kModifierChild && + state.matcherType == kMatchTypeNumeric) { + auto len = static_cast(state.target->getChildCount()); + auto index = state.number < 0 ? state.number + len : state.number; + state.target = (index < 0 || index >= len) ? nullptr : state.target->getCoreChildAt(index); + } + else { + auto f = sDispatchFunctions[state.modifierType]; + switch (state.matcherType) { + case kMatchTypeNumeric: + state.target = f(state.target, [&](const CoreComponentPtr& component) { + return --state.number <= 0; + }); + break; + case kMatchTypeId: + state.target = f(state.target, [&](const CoreComponentPtr& component) { + return component->getId() == state.value; + }); + break; + case kMatchTypeType: + state.target = f(state.target, [&](const CoreComponentPtr& component) { + return matchComponentType(component, state.value); + }); + break; + } + } + + // Clean patterns for the next modifier + state.matcherType = kMatchTypeNumeric; + state.value.clear(); + state.number = 0; + state.somethingMatched = true; + } +}; + +} // namespace selectorgrammar + +CoreComponentPtr +Selector::resolve(const std::string& string, + const ContextPtr& context, + const CoreComponentPtr& source) +{ + selectorgrammar::selector_state state(context, source); + pegtl::string_input<> in(string, ""); + + if (!pegtl::parse(in, state) || + state.failed || + !state.somethingMatched) { + CONSOLE(context) << "Error parsing selector '" << string << "' " << state.what(); + return nullptr; + } + + return state.target; +} + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/component/sequencecomponent.cpp b/aplcore/src/component/sequencecomponent.cpp index 6e2cd74..b4bf94d 100644 --- a/aplcore/src/component/sequencecomponent.cpp +++ b/aplcore/src/component/sequencecomponent.cpp @@ -13,8 +13,9 @@ * permissions and limitations under the License. */ -#include "apl/component/componentpropdef.h" #include "apl/component/sequencecomponent.h" + +#include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" #include "apl/content/rootconfig.h" diff --git a/aplcore/src/component/textcomponent.cpp b/aplcore/src/component/textcomponent.cpp index 6d0beb2..774956d 100644 --- a/aplcore/src/component/textcomponent.cpp +++ b/aplcore/src/component/textcomponent.cpp @@ -14,9 +14,13 @@ */ #include "apl/component/textcomponent.h" + #include "apl/component/componentpropdef.h" #include "apl/component/textmeasurement.h" #include "apl/content/rootconfig.h" +#include "apl/primitives/styledtext.h" +#include "apl/utils/session.h" + #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" #include "apl/scenegraph/scenegraph.h" @@ -26,7 +30,6 @@ #include "apl/scenegraph/textproperties.h" #include "apl/scenegraph/utilities.h" #endif // SCENEGRAPH -#include "apl/utils/session.h" namespace apl { diff --git a/aplcore/src/component/textmeasurement.cpp b/aplcore/src/component/textmeasurement.cpp index 8653495..988926f 100644 --- a/aplcore/src/component/textmeasurement.cpp +++ b/aplcore/src/component/textmeasurement.cpp @@ -34,6 +34,10 @@ class DummyTextMeasurement : public TextMeasurement { } }; +/** + * By design, this is a global. We expect a single TextMeasurement to be shared + * across all root contexts. + */ static TextMeasurementPtr sTextMeasurement = std::make_shared(); void diff --git a/aplcore/src/component/touchablecomponent.cpp b/aplcore/src/component/touchablecomponent.cpp index c06e9cd..25e7e7b 100644 --- a/aplcore/src/component/touchablecomponent.cpp +++ b/aplcore/src/component/touchablecomponent.cpp @@ -13,8 +13,9 @@ * permissions and limitations under the License. */ -#include "apl/component/componentpropdef.h" #include "apl/component/touchablecomponent.h" + +#include "apl/component/componentpropdef.h" #include "apl/content/rootconfig.h" #include "apl/engine/keyboardmanager.h" #include "apl/engine/propdef.h" @@ -23,7 +24,7 @@ namespace apl { -static std::map sPropertyHandlers = { +static const std::map sPropertyHandlers = { {kPropertyOnCancel, "Cancel"}, {kPropertyOnDown, "Down"}, {kPropertyOnMove, "Move"}, @@ -31,7 +32,7 @@ static std::map sPropertyHandlers = { {kPropertyOnUp, "Up"} }; -static std::map sPropertyExecutesFast = { +static const std::map sPropertyExecutesFast = { {kPropertyOnCancel, true}, {kPropertyOnDown, true}, {kPropertyOnMove, true}, diff --git a/aplcore/src/component/touchwrappercomponent.cpp b/aplcore/src/component/touchwrappercomponent.cpp index db1df6e..c6f1e2f 100644 --- a/aplcore/src/component/touchwrappercomponent.cpp +++ b/aplcore/src/component/touchwrappercomponent.cpp @@ -13,8 +13,9 @@ * permissions and limitations under the License. */ -#include "apl/component/componentpropdef.h" #include "apl/component/touchwrappercomponent.h" + +#include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" #include "apl/primitives/keyboard.h" #include "apl/time/sequencer.h" diff --git a/aplcore/src/component/vectorgraphiccomponent.cpp b/aplcore/src/component/vectorgraphiccomponent.cpp index 5193c75..93152e5 100644 --- a/aplcore/src/component/vectorgraphiccomponent.cpp +++ b/aplcore/src/component/vectorgraphiccomponent.cpp @@ -13,18 +13,21 @@ * permissions and limitations under the License. */ -#include "apl/utils/url.h" -#include "apl/component/componentpropdef.h" #include "apl/component/vectorgraphiccomponent.h" + +#include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" #include "apl/graphic/graphic.h" #include "apl/media/mediamanager.h" #include "apl/media/mediaobject.h" +#include "apl/time/sequencer.h" +#include "apl/utils/url.h" #ifdef SCENEGRAPH +#include "apl/content/rootconfig.h" #include "apl/scenegraph/builder.h" +#include "apl/scenegraph/graphicfragment.h" #include "apl/scenegraph/scenegraph.h" #endif // SCENEGRAPH -#include "apl/time/sequencer.h" namespace apl { @@ -454,7 +457,7 @@ VectorGraphicComponent::postProcessLayoutChanges() { * * Layer (CoreComponent) * Layer (MediaLayer) [bounds match the vector graphic] - * Transform (within MediaLayer) + * Transform (within MediaLayer - scales the coordinate system from graphic to screen) * Graphic * * If the graphic is not valid, we still construct the mediaLayer and transform for later use. @@ -467,6 +470,7 @@ VectorGraphicComponent::constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGra // Build the basic data structures auto mediaLayer = sg::layer(mUniqueId + "-mediaLayer", Rect{0,0,1,1}, 1.0f, Transform2D()); + mediaLayer->setCharacteristic(sg::Layer::kCharacteristicRenderOnly); auto transform = sg::transform(); mediaLayer->setContent(transform); layer->appendChild(mediaLayer); @@ -492,18 +496,18 @@ VectorGraphicComponent::updateSceneGraphInternal(sg::SceneGraphUpdates& sceneGra auto validGraphic = fixMediaLayer(sceneGraph, mediaLayer); // If the media layer size changed, then we also need to redraw its content - if (mediaLayer->isFlagSet(sg::Layer::kFlagSizeChanged)) + if (mediaLayer->isFlagSet(sg::Layer::kFlagSizeChanged) && mediaLayer->content()) mediaLayer->setFlag(sg::Layer::kFlagRedrawContent); // If we changed the graphic, we definitely need to redraw the content if (mGraphicReplaced) { - mediaLayer->setFlag(sg::Layer::kFlagRedrawContent); + if (mediaLayer->content()) + mediaLayer->setFlag(sg::Layer::kFlagRedrawContent); mGraphicReplaced = false; } else if (graphicDirty && validGraphic) { // If it didn't change but was dirty, let the update routine decide auto graphic = getCalculated(kPropertyGraphic).get(); - if (graphic->updateSceneGraph(sceneGraph)) - mediaLayer->setFlag(sg::Layer::kFlagRedrawContent); + graphic->updateSceneGraph(sceneGraph); } // Copy over media layer flags, if set @@ -522,26 +526,68 @@ bool VectorGraphicComponent::fixMediaLayer(sg::SceneGraphUpdates& sceneGraph, const sg::LayerPtr& mediaLayer) { - auto* transform = sg::TransformNode::cast(mediaLayer->content()); - transform->removeAllChildren(); - auto object = getCalculated(kPropertyGraphic); - if (!object.is()) + if (!object.is()) { + mediaLayer->setContent(nullptr); + mediaLayer->removeAllChildren(); return false; + } auto graphic = object.get(); - if (!graphic || !graphic->isValid()) + if (!graphic || !graphic->isValid()) { + mediaLayer->setContent(nullptr); + mediaLayer->removeAllChildren(); return false; + } const auto& mediaBounds = getCalculated(kPropertyMediaBounds).get(); mediaLayer->setBounds(mediaBounds); - sg::TransformNode::cast(transform)->setTransform( - Transform2D::scale(mediaBounds.getWidth() / graphic->getViewportWidth(), - mediaBounds.getHeight() / graphic->getViewportHeight())); - - transform->setChild(graphic->getSceneGraph(sceneGraph)); - return true; + auto transform = Transform2D::scale(mediaBounds.getWidth() / graphic->getViewportWidth(), + mediaBounds.getHeight() / graphic->getViewportHeight()); + auto useLayers = getRootConfig().experimentalFeatureEnabled(RootConfig::kExperimentalFeatureGraphicLayers); + auto fragment = graphic->getSceneGraph(useLayers, sceneGraph, mediaLayer); + assert(fragment); + + if (fragment->isLayer()) { + // The graphic is a layer + auto layer = fragment->layer(); + + transform = Transform2D::translate(-layer->getContentOffset()) * transform * + Transform2D::translate(layer->getContentOffset()); + layer->setTransform(transform); + mediaLayer->setContent(nullptr); + + // Fix up the child layers. There should be only one. + if (mediaLayer->children().size() != 1 || mediaLayer->children().at(0) != layer) { + mediaLayer->removeAllChildren(); + mediaLayer->appendChild(layer); + } + return true; // We have a valid graphic + } + else if (fragment->isNode()) { + // Check to see if we already have a transform node in place + auto node = fragment->node(); // The child node we're expecting + + // If we already have content, then this should be a transform node + if (mediaLayer->content()) { + assert(sg::TransformNode::is_type(mediaLayer->content())); + auto t = sg::TransformNode::cast(mediaLayer->content()); + auto result = t->setTransform(transform); + result |= t->setChild(node); + if (result) + mediaLayer->setFlag(sg::Layer::kFlagRedrawContent); + } + else { + mediaLayer->setContent(sg::transform(transform, node)); + } + return true; + } + else { // Clear any existing content + mediaLayer->setContent(nullptr); + mediaLayer->removeAllChildren(); + return false; + } } #endif // SCENEGRAPH diff --git a/aplcore/src/component/videocomponent.cpp b/aplcore/src/component/videocomponent.cpp index 792f61e..6794bf3 100644 --- a/aplcore/src/component/videocomponent.cpp +++ b/aplcore/src/component/videocomponent.cpp @@ -13,15 +13,17 @@ * permissions and limitations under the License. */ -#include "apl/apl.h" -#include "apl/component/componentpropdef.h" #include "apl/component/videocomponent.h" + +#include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" #include "apl/media/mediautils.h" +#include "apl/primitives/mediasource.h" +#include "apl/time/sequencer.h" +#include "apl/utils/session.h" #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" #endif // SCENEGRAPH -#include "apl/time/sequencer.h" namespace apl { diff --git a/aplcore/src/component/yogaproperties.cpp b/aplcore/src/component/yogaproperties.cpp index a6e24b0..565b00e 100644 --- a/aplcore/src/component/yogaproperties.cpp +++ b/aplcore/src/component/yogaproperties.cpp @@ -14,6 +14,7 @@ */ #include "apl/component/yogaproperties.h" + #include "apl/component/corecomponent.h" #include "apl/primitives/dimension.h" #include "apl/primitives/object.h" diff --git a/aplcore/src/content/CMakeLists.txt b/aplcore/src/content/CMakeLists.txt index 964d5a9..9aa0879 100644 --- a/aplcore/src/content/CMakeLists.txt +++ b/aplcore/src/content/CMakeLists.txt @@ -17,6 +17,8 @@ target_sources_local(apl configurationchange.cpp content.cpp directive.cpp + extensioncommanddefinition.cpp + extensionfilterdefinition.cpp importrequest.cpp jsondata.cpp metrics.cpp diff --git a/aplcore/src/content/aplversion.cpp b/aplcore/src/content/aplversion.cpp index d34a9c9..4018841 100644 --- a/aplcore/src/content/aplversion.cpp +++ b/aplcore/src/content/aplversion.cpp @@ -20,7 +20,7 @@ namespace apl { -static Bimap sVersionMap = { +static const Bimap sVersionMap = { { APLVersion::kAPLVersion10, "1.0" }, { APLVersion::kAPLVersion11, "1.1" }, { APLVersion::kAPLVersion12, "1.2" }, @@ -33,6 +33,7 @@ static Bimap sVersionMap = { { APLVersion::kAPLVersion19, "1.9" }, { APLVersion::kAPLVersion20221, "2022.1" }, { APLVersion::kAPLVersion20222, "2022.2" }, + { APLVersion::kAPLVersion20231, "2023.1" }, }; bool diff --git a/aplcore/src/content/content.cpp b/aplcore/src/content/content.cpp index fe5af24..357751c 100644 --- a/aplcore/src/content/content.cpp +++ b/aplcore/src/content/content.cpp @@ -13,19 +13,21 @@ * permissions and limitations under the License. */ +#include "apl/content/content.h" + #include #include "apl/buildTimeConstants.h" -#include "apl/engine/arrayify.h" -#include "apl/engine/parameterarray.h" -#include "apl/content/package.h" -#include "apl/engine/propdef.h" -#include "apl/content/settings.h" +#include "apl/command/arraycommand.h" #include "apl/content/importrequest.h" -#include "apl/content/content.h" #include "apl/content/jsondata.h" #include "apl/content/metrics.h" -#include "apl/command/arraycommand.h" +#include "apl/content/package.h" +#include "apl/content/settings.h" +#include "apl/engine/arrayify.h" +#include "apl/engine/parameterarray.h" +#include "apl/engine/propdef.h" +#include "apl/utils/identifier.h" #include "apl/utils/log.h" #include "apl/utils/session.h" @@ -450,12 +452,17 @@ Content::getEnvironment(const RootConfig& config) const } } - // If the document has defined a "environment" section, we parse that + // If the document has defined an "environment" section, we parse that auto envIter = json.FindMember(DOCUMENT_ENVIRONMENT); if (envIter != json.MemberEnd() && envIter->value.IsObject()) { auto context = Context::createTypeEvaluationContext(config); - for (const auto& m : mEnvironmentParameters) - context->putUserWriteable(m, mParameterValues.at(m).get()); + for (const auto& m : mEnvironmentParameters) { + if (!isValidIdentifier(m)) + CONSOLE(context) << "Unable to add environment parameter '" << m + << "' to context. Invalid identifier."; + else + context->putUserWriteable(m, mParameterValues.at(m).get()); + } // Update the language, if it is defined language = propertyAsString(*context, envIter->value, DOCUMENT_LANGUAGE, language); diff --git a/aplcore/src/content/extensioncommanddefinition.cpp b/aplcore/src/content/extensioncommanddefinition.cpp new file mode 100644 index 0000000..b678db9 --- /dev/null +++ b/aplcore/src/content/extensioncommanddefinition.cpp @@ -0,0 +1,42 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "apl/content/extensioncommanddefinition.h" + +#include "apl/utils/log.h" + +namespace apl { + +ExtensionCommandDefinition& +ExtensionCommandDefinition::property(const std::string& name, ExtensionProperty&& prop) +{ + if (name == "when" || name == "type") + LOG(LogLevel::kWarn) << "Unable to register property '" << name << "' in custom command " << mName; + else + mPropertyMap.emplace(name, std::move(prop)); + return *this; +} + +ExtensionCommandDefinition& +ExtensionCommandDefinition::arrayProperty(std::string property, bool required) +{ + if (property == "when" || property == "type") + LOG(LogLevel::kWarn) << "Unable to register array-ified property '" << property << "' in custom command " << mName; + else + mPropertyMap.emplace(property, ExtensionProperty{kBindingTypeArray, Object::EMPTY_ARRAY(), required}); + return *this; +} + +} // namespace apl diff --git a/aplcore/src/content/extensionfilterdefinition.cpp b/aplcore/src/content/extensionfilterdefinition.cpp new file mode 100644 index 0000000..d911e0a --- /dev/null +++ b/aplcore/src/content/extensionfilterdefinition.cpp @@ -0,0 +1,32 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "apl/content/extensionfilterdefinition.h" + +#include "apl/utils/log.h" + +namespace apl { + +ExtensionFilterDefinition& +ExtensionFilterDefinition::property(const std::string& name, Property&& prop) +{ + if (name == "when" || name == "type" || name == "source" || name == "destination") + LOG(LogLevel::kWarn) << "Unable to register property '" << name << "' in custom filter extension " << mName; + else + mPropertyMap.emplace(name, std::move(prop)); + return *this; +} + +} // namespace apl diff --git a/aplcore/src/content/rootconfig.cpp b/aplcore/src/content/rootconfig.cpp index 65c579c..ab7d240 100644 --- a/aplcore/src/content/rootconfig.cpp +++ b/aplcore/src/content/rootconfig.cpp @@ -35,6 +35,11 @@ static float angleToSlope(float degrees) { return std::tan(degrees * M_PI / 180); } +inline Object asSlope(const Context& context, const Object& object) { + assert(object.isNumber()); + return angleToSlope(object.getDouble()); +} + static bool isAllowedEnvironmentName(const std::string &name) { static std::set sReserved; if (sReserved.empty()) { @@ -124,11 +129,6 @@ RootConfig::evaluationContext() return *mContext; } -inline Object asSlope(const Context& context, const Object& object) { - assert(object.isNumber()); - return angleToSlope(object.getDouble()); -} - const RootPropDefSet& RootConfig::propDefSet() const { @@ -275,7 +275,7 @@ RootConfig::setEnvironmentValue(const std::string& name, const Object& value) { } /** - * @return Animation easing for SwipeAway gesture. + * @return Animation easing for SwipeAway gesture. */ EasingPtr RootConfig::getSwipeAwayAnimationEasing() const { diff --git a/aplcore/src/content/viewport.cpp b/aplcore/src/content/viewport.cpp index fe1f93b..c203f25 100644 --- a/aplcore/src/content/viewport.cpp +++ b/aplcore/src/content/viewport.cpp @@ -14,8 +14,9 @@ */ #include "apl/content/viewport.h" -#include "apl/content/metrics.h" + #include "apl/content/content.h" +#include "apl/content/metrics.h" namespace apl { diff --git a/aplcore/src/datagrammar/bytecodeoptimizer.cpp b/aplcore/src/datagrammar/bytecodeoptimizer.cpp index 192311b..02cc397 100644 --- a/aplcore/src/datagrammar/bytecodeoptimizer.cpp +++ b/aplcore/src/datagrammar/bytecodeoptimizer.cpp @@ -102,7 +102,7 @@ ByteCodeOptimizer::simplifyOperands() using UnaryFunction = Object (*)(const Object&); using BinaryFunction = Object (*)(const Object&, const Object&); -static bool DEBUG_OPTIMIZER = false; +static const bool DEBUG_OPTIMIZER = false; /** * Peephole optimization diff --git a/aplcore/src/datagrammar/functions.cpp b/aplcore/src/datagrammar/functions.cpp index 95d5578..541c204 100644 --- a/aplcore/src/datagrammar/functions.cpp +++ b/aplcore/src/datagrammar/functions.cpp @@ -13,14 +13,14 @@ * permissions and limitations under the License. */ -#include +#include "apl/primitives/functions.h" + #include #include "apl/datagrammar/boundsymbol.h" #include "apl/engine/context.h" #include "apl/primitives/color.h" #include "apl/primitives/dimension.h" -#include "apl/primitives/functions.h" #include "apl/utils/log.h" #include "apl/utils/session.h" diff --git a/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp b/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp index 0381c24..85c8a01 100644 --- a/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp +++ b/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp @@ -14,7 +14,12 @@ */ #include "apl/datasource/dynamiclistdatasourceprovider.h" + +#include "rapidjson/error/en.h" + +#include "apl/content/rootconfig.h" #include "apl/datasource/datasource.h" +#include "apl/engine/event.h" #include "apl/time/timemanager.h" using namespace apl; diff --git a/aplcore/src/engine/CMakeLists.txt b/aplcore/src/engine/CMakeLists.txt index c1f847f..a5759e3 100644 --- a/aplcore/src/engine/CMakeLists.txt +++ b/aplcore/src/engine/CMakeLists.txt @@ -16,8 +16,8 @@ target_sources_local(apl arrayify.cpp binding.cpp builder.cpp - context.cpp componentdependant.cpp + context.cpp contextdependant.cpp contextobject.cpp contextwrapper.cpp @@ -35,9 +35,9 @@ target_sources_local(apl rootcontext.cpp rootcontextdata.cpp state.cpp - styleinstance.cpp styledefinition.cpp + styleinstance.cpp styles.cpp - uidobject.cpp uidmanager.cpp + uidobject.cpp ) diff --git a/aplcore/src/engine/binding.cpp b/aplcore/src/engine/binding.cpp index 552dc76..b3ad1cc 100644 --- a/aplcore/src/engine/binding.cpp +++ b/aplcore/src/engine/binding.cpp @@ -14,9 +14,10 @@ */ #include "apl/engine/binding.h" + +#include "apl/primitives/color.h" #include "apl/primitives/dimension.h" #include "apl/primitives/object.h" -#include "apl/primitives/color.h" namespace apl { diff --git a/aplcore/src/engine/builder.cpp b/aplcore/src/engine/builder.cpp index 4fbcfdb..58cc2c1 100644 --- a/aplcore/src/engine/builder.cpp +++ b/aplcore/src/engine/builder.cpp @@ -15,8 +15,7 @@ * Construct a virtual DOM hierarchy */ - -#include +#include "apl/engine/builder.h" #include "apl/component/containercomponent.h" #include "apl/component/edittextcomponent.h" @@ -33,18 +32,18 @@ #include "apl/content/rootconfig.h" #include "apl/engine/arrayify.h" #include "apl/engine/binding.h" -#include "apl/engine/builder.h" #include "apl/engine/context.h" #include "apl/engine/contextdependant.h" #include "apl/engine/evaluate.h" #include "apl/engine/parameterarray.h" -#include "apl/livedata/livearray.h" -#include "apl/livedata/livearrayobject.h" #include "apl/engine/properties.h" #include "apl/extension/extensioncomponent.h" #include "apl/extension/extensionmanager.h" #include "apl/livedata/layoutrebuilder.h" +#include "apl/livedata/livearray.h" +#include "apl/livedata/livearrayobject.h" #include "apl/primitives/object.h" +#include "apl/utils/identifier.h" #include "apl/utils/log.h" #include "apl/utils/path.h" #include "apl/utils/session.h" @@ -54,7 +53,7 @@ namespace apl { const bool DEBUG_BUILDER = false; -static std::map sComponentMap = { +static const std::map sComponentMap = { {"Container", ContainerComponent::create}, {"Text", TextComponent::create}, {"Image", ImageComponent::create}, @@ -153,6 +152,7 @@ Builder::populateLayoutComponent(const ContextPtr& context, for (int i = 0; i < length; i++) { const auto& element = items.at(i); auto childContext = Context::createFromParent(context); + childContext->putConstant("__source", "index"); childContext->putConstant("index", index); childContext->putConstant("length", length); if (numbered) @@ -250,6 +250,8 @@ Builder::expandSingleComponent(const ContextPtr& context, // Create a new context and fill out the binding ContextPtr expanded = Context::createFromParent(context); + expanded->putConstant("__source", "component"); + expanded->putConstant("__name", type); attachBindings(expanded, item); // Construct the component @@ -291,7 +293,7 @@ Builder::expandSingleComponent(const ContextPtr& context, auto resource = context->getLayout(type); if (!resource.empty()) { properties.emplace(item); - return expandLayout(context, properties, resource.json(), parent, resource.path(), fullBuild, useDirtyFlag); + return expandLayout(type, context, properties, resource.json(), parent, resource.path(), fullBuild, useDirtyFlag); } CONSOLE(context) << "Unable to find layout or component '" << type << "'"; @@ -310,8 +312,15 @@ Builder::attachBindings(const apl::ContextPtr& context, const apl::Object& item) auto bindings = arrayifyProperty(*context, item, "bind"); for (const auto& binding : bindings) { auto name = propertyAsString(*context, binding, "name"); - if (name.empty() || !binding.has("value")) + if (!isValidIdentifier(name)) { + CONSOLE(context) << "Invalid binding name '" << name << "'"; continue; + } + + if (!binding.has("value")) { + CONSOLE(context) << "Binding '" << name << "' did not specify a value"; + continue; + } if (context->hasLocal(name)) { CONSOLE(context) << "Attempted to bind to pre-existing property '" << name << "'"; @@ -371,6 +380,7 @@ Builder::expandSingleComponentFromArray(const ContextPtr& context, * Expand a layout defined either as the "mainTemplate" or a named layout * from the "layouts" section of a package. * + * @param name The name of the layout * @param context Current data-binding context. * @param properties The user-specified properties for this layout. * @param layout The JSON definition of the layout object. @@ -379,7 +389,8 @@ Builder::expandSingleComponentFromArray(const ContextPtr& context, * @param useDirtyFlag true to notify runtime about changes with dirty properties */ CoreComponentPtr -Builder::expandLayout(const ContextPtr& context, +Builder::expandLayout(const std::string& name, + const ContextPtr& context, Properties& properties, const rapidjson::Value& layout, const CoreComponentPtr& parent, @@ -397,6 +408,8 @@ Builder::expandLayout(const ContextPtr& context, // Build a new context for this layout. ContextPtr cptr = Context::createFromParent(context); + cptr->putConstant("__source", "layout"); + cptr->putConstant("__name", name); // Add each parameter to the context. It's either going to come from // a property or its default value. This will remove the matching property from @@ -494,7 +507,7 @@ Builder::inflate(const ContextPtr& context, const rapidjson::Value& mainDocument) { APL_TRACE_BLOCK("Builder:inflate"); - return expandLayout(context, mainProperties, mainDocument, nullptr, + return expandLayout("mainTemplate", context, mainProperties, mainDocument, nullptr, Path(context->getRootConfig().getTrackProvenance() ? std::string(Path::MAIN) + "/mainTemplate" : ""), true, false); } diff --git a/aplcore/src/engine/context.cpp b/aplcore/src/engine/context.cpp index 2aead31..d5d4495 100644 --- a/aplcore/src/engine/context.cpp +++ b/aplcore/src/engine/context.cpp @@ -15,13 +15,14 @@ #include +#include "apl/engine/context.h" + #include "apl/buildTimeConstants.h" #include "apl/component/textmeasurement.h" #include "apl/content/metrics.h" #include "apl/content/rootconfig.h" #include "apl/content/viewport.h" #include "apl/engine/builder.h" -#include "apl/engine/context.h" #include "apl/engine/event.h" #include "apl/engine/resources.h" #include "apl/engine/rootcontextdata.h" @@ -294,10 +295,18 @@ Context::findComponentById(const std::string& id) const return top ? top->findComponentById(id) : nullptr; } +ComponentPtr +Context::topComponent() const +{ + assert(mCore); + return mCore->top(); +} + void -Context::pushEvent(Event&& event) { +Context::pushEvent(Event&& event) +{ assert(mCore); - mCore->events.push(event); + mCore->events->push(event); } #ifdef ALEXAEXTENSIONS @@ -310,7 +319,8 @@ Context::pushExtensionEvent(Event&& event) #endif void -Context::setDirty(const ComponentPtr& ptr) { +Context::setDirty(const ComponentPtr& ptr) +{ assert(mCore); mCore->dirty.emplace(ptr); } @@ -323,19 +333,22 @@ Context::clearDirty(const ComponentPtr& ptr) } void -Context::setDirtyVisualContext(const ComponentPtr& ptr) { +Context::setDirtyVisualContext(const ComponentPtr& ptr) +{ assert(mCore); mCore->dirtyVisualContext.emplace(ptr); } bool -Context::isVisualContextDirty(const ComponentPtr& ptr) { +Context::isVisualContextDirty(const ComponentPtr& ptr) +{ auto found = mCore->dirtyVisualContext.find(ptr); return found != mCore->dirtyVisualContext.end(); } void -Context::setDirtyDataSourceContext(const DataSourceConnectionPtr& ptr) { +Context::setDirtyDataSourceContext(const DataSourceConnectionPtr& ptr) +{ assert(mCore); mCore->dirtyDatasourceContext.emplace(ptr); } @@ -353,47 +366,56 @@ Context::focusManager() const } HoverManager& -Context::hoverManager() const { +Context::hoverManager() const +{ return mCore->hoverManager(); } KeyboardManager & -Context::keyboardManager() const { +Context::keyboardManager() const +{ return mCore->keyboardManager(); } LiveDataManager& -Context::dataManager() const { +Context::dataManager() const +{ return mCore->dataManager(); } ExtensionManager& -Context::extensionManager() const { +Context::extensionManager() const +{ return mCore->extensionManager(); } LayoutManager& -Context::layoutManager() const { +Context::layoutManager() const +{ return mCore->layoutManager(); } MediaManager& -Context::mediaManager() const { +Context::mediaManager() const +{ return mCore->mediaManager(); } MediaPlayerFactory& -Context::mediaPlayerFactory() const { +Context::mediaPlayerFactory() const +{ return mCore->mediaPlayerFactory(); } UIDManager& -Context::uniqueIdManager() const { +Context::uniqueIdManager() const +{ return mCore->uniqueIdManager(); } const SessionPtr& -Context::session() const { +Context::session() const +{ return mCore->session(); } @@ -455,7 +477,8 @@ Context::inflate(const rapidjson::Value& component) } rapidjson::Value -Context::serialize(rapidjson::Document::AllocatorType& allocator) { +Context::serialize(rapidjson::Document::AllocatorType& allocator) +{ rapidjson::Value out(rapidjson::kArrayType); for (const auto& m : mMap) { @@ -491,7 +514,8 @@ operator<<(streamer& os, const Context& context) } -bool Context::userUpdateAndRecalculate(const std::string& key, const Object& value, bool useDirtyFlag) { +bool Context::userUpdateAndRecalculate(const std::string& key, const Object& value, bool useDirtyFlag) +{ auto it = mMap.find(key); if (it != mMap.end()) { if (it->second.isUserWriteable()) { @@ -511,7 +535,8 @@ bool Context::userUpdateAndRecalculate(const std::string& key, const Object& val return false; } -bool Context::systemUpdateAndRecalculate(const std::string& key, const Object& value, bool useDirtyFlag) { +bool Context::systemUpdateAndRecalculate(const std::string& key, const Object& value, bool useDirtyFlag) +{ auto it = mMap.find(key); if (it == mMap.end()) return false; diff --git a/aplcore/src/engine/evaluate.cpp b/aplcore/src/engine/evaluate.cpp index f4bdd86..9af7824 100644 --- a/aplcore/src/engine/evaluate.cpp +++ b/aplcore/src/engine/evaluate.cpp @@ -18,7 +18,6 @@ #include "apl/engine/evaluate.h" #include "apl/datagrammar/bytecodeassembler.h" #include "apl/engine/context.h" -#include "apl/primitives/dimension.h" #include "apl/utils/log.h" #include "apl/utils/session.h" diff --git a/aplcore/src/engine/event.cpp b/aplcore/src/engine/event.cpp index e57a316..5a0f412 100644 --- a/aplcore/src/engine/event.cpp +++ b/aplcore/src/engine/event.cpp @@ -13,9 +13,8 @@ * permissions and limitations under the License. */ -#include - #include "apl/engine/event.h" + #include "apl/command/command.h" #include "apl/command/corecommand.h" diff --git a/aplcore/src/engine/info.cpp b/aplcore/src/engine/info.cpp index dae43fa..731bdf5 100644 --- a/aplcore/src/engine/info.cpp +++ b/aplcore/src/engine/info.cpp @@ -13,8 +13,9 @@ * permissions and limitations under the License. */ -#include "apl/engine/context.h" #include "apl/engine/info.h" + +#include "apl/engine/context.h" #include "apl/engine/rootcontextdata.h" #include "apl/engine/styles.h" diff --git a/aplcore/src/engine/keyboardmanager.cpp b/aplcore/src/engine/keyboardmanager.cpp index edf5fda..f866bf7 100644 --- a/aplcore/src/engine/keyboardmanager.cpp +++ b/aplcore/src/engine/keyboardmanager.cpp @@ -31,12 +31,12 @@ namespace apl { static const bool DEBUG_KEYBOARD_MANAGER = false; -static std::map sHandlerIds = { // NOLINT(cert-err58-cpp) +static const std::map sHandlerIds = { // NOLINT(cert-err58-cpp) {kKeyUp, "KeyUp"}, {kKeyDown, "KeyDown"} }; -static std::map sHandlerProperty = { // NOLINT(cert-err58-cpp) +static const std::map sHandlerProperty = { // NOLINT(cert-err58-cpp) {kKeyUp, kPropertyHandleKeyUp}, {kKeyDown, kPropertyHandleKeyDown} }; diff --git a/aplcore/src/engine/layoutmanager.cpp b/aplcore/src/engine/layoutmanager.cpp index 5c1fe69..a97ee98 100644 --- a/aplcore/src/engine/layoutmanager.cpp +++ b/aplcore/src/engine/layoutmanager.cpp @@ -14,6 +14,7 @@ */ #include "apl/engine/layoutmanager.h" + #include "apl/component/corecomponent.h" #include "apl/content/configurationchange.h" #include "apl/engine/rootcontextdata.h" diff --git a/aplcore/src/engine/parameterarray.cpp b/aplcore/src/engine/parameterarray.cpp index 29cd6ef..6af7a7b 100644 --- a/aplcore/src/engine/parameterarray.cpp +++ b/aplcore/src/engine/parameterarray.cpp @@ -14,7 +14,6 @@ */ #include "apl/engine/parameterarray.h" -#include "apl/primitives/dimension.h" #include "apl/engine/arrayify.h" namespace apl { diff --git a/aplcore/src/engine/propdef.cpp b/aplcore/src/engine/propdef.cpp index b7cb61a..c2c69fd 100644 --- a/aplcore/src/engine/propdef.cpp +++ b/aplcore/src/engine/propdef.cpp @@ -14,12 +14,23 @@ */ #include "apl/engine/propdef.h" -#include "apl/content/rootconfig.h" #include "rapidjson/document.h" #include "rapidjson/stringbuffer.h" #include "rapidjson/writer.h" +#include "apl/animation/easing.h" +#include "apl/content/rootconfig.h" +#include "apl/graphic/graphicfilter.h" +#include "apl/graphic/graphicpattern.h" +#include "apl/primitives/filter.h" +#include "apl/primitives/gradient.h" +#include "apl/primitives/mediasource.h" +#include "apl/primitives/object.h" +#include "apl/primitives/styledtext.h" +#include "apl/primitives/transform.h" +#include "apl/primitives/urlrequest.h" + namespace apl { Object asOldArray(const Context &context, const Object &object) { @@ -45,7 +56,7 @@ Object asOldArray(const Context &context, const Object &object) { } } - return Object(std::move(array)); + return std::move(array); }; Object asOldBoolean(const Context& context, const Object& object) @@ -56,6 +67,116 @@ Object asOldBoolean(const Context& context, const Object& object) return object.asBoolean(); } +Object asStyledText(const Context& context, const Object& object) +{ + return StyledText::create(context, object); +} + +Object asColor(const Context& context, const Object& object) +{ + return object.asColor(context); +} + +Object asFilterArray(const Context& context, const Object& object) +{ + std::vector data; + for (auto& m : arrayify(context, object)) { + auto f = Filter::create(context, m); + if (f.is()) + data.push_back(std::move(f)); + } + return std::move(data); +} + +Object asGraphicFilterArray(const Context& context, const Object& object) +{ + std::vector data; + for (auto& m : arrayify(context, object)) { + auto f = GraphicFilter::create(context, m); + if (f.is()) + data.push_back(std::move(f)); + } + return std::move(data); +} + +Object asGradient(const Context& context, const Object& object) +{ + return Gradient::create(context, object); +} + +Object asFill(const Context& context, const Object& object) +{ + auto gradient = asGradient(context, object); + return gradient.is() ? gradient : asColor(context, object); +} + +Object asVectorGraphicSource(const Context& context, const Object& object) +{ + return object.isString() ? object : URLRequest::create(context, object); +} + +Object asImageSourceArray(const Context& context, const Object& object) +{ + std::vector data; + for (auto& m : arrayify(context, object)) { + auto request = m.isString() ? m : URLRequest::create(context, m); + if (!request.isNull()) + data.emplace_back(std::move(request)); + } + + if (data.empty()) + return ""; + + // In case of URLs the spec enforces an array of elements. + // We don't want to break runtimes that are not using urls and depend + // on the old logic that evaluates the strings via arrayify + if (data.size() == 1 && data.front().isString()) + return data.front(); + + return std::move(data); +} + +Object asMediaSourceArray(const Context& context, const Object& object) { + std::vector data; + for (auto& m : arrayify(context, object)) { + auto ms = MediaSource::create(context, m); + if (ms.is()) + data.push_back(std::move(ms)); + } + return std::move(data); +} + +Object asFilteredText(const Context& context, const Object& object) +{ + return StyledText::create(context, object).getText(); +} + +Object asTransformOrArray(const Context& context, const Object& object) +{ + if (object.is()) + return object; + + return evaluateRecursive(context, arrayify(context, object)); +} + +Object asEasing(const Context& context, const Object& object) +{ + if (object.is()) + return object; + + return Easing::parse(context.session(), object.asString()); +} + +Object asGraphicPattern(const Context& context, const Object& object) +{ + return GraphicPattern::create(context, object); +} + +Object asAvgGradient(const Context& context, const Object& object) +{ + return Gradient::createAVG(context, object); +} + Object asPaddingArray(const Context& context, const Object& object) { std::vector data; @@ -79,7 +200,7 @@ Object asPaddingArray(const Context& context, const Object& object) data[3] = data[1]; // [X,Y,Z] -> [X,Y,Z,Y] } - return Object(std::move(data)); + return {std::move(data)}; } } // namespace apl diff --git a/aplcore/src/engine/properties.cpp b/aplcore/src/engine/properties.cpp index 13f52b1..929796a 100644 --- a/aplcore/src/engine/properties.cpp +++ b/aplcore/src/engine/properties.cpp @@ -13,16 +13,18 @@ * permissions and limitations under the License. */ -#include +#include "apl/engine/properties.h" + #include #include "apl/content/rootconfig.h" +#include "apl/datasource/datasource.h" #include "apl/engine/context.h" #include "apl/engine/contextdependant.h" -#include "apl/engine/properties.h" #include "apl/engine/evaluate.h" #include "apl/primitives/dimension.h" -#include "apl/datasource/datasource.h" +#include "apl/utils/identifier.h" +#include "apl/utils/session.h" #include "apl/utils/stringfunctions.h" namespace apl { @@ -103,7 +105,6 @@ Properties::emplace(const Object& item) void Properties::addToContext(const ContextPtr &context, const Parameter ¶meter, bool userWriteable) { - bool validNamedProperty = false; Object tmp; Object result; auto bindingFunc = sBindingFunctions.at(parameter.type); @@ -111,30 +112,33 @@ Properties::addToContext(const ContextPtr &context, const Parameter ¶meter, auto it = mProperties.find(parameter.name); if (it != mProperties.end()) { tmp = it->second; - mProperties.erase(it); // Remove the property from the list - - // Extract as an optional node tree for dependant - tmp = tmp.isString() ? parseDataBinding(*context, tmp.getString()) : tmp; - - auto value = evaluate(*context, tmp); - if (!value.isNull()) { - result = bindingFunc(*context, value); - - // If type explicitly specified we may want to "enrich" the object. - if (value.isMap() && value.has("type")) { - std::string type = value.get("type").getString(); - if (sBindingMap.has(type)) { - bindingFunc = sBindingFunctions.at(sBindingMap.at(type)); - result = bindingFunc(*context, value); - } else if (context->getRootConfig().isDataSource(type)) { - result = DataSource::create(context, value, parameter.name); - } - } - validNamedProperty = true; - } + mProperties.erase(it); // Remove the property from the list } - if (!validNamedProperty) { + // Parameter names are only added to the data-binding context if they are valid identifiers. + if (!isValidIdentifier(parameter.name)) { + CONSOLE(context) << "Unable to add parameter '" << parameter.name + << "' to context. Invalid identifier."; + return; + } + + // Extract as an optional node tree for dependant + tmp = tmp.isString() ? parseDataBinding(*context, tmp.getString()) : tmp; + auto value = evaluate(*context, tmp); + if (!value.isNull()) { + result = bindingFunc(*context, value); + + // If type explicitly specified we may want to "enrich" the object. + if (value.isMap() && value.has("type")) { + std::string type = value.get("type").getString(); + if (sBindingMap.has(type)) { + bindingFunc = sBindingFunctions.at(sBindingMap.at(type)); + result = bindingFunc(*context, value); + } else if (context->getRootConfig().isDataSource(type)) { + result = DataSource::create(context, value, parameter.name); + } + } + } else { result = bindingFunc(*context, evaluate(*context, parameter.defvalue)); } diff --git a/aplcore/src/engine/resources.cpp b/aplcore/src/engine/resources.cpp index 337c0af..034d30d 100644 --- a/aplcore/src/engine/resources.cpp +++ b/aplcore/src/engine/resources.cpp @@ -13,13 +13,14 @@ * permissions and limitations under the License. */ +#include "apl/engine/resources.h" + #include "rapidjson/document.h" #include "apl/engine/context.h" #include "apl/engine/evaluate.h" -#include "apl/engine/resources.h" -#include "apl/utils/log.h" #include "apl/engine/propdef.h" +#include "apl/utils/log.h" namespace apl { diff --git a/aplcore/src/engine/rootcontext.cpp b/aplcore/src/engine/rootcontext.cpp index 758e6f4..6a327ea 100644 --- a/aplcore/src/engine/rootcontext.cpp +++ b/aplcore/src/engine/rootcontext.cpp @@ -13,41 +13,42 @@ * permissions and limitations under the License. */ +#include "apl/engine/rootcontext.h" + #include #include "rapidjson/stringbuffer.h" -#include "apl/engine/evaluate.h" -#include "apl/engine/styles.h" -#include "apl/engine/propdef.h" #include "apl/action/scrolltoaction.h" #include "apl/command/arraycommand.h" #include "apl/command/configchangecommand.h" #include "apl/command/displaystatechangecommand.h" #include "apl/command/documentcommand.h" #include "apl/component/yogaproperties.h" +#include "apl/content/configurationchange.h" #include "apl/content/content.h" #include "apl/content/metrics.h" #include "apl/content/rootconfig.h" -#include "apl/content/configurationchange.h" #include "apl/datasource/datasource.h" #include "apl/datasource/datasourceprovider.h" #include "apl/engine/builder.h" -#include "apl/extension/extensionmanager.h" -#include "apl/engine/rootcontext.h" +#include "apl/engine/queueeventmanager.h" #include "apl/engine/resources.h" #include "apl/engine/rootcontextdata.h" +#include "apl/engine/styles.h" #include "apl/engine/uidmanager.h" +#include "apl/extension/extensionmanager.h" #include "apl/graphic/graphic.h" #include "apl/livedata/livedataobject.h" #include "apl/livedata/livedataobjectwatcher.h" +#include "apl/time/timemanager.h" +#include "apl/utils/log.h" +#include "apl/utils/tracing.h" #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" #include "apl/scenegraph/scenegraph.h" #endif // SCENEGRAPH -#include "apl/time/timemanager.h" -#include "apl/utils/log.h" -#include "apl/utils/tracing.h" + namespace apl { @@ -75,13 +76,21 @@ RootContext::create(const Metrics& metrics, const ContentPtr& content, const Roo RootContextPtr RootContext::create(const Metrics& metrics, const ContentPtr& content, const RootConfig& config, std::function callback) +{ + return create(metrics, content, config, callback, std::make_shared()); +} + +RootContextPtr +RootContext::create(const Metrics& metrics, const ContentPtr& content, + const RootConfig& config, std::function callback, + const EventManagerPtr& eventManager) { if (!content->isReady()) { LOG(LogLevel::kError).session(content->getSession()) << "Attempting to create root context with illegal content"; return nullptr; } - auto root = std::make_shared(metrics, content, config); + auto root = std::make_shared(metrics, content, config, eventManager); if (callback) callback(root); if (!root->setup(nullptr)) @@ -90,12 +99,21 @@ RootContext::create(const Metrics& metrics, const ContentPtr& content, return root; } -RootContext::RootContext(const Metrics& metrics, const ContentPtr& content, const RootConfig& config) +RootContext::RootContext(const Metrics& metrics, + const ContentPtr& content, + const RootConfig& config) + : RootContext(metrics, content, config, std::make_shared()) +{} + +RootContext::RootContext(const Metrics& metrics, + const ContentPtr& content, + const RootConfig& config, + const EventManagerPtr& eventManager) : mContent(content), mTimeManager(config.getTimeManager()), mDisplayState(static_cast(config.getProperty(RootProperty::kInitialDisplayState).getInteger())) { - init(metrics, config, false); + init(metrics, config, false, eventManager); } RootContext::~RootContext() { @@ -187,7 +205,7 @@ RootContext::reinflate() assert(mTimeManager->size() == 0); // The initialization routine replaces mCore with a new core - init(metrics, config, true); + init(metrics, config, true, std::make_shared()); setup(oldTop); // Pass the old top component // If there was a previous top component, release it and its children to free memory @@ -215,7 +233,10 @@ RootContext::resize() void -RootContext::init(const Metrics& metrics, const RootConfig& config, bool reinflation) +RootContext::init(const Metrics& metrics, + const RootConfig& config, + bool reinflation, + const EventManagerPtr& eventManager) { APL_TRACE_BLOCK("RootContext:init"); std::string theme = metrics.getTheme(); @@ -235,7 +256,8 @@ RootContext::init(const Metrics& metrics, const RootConfig& config, bool reinfla reinflation), mContent->getDocumentSettings(), session, - mContent->mExtensionRequests); + mContent->mExtensionRequests, + eventManager); auto env = mContent->getEnvironment(config); mCore->lang(env.language) @@ -331,7 +353,7 @@ RootContext::hasEvent() const { assert(mCore); clearPending(); - return !mCore->events.empty(); + return !mCore->events->empty(); } Event @@ -340,9 +362,9 @@ RootContext::popEvent() assert(mCore); clearPending(); - if (!mCore->events.empty()) { - Event event = mCore->events.front(); - mCore->events.pop(); + if (!mCore->events->empty()) { + Event event = mCore->events->front(); + mCore->events->pop(); return event; } @@ -1070,6 +1092,7 @@ RootContext::getSceneGraph() mSceneGraph->setLayer(top->getSceneGraph(mSceneGraph->updates())); } + mSceneGraph->updates().fixCreatedFlags(); clearDirty(); return mSceneGraph; } diff --git a/aplcore/src/engine/rootcontextdata.cpp b/aplcore/src/engine/rootcontextdata.cpp index 72c6ec9..5f71eb8 100644 --- a/aplcore/src/engine/rootcontextdata.cpp +++ b/aplcore/src/engine/rootcontextdata.cpp @@ -13,21 +13,23 @@ * permissions and limitations under the License. */ +#include "apl/engine/rootcontextdata.h" + #include "apl/component/corecomponent.h" #include "apl/component/textmeasurement.h" #include "apl/content/metrics.h" +#include "apl/engine/queueeventmanager.h" #include "apl/content/rootconfig.h" -#include "apl/engine/rootcontextdata.h" #include "apl/engine/keyboardmanager.h" #include "apl/engine/styles.h" #include "apl/engine/uidmanager.h" #include "apl/focus/focusmanager.h" #include "apl/livedata/livedatamanager.h" +#include "apl/time/timemanager.h" + #ifdef SCENEGRAPH #include "apl/scenegraph/textpropertiescache.h" #endif // SCENEGRAPH -#include "apl/time/timemanager.h" -#include "apl/utils/log.h" namespace apl { @@ -79,8 +81,10 @@ RootContextData::RootContextData(const Metrics& metrics, RuntimeState runtimeState, const SettingsPtr& settings, const SessionPtr& session, - const std::vector>& extensions) - : mRuntimeState(std::move(runtimeState)), + const std::vector>& extensions, + const EventManagerPtr& eventManager) + : events(eventManager), + mRuntimeState(std::move(runtimeState)), mMetrics(metrics), mStyles(new Styles()), mSequencer(new Sequencer(config.getTimeManager(), mRuntimeState.getRequestedAPLVersion())), @@ -135,7 +139,7 @@ RootContextData::halt() } // Clear any pending events and dirty components - events = std::queue(); + events->clear(); dirty.clear(); dirtyVisualContext.clear(); diff --git a/aplcore/src/extension/extensionclient.cpp b/aplcore/src/extension/extensionclient.cpp index aa62773..7ab4d61 100644 --- a/aplcore/src/extension/extensionclient.cpp +++ b/aplcore/src/extension/extensionclient.cpp @@ -13,18 +13,19 @@ * permissions and limitations under the License. */ +#include "apl/extension/extensionclient.h" + #include -#include "apl/content/rootconfig.h" #include "apl/component/componentpropdef.h" #include "apl/component/componentproperties.h" #include "apl/content/content.h" -#include "apl/extension/extensionclient.h" -#include "apl/extension/extensionmanager.h" -#include "apl/engine/evaluate.h" +#include "apl/content/rootconfig.h" #include "apl/engine/arrayify.h" #include "apl/engine/binding.h" +#include "apl/engine/evaluate.h" #include "apl/engine/rootcontext.h" +#include "apl/extension/extensionmanager.h" #include "apl/livedata/livearray.h" #include "apl/livedata/livearrayobject.h" #include "apl/livedata/livemap.h" diff --git a/aplcore/src/extension/extensioncomponent.cpp b/aplcore/src/extension/extensioncomponent.cpp index 9bdf066..0dac523 100644 --- a/aplcore/src/extension/extensioncomponent.cpp +++ b/aplcore/src/extension/extensioncomponent.cpp @@ -14,6 +14,7 @@ */ #include "apl/extension/extensioncomponent.h" + #include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" #include "apl/extension/extensionmanager.h" @@ -22,7 +23,7 @@ namespace apl { -static std::map sPropertyHandlers = { +static const std::map sPropertyHandlers = { {kPropertyResourceOnFatalError, "FatalError"} }; diff --git a/aplcore/src/extension/extensionmanager.cpp b/aplcore/src/extension/extensionmanager.cpp index 8dff2e9..ac943e8 100644 --- a/aplcore/src/extension/extensionmanager.cpp +++ b/aplcore/src/extension/extensionmanager.cpp @@ -13,8 +13,9 @@ * permissions and limitations under the License. */ -#include "apl/content/rootconfig.h" #include "apl/extension/extensionmanager.h" + +#include "apl/content/rootconfig.h" #include "apl/extension/extensionmediator.h" namespace apl { diff --git a/aplcore/src/extension/extensionmediator.cpp b/aplcore/src/extension/extensionmediator.cpp index 6bd0b20..b5ff4e9 100644 --- a/aplcore/src/extension/extensionmediator.cpp +++ b/aplcore/src/extension/extensionmediator.cpp @@ -15,15 +15,16 @@ #ifdef ALEXAEXTENSIONS -#include +#include "apl/extension/extensionmediator.h" + #include #include #include #include +#include #include -#include #include "apl/extension/extensioncomponent.h" #include "apl/extension/extensionmediator.h" #include "apl/primitives/objectdata.h" diff --git a/aplcore/src/graphic/graphic.cpp b/aplcore/src/graphic/graphic.cpp index dfa2b4c..cd0c89c 100644 --- a/aplcore/src/graphic/graphic.cpp +++ b/aplcore/src/graphic/graphic.cpp @@ -13,25 +13,28 @@ * permissions and limitations under the License. */ +#include "apl/graphic/graphic.h" + +#include "apl/component/vectorgraphiccomponent.h" +#include "apl/engine/arrayify.h" #include "apl/engine/contextdependant.h" +#include "apl/engine/propdef.h" #include "apl/engine/resources.h" -#include "apl/engine/arrayify.h" -#include "apl/graphic/graphic.h" #include "apl/graphic/graphicbuilder.h" +#include "apl/utils/identifier.h" #include "apl/utils/log.h" #include "apl/utils/session.h" -#include "apl/engine/propdef.h" -#include "apl/component/vectorgraphiccomponent.h" #ifdef SCENEGRAPH -#include "apl/scenegraph/scenegraphupdates.h" +#include "apl/scenegraph/graphicfragment.h" #include "apl/scenegraph/node.h" +#include "apl/scenegraph/scenegraphupdates.h" #endif // SCENEGRAPH namespace apl { const bool DEBUG_GRAPHIC = false; -static ResourceOperators sGraphicResourceOperators = { +static const ResourceOperators sGraphicResourceOperators = { {"number", asNumber}, {"numbers", asNumber}, {"string", asString}, @@ -110,6 +113,12 @@ void Graphic::release() { clearDirty(); + +#ifdef SCENEGRAPH + mSceneGraphFragment = {}; + mHasSceneGraph = false; +#endif + if (mRootElement) mRootElement->release(); if (mContext) @@ -172,6 +181,12 @@ Graphic::initialize(const ContextPtr& sourceContext, // Populate the data-binding context with parameters for (const auto& param : mParameterArray) { LOG_IF(DEBUG_GRAPHIC).session(sourceContext) << "Parse parameter: " << param.name; + if (!isValidIdentifier(param.name)) { + CONSOLE(mContext) << "Unable to add graphic parameter '" << param.name + << "' to context. Invalid identifier"; + continue; + } + const auto& conversionFunc = sBindingFunctions.at(param.type); auto value = conversionFunc(*sourceContext, evaluate(*mContext, param.defvalue)); Object parsed; @@ -207,6 +222,9 @@ Graphic::initialize(const ContextPtr& sourceContext, auto self = std::static_pointer_cast(shared_from_this()); mRootElement = GraphicBuilder::build(self, json); +#ifdef SCENEGRAPH + mHasSceneGraph = false; +#endif } bool @@ -388,32 +406,40 @@ Graphic::serialize(rapidjson::Document::AllocatorType& allocator) const { } #ifdef SCENEGRAPH -sg::NodePtr -Graphic::getSceneGraph(sg::SceneGraphUpdates& sceneGraph) + +sg::GraphicFragmentPtr +Graphic::getSceneGraph(bool allowLayers, + sg::SceneGraphUpdates& sceneGraph, + const sg::LayerPtr& containingLayer) { if (!mRootElement) - return nullptr; + return {}; + + if (!mHasSceneGraph) { + mSceneGraphFragment = mRootElement->buildSceneGraph(allowLayers, sceneGraph); + if (mSceneGraphFragment && mSceneGraphFragment->isNode()) + mSceneGraphFragment->assignToLayer(containingLayer); + mSceneGraphFragment->clearElements(); // These are no longer needed + mHasSceneGraph = true; + } - return mRootElement->getSceneGraph(sceneGraph); + return mSceneGraphFragment; } -bool +void Graphic::updateSceneGraph(sg::SceneGraphUpdates& sceneGraph) { - if (!mRootElement) - return false; + assert(mHasSceneGraph); + if (!mSceneGraphFragment) + return; - sg::ModifiedNodeList list; for (const auto& element : mDirty) - element->updateSceneGraph(list); + element->updateSceneGraph(sceneGraph); - clearDirty(); + if (mSceneGraphFragment->isLayer()) + sceneGraph.processResize(); - for (auto node = mRootElement->getSceneGraphNode() ; node ; node = node->next()) - if (node->needsRedraw()) - return true; - - return false; + clearDirty(); } #endif // SCENEGRAPH diff --git a/aplcore/src/graphic/graphicbuilder.cpp b/aplcore/src/graphic/graphicbuilder.cpp index 18ecdd8..55e0e45 100644 --- a/aplcore/src/graphic/graphicbuilder.cpp +++ b/aplcore/src/graphic/graphicbuilder.cpp @@ -24,6 +24,7 @@ #include "apl/graphic/graphicelementgroup.h" #include "apl/graphic/graphicelementtext.h" +#include "apl/utils/identifier.h" #include "apl/utils/session.h" namespace apl { @@ -150,8 +151,15 @@ GraphicBuilder::createChild(const ContextPtr& context, const Object& json) auto bindings = arrayifyProperty(*context, json, "bind"); for (const auto& binding : bindings) { auto name = propertyAsString(*expanded, binding, "name"); - if (name.empty() || !binding.has("value")) + if (!isValidIdentifier(name)) { + CONSOLE(context) << "Invalid binding name '" << name << "'"; continue; + } + + if (!binding.has("value")) { + CONSOLE(context) << "Binding '" << name << "' did not specify a value"; + continue; + } // Extract the binding as an optional node tree. auto tmp = propertyAsNode(*expanded, binding, "value"); diff --git a/aplcore/src/graphic/graphicelement.cpp b/aplcore/src/graphic/graphicelement.cpp index fc8b447..df9fc83 100644 --- a/aplcore/src/graphic/graphicelement.cpp +++ b/aplcore/src/graphic/graphicelement.cpp @@ -13,18 +13,25 @@ * permissions and limitations under the License. */ +#include "apl/graphic/graphicelement.h" #include "apl/component/corecomponent.h" #include "apl/engine/propdef.h" #include "apl/graphic/graphic.h" #include "apl/graphic/graphicdependant.h" -#include "apl/graphic/graphicelement.h" +#include "apl/graphic/graphicpattern.h" #include "apl/graphic/graphicpropdef.h" +#include "apl/primitives/color.h" +#include "apl/primitives/gradient.h" +#include "apl/primitives/transform2d.h" +#include "apl/utils/session.h" + #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" +#include "apl/scenegraph/graphicfragment.h" #include "apl/scenegraph/node.h" +#include "apl/scenegraph/scenegraphupdates.h" #endif // SCENEGRAPH -#include "apl/utils/session.h" namespace apl { @@ -186,6 +193,9 @@ GraphicElement::serialize(rapidjson::Document::AllocatorType& allocator) const children.PushBack(child->serialize(allocator), allocator); } v.AddMember("children", children.Move(), allocator); +#ifdef SCENEGRAPH + v.AddMember("layer", rapidjson::Value(mContainingLayer ? mContainingLayer->getName().c_str() : "", allocator), allocator); +#endif return v; } @@ -214,9 +224,9 @@ GraphicElement::getStyle(const GraphicPtr& graphic) const } void -GraphicElement::updateStyleInternal( - const StyleInstancePtr& stylePtr, - const GraphicPropDefSet& gds) { +GraphicElement::updateStyleInternal(const StyleInstancePtr& stylePtr, + const GraphicPropDefSet& gds) +{ for (const auto& it : gds) { const GraphicPropDef& pd = it.second; @@ -235,7 +245,8 @@ GraphicElement::updateStyleInternal( } void -GraphicElement::updateStyle(const GraphicPtr& graphic) { +GraphicElement::updateStyle(const GraphicPtr& graphic) +{ auto stylePtr = getStyle(graphic); if (stylePtr) { updateStyleInternal(stylePtr, propDefSet()); @@ -287,51 +298,108 @@ GraphicElement::fixStrokeTransform(GraphicElement& element) { void GraphicElement::release() { RecalculateTarget::removeUpstreamDependencies(); + +#ifdef SCENEGRAPH + mContainingLayer = nullptr; +#endif + for (auto& child : mChildren) { child->release(); } } #ifdef SCENEGRAPH -sg::NodePtr -GraphicElement::getSceneGraph(sg::SceneGraphUpdates& sceneGraph) + +/** + * Construct a GraphicFragment for this element that contains all of the GraphicFragments + * from child elements. + * + * Two child fragments may be merged if they match "sufficiently well". Refer to the + * GraphicFragment::mergeWith() method for details. + */ +sg::GraphicFragmentPtr +GraphicElement::ensureSceneGraphChildren(bool allowLayers, sg::SceneGraphUpdates& sceneGraph) { - if (!mSceneGraphNode) { - mInnerSceneGraphNode = buildSceneGraph(sceneGraph); - mSceneGraphNode = mInnerSceneGraphNode; - - const auto& filters = mValues.get(kGraphicPropertyFilters); - if (!filters.empty()) { - // Build up the filter list in reverse order - for (int i = 0; i < filters.size(); i++) { - const auto& filter = filters.at(i).get(); - switch (filter.getType()) { - case kGraphicFilterTypeDropShadow: - mSceneGraphNode = sg::shadowNode( - sg::shadow( - filter.getValue(kGraphicPropertyFilterColor).getColor(), - Point{filter.getValue(kGraphicPropertyFilterHorizontalOffset) - .asFloat(), - filter.getValue(kGraphicPropertyFilterVerticalOffset) - .asFloat()}, - filter.getValue(kGraphicPropertyFilterRadius).asFloat()), - mSceneGraphNode); - break; - } - } + auto result = sg::GraphicFragment::create(shared_from_this()); + sg::GraphicFragmentPtr accumulator; + + for (auto& childElement : mChildren) { + auto fragment = childElement->buildSceneGraph(allowLayers, sceneGraph); + if (!accumulator) + accumulator = fragment; + else if (!accumulator->mergeWith(fragment)) { + // Failed to merge + result->ensureLayer(sceneGraph); + result->addChild(accumulator, sceneGraph); + accumulator = fragment; } } - return mSceneGraphNode; + result->addChild(accumulator, sceneGraph); + return result; +} + +void +GraphicElement::assignSceneGraphLayer(const sg::LayerPtr& containingLayer) +{ + mContainingLayer = containingLayer; +} + +void +GraphicElement::requestRedraw(sg::SceneGraphUpdates& sceneGraph) +{ + if (mContainingLayer) { + mContainingLayer->setFlag(sg::Layer::kFlagRedrawContent); + sceneGraph.changed(mContainingLayer); + } } void -GraphicElement::updateSceneGraph(sg::ModifiedNodeList& modList) +GraphicElement::requestSizeCheck(sg::SceneGraphUpdates& sceneGraph) +{ + if (mContainingLayer) + sceneGraph.resize(mContainingLayer); +} + +/** + * Decide if a certain graphic property should be included in the scene graph content node. For + * example, a path with a stroke operation should be included in the scene graph iff (a) the + * stroke color/gradient/pattern is not transparent, (b) the stroke width is > 0, and (c) the + * stroke opacity > 0; if any of these are false, there's no point in adding the stroke to the + * scene graph because nothing will be drawn. + * + * Note that having an upstream driver for one of these properties means that it _could_ be + * valid in the future, and so this method will return true without checking the current value. + * + * This method is not useful for arbitrary keys; it is meaningless for properties like + * kGraphicPropertyScaleX or kGraphicPropertyFontStyle. + * + * @param key The graphic key to examine for this element + * @return True if the key doesn't block drawing + */ +bool +GraphicElement::includeInSceneGraph(GraphicPropertyKey key) { - if (!mSceneGraphNode) - return; + if (hasUpstream(key)) + return true; + + auto value = getValue(key); + + // Numbers are used for opacity and stroke width + if (value.isNumber()) + return value.asNumber() > 0; + + // Fills and strokes are either colors, gradients, or patterns + if (value.is()) + return Color(value.getColor()).alpha() != 0; + + if (value.is()) { + auto colors = value.get().getColorRange(); + return std::any_of(colors.begin(), colors.end(), [](Color c){ return c.alpha() != 0; }); + } - updateSceneGraphInternal(modList, mInnerSceneGraphNode); + // Assume patterns render and everything else doesn't + return value.is(); } #endif // SCENEGRAPH diff --git a/aplcore/src/graphic/graphicelementcontainer.cpp b/aplcore/src/graphic/graphicelementcontainer.cpp index f8a8612..cc4f055 100644 --- a/aplcore/src/graphic/graphicelementcontainer.cpp +++ b/aplcore/src/graphic/graphicelementcontainer.cpp @@ -14,10 +14,15 @@ */ #include "apl/graphic/graphicelementcontainer.h" + #include "apl/graphic/graphicpropdef.h" +#include "apl/primitives/dimension.h" + #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" #include "apl/scenegraph/node.h" +#include "apl/scenegraph/graphicfragment.h" +#include "apl/scenegraph/scenegraphupdates.h" #endif // SCENEGRAPH #include "apl/utils/session.h" @@ -109,24 +114,26 @@ GraphicElementContainer::initialize(const GraphicPtr& graphic, const Object& jso } #ifdef SCENEGRAPH -sg::NodePtr -GraphicElementContainer::buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) -{ - sg::NodePtr node = nullptr; - for (auto it = mChildren.rbegin() ; it != mChildren.rend() ; it++) { - auto child = (*it)->getSceneGraph(sceneGraph); - if (child) - node = child->setNext(node); +sg::GraphicFragmentPtr +GraphicElementContainer::buildSceneGraph(bool allowLayers, + sg::SceneGraphUpdates& sceneGraph) { + auto result = ensureSceneGraphChildren(allowLayers, sceneGraph); + + // Enforce a layer if possible. The transform on this layer will be set + // by the VectorGraphic component to position the container. Note: If the + // ensureSceneGraphChildren method returned a layer, the transform on that layer + // will be used for positioning + if (allowLayers && !result->empty()) + result->ensureLayer(sceneGraph); + + if (result->isLayer()) { + mContainingLayer = result->layer(); + result->fixBoundingBox(); } - return node; + return result; } -void -GraphicElementContainer::updateSceneGraphInternal(sg::ModifiedNodeList& modList, const sg::NodePtr& node) -{ - // Graphic property nodes are fixed -} #endif // SCENEGRAPH } // namespace apl diff --git a/aplcore/src/graphic/graphicelementgroup.cpp b/aplcore/src/graphic/graphicelementgroup.cpp index b6a4f90..89ffd0b 100644 --- a/aplcore/src/graphic/graphicelementgroup.cpp +++ b/aplcore/src/graphic/graphicelementgroup.cpp @@ -14,10 +14,15 @@ */ #include "apl/graphic/graphicelementgroup.h" + #include "apl/graphic/graphicpropdef.h" +#include "apl/primitives/transform2d.h" + #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" +#include "apl/scenegraph/graphicfragment.h" #include "apl/scenegraph/scenegraph.h" +#include "apl/scenegraph/scenegraphupdates.h" #endif // SCENEGRAPH namespace apl { @@ -115,29 +120,99 @@ GraphicElementGroup::updateTransform(const Context& context, bool useDirtyFlag) } } + +void +GraphicElementGroup::release() +{ + GraphicElement::release(); +#ifdef SCENEGRAPH + mSceneGraphLayer = nullptr; + mSceneGraphNode = nullptr; +#endif +} + + #ifdef SCENEGRAPH -sg::NodePtr -GraphicElementGroup::buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) +sg::GraphicFragmentPtr +GraphicElementGroup::buildSceneGraph(bool allowLayers, sg::SceneGraphUpdates& sceneGraph) { - sg::NodePtr node = nullptr; - for (auto it = mChildren.rbegin() ; it != mChildren.rend() ; it++) { - auto child = (*it)->getSceneGraph(sceneGraph); - if (child) - node = child->setNext(node); + // Clear cached items + mSceneGraphLayer = nullptr; + mSceneGraphNode = nullptr; + + const auto hasStyle = !mStyle.empty(); + + // A group with zero opacity will never be rendered, so we can ignore it - unless it has + // an upstream driver or a style assigned, in which case it may be toggled to be visible + // in the future. + auto opacity = static_cast(getValue(kGraphicPropertyOpacity).asNumber()); + if (opacity == 0 && !hasUpstream(kGraphicPropertyOpacity) && !hasStyle) + return {}; + + // No children? Ignore the group + auto result = ensureSceneGraphChildren(allowLayers, sceneGraph); + if (result->empty()) + return {}; + + auto clipPath = sg::path(getValue(kGraphicPropertyClipPath).asString()); + auto transform = getValue(kGraphicPropertyTransform).get(); + + // Force a layer if this group can be modified + const auto groupMutable = hasStyle || hasUpstream(); + if (allowLayers && groupMutable) + result->ensureLayer(sceneGraph); + + if (result->isLayer()) { + auto layer = result->layer(); + mContainingLayer = layer; + + result->fixBoundingBox(); + auto offset = layer->getContentOffset(); + if (!offset.empty()) + transform = Transform2D::translate(-offset) * transform * Transform2D::translate(offset); + + layer->setOutline(clipPath); + layer->setOpacity(opacity); + layer->setTransform(transform); + + if (groupMutable) { + result->setType(sg::GraphicFragment::kLayerMutable); + mSceneGraphLayer = layer; + } } + else { + // Not a layer; configure as a node + auto node = result->node(); + + if (!clipPath->empty() || hasUpstream(kGraphicPropertyClipPath)) + node = sg::clip(clipPath, node); + if (!transform.empty() || hasUpstream(kGraphicPropertyTransformAssigned) || + hasUpstream(kGraphicPropertyRotation) || hasUpstream(kGraphicPropertyPivotX) || + hasUpstream(kGraphicPropertyPivotY) || hasUpstream(kGraphicPropertyScaleX) || + hasUpstream(kGraphicPropertyScaleY) || hasUpstream(kGraphicPropertyTranslateX) || + hasUpstream(kGraphicPropertyTranslateY)) + node = sg::transform(transform, node); + if (opacity < 1.0f || hasUpstream(kGraphicPropertyOpacity)) + node = sg::opacity(opacity, node); + + result->setNode(node); - auto clip = sg::clip(sg::path(getValue(kGraphicPropertyClipPath).asString()), node); + if (groupMutable) { + result->setType(sg::GraphicFragment::kNodeContentMutable); + mSceneGraphNode = node; + } + } - return sg::opacity( - getValue(kGraphicPropertyOpacity), - sg::transform( - getValue(kGraphicPropertyTransform), - clip)); + result->applyFilters(mValues.get(kGraphicPropertyFilters)); + return result; } void -GraphicElementGroup::updateSceneGraphInternal(sg::ModifiedNodeList& modList, const sg::NodePtr& node) +GraphicElementGroup::updateSceneGraph(sg::SceneGraphUpdates& sceneGraph) { + if (!mSceneGraphLayer && !mSceneGraphNode) + return; + const auto clipChanged = isDirty(kGraphicPropertyClipPath); const auto opacityChanged = isDirty(kGraphicPropertyOpacity); const auto transformChanged = isDirty(kGraphicPropertyTransform); @@ -145,18 +220,55 @@ GraphicElementGroup::updateSceneGraphInternal(sg::ModifiedNodeList& modList, con if (!clipChanged && !opacityChanged && !transformChanged) return; - auto* opacity = sg::OpacityNode::cast(node); - if (opacityChanged && opacity->setOpacity(getValue(kGraphicPropertyOpacity).asFloat())) - modList.contentChanged(opacity); + if (mSceneGraphLayer) { + auto layer = mSceneGraphLayer; + + if (opacityChanged && layer->setOpacity(getValue(kGraphicPropertyOpacity).asFloat())) + sceneGraph.changed(layer); + + if (transformChanged) { + auto offset = mSceneGraphLayer->getContentOffset(); + auto transform = getValue(kGraphicPropertyTransform).get(); + transform = Transform2D::translate(-offset) * transform * Transform2D::translate(offset); + if (layer->setTransform(transform)) + sceneGraph.changed(layer); + } - auto* transform = sg::TransformNode::cast(opacity->child()); - if (transformChanged && - transform->setTransform(getValue(kGraphicPropertyTransform).get())) - modList.contentChanged(transform); + if (clipChanged && + layer->setOutline(sg::path(getValue(kGraphicPropertyClipPath).asString()))) { + sceneGraph.changed(layer); + requestSizeCheck(sceneGraph); + } + } + else { + auto node = mSceneGraphNode; + + if (sg::OpacityNode::is_type(node)) { + auto* opacity = sg::OpacityNode::cast(node); + if (opacityChanged && opacity->setOpacity(getValue(kGraphicPropertyOpacity).asFloat())) + requestRedraw(sceneGraph); + node = node->child(); + } - auto* clip = sg::ClipNode::cast(transform->child()); - if (clipChanged && clip->setPath(sg::path(getValue(kGraphicPropertyClipPath).asString()))) - modList.contentChanged(clip); + if (sg::TransformNode::is_type(node)) { + auto* transform = sg::TransformNode::cast(node); + if (transformChanged && + transform->setTransform(getValue(kGraphicPropertyTransform).get())) { + requestRedraw(sceneGraph); + requestSizeCheck(sceneGraph); + } + node = node->child(); + } + + if (sg::ClipNode::is_type(node)) { + auto* clip = sg::ClipNode::cast(node); + if (clipChanged && + clip->setPath(sg::path(getValue(kGraphicPropertyClipPath).asString()))) { + requestRedraw(sceneGraph); + requestSizeCheck(sceneGraph); + } + } + } } #endif // SCENEGRAPH } // namespace apl diff --git a/aplcore/src/graphic/graphicelementpath.cpp b/aplcore/src/graphic/graphicelementpath.cpp index f26563e..861c0e6 100644 --- a/aplcore/src/graphic/graphicelementpath.cpp +++ b/aplcore/src/graphic/graphicelementpath.cpp @@ -14,10 +14,16 @@ */ #include "apl/graphic/graphicelementpath.h" + #include "apl/graphic/graphicpropdef.h" +#include "apl/primitives/color.h" +#include "apl/primitives/transform2d.h" + #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" +#include "apl/scenegraph/graphicfragment.h" #include "apl/scenegraph/scenegraph.h" +#include "apl/scenegraph/scenegraphupdates.h" #endif // SCENEGRAPH namespace apl { @@ -78,11 +84,32 @@ GraphicElementPath::initialize(const GraphicPtr& graphic, const Object& json) return true; } +void +GraphicElementPath::release() +{ +#ifdef SCENEGRAPH + mSceneGraphNode = nullptr; +#endif + + GraphicElement::release(); +} + #ifdef SCENEGRAPH -sg::NodePtr -GraphicElementPath::buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) +sg::GraphicFragmentPtr +GraphicElementPath::buildSceneGraph(bool allowLayers, sg::SceneGraphUpdates& sceneGraph) { + // Clear cached items + mSceneGraphNode = nullptr; + auto path = sg::path(getValue(kGraphicPropertyPathData).asString()); + + // Return nothing if there is no path and the path never mutates + const auto pathMutates = hasUpstream(kGraphicPropertyPathData); + const auto hasStyle = !mStyle.empty(); + if (path->empty() && !pathMutates && !hasStyle) + return {}; + + // Calculate the current fill and stroke auto fill = sg::fill(sg::paint(getValue(kGraphicPropertyFill), getValue(kGraphicPropertyFillOpacity).asFloat(), getValue(kGraphicPropertyFillTransform).get())); @@ -100,21 +127,16 @@ GraphicElementPath::buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) .dashes(getValue(kGraphicPropertyStrokeDashArray)) .get(); - // Return nothing if there is no path and the path never mutates - const auto pathMutates = hasUpstream(kGraphicPropertyPathData); - if (path->empty() && !pathMutates) - return nullptr; - // Include the fill operation if it is visible or if it can change to be visible - const auto includeFill = fill->visible() || - hasUpstream(kGraphicPropertyFill) || - hasUpstream(kGraphicPropertyFillOpacity); + const auto includeFill = hasStyle || + (includeInSceneGraph(kGraphicPropertyFill) && + includeInSceneGraph(kGraphicPropertyFillOpacity)); // Include the stroke operation if it is visible or if it can change to be visible - const auto includeStroke = stroke->visible() || - hasUpstream(kGraphicPropertyStroke) || - hasUpstream(kGraphicPropertyStrokeOpacity) || - hasUpstream(kGraphicPropertyStrokeWidth); + const auto includeStroke = hasStyle || + (includeInSceneGraph(kGraphicPropertyStroke) && + includeInSceneGraph(kGraphicPropertyStrokeOpacity) && + includeInSceneGraph(kGraphicPropertyStrokeWidth)); // Assemble the operations list in reverse order so that fill occurs before stroke. auto op = includeStroke ? stroke : nullptr; @@ -123,32 +145,71 @@ GraphicElementPath::buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) op = fill; } - // If there are no drawing operations, return a null node. - return op ? sg::draw(path, op) : nullptr; + // If there are no drawing operations, leave this node empty + if (!op) + return {}; + + auto node = sg::draw(path, op); + + // The content is mutable if there is an upstream dependency OR a style that can change the + // properties. + // TODO: Check what properties the style can change for a more fine-grained test. + const auto pathMutable = hasStyle || hasUpstream(); + if (pathMutable) + mSceneGraphNode = node; + + // If mutable and layers are allowed, set up a layer + sg::GraphicFragmentPtr result; + if (allowLayers && pathMutable) { + auto layer = sg::layer(getUniqueId() + "_path", Rect(), 1.0f, Transform2D()); + layer->setCharacteristic(sg::Layer::kCharacteristicRenderOnly | + sg::Layer::kCharacteristicDoNotClipChildren); + sceneGraph.created(layer); + layer->setContent(node); + mContainingLayer = layer; + + auto bounds = node->boundingBox(Transform2D()); + layer->setContentOffset(bounds.getTopLeft()); + layer->setBounds(bounds); + result = sg::GraphicFragment::create(shared_from_this(), layer, + sg::GraphicFragment::kLayerFixedContentMutable); + } + else { + result = + sg::GraphicFragment::create(shared_from_this(), node, + pathMutable ? sg::GraphicFragment::kNodeContentMutable + : sg::GraphicFragment::kNodeContentFixed); + } + + result->applyFilters(mValues.get(kGraphicPropertyFilters)); + return result; } void -GraphicElementPath::updateSceneGraphInternal(sg::ModifiedNodeList& modList, const sg::NodePtr& node) +GraphicElementPath::updateSceneGraph(sg::SceneGraphUpdates& sceneGraph) { + if (!mSceneGraphNode) + return; + const auto pathChanged = isDirty(kGraphicPropertyPathData); - const auto fillChanged = isDirty(kGraphicPropertyFill) || - isDirty(kGraphicPropertyFillOpacity) || - isDirty(kGraphicPropertyFillTransform); - const auto strokePaintChanged = isDirty(kGraphicPropertyStroke) || - isDirty(kGraphicPropertyStrokeOpacity) || - isDirty(kGraphicPropertyStrokeTransform); - - const auto strokeChanged = - isDirty(kGraphicPropertyStrokeWidth) || isDirty(kGraphicPropertyStrokeMiterLimit) || - isDirty(kGraphicPropertyPathLength) || isDirty(kGraphicPropertyStrokeDashOffset) || - isDirty(kGraphicPropertyStrokeLineCap) || isDirty(kGraphicPropertyStrokeLineJoin) || - isDirty(kGraphicPropertyStrokeDashArray); + const auto fillChanged = + isDirty({kGraphicPropertyFill, kGraphicPropertyFillOpacity, kGraphicPropertyFillTransform}); + const auto strokePaintChanged = isDirty( + {kGraphicPropertyStroke, kGraphicPropertyStrokeOpacity, kGraphicPropertyStrokeTransform}); + const auto strokeChanged = isDirty( + {kGraphicPropertyStrokeWidth, kGraphicPropertyStrokeMiterLimit, kGraphicPropertyPathLength, + kGraphicPropertyStrokeDashOffset, kGraphicPropertyStrokeLineCap, + kGraphicPropertyStrokeLineJoin, kGraphicPropertyStrokeDashArray}); + + // Fix up the drawing content if (pathChanged || fillChanged || strokePaintChanged || strokeChanged) { - auto* draw = sg::DrawNode::cast(node); + auto* draw = sg::DrawNode::cast(mSceneGraphNode); - if (pathChanged && draw->setPath(sg::path(getValue(kGraphicPropertyPathData).asString()))) - modList.contentChanged(draw); + if (pathChanged && draw->setPath(sg::path(getValue(kGraphicPropertyPathData).asString()))) { + requestRedraw(sceneGraph); + requestSizeCheck(sceneGraph); + } auto op = draw->getOp(); if (sg::FillPathOp::is_type(op) && fillChanged) { @@ -165,7 +226,7 @@ GraphicElementPath::updateSceneGraphInternal(sg::ModifiedNodeList& modList, cons } // TODO: Do a fine-grained check to see if the fill actually changed - modList.contentChanged(draw); + requestRedraw(sceneGraph); } // Advance to the stroke operation if the fill was defined. @@ -187,7 +248,7 @@ GraphicElementPath::updateSceneGraphInternal(sg::ModifiedNodeList& modList, cons op->paint->setTransform( getValue(kGraphicPropertyStrokeTransform).get()); } - modList.contentChanged(draw); + requestRedraw(sceneGraph); } if (strokeChanged) { @@ -201,10 +262,12 @@ GraphicElementPath::updateSceneGraphInternal(sg::ModifiedNodeList& modList, cons .lineJoin(static_cast( getValue(kGraphicPropertyStrokeLineJoin).asInt())) .dashes(getValue(kGraphicPropertyStrokeDashArray)); - modList.contentChanged(draw); + requestRedraw(sceneGraph); + requestSizeCheck(sceneGraph); } } } } + #endif // SCENEGRAPH } // namespace apl diff --git a/aplcore/src/graphic/graphicelementtext.cpp b/aplcore/src/graphic/graphicelementtext.cpp index 8a7131e..1e6b787 100644 --- a/aplcore/src/graphic/graphicelementtext.cpp +++ b/aplcore/src/graphic/graphicelementtext.cpp @@ -14,14 +14,18 @@ */ #include "apl/graphic/graphicelementtext.h" -#include "apl/graphic/graphicpropdef.h" #include "apl/component/componentproperties.h" #include "apl/content/rootconfig.h" +#include "apl/graphic/graphicpropdef.h" +#include "apl/primitives/color.h" +#include "apl/primitives/transform2d.h" #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" +#include "apl/scenegraph/graphicfragment.h" #include "apl/scenegraph/scenegraph.h" +#include "apl/scenegraph/scenegraphupdates.h" #include "apl/scenegraph/textchunk.h" #include "apl/scenegraph/textlayout.h" #include "apl/scenegraph/textmeasurement.h" @@ -110,12 +114,33 @@ GraphicElementText::initialize(const GraphicPtr& graphic, const Object& json) return true; } +void +GraphicElementText::release() +{ #ifdef SCENEGRAPH -sg::NodePtr -GraphicElementText::buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) + mTextChunk = nullptr; + mTextProperties = nullptr; + mLayout = nullptr; + mSceneGraphNode = nullptr; +#endif + GraphicElement::release(); +} + +#ifdef SCENEGRAPH +sg::GraphicFragmentPtr +GraphicElementText::buildSceneGraph(bool allowLayers, sg::SceneGraphUpdates& sceneGraph) { + // Clear cached items + mSceneGraphNode = nullptr; + ensureSGTextLayout(); + // Return nothing if there is no text and the text never mutates + const auto hasStyle = !mStyle.empty(); + if (mTextChunk->styledText().empty() && !hasStyle && !hasUpstream(kGraphicPropertyText)) + return {}; + + // Calculate the current fill and stroke auto fill = sg::fill(sg::paint(getValue(kGraphicPropertyFill), getValue(kGraphicPropertyFillOpacity).asFloat(), getValue(kGraphicPropertyFillTransform).get())); @@ -126,17 +151,15 @@ GraphicElementText::buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) .strokeWidth(getValue(kGraphicPropertyStrokeWidth).asFloat()) .get(); - // Include the fill operation if it is visible or if it can change to be visible - const auto includeFill = fill->visible() || - hasUpstream(kGraphicPropertyFill) || - hasUpstream(kGraphicPropertyFillOpacity); + const auto includeFill = + hasStyle || (includeInSceneGraph(kGraphicPropertyFill) && + includeInSceneGraph(kGraphicPropertyFillOpacity)); // Include the stroke operation if it is visible or if it can change to be visible - const auto includeStroke = stroke->visible() || - hasUpstream(kGraphicPropertyStroke) || - hasUpstream(kGraphicPropertyStrokeOpacity) || - hasUpstream(kGraphicPropertyStrokeWidth); + const auto includeStroke = hasStyle || (includeInSceneGraph(kGraphicPropertyStroke) && + includeInSceneGraph(kGraphicPropertyStrokeOpacity) && + includeInSceneGraph(kGraphicPropertyStrokeWidth)); // Assemble the operations list in reverse order so that fill occurs before stroke. auto op = includeStroke ? stroke : nullptr; @@ -145,10 +168,45 @@ GraphicElementText::buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) op = fill; } - // If there are no drawing operations, return a null node - return op ? sg::transform(getPosition(), sg::text(mLayout, op)) : nullptr; + // If there are no drawing operations, leave this node empty + if (!op) + return {}; + + auto node = sg::transform(getPosition(), sg::text(mLayout, op)); + const auto textMutable = hasStyle || hasUpstream(); + if (textMutable) + mSceneGraphNode = node; + + // If this element is mutable create a layer to cache the drawn text. + sg::GraphicFragmentPtr result; + if (allowLayers && textMutable) { + auto layer = sg::layer(getUniqueId() + "_text", Rect(), 1.0f, Transform2D()); + layer->setCharacteristic(sg::Layer::kCharacteristicRenderOnly | + sg::Layer::kCharacteristicDoNotClipChildren); + sceneGraph.created(layer); + layer->setContent(node); + mContainingLayer = layer; + + if (mLayout) { + auto bb = node->boundingBox(Transform2D()); + layer->setBounds(bb); + layer->setContentOffset(bb.getTopLeft()); + } + + result = sg::GraphicFragment::create(shared_from_this(), layer, + sg::GraphicFragment::kLayerFixedContentMutable); + } + else { + result = sg::GraphicFragment::create(shared_from_this(), node, + textMutable ? sg::GraphicFragment::kNodeContentMutable + : sg::GraphicFragment::kNodeContentFixed); + } + + result->applyFilters(mValues.get(kGraphicPropertyFilters)); + return result; } + /* * This method is called if the GraphicElementText has a pre-existing scene graph. * Nominally "isDirty(PROPERTY)" tells us if a component property has changed and hence @@ -169,7 +227,6 @@ GraphicElementText::buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) * kGraphicPropertyFill * kGraphicPropertyFillOpacity * kGraphicPropertyFillTransform - * kGraphicPropertyFilters * kGraphicPropertyStroke * kGraphicPropertyStrokeOpacity * kGraphicPropertyStrokeTransform @@ -181,33 +238,42 @@ GraphicElementText::buildSceneGraph(sg::SceneGraphUpdates& sceneGraph) * kGraphicPropertyFilters is not dynamic, so it doesn't change */ void -GraphicElementText::updateSceneGraphInternal(sg::ModifiedNodeList& modList, const sg::NodePtr& node) +GraphicElementText::updateSceneGraph(sg::SceneGraphUpdates& sceneGraph) { + if (!mSceneGraphNode) + return; + const auto textChanged = !mLayout; - const auto fillChanged = isDirty(kGraphicPropertyFill) || - isDirty(kGraphicPropertyFillOpacity) || - isDirty(kGraphicPropertyFillTransform); - const auto strokePaintChanged = isDirty(kGraphicPropertyStroke) || - isDirty(kGraphicPropertyStrokeOpacity) || - isDirty(kGraphicPropertyStrokeTransform); + const auto fillChanged = + isDirty({kGraphicPropertyFill, kGraphicPropertyFillOpacity, kGraphicPropertyFillTransform}); + const auto strokePaintChanged = isDirty( + {kGraphicPropertyStroke, kGraphicPropertyStrokeOpacity, kGraphicPropertyStrokeTransform}); const auto strokeWidthChanged = isDirty(kGraphicPropertyStrokeWidth); - const auto transformChanged = isDirty(kGraphicPropertyCoordinateX) || - isDirty(kGraphicPropertyCoordinateY) || - isDirty(kGraphicPropertyTextAnchor); + const auto transformChanged = isDirty( + {kGraphicPropertyCoordinateX, kGraphicPropertyCoordinateY, kGraphicPropertyTextAnchor}); ensureSGTextLayout(); - auto* transform = sg::TransformNode::cast(node); - if ((textChanged || transformChanged) && - transform->setTransform(Transform2D::translate(getPosition()))) { - modList.contentChanged(transform); + // Update the transform. + auto* transform = sg::TransformNode::cast(mSceneGraphNode); + if (textChanged || transformChanged) { + if (transform->setTransform(Transform2D::translate(getPosition()))) { + // TODO: As a future optimization, if the only change was the X/Y coordinates AND we + // are in a layer, then we won't need to redraw. But if the text anchor or + // text content change, we must redraw. + requestRedraw(sceneGraph); + requestSizeCheck(sceneGraph); + } } + // Fix up the drawing content. if (textChanged || fillChanged || strokePaintChanged || strokeWidthChanged) { auto* text = sg::TextNode::cast(transform->child()); - if (textChanged) + if (textChanged) { text->setTextLayout(mLayout); + requestSizeCheck(sceneGraph); + } auto op = text->getOp(); if (sg::FillPathOp::is_type(op) && fillChanged) { @@ -245,12 +311,14 @@ GraphicElementText::updateSceneGraphInternal(sg::ModifiedNodeList& modList, cons } } - if (strokeWidthChanged) + if (strokeWidthChanged) { sg::stroke(sg::StrokePathOp::castptr(op)) .strokeWidth(getValue(kGraphicPropertyStrokeWidth).asFloat()); + requestSizeCheck(sceneGraph); + } } - modList.contentChanged(text); + requestRedraw(sceneGraph); } } diff --git a/aplcore/src/graphic/graphicfilter.cpp b/aplcore/src/graphic/graphicfilter.cpp index a903f9e..3916a4c 100644 --- a/aplcore/src/graphic/graphicfilter.cpp +++ b/aplcore/src/graphic/graphicfilter.cpp @@ -14,8 +14,10 @@ */ #include "apl/graphic/graphicfilter.h" + #include "apl/engine/evaluate.h" #include "apl/engine/propdef.h" +#include "apl/primitives/color.h" #include "apl/utils/session.h" namespace apl { diff --git a/aplcore/src/livedata/layoutrebuilder.cpp b/aplcore/src/livedata/layoutrebuilder.cpp index de10595..60263ea 100644 --- a/aplcore/src/livedata/layoutrebuilder.cpp +++ b/aplcore/src/livedata/layoutrebuilder.cpp @@ -13,11 +13,12 @@ * permissions and limitations under the License. */ -#include "apl/livedata/livearraychange.h" #include "apl/livedata/layoutrebuilder.h" + #include "apl/component/corecomponent.h" -#include "apl/livedata/livearrayobject.h" #include "apl/engine/builder.h" +#include "apl/livedata/livearraychange.h" +#include "apl/livedata/livearrayobject.h" namespace apl { diff --git a/aplcore/src/media/CMakeLists.txt b/aplcore/src/media/CMakeLists.txt index d719d35..6e956e9 100644 --- a/aplcore/src/media/CMakeLists.txt +++ b/aplcore/src/media/CMakeLists.txt @@ -16,4 +16,5 @@ target_sources_local(apl coremediamanager.cpp mediaplayer.cpp mediautils.cpp + mediatrack.cpp ) diff --git a/aplcore/src/media/coremediamanager.cpp b/aplcore/src/media/coremediamanager.cpp index 20df98d..e63f041 100644 --- a/aplcore/src/media/coremediamanager.cpp +++ b/aplcore/src/media/coremediamanager.cpp @@ -179,7 +179,11 @@ CoreMediaManager::mediaLoadComplete( mediaObject->mState = isReady ? MediaObject::kReady : MediaObject::kError; mediaObject->mMediaErrorCode = isReady ? 0 : errorCode; mediaObject->mMediaErrorText = isReady ? std::string() : errorReason; - for (const auto& m : mediaObject->mCallbacks) + + // Make a copy first because the callbacks can trigger commands that result in more callbacks + const std::map callbacks(mediaObject->mCallbacks); + + for (const auto& m : callbacks) m.second(ptr); mediaObject->mCallbacks.clear(); } diff --git a/aplcore/src/media/mediatrack.cpp b/aplcore/src/media/mediatrack.cpp new file mode 100644 index 0000000..a2de890 --- /dev/null +++ b/aplcore/src/media/mediatrack.cpp @@ -0,0 +1,24 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "apl/media/mediatrack.h" + +namespace apl { + +Bimap sTextTrackTypeMap = { + {kTextTrackTypeCaption, "caption"}, +}; + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/media/mediautils.cpp b/aplcore/src/media/mediautils.cpp index 8d6b0ae..bf5fd51 100644 --- a/aplcore/src/media/mediautils.cpp +++ b/aplcore/src/media/mediautils.cpp @@ -22,15 +22,20 @@ std::vector mediaSourcesToTracks(const Object& mediaSources) { std::vector result; - for (auto i = 0 ; i < mediaSources.size() ; i++) { - const auto& ms = mediaSources.at(i).get(); - result.emplace_back(MediaTrack{ - ms.getUrl(), // URL - ms.getHeaders(), // HTTP HeaderArray - ms.getOffset(), // Offset - ms.getDuration(), // Duration - ms.getRepeatCount() // Repeat count - }); + if (mediaSources.isArray()) { + for (auto i = 0 ; i < mediaSources.size() ; i++) { + if (mediaSources.at(i).is()) { + const auto& ms = mediaSources.at(i).get(); + result.emplace_back(MediaTrack{ + ms.getUrl(), // URL + ms.getOffset(), // Offset + ms.getDuration(), // Duration + ms.getRepeatCount(), // Repeat count + ms.getHeaders(), // HTTP HeaderArray + ms.getTextTracks() // Text Tracks + }); + } + } } return result; } diff --git a/aplcore/src/primitives/CMakeLists.txt b/aplcore/src/primitives/CMakeLists.txt index 0b1849b..1cb3692 100644 --- a/aplcore/src/primitives/CMakeLists.txt +++ b/aplcore/src/primitives/CMakeLists.txt @@ -15,25 +15,25 @@ target_sources_local(apl PRIVATE accessibilityaction.cpp color.cpp - range.cpp dimension.cpp filter.cpp functions.cpp gradient.cpp keyboard.cpp mediasource.cpp + object.cpp + objecttype.cpp radii.cpp + range.cpp rect.cpp roundedrect.cpp - symbolreferencemap.cpp styledtext.cpp styledtextstate.cpp + symbolreferencemap.cpp timefunctions.cpp timegrammar.cpp transform.cpp transform2d.cpp - object.cpp - objecttype.cpp unicode.cpp urlrequest.cpp ) diff --git a/aplcore/src/primitives/filter.cpp b/aplcore/src/primitives/filter.cpp index a2d9507..6624446 100644 --- a/aplcore/src/primitives/filter.cpp +++ b/aplcore/src/primitives/filter.cpp @@ -13,10 +13,12 @@ * permissions and limitations under the License. */ -#include "apl/extension/extensionmanager.h" #include "apl/primitives/filter.h" + #include "apl/engine/evaluate.h" #include "apl/engine/propdef.h" +#include "apl/extension/extensionmanager.h" +#include "apl/primitives/color.h" #include "apl/utils/session.h" namespace apl { @@ -74,6 +76,12 @@ Bimap sBlendModeBimap = { {kBlendModeSaturation, "saturation"}, {kBlendModeColor, "color"}, {kBlendModeLuminosity, "luminosity"}, + {kBlendModeSourceAtop, "source-atop"}, + {kBlendModeSourceAtop, "sourceAtop"}, + {kBlendModeSourceIn, "source-in"}, + {kBlendModeSourceIn, "sourceIn"}, + {kBlendModeSourceOut, "source-out"}, + {kBlendModeSourceOut, "sourceOut"}, }; using FilterPropDef = PropDef; diff --git a/aplcore/src/primitives/functions.cpp b/aplcore/src/primitives/functions.cpp index 05f437c..4935785 100644 --- a/aplcore/src/primitives/functions.cpp +++ b/aplcore/src/primitives/functions.cpp @@ -13,17 +13,18 @@ * permissions and limitations under the License. */ +#include "apl/primitives/functions.h" + #include #include +#include +#include #ifdef APL_CORE_UWP #include #endif -#include -#include #include "apl/animation/coreeasing.h" #include "apl/engine/context.h" -#include "apl/primitives/functions.h" #include "apl/primitives/rangegenerator.h" #include "apl/primitives/slicegenerator.h" #include "apl/primitives/timefunctions.h" diff --git a/aplcore/src/primitives/mediasource.cpp b/aplcore/src/primitives/mediasource.cpp index 87fa01e..734f980 100644 --- a/aplcore/src/primitives/mediasource.cpp +++ b/aplcore/src/primitives/mediasource.cpp @@ -18,6 +18,7 @@ #include "apl/content/rootconfig.h" #include "apl/primitives/mediasource.h" #include "apl/primitives/objectdata.h" +#include "apl/media/mediatrack.h" #include "apl/utils/log.h" #include "apl/utils/session.h" @@ -28,12 +29,14 @@ MediaSource::MediaSource(URLRequest urlRequest, int duration, int repeatCount, Object entities, - int offset) : mUrlRequest(std::move(urlRequest)), + int offset, + TextTrackArray textTracks) : mUrlRequest(std::move(urlRequest)), mDescription(std::move(description)), mDuration(duration), mRepeatCount(repeatCount), mEntities(std::move(entities)), - mOffset(offset) + mOffset(offset), + mTextTracks(textTracks) {} Object @@ -53,7 +56,8 @@ MediaSource::create(const Context& context, const Object& object) 0, 0, std::vector(), - 0)); + 0, + TextTrackArray())); } if (!object.isMap()) @@ -71,12 +75,41 @@ MediaSource::create(const Context& context, const Object& object) int offset = propertyAsInt(context, object, "offset", 0); auto entities = Object(arrayifyProperty(context, object, "entities", "entity")); + TextTrackArray tracks; + for (auto& m : arrayifyProperty(context, object, "textTrack")) { + if (!m.isMap()) { + CONSOLE(context) << "Text Track is not an object."; + continue; + } + + auto type = propertyAsMapped(context, m, "type", static_cast(-1), sTextTrackTypeMap); + if (type < 0) { + CONSOLE(context) << "Unrecognized type field in Text Track"; + continue; + } + + std::string url = propertyAsString(context, m, "url"); + if (url.empty()) { + CONSOLE(context) << "Text Track has no URL defined."; + continue; + } + + std::string description = propertyAsString(context, m, "description"); + + tracks.push_back(TextTrack{ + static_cast(type), + URLRequest::create(context, m).get().getUrl(), + description + }); + } + return Object(MediaSource(URLRequest::create(context, object).get(), description, duration, repeatCount, entities, - offset)); + offset, + tracks)); } std::string @@ -101,6 +134,15 @@ MediaSource::serialize(rapidjson::Document::AllocatorType& allocator) const { for(const auto& header : getHeaders()) vHeaders.PushBack(Value(header.c_str(), allocator).Move(), allocator); v.AddMember("headers", vHeaders, allocator); + rapidjson::Value vTextTracks(rapidjson::kArrayType); + for(const auto& track : getTextTracks()) { + rapidjson::Value t(rapidjson::kObjectType); + t.AddMember("url", Value(track.url.c_str(), allocator).Move(), allocator); + t.AddMember("description", Value(track.description.c_str(), allocator).Move(), allocator); + t.AddMember("type", Value(sTextTrackTypeMap.at(track.type).c_str(), allocator), allocator); + vTextTracks.PushBack(t, allocator); + } + v.AddMember("textTracks", vTextTracks, allocator); return v; } diff --git a/aplcore/src/primitives/rect.cpp b/aplcore/src/primitives/rect.cpp index 5e83ab3..4c38a82 100644 --- a/aplcore/src/primitives/rect.cpp +++ b/aplcore/src/primitives/rect.cpp @@ -156,7 +156,9 @@ Rect::serialize(rapidjson::Document::AllocatorType& allocator) const { std::string Rect::toDebugString() const{ - return "Rect<" + toString() + ">"; + return "Rect<" + std::to_string(mWidth) + "x" + std::to_string(mHeight) + + (mX >= 0 ? "+" : "") + std::to_string(mX) + + (mY >= 0 ? "+" : "") + std::to_string(mY) + ">"; } } // namespace apl diff --git a/aplcore/src/primitives/styledtext.cpp b/aplcore/src/primitives/styledtext.cpp index ba42979..33ba07a 100644 --- a/aplcore/src/primitives/styledtext.cpp +++ b/aplcore/src/primitives/styledtext.cpp @@ -13,18 +13,19 @@ * permissions and limitations under the License. */ +#include "apl/primitives/styledtext.h" + #include #include #include #include -#include "apl/primitives/objectdata.h" #include "apl/primitives/color.h" #include "apl/primitives/dimension.h" -#include "apl/primitives/unicode.h" -#include "apl/primitives/styledtext.h" +#include "apl/primitives/objectdata.h" #include "apl/primitives/styledtextstate.h" +#include "apl/primitives/unicode.h" #include "apl/utils/stringfunctions.h" namespace apl { diff --git a/aplcore/src/primitives/styledtextstate.cpp b/aplcore/src/primitives/styledtextstate.cpp index 29d1482..490212c 100644 --- a/aplcore/src/primitives/styledtextstate.cpp +++ b/aplcore/src/primitives/styledtextstate.cpp @@ -13,15 +13,17 @@ * permissions and limitations under the License. */ -#include "apl/engine/evaluate.h" -#include "apl/primitives/color.h" #include "apl/primitives/styledtextstate.h" -#include "apl/utils/stringfunctions.h" #include #include #include +#include "apl/engine/evaluate.h" +#include "apl/primitives/color.h" +#include "apl/primitives/dimension.h" +#include "apl/utils/stringfunctions.h" + namespace apl { const std::string INHERIT_ATTRIBUTE_VALUE = "inherit"; diff --git a/aplcore/src/primitives/timegrammar.cpp b/aplcore/src/primitives/timegrammar.cpp index cb2c6f5..2d38566 100644 --- a/aplcore/src/primitives/timegrammar.cpp +++ b/aplcore/src/primitives/timegrammar.cpp @@ -15,6 +15,8 @@ #include "apl/primitives/timegrammar.h" +#include "apl/utils/log.h" + namespace apl { namespace timegrammar { diff --git a/aplcore/src/primitives/transform2d.cpp b/aplcore/src/primitives/transform2d.cpp index 046c2bc..efe2f55 100644 --- a/aplcore/src/primitives/transform2d.cpp +++ b/aplcore/src/primitives/transform2d.cpp @@ -28,7 +28,7 @@ namespace t2grammar { // Isolate the PEGTL to a local namespace namespace pegtl = tao::TAO_PEGTL_NAMESPACE; using namespace pegtl; -static bool DEBUG_GRAMMAR = false; +static const bool DEBUG_GRAMMAR = false; /** * \cond Showt2grammar diff --git a/aplcore/src/primitives/unicode.cpp b/aplcore/src/primitives/unicode.cpp index 3ee748f..0b2ea8b 100644 --- a/aplcore/src/primitives/unicode.cpp +++ b/aplcore/src/primitives/unicode.cpp @@ -261,7 +261,7 @@ utf8StringTrim(std::string& utf8String, int maxLength) auto it = utf8String.begin(); for (int i = 0 ; i < maxLength ; i++) { - if (*it == 0) + if (it == utf8String.end()) return false; it += countUTF8TrailingBytes(*it) + 1; } diff --git a/aplcore/src/scenegraph/CMakeLists.txt b/aplcore/src/scenegraph/CMakeLists.txt index 14a6888..c78b78a 100644 --- a/aplcore/src/scenegraph/CMakeLists.txt +++ b/aplcore/src/scenegraph/CMakeLists.txt @@ -18,8 +18,8 @@ target_sources_local(apl edittextconfig.cpp filter.cpp layer.cpp - modifiednodelist.cpp node.cpp + graphicfragment.cpp paint.cpp path.cpp pathop.cpp diff --git a/aplcore/src/scenegraph/builder.cpp b/aplcore/src/scenegraph/builder.cpp index f7c2592..4f6fc6d 100644 --- a/aplcore/src/scenegraph/builder.cpp +++ b/aplcore/src/scenegraph/builder.cpp @@ -21,6 +21,7 @@ #include "apl/primitives/accessibilityaction.h" #include "apl/scenegraph/accessibility.h" #include "apl/scenegraph/builder.h" +#include "apl/scenegraph/graphicfragment.h" #include "apl/scenegraph/pathparser.h" #include "apl/scenegraph/scenegraphupdates.h" @@ -285,7 +286,7 @@ paint(const GraphicPatternPtr& pattern, float opacity, Transform2D transform) NodePtr node = nullptr; for (auto it = pattern->getItems().rbegin() ; it != pattern->getItems().rend() ; it++) { - auto child = (*it)->getSceneGraph(updates); + auto child = (*it)->buildSceneGraph(false, updates)->node(); if (child) node = child->setNext(node); } diff --git a/aplcore/src/scenegraph/graphicfragment.cpp b/aplcore/src/scenegraph/graphicfragment.cpp new file mode 100644 index 0000000..f356466 --- /dev/null +++ b/aplcore/src/scenegraph/graphicfragment.cpp @@ -0,0 +1,330 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +/** + * Merging layers is only possible if the following conditions are met: + * + * 1. Both layers must have kCharacteristicImmutableProperties set. That is, the layers won't transform + * or change opacity + * 2. If the current layer has child layers, the merged layer must not have content + * (otherwise the drawing order would be wrong) + * + * If the current layer has: + * No Content --- The merge is okay. Copy the merged layer kCharacteristicImmutableContent flag setting. + * kCharacteristicImmutableContent --- The merged layer must have kCharacteristicImmutableContent or no content + * !kCharacteristicImmutableContent --- The merged layer must have !kCharacteristicImmutableContent or no content + */ +#include "apl/graphic/graphicelement.h" +#include "apl/graphic/graphicfilter.h" +#include "apl/scenegraph/builder.h" +#include "apl/scenegraph/graphicfragment.h" +#include "apl/scenegraph/scenegraphupdates.h" + +namespace apl { +namespace sg { + +GraphicFragmentPtr +GraphicFragment::create(const GraphicElementPtr& element) +{ + auto result = std::make_shared(); + result->mElements.push_back(element); + return result; +} + +GraphicFragmentPtr +GraphicFragment::create(const GraphicElementPtr& element, + const sg::NodePtr& node, Type flags) +{ + assert(flags == kNodeContentFixed || flags == kNodeContentMutable); + + auto result = std::make_shared(); + result->mElements.push_back(element); + result->mNode = node; + result->mType = flags; + return result; +} + +GraphicFragmentPtr +GraphicFragment::create(const GraphicElementPtr& element, + const sg::LayerPtr& layer, Type flags) +{ + assert(layer); + assert(flags != kNodeContentFixed && flags != kNodeContentMutable); + element->assignSceneGraphLayer(layer); + + auto result = std::make_shared(); + result->mElements.push_back(element); + result->mLayer = layer; + result->mType = flags; + return result; +} + +/** + * Merge a node or layer into this node or layer. Note that anything that is a Node is considered + * to be immutable; it doesn't vary over time. Anything that is a Layer may or may not be mutable. + * + * If both are nodes (and hence both are fixed), combine them into a node and return true. + * + * If this is a node and the other is a layer, return false. + * + * If this is a layer and the other is a node, then convert the other to a node and set CONTENT/LAYER fixed + * on that node. + * + * If this is a layer and the other is a layer, then they can be combined only if: + * (1) this has no child layers AND + * (2) this and the other both have the same CONTENT_FIXED flag AND + * (3) neither this nor the other has the LAYER_FIXED flag unset (i.e., both must be fixed) AND + * (4) neither this nor the other has a transform or opacity applied. + * + * + * @param other + * @return + */ +bool +GraphicFragment::mergeWith(const GraphicFragmentPtr& otherPtr) +{ + // An empty fragment should never call mergeWith(). Empty fragments are only used + // to accumulate children (see GraphicElement::ensureSceneGraphChildren) + assert(!empty()); + + if (!otherPtr) + return true; + + auto& other = *otherPtr.get(); + if (other.empty()) + return true; + + // Check if this is a node + if (isNode()) { + if (other.isLayer()) + return false; + + // Merge the nodes together + mNode = sg::Node::appendSiblingToNode(mNode, other.node()); + mElements.insert(mElements.end(), other.mElements.begin(), other.mElements.end()); + return true; + } + + if (other.isNode()) + return false; + + // Two layers. They can't merge unless both have fixed layer properties. + if (mType == kLayerMutable || other.mType == kLayerMutable) + return false; + + // Two fixed layers. + // Do not merge if one has mutating content and the other has non-null fixed content + if ((mType == kLayerFixedContentMutable && other.mType == kLayerFixedContentFixed && other.mNode) || + (mType == kLayerFixedContentFixed && other.mType == kLayerFixedContentMutable && mNode)) + return false; + + // Don't merge layers with shadows + if (mLayer->getShadow() != nullptr || other.mLayer->getShadow() != nullptr) + return false; + + // Don't merge layers with different outlines + if (mLayer->getOutline() != other.mLayer->getOutline()) + return false; + + // Don't merge layers with different transforms + // TODO: This could be allowed if the layers only have content (no sub-layers) and we + // TODO: push the transform into a content node + if (mLayer->getTransform() != other.mLayer->getTransform()) + return false; + + // Don't merge layers with different opacities + // TODO: Allowable if the layers only have content (no sub-layers) and the opacity is pushed + // TODO: into the content nodes + if (mLayer->getOpacity() != other.mLayer->getOpacity()) + return false; + + // If this layer has child layers and the other layer has content, don't allow the merge + // because the draw order will be messed up. + // TODO: We could move the other layer's content into a layer and merge anyway + if (!mLayer->children().empty() && other.layer()->content()) + return false; + + // The merge is okay. Combine the sub-layers + mLayer->appendChildren(other.layer()->children()); + + // Join the content together and calculate a new bounding box + auto node = mLayer->content(); + auto otherNode = other.layer()->content(); + mLayer->setContent(sg::Node::appendSiblingToNode(node, otherNode)); + fixBoundingBox(); + + // Update the other elements to point to the new layer + assert(!other.mElements.empty()); + for (const auto& m : other.mElements) + m->assignSceneGraphLayer(mLayer); + mElements.insert(mElements.end(), other.mElements.begin(), other.mElements.end()); + return true; +} + +void +GraphicFragment::addChild(const GraphicFragmentPtr& otherPtr, sg::SceneGraphUpdates& sceneGraph) +{ + if (!otherPtr) + return; + + auto& other = *otherPtr.get(); + if (other.empty()) + return; + + // If the other is a layer, we force this to be a layer and add the other to our children list + if (other.isLayer()) { + ensureLayer(sceneGraph); + mLayer->appendChild(other.layer()); + return; + } + + // If we're a node and the other is a node (as tested above), just append the nodes + if (!isLayer()) { + mNode = sg::Node::appendSiblingToNode(mNode, other.node()); + if (mType != kNodeContentMutable) + mType = other.mType; + mElements.insert(mElements.begin(), other.mElements.begin(), other.mElements.end()); + return; + } + + // ==== We're a layer and the other is a node ==== + assert(other.mType == kNodeContentFixed); // If it was mutable, it would be a layer + + // We have at least one child already. Force the other to become a layer and append it + if (!mLayer->children().empty()) { + other.ensureLayer(sceneGraph); + mLayer->appendChild(other.layer()); + return; + } + + // We have no children. The other node can be appended onto our content. + // Fix the bounding box and copy over the element list from the other fragment + mLayer->setContent(sg::Node::appendSiblingToNode(mLayer->content(), other.node())); + fixBoundingBox(); + for (const auto& m : other.mElements) + m->assignSceneGraphLayer(mLayer); + mElements.insert(mElements.begin(), other.mElements.begin(), other.mElements.end()); +} + +void +GraphicFragment::ensureLayer(sg::SceneGraphUpdates& sceneGraph) +{ + if (!mLayer) { + assert(!mElements.empty()); + mLayer = sg::layer(mElements.front()->getUniqueId()+"_sub", Rect(0,0,0,0), 1.0f, Transform2D()); + sceneGraph.created(mLayer); + mLayer->setContent(mNode); + mLayer->setCharacteristic(sg::Layer::kCharacteristicRenderOnly | + sg::Layer::kCharacteristicDoNotClipChildren); + mNode = nullptr; + mType = (mType == kNodeContentMutable ? kLayerFixedContentMutable : kLayerFixedContentFixed); + for (const auto& m : mElements) + m->assignSceneGraphLayer(mLayer); + fixBoundingBox(); + } +} + +void +GraphicFragment::fixBoundingBox() +{ + if (mLayer) { + Rect bb = sg::Node::calculateBoundingBox(mLayer->content()); + if (!bb.empty()) { + mLayer->setBounds(bb); + mLayer->setContentOffset(bb.getTopLeft()); + mLayer->setChildOffset(bb.getTopLeft()); + } + else { + mLayer->setBounds(Rect(0,0,0,0)); + } + } +} + +void +GraphicFragment::applyFilters(const Object &filters) +{ + // Build up the filter list in reverse order + for (int i = 0; i < filters.size(); i++) { + const auto& filter = filters.at(i).get(); + switch (filter.getType()) { + case kGraphicFilterTypeDropShadow: { + addShadow(sg::shadow( + filter.getValue(kGraphicPropertyFilterColor).getColor(), + Point{filter.getValue(kGraphicPropertyFilterHorizontalOffset).asFloat(), + filter.getValue(kGraphicPropertyFilterVerticalOffset).asFloat()}, + filter.getValue(kGraphicPropertyFilterRadius).asFloat())); + } break; + } + } +} + +void +GraphicFragment::assignToLayer(const sg::LayerPtr& containingLayer) +{ + for (const auto& m : mElements) + m->assignSceneGraphLayer(containingLayer); +} + +std::string +GraphicFragment::toDebugString() const +{ + std::string result = ""; + + switch (mType) { + case kEmpty: + result += "NodeEmpty<"; + break; + case kNodeContentFixed: + result += "NodeContentFixed<"; + break; + case kNodeContentMutable: + result += "NodeContentMutable<"; + break; + case kLayerFixedContentFixed: + result += "LayerFixedContentFixed<"; + break; + case kLayerFixedContentMutable: + result += "LayerFixedContentMutable<"; + break; + case kLayerMutable: + result += "LayerMutable<"; + break; + } + + if (mLayer) + result += "layer=" + mLayer->getName() + " sublayers=" + std::to_string(mLayer->children().size()); + else if (mNode) + result += "node=" + mNode->toDebugString(); + + for (const auto& m : mElements) + result += " element=" + m->toDebugString(); + + return result + ">"; +} + +void +GraphicFragment::addShadow(const sg::ShadowPtr& shadow) +{ + if (mLayer) { + mLayer->setShadow(shadow); + } + else if (mNode) { + mNode = sg::shadowNode(shadow, mNode); + } +} + +} // namespace sg +} // namsspace apl diff --git a/aplcore/src/scenegraph/layer.cpp b/aplcore/src/scenegraph/layer.cpp index 44a132e..94b9a61 100644 --- a/aplcore/src/scenegraph/layer.cpp +++ b/aplcore/src/scenegraph/layer.cpp @@ -84,9 +84,29 @@ Layer::debugInteractionString() const return result; } +std::string +Layer::debugCharacteristicString() const +{ + static const char *FIXED_NAMES[] = { + "DO_NOT_CLIP_CHILDREN", // 1u << 0 + "RENDER_ONLY", // 1u << 1 + nullptr + }; + + std::string result; + for (int i = 0 ; FIXED_NAMES[i] ; i++) { + if ((mCharacteristics & (1u << i)) != 0) { + if (!result.empty()) + result += ' '; + result += FIXED_NAMES[i]; + } + } + return result; +} + void -Layer::updateInteraction(unsigned int interaction, bool isSet) +Layer::updateInteraction(InteractionType interaction, bool isSet) { auto old = mInteraction; if (isSet) @@ -117,6 +137,12 @@ Layer::appendChild(const LayerPtr& child) setFlag(kFlagChildrenChanged); } +void +Layer::appendChildren(const std::vector& children) +{ + mChildren.insert(mChildren.end(), children.begin(), children.end()); +} + void Layer::setContent(const NodePtr& node) { @@ -126,6 +152,15 @@ Layer::setContent(const NodePtr& node) } } +void +Layer::setContentOffset(const apl::Point& offset) +{ + if (mContentOffset != offset) { + mContentOffset = offset; + setFlag(kFlagRedrawContent); + } +} + bool Layer::setBounds(const Rect& bounds) { @@ -145,10 +180,12 @@ Layer::setBounds(const Rect& bounds) bool Layer::setOutline(const PathPtr& outline) { - if (outline == mOutline) + // Empty outlines are treated as nullptr + auto path = outline && !outline->empty() ? outline : nullptr; + if (path == mOutline) return false; - mOutline = outline; + mOutline = path; setFlag(kFlagOutlineChanged); return true; } @@ -156,10 +193,12 @@ Layer::setOutline(const PathPtr& outline) bool Layer::setChildClip(const PathPtr& childClip) { - if (childClip == mChildClip) + // Empty clipping paths are treated as nullptr + auto path = childClip && !childClip->empty() ? childClip : nullptr; + if (path == mChildClip) return false; - mChildClip = childClip; + mChildClip = path; setFlag(kFlagChildClipChanged); return true; } @@ -201,10 +240,12 @@ Layer::setChildOffset(Point childOffset) bool Layer::setShadow(const ShadowPtr& shadow) { - if (shadow == mShadow) + // An invisible shadow is treated as a nullptr + auto s = shadow && shadow->visible() ? shadow : nullptr; + if (s == mShadow) return false; - mShadow = std::move(shadow); + mShadow = std::move(s); setFlag(kFlagRedrawShadow); return true; } @@ -223,7 +264,7 @@ Layer::setAccessibility(const AccessibilityPtr& accessibility) std::string Layer::toDebugString() const { - return "Layer"; + return "Layer " + mName; } bool @@ -256,6 +297,7 @@ Layer::serialize(rapidjson::Document::AllocatorType& allocator) const out.AddMember("bounds", mBounds.serialize(allocator), allocator); out.AddMember("transform", mTransform.serialize(allocator), allocator); out.AddMember("childOffset", mChildOffset.serialize(allocator), allocator); + out.AddMember("contentOffset", mContentOffset.serialize(allocator), allocator); if (mAccessibility) out.AddMember("accessibility", mAccessibility->serialize(allocator), allocator); @@ -281,6 +323,7 @@ Layer::serialize(rapidjson::Document::AllocatorType& allocator) const } out.AddMember("interaction", mInteraction, allocator); + out.AddMember("characteristics", mCharacteristics, allocator); return out; } diff --git a/aplcore/src/scenegraph/modifiednodelist.cpp b/aplcore/src/scenegraph/modifiednodelist.cpp deleted file mode 100644 index 6c2160c..0000000 --- a/aplcore/src/scenegraph/modifiednodelist.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0/ - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -#include "apl/scenegraph/modifiednodelist.h" -#include "apl/scenegraph/node.h" - -namespace apl { -namespace sg { - - -static NodePtr sTerminal = std::make_shared(); - -ModifiedNodeList::ModifiedNodeList() -{ - // Always start the list with a known terminal. - mModified = sTerminal; -} - -ModifiedNodeList::~ModifiedNodeList() -{ - clear(); -} - -void -ModifiedNodeList::clear() -{ - // Release all of the node chain - auto *ptr = mModified.get(); - while (ptr) { - ptr->clearFlags(); - auto *next = ptr->mNextModified.get(); - ptr->mNextModified.reset(); - ptr = next; - } -} - -void -ModifiedNodeList::contentChanged(Node* node) -{ - assert(node); - node->setFlag(Node::kNodeFlagModified); - - if (!node->mNextModified) { - node->mNextModified = mModified; - mModified = node->shared_from_this(); - } -} - -} // namespace sg -} // namespace apl diff --git a/aplcore/src/scenegraph/node.cpp b/aplcore/src/scenegraph/node.cpp index 07e54b5..e77508b 100644 --- a/aplcore/src/scenegraph/node.cpp +++ b/aplcore/src/scenegraph/node.cpp @@ -34,10 +34,14 @@ Node::~Node() { } -void +bool Node::setChild(const NodePtr& child) { + if (mFirstChild == child) + return false; + mFirstChild = child; + return true; } NodePtr @@ -47,6 +51,19 @@ Node::setNext(const NodePtr& sibling) return shared_from_this(); } +NodePtr +Node::appendSiblingToNode(const NodePtr& head, const NodePtr& sibling) +{ + if (!head) + return sibling; + + auto n = head; + while (n->mNextSibling) + n = n->mNextSibling; + n->mNextSibling = sibling; + return head; +} + void Node::removeAllChildren() { @@ -65,25 +82,6 @@ Node::childCount() const return result; } -bool -Node::needsRedraw() const -{ - // If there are no children and the children haven't changed, there is nothing to draw. - // Note: this must be overridden by classes that actually draw something. - if (!mFirstChild && !isFlagSet(kNodeFlagChildrenChanged)) - return false; - - // If something changed, we need to be drawn - if (anyFlagSet()) - return true; - - for (auto child = mFirstChild ; child ; child = child->mNextSibling) - if (child->needsRedraw()) - return true; - - return false; -} - bool Node::visible() const { @@ -97,19 +95,19 @@ Node::visible() const Rect Node::boundingBox(const Transform2D& transform) const { - Rect result; - for (auto child = mFirstChild ; child ; child = child->mNextSibling) - result = result.extend(child->boundingBox(transform)); + Rect result = localBoundingBox(transform); + if (mNextSibling) + result = result.extend(mNextSibling->boundingBox(transform)); return result; } Rect -Node::calculateBoundingBox(const NodePtr& node, const Transform2D& transform) +Node::localBoundingBox(const apl::Transform2D& transform) const { - Rect result; - for (auto n = node ; n ; n = n->next()) - result = result.extend(n->boundingBox(transform)); - return result; + if (!mFirstChild) + return {}; + + return mFirstChild->boundingBox(transform); } rapidjson::Value @@ -153,13 +151,6 @@ DrawNode::toDebugString() const return "DrawNode"; } -bool -DrawNode::needsRedraw() const -{ - // TODO: Do something smarter with the paint - return anyFlagSet(); -} - bool DrawNode::visible() const { @@ -172,7 +163,7 @@ DrawNode::visible() const } Rect -DrawNode::boundingBox(const Transform2D& transform) const +DrawNode::localBoundingBox(const Transform2D& transform) const { if (!mPath) return {}; @@ -251,13 +242,6 @@ TextNode::toDebugString() const return result; } -bool -TextNode::needsRedraw() const -{ - // TODO: Do something smarter with the paint - return anyFlagSet(); -} - bool TextNode::visible() const { @@ -273,7 +257,7 @@ TextNode::visible() const } Rect -TextNode::boundingBox(const Transform2D& transform) const +TextNode::localBoundingBox(const Transform2D& transform) const { if (!mTextLayout) return {}; @@ -329,9 +313,9 @@ TransformNode::toDebugString() const } Rect -TransformNode::boundingBox(const Transform2D& transform) const +TransformNode::localBoundingBox(const Transform2D& transform) const { - return Node::boundingBox(transform * mTransform); + return Node::localBoundingBox(transform * mTransform); } rapidjson::Value @@ -363,9 +347,9 @@ ClipNode::toDebugString() const } Rect -ClipNode::boundingBox(const Transform2D& transform) const +ClipNode::localBoundingBox(const Transform2D& transform) const { - auto bb = Node::boundingBox(transform); + auto bb = Node::localBoundingBox(transform); if (mPath) bb = bb.intersect(mPath->boundingBox(transform)); return bb; @@ -400,29 +384,6 @@ OpacityNode::toDebugString() const return "OpacityNode opacity=" + std::to_string(mOpacity); } -bool -OpacityNode::needsRedraw() const -{ - // If there are no children and the children haven't changed, there is nothing to draw. - // Note: this must be overridden by classes that actually draw something. - if (!mFirstChild && !isFlagSet(kNodeFlagChildrenChanged)) - return false; - - if (mOpacity == 0.0f && !isFlagSet(kNodeFlagModified)) - return false; - - // Ask the children if they need to be drawn - if (mOpacity > 0.0f && !anyFlagSet()) { - for (auto child = mFirstChild ; child ; child = child->next()) - if (child->needsRedraw()) - return true; - - return false; - } - - return true; -} - bool OpacityNode::visible() const { @@ -479,13 +440,6 @@ ImageNode::toDebugString() const return "ImageNode target="+mTarget.toDebugString() + " source=" + mSource.toDebugString(); } -bool -ImageNode::needsRedraw() const -{ - // TODO: Do something smarter with the paint - return anyFlagSet(); -} - bool ImageNode::visible() const { @@ -493,7 +447,7 @@ ImageNode::visible() const } Rect -ImageNode::boundingBox(const Transform2D& transform) const +ImageNode::localBoundingBox(const Transform2D& transform) const { return mTarget.boundingBox(transform); } @@ -551,13 +505,6 @@ VideoNode::toDebugString() const return result; } -bool -VideoNode::needsRedraw() const -{ - // TODO: Do something smarter with the paint - return anyFlagSet(); -} - bool VideoNode::visible() const { @@ -565,7 +512,7 @@ VideoNode::visible() const } Rect -VideoNode::boundingBox(const Transform2D& transform) const +VideoNode::localBoundingBox(const Transform2D& transform) const { return mTarget.boundingBox(transform); } @@ -601,13 +548,16 @@ ShadowNode::toDebugString() const } Rect -ShadowNode::boundingBox(const Transform2D& transform) const +ShadowNode::localBoundingBox(const Transform2D& transform) const { if (!mShadow) - return Node::boundingBox(transform); + return Node::localBoundingBox(transform); + + if (!mFirstChild) + return {}; // Calculate the content size in the local coordinate system - auto inner = Node::boundingBox(Transform2D()); + auto inner = mFirstChild->boundingBox(Transform2D()); auto offset = mShadow->getOffset(); auto delta = mShadow->getRadius(); @@ -682,27 +632,12 @@ EditTextNode::toDebugString() const return result; } -bool -EditTextNode::needsRedraw() const -{ - // TODO: Do something smarter with the paint - return anyFlagSet(); -} - bool EditTextNode::visible() const { return true; } -Rect -EditTextNode::boundingBox(const Transform2D& transform) const -{ - // Note: The EditTextNode has no intrinsic size. An EditTextNode is required to - // be directly under a layer and the layer sets the size of the edit text box. - return {}; -} - rapidjson::Value EditTextNode::serialize(rapidjson::Document::AllocatorType& allocator) const { diff --git a/aplcore/src/scenegraph/pathbounds.cpp b/aplcore/src/scenegraph/pathbounds.cpp index 550f814..6f6d943 100644 --- a/aplcore/src/scenegraph/pathbounds.cpp +++ b/aplcore/src/scenegraph/pathbounds.cpp @@ -57,67 +57,74 @@ class ExpandingRect { }; /** - * Given the cubic polynomial f(t) = a*(1-t)^3 + 3*b*t*(1-t)^2 + 3*c*t^2*(1-t) + d*t^3, + * Given the cubic polynomial f(t) = p1*(1-t)^3 + 3*p2*t*(1-t)^2 + 3*p3*t^2*(1-t) + p4*t^3, * calculate the values of t where f'(t) = 0 and 0 < t < 1. Return the number of roots * found and store the values of those roots in the provided array. - * @param a The starting point, a = f(0) - * @param b The first control point - * @param c The second control point - * @param d The ending point, d = f(1) + * @param p1 The starting point, p1 = f(0) + * @param p2 The first control point + * @param p3 The second control point + * @param p4 The ending point, p4 = f(1) * @param root An reference to an array with at least two elements. Return the value of t where * f'(t) = 0 if it exists and is in the interval (0,1) * @return The number of valid roots found. May be 0, 1, or 2. */ -int findCubicZeros(float a, float b, float c, float d, float roots[]) +int findCubicZeros(float p1, float p2, float p3, float p4, float roots[]) { - // Reduce f'(t) down to e*t^2 + 2*f*t + g - auto e = -a + 3*b - 3*c + d; - auto f = a - 2*b + c; - auto g = b - a; - - if (std::abs(e) < 1e-12) { // If e is zero we have a simple linear equation - if (std::abs(f) < 1e-12) // If f is also zero we have straight line - return 0; - - auto t = -g/(2*f); - if (t < 0.0f || t >= 1.0f) - return 0; - roots[0] = t; - return 1; - } + // Reduce b'(t) down to a*t^2 + b*t + c + auto a = -p1 + 3*p2 - 3*p3 + p4; + auto b = 2 * (p1 - 2*p2 + p3); + auto c = p2 - p1; - auto discriminant = f*f - e*g; + // Following Numerical Recipes in C (section 5.6) + auto discriminant = b*b - 4*a*c; if (discriminant < 0.0f) return 0; // Imaginary roots - auto h = sqrt(discriminant); - auto t1 = (-f + h) / e; - auto t2 = (-f - h) / e; + auto sqrt_d = std::sqrt(discriminant); + if (b < 0.0) + sqrt_d = -sqrt_d; + auto q = -0.5 * (b + sqrt_d); int i = 0; - if (t1 > 0 && t1 < 1.0f) - roots[i++] = t1; - if (t2 > 0 && t2 < 1.0f) - roots[i++] = t2; + + // The roots are q/a and c/q. + if (std::abs(a) > std::abs(q)) { + auto t = q / a; + if (t > 0 && t < 1.0f) + roots[i++] = static_cast(t); + } + + if (std::abs(q) > std::abs(c)) { + auto t = c / q; + if (t > 0 && t < 1.0f) + roots[i++] = static_cast(t); + } + return i; } /** - * Given the quadratic polynomial f(t) = a*(1-t)^2 + 2*b*t*(1-t) + c*t^2, calculate + * Given the quadratic polynomial f(t) = p1*(1-t)^2 + 2*p2*t*(1-t) + p3*t^2, calculate * the values of t where f'(t) = 0 and 0 < t < 1. - * @param a The starting point, a = f(0) - * @param b The control point. - * @param c The ending point, c = f(1) + * @param p1 The starting point, p1 = f(0) + * @param p2 The control point. + * @param p3 The ending point, p3 = f(1) * @param root Return the value of t where f'(t) = 0 if it exists and is in the interval (0,1) - * @return True if a root was found. + * @return True if p1 root was found. */ -bool findQuadraticZero(float a, float b, float c, float& root) +bool findQuadraticZero(float p1, float p2, float p3, float& root) { - auto d = a - 2 * b + c; - if (std::abs(d) < 1e-12) - return 0; + // f'(t) = -a + b*t [or (p2 - p1) + (p1 - 2*p2 + p3)t ] + // Root is a / b + auto a = p1 - p2; + auto b = p1 - 2 * p2 + p3; + + if (std::abs(b) > std::abs(a)) { + root = a / b; + if (root > 0 && root < 1.0f) + return true; + } - root = (a-b)/d; - return root > 0 && root < 1.0f; + return false; } /** diff --git a/aplcore/src/scenegraph/pathparser.cpp b/aplcore/src/scenegraph/pathparser.cpp index 6247c2b..4d75f40 100644 --- a/aplcore/src/scenegraph/pathparser.cpp +++ b/aplcore/src/scenegraph/pathparser.cpp @@ -179,7 +179,7 @@ MutablePath::getGeneralPath() { } // Drop any move commands from the end of the command list - while (mCommands.back() == 'M') { + while (!mCommands.empty() && mCommands.back() == 'M') { mCommands.resize(mCommands.size() - 1); mPoints.resize(mPoints.size() - 2); } diff --git a/aplcore/src/scenegraph/scenegraphupdates.cpp b/aplcore/src/scenegraph/scenegraphupdates.cpp index 8f8bf34..2eeb48a 100644 --- a/aplcore/src/scenegraph/scenegraphupdates.cpp +++ b/aplcore/src/scenegraph/scenegraphupdates.cpp @@ -33,6 +33,8 @@ SceneGraphUpdates::clear() for (const auto& m : mCreated) m->clearFlags(); mCreated.clear(); + + mResize.clear(); } void @@ -47,13 +49,6 @@ SceneGraphUpdates::changed(const LayerPtr& layer) mChanged.emplace(layer); } -void -SceneGraphUpdates::changed(Layer *layer) -{ - assert(layer); - changed(layer->shared_from_this()); -} - void SceneGraphUpdates::created(const LayerPtr& layer) { @@ -66,23 +61,39 @@ SceneGraphUpdates::created(const LayerPtr& layer) } void -SceneGraphUpdates::created(Layer *layer) +SceneGraphUpdates::resize(const LayerPtr& layer) { - assert(layer); - created(layer->shared_from_this()); + mResize.emplace(layer); } -void SceneGraphUpdates::mapChanged(std::function func) +void +SceneGraphUpdates::mapChanged(const std::function& func) { - std::for_each(mChanged.begin(), mChanged.end(), std::move(func)); + for (const auto& m : mChanged) + func(m); } -void SceneGraphUpdates::mapCreated(std::function func) +void +SceneGraphUpdates::fixCreatedFlags() { - std::for_each(mCreated.begin(), mCreated.end(), std::move(func)); + for (const auto& m : mCreated) + m->clearFlags(); } - +void +SceneGraphUpdates::processResize() +{ + for (const auto& m : mResize) { + Rect bb = sg::Node::calculateBoundingBox(m->content()); + if (m->setBounds(bb)) { + m->setContentOffset(bb.getTopLeft()); + m->setChildOffset(bb.getTopLeft()); + changed(m); + } + } + + mResize.clear(); +} } // namespace sg -} // namespace apl \ No newline at end of file +} // namespace apl diff --git a/aplcore/src/touch/gesture.cpp b/aplcore/src/touch/gesture.cpp index 4f7d465..63d02aa 100644 --- a/aplcore/src/touch/gesture.cpp +++ b/aplcore/src/touch/gesture.cpp @@ -15,14 +15,13 @@ #include "apl/touch/gesture.h" -#include "apl/engine/evaluate.h" #include "apl/component/touchablecomponent.h" -#include "apl/utils/session.h" - +#include "apl/engine/evaluate.h" #include "apl/touch/gestures/doublepressgesture.h" #include "apl/touch/gestures/longpressgesture.h" #include "apl/touch/gestures/swipeawaygesture.h" #include "apl/touch/gestures/tapgesture.h" +#include "apl/utils/session.h" namespace apl { @@ -41,7 +40,7 @@ Bimap sGestureTypeBimap = { {kGestureTypeTap, "Tap"} }; -static std::map sGestureFunctions = +static const std::map sGestureFunctions = { {kGestureTypeDoublePress, DoublePressGesture::create}, {kGestureTypeLongPress, LongPressGesture::create}, diff --git a/aplcore/src/touch/gestures/doublepressgesture.cpp b/aplcore/src/touch/gestures/doublepressgesture.cpp index 6d4f859..147d3c5 100644 --- a/aplcore/src/touch/gestures/doublepressgesture.cpp +++ b/aplcore/src/touch/gestures/doublepressgesture.cpp @@ -15,12 +15,10 @@ #include "apl/touch/gestures/doublepressgesture.h" -#include "apl/engine/evaluate.h" #include "apl/component/touchablecomponent.h" -#include "apl/utils/session.h" -#include "apl/engine/propdef.h" - #include "apl/content/rootconfig.h" +#include "apl/engine/propdef.h" +#include "apl/utils/session.h" namespace apl { diff --git a/aplcore/src/touch/gestures/flinggesture.cpp b/aplcore/src/touch/gestures/flinggesture.cpp index 41a925b..f466405 100644 --- a/aplcore/src/touch/gestures/flinggesture.cpp +++ b/aplcore/src/touch/gestures/flinggesture.cpp @@ -15,17 +15,15 @@ #include "apl/touch/gestures/flinggesture.h" +#include "apl/action/action.h" +#include "apl/animation/coreeasing.h" #include "apl/component/actionablecomponent.h" #include "apl/content/rootconfig.h" - #include "apl/engine/builder.h" -#include "apl/action/action.h" - -#include "apl/animation/coreeasing.h" #include "apl/primitives/timefunctions.h" +#include "apl/time/sequencer.h" #include "apl/touch/utils/velocitytracker.h" #include "apl/utils/session.h" -#include "apl/time/sequencer.h" namespace apl { diff --git a/aplcore/src/touch/gestures/longpressgesture.cpp b/aplcore/src/touch/gestures/longpressgesture.cpp index ee3f435..1ca5731 100644 --- a/aplcore/src/touch/gestures/longpressgesture.cpp +++ b/aplcore/src/touch/gestures/longpressgesture.cpp @@ -15,12 +15,10 @@ #include "apl/touch/gestures/longpressgesture.h" -#include "apl/engine/evaluate.h" #include "apl/component/touchwrappercomponent.h" -#include "apl/utils/session.h" -#include "apl/engine/propdef.h" - #include "apl/content/rootconfig.h" +#include "apl/engine/propdef.h" +#include "apl/utils/session.h" namespace apl { diff --git a/aplcore/src/touch/gestures/pagerflinggesture.cpp b/aplcore/src/touch/gestures/pagerflinggesture.cpp index e39a262..60ffba8 100644 --- a/aplcore/src/touch/gestures/pagerflinggesture.cpp +++ b/aplcore/src/touch/gestures/pagerflinggesture.cpp @@ -18,17 +18,15 @@ #include #include +#include "apl/action/action.h" +#include "apl/animation/coreeasing.h" #include "apl/component/pagercomponent.h" #include "apl/content/rootconfig.h" +#include "apl/engine/builder.h" +#include "apl/primitives/timefunctions.h" #include "apl/time/sequencer.h" #include "apl/time/timemanager.h" - -#include "apl/engine/builder.h" -#include "apl/action/action.h" - -#include "apl/animation/coreeasing.h" #include "apl/touch/utils/velocitytracker.h" -#include "apl/primitives/timefunctions.h" #include "apl/utils/session.h" namespace apl { diff --git a/aplcore/src/touch/gestures/scrollgesture.cpp b/aplcore/src/touch/gestures/scrollgesture.cpp index bc339cc..7ab808a 100644 --- a/aplcore/src/touch/gestures/scrollgesture.cpp +++ b/aplcore/src/touch/gestures/scrollgesture.cpp @@ -17,19 +17,15 @@ #include -#include "apl/component/scrollablecomponent.h" +#include "apl/animation/coreeasing.h" #include "apl/content/rootconfig.h" - #include "apl/engine/builder.h" -#include "apl/action/action.h" - -#include "apl/animation/coreeasing.h" #include "apl/primitives/timefunctions.h" +#include "apl/time/sequencer.h" #include "apl/touch/utils/autoscroller.h" #include "apl/touch/utils/velocitytracker.h" #include "apl/utils/make_unique.h" #include "apl/utils/session.h" -#include "apl/time/sequencer.h" namespace apl { diff --git a/aplcore/src/touch/gestures/swipeawaygesture.cpp b/aplcore/src/touch/gestures/swipeawaygesture.cpp index c300a98..aef3df0 100644 --- a/aplcore/src/touch/gestures/swipeawaygesture.cpp +++ b/aplcore/src/touch/gestures/swipeawaygesture.cpp @@ -17,19 +17,19 @@ #include -#include "apl/engine/evaluate.h" +#include "apl/action/action.h" +#include "apl/animation/easing.h" #include "apl/component/touchwrappercomponent.h" -#include "apl/utils/log.h" -#include "apl/utils/session.h" -#include "apl/engine/propdef.h" - #include "apl/content/rootconfig.h" +#include "apl/engine/builder.h" +#include "apl/engine/evaluate.h" +#include "apl/engine/propdef.h" #include "apl/primitives/timefunctions.h" +#include "apl/primitives/transform.h" #include "apl/time/timemanager.h" #include "apl/touch/utils/velocitytracker.h" - -#include "apl/engine/builder.h" -#include "apl/action/action.h" +#include "apl/utils/log.h" +#include "apl/utils/session.h" namespace apl { diff --git a/aplcore/src/touch/gestures/tapgesture.cpp b/aplcore/src/touch/gestures/tapgesture.cpp index e79962d..724d7ce 100644 --- a/aplcore/src/touch/gestures/tapgesture.cpp +++ b/aplcore/src/touch/gestures/tapgesture.cpp @@ -13,11 +13,10 @@ * permissions and limitations under the License. */ -#include -#include - #include "apl/touch/gestures/tapgesture.h" +#include + #include "apl/component/touchablecomponent.h" #include "apl/content/rootconfig.h" #include "apl/engine/propdef.h" diff --git a/aplcore/src/touch/pointermanager.cpp b/aplcore/src/touch/pointermanager.cpp index 29d17c6..a4a3ed9 100644 --- a/aplcore/src/touch/pointermanager.cpp +++ b/aplcore/src/touch/pointermanager.cpp @@ -13,11 +13,12 @@ * permissions and limitations under the License. */ +#include "apl/touch/pointermanager.h" + #include "apl/component/corecomponent.h" #include "apl/component/touchablecomponent.h" #include "apl/engine/rootcontextdata.h" #include "apl/touch/pointer.h" -#include "apl/touch/pointermanager.h" #include "apl/utils/log.h" #include "apl/utils/searchvisitor.h" diff --git a/aplcore/src/touch/utils/autoscroller.cpp b/aplcore/src/touch/utils/autoscroller.cpp index ce9d079..04f2900 100644 --- a/aplcore/src/touch/utils/autoscroller.cpp +++ b/aplcore/src/touch/utils/autoscroller.cpp @@ -14,13 +14,13 @@ */ #include "apl/touch/utils/autoscroller.h" -#include "apl/touch/utils/unidirectionaleasingscroller.h" #include "apl/animation/coreeasing.h" -#include "apl/content/rootconfig.h" #include "apl/component/scrollablecomponent.h" +#include "apl/content/rootconfig.h" #include "apl/primitives/timefunctions.h" #include "apl/time/timemanager.h" +#include "apl/touch/utils/unidirectionaleasingscroller.h" namespace apl { diff --git a/aplcore/src/touch/utils/pagemovehandler.cpp b/aplcore/src/touch/utils/pagemovehandler.cpp index 69797ae..b00be51 100644 --- a/aplcore/src/touch/utils/pagemovehandler.cpp +++ b/aplcore/src/touch/utils/pagemovehandler.cpp @@ -14,7 +14,6 @@ */ #include "apl/touch/utils/pagemovehandler.h" -#include "apl/utils/bimap.h" #include "apl/animation/easing.h" #include "apl/component/component.h" @@ -24,6 +23,7 @@ #include "apl/engine/evaluate.h" #include "apl/primitives/transform.h" #include "apl/time/sequencer.h" +#include "apl/utils/bimap.h" #include "apl/utils/make_unique.h" namespace apl { diff --git a/aplcore/src/utils/stickyfunctions.cpp b/aplcore/src/utils/stickyfunctions.cpp index 841aeac..8e19af1 100644 --- a/aplcore/src/utils/stickyfunctions.cpp +++ b/aplcore/src/utils/stickyfunctions.cpp @@ -14,6 +14,7 @@ */ #include "apl/utils/stickyfunctions.h" + #include "apl/component/corecomponent.h" #include "apl/component/yogaproperties.h" diff --git a/bin/apl-header-inclusion-validation.sh b/bin/apl-header-inclusion-validation.sh index 65882f7..98f413d 100644 --- a/bin/apl-header-inclusion-validation.sh +++ b/bin/apl-header-inclusion-validation.sh @@ -86,6 +86,7 @@ public_apl_headers=( "apl/livedata/livemap.h" "apl/livedata/liveobject.h" "apl/livedata/livedataobjectwatcher.h" + "apl/media/mediatrack.h" "apl/media/mediamanager.h" "apl/media/mediaobject.h" "apl/media/mediaplayer.h" @@ -95,6 +96,7 @@ public_apl_headers=( "apl/primitives/dimension.h" "apl/primitives/filter.h" "apl/primitives/gradient.h" + "apl/primitives/header.h" "apl/primitives/keyboard.h" "apl/primitives/mediasource.h" "apl/primitives/mediastate.h" @@ -120,7 +122,6 @@ public_apl_headers=( "apl/scenegraph/edittextfactory.h" "apl/scenegraph/filter.h" "apl/scenegraph/layer.h" - "apl/scenegraph/modifiednodelist.h" "apl/scenegraph/node.h" "apl/scenegraph/paint.h" "apl/scenegraph/path.h" diff --git a/doc/scenegraph.md b/doc/scenegraph.md index 83ab7e8..d2cdbdf 100644 --- a/doc/scenegraph.md +++ b/doc/scenegraph.md @@ -130,7 +130,7 @@ The basic drawing node has the following properties: * A pointer to the **next modified** drawing node. This is the *dirty* list of nodes that have changed since the last scene graph retrieval. * A set of boolean **flags**. The two supported flags are: - * `kNodeFlagChildrenChanged`: The sibling child list has been modified + * `kNodeFlagChildChanged`: The sibling child list has been modified * `kNodeFlagModified`: Some property of this node has changed. Drawing nodes come in the following types: diff --git a/unit/CMakeLists.txt b/unit/CMakeLists.txt index f5f3c79..b4f619d 100644 --- a/unit/CMakeLists.txt +++ b/unit/CMakeLists.txt @@ -48,12 +48,12 @@ add_subdirectory(livedata) add_subdirectory(media) add_subdirectory(primitives) add_subdirectory(scaling) -if(ENABLE_SCENEGRAPH) - add_subdirectory(scenegraph) -endif(ENABLE_SCENEGRAPH) add_subdirectory(time) add_subdirectory(touch) add_subdirectory(utils) +if(ENABLE_SCENEGRAPH) +add_subdirectory(scenegraph) +endif(ENABLE_SCENEGRAPH) # Add googletest directly to our build. This defines # the gtest and gtest_main targets. diff --git a/unit/animation/unittest_easing_approximation.cpp b/unit/animation/unittest_easing_approximation.cpp index 083606b..ab29863 100644 --- a/unit/animation/unittest_easing_approximation.cpp +++ b/unit/animation/unittest_easing_approximation.cpp @@ -46,7 +46,7 @@ TEST(EasingApproximation, StraightLine) float tout[] = {0.33333}; // b - a float tin[] = {-0.33333}; // c - d - auto approx = EasingApproximation::create(1, start, tout, tin, end, 11); + auto approx = EasingApproximation::getOrCreate(1, start, tout, tin, end, 11); ASSERT_TRUE(approx); ASSERT_EQ(0, approx->getPosition(0, 0)); @@ -69,7 +69,7 @@ TEST(EasingApproximation, OffsetStraightLine) float tout[] = {3}; // b - a float tin[] = {-3}; // c - d - auto approx = EasingApproximation::create(1, start, tout, tin, end, 101); + auto approx = EasingApproximation::getOrCreate(1, start, tout, tin, end, 101); ASSERT_TRUE(approx); ASSERT_EQ(6, approx->getPosition(0, 0)); @@ -92,7 +92,7 @@ TEST(EasingApproximation, Parabola) float tout[] = {0.33333,-1.33333}; // b - a float tin[] = {-0.33333,-1.33333}; // c - d - auto approx = EasingApproximation::create(2, start, tout, tin, end, 101); + auto approx = EasingApproximation::getOrCreate(2, start, tout, tin, end, 101); ASSERT_TRUE(approx); ASSERT_EQ(0, approx->getPosition(0, 0)); // x(0) = 0 @@ -118,3 +118,41 @@ TEST(EasingApproximation, Parabola) Cubic(0, 1, 1,-1./3,-1./3,1)}, approx, 0.02, 0.02)); } + +TEST(EasingApproximation, BuiltInCacheReusesEquivalentObjects) +{ + float start[] = {0,1}; + float end[] = {1,1}; + float tout[] = {0.33333,-1.33333}; + float tin[] = {-0.33333,-1.33333}; + + auto approx = EasingApproximation::getOrCreate(2, start, tout, tin, end, 101); + ASSERT_TRUE(approx); + + auto approxSame = EasingApproximation::getOrCreate(2, start, tout, tin, end, 101); + ASSERT_EQ(approx, approxSame); + + auto approxOther = EasingApproximation::getOrCreate(3, start, tout, tin, end, 101); + ASSERT_NE(approx, approxOther); +} + +TEST(EasingApproximation, BuiltInCacheIsWeak) +{ + float start[] = {0,1}; + float end[] = {1,1}; + float tout[] = {0.33333,-1.33333}; + float tin[] = {-0.33333,-1.33333}; + + std::weak_ptr approxWeak; + { + auto approx = EasingApproximation::getOrCreate(2, start, tout, tin, end, 101); + auto approxSame = EasingApproximation::getOrCreate(2, start, tout, tin, end, 101); + approxWeak = approx; + + ASSERT_FALSE(approxWeak.expired()); + ASSERT_EQ(approxWeak.lock(), approx); + ASSERT_EQ(approxWeak.lock(), approxSame); + } + + ASSERT_TRUE(approxWeak.expired()); +} diff --git a/unit/command/unittest_sequencer_preservation.cpp b/unit/command/unittest_sequencer_preservation.cpp index d012bb8..537d34b 100644 --- a/unit/command/unittest_sequencer_preservation.cpp +++ b/unit/command/unittest_sequencer_preservation.cpp @@ -1110,6 +1110,58 @@ TEST_F(SequencerPreservationTest, ScrollToComponentSequenceNoTargetComponent) // 7 * 100 - 500/2 ASSERT_EQ(Point(0,0), component->scrollPosition()); + // complaint about failed preserve + ASSERT_TRUE(ConsoleMessage()); +} + +static const char *COMMAND_NO_ID = R"apl({ + "type": "APL", + "version": "2022.1", + "theme": "dark", + "onConfigChange": { + "type": "Reinflate", + "preservedSequencers": ["MAGIC"] + }, + "mainTemplate": { + "items": [ + { + "type": "Container", + "items": { + "type": "Frame", + "opacity": 1, + "onMount": { + "sequencer": "MAGIC", + "type": "AnimateItem", + "duration": 1000, + "easing": "linear", + "value": { + "property": "opacity", + "from": 1, + "to": 0 + } + } + } + } + ] + } +})apl"; + +TEST_F(SequencerPreservationTest, AnimateItemNoTargetComponent) +{ + loadDocument(COMMAND_NO_ID); + + auto framy = component->getCoreChildAt(0); + + advanceTime(250); + + ASSERT_EQ(0.75, framy->getCalculated(kPropertyOpacity).asFloat()); + + configChange(ConfigurationChange(1000,1000)); + processReinflate(); + + auto reinflatedFramy = component->getCoreChildAt(0); + ASSERT_EQ(0, reinflatedFramy->getCalculated(kPropertyOpacity).asFloat()); + // complaint about failed preserve ASSERT_TRUE(ConsoleMessage()); } \ No newline at end of file diff --git a/unit/component/CMakeLists.txt b/unit/component/CMakeLists.txt index 10c5719..56d4ab0 100644 --- a/unit/component/CMakeLists.txt +++ b/unit/component/CMakeLists.txt @@ -31,6 +31,7 @@ target_sources_local(unittest unittest_pager.cpp unittest_properties.cpp unittest_scroll.cpp + unittest_selector.cpp unittest_serialize.cpp unittest_signature.cpp unittest_state.cpp diff --git a/unit/component/unittest_edit_text_component.cpp b/unit/component/unittest_edit_text_component.cpp index a09f777..0e0324c 100644 --- a/unit/component/unittest_edit_text_component.cpp +++ b/unit/component/unittest_edit_text_component.cpp @@ -14,6 +14,7 @@ */ #include "../testeventloop.h" + #include "apl/component/edittextcomponent.h" #include "apl/engine/event.h" #include "apl/focus/focusmanager.h" diff --git a/unit/component/unittest_properties.cpp b/unit/component/unittest_properties.cpp index b7586ba..017c227 100644 --- a/unit/component/unittest_properties.cpp +++ b/unit/component/unittest_properties.cpp @@ -108,11 +108,13 @@ TEST_F(PropertyTest, Roles) /** * Verify all of the blending modes */ -TEST_F(PropertyTest, BlendMode) { - ASSERT_TRUE(CheckBimap(sBlendModeBimap, - {"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", - "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", - "saturation", "color", "luminosity"}, - {"colorDodge", "colorBurn", "hardLight", "softLight"}) - ); +TEST_F(PropertyTest, BlendMode) +{ + ASSERT_TRUE( + CheckBimap(sBlendModeBimap, + {"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", + "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", + "saturation", "color", "luminosity", "source-atop", "source-in", "source-out"}, + {"colorDodge", "colorBurn", "hardLight", "softLight", "sourceAtop", "sourceIn", + "sourceOut"})); } diff --git a/unit/component/unittest_selector.cpp b/unit/component/unittest_selector.cpp new file mode 100644 index 0000000..c444a77 --- /dev/null +++ b/unit/component/unittest_selector.cpp @@ -0,0 +1,523 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +#include "../testeventloop.h" +#include "apl/component/selector.h" + +using namespace apl; + +class SelectorTest : public DocumentWrapper {}; + +static const char *BASIC = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Container", + "id": "TOP", + "items": { + "type": "Text", + "id": "TEXT_${index}", + "text": "Item ${index}" + }, + "data": "${Array.range(10)}" + } + } +} +)apl"; + +TEST_F(SelectorTest, Basic) +{ + loadDocument(BASIC); + ASSERT_TRUE(component); + auto child3 = component->getCoreChildAt(3); + auto child6 = component->getCoreChildAt(6); + + // ":root" -> always returns root + ASSERT_EQ(component, Selector::resolve(":root", context, component)); // Start from the root + ASSERT_EQ(component, Selector::resolve(":root", context, child3)); // Start from a child + + // ":source" -> always return the element you start with + ASSERT_EQ(component, Selector::resolve(":source", context, component)); + ASSERT_EQ(child3, Selector::resolve(":source", context, child3)); + + // "TEXT_3" -> findComponentById starting at the current point + ASSERT_EQ(child3, Selector::resolve("TEXT_3",context,component)); + ASSERT_EQ(child3, Selector::resolve("TEXT_3",context,child3)); + ASSERT_EQ(child3, Selector::resolve("TEXT_3",context,child6)); + + // Use the unique ID of one of the components + ASSERT_EQ(child6, Selector::resolve(child6->getUniqueId(), context,component)); + ASSERT_EQ(child6, Selector::resolve(child6->getUniqueId(), context,child3)); + ASSERT_EQ(child6, Selector::resolve(child6->getUniqueId(), context,child6)); + + // Empty selector should not return anything + ASSERT_FALSE(Selector::resolve("", context, component)); + ASSERT_FALSE(Selector::resolve(" ", context, component)); + + // White-space only parse fail. + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(SelectorTest, BasicWithWhitespace) +{ + loadDocument(BASIC); + ASSERT_TRUE(component); + auto child3 = component->getCoreChildAt(3); + auto child6 = component->getCoreChildAt(6); + + // ":root" -> always returns root + std::string s = " :root "; + ASSERT_EQ(component, Selector::resolve(s, context, component)); // Start from the root + ASSERT_EQ(component, Selector::resolve(s, context, child3)); // Start from a child + + // ":source" -> always return the element you start with + s = " :source "; + ASSERT_EQ(component, Selector::resolve(s, context, component)); + ASSERT_EQ(child3, Selector::resolve(s, context, child3)); + + // "TEXT_3" -> findComponentById starting at the current point + s = " TEXT_3 "; + ASSERT_EQ(child3, Selector::resolve(s, context, component)); + ASSERT_EQ(child3, Selector::resolve(s, context, child3)); + ASSERT_EQ(child3, Selector::resolve(s, context, child6)); + + // Use the unique ID of one of the components + s = " " + child6->getUniqueId() + " "; + ASSERT_EQ(child6, Selector::resolve(s, context, component)); + ASSERT_EQ(child6, Selector::resolve(s, context, child3)); + ASSERT_EQ(child6, Selector::resolve(s, context, child6)); +} + +TEST_F(SelectorTest, BasicChildByIndex) +{ + loadDocument(BASIC); + ASSERT_TRUE(component); + auto child3 = component->getCoreChildAt(3); + auto child6 = component->getCoreChildAt(6); + + ASSERT_EQ(child3, Selector::resolve(":root:child(3)", context, component)); + ASSERT_EQ(child3, Selector::resolve(":root:child(3)", context, child3)); + ASSERT_EQ(child3, Selector::resolve(":root:child(3)", context, child6)); + + ASSERT_EQ(child6, Selector::resolve(":root:child(-4)", context, component)); + ASSERT_EQ(child6, Selector::resolve(":root:child(-4)", context, child3)); + ASSERT_EQ(child6, Selector::resolve(":root:child(-4)", context, child6)); + + ASSERT_EQ(child3, Selector::resolve(":child(3)", context, component)); + ASSERT_EQ(child3, Selector::resolve(":child(-7)", context, component)); + ASSERT_EQ(child6, Selector::resolve(":child(6)", context, component)); + ASSERT_EQ(child6, Selector::resolve(":child(-4)", context, component)); + + ASSERT_EQ(nullptr, Selector::resolve(":child(20)", context, component)); + ASSERT_EQ(nullptr, Selector::resolve(":child(-20)", context, component)); +} + +TEST_F(SelectorTest, BasicChildById) +{ + loadDocument(BASIC); + ASSERT_TRUE(component); + auto child3 = component->getCoreChildAt(3); + auto child6 = component->getCoreChildAt(6); + + ASSERT_EQ(child3, Selector::resolve(":root:child(id=TEXT_3)", context, component)); +} + +TEST_F(SelectorTest, BasicChildByRelative) +{ + loadDocument(BASIC); + ASSERT_TRUE(component); + auto child3 = component->getCoreChildAt(3); + auto child4 = component->getCoreChildAt(4); + auto child6 = component->getCoreChildAt(6); + + ASSERT_EQ(child4, Selector::resolve(":next()", context, child3)); + ASSERT_EQ(child3, Selector::resolve(":previous()", context, child4)); + ASSERT_EQ(child6, Selector::resolve(":next(2)", context, child4)); + ASSERT_EQ(child3, Selector::resolve(":previous(3)", context, child6)); +} + +TEST_F(SelectorTest, Missing) +{ + loadDocument(BASIC); + ASSERT_TRUE(component); + + ASSERT_FALSE(Selector::resolve(":child(id=TEXT_99)", context, component)); + ASSERT_FALSE(Selector::resolve(":find(id=TEXT_99)", context, component)); + ASSERT_FALSE(Selector::resolve(":next()", context, component)); + ASSERT_FALSE(Selector::resolve(":previous()", context, component)); + ASSERT_FALSE(Selector::resolve(":parent()", context, component)); +} + +static std::vector BAD_CASES = { + ":", + ":roo", + "fo:oo", + ":previous(color=blue)", + ":parent(typ=Container", + ":parent(type=Container", +}; + +TEST_F(SelectorTest, BadParser) +{ + loadDocument(BASIC); + ASSERT_TRUE(component); + + for (const auto& m : {":", ":roo", "fo:oo", ":previous(color=blue)", ":parent(typ=Container"}) { + auto s = Selector::resolve(m, context); + ASSERT_FALSE(s); + ASSERT_TRUE(ConsoleMessage()); + } +} + + +static const char *ALTERNATE_TEXT_IMAGE = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Container", + "id": "TOP", + "items": [ + { + "when": "${index%2}", + "type": "Text", + "id": "TEXT_${index}", + "text": "Item ${index}" + }, + { + "type": "Image", + "id": "IMAGE_${index}", + "source": "foo" + } + ], + "data": "${Array.range(10)}" + } + } +} +)apl"; + +TEST_F(SelectorTest, ChildByType) +{ + loadDocument(ALTERNATE_TEXT_IMAGE); + ASSERT_TRUE(component); + + ASSERT_EQ(component->getCoreChildAt(0), + Selector::resolve(":root:child(type=Image)", context, component)); + ASSERT_EQ(component->getCoreChildAt(1), + Selector::resolve(":root:child(type=Text)", context, component)); +} + +static const char *DEEP = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Container", + "id": "TOP", + "items": { + "type": "Container", + "id": "BOX_${index}", + "bind": { "name": "X", "value": "${index}" }, + "item": [ + { + "when": "${index%3 == 0}", + "type": "Text", + "id": "TEXT_${index}", + "text": "Item ${X},${index}" + }, + { + "when": "${index%3 == 1}", + "type": "Image", + "id": "IMAGE_${index}", + "source": "${X}/${index}" + }, + { + "type": "Frame", + "id": "FRAME_${index}", + "bind": { "name": "Y", "value": "${index}" }, + "item": { + "type": "Video", + "id": "VIDEO", + "source": "${X}/${Y}" + } + } + ], + "data": "${Array.range(6)}" + }, + "data": "${Array.range(10)}" + } + } +} +)apl"; + +TEST_F(SelectorTest, Deep) +{ + loadDocument(DEEP); + ASSERT_TRUE(component); + + auto t2_3 = component->getCoreChildAt(2)->getCoreChildAt(3); + ASSERT_EQ(t2_3, Selector::resolve(":root:child(2):child(3)", context, component)); + + // Use the parent relative reference and search for the first video + auto t2_2_1 = component->getCoreChildAt(2)->getCoreChildAt(2)->getCoreChildAt(0); + ASSERT_EQ(t2_2_1, Selector::resolve(":source:parent():find(type=Video)", context, t2_3)); + + // The ":source" is optional + ASSERT_EQ(t2_2_1, Selector::resolve(":parent():find(type=Video)", context, t2_3)); + + // If you use the child method, you will fail because it is deeply buried + ASSERT_FALSE(Selector::resolve(":source:parent():child(type=Video)", context, t2_3)); + + // Move around based on type + ASSERT_EQ("IMAGE_4", Selector::resolve(":next(type=Image)", context, t2_3)->getId()); + + // Grandparent + ASSERT_EQ(component, Selector::resolve(":parent(2)", context, t2_3)); +} + +TEST_F(SelectorTest, Parent) +{ + loadDocument(DEEP); + ASSERT_TRUE(component); + + auto container = component->getCoreChildAt(2); + auto frame = container->getCoreChildAt(2); + auto video = frame->getCoreChildAt(0); + + ASSERT_EQ(container, Selector::resolve(":parent()", context, frame)); + ASSERT_EQ(container, Selector::resolve(":parent(1)", context, frame)); + ASSERT_EQ(component, Selector::resolve(":parent(2)", context, frame)); + ASSERT_EQ(nullptr, Selector::resolve(":parent(3)", context, frame)); + ASSERT_EQ(nullptr, Selector::resolve(":parent(212)", context, frame)); + + ASSERT_EQ(frame, Selector::resolve(":parent(1)", context, video)); + ASSERT_EQ(container, Selector::resolve(":parent(2)", context, video)); + ASSERT_EQ(component, Selector::resolve(":parent(3)", context, video)); + ASSERT_EQ(nullptr, Selector::resolve(":parent(4)", context, video)); +} + + +static const char * LAYOUTS = R"apl( +{ + "type": "APL", + "version": "2022.2", + "layouts": { + "Label": { + "parameters": [ + "LABEL", + "COLOR" + ], + "item": { + "type": "Text", + "text": "${LABEL}", + "color": "${COLOR}" + } + }, + "BlueLabel": { + "item": { + "type": "Label", + "COLOR": "blue" + } + }, + "RedLabel": { + "item": { + "type": "Label", + "COLOR": "red" + } + } + }, + "mainTemplate": { + "items": { + "type": "Container", + "items": [ + { + "type": "BlueLabel", + "LABEL": "This is blue" + }, + { + "type": "RedLabel", + "LABEL": "This is red" + } + ] + } + } +} +)apl"; + +TEST_F(SelectorTest, Layouts) +{ + loadDocument(LAYOUTS); + ASSERT_TRUE(component); + + auto blueText = component->getCoreChildAt(0); + auto redText = component->getCoreChildAt(1); + + ASSERT_EQ(blueText, Selector::resolve(":root:find(type=Text)", context)); + ASSERT_EQ(blueText, Selector::resolve(":root:find(type=Label)", context)); + ASSERT_EQ(blueText, Selector::resolve(":root:find(type=BlueLabel)", context)); + ASSERT_EQ(redText, Selector::resolve(":root:find(type=RedLabel)", context)); + + ASSERT_FALSE(Selector::resolve(":root:find(type=Video)", context)); + ASSERT_FALSE(Selector::resolve(":find(type=Label)", context, blueText)); +} + +TEST_F(SelectorTest, Spacing) +{ + loadDocument(LAYOUTS); + ASSERT_TRUE(component); + + auto blueText = component->getCoreChildAt(0); + auto redText = component->getCoreChildAt(1); + + ASSERT_EQ(blueText, Selector::resolve(" :root :find(type=Text) ", context)); + ASSERT_EQ(blueText, Selector::resolve(" :root :find(type=Label) ", context)); + ASSERT_EQ(blueText, Selector::resolve(" :root :find(type=BlueLabel) ", context)); +} + + +::testing::AssertionResult +CheckSendEventComponents(const RootContextPtr& root, + const std::string& label, + const std::map& map) +{ + if (!root->hasEvent()) + return ::testing::AssertionFailure() << "Has no events."; + + auto event = root->popEvent(); + if (event.getType() != kEventTypeSendEvent) + return ::testing::AssertionFailure() << "Event has wrong type:" + << " Expected: SendEvent" + << ", actual: " << sEventTypeBimap.at(event.getType()); + + auto arguments = event.getValue(kEventPropertyArguments); + if (arguments.size() != 1) + return ::testing::AssertionFailure() << "Expected one argument"; + + auto actual_label = event.getValue(apl::kEventPropertyArguments).at(0); + if (!IsEqual(label, actual_label)) + return ::testing::AssertionFailure() << "Mismatched label. Expected=" << label + << " actual=" << actual_label; + + auto actual_components = event.getValue(apl::kEventPropertyComponents); + if (actual_components.size() != map.size()) + return ::testing::AssertionFailure() << "Component map size mismatch. Expected size=" << + map.size() << " actual size=" << actual_components.size(); + + for (const auto& iter : map) { + if (!actual_components.has(iter.first)) + return ::testing::AssertionFailure() + << "Did not find key " << iter.first << " in components map"; + + auto value = actual_components.get(iter.first); + if (!IsEqual(value, iter.second)) + return ::testing::AssertionFailure() << "Component mismatch for key=" << iter.first + << " expected=" << iter.second + << " actual=" << value; + } + + return ::testing::AssertionSuccess(); +} + +static const char * SEND_EVENT = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "items": { + "type": "Container", + "item": { + "type": "TouchWrapper", + "id": "TOUCH_${index}", + "onPress": { + "type": "SendEvent", + "arguments": [ + "INDEX ${index}" + ], + "components": "${data}" + } + }, + "data": [ + ":source", + ":root", + "TOUCH_0", + ":previous(1)", + ":next(1)", + [ + ":source", + ":previous(1)", + "TOUCH_2:previous(2)" + ] + ] + } + } +} +)apl"; + +TEST_F(SelectorTest, SendEvent) { + loadDocument(SEND_EVENT); + ASSERT_TRUE(component); + + std::vector touch; + for (auto i = 0; i < component->getChildCount(); i++) + touch.emplace_back(component->getCoreChildAt(i)); + + // Press the first button + touch[0]->update(apl::kUpdatePressed, 0); + ASSERT_TRUE(CheckSendEventComponents(root, "INDEX 0", {{":source", false}})); + + touch[0]->setState(apl::kStateChecked, true); + touch[0]->update(kUpdatePressed, 0); + ASSERT_TRUE(CheckSendEventComponents(root, "INDEX 0", {{":source", true}})); + + // The second button refers to null + touch[1]->update(apl::kUpdatePressed, 0); + ASSERT_TRUE(CheckSendEventComponents(root, "INDEX 1", {{":root", Object::NULL_OBJECT()}})); + + // The third button refers to the first button + touch[2]->update(apl::kUpdatePressed, 0); + ASSERT_TRUE(CheckSendEventComponents(root, "INDEX 2", {{"TOUCH_0", true}})); + + // The fourth button refers to the previous button + touch[3]->update(apl::kUpdatePressed, 0); + ASSERT_TRUE(CheckSendEventComponents(root, "INDEX 3", {{":previous(1)", false}})); + touch[2]->setState(kStateChecked, true); + touch[3]->update(apl::kUpdatePressed, 0); + ASSERT_TRUE(CheckSendEventComponents(root, "INDEX 3", {{":previous(1)", true}})); + + // The fifth button refers to the next button + touch[4]->update(apl::kUpdatePressed, 0); + ASSERT_TRUE(CheckSendEventComponents(root, "INDEX 4", {{":next(1)", false}})); + touch[5]->setState(kStateChecked, true); + touch[4]->update(apl::kUpdatePressed, 0); + ASSERT_TRUE(CheckSendEventComponents(root, "INDEX 4", {{":next(1)", true}})); + + // The sixth button lists out three buttons (including itself) + touch[5]->update(apl::kUpdatePressed, 0); + ASSERT_TRUE(CheckSendEventComponents( + root, "INDEX 5", + {{":source", true}, {":previous(1)", false}, {"TOUCH_2:previous(2)", true}})); + touch[5]->setState(kStateChecked, false); + touch[4]->setState(kStateChecked, true); + touch[0]->setState(kStateChecked, false); + touch[5]->update(apl::kUpdatePressed, 0); + ASSERT_TRUE(CheckSendEventComponents( + root, "INDEX 5", + {{":source", false}, {":previous(1)", true}, {"TOUCH_2:previous(2)", false}})); +} \ No newline at end of file diff --git a/unit/datagrammar/unittest_grammar.cpp b/unit/datagrammar/unittest_grammar.cpp index 35edb31..d1fb885 100644 --- a/unit/datagrammar/unittest_grammar.cpp +++ b/unit/datagrammar/unittest_grammar.cpp @@ -13,23 +13,23 @@ * permissions and limitations under the License. */ +#include "../testeventloop.h" + #include #include #include #include "gtest/gtest.h" +#include "apl/animation/easing.h" +#include "apl/content/content.h" +#include "apl/content/metrics.h" #include "apl/datagrammar/bytecode.h" +#include "apl/engine/context.h" #include "apl/engine/evaluate.h" -#include "apl/content/metrics.h" -#include "apl/content/content.h" #include "apl/engine/rootcontext.h" -#include "apl/engine/context.h" -#include "apl/primitives/symbolreferencemap.h" - #include "apl/primitives/functions.h" - -#include "../testeventloop.h" +#include "apl/primitives/symbolreferencemap.h" using namespace apl; diff --git a/unit/datasource/unittest_dynamicindexlist.cpp b/unit/datasource/unittest_dynamicindexlist.cpp index d6685ae..ed48955 100644 --- a/unit/datasource/unittest_dynamicindexlist.cpp +++ b/unit/datasource/unittest_dynamicindexlist.cpp @@ -14,6 +14,7 @@ */ #include "../testeventloop.h" + #include "apl/component/pagercomponent.h" /** diff --git a/unit/engine/CMakeLists.txt b/unit/engine/CMakeLists.txt index 558c938..4bb51e4 100644 --- a/unit/engine/CMakeLists.txt +++ b/unit/engine/CMakeLists.txt @@ -31,6 +31,7 @@ target_sources_local(unittest unittest_layouts.cpp unittest_memory.cpp unittest_propdef.cpp + unittest_queue_event_manager.cpp unittest_resources.cpp unittest_styles.cpp unittest_viewhost.cpp diff --git a/unit/engine/unittest_builder.cpp b/unit/engine/unittest_builder.cpp index 993aa8b..8b69237 100644 --- a/unit/engine/unittest_builder.cpp +++ b/unit/engine/unittest_builder.cpp @@ -1654,7 +1654,7 @@ static const char *MEDIA_SOURCE = R"( "entity": [ "Entity." ], "offset": 100 } - }, + }, { "type": "Video", "source": [ "URL1", { "url": "URL2" } ] @@ -1692,6 +1692,7 @@ TEST_F(BuilderTest, MediaSource) auto source = sources.at(0).get(); ASSERT_EQ("", source.getDescription()); ASSERT_EQ(0, source.getDuration()); + ASSERT_EQ(0, source.getTextTracks().size()); ASSERT_EQ("URL1", source.getUrl()); ASSERT_EQ(0, source.getRepeatCount()); ASSERT_TRUE(source.getEntities().empty()); @@ -1702,6 +1703,7 @@ TEST_F(BuilderTest, MediaSource) ASSERT_EQ(1, sources.size()); source = sources.at(0).get(); ASSERT_EQ("Sample video.", source.getDescription()); + ASSERT_EQ(0, source.getTextTracks().size()); ASSERT_EQ(1000, source.getDuration()); ASSERT_EQ("URL1", source.getUrl()); ASSERT_EQ(2, source.getRepeatCount()); @@ -1714,18 +1716,104 @@ TEST_F(BuilderTest, MediaSource) source = sources.at(0).get(); ASSERT_EQ("", source.getDescription()); ASSERT_EQ(0, source.getDuration()); + ASSERT_EQ(0, source.getTextTracks().size()); ASSERT_EQ("URL1", source.getUrl()); ASSERT_EQ(0, source.getRepeatCount()); ASSERT_EQ(0, source.getOffset()); source = sources.at(1).get(); ASSERT_EQ("", source.getDescription()); ASSERT_EQ(0, source.getDuration()); + ASSERT_EQ(0, source.getTextTracks().size()); ASSERT_EQ("URL2", source.getUrl()); ASSERT_EQ(0, source.getRepeatCount()); ASSERT_TRUE(source.getEntities().empty()); ASSERT_EQ(0, source.getOffset()); } +static const char *MEDIA_SOURCE_WITH_TEXTTRACK = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": + { + "type": "Container", + "items": + [ + { + "type": "Video", + "source": + { + "url": "URL1", + "textTrack": [{ "description": "foo", "url": "URL1", "type": "caption" }] + } + }, + { + "type": "Video", + "source": { + "url": "URL1", + "textTrack": [{ "url": "URL1", "type": "caption" }] + } + }, + { + "type": "Video", + "source": { + "url": "URL1", + "textTrack": [{ "url": "URL1", "type": "notcaption" }] + } + } + ] + } + } +})"; + +TEST_F(BuilderTest, MediaSourceWithTextTrack) +{ + loadDocument(MEDIA_SOURCE_WITH_TEXTTRACK); + + ASSERT_EQ(kComponentTypeContainer, component->getType()); + ASSERT_EQ(3, component->getChildCount()); + + auto video0 = component->getCoreChildAt(0); + auto video1 = component->getCoreChildAt(1); + auto video2 = component->getCoreChildAt(2); + + ASSERT_EQ(kComponentTypeVideo, video0->getType()); + ASSERT_EQ(kComponentTypeVideo, video1->getType()); + ASSERT_EQ(kComponentTypeVideo, video2->getType()); + + auto sources = video0->getCalculated(kPropertySource); + ASSERT_TRUE(sources.isArray()); + ASSERT_EQ(1, sources.size()); + auto source = sources.at(0).get(); + auto tracks = source.getTextTracks(); + ASSERT_EQ(1, tracks.size()); + auto track = tracks[0]; + ASSERT_EQ("foo", track.description); + ASSERT_EQ("URL1", track.url); + ASSERT_EQ(kTextTrackTypeCaption, track.type); + + //4 - Testing textTrack with no description. + sources = video1->getCalculated(kPropertySource); + ASSERT_TRUE(sources.isArray()); + ASSERT_EQ(1, sources.size()); + source = sources.at(0).get(); + tracks = source.getTextTracks(); + ASSERT_EQ(1, tracks.size()); + track = tracks[0]; + ASSERT_EQ("", track.description); + ASSERT_EQ("URL1", track.url); + ASSERT_EQ(kTextTrackTypeCaption, track.type); + + //5 - Testing textTrack with invalid type. + ASSERT_TRUE(ConsoleMessage()); + sources = video2->getCalculated(kPropertySource); + ASSERT_TRUE(sources.isArray()); + ASSERT_EQ(1, sources.size()); + source = sources.at(0).get(); + tracks = source.getTextTracks(); + ASSERT_EQ(0, tracks.size()); +} + static const char *MEDIA_SOURCE_2 = R"( { "type": "APL", diff --git a/unit/engine/unittest_builder_bind.cpp b/unit/engine/unittest_builder_bind.cpp index a6125ff..d4d7d8e 100644 --- a/unit/engine/unittest_builder_bind.cpp +++ b/unit/engine/unittest_builder_bind.cpp @@ -255,3 +255,82 @@ TEST_F(BuilderBindTest, BindDuplicatesBind) ASSERT_TRUE(component); ASSERT_TRUE(IsEqual("23", component->getCalculated(kPropertyText).asString())); // The second bind failed } + + +static const char *BIND_NAMING = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Container", + "bind": { "name": "NAME", "value": "VALUE" } + } + } +} +)apl"; + +static std::map GOOD_NAME_TESTS = { + {"_foo", "A"}, + {"__bar__", "B"}, + {"_234", "C"}, + {"a", "D"}, + {"a99_____", "E"}, + {"_", "F"}, +}; + +TEST_F(BuilderBindTest, GoodNameCheck) +{ + for (const auto& it : GOOD_NAME_TESTS) { + auto doc = std::regex_replace(BIND_NAMING, std::regex("NAME"), it.first); + doc = std::regex_replace(doc, std::regex("VALUE"), it.second); + loadDocument(doc.c_str()); + ASSERT_TRUE(component); + auto context = component->getContext(); + ASSERT_TRUE(context->hasLocal(it.first)); + ASSERT_TRUE(IsEqual(it.second, evaluate(*context, "${" + it.first + "}"))); + } +} + +static std::vector BAD_NAME_TESTS = { + "234_foo", + "Ã¥bc", + "abç", + "a-b", + "0", + "", +}; + +TEST_F(BuilderBindTest, BadNameCheck) +{ + for (const auto& m : BAD_NAME_TESTS) { + auto doc = std::regex_replace(BIND_NAMING, std::regex("NAME"), m); + loadDocument(doc.c_str()); + ASSERT_TRUE(component); + auto context = component->getContext(); + ASSERT_FALSE(context->hasLocal(m)); + ASSERT_TRUE(ConsoleMessage()); + } +} + +static const char *MISSING_VALUE = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Container", + "bind": { "name": "NAME" } + } + } +} +)apl"; + +TEST_F(BuilderBindTest, MissingValue) +{ + loadDocument(MISSING_VALUE); + ASSERT_TRUE(component); + auto context = component->getContext(); + ASSERT_FALSE(context->hasLocal("NAME")); + ASSERT_TRUE(ConsoleMessage()); +} \ No newline at end of file diff --git a/unit/engine/unittest_context.cpp b/unit/engine/unittest_context.cpp index 3c85681..5e12298 100644 --- a/unit/engine/unittest_context.cpp +++ b/unit/engine/unittest_context.cpp @@ -49,7 +49,7 @@ TEST_F(ContextTest, Basic) EXPECT_EQ("1.0", env.get("agentVersion").asString()); EXPECT_EQ("normal", env.get("animation").asString()); EXPECT_FALSE(env.get("allowOpenURL").asBoolean()); - EXPECT_EQ("2022.2", env.get("aplVersion").asString()); + EXPECT_EQ("2023.1", env.get("aplVersion").asString()); EXPECT_FALSE(env.get("disallowDialog").asBoolean()); EXPECT_FALSE(env.get("disallowEditText").asBoolean()); EXPECT_FALSE(env.get("disallowVideo").asBoolean()); @@ -414,10 +414,39 @@ static const char *ENVIRONMENT_PAYLOAD = R"apl( TEST_F(ContextTest, EnvironmentPayload) { auto rootConfig = RootConfig(); - auto content = Content::create(ENVIRONMENT_PAYLOAD); //, ); + auto content = Content::create(ENVIRONMENT_PAYLOAD); content->addData("payload", R"({"lang": "en-ES", "layoutDirection": "RTL"})" ); auto root = RootContext::create(Metrics(), content, rootConfig); auto component = root->topComponent(); ASSERT_EQ("Document Lang: en-ES LayoutDirection: RTL", component->getCalculated(kPropertyText).asString()); } + + +static const char *INVALID_ENVIRONMENT_PARAMETER = R"apl( +{ + "type": "APL", + "version": "2022.2", + "environment": { + "parameters": "0_payload" + }, + "mainTemplate": { + "parameters": "0_payload", + "item": { + "type": "Text", + "text": "Document language ${environment.lang}" + } + } +} +)apl"; + +TEST_F(ContextTest, InvalidEnvironmentParameter) +{ + auto rootConfig = RootConfig(); + rootConfig.session(session); + auto content = Content::create(INVALID_ENVIRONMENT_PARAMETER); + content->addData("0_payload", R"({"lang": "en-ES", "layoutDirection": "RTL"})" ); + auto root = RootContext::create(Metrics(), content, rootConfig); + ASSERT_TRUE(root); + ASSERT_TRUE(ConsoleMessage()); +} diff --git a/unit/engine/unittest_dependant.cpp b/unit/engine/unittest_dependant.cpp index 6aea07e..69324a4 100644 --- a/unit/engine/unittest_dependant.cpp +++ b/unit/engine/unittest_dependant.cpp @@ -14,8 +14,9 @@ */ #include "../testeventloop.h" + +#include "apl/component/touchwrappercomponent.h" #include "apl/engine/contextdependant.h" -#include using namespace apl; diff --git a/unit/engine/unittest_hover.cpp b/unit/engine/unittest_hover.cpp index 8e7aff3..77788cb 100644 --- a/unit/engine/unittest_hover.cpp +++ b/unit/engine/unittest_hover.cpp @@ -13,29 +13,18 @@ * permissions and limitations under the License. */ -#include "gtest/gtest.h" - -/** - * The purpose of this unit test is to verify that apl/apl.h includes - * all of the files that a consumer will need in order to use the core - * of APL. - * - * Do NOT add any more include files here!!!! - */ +#include "../testeventloop.h" #include "rapidjson/document.h" #include "apl/apl.h" +#include "apl/component/scrollviewcomponent.h" +#include "apl/component/sequencecomponent.h" #include "apl/engine/evaluate.h" +#include "apl/engine/hovermanager.h" #include "apl/engine/rootcontext.h" #include "apl/engine/styles.h" - #include "apl/primitives/dimension.h" -#include "apl/engine/hovermanager.h" -#include "apl/component/scrollviewcomponent.h" -#include "apl/component/sequencecomponent.h" - -#include "../testeventloop.h" using namespace apl; diff --git a/unit/engine/unittest_keyboard_manager.cpp b/unit/engine/unittest_keyboard_manager.cpp index 1450a27..436f3e1 100644 --- a/unit/engine/unittest_keyboard_manager.cpp +++ b/unit/engine/unittest_keyboard_manager.cpp @@ -13,17 +13,16 @@ * permissions and limitations under the License. */ -#include +#include "../testeventloop.h" -#include "apl/time/sequencer.h" +#include -#include "../testeventloop.h" #include "apl/component/component.h" #include "apl/component/pagercomponent.h" #include "apl/engine/keyboardmanager.h" #include "apl/focus/focusmanager.h" #include "apl/primitives/object.h" -#include "gtest/gtest.h" +#include "apl/time/sequencer.h" using namespace apl; diff --git a/unit/engine/unittest_layouts.cpp b/unit/engine/unittest_layouts.cpp index bf26cb0..c751d6c 100644 --- a/unit/engine/unittest_layouts.cpp +++ b/unit/engine/unittest_layouts.cpp @@ -21,34 +21,37 @@ using namespace apl; -static const char *DATA = "{" - "\"title\": \"Pecan Pie V\"" - "}"; +static const char *DATA = R"apl( +{ + "title": "Pecan Pie V" +} +)apl"; class LayoutTest : public DocumentWrapper {}; -static const char *SIMPLE_LAYOUT = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"layouts\": {" - " \"SimpleLayout\": {" - " \"parameters\": []," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"${payload.title}\"" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"SimpleLayout\"" - " }" - " }" - "}"; +static const char *SIMPLE_LAYOUT = R"apl( +{ + "type": "APL", + "version": "1.0", + "layouts": { + "SimpleLayout": { + "parameters": [], + "items": { + "type": "Text", + "text": "${payload.title}" + } + } + }, + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "SimpleLayout" + } + } +} +)apl"; TEST_F(LayoutTest, Simple) { @@ -72,32 +75,33 @@ TEST_F(LayoutTest, SimpleInfo) } -static const char *PARAMETERIZED = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"layouts\": {" - " \"SimpleLayout\": {" - " \"parameters\": [" - " \"text\"" - " ]," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"${text}\"" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"SimpleLayout\"," - " \"text\": \"${payload.title}\"," - " \"width\": 222" - " }" - " }" - "}"; +static const char *PARAMETERIZED = R"apl( +{ + "type": "APL", + "version": "1.0", + "layouts": { + "SimpleLayout": { + "parameters": [ + "text" + ], + "items": { + "type": "Text", + "text": "${text}" + } + } + }, + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "SimpleLayout", + "text": "${payload.title}", + "width": 222 + } + } +} +)apl"; TEST_F(LayoutTest, Parameterized) @@ -111,44 +115,45 @@ TEST_F(LayoutTest, Parameterized) ASSERT_EQ(Object(Dimension()), map[kPropertyHeight]); } -static const char* PARAMETERIZED_DEFAULT_EVALUATE = - "{\n" - " \"type\": \"APL\",\n" - " \"version\": \"1.1\",\n" - " \"layouts\": {\n" - " \"myLayout\": {\n" - " \"parameters\": [\n" - " {\n" - " \"name\": \"content\",\n" - " \"type\": \"string\",\n" - " \"default\": \"${ordinal}\"\n" - " }\n" - " ],\n" - " \"item\": {\n" - " \"type\": \"Text\",\n" - " \"text\": \"${content}\"\n" - " }\n" - " }\n" - " },\n" - " \"mainTemplate\": {\n" - " \"items\": [\n" - " {\n" - " \"type\": \"Sequence\",\n" - " \"data\": [\n" - " \"One\",\n" - " \"Two\",\n" - " \"Three\"\n" - " ],\n" - " \"numbered\": true,\n" - " \"items\": [\n" - " {\n" - " \"type\": \"myLayout\"\n" - " }\n" - " ]\n" - " }\n" - " ]\n" - " }\n" - "}"; +static const char* PARAMETERIZED_DEFAULT_EVALUATE = R"apl( +{ + "type": "APL", + "version": "1.1", + "layouts": { + "myLayout": { + "parameters": [ + { + "name": "content", + "type": "string", + "default": "${ordinal}" + } + ], + "item": { + "type": "Text", + "text": "${content}" + } + } + }, + "mainTemplate": { + "items": [ + { + "type": "Sequence", + "data": [ + "One", + "Two", + "Three" + ], + "numbered": true, + "items": [ + { + "type": "myLayout" + } + ] + } + ] + } +} +)apl"; TEST_F(LayoutTest, ParameterizedDefaultEvaluate) { @@ -166,50 +171,51 @@ TEST_F(LayoutTest, ParameterizedDefaultEvaluate) } -static const char *DOUBLE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"layouts\": {" - " \"A\": {" - " \"parameters\": [" - " \"text\"" - " ]," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"${text}\"" - " }" - " }," - " \"B\": {" - " \"parameters\": [" - " {" - " \"name\": \"w\"," - " \"default\": 10" - " }," - " {" - " \"name\": \"h\"," - " \"default\": 10" - " }" - " ]," - " \"items\": {" - " \"type\": \"A\"," - " \"height\": \"${h}\"," - " \"width\": \"${w}\"" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"B\"," - " \"h\": 52," - " \"text\": \"${payload.title}\"," - " \"width\": 222" - " }" - " }" - "}"; +static const char *DOUBLE = R"apl( +{ + "type": "APL", + "version": "1.0", + "layouts": { + "A": { + "parameters": [ + "text" + ], + "items": { + "type": "Text", + "text": "${text}" + } + }, + "B": { + "parameters": [ + { + "name": "w", + "default": 10 + }, + { + "name": "h", + "default": 10 + } + ], + "items": { + "type": "A", + "height": "${h}", + "width": "${w}" + } + } + }, + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "B", + "h": 52, + "text": "${payload.title}", + "width": 222 + } + } +} +)apl"; TEST_F(LayoutTest, Double) { @@ -223,68 +229,70 @@ TEST_F(LayoutTest, Double) } -static const char *BASIC_WHEN = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"layouts\": {" - " \"Basic\": {" - " \"parameters\": [" - " \"text\"" - " ]," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"${text}\"" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Basic\"," - " \"when\": false," - " \"text\": \"${payload.title}\"" - " }" - " }" - "}"; +static const char *BASIC_WHEN = R"apl( +{ + "type": "APL", + "version": "1.0", + "layouts": { + "Basic": { + "parameters": [ + "text" + ], + "items": { + "type": "Text", + "text": "${text}" + } + } + }, + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Basic", + "when": false, + "text": "${payload.title}" + } + } +} +)apl"; TEST_F(LayoutTest, BasicWhen) { loadDocumentExpectFailure(BASIC_WHEN, DATA); } -static const char *BASIC_WHEN_NESTED = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"layouts\": {" - " \"Basic\": {" - " \"parameters\": [" - " \"text\"," - " {" - " \"name\": \"inflate\"," - " \"default\": false" - " }" - " ]," - " \"items\": {" - " \"type\": \"Text\"," - " \"when\": \"${inflate}\"," - " \"text\": \"${text}\"" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Basic\"," - " \"text\": \"${payload.title}\"" - " }" - " }" - "}"; +static const char *BASIC_WHEN_NESTED = R"apl( +{ + "type": "APL", + "version": "1.0", + "layouts": { + "Basic": { + "parameters": [ + "text", + { + "name": "inflate", + "default": false + } + ], + "items": { + "type": "Text", + "when": "${inflate}", + "text": "${text}" + } + } + }, + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Basic", + "text": "${payload.title}" + } + } +} +)apl"; TEST_F(LayoutTest, BasicWhenNested) { @@ -292,73 +300,74 @@ TEST_F(LayoutTest, BasicWhenNested) } -static const char *DOUBLE_NESTED = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"layouts\": {" - " \"A\": {" - " \"parameters\": [" - " \"text\"," - " {" - " \"name\": \"inflate\"," - " \"default\": true" - " }" - " ]," - " \"items\": {" - " \"type\": \"Text\"," - " \"when\": \"${inflate}\"," - " \"text\": \"${text}\"" - " }" - " }," - " \"B\": {" - " \"parameters\": [" - " {" - " \"name\": \"doB\"," - " \"default\": true" - " }" - " ]," - " \"items\": {" - " \"type\": \"A\"," - " \"when\": \"${doB}\"" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"B\"," - " \"doB\": true," - " \"inflate\": false," - " \"text\": \"doB=true inflate=false\"" - " }," - " {" - " \"type\": \"B\"," - " \"doB\": false," - " \"inflate\": false," - " \"text\": \"doB=false inflate=false\"" - " }," - " {" - " \"type\": \"B\"," - " \"doB\": true," - " \"inflate\": true," - " \"text\": \"doB=true inflate=true\"" - " }," - " {" - " \"type\": \"B\"," - " \"doB\": true," - " \"inflate\": false," - " \"text\": \"doB=true inflate=false\"" - " }" - " ]" - " }" - " }" - "}"; +static const char *DOUBLE_NESTED = R"apl( +{ + "type": "APL", + "version": "1.0", + "layouts": { + "A": { + "parameters": [ + "text", + { + "name": "inflate", + "default": true + } + ], + "items": { + "type": "Text", + "when": "${inflate}", + "text": "${text}" + } + }, + "B": { + "parameters": [ + { + "name": "doB", + "default": true + } + ], + "items": { + "type": "A", + "when": "${doB}" + } + } + }, + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "items": [ + { + "type": "B", + "doB": true, + "inflate": false, + "text": "doB=true inflate=false" + }, + { + "type": "B", + "doB": false, + "inflate": false, + "text": "doB=false inflate=false" + }, + { + "type": "B", + "doB": true, + "inflate": true, + "text": "doB=true inflate=true" + }, + { + "type": "B", + "doB": true, + "inflate": false, + "text": "doB=true inflate=false" + } + ] + } + } +} +)apl"; TEST_F(LayoutTest, DoubleNested) { @@ -373,43 +382,44 @@ TEST_F(LayoutTest, DoubleNested) ASSERT_EQ("doB=true inflate=true", child->getCalculated(kPropertyText).asString()); } -static const char *EMBEDDED_CONTENT = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"layouts\": {" - " \"contentControl\": {" - " \"parameters\": [" - " {" - " \"name\": \"content\"," - " \"type\": \"component\"" - " }," - " \"backgroundColor\"" - " ]," - " \"item\": {" - " \"type\": \"Frame\"," - " \"backgroundColor\": \"${backgroundColor}\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"item\": \"${content}\"" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"items\": [" - " {" - " \"type\": \"contentControl\"," - " \"width\": \"80vw\"," - " \"height\": \"80vh\"," - " \"backgroundColor\": \"red\"," - " \"content\": {" - " \"type\": \"Text\"," - " \"text\": \"child\"" - " }" - " }" - " ]" - " }" - "}"; +static const char *EMBEDDED_CONTENT = R"apl( +{ + "type": "APL", + "version": "1.1", + "layouts": { + "contentControl": { + "parameters": [ + { + "name": "content", + "type": "component" + }, + "backgroundColor" + ], + "item": { + "type": "Frame", + "backgroundColor": "${backgroundColor}", + "width": "100%", + "height": "100%", + "item": "${content}" + } + } + }, + "mainTemplate": { + "items": [ + { + "type": "contentControl", + "width": "80vw", + "height": "80vh", + "backgroundColor": "red", + "content": { + "type": "Text", + "text": "child" + } + } + ] + } +} +)apl"; TEST_F(LayoutTest, EmbeddedContent) { @@ -424,43 +434,44 @@ TEST_F(LayoutTest, EmbeddedContent) ASSERT_TRUE(IsEqual("child", child->getCalculated(kPropertyText).asString())); } -const char *DIMENSION_PARAMETER_DEFAULT = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"layouts\": {" - " \"MyText\": {" - " \"parameters\": [" - " {" - " \"name\": \"size\"," - " \"description\": \"Size (height and width) of the text. Defaults to 300dp.\"," - " \"type\": \"dimension\"," - " \"default\": \"300dp\"" - " }" - " ]," - " \"item\": {" - " \"type\": \"Text\"," - " \"text\": \"${size/2}\"," - " \"width\": \"${size}\"," - " \"height\": \"${size}\"" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"items\": [" - " {" - " \"type\": \"Container\"," - " \"width\": \"100vw\"," - " \"height\": \"100vh\"," - " \"items\": [" - " {" - " \"type\": \"MyText\"" - " }" - " ]" - " }" - " ]" - " }" - "}"; +static const char *DIMENSION_PARAMETER_DEFAULT = R"apl( +{ + "type": "APL", + "version": "1.1", + "layouts": { + "MyText": { + "parameters": [ + { + "name": "size", + "description": "Size (height and width) of the text. Defaults to 300dp.", + "type": "dimension", + "default": "300dp" + } + ], + "item": { + "type": "Text", + "text": "${size/2}", + "width": "${size}", + "height": "${size}" + } + } + }, + "mainTemplate": { + "items": [ + { + "type": "Container", + "width": "100vw", + "height": "100vh", + "items": [ + { + "type": "MyText" + } + ] + } + ] + } +} +)apl"; TEST_F(LayoutTest, TypedLayoutParameterDefault) { @@ -475,44 +486,45 @@ TEST_F(LayoutTest, TypedLayoutParameterDefault) ASSERT_EQ(Rect(0, 0, 300, 300), text->getCalculated(kPropertyBounds).get()); } -const char *DIMENSION_PARAMETER = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"layouts\": {" - " \"MyText\": {" - " \"parameters\": [" - " {" - " \"name\": \"size\"," - " \"description\": \"Size (height and width) of the text. Defaults to 300dp.\"," - " \"type\": \"dimension\"," - " \"default\": \"300dp\"" - " }" - " ]," - " \"item\": {" - " \"type\": \"Text\"," - " \"text\": \"${size/2}\"," - " \"width\": \"${size}\"," - " \"height\": \"${size}\"" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"items\": [" - " {" - " \"type\": \"Container\"," - " \"width\": \"100vw\"," - " \"height\": \"100vh\"," - " \"items\": [" - " {" - " \"type\": \"MyText\"," - " \"size\": \"200dp\"" - " }" - " ]" - " }" - " ]" - " }" - "}"; +static const char *DIMENSION_PARAMETER = R"apl( +{ + "type": "APL", + "version": "1.1", + "layouts": { + "MyText": { + "parameters": [ + { + "name": "size", + "description": "Size (height and width) of the text. Defaults to 300dp.", + "type": "dimension", + "default": "300dp" + } + ], + "item": { + "type": "Text", + "text": "${size/2}", + "width": "${size}", + "height": "${size}" + } + } + }, + "mainTemplate": { + "items": [ + { + "type": "Container", + "width": "100vw", + "height": "100vh", + "items": [ + { + "type": "MyText", + "size": "200dp" + } + ] + } + ] + } +} +)apl"; TEST_F(LayoutTest, TypedLayoutParameter) { @@ -526,3 +538,45 @@ TEST_F(LayoutTest, TypedLayoutParameter) ASSERT_EQ("100dp", text->getCalculated(kPropertyText).asString()); ASSERT_EQ(Rect(0, 0, 200, 200), text->getCalculated(kPropertyBounds).get()); } + +static const char *PROBLEM_PARAMETERS = R"apl( +{ + "type": "APL", + "version": "2022.2", + "layouts": { + "MyText": { + "parameters": [ + "0_FIRST", + "SECOND" + ], + "item": { + "type": "Text", + "text": "${0_FIRST} ${SECOND}" + } + } + }, + "mainTemplate": { + "items": { + "type": "MyText", + "0_FIRST": "Hello", + "SECOND": "Goodbye" + } + } +} +)apl"; + +TEST_F(LayoutTest, MapParameter) +{ + loadDocument(PROBLEM_PARAMETERS); + ASSERT_TRUE(component); + + auto context = component->getContext(); + ASSERT_FALSE(context->has("0_FIRST")); + ASSERT_TRUE(context->has("SECOND")); + ASSERT_TRUE(IsEqual("Goodbye", context->opt("SECOND"))); + + // Because the first parameter is invalid, the entire string fails to evaluate + ASSERT_TRUE(IsEqual("${0_FIRST} ${SECOND}", + component->getCalculated(apl::kPropertyText).asString())); + ASSERT_TRUE(ConsoleMessage()); +} diff --git a/unit/engine/unittest_queue_event_manager.cpp b/unit/engine/unittest_queue_event_manager.cpp new file mode 100644 index 0000000..f02a5df --- /dev/null +++ b/unit/engine/unittest_queue_event_manager.cpp @@ -0,0 +1,88 @@ +/** +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. + */ + +#include + +#include "gtest/gtest.h" + +#include "apl/engine/queueeventmanager.h" + +using namespace apl; + +class QueueEventManagerTest : public ::testing::Test +{ +protected: + QueueEventManager eventManager; +}; + +TEST_F(QueueEventManagerTest, TestPushFrontPopEmpty) +{ + ASSERT_TRUE(eventManager.empty()); + EventBag bag; + bag.emplace(kEventPropertyName, "arbitraryName"); + Event event(kEventTypeSendEvent, std::move(bag)); + eventManager.push(event); + ASSERT_FALSE(eventManager.empty()); + ASSERT_TRUE(event.matches(eventManager.front())); + ASSERT_FALSE(eventManager.empty()); + eventManager.pop(); + ASSERT_TRUE(eventManager.empty()); +} + +TEST_F(QueueEventManagerTest, TestPushFrontPopEmptyConst) +{ + ASSERT_TRUE(eventManager.empty()); + EventBag bag; + bag.emplace(kEventPropertyName, "arbitraryName"); + const Event event(kEventTypeSendEvent, std::move(bag)); + eventManager.push(event); + ASSERT_FALSE(eventManager.empty()); + ASSERT_TRUE(event.matches(eventManager.front())); + ASSERT_FALSE(eventManager.empty()); + eventManager.pop(); + ASSERT_TRUE(eventManager.empty()); +} + +TEST_F(QueueEventManagerTest, TestPushClearEmpty) +{ + ASSERT_TRUE(eventManager.empty()); + EventBag bag; + bag.emplace(kEventPropertyName, "arbitraryName"); + const Event event(kEventTypeSendEvent, std::move(bag)); + eventManager.push(event); + eventManager.push(event); + ASSERT_FALSE(eventManager.empty()); + eventManager.clear(); + ASSERT_TRUE(eventManager.empty()); +} + +TEST_F(QueueEventManagerTest, TestFIFO) +{ + ASSERT_TRUE(eventManager.empty()); + EventBag firstBag; + firstBag.emplace(kEventPropertyName, "arbitraryName"); + const Event first(kEventTypeSendEvent, std::move(firstBag)); + EventBag secondBag; + firstBag.emplace(kEventPropertyName, "differentArbitraryName"); + const Event second(kEventTypeSendEvent, std::move(secondBag)); + eventManager.push(first); + eventManager.push(second); + + ASSERT_TRUE(first.matches(eventManager.front())); + eventManager.pop(); + ASSERT_TRUE(second.matches(eventManager.front())); + eventManager.pop(); + ASSERT_TRUE(eventManager.empty()); +} \ No newline at end of file diff --git a/unit/engine/unittest_resources.cpp b/unit/engine/unittest_resources.cpp index 9a98fc2..3a08434 100644 --- a/unit/engine/unittest_resources.cpp +++ b/unit/engine/unittest_resources.cpp @@ -15,6 +15,8 @@ #include "../testeventloop.h" +#include "apl/animation/easing.h" + using namespace apl; class ResourceTest : public DocumentWrapper {}; diff --git a/unit/extension/unittest_extension_mediator.cpp b/unit/extension/unittest_extension_mediator.cpp index b6da172..29c5d18 100644 --- a/unit/extension/unittest_extension_mediator.cpp +++ b/unit/extension/unittest_extension_mediator.cpp @@ -232,7 +232,7 @@ class TestExtension final : public alexaext::ExtensionBase { auto flags = RegistrationRequest::FLAGS().Get(registerRequest); if (flags->IsString()) - mFlags = flags->GetString(); + mType = flags->GetString(); auto settings = RegistrationRequest::SETTINGS().Get(registerRequest); if (settings->IsObject()) { auto find = settings->FindMember("authorizationCode"); @@ -285,7 +285,7 @@ class TestExtension final : public alexaext::ExtensionBase { int lastCommandId; std::string lastCommandName; bool registered = false; - std::string mFlags; + std::string mType; std::string mAuthorizationCode; ResourceHolderPtr mResource; }; @@ -856,7 +856,7 @@ TEST_F(ExtensionMediatorTest, RegistrationFlags) { auto hello = testExtensions["aplext:hello:10"].lock(); ASSERT_TRUE(hello); - ASSERT_EQ("--hello", hello->mFlags); + ASSERT_EQ("--hello", hello->mType); } /** diff --git a/unit/graphic/unittest_graphic_bind.cpp b/unit/graphic/unittest_graphic_bind.cpp index 73988fd..eed10ca 100644 --- a/unit/graphic/unittest_graphic_bind.cpp +++ b/unit/graphic/unittest_graphic_bind.cpp @@ -192,3 +192,118 @@ TEST_F(GraphicBindTest, Nested) first->getValue(kGraphicPropertyFill).getColor())); } } + + +static const char *BIND_NAMING = R"apl( +{ + "type": "APL", + "version": "2022.2", + "graphics": { + "BOX": { + "type": "AVG", + "version": "1.2", + "width": 100, + "height": 100, + "items": { + "type": "text", + "bind": { "name": "NAME", "value": "VALUE" }, + "text": "${NAME}" + } + } + }, + "mainTemplate": { + "item": { + "type": "VectorGraphic", + "source": "BOX" + } + } +} +)apl"; + +static std::map GOOD_NAME_TESTS = { + {"_foo", "A"}, + {"__bar__", "B"}, + {"_234", "C"}, + {"a", "D"}, + {"a99_____", "E"}, + {"_", "F"}, +}; + +TEST_F(GraphicBindTest, GoodNameCheck) +{ + for (const auto& it : GOOD_NAME_TESTS) { + auto doc = std::regex_replace(BIND_NAMING, std::regex("NAME"), it.first); + doc = std::regex_replace(doc, std::regex("VALUE"), it.second); + loadDocument(doc.c_str()); + ASSERT_TRUE(component); + + auto graphic = component->getCalculated(apl::kPropertyGraphic).get(); + auto container = graphic->getRoot(); + ASSERT_EQ(kGraphicElementTypeContainer, container->getType()); + ASSERT_EQ(1, container->getChildCount()); + auto text = container->getChildAt(0); + ASSERT_TRUE(IsEqual(it.second, text->getValue(apl::kGraphicPropertyText).asString())); + } +} + +static std::map BAD_NAME_TESTS = { + { "234_foo", "${234_foo}" }, + { "Ã¥bc", "${Ã¥bc}" }, + { "abç", "${abç}" }, + { "a-b", "nan" }, + { "0", "0" }, + { "", "" }, +}; + +TEST_F(GraphicBindTest, BadNameCheck) +{ + for (const auto& it : BAD_NAME_TESTS) { + auto doc = std::regex_replace(BIND_NAMING, std::regex("NAME"), it.first); + loadDocument(doc.c_str()); + auto graphic = component->getCalculated(apl::kPropertyGraphic).get(); + auto container = graphic->getRoot(); + ASSERT_EQ(kGraphicElementTypeContainer, container->getType()); + ASSERT_EQ(1, container->getChildCount()); + auto text = container->getChildAt(0); + ASSERT_TRUE(IsEqual(it.second, text->getValue(apl::kGraphicPropertyText))); + ASSERT_TRUE(ConsoleMessage()); + } +} + +static const char *MISSING_VALUE = R"apl( +{ + "type": "APL", + "version": "2022.2", + "graphics": { + "BOX": { + "type": "AVG", + "version": "1.2", + "width": 100, + "height": 100, + "items": { + "type": "text", + "bind": { "name": "NAME" }, + "text": "${NAME}" + } + } + }, + "mainTemplate": { + "item": { + "type": "VectorGraphic", + "source": "BOX" + } + } +} +)apl"; + +TEST_F(GraphicBindTest, MissingValue) +{ + loadDocument(MISSING_VALUE); + auto graphic = component->getCalculated(apl::kPropertyGraphic).get(); + auto container = graphic->getRoot(); + ASSERT_EQ(kGraphicElementTypeContainer, container->getType()); + ASSERT_EQ(1, container->getChildCount()); + auto text = container->getChildAt(0); + ASSERT_TRUE(IsEqual("", text->getValue(apl::kGraphicPropertyText))); + ASSERT_TRUE(ConsoleMessage()); +} \ No newline at end of file diff --git a/unit/graphic/unittest_graphic_data.cpp b/unit/graphic/unittest_graphic_data.cpp index 889aa48..c7f40a5 100644 --- a/unit/graphic/unittest_graphic_data.cpp +++ b/unit/graphic/unittest_graphic_data.cpp @@ -316,3 +316,80 @@ TEST_F(GraphicDataTest, TestVersion) // The ${index} value is not bound. Note that the StyledText code will strip the trailing space ASSERT_TRUE(IsEqual("Item", box->getChildAt(0)->getValue(kGraphicPropertyText))); } + +static const char *BIND_NAMING = R"apl( +{ + "type": "APL", + "version": "2022.2", + "graphics": { + "BOX": { + "type": "AVG", + "version": "1.2", + "width": 100, + "height": 100, + "parameters": [ { "name": "NAME", "default": "XYZZY" } ], + "items": { + "type": "text", + "text": "${NAME}" + } + } + }, + "mainTemplate": { + "item": { + "type": "VectorGraphic", + "source": "BOX", + "NAME": "VALUE" + } + } +} +)apl"; + +static std::map GOOD_NAME_TESTS = { + {"_foo", "A"}, + {"__bar__", "B"}, + {"_234", "C"}, + {"a", "D"}, + {"a99_____", "E"}, + {"_", "F"}, +}; + +TEST_F(GraphicDataTest, GoodNameCheck) +{ + for (const auto& it : GOOD_NAME_TESTS) { + auto doc = std::regex_replace(BIND_NAMING, std::regex("NAME"), it.first); + doc = std::regex_replace(doc, std::regex("VALUE"), it.second); + loadDocument(doc.c_str()); + ASSERT_TRUE(component); + + auto graphic = component->getCalculated(apl::kPropertyGraphic).get(); + auto container = graphic->getRoot(); + ASSERT_EQ(kGraphicElementTypeContainer, container->getType()); + ASSERT_EQ(1, container->getChildCount()); + auto text = container->getChildAt(0); + ASSERT_TRUE(IsEqual(it.second, text->getValue(apl::kGraphicPropertyText).asString())); + } +} + +static std::map BAD_NAME_TESTS = { + { "234_foo", "${234_foo}" }, + { "Ã¥bc", "${Ã¥bc}" }, + { "abç", "${abç}" }, + { "a-b", "nan" }, + { "0", "0" }, + { "", "" }, +}; + +TEST_F(GraphicDataTest, BadNameCheck) +{ + for (const auto& it : BAD_NAME_TESTS) { + auto doc = std::regex_replace(BIND_NAMING, std::regex("NAME"), it.first); + loadDocument(doc.c_str()); + auto graphic = component->getCalculated(apl::kPropertyGraphic).get(); + auto container = graphic->getRoot(); + ASSERT_EQ(kGraphicElementTypeContainer, container->getType()); + ASSERT_EQ(1, container->getChildCount()); + auto text = container->getChildAt(0); + ASSERT_TRUE(IsEqual(it.second, text->getValue(apl::kGraphicPropertyText))); + ASSERT_TRUE(ConsoleMessage()); + } +} diff --git a/unit/media/CMakeLists.txt b/unit/media/CMakeLists.txt index e8f29e7..123aca8 100644 --- a/unit/media/CMakeLists.txt +++ b/unit/media/CMakeLists.txt @@ -19,4 +19,5 @@ target_sources_local(unittest unittest_media_manager.cpp unittest_media_player.cpp unittest_media_preservation.cpp + unittest_media_utils.cpp ) \ No newline at end of file diff --git a/unit/media/unittest_fake_player.cpp b/unit/media/unittest_fake_player.cpp index b569725..f02b311 100644 --- a/unit/media/unittest_fake_player.cpp +++ b/unit/media/unittest_fake_player.cpp @@ -39,7 +39,7 @@ CheckAdvance( FakePlayer::FakeEvent event, int advanceTime, std::pairmediaLoadFailed("original", 2, "Invalid image"); + + ASSERT_TRUE(MediaRequested(kEventMediaTypeImage, "fallback")); + advanceTime(100); +} diff --git a/unit/media/unittest_media_utils.cpp b/unit/media/unittest_media_utils.cpp new file mode 100644 index 0000000..719df31 --- /dev/null +++ b/unit/media/unittest_media_utils.cpp @@ -0,0 +1,101 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "../testeventloop.h" + +#include "apl/media/mediatrack.h" +#include "apl/primitives/mediasource.h" +#include "apl/media/mediautils.h" + +using namespace apl; + +TEST(MediaUtilsTest, CopiesTextTracks) +{ + auto context = Context::createTestContext(Metrics(), makeDefaultSession()); + + JsonData json(R"({"url":"URL", "textTrack": [{ "url": "URL", "type": "caption", "description": "foobar" }]})"); + const auto mediaSourceObject = MediaSource::create(*context, json.get()); + ASSERT_TRUE(mediaSourceObject.is()); + const auto& ms = mediaSourceObject.get(); + + auto mediaSources = Object(std::vector({ mediaSourceObject })); + + std::vector mediaTracks = mediaSourcesToTracks(mediaSources); + + ASSERT_EQ(1, mediaTracks.size()); + MediaTrack mediaTrack = mediaTracks[0]; + + ASSERT_EQ(1, mediaTrack.textTracks.size()); + ASSERT_EQ(1, ms.getTextTracks().size()); + ASSERT_EQ(ms.getTextTracks()[0].type, mediaTrack.textTracks[0].type); + ASSERT_EQ(ms.getTextTracks()[0].url, mediaTrack.textTracks[0].url); + ASSERT_EQ(ms.getTextTracks()[0].description, mediaTrack.textTracks[0].description); + + ASSERT_EQ(ms.getUrl(), mediaTrack.url); + ASSERT_EQ(ms.getOffset(), mediaTrack.offset); + ASSERT_EQ(ms.getDuration(), mediaTrack.duration); + ASSERT_EQ(ms.getRepeatCount(), mediaTrack.repeatCount); + ASSERT_EQ(ms.getHeaders(), mediaTrack.headers); +} + +TEST(MediaUtilsTest, CopiesDefault) +{ + auto context = Context::createTestContext(Metrics(), makeDefaultSession()); + + JsonData json(R"("URL")"); + const auto mediaSourceObject = MediaSource::create(*context, json.get()); + ASSERT_TRUE(mediaSourceObject.is()); + const auto& ms = mediaSourceObject.get(); + + auto mediaSources = Object(std::vector({ mediaSourceObject })); + + std::vector mediaTracks = mediaSourcesToTracks(mediaSources); + + ASSERT_EQ(1, mediaTracks.size()); + MediaTrack mediaTrack = mediaTracks[0]; + + ASSERT_EQ(0, mediaTrack.textTracks.size()); + ASSERT_EQ(0, ms.getTextTracks().size()); + + ASSERT_EQ(ms.getUrl(), mediaTrack.url); + ASSERT_EQ(ms.getOffset(), mediaTrack.offset); + ASSERT_EQ(ms.getDuration(), mediaTrack.duration); + ASSERT_EQ(ms.getRepeatCount(), mediaTrack.repeatCount); + ASSERT_EQ(ms.getHeaders(), mediaTrack.headers); +} + +TEST(MediaUtilsTest, NotArray) +{ + auto context = Context::createTestContext(Metrics(), makeDefaultSession()); + + JsonData json(R"("URL")"); + const auto mediaSourceObject = MediaSource::create(*context, json.get()); + ASSERT_TRUE(mediaSourceObject.is()); + + auto mediaSources = Object(std::vector({ mediaSourceObject })); + + std::vector mediaTracks = mediaSourcesToTracks(mediaSourceObject); + + ASSERT_EQ(0, mediaTracks.size()); +} + +TEST(MediaUtilsTest, NotMediaSource) +{ + auto objectArray = Object(std::vector({ Object::NULL_OBJECT() })); + + std::vector mediaTracks = mediaSourcesToTracks(objectArray); + + ASSERT_EQ(0, mediaTracks.size()); +} \ No newline at end of file diff --git a/unit/primitives/unittest_transform.cpp b/unit/primitives/unittest_transform.cpp index 8878353..a592e3f 100644 --- a/unit/primitives/unittest_transform.cpp +++ b/unit/primitives/unittest_transform.cpp @@ -13,13 +13,15 @@ * permissions and limitations under the License. */ -#include "gtest/gtest.h" - #include "../testeventloop.h" #include #include +#include "gtest/gtest.h" + +#include "apl/primitives/transform.h" + using namespace apl; const double EPSILON = 0.00001; diff --git a/unit/scenegraph/CMakeLists.txt b/unit/scenegraph/CMakeLists.txt index da7d6dd..ca62468 100644 --- a/unit/scenegraph/CMakeLists.txt +++ b/unit/scenegraph/CMakeLists.txt @@ -22,6 +22,7 @@ target_sources_local(unittest unittest_sg_frame.cpp unittest_sg_graphic.cpp unittest_sg_graphic_component.cpp + unittest_sg_graphic_layers.cpp unittest_sg_graphic_loading.cpp unittest_sg_image.cpp unittest_sg_layer.cpp diff --git a/unit/scenegraph/test_sg.cpp b/unit/scenegraph/test_sg.cpp index cebfa43..1b2773a 100644 --- a/unit/scenegraph/test_sg.cpp +++ b/unit/scenegraph/test_sg.cpp @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -#include "gtest/gtest.h" #include "test_sg.h" +#include "../test_comparisons.h" #include "apl/media/mediaobject.h" #include "apl/scenegraph/edittextconfig.h" @@ -22,8 +22,6 @@ #include "apl/scenegraph/textchunk.h" #include "apl/scenegraph/textproperties.h" - - /** * Template to convert an array of objects into a single string, where each object * is converted into a string using the "convert" method. This is useful for printing @@ -743,14 +741,18 @@ IsLayer::operator()(sg::LayerPtr layer) { SGASSERT(CheckNotNull(layer, "Missing layer"), mMsg); - SGASSERT(CompareDebug(layer->getBounds(), mBounds, "Layer Bounds"), mMsg); + SGASSERT(IsEqual(layer->getBounds(), mBounds) << " Layer Bounds", mMsg); SGASSERT(CompareVisible(layer->getShadow(), mShadowTest, "Layer Shadow"), mMsg); SGASSERT(CompareOptional(layer->getOutline(), mOutlineTest, "Layer Outline"), mMsg); SGASSERT(CompareOptional(layer->getChildClip(), mChildClipTest, "Layer ChildClip"), mMsg); - SGASSERT(CompareBasic(layer->getOpacity(), mOpacity, "Layer Opacity"), mMsg); - SGASSERT(CompareDebug(layer->getTransform(), mTransform, "Layer Transform"), mMsg); + SGASSERT(IsEqual(layer->getOpacity(), mOpacity) << " Layer Opacity", mMsg); + SGASSERT(IsEqual(layer->getTransform(), mTransform) << " Layer Transform", mMsg); SGASSERT(CompareOptional(layer->getAccessibility(), mAccessibilityTest, "Layer Accessibility"), mMsg); - SGASSERT(CompareDebug(layer->getChildOffset(), mChildOffset, "Layer Child Offset"), mMsg); + if (!layer->children().empty()) + SGASSERT(IsEqual(layer->getChildOffset(), mChildOffset) << " Layer Child Offset", mMsg); + if (layer->content()) + SGASSERT(IsEqual(layer->getContentOffset(), mContentOffset) << " Layer Content Offset", + mMsg); SGASSERT(CompareBasic(layer->getInteraction(), mInteraction, "Interaction"), mMsg); SGASSERT(CheckNode(layer->content(), mContentTest), std::string("Layer Content") + mMsg); SGASSERT(CompareDebug(layer->children(), mLayerTests, "Layer Children"), mMsg); @@ -860,7 +862,6 @@ IsSolidFilter( PaintTest paintTest, const std::string& msg) ::testing::AssertionResult CheckSceneGraph(const sg::SceneGraphPtr& sg, const LayerTest& layerTest) { - sg->updates().mapCreated([](const sg::LayerPtr& layer) { layer->clearFlags(); }); return layerTest(sg->getLayer()); } @@ -875,6 +876,15 @@ CheckSceneGraph(sg::SceneGraphUpdates& updates, const sg::NodePtr& node, const N return result; } +::testing::AssertionResult +CheckSceneGraph(sg::SceneGraphUpdates& updates, const sg::LayerPtr& layer, const LayerTest& layerTest) +{ + auto result = layerTest(layer); + if (!result) + return result; + updates.clear(); + return result; +} void DumpSceneGraph(const sg::PaintPtr& ptr, int inset) // NOLINT(misc-no-recursion) @@ -987,6 +997,10 @@ DumpSceneGraph(const sg::LayerPtr& ptr, int inset) // NOLINT(misc-no-recursi if (!flags.empty()) std::cout << p << "Dirty flags " << flags << std::endl; + auto fixed = ptr->debugCharacteristicString(); + if (!fixed.empty()) + std::cout << p << "Fixed flags " << fixed << std::endl; + if (ptr->getOutline()) std::cout << p << "Outline " << ptr->getOutline()->toDebugString() << std::endl; @@ -1008,12 +1022,15 @@ DumpSceneGraph(const sg::LayerPtr& ptr, int inset) // NOLINT(misc-no-recursi if (ptr->getInteraction()) std::cout << p << "Interaction: " << ptr->debugInteractionString() << std::endl; - if (!ptr->getChildOffset().empty()) - std::cout << p << "ChildTransform " << ptr->getChildOffset().toString() << std::endl; - if (ptr->getShadow()) std::cout << p << "Shadow " << ptr->getShadow()->toDebugString() << std::endl; + if (!ptr->getChildOffset().empty()) + std::cout << p << "ChildOffset " << ptr->getChildOffset().toString() << std::endl; + + if (!ptr->getContentOffset().empty()) + std::cout << p << "ContentOffset " << ptr->getContentOffset().toString() << std::endl; + if (ptr->content()) { std::cout << p << "Content" << std::endl; DumpSceneGraph(ptr->content(), inset + 4); @@ -1026,6 +1043,14 @@ DumpSceneGraph(const sg::LayerPtr& ptr, int inset) // NOLINT(misc-no-recursi } } +void +DumpSceneGraph(const sg::GraphicFragmentPtr& fragment, int inset) +{ + if (fragment->layer()) + DumpSceneGraph(fragment->layer(), inset); + else if (fragment->node()) + DumpSceneGraph(fragment->node(), inset); +} void DumpSceneGraph(const sg::FilterPtr& ptr, int inset) // NOLINT(misc-no-recursion) diff --git a/unit/scenegraph/test_sg.h b/unit/scenegraph/test_sg.h index a6222d6..69aabf9 100644 --- a/unit/scenegraph/test_sg.h +++ b/unit/scenegraph/test_sg.h @@ -21,6 +21,7 @@ #include "apl/scenegraph/filter.h" #include "apl/scenegraph/layer.h" #include "apl/scenegraph/node.h" +#include "apl/scenegraph/graphicfragment.h" #include "apl/scenegraph/paint.h" #include "apl/scenegraph/path.h" #include "apl/scenegraph/pathop.h" @@ -155,6 +156,7 @@ class IsTransformNode : public IsWrapper { explicit IsTransformNode(std::string msg="") : IsWrapper(sg::Node::kTransform, std::move(msg)) {} IsTransformNode& transform(Transform2D transform) { mTransform = transform; return *this; } IsTransformNode& translate(Point point) { mTransform = Transform2D::translate(point); return *this; } + IsTransformNode& translate(float x, float y) { return translate({x,y}); } ::testing::AssertionResult operator()(sg::NodePtr node); private: @@ -266,8 +268,11 @@ class IsLayer { IsLayer& shadow(ShadowTest test) { mShadowTest = std::move(test); return *this; } IsLayer& outline(PathTest test) { mOutlineTest = std::move(test); return *this; } IsLayer& childClip(PathTest test) { mChildClipTest = std::move(test); return *this; } - IsLayer& transform(Transform2D transform) { mTransform = std::move(transform); return *this; } - IsLayer& childOffset(Point offset) { mChildOffset = std::move(offset); return *this; } + IsLayer& transform(Transform2D transform) { mTransform = transform; return *this; } + IsLayer& childOffset(Point offset) { mChildOffset = offset; return *this; } + IsLayer& childOffset(float x, float y) { mChildOffset = {x,y}; return *this; } + IsLayer& contentOffset(Point offset) { mContentOffset = offset; return *this; } + IsLayer& contentOffset(float x, float y) { mContentOffset = {x,y}; return *this; } IsLayer& opacity(float opacity) { mOpacity = opacity; return *this; } IsLayer& accessibility(AccessibilityTest test) { mAccessibilityTest = std::move(test); return *this; } @@ -277,11 +282,11 @@ class IsLayer { IsLayer& horizontal() { mInteraction |= sg::Layer::kInteractionScrollHorizontal; return *this; } IsLayer& vertical() { mInteraction |= sg::Layer::kInteractionScrollVertical; return *this; } - IsLayer& content(NodeTest test) { assert(!mContentTest); mContentTest = test; return *this; } + IsLayer& content(NodeTest test) { assert(!mContentTest); mContentTest = std::move(test); return *this; } IsLayer& child(LayerTest test) { mLayerTests.emplace_back(std::move(test)); return *this; } IsLayer& children(std::vector tests) { mLayerTests = std::move(tests); return *this; } - IsLayer& dirty(unsigned flags) { mDirtyFlags = flags; return *this; } + IsLayer& dirty(sg::Layer::FlagType flags) { mDirtyFlags = flags; return *this; } ::testing::AssertionResult operator()(sg::LayerPtr layer); @@ -293,24 +298,29 @@ class IsLayer { PathTest mChildClipTest; Transform2D mTransform; Point mChildOffset; + Point mContentOffset; float mOpacity = 1.0f; NodeTest mContentTest; std::vector mLayerTests; std::string mMsg; - unsigned mDirtyFlags = 0; - unsigned mInteraction = 0; + sg::Layer::FlagType mDirtyFlags = 0; + sg::Layer::InteractionType mInteraction = 0; }; ::testing::AssertionResult CheckSceneGraph(const sg::SceneGraphPtr& sg, const LayerTest& layerTest); ::testing::AssertionResult CheckSceneGraph(sg::SceneGraphUpdates& updates, const sg::NodePtr& node, const NodeTest& nodeTest); +::testing::AssertionResult CheckSceneGraph(sg::SceneGraphUpdates& updates, + const sg::LayerPtr& layer, const LayerTest& layerTest); void DumpSceneGraph(const sg::PaintPtr& ptr, int inset=0); void DumpSceneGraph(const sg::PathOpPtr& ptr, int inset=0); void DumpSceneGraph(const sg::PathPtr& ptr, int inset=0); void DumpSceneGraph(const sg::NodePtr& ptr, int inset=0); void DumpSceneGraph(const sg::FilterPtr& ptr, int inset=0); +void DumpSceneGraph(const sg::LayerPtr& ptr, int inset=0); +void DumpSceneGraph(const sg::GraphicFragmentPtr& fragment, int inset=0); void DumpSceneGraph(const sg::SceneGraphPtr& ptr); // Test classes for text layout @@ -422,8 +432,15 @@ class MyTestLayout : public sg::TextLayout { } Rect getBoundingBoxForLines(Range lineRange) const override { + if (mLines.empty()) + return {}; + + auto range = Range(0, mLines.size() - 1); + if (!lineRange.empty()) + range = range.intersectWith(lineRange); + Rect result; - for (auto line : lineRange) + for (auto line : range) result = result.extend(mLines.at(line).rect); return result; diff --git a/unit/scenegraph/unittest_sg_graphic.cpp b/unit/scenegraph/unittest_sg_graphic.cpp index f86fdd5..466596a 100644 --- a/unit/scenegraph/unittest_sg_graphic.cpp +++ b/unit/scenegraph/unittest_sg_graphic.cpp @@ -38,6 +38,8 @@ class SGGraphicTest : public DocumentWrapper { void TearDown() override { graphic = nullptr; + gc = nullptr; + updates.clear(); DocumentWrapper::TearDown(); } @@ -56,7 +58,7 @@ static const char *BASIC_RECT = R"apl( "items": { "type": "path", "fill": "red", - "pathData": "M0,0 L100,0 L100,100 L0,100 z" + "pathData": "M10,10 L100,10 L100,100 L10,100 z" } } )apl"; @@ -65,13 +67,48 @@ TEST_F(SGGraphicTest, BasicRect) { loadGraphic(BASIC_RECT); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); ASSERT_TRUE(CheckSceneGraph(updates, node, IsDrawNode() - .path(IsGeneralPath("MLLLZ", {0, 0, 100, 0, 100, 100, 0, 100})) + .path(IsGeneralPath("MLLLZ", {10, 10, 100, 10, 100, 100, 10, 100})) .pathOp(IsFillOp(IsColorPaint(Color::RED))))); } +TEST_F(SGGraphicTest, BasicRectLayers) +{ + loadGraphic(BASIC_RECT); + + auto sg = graphic->getSceneGraph(true, updates); + updates.clear(); + ASSERT_TRUE(CheckSceneGraph( + updates, sg->layer(), + IsLayer(Rect(10, 10, 90, 90)) + .contentOffset(Point(10, 10)) + .content(IsDrawNode() + .path(IsGeneralPath("MLLLZ", {10, 10, 100, 10, 100, 100, 10, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))); +} + +static const char *ILLEGAL_GRAPHIC = R"apl( + { + "type": "AVG", + "version": "2.3", + "height": 100, + "width": 100, + "items": { + "type": "path", + "fill": "red", + "pathData": "M10,10 L100,10 L100,100 L10,100 z" + } + } +)apl"; + +TEST_F(SGGraphicTest, IllegalGraphic) +{ + gc = GraphicContent::create(session, ILLEGAL_GRAPHIC); + ASSERT_FALSE(gc); + ASSERT_TRUE(ConsoleMessage()); +} static const char *TWO_RECTS = R"apl( { @@ -97,7 +134,7 @@ static const char *TWO_RECTS = R"apl( TEST_F(SGGraphicTest, TwoRects) { loadGraphic(TWO_RECTS); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); ASSERT_TRUE(CheckSceneGraph( updates, node, IsDrawNode() @@ -106,11 +143,24 @@ TEST_F(SGGraphicTest, TwoRects) .next(IsDrawNode() .path(IsGeneralPath("MLLLZ", {20, 20, 60, 20, 60, 60, 20, 60})) .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))); - - } +TEST_F(SGGraphicTest, TwoRectsLayers) +{ + loadGraphic(TWO_RECTS); + auto sg = graphic->getSceneGraph(true, updates); + updates.clear(); + ASSERT_TRUE(CheckSceneGraph( + updates, sg->layer(), + IsLayer(Rect(0, 0, 100, 100)).content( + IsDrawNode() + .path(IsGeneralPath("MLLLZ", {0, 0, 100, 0, 100, 100, 0, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::RED))) + .next(IsDrawNode() + .path(IsGeneralPath("MLLLZ", {20, 20, 60, 20, 60, 60, 20, 60})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))); +} static const char *COMPLICATED_RECT = R"apl( @@ -160,7 +210,7 @@ static const char *COMPLICATED_RECT = R"apl( TEST_F(SGGraphicTest, ComplicatedRect) { loadGraphic(COMPLICATED_RECT); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); ASSERT_TRUE(CheckSceneGraph( updates, node, @@ -178,6 +228,30 @@ TEST_F(SGGraphicTest, ComplicatedRect) }))); } +TEST_F(SGGraphicTest, ComplicatedRectLayers) +{ + loadGraphic(COMPLICATED_RECT); + auto sg = graphic->getSceneGraph(true, updates); + updates.clear(); + ASSERT_TRUE(CheckSceneGraph( + updates, sg->layer(), + IsLayer(Rect(-1, -1, 102, 102)) + .contentOffset(Point(-1,-1)) + .content( + IsDrawNode() + .path(IsGeneralPath("MLLLZ", {0, 0, 100, 0, 100, 100, 0, 100})) + .pathOp({IsFillOp(IsColorPaint(Color::RED, 0.5)), + IsStrokeOp(IsLinearGradientPaint({0, 1}, {Color::RED, Color::WHITE}, + Gradient::GradientSpreadMethod::PAD, true, + {0, 0}, {1, 1}, 0.25f, + Transform2D::rotate(90.f)), + 2.0f, 10.0f, 100.0f, 1.0f, GraphicLineCap::kGraphicLineCapRound, + GraphicLineJoin::kGraphicLineJoinRound, + {1.0f, 2.0f, 3.0f, 1.0f, 2.0f, 3.0f}) + + })))); +} + static const char *PARAMETERIZED = R"apl( { @@ -206,35 +280,21 @@ static const char *PARAMETERIZED = R"apl( "parameters": [ "color", - { - "name": "opacity", - "default": 1.0 - }, + { "name": "strokeWidth", "default": 1.0 }, + { "name": "opacity", "default": 1.0 }, "transform", - { - "name": "pathLength", - "default": 10.0 - }, - { - "name": "dashArray", - "default": - [] - }, - { - "name": "dashOffset", - "default": 0 - }, + { "name": "pathLength", "default": 10.0 }, + { "name": "dashArray", "default": [] }, + { "name": "dashOffset", "default": 0 }, "lineCap", "lineJoin", - { - "name": "miterLimit", - "default": "5.0" - }, + { "name": "miterLimit", "default": "5.0" }, "path" ], "items": { "type": "path", "stroke": "${color}", + "strokeWidth": "${strokeWidth}", "strokeOpacity": "${opacity}", "strokeTransform": "${transform}", "pathLength": "${pathLength}", @@ -251,13 +311,14 @@ static const char *PARAMETERIZED = R"apl( TEST_F(SGGraphicTest, Parameterized) { loadGraphic(PARAMETERIZED); - auto node = graphic->getSceneGraph(updates); - + auto result = graphic->getSceneGraph(false, updates); + auto node = result->node(); ASSERT_FALSE(node->visible()); graphic->setProperty("color", Color::GREEN); graphic->setProperty("path", "M0,0 L100,100"); graphic->setProperty("opacity", 1.0f); + graphic->setProperty("strokeWidth", 1.0f); graphic->updateSceneGraph(updates); ASSERT_TRUE( @@ -362,6 +423,183 @@ TEST_F(SGGraphicTest, Parameterized) ASSERT_FALSE(node->visible()); } +TEST_F(SGGraphicTest, ParameterizedLayers) +{ + loadGraphic(PARAMETERIZED); + auto result = graphic->getSceneGraph(true, updates); + updates.clear(); + auto layer = result->layer(); + + graphic->setProperty("color", Color::GREEN); + graphic->setProperty("path", "M0,0 L100,100"); + graphic->setProperty("opacity", 1.0f); + + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-2.5f, -2.5f, 105, 105), + "...path") // Miter limit leaves 5 unit padding + .dirty(sg::Layer::kFlagRedrawContent | sg::Layer::kFlagPositionChanged | + sg::Layer::kFlagSizeChanged | sg::Layer::kFlagChildOffsetChanged) + .contentOffset(Point(-2.5f, -2.5f)) + .childOffset(Point(-2.5f, -2.5f)) + .content(IsDrawNode() + .path(IsGeneralPath("ML", {0, 0, 100, 100})) + .pathOp(IsStrokeOp(IsColorPaint(Color::GREEN), 1.0f, 5.0f, + 10.0f, 0.0f, kGraphicLineCapButt, + kGraphicLineJoinMiter, {})))))); + + graphic->setProperty("lineJoin", "round"); // This will change the size of the layer + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .dirty(sg::Layer::kFlagRedrawContent | sg::Layer::kFlagPositionChanged | + sg::Layer::kFlagSizeChanged | sg::Layer::kFlagChildOffsetChanged) + .contentOffset(Point(-0.5f, -0.5f)) + .childOffset(Point(-0.5f, -0.5f)) + .content(IsDrawNode() + .path(IsGeneralPath("ML", {0, 0, 100, 100})) + .pathOp(IsStrokeOp(IsColorPaint(Color::GREEN), 1.0f, 5.0f, + 10.0f, 0.0f, kGraphicLineCapButt, + kGraphicLineJoinRound, {})))))); + + graphic->setProperty("pathLength", 20); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-0.5f, -0.5f)) + .childOffset(Point(-0.5f, -0.5f)) + .content(IsDrawNode() + .path(IsGeneralPath("ML", {0, 0, 100, 100})) + .pathOp(IsStrokeOp(IsColorPaint(Color::GREEN), 1.0f, 5.0f, + 20.0f, 0.0f, kGraphicLineCapButt, + kGraphicLineJoinRound, {})))))); + + graphic->setProperty("dashArray", std::vector(2, 2)); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-0.5f, -0.5f)) + .childOffset(Point(-0.5f, -0.5f)) + .content(IsDrawNode() + .path(IsGeneralPath("ML", {0, 0, 100, 100})) + .pathOp(IsStrokeOp(IsColorPaint(Color::GREEN), 1.0f, 5.0f, + 20.0f, 0.0f, kGraphicLineCapButt, + kGraphicLineJoinRound, {2.0f, 2.0f})))))); + + graphic->setProperty("dashOffset", 1.5f); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-0.5f, -0.5f)) + .childOffset(Point(-0.5f, -0.5f)) + .content(IsDrawNode() + .path(IsGeneralPath("ML", {0, 0, 100, 100})) + .pathOp(IsStrokeOp(IsColorPaint(Color::GREEN), 1.0f, 5.0f, + 20.0f, 1.5f, kGraphicLineCapButt, + kGraphicLineJoinRound, {2.0f, 2.0f})))))); + + graphic->setProperty("lineCap", "square"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-0.5f, -0.5f)) + .childOffset(Point(-0.5f, -0.5f)) + .content(IsDrawNode() + .path(IsGeneralPath("ML", {0, 0, 100, 100})) + .pathOp(IsStrokeOp(IsColorPaint(Color::GREEN), 1.0f, 5.0f, + 20.0f, 1.5f, kGraphicLineCapSquare, + kGraphicLineJoinRound, {2.0f, 2.0f})))))); + + graphic->setProperty("miterLimit", 23.0f); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-0.5f, -0.5f)) + .childOffset(Point(-0.5f, -0.5f)) + .content(IsDrawNode() + .path(IsGeneralPath("ML", {0, 0, 100, 100})) + .pathOp(IsStrokeOp(IsColorPaint(Color::GREEN), 1.0f, 23.0f, + 20.0f, 1.5f, kGraphicLineCapSquare, + kGraphicLineJoinRound, {2.0f, 2.0f})))))); + + graphic->setProperty("opacity", 0.5f); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-0.5f, -0.5f)) + .childOffset(Point(-0.5f, -0.5f)) + .content(IsDrawNode() + .path(IsGeneralPath("ML", {0, 0, 100, 100})) + .pathOp(IsStrokeOp(IsColorPaint(Color::GREEN, 0.5f), 1.0f, + 23.0f, 20.0f, 1.5f, kGraphicLineCapSquare, + kGraphicLineJoinRound, {2.0f, 2.0f})))))); + + // Update the transform - but color paint doesn't use transform, so nothing changes + // However, the draw node is marked as modified because the transform did actually change + graphic->setProperty("transform", "translate(1 2)"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-0.5f, -0.5f)) + .childOffset(Point(-0.5f, -0.5f)) + .content(IsDrawNode() + .path(IsGeneralPath("ML", {0, 0, 100, 100})) + .pathOp(IsStrokeOp(IsColorPaint(Color::GREEN, 0.5f), 1.0f, + 23.0f, 20.0f, 1.5f, kGraphicLineCapSquare, + kGraphicLineJoinRound, {2.0f, 2.0f})))))); + + // Assign a gradient. This will pick up the translate + graphic->setProperty("color", "@FOO"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-0.5f, -0.5f, 101, 101), "...path") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-0.5f, -0.5f)) + .childOffset(Point(-0.5f, -0.5f)) + .content(IsDrawNode() + .path(IsGeneralPath("ML", {0, 0, 100, 100})) + .pathOp(IsStrokeOp( + IsLinearGradientPaint({0, 1}, {Color::RED, Color::WHITE}, + Gradient::GradientSpreadMethod::PAD, + true, {0, 0}, {1, 1}, 0.5f, + Transform2D::translate({1, 2})), + 1.0f, 23.0f, 20.0f, 1.5f, kGraphicLineCapSquare, + kGraphicLineJoinRound, {2.0f, 2.0f})))))); + + // Clear the opacity + graphic->setProperty("opacity", 0); + graphic->updateSceneGraph(updates); + + ASSERT_FALSE(layer->visible()); +} + static const char *BASIC_GROUP = R"apl( { @@ -371,11 +609,18 @@ static const char *BASIC_GROUP = R"apl( "width": 100, "items": { "type": "group", - "item": { - "type": "path", - "fill": "blue", - "pathData": "M0,0 L100,50 L50,100 z" - } + "item": [ + { + "type": "path", + "fill": "blue", + "pathData": "M0,0 L100,50 L50,100 z" + }, + { + "type": "path", + "fill": "red", + "pathData": "M10,10 L110,60 L60,110 z" + } + ] } } )apl"; @@ -384,17 +629,32 @@ static const char *BASIC_GROUP = R"apl( TEST_F(SGGraphicTest, BasicGroup) { loadGraphic(BASIC_GROUP); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); - ASSERT_TRUE(CheckSceneGraph( - updates, node, - IsOpacityNode("...opacity") - .child(IsTransformNode().child( - IsClipNode("...clip") - .path(IsGeneralPath("", {})) - .child(IsDrawNode("...draw") - .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) - .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))))); + ASSERT_TRUE( + CheckSceneGraph(updates, node, + IsDrawNode("...draw") + .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))) + .next(IsDrawNode("...path2") + .path(IsGeneralPath("MLLZ", {10, 10, 110, 60, 60, 110})) + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))); +} + +TEST_F(SGGraphicTest, BasicGroupLayers) +{ + loadGraphic(BASIC_GROUP); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + updates.clear(); + ASSERT_TRUE( + CheckSceneGraph(updates, layer, + IsLayer(Rect(0,0,110,110)).content( + IsDrawNode("...draw") + .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))) + .next(IsDrawNode("...path2") + .path(IsGeneralPath("MLLZ", {10, 10, 110, 60, 60, 110})) + .pathOp(IsFillOp(IsColorPaint(Color::RED))))))); } @@ -421,7 +681,7 @@ static const char *FULL_GROUP = R"apl( TEST_F(SGGraphicTest, FullGroup) { loadGraphic(FULL_GROUP); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); ASSERT_TRUE(CheckSceneGraph( updates, node, @@ -435,6 +695,27 @@ TEST_F(SGGraphicTest, FullGroup) .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))))); } +TEST_F(SGGraphicTest, FullGroupLayer) { + loadGraphic(FULL_GROUP); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + updates.clear(); + + auto p = 50.0f / M_SQRT2; // The length of one-half of the side of the clipping square + + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(-p, p, 2 * p, 2 * p)) + .contentOffset(Point(-p, p)) + .content(IsOpacityNode().opacity(0.5f).child( + IsTransformNode() + .transform(Transform2D::rotate(45)) + .child(IsClipNode() + .path(IsGeneralPath("MLLLZ", {0, 50, 50, 0, 100, 50, 50, 100})) + .child(IsDrawNode() + .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))))); +} + static const char *PARAMETERIZED_GROUP = R"apl( { @@ -468,7 +749,7 @@ static const char *PARAMETERIZED_GROUP = R"apl( TEST_F(SGGraphicTest, ParameterizedGroup) { loadGraphic(PARAMETERIZED_GROUP); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); ASSERT_TRUE( CheckSceneGraph(updates, node, @@ -481,6 +762,7 @@ TEST_F(SGGraphicTest, ParameterizedGroup) graphic->setProperty("clipPath", "M50,0 L100,100 L0,50 z"); graphic->updateSceneGraph(updates); + ASSERT_TRUE( CheckSceneGraph(updates, node, IsOpacityNode().opacity(0.5f).child(IsTransformNode().child( @@ -522,6 +804,67 @@ TEST_F(SGGraphicTest, ParameterizedGroup) ASSERT_FALSE(node->visible()); } +TEST_F(SGGraphicTest, ParameterizedGroupLayouts) +{ + loadGraphic(PARAMETERIZED_GROUP); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + updates.clear(); + + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0,0,0,0), "...container") + .child(IsLayer(Rect(0,0,100,100), "...group") + .opacity(0.5f) // Opacity pulled into the layer + .content(IsDrawNode() + .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))); + + graphic->setProperty("clipPath", "M50,0 L100,100 L0,50 z"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0,0,0,0), "...container") + .child(IsLayer(Rect(0,0,100,100), "...group") + .dirty(sg::Layer::kFlagOutlineChanged) + .opacity(0.5f) // Opacity pulled into the layer + .outline(IsGeneralPath("MLLZ", {50, 0, 100, 100, 0, 50})) + .content(IsDrawNode() + .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))); + + graphic->setProperty("transform", "scale(2)"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0,0,0,0), "...container") + .child(IsLayer(Rect(0,0,100,100), "...group") + .dirty(sg::Layer::kFlagTransformChanged) + .opacity(0.5f) // Opacity pulled into the layer + .outline(IsGeneralPath("MLLZ", {50, 0, 100, 100, 0, 50})) + .transform(Transform2D::scale(2.0f)) + .content(IsDrawNode("..draw") + .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))); + + graphic->setProperty("opacity", 1.0); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0,0,0,0), "...container") + .child(IsLayer(Rect(0,0,100,100), "...group") + .dirty(sg::Layer::kFlagOpacityChanged) + .opacity(1.0f) // Opacity pulled into the layer + .outline(IsGeneralPath("MLLZ", {50, 0, 100, 100, 0, 50})) + .transform(Transform2D::scale(2.0f)) + .content(IsDrawNode("..draw") + .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))); + + graphic->setProperty("opacity", 0.0); + graphic->updateSceneGraph(updates); + ASSERT_FALSE(layer->visible()); +} + static const char *MULTI_CHILD_ONE = R"apl( { @@ -557,16 +900,12 @@ static const char *MULTI_CHILD_ONE = R"apl( TEST_F(SGGraphicTest, MultiChild) { loadGraphic(MULTI_CHILD_ONE); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); - ASSERT_TRUE( - CheckSceneGraph(updates, node, - IsOpacityNode().child(IsTransformNode().child( - IsClipNode() - .path(IsGeneralPath("", {})) - .child(IsDrawNode() - .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) - .pathOp(IsFillOp(IsColorPaint(Color::BLUE, 0.5f)))))))); + ASSERT_TRUE(CheckSceneGraph(updates, node, + IsDrawNode() + .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE, 0.5f))))); graphic->setProperty("opacity", 0.0f); graphic->updateSceneGraph(updates); @@ -574,6 +913,27 @@ TEST_F(SGGraphicTest, MultiChild) ASSERT_FALSE(node->visible()); } +TEST_F(SGGraphicTest, MultiChildLayer) +{ + loadGraphic(MULTI_CHILD_ONE); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + updates.clear(); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0,0,0,0), "...container") + .child( + IsLayer(Rect(0,0,0,0), "...group") + .child(IsLayer(Rect(0,0,100,100), "...path") + .content(IsDrawNode() + .path(IsGeneralPath("MLLZ", {0, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE, 0.5f)))))))); + + graphic->setProperty("opacity", 0.0f); + graphic->updateSceneGraph(updates); + + ASSERT_FALSE(layer->visible()); +} + static const char *BASIC_TEXT = R"apl( { @@ -593,7 +953,7 @@ static const char *BASIC_TEXT = R"apl( TEST_F(SGGraphicTest, BasicText) { loadGraphic(BASIC_TEXT); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); ASSERT_TRUE(CheckSceneGraph( updates, node, @@ -602,6 +962,23 @@ TEST_F(SGGraphicTest, BasicText) .child(IsTextNode().text("Hello, World!").pathOp(IsFillOp(IsColorPaint(Color::RED)))))); } +TEST_F(SGGraphicTest, BasicTextLayer) +{ + loadGraphic(BASIC_TEXT); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + updates.clear(); + ASSERT_TRUE( + CheckSceneGraph(updates, layer, + IsLayer(Rect(0, -8, 130, 10)) + .contentOffset(Point(0, -8)) + .childOffset(Point(0, -8)) + .content(IsTransformNode() + .translate(Point{0, -8}) + .child(IsTextNode() + .text("Hello, World!") + .pathOp(IsFillOp(IsColorPaint(Color::RED))))))); +} + static const char *COMPLICATED_TEXT = R"apl( { @@ -643,7 +1020,7 @@ static const char *COMPLICATED_TEXT = R"apl( TEST_F(SGGraphicTest, ComplicatedText) { loadGraphic(COMPLICATED_TEXT); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); ASSERT_TRUE(CheckSceneGraph( updates, node, @@ -659,6 +1036,31 @@ TEST_F(SGGraphicTest, ComplicatedText) 2.0f))))); } +TEST_F(SGGraphicTest, ComplicatedTextLayer) +{ + loadGraphic(COMPLICATED_TEXT); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + updates.clear(); + // Text width = 15 characters * 10 per character + 2 strokeWidth * 4 miterlimit = 158 + // Text height = 10 + 2 strokeWidth * 4 miterLimit = 18 + // Vertical offset = 0.5 * (2 strokeWidth * 4 miterLimit) + 8 ascender = 12 + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(-4, -12, 158, 18)) + .contentOffset(Point(-4, -12)) + .childOffset(Point(-4, -8)) + .content(IsTransformNode() + .translate(Point{0, -8}) + .child(IsTextNode() + .text("Fill and Stroke") + .pathOp(IsFillOp(IsColorPaint(Color::RED, 0.5))) + .pathOp(IsStrokeOp( + IsLinearGradientPaint({0, 1}, {Color::RED, Color::WHITE}, + Gradient::GradientSpreadMethod::PAD, + true, {0, 0}, {1, 1}, 0.25f, + Transform2D::rotate(90.f)), + 2.0f)))))); +} static const char *PARAMETERIZED_TEXT = R"apl( { @@ -708,7 +1110,7 @@ static const char *PARAMETERIZED_TEXT = R"apl( TEST_F(SGGraphicTest, ParameterizedText) { loadGraphic(PARAMETERIZED_TEXT); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); ASSERT_FALSE(node->visible()); @@ -795,6 +1197,140 @@ TEST_F(SGGraphicTest, ParameterizedText) } +TEST_F(SGGraphicTest, ParameterizedTextLayout) +{ + loadGraphic(PARAMETERIZED_TEXT); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + updates.clear(); + + ASSERT_FALSE(layer->visible()); + + graphic->setProperty("color", Color::GREEN); + graphic->setProperty("text", "Woof!"); + + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(0, -8, 50, 10), "...text") + .dirty(sg::Layer::kFlagRedrawContent | sg::Layer::kFlagPositionChanged | + sg::Layer::kFlagSizeChanged | sg::Layer::kFlagChildOffsetChanged) + .contentOffset(0, -8) + .content(IsTransformNode() + .translate(Point(0, -8)) + .child(IsTextNode(".text").text("Woof!").pathOp( + IsFillOp(IsColorPaint(Color::GREEN)))))))); + + graphic->setProperty("opacity", 0.5f); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(0, -8, 50, 10), "...text") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(0, -8) + .content(IsTransformNode() + .translate(Point(0, -8)) + .child(IsTextNode(".text").text("Woof!").pathOp( + IsFillOp(IsColorPaint(Color::GREEN, 0.5f)))))))); + + graphic->setProperty("color", "@FOO"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(0, -8, 50, 10), "...text") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(0, -8) + .content(IsTransformNode() + .translate(Point(0, -8)) + .child(IsTextNode(".text").text("Woof!").pathOp(IsFillOp( + IsLinearGradientPaint({0, 1}, {Color::RED, Color::WHITE}, + Gradient::GradientSpreadMethod::PAD, + true, {0, 0}, {1, 1}, 0.5f, + Transform2D())))))))); + + graphic->setProperty("transform", "translate(1,2)"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(0, -8, 50, 10), "...text") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(0, -8) + .content(IsTransformNode() + .translate(Point(0, -8)) + .child(IsTextNode(".text").text("Woof!").pathOp(IsFillOp( + IsLinearGradientPaint({0, 1}, {Color::RED, Color::WHITE}, + Gradient::GradientSpreadMethod::PAD, + true, {0, 0}, {1, 1}, 0.5f, + Transform2D::translate(1, 2))))))))); + + graphic->setProperty("text", ""); + graphic->updateSceneGraph(updates); + ASSERT_FALSE(layer->visible()); + + graphic->setProperty("text", "Once upon a time"); + graphic->setProperty("opacity", 0); + graphic->updateSceneGraph(updates); + ASSERT_FALSE(layer->visible()); + + graphic->setProperty("opacity", 1.0f); + graphic->setProperty("color", "blue"); + graphic->setProperty("text", "123"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(0, -8, 30, 10), "...text") + .dirty(sg::Layer::kFlagChildOffsetChanged | sg::Layer::kFlagRedrawContent | + sg::Layer::kFlagSizeChanged | sg::Layer::kFlagPositionChanged) + .contentOffset(0, -8) + .content(IsTransformNode().translate(0, -8).child( + IsTextNode(".text").text("123").pathOp( + IsFillOp(IsColorPaint(Color::BLUE)))))))); + + graphic->setProperty("x", 10); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(10, -8, 30, 10), "...text") + .dirty(sg::Layer::kFlagChildOffsetChanged | sg::Layer::kFlagRedrawContent | + sg::Layer::kFlagPositionChanged) + .contentOffset(10, -8) + .content(IsTransformNode().translate(10, -8).child( + IsTextNode(".text").text("123").pathOp( + IsFillOp(IsColorPaint(Color::BLUE)))))))); + + graphic->setProperty("y", 20); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(10, 12, 30, 10), "...text") + .dirty(sg::Layer::kFlagChildOffsetChanged | sg::Layer::kFlagRedrawContent | + sg::Layer::kFlagPositionChanged) + .contentOffset(10, 12) + .content(IsTransformNode().translate(10, 12).child( + IsTextNode(".text").text("123").pathOp( + IsFillOp(IsColorPaint(Color::BLUE)))))))); + + graphic->setProperty("anchor", "end"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-20, 12, 30, 10), "...text") + .dirty(sg::Layer::kFlagChildOffsetChanged | sg::Layer::kFlagRedrawContent | + sg::Layer::kFlagPositionChanged) + .contentOffset(-20, 12) + .content(IsTransformNode().translate(-20, 12).child( + IsTextNode(".text").text("123").pathOp( + IsFillOp(IsColorPaint(Color::BLUE)))))))); +} + + static const char *PARAMETERIZED_TEXT_STROKE = R"apl( { "type": "AVG", @@ -840,7 +1376,7 @@ static const char *PARAMETERIZED_TEXT_STROKE = R"apl( TEST_F(SGGraphicTest, ParameterizedTextStroke) { loadGraphic(PARAMETERIZED_TEXT_STROKE); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); ASSERT_FALSE(node->visible()); @@ -894,6 +1430,87 @@ TEST_F(SGGraphicTest, ParameterizedTextStroke) ASSERT_FALSE(node->visible()); } +TEST_F(SGGraphicTest, ParameterizedTextStrokeLayouts) +{ + loadGraphic(PARAMETERIZED_TEXT_STROKE); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + updates.clear(); + + ASSERT_FALSE(layer->visible()); + + graphic->setProperty("color", Color::GREEN); + graphic->updateSceneGraph(updates); + ASSERT_TRUE( + CheckSceneGraph(updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-2, -10, 54, 14), "....text") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-2, -10)) + .content(IsTransformNode().translate(0, -8).child( + IsTextNode(".text").text("HELLO").pathOp( + IsStrokeOp(IsColorPaint(Color::GREEN), 1))))))); + + graphic->setProperty("opacity", 0.5f); + graphic->updateSceneGraph(updates); + ASSERT_TRUE( + CheckSceneGraph(updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-2, -10, 54, 14), "....text") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-2, -10)) + .content(IsTransformNode().translate(0, -8).child( + IsTextNode(".text").text("HELLO").pathOp(IsStrokeOp( + IsColorPaint(Color::GREEN, 0.5f), 1))))))); + + graphic->setProperty("color", "@FOO"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-2, -10, 54, 14), "....text") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-2, -10)) + .content(IsTransformNode().translate(0, -8).child( + IsTextNode(".text").text("HELLO").pathOp(IsStrokeOp( + IsLinearGradientPaint({0, 1}, {Color::RED, Color::WHITE}, + Gradient::GradientSpreadMethod::PAD, true, + {0, 0}, {1, 1}, 0.5f, Transform2D()), + 1.0f))))))); + + graphic->setProperty("transform", "translate(1,2)"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child( + IsLayer(Rect(-2, -10, 54, 14), "....text") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-2, -10)) + .content(IsTransformNode().translate(0, -8).child( + IsTextNode(".text").text("HELLO").pathOp(IsStrokeOp( + IsLinearGradientPaint({0, 1}, {Color::RED, Color::WHITE}, + Gradient::GradientSpreadMethod::PAD, true, {0, 0}, + {1, 1}, 0.5f, Transform2D::translate(1, 2)), + 1.0f))))))); + + graphic->setProperty("opacity", 1.0f); + graphic->setProperty("color", "blue"); + graphic->updateSceneGraph(updates); + ASSERT_TRUE( + CheckSceneGraph(updates, layer, + IsLayer(Rect(0, 0, 0, 0), "...container") + .child(IsLayer(Rect(-2, -10, 54, 14), "....text") + .dirty(sg::Layer::kFlagRedrawContent) + .contentOffset(Point(-2, -10)) + .content(IsTransformNode().translate(0, -8).child( + IsTextNode(".text").text("HELLO").pathOp( + IsStrokeOp(IsColorPaint(Color::BLUE), 1))))))); + + graphic->setProperty("swidth", 0); + graphic->updateSceneGraph(updates); + ASSERT_FALSE(layer->visible()); +} + static const char *SHADOW = R"apl( { @@ -926,7 +1543,7 @@ static const char *SHADOW = R"apl( TEST_F(SGGraphicTest, Shadow) { loadGraphic(SHADOW); - auto node = graphic->getSceneGraph(updates); + auto node = graphic->getSceneGraph(false, updates)->node(); ASSERT_TRUE(CheckSceneGraph( updates, node, @@ -949,259 +1566,36 @@ TEST_F(SGGraphicTest, Shadow) .pathOp(IsFillOp(IsColorPaint(Color::RED)))))); } -// Use a custom test for checking the nodes. The regular test_sg.h code skips over operations -// that are not visible. But we want to verify that we have exactly the correct operations. -static ::testing::AssertionResult -checkOps(sg::PathOp *op, bool hasFill, Color fillColor, bool hasStroke, Color strokeColor) { - if (hasFill) { - if (!sg::FillPathOp::is_type(op)) - return ::testing::AssertionFailure() << "Expected a fill operation"; - auto *fill = sg::FillPathOp::cast(op); - auto *paint = sg::ColorPaint::cast(fill->paint); - if (!paint || paint->getColor() != fillColor) - return ::testing::AssertionFailure() << "Fill color mismatch"; - op = op->nextSibling.get(); - } - - if (hasStroke) { - if (!sg::StrokePathOp::is_type(op)) - return ::testing::AssertionFailure() << "Missing stroke operation"; - auto *stroke = sg::StrokePathOp::cast(op); - auto *paint = sg::ColorPaint::cast(stroke->paint); - if (!paint || paint->getColor() != strokeColor) - return ::testing::AssertionFailure() << "Stroke color mismatch"; - op = op->nextSibling.get(); - } - - if (op) - return ::testing::AssertionFailure() << "Extra operation"; - - return ::testing::AssertionSuccess(); -} - -static ::testing::AssertionResult -checkDraw(sg::NodePtr node, float x, bool hasFill, Color fillColor, bool hasStroke, Color strokeColor) { - auto *draw = sg::DrawNode::cast(node); - if (!draw) - return ::testing::AssertionFailure() << "not a draw node"; - - auto *path = sg::GeneralPath::cast(draw->getPath()); - if (!path) - return ::testing::AssertionFailure() << "missing path node"; - - // negative x used to indicate no points - if (path->getPoints() != (x < 0 ? std::vector{} : std::vector{0,0,0,x,x,x})) - return ::testing::AssertionFailure() << "mismatched points"; - - return checkOps(draw->getOp().get(), hasFill, fillColor, hasStroke, strokeColor); -} - -static ::testing::AssertionResult -checkText(sg::NodePtr node, std::string textString, bool hasFill, Color fillColor, bool hasStroke, Color strokeColor) { - auto *text = sg::TextNode::cast(node); - if (!text) - return ::testing::AssertionFailure() << "not a text node"; - - if (text->getTextLayout()->toDebugString() != textString) - return ::testing::AssertionFailure() << "text mismatch"; - - return checkOps(text->getOp().get(), hasFill, fillColor, hasStroke, strokeColor); -} - -static const char *DRAW_OPTIMIZATION = R"apl( -{ - "type": "AVG", - "version": "1.2", - "height": 100, - "width": 100, - "parameters": [ - { - "name": "X", - "default": false - } - ], - "items": [ - { - "type": "path", - "description": "Empty path", - "fill": "red", - "pathData": "M10,10 M20,20" - }, - { - "type": "path", - "description": "Just fill", - "fill": "blue", - "pathData": "M0,0 L0,1 L1,1 z" - }, - { - "type": "path", - "description": "Just stroke", - "stroke": "red", - "pathData": "M0,0 L0,2 L2,2 z" - }, - { - "type": "path", - "description": "Stroke, but no width", - "stroke": "green", - "strokeWidth": 0, - "pathData": "M0,0 L0,3 L3,3 z" - }, - { - "type": "path", - "description": "Stroke and fill", - "stroke": "yellow", - "fill": "black", - "strokeWidth": 5, - "pathData": "M0,0 L0,4 L4,4 z" - }, - { - "type": "path", - "description": "Stroke and fill opacity zero", - "stroke": "pink", - "strokeOpacity": 0, - "fill": "blue", - "fillOpacity": 0, - "strokeWidth": 5, - "pathData": "M0,0 L0,5 L5,5 z" - }, - { - "type": "path", - "description": "Path depends on X", - "pathData": "${X ? 'M0,0 L0,6 L6,6 z' : 'M0,0'}", - "fill": "purple" - }, - { - "type": "path", - "description": "Fill depends on X", - "pathData": "M0,0 L0,7 L7,7 z", - "fill": "${X ? 'blue' : 'transparent'}" - }, - { - "type": "path", - "description": "Stroke depends on X", - "pathData": "M0,0 L0,8 L8,8 z", - "stroke": "${X ? 'red' : 'transparent'}" - } - ] -} -)apl"; - -TEST_F(SGGraphicTest, DrawOptimization) +TEST_F(SGGraphicTest, ShadowLayers) { - loadGraphic(DRAW_OPTIMIZATION); - auto node = graphic->getSceneGraph(updates); - - // Skip the empty path - there is no path data - - // Fill blue - ASSERT_TRUE(checkDraw(node, 1, true, Color::BLUE, false, Color::TRANSPARENT)); - node = node->next(); - - // Stroke red - ASSERT_TRUE(checkDraw(node, 2, false, Color::TRANSPARENT, true, Color::RED)); - node = node->next(); - - // Skip the stroke with green because there is no stroke width - - // Stroke yellow, fill black - ASSERT_TRUE(checkDraw(node, 4, true, Color::BLACK, true, Color::YELLOW)); - node = node->next(); - - // Skip stroke pink, fill blue because the opacities hide all colors - - // Allow the fill purple even though there is no path because the path is mutable - ASSERT_TRUE(checkDraw(node, -1, true, Color::PURPLE, false, Color::TRANSPARENT)); - node = node->next(); - - // Allow the 7th case - the fill color can be changed - ASSERT_TRUE(checkDraw(node, 7, true, Color::TRANSPARENT, false, Color::TRANSPARENT)); - node = node->next(); - - // Allow the 8th case - the stroke color can be changed - ASSERT_TRUE(checkDraw(node, 8, false, Color::TRANSPARENT, true, Color::TRANSPARENT)); - node = node->next(); - - ASSERT_FALSE(node); -} + loadGraphic(SHADOW); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + updates.clear(); + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0,0,0,0), "...container") + .child(IsLayer(Rect(10,10,80,80), "...path") + .shadow(IsShadow(Color::BLUE, Point{3, 3}, 5)) + .contentOffset(Point(10,10)) + .content(IsDrawNode() + .path(IsGeneralPath("MLLLZ", {10, 10, 90, 10, 90, 90, 10, 90})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))); + graphic->setProperty("COLOR", "red"); + graphic->updateSceneGraph(updates); -static const char *TEXT_OPTIMIZATION = R"apl( -{ - "type": "AVG", - "version": "1.2", - "height": 100, - "width": 100, - "parameters": [ - { - "name": "X", - "default": false - } - ], - "items": [ - { - "type": "text", - "text": "Just fill", - "fill": "red" - }, - { - "type": "text", - "text": "Just stroke", - "stroke": "yellow", - "fillOpacity": 0, - "strokeWidth": 1 - }, - { - "type": "text", - "text": "Stroke and fill", - "stroke": "green", - "strokeWidth": 2, - "fill": "blue" - }, - { - "type": "text", - "text": "Nothing to draw", - "fillOpacity": 0 - }, - { - "type": "text", - "text": "", - "fill": "purple" - }, - { - "type": "text", - "text": "Default" - } - ] + // Note: The shadow color does not change despite the data-binding. That is because graphic + // element filters are not dynamic. This may change in the future. + ASSERT_TRUE(CheckSceneGraph( + updates, layer, + IsLayer(Rect(0,0,0,0), "...container") + .child(IsLayer(Rect(10,10,80,80), "...path") + .dirty(sg::Layer::kFlagRedrawContent) + .shadow(IsShadow(Color::BLUE, Point{3, 3}, 5)) + .contentOffset(Point(10,10)) + .content(IsDrawNode() + .path(IsGeneralPath("MLLLZ", {10, 10, 90, 10, 90, 90, 10, 90})) + .pathOp(IsFillOp(IsColorPaint(Color::RED))))))); } -)apl"; - -TEST_F(SGGraphicTest, TextOptimization) -{ - loadGraphic(TEXT_OPTIMIZATION); - auto node = graphic->getSceneGraph(updates); - - // Fill red (well, it's hidden under a transform) - ASSERT_TRUE(checkText(node->child(), "Just fill", true, Color::RED, false, Color::TRANSPARENT)); - node = node->next(); - - // Stroke yellow - ASSERT_TRUE(checkText(node->child(), "Just stroke", false, Color::TRANSPARENT, true, Color::YELLOW)); - node = node->next(); - // Stroke green, fill blue - ASSERT_TRUE(checkText(node->child(), "Stroke and fill", true, Color::BLUE, true, Color::GREEN)); - node = node->next(); - - // Skip the "Nothing to draw" - there is no fill or stroke - - // Fill purple (even though there is no text to draw. Fix this?) - ASSERT_TRUE(checkText(node->child(), "", true, Color::PURPLE, false, Color::TRANSPARENT)); - node = node->next(); - - // Fill with black (the default color) - ASSERT_TRUE(checkText(node->child(), "Default", true, Color::BLACK, false, Color::TRANSPARENT)); - node = node->next(); - - ASSERT_FALSE(node); -} // TODO: Check style changes - they should update properties as appropriate \ No newline at end of file diff --git a/unit/scenegraph/unittest_sg_graphic_component.cpp b/unit/scenegraph/unittest_sg_graphic_component.cpp index 1be9c1e..cfb1fc3 100644 --- a/unit/scenegraph/unittest_sg_graphic_component.cpp +++ b/unit/scenegraph/unittest_sg_graphic_component.cpp @@ -59,7 +59,6 @@ TEST_F(SGGraphicComponentTest, Basic) { loadDocument(BASIC); auto sg = root->getSceneGraph(); - ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") @@ -73,7 +72,6 @@ TEST_F(SGGraphicComponentTest, Basic) executeCommand("SetValue", {{"componentId", "VG"}, {"property", "align"}, {"value", "top-right"}}, true); sg = root->getSceneGraph(); - ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") .child(IsLayer(Rect{100, 0, 100, 100}, ".MediaLayer") @@ -112,6 +110,103 @@ TEST_F(SGGraphicComponentTest, Missing) .child(IsLayer(Rect{0, 0, 1, 1}, ".MediaLayer")))); // No content is visible } +static const char * TOGGLE_OFF_AND_ON = R"apl( + { + "type": "APL", + "version": "1.6", + "graphics": { + "Diamond": { + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "items": { + "type": "path", + "fill": "green", + "pathData": "M0,50 L50,0 L100,50 L50,100 z" + } + }, + "Bad": { + "type": "AVG", + "version": "1.2", + "height": 0, + "width": 0 + }, + "Empty": { + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100 + } + }, + "mainTemplate": { + "items": { + "type": "VectorGraphic", + "id": "VG", + "width": 200, + "height": 200, + "source": "Diamond" + } + } + } +)apl"; + +TEST_F(SGGraphicComponentTest, ToggleOffAndOn) +{ + loadDocument(TOGGLE_OFF_AND_ON); + auto sg = root->getSceneGraph(); + + ASSERT_TRUE(CheckSceneGraph( + sg, + IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") + .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer") + .content(IsTransformNode(".transform") + .child(IsDrawNode(".draw") + .path(IsGeneralPath( + "MLLLZ", {0, 50, 50, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::GREEN)))))))); + + // Set an invalid vector graphic + executeCommand("SetValue", + {{"componentId", "VG"}, {"property", "source"}, {"value", "Missing"}}, true); + sg = root->getSceneGraph(); + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") + .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer") + .dirty(sg::Layer::kFlagRedrawContent)))); // No content is visible + + // Set to a bad vector graphic - one with an illegal height/width + executeCommand("SetValue", + {{"componentId", "VG"}, {"property", "source"}, {"value", "Bad"}}, true); + ASSERT_TRUE(ConsoleMessage()); // It should complain about the invalid graphic + sg = root->getSceneGraph(); + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") + .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer")))); + + // Set to an empty vector graphic - one with no content + executeCommand("SetValue", + {{"componentId", "VG"}, {"property", "source"}, {"value", "Empty"}}, true); + sg = root->getSceneGraph(); + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") + .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer")))); + + // Set it back to the original + executeCommand("SetValue", + {{"componentId", "VG"}, {"property", "source"}, {"value", "Diamond"}}, true); + sg = root->getSceneGraph(); + ASSERT_TRUE(CheckSceneGraph( + sg, + IsLayer(Rect{0, 0, 200, 200}, ".VectorGraphic") + .child(IsLayer(Rect{50, 50, 100, 100}, ".MediaLayer") + .dirty(sg::Layer::kFlagRedrawContent) + .content(IsTransformNode(".transform") + .child(IsDrawNode(".draw") + .path(IsGeneralPath( + "MLLLZ", {0, 50, 50, 0, 100, 50, 50, 100})) + .pathOp(IsFillOp(IsColorPaint(Color::GREEN)))))))); +} static const char *MULTI_TEXT = R"apl( { @@ -215,43 +310,143 @@ TEST_F(SGGraphicComponentTest, Moving) { loadDocument(MOVING); auto sg = root->getSceneGraph(); - ASSERT_TRUE(CheckSceneGraph( - sg, IsLayer(Rect{0, 0, 200, 200}) - .child(IsLayer(Rect{0, 0, 200, 200}) - .content(IsTransformNode(".alignment") - .child(IsOpacityNode(".groupOpacity") - .child(IsTransformNode(".group").child( - IsClipNode(".groupClip") - .path(IsGeneralPath("", {})) - .child(IsDrawNode() - .path(IsGeneralPath( - "MLLLZ", {0, 0, 10, 0, 10, - 10, 0, 10})) - .pathOp(IsFillOp(IsColorPaint( - Color::BLUE))))))))))); + sg, + IsLayer(Rect{0, 0, 200, 200}) + .child( + IsLayer(Rect{0, 0, 200, 200}) + .content(IsTransformNode(".alignment") + .child(IsTransformNode(".group").child( + IsDrawNode() + .path(IsGeneralPath("MLLLZ", {0, 0, 10, 0, 10, 10, 0, 10})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))))); root->updateTime(100); root->clearPending(); sg = root->getSceneGraph(); - ASSERT_TRUE(CheckSceneGraph( sg, - IsLayer(Rect{0, 0, 200, 200}) + IsLayer(Rect{0, 0, 200, 200}, "...vector graphic") .child( - IsLayer(Rect{0, 0, 200, 200}) + IsLayer(Rect{0, 0, 200, 200}, "...media layer") .dirty(sg::Layer::kFlagRedrawContent) - .content( - IsTransformNode(".alignment") - .child( - IsOpacityNode(".groupOpacity") - .child(IsTransformNode(".group").translate({100, 0}).child( - IsClipNode(".groupClip") - .path(IsGeneralPath("", {})) - .child(IsDrawNode() - .path(IsGeneralPath( - "MLLLZ", {0, 0, 10, 0, 10, 10, 0, 10})) - .pathOp(IsFillOp( - IsColorPaint(Color::BLUE))))))))))); + .content(IsTransformNode(".alignment") + .child(IsTransformNode(".group").translate({100, 0}).child( + IsDrawNode() + .path(IsGeneralPath("MLLLZ", {0, 0, 10, 0, 10, 10, 0, 10})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))))))); +} + +/** + * Turning on the experimental feature to generate layers for parameterized AVG puts the + * group in its own layer. + */ +TEST_F(SGGraphicComponentTest, MovingLayers) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureGraphicLayers); + loadDocument(MOVING); + auto sg = root->getSceneGraph(); + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 200, 200}, "...vector graphic") + .child(IsLayer(Rect{0, 0, 200, 200}, "...media layer") + .child(IsLayer(Rect(0,0,0,0), "...container") + .child(IsLayer(Rect(0,0,10,10), "...group") + .content( + IsDrawNode() + .path(IsGeneralPath( + "MLLLZ", {0, 0, 10, 0, 10, 10, 0, 10})) + .pathOp(IsFillOp( + IsColorPaint(Color::BLUE))))))))); + + root->updateTime(100); + root->clearPending(); + + sg = root->getSceneGraph(); + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 200, 200}) + .child(IsLayer(Rect{0, 0, 200, 200}) + .child(IsLayer(Rect(0,0,0,0), "...container") + .child(IsLayer(Rect(0,0,10,10), "...group") + .dirty(sg::Layer::kFlagTransformChanged) + .transform(Transform2D::translate({100,0})) + .content( + IsDrawNode() + .path(IsGeneralPath( + "MLLLZ", {0, 0, 10, 0, 10, 10, 0, 10})) + .pathOp(IsFillOp( + IsColorPaint(Color::BLUE))))))))); +} + + +static const char *REPLACE_SOURCE = R"apl( +{ + "type": "APL", + "version": "2022.1", + "graphics": { + "BlueBox": { + "type": "AVG", + "version": "1.2", + "width": 200, + "height": 200, + "items": { + "type": "path", + "fill": "blue", + "pathData": "h200 v200 h-200 z" + } + }, + "RedBox": { + "type": "AVG", + "version": "1.2", + "width": 200, + "height": 200, + "items": { + "type": "path", + "fill": "red", + "pathData": "h200 v200 h-200 z" + } + } + }, + "mainTemplate": { + "items": { + "type": "VectorGraphic", + "source": "BlueBox", + "scale": "best-fit", + "width": "200", + "height": "200" + } + } +} +)apl"; + +TEST_F(SGGraphicComponentTest, ReplaceSource) +{ + config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureGraphicLayers); + loadDocument(REPLACE_SOURCE); + auto sg = root->getSceneGraph(); + ASSERT_TRUE(CheckSceneGraph( + sg, + IsLayer(Rect{0, 0, 200, 200}, "...vector graphic") + .child(IsLayer(Rect{0, 0, 200, 200}, "...media layer") + .child(IsLayer(Rect(0, 0, 200, 200), "...container") + .content(IsDrawNode() + .path(IsGeneralPath( + "MLLLZ", {0, 0, 200, 0, 200, 200, 0, 200})) + .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))))); + + component->setProperty(apl::kPropertySource, "RedBox"); + ASSERT_TRUE(CheckDirty(component, kPropertySource, kPropertyGraphic, kPropertyVisualHash)); + + root->clearPending(); + sg = root->getSceneGraph(); + ASSERT_TRUE(CheckSceneGraph( + sg, + IsLayer(Rect{0, 0, 200, 200}, "...vector graphic") + .child(IsLayer(Rect{0, 0, 200, 200}, "...media layer") + .dirty(sg::Layer::kFlagChildrenChanged) + .child(IsLayer(Rect(0, 0, 200, 200), "...container") + .content(IsDrawNode() + .path(IsGeneralPath( + "MLLLZ", {0, 0, 200, 0, 200, 200, 0, 200})) + .pathOp(IsFillOp(IsColorPaint(Color::RED)))))))); } \ No newline at end of file diff --git a/unit/scenegraph/unittest_sg_graphic_layers.cpp b/unit/scenegraph/unittest_sg_graphic_layers.cpp new file mode 100644 index 0000000..dc06115 --- /dev/null +++ b/unit/scenegraph/unittest_sg_graphic_layers.cpp @@ -0,0 +1,1128 @@ +/** +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. +*/ + +#include "../testeventloop.h" +#include "apl/graphic/graphicbuilder.h" +#include "apl/graphic/graphicelementgroup.h" +#include "apl/graphic/graphicelementpath.h" +#include "apl/graphic/graphicelementtext.h" +#include "apl/scenegraph/builder.h" +#include "apl/scenegraph/node.h" +#include "test_sg.h" + +using namespace apl; + +class SGGraphicTestLayers : public DocumentWrapper { +public: + SGGraphicTestLayers() : DocumentWrapper() { + config->measure(std::make_shared()); + } + + void loadGraphic(const char *str, const StyleInstancePtr& style = nullptr) { + gc = GraphicContent::create(session, str); + ASSERT_TRUE(gc); + auto jr = JsonResource(&gc->get(), Path()); + auto context = Context::createTestContext(metrics, *config); + Properties properties; + graphic = Graphic::create(context, jr, std::move(properties), nullptr); + ASSERT_TRUE(graphic); + } + + void TearDown() override { + graphic = nullptr; + gc = nullptr; + updates.clear(); + DocumentWrapper::TearDown(); + } + + GraphicContentPtr gc; + GraphicPtr graphic; + sg::SceneGraphUpdates updates; +}; + + +// Use a custom test for checking the nodes. The regular test_sg.h code skips over operations +// that are not visible. But we want to verify that we have exactly the correct operations. +static ::testing::AssertionResult +checkOps(sg::PathOp *op, bool hasFill, Color fillColor, bool hasStroke, Color strokeColor) { + if (hasFill) { + if (!sg::FillPathOp::is_type(op)) + return ::testing::AssertionFailure() << "Expected a fill operation"; + auto *fill = sg::FillPathOp::cast(op); + auto *paint = sg::ColorPaint::cast(fill->paint); + if (!paint || paint->getColor() != fillColor) + return ::testing::AssertionFailure() << "Fill color mismatch"; + op = op->nextSibling.get(); + } + + if (hasStroke) { + if (!sg::StrokePathOp::is_type(op)) + return ::testing::AssertionFailure() << "Missing stroke operation"; + auto *stroke = sg::StrokePathOp::cast(op); + auto *paint = sg::ColorPaint::cast(stroke->paint); + if (!paint || paint->getColor() != strokeColor) + return ::testing::AssertionFailure() << "Stroke color mismatch"; + op = op->nextSibling.get(); + } + + if (op) + return ::testing::AssertionFailure() << "Extra operation"; + + return ::testing::AssertionSuccess(); +} + +static ::testing::AssertionResult +checkDraw(const sg::NodePtr& node, + float x, + bool hasFill, + Color fillColor, + bool hasStroke, + Color strokeColor) +{ + if (!node) + return ::testing::AssertionFailure() << "null draw node"; + + if (!sg::DrawNode::is_type(node)) + return ::testing::AssertionFailure() << "not a draw node"; + auto *draw = sg::DrawNode::cast(node); + + auto *path = sg::GeneralPath::cast(draw->getPath()); + if (!path) + return ::testing::AssertionFailure() << "missing path node"; + + // negative x used to indicate no points + if (path->getPoints() != (x < 0 ? std::vector{} : std::vector{0,0,x,0,x,x})) + return ::testing::AssertionFailure() << "mismatched points"; + + return checkOps(draw->getOp().get(), hasFill, fillColor, hasStroke, strokeColor); +} + +static ::testing::AssertionResult +checkText(const sg::NodePtr& node, + const std::string& textString, + bool hasFill, + Color fillColor, + bool hasStroke, + Color strokeColor) +{ + if (!node) + return ::testing::AssertionFailure() << "null text node"; + + if (!sg::TextNode::is_type(node)) + return ::testing::AssertionFailure() << "not a text node"; + + auto* text = sg::TextNode::cast(node); + if (text->getTextLayout()->toDebugString() != textString) + return ::testing::AssertionFailure() << "text mismatch"; + + return checkOps(text->getOp().get(), hasFill, fillColor, hasStroke, strokeColor); +} + +static const char *DRAW_OPTIMIZATION = R"apl( +{ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "parameters": [ + { + "name": "X", + "default": false + } + ], + "items": [ + { + "type": "path", + "description": "Empty path", + "fill": "red", + "pathData": "M10,10 M20,20" + }, + { + "type": "path", + "description": "Just fill", + "fill": "blue", + "pathData": "h1 v1 z" + }, + { + "type": "path", + "description": "Just stroke", + "stroke": "red", + "pathData": "h2 v2 z" + }, + { + "type": "path", + "description": "Stroke, but no width", + "stroke": "green", + "strokeWidth": 0, + "pathData": "h3 v3 z" + }, + { + "type": "path", + "description": "Stroke and fill", + "stroke": "yellow", + "fill": "black", + "strokeWidth": 5, + "pathData": "h4 v4 z" + }, + { + "type": "path", + "description": "Stroke and fill opacity zero", + "stroke": "pink", + "strokeOpacity": 0, + "fill": "blue", + "fillOpacity": 0, + "strokeWidth": 5, + "pathData": "h5 v5 z" + }, + { + "type": "path", + "description": "Path depends on X", + "pathData": "${X ? 'h6 v6 z' : 'M0,0'}", + "fill": "purple" + }, + { + "type": "path", + "description": "Fill depends on X", + "pathData": "h7 v7 z", + "fill": "${X ? 'blue' : 'transparent'}" + }, + { + "type": "path", + "description": "Stroke depends on X", + "pathData": "h8 v8 z", + "stroke": "${X ? 'red' : 'transparent'}" + } + ] +} +)apl"; + +TEST_F(SGGraphicTestLayers, DrawOptimization) +{ + loadGraphic(DRAW_OPTIMIZATION); + auto node = graphic->getSceneGraph(false, updates)->node(); + + // Skip the empty path - there is no path data + + // Fill blue + ASSERT_TRUE(checkDraw(node, 1, true, Color::BLUE, false, Color::TRANSPARENT)); + node = node->next(); + + // Stroke red + ASSERT_TRUE(checkDraw(node, 2, false, Color::TRANSPARENT, true, Color::RED)); + node = node->next(); + + // Skip the stroke with green because there is no stroke width + + // Stroke yellow, fill black + ASSERT_TRUE(checkDraw(node, 4, true, Color::BLACK, true, Color::YELLOW)); + node = node->next(); + + // Skip stroke pink, fill blue because the opacities hide all colors + + // Allow the fill purple even though there is no path because the path is mutable + ASSERT_TRUE(checkDraw(node, -1, true, Color::PURPLE, false, Color::TRANSPARENT)); + node = node->next(); + + // Allow the 7th case - the fill color can be changed + ASSERT_TRUE(checkDraw(node, 7, true, Color::TRANSPARENT, false, Color::TRANSPARENT)); + node = node->next(); + + // Allow the 8th case - the stroke color can be changed + ASSERT_TRUE(checkDraw(node, 8, false, Color::TRANSPARENT, true, Color::TRANSPARENT)); + node = node->next(); + + ASSERT_FALSE(node); +} + +TEST_F(SGGraphicTestLayers, DrawOptimizationLayers) +{ + loadGraphic(DRAW_OPTIMIZATION); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + + // Until we hit an element that is parameterized, they should all be part of the content of the layer + auto node = layer->content(); + ASSERT_TRUE(node); + + // Skip the empty path - there is no path data + + // Fill blue + ASSERT_TRUE(checkDraw(node, 1, true, Color::BLUE, false, Color::TRANSPARENT)); + node = node->next(); + + // Stroke red + ASSERT_TRUE(checkDraw(node, 2, false, Color::TRANSPARENT, true, Color::RED)); + node = node->next(); + + // Skip the stroke with green because there is no stroke width + + // Stroke yellow, fill black + ASSERT_TRUE(checkDraw(node, 4, true, Color::BLACK, true, Color::YELLOW)); + node = node->next(); + + // Skip stroke pink, fill blue because the opacities hide all colors + // We should have run out of content + ASSERT_FALSE(node); + + // The three parmeterized drawing nodes should be collapsed into a single node + ASSERT_EQ(1, layer->children().size()); + + // Allow the fill purple even though there is no path because the path is mutable + node = layer->children().at(0)->content(); + ASSERT_TRUE(checkDraw(node, -1, true, Color::PURPLE, false, Color::TRANSPARENT)); + node = node->next(); + + // Allow the 7th case - the fill color can be changed + ASSERT_TRUE(checkDraw(node, 7, true, Color::TRANSPARENT, false, Color::TRANSPARENT)); + node = node->next(); + + // Allow the 8th case - the stroke color can be changed + ASSERT_TRUE(checkDraw(node, 8, false, Color::TRANSPARENT, true, Color::TRANSPARENT)); + ASSERT_FALSE(node->next()); +} + + +static const char *TEXT_OPTIMIZATION = R"apl( +{ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "parameters": [ + { + "name": "X", + "default": false + } + ], + "items": [ + { + "type": "text", + "text": "Just fill", + "fill": "red" + }, + { + "type": "text", + "text": "Just stroke", + "stroke": "yellow", + "fillOpacity": 0, + "strokeWidth": 1 + }, + { + "type": "text", + "text": "Stroke and fill", + "stroke": "green", + "strokeWidth": 2, + "fill": "blue" + }, + { + "type": "text", + "text": "Nothing to draw", + "fillOpacity": 0 + }, + { + "type": "text", + "text": "", + "fill": "purple" + }, + { + "type": "text", + "text": "Default" + }, + { + "type": "text", + "text": "Parameterized ${X}" + } + ] +} +)apl"; + +TEST_F(SGGraphicTestLayers, TextOptimization) +{ + loadGraphic(TEXT_OPTIMIZATION); + auto node = graphic->getSceneGraph(false, updates)->node(); + + // Fill red (well, it's hidden under a transform) + ASSERT_TRUE(checkText(node->child(), "Just fill", true, Color::RED, false, Color::TRANSPARENT)); + node = node->next(); + + // Stroke yellow + ASSERT_TRUE( + checkText(node->child(), "Just stroke", false, Color::TRANSPARENT, true, Color::YELLOW)); + node = node->next(); + + // Stroke green, fill blue + ASSERT_TRUE(checkText(node->child(), "Stroke and fill", true, Color::BLUE, true, Color::GREEN)); + node = node->next(); + + // Skip the "Nothing to draw" - there is no fill or stroke + // Skip fill purple - no text to draw + + // Fill with black (the default color) + ASSERT_TRUE(checkText(node->child(), "Default", true, Color::BLACK, false, Color::TRANSPARENT)); + node = node->next(); + + // Parameterized and fill with black (the default color) + ASSERT_TRUE(checkText(node->child(), "Parameterized false", true, Color::BLACK, false, + Color::TRANSPARENT)); + node = node->next(); + + ASSERT_FALSE(node); +} + +TEST_F(SGGraphicTestLayers, TextOptimizationLayers) +{ + loadGraphic(TEXT_OPTIMIZATION); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + DumpSceneGraph(layer); + + // There should be one parameterized sublayer + ASSERT_EQ(1, layer->children().size()); + + auto node = layer->content(); + + // Fill red (well, it's hidden under a transform) + ASSERT_TRUE(checkText(node->child(), "Just fill", true, Color::RED, false, Color::TRANSPARENT)); + node = node->next(); + + // Stroke yellow + ASSERT_TRUE( + checkText(node->child(), "Just stroke", false, Color::TRANSPARENT, true, Color::YELLOW)); + node = node->next(); + + // Stroke green, fill blue + ASSERT_TRUE(checkText(node->child(), "Stroke and fill", true, Color::BLUE, true, Color::GREEN)); + node = node->next(); + + // Skip the "Nothing to draw" - there is no fill or stroke + // Skip fill purple - no text to draw + + // Fill with black (the default color) + ASSERT_TRUE(checkText(node->child(), "Default", true, Color::BLACK, false, Color::TRANSPARENT)); + node = node->next(); + + // No more children + ASSERT_FALSE(node); + + // The parameterized black text is in the first child layer + auto sublayer = layer->children().at(0); + node = sublayer->content(); + ASSERT_TRUE(checkText(node->child(), "Parameterized false", true, Color::BLACK, false, + Color::TRANSPARENT)); + ASSERT_FALSE(node->next()); +} + + +static const char * MERGE_DRAW_LAYERS = R"apl( +{ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "parameters": [{"name": "COLOR","default": "red"}], + "items": [ + { + "type": "path", + "pathData": "h1 v1 z", + "stroke": "black" + }, + { + "type": "path", + "pathData": "h2 v2 z", + "stroke": "black" + }, + { + "type": "path", + "pathData": "h3 v3 z", + "stroke": "${COLOR}" + }, + { + "type": "path", + "pathData": "h4 v4 z", + "stroke": "black" + }, + { + "type": "path", + "pathData": "h5 v5 z", + "stroke": "black" + }, + { + "type": "path", + "pathData": "h6 v6 z", + "stroke": "${COLOR}" + }, + { + "type": "path", + "pathData": "h7 v7 z", + "stroke": "${COLOR}" + } + ] +} +)apl"; + + +// Adjacent layers that are either static or parameterized should merge +TEST_F(SGGraphicTestLayers, MergeDrawLayers) +{ + loadGraphic(MERGE_DRAW_LAYERS); + auto layer = graphic->getSceneGraph(true, updates)->layer(); + + // First two draw nodes should be in the content + auto node = layer->content(); + ASSERT_TRUE(checkDraw(node, 1, false, Color::TRANSPARENT, true, Color::BLACK)); + node = node->next(); + ASSERT_TRUE(checkDraw(node, 2, false, Color::TRANSPARENT, true, Color::BLACK)); + ASSERT_FALSE(node->next()); + + // There should be three child layers + ASSERT_EQ(3, layer->children().size()); + + // The first child layer is a single mutable draw node + node = layer->children().at(0)->content(); + ASSERT_TRUE(checkDraw(node, 3, false, Color::TRANSPARENT, true, Color::RED)); + ASSERT_FALSE(node->next()); + + // The second child layer has the two static draw nodes + node = layer->children().at(1)->content(); + ASSERT_TRUE(checkDraw(node, 4, false, Color::TRANSPARENT, true, Color::BLACK)); + node = node->next(); + ASSERT_TRUE(checkDraw(node, 5, false, Color::TRANSPARENT, true, Color::BLACK)); + ASSERT_FALSE(node->next()); + + // The final child layer has two mutable draw nodes + node = layer->children().at(2)->content(); + ASSERT_TRUE(checkDraw(node, 6, false, Color::TRANSPARENT, true, Color::RED)); + node = node->next(); + ASSERT_TRUE(checkDraw(node, 7, false, Color::TRANSPARENT, true, Color::RED)); + ASSERT_FALSE(node->next()); +} + + + +static const char * DEBUG_CHECK_FIXED = R"apl( +{ + "type": "AVG", "version": "1.2", "height": 100, "width": 100, + "items": { + "type": "path", + "pathData": "h2 v2 z", + "stroke": "black" + } +} +)apl"; + +// This test verifies toDebugString(). It's also good for checking the node and layer construction +// logic to verify that the right layer flags get set. +TEST_F(SGGraphicTestLayers, DebugCheckFixed) +{ + // Node requested on fixed content + loadGraphic(DEBUG_CHECK_FIXED); + auto fragment = graphic->getSceneGraph(false, updates); + ASSERT_FALSE(fragment->isLayer()); + ASSERT_TRUE(fragment->isNode()); + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeContentFixed<")); + + // Layer requested on fixed content + loadGraphic(DEBUG_CHECK_FIXED); + fragment = graphic->getSceneGraph(true, updates); // Ask for a layer + ASSERT_TRUE(fragment->isLayer()); + ASSERT_FALSE(fragment->isNode()); + ASSERT_EQ(0, fragment->toDebugString().rfind("LayerFixedContentFixed<")); + ASSERT_TRUE(fragment->layer()->content()); // Has a drawing node + ASSERT_TRUE(fragment->layer()->children().empty()); // But no child layers +} + +static const char * DEBUG_CHECK_EMPTY = R"apl( +{ + "type": "AVG", "version": "1.2", "height": 100, "width": 100, + "items": { + "type": "group" + } +} +)apl"; + +TEST_F(SGGraphicTestLayers, DebugCheckEmpty) +{ + // Node requested on empty content + loadGraphic(DEBUG_CHECK_EMPTY); + auto fragment = graphic->getSceneGraph(false, updates); + ASSERT_FALSE(fragment->isLayer()); + ASSERT_FALSE(fragment->isNode()); + ASSERT_TRUE(fragment->empty()); + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeEmpty<")); + + // Layer requested on empty content. + loadGraphic(DEBUG_CHECK_EMPTY); + fragment = graphic->getSceneGraph(true, updates); // Ask for a layer on an empty node + ASSERT_FALSE(fragment->isLayer()); + ASSERT_FALSE(fragment->isNode()); + ASSERT_TRUE(fragment->empty()); + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeEmpty<")); +} + +static const char * DEBUG_CHECK_MUTABLE = R"apl( +{ + "type": "AVG", "version": "1.2", "height": 100, "width": 100, + "parameters": [{"name": "COLOR", "default": "blue"}, {"name": "OPACITY", "default": 1.0}], + "items": { + "type": "path", + "pathData": "h3 v3 z", + "stroke": "${COLOR}" + } +} +)apl"; + +// This test verifies toDebugString(). It's also good for checking the node and layer construction +// logic to verify that the right layer flags get set. +TEST_F(SGGraphicTestLayers, DebugCheckMutable) { + // Node requested on mutable content + loadGraphic(DEBUG_CHECK_MUTABLE); + auto fragment = graphic->getSceneGraph(false, updates); + ASSERT_FALSE(fragment->isLayer()); + ASSERT_TRUE(fragment->isNode()); + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeContentMutable<")); +} + + +static const char * DEBUG_CHECK_SHELL = R"apl( +{ + "type": "AVG", "version": "1.2", "height": 100, "width": 100, + "parameters": [ + {"name": "X", "default": "blue"}, + {"name": "Y", "default": 1.0}, + {"name": "T", "default": "scale(2)"}, + {"name": "LC", "default": "butt" }, + {"name": "LJ", "default": "round" } + ], + "items": { "type": "group" } +} +)apl"; + +// It's tricky to build a GraphicFragment with LayerFixedContentMutable or LayerMutable +// because the top-level element (the GraphicContainer) is a fixed layer. So we keep a loaded +// graphic with some parameters, but inflate elements separately from that + +/** + * These test cases are for AVG path objects that don't draw anything. They should return a + * nullptr fragment. + */ +static const std::vector EMPTY_PATH = { + R"("pathData": "h3", "stroke": null )", + R"("pathData": "", "stroke": "${X}")", + R"("pathData": "m20,20 30,30", "stroke": "${X}")", + R"("pathData": "h3 v3", "stroke": "transparent")", + R"("pathData": "h3 v3", "fill": "transparent")", + R"("pathData": "h3 v3", "fill": "blue", "fillOpacity": 0)", + R"("pathData": "h3 v3", "fillOpacity": "${Y}")", + R"("pathData": "h3 v3", "fillTransform": "${T}")", + R"("pathData": "h3 v3", "pathLength": "${Y}")", + R"("pathData": "h3 v3", "strokeDashArray": "${X}")", + R"("pathData": "h3 v3", "strokeDashOffset": "${X}")", + R"("pathData": "h3 v3", "strokeLineCap": "${LC}")", + R"("pathData": "h3 v3", "strokeLineJoin": "${LJ}")", + R"("pathData": "h3 v3", "strokeMiterLimit": "${X}")", + R"("pathData": "h3 v3", "strokeOpacity": "${X}")", + R"("pathData": "h3 v3", "strokeTransform": "${T}")", + R"("pathData": "h3 v3", "strokeWidth": "${X}")", + R"("pathData": "h3 v3", "stroke": "green", "strokeWidth": 0)", + R"("pathData": "h3 v3", "stroke": "transparent", "strokeWidth": "${X}")", + R"("pathData": "h3 v3", "stroke": "green", "strokeOpacity": 0)", + R"("pathData": "h3 v3", "stroke": "transparent", "strokeOpacity": "${X}")", + R"("pathData": "h3 v3", "stroke": {"type": "linear", "colorRange": ["transparent", "transparent"]})", +}; + +TEST_F(SGGraphicTestLayers, EmptyPath) +{ + loadGraphic(DEBUG_CHECK_SHELL); + for (const auto& m : EMPTY_PATH) { + auto data = JsonData(R"({"type": "path", )" + m + "}"); + auto path = GraphicElementPath::create(graphic, graphic->getContext(), data.get()); + ASSERT_TRUE(path) << m; + auto fragment = path->buildSceneGraph(true, updates); + ASSERT_FALSE(fragment) << m; + } +} + +/** + * These test case are for AVG path objects that have fixed properties. They should return + * a fragment containing an sg::NodePtr with the label "NodeContentFixed". + */ +static const std::vector NODE_PATH = { + R"("pathData": "h3", "stroke": "blue" )", + R"("pathData": "h3", "fill": "blue" )", + R"("pathData": "h3 v3", "stroke": {"type": "linear", "colorRange": ["blue", "green"]})", + R"("pathData": "h3 v3", "fill": {"type": "linear", "colorRange": ["blue", "green"]})", +}; + +TEST_F(SGGraphicTestLayers, NodePath) +{ + loadGraphic(DEBUG_CHECK_SHELL); + for (const auto& m : NODE_PATH) { + auto data = JsonData(R"({"type": "path", )" + m + "}"); + auto path = GraphicElementPath::create(graphic, graphic->getContext(), data.get()); + ASSERT_TRUE(path) << m; + auto fragment = path->buildSceneGraph(true, updates); + ASSERT_TRUE(fragment) << m; + ASSERT_FALSE(fragment->isLayer()) << m; + ASSERT_TRUE(fragment->isNode()) << m; + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeContentFixed")) << m; + } +} + +/** + * These test cases are for AVG path objects that do not have fixed properties. They should + * return a fragment containing an sg::LayerPtr with the label "LayerFixedContentMutable". + */ +static const std::vector LAYER_PATH = { + R"("pathData": "h3 v3", "fill": "${X}")", + R"("pathData": "h3 v3", "fill": "blue", "fillOpacity": "${Y}")", + R"("pathData": "h3 v3", "fill": "blue", "fillTransform": "${T}")", + R"("pathData": "h3 v3", "stroke": "white", "pathLength": "${Y}")", + R"("pathData": "h3 v3", "stroke": "${X}")", + R"("pathData": "h3 v3", "stroke": "white","strokeDashArray": "${X}")", + R"("pathData": "h3 v3", "stroke": "white","strokeDashOffset": "${X}")", + R"("pathData": "h3 v3", "stroke": "white","strokeLineCap": "${LC}")", + R"("pathData": "h3 v3", "stroke": "white","strokeLineJoin": "${LJ}")", + R"("pathData": "h3 v3", "stroke": "white","strokeMiterLimit": "${X}")", + R"("pathData": "h3 v3", "stroke": "white","strokeOpacity": "${X}")", + R"("pathData": "h3 v3", "stroke": "white","strokeTransform": "${T}")", + R"("pathData": "h3 v3", "stroke": "white","strokeWidth": "${X}")", + // Layer with a style is ALWAYS mutable + R"("pathData": "", "style": "foo" )", +}; + +TEST_F(SGGraphicTestLayers, LayerPath) +{ + loadGraphic(DEBUG_CHECK_SHELL); + for (const auto& m : LAYER_PATH) { + auto data = JsonData(R"({"type": "path", )" + m + "}"); + auto path = GraphicElementPath::create(graphic, graphic->getContext(), data.get()); + ASSERT_TRUE(path) << m; + auto fragment = path->buildSceneGraph(true, updates); + ASSERT_TRUE(fragment) << m; + ASSERT_TRUE(fragment->isLayer()) << m; + ASSERT_FALSE(fragment->isNode()) << m; + ASSERT_EQ(0, fragment->toDebugString().rfind("LayerFixedContentMutable")) << m; + } +} + + +static const char * DEBUG_CHECK_TEXT = R"apl( +{ + "type": "AVG", "version": "1.2", "height": 100, "width": 100, + "parameters": [ + {"name": "C", "default": "blue"}, + {"name": "X", "default": 1.0}, + {"name": "T", "default": "scale(2)"}, + {"name": "FF", "default": "serif" }, + {"name": "FS", "default": 40 }, + {"name": "FT", "default": "italic" }, + {"name": "FW", "default": 700 }, + {"name": "SW", "default": 2.0 }, + {"name": "TA", "default": "end" } + ], + "items": { "type": "group" } +} +)apl"; + +/** + * These test cases are for AVG text objects that don't draw anything. They should return a + * nullptr fragment. + */ +static const std::vector EMPTY_TEXT = { + R"("text": "" )", + R"("text": "Hi", "fill": "transparent" )", + R"("text": "Hi", "fill": "transparent", "fillOpacity": "${X}" )", + R"("text": "Hi", "fillOpacity": 0 )", + R"("text": "Hi", "file": "${C}", "fillOpacity": 0 )", + R"("text": "Hi", "fill": {"type": "linear", "colorRange": ["#0000", "#0000"]} )", + R"("text": "Hi", "fill": "transparent", "stroke": "blue" )", + R"("text": "Hi", "fill": "transparent", "stroke": "blue", "strokeWidth": 1, "strokeOpacity": 0 )", + R"("text": "Hi", "fill": "transparent", "strokeWidth": 1, "strokeOpacity": 1 )", +}; + +TEST_F(SGGraphicTestLayers, EmptyText) +{ + loadGraphic(DEBUG_CHECK_TEXT); + + for (const auto& m : EMPTY_TEXT) { + auto data = JsonData(R"({"type": "text", )" + m + "}"); + auto text = GraphicElementText::create(graphic, graphic->getContext(), data.get()); + ASSERT_TRUE(text) << m; + auto fragment = text->buildSceneGraph(true, updates); + ASSERT_FALSE(fragment) << m; + } +} + +/** + * These test case are for AVG text objects that have fixed properties. They should return + * a fragment containing an sg::NodePtr with the label "NodeContentFixed". + */ +static const std::vector NODE_TEXT = { + R"("text": "Hello" )", + R"("text": "Hello", "fill": "green" )", + R"("text": "Hello", "fillOpacity": 0.5 )", + R"("text": "Hello", "fontFamily": "serif" )", + R"("text": "Hello", "fontSize": 10 )", + R"("text": "Hello", "fontStyle": "italic" )", + R"("text": "Hello", "fontWeight": 100 )", + R"("text": "Hello", "letterSpacing": 2.0 )", + R"("text": "Hello", "fill": "transparent", "stroke": "blue", "strokeWidth": 1 )", + R"("text": "Hello", "textAnchor": "end" )", + R"("text": "Hello", "x": 23 )", + R"("text": "Hello", "y": 100 )", +}; + +TEST_F(SGGraphicTestLayers, NodeText) +{ + loadGraphic(DEBUG_CHECK_TEXT); + + for (const auto& m : NODE_TEXT) { + auto data = JsonData(R"({"type": "text", )" + m + "}"); + auto path = GraphicElementText::create(graphic, graphic->getContext(), data.get()); + ASSERT_TRUE(path) << m; + auto fragment = path->buildSceneGraph(true, updates); + ASSERT_TRUE(fragment) << m; + ASSERT_FALSE(fragment->isLayer()) << m; + ASSERT_TRUE(fragment->isNode()) << m; + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeContentFixed")) << m; + } +} + +/** + * These test cases are for AVG text objects that do not have fixed properties. They should + * return a fragment containing an sg::LayerPtr with the label "LayerFixedContentMutable". + */ +static const std::vector LAYER_TEXT = { + R"("text": "${C}" )", + R"("text": "Hello", "fill": "${C}" )", + R"("text": "Hello", "fillOpacity": "${X}" )", + R"("text": "Hello", "fontFamily": "${FF}" )", + R"("text": "Hello", "fontSize": "${FS}" )", + R"("text": "Hello", "fontStyle": "${FT}" )", + R"("text": "Hello", "fontWeight": "${FW}" )", + R"("text": "Hello", "letterSpacing": "${X}" )", + R"("text": "Hello", "fill": "transparent", "stroke": "${C}", "strokeWidth": 1 )", + R"("text": "Hello", "fill": "transparent", "stroke": "blue", "strokeOpacity": "${X}", "strokeWidth": 1 )", + R"("text": "Hello", "fill": "transparent", "stroke": "blue", "strokeWidth": "${SW}" )", + R"("text": "Hello", "textAnchor": "${TA}" )", + R"("text": "Hello", "x": "${X}" )", + R"("text": "Hello", "y": "${X}" )", + R"("text": "", "style": "foo" )", +}; + +TEST_F(SGGraphicTestLayers, LayerText) +{ + loadGraphic(DEBUG_CHECK_TEXT); + + for (const auto& m : LAYER_TEXT) { + auto data = JsonData(R"({"type": "text", )" + m + "}"); + auto path = GraphicElementText::create(graphic, graphic->getContext(), data.get()); + ASSERT_TRUE(path) << m; + auto fragment = path->buildSceneGraph(true, updates); + ASSERT_TRUE(fragment) << m; + ASSERT_TRUE(fragment->isLayer()) << m; + ASSERT_FALSE(fragment->isNode()) << m; + ASSERT_EQ(0, fragment->toDebugString().rfind("LayerFixedContentMutable")) << m; + } +} + +/** + * These test cases are for AVG group objects that return a nullptr fragment because + * they don't have children or the children are not visible + * children should return a nullptr fragment. + */ +TEST_F(SGGraphicTestLayers, EmptyGroups) { + // No children + loadGraphic(R"({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "items": { + "type": "group" + } + })"); + ASSERT_TRUE(graphic); + auto group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + auto fragment = group->buildSceneGraph(true, updates); + ASSERT_FALSE(fragment); + + // Opacity fixed at zero can never be seen + loadGraphic(R"({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "items": { + "type": "group", + "opacity": 0, + "items": { + "type": "text", + "text": "HI" + } + } + })"); + ASSERT_TRUE(graphic); + group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + ASSERT_EQ(1, group->getChildCount()); + fragment = group->buildSceneGraph(true, updates); + ASSERT_FALSE(fragment); +} + +TEST_F(SGGraphicTestLayers, FixedNodeGroups) +{ + // One child with fixed drawing content + loadGraphic(R"apl({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "items": { + "type": "group", + "opacity": 0.5, + "transform": "rotate(45)", + "items": { + "type": "text", + "text": "dog" + } + } + })apl"); + ASSERT_TRUE(graphic); + auto group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + auto fragment = group->buildSceneGraph(true, updates); + ASSERT_TRUE(fragment); + ASSERT_TRUE(fragment->isNode()); + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeContentFixed")); + + // Multiple children child with fixed drawing content + loadGraphic(R"apl({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "items": { + "type": "group", + "opacity": 0.5, + "transform": "rotate(45)", + "items": { + "type": "text", + "text": "dog" + }, + "data": "${Array.range(4)}" + } + })apl"); + ASSERT_TRUE(graphic); + group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + fragment = group->buildSceneGraph(true, updates); + ASSERT_TRUE(fragment); + ASSERT_TRUE(fragment->isNode()); + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeContentFixed")); +} + + +TEST_F(SGGraphicTestLayers, MutableNodeGroups) { + // One child with mutable drawing content + loadGraphic(R"apl({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "parameters": "TEXT", + "items": { + "type": "group", + "opacity": 0.5, + "transform": "rotate(45)", + "items": { + "type": "text", + "text": "${TEXT}" + } + } + })apl"); + ASSERT_TRUE(graphic); + auto group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + auto fragment = group->buildSceneGraph(false, updates); // Note: force a node, or this will be a layer + ASSERT_TRUE(fragment); + ASSERT_TRUE(fragment->isNode()); + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeContentMutable")); + + // Same as above, but this time put the mutation in the group + loadGraphic(R"apl({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "parameters": {"name": "OPACITY", "default": 1 }, + "items": { + "type": "group", + "opacity": "${OPACITY}", + "transform": "rotate(45)", + "items": { + "type": "text", + "text": "hi" + } + } + })apl"); + ASSERT_TRUE(graphic); + group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + fragment = group->buildSceneGraph(false, updates); // Note: force a node, or this will be a layer + ASSERT_TRUE(fragment); + ASSERT_TRUE(fragment->isNode()); + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeContentMutable")); + + // Assign a style to the group + loadGraphic(R"apl({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "items": { + "type": "group", + "style": "happy_feet", + "transform": "rotate(45)", + "items": { + "type": "text", + "text": "hi" + } + } + })apl"); + ASSERT_TRUE(graphic); + group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + fragment = group->buildSceneGraph(false, updates); // Note: force a node, or this will be a layer + ASSERT_TRUE(fragment); + ASSERT_TRUE(fragment->isNode()); + ASSERT_EQ(0, fragment->toDebugString().rfind("NodeContentMutable")); +} + +TEST_F(SGGraphicTestLayers, LayerContent) { + // Enforce a layer with a mutable child item + loadGraphic(R"apl({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "parameters": "TEXT", + "items": { + "type": "group", + "opacity": 0.5, + "transform": "rotate(45)", + "items": { + "type": "text", + "text": "${TEXT}" + } + } + })apl"); + ASSERT_TRUE(graphic); + auto group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + auto fragment = group->buildSceneGraph(true, updates); + ASSERT_TRUE(fragment); + ASSERT_TRUE(fragment->isLayer()); + ASSERT_EQ(0, fragment->toDebugString().rfind("LayerFixedContentFixed")); + + // Same as above, but this time put the mutation in the group + loadGraphic(R"apl({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "parameters": {"name": "OPACITY", "default": 1 }, + "items": { + "type": "group", + "opacity": "${OPACITY}", + "transform": "rotate(45)", + "items": { + "type": "text", + "text": "hi" + } + } + })apl"); + ASSERT_TRUE(graphic); + group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + fragment = group->buildSceneGraph(true, updates); + ASSERT_TRUE(fragment); + ASSERT_TRUE(fragment->isLayer()); + ASSERT_EQ(0, fragment->toDebugString().rfind("LayerMutable")) << fragment->toDebugString(); + + // Assign a style + loadGraphic(R"apl({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "items": { + "type": "group", + "style": "happy_feet", + "transform": "rotate(45)", + "items": { + "type": "text", + "text": "hi" + } + } + })apl"); + ASSERT_TRUE(graphic); + group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + fragment = group->buildSceneGraph(true, updates); + ASSERT_TRUE(fragment); + ASSERT_TRUE(fragment->isLayer()); + ASSERT_EQ(0, fragment->toDebugString().rfind("LayerMutable")) << fragment->toDebugString(); +} + +TEST_F(SGGraphicTestLayers, MergeGroups) { + // Enforce a layer with a mutable child item + loadGraphic(R"apl({ + "type": "AVG", + "version": "1.2", + "height": 100, + "width": 100, + "parameters": "TEXT", + "items": { + "type": "group", + "opacity": 0.5, + "transform": "rotate(45)", + "items": [ + { + "type": "group" + }, + { + "type": "group", + "item": { + "type": "text", + "text": "Um..." + } + }, + { + "type": "text", + "text": "${TEXT}" + } + ] + } + })apl"); + ASSERT_TRUE(graphic); + auto group = graphic->getRoot()->getChildAt(0); + ASSERT_TRUE(group); + auto fragment = group->buildSceneGraph(true, updates); + ASSERT_TRUE(fragment); + ASSERT_TRUE(fragment->isLayer()); + ASSERT_EQ(0, fragment->toDebugString().rfind("LayerFixedContentFixed")); +} + +// TODO: Check style changes - they should update properties as appropriate \ No newline at end of file diff --git a/unit/scenegraph/unittest_sg_layer.cpp b/unit/scenegraph/unittest_sg_layer.cpp index c834bbe..79de671 100644 --- a/unit/scenegraph/unittest_sg_layer.cpp +++ b/unit/scenegraph/unittest_sg_layer.cpp @@ -46,7 +46,7 @@ TEST_F(SGLayerTest, Basic) ASSERT_FALSE(layer->getShadow()); ASSERT_FALSE(layer->getAccessibility()); ASSERT_FALSE(layer->visible()); - ASSERT_EQ(layer->toDebugString(), "Layer"); + ASSERT_EQ(layer->toDebugString(), "Layer Test"); rapidjson::Document doc; ASSERT_TRUE(IsEqual(layer->serialize(doc.GetAllocator()), @@ -57,7 +57,9 @@ TEST_F(SGLayerTest, Basic) "bounds": [ 10, 20, 200, 300 ], "transform": [ 2, 0, 0, 2, 0, 0], "childOffset": [ 0, 0 ], - "interaction": 0 + "contentOffset": [ 0, 0 ], + "interaction": 0, + "characteristics": 0 } )apl"))); } @@ -123,6 +125,7 @@ TEST_F(SGLayerTest, Rich) "bounds": [ 0, 0, 100, 100 ], "transform": [ 1, 0, 0, 1, 0, 0], "childOffset": [ 20, 20 ], + "contentOffset": [ 0, 0 ], "outline": { "type": "roundedRectPath", "rect": [ 0, 0, 100, 100 ], @@ -153,7 +156,8 @@ TEST_F(SGLayerTest, Rich) ] } ], - "interaction": 0 + "interaction": 0, + "characteristics": 0 } )apl"))); } @@ -206,7 +210,9 @@ TEST_F(SGLayerTest, Children) "bounds": [ 0, 0, 100, 100 ], "transform": [ 1, 0, 0, 1, 0, 0], "childOffset": [ 0, 0 ], - "interaction": 0 + "contentOffset": [ 0, 0 ], + "interaction": 0, + "characteristics": 0 } )apl"))); @@ -237,7 +243,9 @@ TEST_F(SGLayerTest, Children) "bounds": [ 0, 0, 100, 100 ], "transform": [ 1, 0, 0, 1, 0, 0], "childOffset": [ 0, 0 ], + "contentOffset": [ 0, 0 ], "interaction": 0, + "characteristics": 0, "children": [ { "name": "Child1", @@ -245,7 +253,9 @@ TEST_F(SGLayerTest, Children) "bounds": [ 20, 20, 60, 10 ], "transform": [ 1, 0, 0, 1, 0, 0], "childOffset": [ 0, 0 ], - "interaction": 0 + "contentOffset": [ 0, 0 ], + "interaction": 0, + "characteristics": 0 }, { "name": "Child2", @@ -253,7 +263,9 @@ TEST_F(SGLayerTest, Children) "bounds": [ 20, 50, 60, 10 ], "transform": [ 1, 0, 0, 1, 0, 0], "childOffset": [ 0, 0 ], + "contentOffset": [ 0, 0 ], "interaction": 0, + "characteristics": 0, "shadow": { "color": "#000000ff", "offset": [2, 2], @@ -264,4 +276,28 @@ TEST_F(SGLayerTest, Children) } )apl"))); +} + +TEST_F(SGLayerTest, Characteristics) +{ + auto layer = sg::layer("Test", Rect(0, 0, 100, 100), 1.0f, Transform2D()); + ASSERT_EQ(0, layer->getCharacteristic()); + ASSERT_EQ(layer->debugCharacteristicString().find("DO_NOT_CLIP_CHILDREN"), std::string::npos); + ASSERT_EQ(layer->debugCharacteristicString().find("RENDER_ONLY"), std::string::npos); + ASSERT_FALSE(layer->isCharacteristicSet(sg::Layer::kCharacteristicRenderOnly)); + ASSERT_FALSE(layer->isCharacteristicSet(sg::Layer::kCharacteristicDoNotClipChildren)); + + layer->setCharacteristic(sg::Layer::kCharacteristicRenderOnly); + ASSERT_EQ(sg::Layer::kCharacteristicRenderOnly, layer->getCharacteristic()); + ASSERT_EQ(layer->debugCharacteristicString().find("DO_NOT_CLIP_CHILDREN"), std::string::npos); + ASSERT_NE(layer->debugCharacteristicString().find("RENDER_ONLY"), std::string::npos); + ASSERT_TRUE(layer->isCharacteristicSet(sg::Layer::kCharacteristicRenderOnly)); + ASSERT_FALSE(layer->isCharacteristicSet(sg::Layer::kCharacteristicDoNotClipChildren)); + + layer->setCharacteristic(sg::Layer::kCharacteristicDoNotClipChildren); + ASSERT_EQ(sg::Layer::kCharacteristicRenderOnly | sg::Layer::kCharacteristicDoNotClipChildren, layer->getCharacteristic()); + ASSERT_NE(layer->debugCharacteristicString().find("DO_NOT_CLIP_CHILDREN"), std::string::npos); + ASSERT_NE(layer->debugCharacteristicString().find("RENDER_ONLY"), std::string::npos); + ASSERT_TRUE(layer->isCharacteristicSet(sg::Layer::kCharacteristicRenderOnly)); + ASSERT_TRUE(layer->isCharacteristicSet(sg::Layer::kCharacteristicDoNotClipChildren)); } \ No newline at end of file diff --git a/unit/scenegraph/unittest_sg_node.cpp b/unit/scenegraph/unittest_sg_node.cpp index b7daa7e..b807a0b 100644 --- a/unit/scenegraph/unittest_sg_node.cpp +++ b/unit/scenegraph/unittest_sg_node.cpp @@ -225,7 +225,7 @@ TEST_F(SGNodeTest, ImageNode) auto filter = sg::solid(paint); auto node = sg::image(filter, Rect(0, 0, 100, 100), Rect(0, 0, 1, 1)); - ASSERT_EQ("ImageNode target=Rect<100x100+0+0> source=Rect<1x1+0+0>", node->toDebugString()); + ASSERT_EQ(0, node->toDebugString().rfind("ImageNode target=Rect")); ASSERT_TRUE(node->visible()); rapidjson::Document doc; @@ -296,7 +296,7 @@ TEST_F(SGNodeTest, VideoNode) const MediaState& state){}, "Foobar"); auto node = sg::video(player, Rect(0, 0, 100, 100), VideoScale::kVideoScaleBestFill); - ASSERT_EQ("VideoNode target=Rect<100x100+0+0> PLAYER", node->toDebugString()); + ASSERT_EQ(0, node->toDebugString().rfind("VideoNode target=Rect")); ASSERT_TRUE(node->visible()); rapidjson::Document doc; @@ -336,7 +336,7 @@ TEST_F(SGNodeTest, ShadowNode) auto shadow = sg::shadow(Color(Color::FUCHSIA), Point(5,5), 3.0f); auto node = sg::shadowNode(shadow, child); - ASSERT_EQ("ShadowNode", node->toDebugString()); + ASSERT_EQ(0, node->toDebugString().rfind("ShadowNode")); ASSERT_FALSE(node->visible()); rapidjson::Document doc; @@ -404,7 +404,7 @@ TEST_F(SGNodeTest, EditTextNode) auto node = sg::editText(editText, editTextBox, editTextConfig, "Hello, world!"); - ASSERT_EQ("EditTextNode text=Hello, world! color=#ff0000ff", node->toDebugString()); + ASSERT_EQ(0, node->toDebugString().rfind("EditTextNode text=")); ASSERT_TRUE(node->visible()); rapidjson::Document doc; diff --git a/unit/scenegraph/unittest_sg_nodebounds.cpp b/unit/scenegraph/unittest_sg_nodebounds.cpp index 9d66d4b..02c68cd 100644 --- a/unit/scenegraph/unittest_sg_nodebounds.cpp +++ b/unit/scenegraph/unittest_sg_nodebounds.cpp @@ -255,10 +255,7 @@ TEST_F(SGNodeBoundsTest, NodeSiblings) n3->setNext(n4); // Measuring the size of a node just gets that node itself and any children - ASSERT_TRUE(IsEqual(Rect(0, 0, 10, 20), n1->boundingBox(Transform2D()))); - - // Use the calculate method to include siblings - ASSERT_TRUE(IsEqual(Rect(-14, 0, 35, 50), sg::Node::calculateBoundingBox(n1, Transform2D()))); + ASSERT_TRUE(IsEqual(Rect(-14, 0, 35, 50), n1->boundingBox(Transform2D()))); // Adding a parent node captures just the siblings in the chain. In this case, n3 & n4 auto t = sg::transform(Transform2D::scale(0.5), n3); @@ -266,4 +263,53 @@ TEST_F(SGNodeBoundsTest, NodeSiblings) ASSERT_TRUE(IsEqual(Rect(-14, 0, 24, 50), t->boundingBox(Transform2D::scale(2)))); } +TEST_F(SGNodeBoundsTest, Nested) +{ + auto paint = sg::paint(Color(Color::BLACK)); + + auto n1 = sg::draw(sg::path("l10,20"), sg::fill(paint)); + ASSERT_TRUE(IsEqual(Rect(0, 0, 10, 20), sg::Node::calculateBoundingBox(n1))); + + auto n2 = sg::draw(sg::path("M6,2 l15,25"), sg::fill(paint)); + ASSERT_TRUE(IsEqual(Rect(6, 2, 15, 25), sg::Node::calculateBoundingBox(n2))); + + // Attach siblings. Now the size of n1 will include n2 + n1->setNext(n2); + ASSERT_TRUE(IsEqual(Rect(0, 0, 21, 27), sg::Node::calculateBoundingBox(n1))); + + auto n3 = sg::draw(sg::path("l10,50"), sg::fill(paint)); + ASSERT_TRUE(IsEqual(Rect(0, 0, 10, 50), sg::Node::calculateBoundingBox(n3))); + + auto n4 = sg::draw(sg::path("M-14,18 l15,15"), sg::fill(paint)); + ASSERT_TRUE(IsEqual(Rect(-14, 18, 15, 15), sg::Node::calculateBoundingBox(n4))); + + n3->setNext(n4); + ASSERT_TRUE(IsEqual(Rect(-14, 0, 24, 50), sg::Node::calculateBoundingBox(n3))); + + auto g1 = sg::transform(Transform2D::translate(-10,-12), n1); + ASSERT_TRUE(IsEqual(Rect(-10, -12, 21, 27), sg::Node::calculateBoundingBox(g1))); + + auto g2 = sg::transform(Transform2D::scale(0.5), n3); + ASSERT_TRUE(IsEqual(Rect(-7, 0, 12, 25), sg::Node::calculateBoundingBox(g2))); + + g1->setNext(g2); + ASSERT_TRUE(IsEqual(Rect(-10, -12, 21, 37), sg::Node::calculateBoundingBox(g1))); +} + +// This test case came from a design tool that exported an AVG file where subtle variations in +// a spline's endpoint caused the bounding box calculation to fail. +const char * BAD_TEST = "M94.0774256,67.9984627 C188.476854,-22.6660248 341.527998,-22.6660248 435.926415,67.9984627"; +const char * GOOD_TEST = "M94.0774256,67.9983657 C188.476854,-22.6661219 341.527998,-22.6661219 435.926415,67.9983657"; +const char * ROUND_TEST = "M94.08,67.99846 C188.476854,-22.6660248 341.527998,-22.6660248 435.926415,67.9984627"; + +TEST_F(SGNodeBoundsTest, OddCase) +{ + auto paint = sg::paint(Color(Color::BLACK)); + + auto bad = sg::draw(sg::path(BAD_TEST), sg::fill(paint))->boundingBox(Transform2D()); + auto good = sg::draw(sg::path(GOOD_TEST), sg::fill(paint))->boundingBox(Transform2D()); + auto rounded = sg::draw(sg::path(ROUND_TEST), sg::fill(paint))->boundingBox(Transform2D()); + ASSERT_TRUE(IsEqual(bad,good,0.1)); + ASSERT_TRUE(IsEqual(bad,rounded,0.1)); +} \ No newline at end of file diff --git a/unit/scenegraph/unittest_sg_path.cpp b/unit/scenegraph/unittest_sg_path.cpp index fd55866..642acd4 100644 --- a/unit/scenegraph/unittest_sg_path.cpp +++ b/unit/scenegraph/unittest_sg_path.cpp @@ -21,7 +21,7 @@ using namespace apl; TEST(SGPathTest, Rectangle) { - ASSERT_EQ(sg::path(Rect{0, 20, 100, 100})->toDebugString(), "RectPath Rect<100x100+0+20>"); + ASSERT_EQ(0, sg::path(Rect{0, 20, 100, 100})->toDebugString().rfind("RectPath Rect")); ASSERT_EQ(sg::path(Rect{0, 20, 100, 100}), sg::path(Rect{0, 20, 100, 100})); ASSERT_NE(sg::path(Rect{10, 20, 30, 40}), sg::path(Rect{10, 20, 30, 60})); @@ -35,8 +35,7 @@ TEST(SGPathTest, Rectangle) TEST(SGPathTest, RoundedRect) { - ASSERT_EQ(sg::path(Rect{0, 20, 100, 100}, 5)->toDebugString(), - "RoundedRectPath Rect<100x100+0+20>:Radii<5.000000, 5.000000, 5.000000, 5.000000>"); + ASSERT_EQ(0, sg::path(Rect{0, 20, 100, 100}, 5)->toDebugString().rfind("RoundedRectPath Rect")); ASSERT_EQ(sg::path(Rect{10, 20, 30, 40}, 5), sg::path(Rect{10, 20, 30, 40}, 5)); ASSERT_NE(sg::path(Rect{10, 20, 30, 40}, 5), sg::path(Rect{10, 20, 30, 77}, 5)); @@ -62,8 +61,7 @@ TEST(SGPathTest, RoundedRect) TEST(SGPathTest, GeneralPath) { - ASSERT_EQ(sg::path("h20 v20 h-20 z")->toDebugString(), - "GeneralPath MLLLZ [0.000000,0.000000,20.000000,0.000000,20.000000,20.000000,0.000000,20.000000]"); + ASSERT_EQ(0, sg::path("h20 v20 h-20 z")->toDebugString().rfind("GeneralPath MLLLZ")); ASSERT_EQ(sg::path("M5,5 h20"), sg::path("M 5 5 L25,5")); ASSERT_NE(sg::path("M5,5 h20"), sg::path("M 5 5 L25,6")); @@ -84,8 +82,7 @@ TEST(SGPathTest, GeneralPath) TEST(SGPathTest, FramePath) { - ASSERT_EQ(sg::path(RoundedRect(Rect{0,0,10,10}, 4), 2)->toDebugString(), - "FramePath Rect<10x10+0+0>:Radii<4.000000, 4.000000, 4.000000, 4.000000> inset=2.000000"); + ASSERT_EQ(0, sg::path(RoundedRect(Rect{0,0,10,10}, 4), 2)->toDebugString().rfind("FramePath Rect")); ASSERT_FALSE(sg::path(RoundedRect(Rect{0,0,10,10}, 4), 2)->empty()); ASSERT_FALSE(sg::path(RoundedRect(Rect{0,0,0,10}, 4), 2)->empty()); diff --git a/unit/scenegraph/unittest_sg_pathbounds.cpp b/unit/scenegraph/unittest_sg_pathbounds.cpp index f1d37cc..dae11be 100644 --- a/unit/scenegraph/unittest_sg_pathbounds.cpp +++ b/unit/scenegraph/unittest_sg_pathbounds.cpp @@ -18,6 +18,7 @@ #include "test_sg.h" #include "apl/scenegraph/builder.h" #include "apl/scenegraph/pathparser.h" +#include "apl/scenegraph/pathbounds.h" using namespace apl; @@ -99,3 +100,33 @@ TEST_F(SGPathBoundsTest, StrokePathMaxWidth) ASSERT_EQ(24.0, op->maxWidth()); } +const std::vector T1 = { + 94.077423, + 67.9983673, + 188.476852, + -22.6661224, + 341.527985, + -22.6661224, + 435.926422, + 67.9983673,}; + +const std::vector T2 = { + 94.077423, + 67.9984665, + 188.476852, + -22.6660252, + 341.527985, + -22.6660252, + 435.926422, + 67.9984665, +}; + +/** + * This test case checks for numerical instability in the calculation of the bounding box + * of a cubic spline. The two splines differ only slightly, but one of them triggered a + * "can't find root" condition and resulted in a straight line instead of an arc. + */ +TEST_F(SGPathBoundsTest, NumericalInstability) +{ + ASSERT_TRUE(IsEqual(sg::calculatePathBounds("MC", T1), sg::calculatePathBounds("MC", T2))); +} \ No newline at end of file diff --git a/unit/scenegraph/unittest_sg_pathparser.cpp b/unit/scenegraph/unittest_sg_pathparser.cpp index e010dd0..8066d52 100644 --- a/unit/scenegraph/unittest_sg_pathparser.cpp +++ b/unit/scenegraph/unittest_sg_pathparser.cpp @@ -156,22 +156,20 @@ TEST_F(SGPathParserTest, Path) ASSERT_TRUE(CheckSceneGraph( sg, IsLayer(Rect{0, 0, 1024, 800}, "..VectorGraphic") - .child(IsLayer(Rect{112, 0, 800, 800}, "...Graphic") - .content( - IsTransformNode() - .transform(Transform2D::scale(2)) - .child( - IsOpacityNode().child(IsTransformNode().child( - IsClipNode() - .path(IsGeneralPath( - "MLLLZ", {0, 200, 200, 0, 400, 200, 200, 400})) - .child(IsDrawNode() - .path(IsGeneralPath( - "MLLLZ", - {40, 40, 360, 40, 360, 360, 40, 360})) - .pathOp(IsFillOp(IsColorPaint(Color::RED))) - .pathOp(IsStrokeOp(IsColorPaint(Color::BLUE), - 10)))))))))); + .child( + IsLayer(Rect{112, 0, 800, 800}, "...Graphic") + .content( + IsTransformNode() + .transform(Transform2D::scale(2)) + .child(IsClipNode() + .path(IsGeneralPath("MLLLZ", + {0, 200, 200, 0, 400, 200, 200, 400})) + .child(IsDrawNode() + .path(IsGeneralPath("MLLLZ", {40, 40, 360, 40, + 360, 360, 40, 360})) + .pathOp(IsFillOp(IsColorPaint(Color::RED))) + .pathOp(IsStrokeOp(IsColorPaint(Color::BLUE), + 10)))))))); } static const char *PATTERN = R"apl( diff --git a/unit/test_comparisons.h b/unit/test_comparisons.h new file mode 100644 index 0000000..27d6e39 --- /dev/null +++ b/unit/test_comparisons.h @@ -0,0 +1,116 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +/** + * This file contains some common tests for comparing primitive objects + */ + +#ifndef _APL_TEST_COMPARISONS_H +#define _APL_TEST_COMPARISONS_H + +#include "gtest/gtest.h" + +#include "apl/primitives/transform2d.h" +#include "apl/primitives/point.h" +#include "apl/primitives/rect.h" +#include "apl/primitives/object.h" + +namespace apl { + +inline ::testing::AssertionResult +IsEqual(float lhs, float rhs, float epsilon = 0.0001) { + if (std::abs(lhs - rhs) > epsilon) + return ::testing::AssertionFailure() << lhs << " != " << rhs; + + return ::testing::AssertionSuccess(); +} + +inline ::testing::AssertionResult +IsEqual(const Transform2D& lhs, const Transform2D& rhs, float epsilon = 0.0001) { + for (int i = 0; i < 6; i++) + if (std::abs(lhs.get()[i] - rhs.get()[i]) > epsilon) { + streamer s; + s << "[" << lhs << "] != [" << rhs << "]"; + return ::testing::AssertionFailure() << s.str(); + } + + return ::testing::AssertionSuccess(); +} + +inline ::testing::AssertionResult +IsEqual(const Point& lhs, const Point& rhs, float epsilon = 0.0001) { + if (std::abs(lhs.getX() - rhs.getX()) > epsilon || std::abs(lhs.getY() - rhs.getY()) > epsilon) + return ::testing::AssertionFailure() + << lhs.toDebugString() << " != " << rhs.toDebugString(); + + return ::testing::AssertionSuccess(); +} + +inline ::testing::AssertionResult +IsEqual(const Rect& lhs, const Rect& rhs, float epsilon = 0.0001) { + if (std::abs(lhs.getX() - rhs.getX()) > epsilon || + std::abs(lhs.getY() - rhs.getY()) > epsilon || + std::abs(lhs.getWidth() - rhs.getWidth()) > epsilon || + std::abs(lhs.getHeight() - rhs.getHeight()) > epsilon) + return ::testing::AssertionFailure() + << lhs.toDebugString() << " != " << rhs.toDebugString(); + + return ::testing::AssertionSuccess(); +} + +// This template compares two vectors of floating point numbers (doubles, floats, etc) +template ::value, bool>::type = true> +::testing::AssertionResult +IsEqual(const std::vector& a, const std::vector& b, float epsilon = 1e-6) { + if (a.size() != b.size()) + return ::testing::AssertionFailure() << "Size mismatch a=" << a.size() << " b=" << b.size(); + + for (int i = 0; i < a.size(); i++) + if (std::abs(a.at(i) - b.at(i)) > epsilon) + return ::testing::AssertionFailure() + << "Element mismatch index=" << i << " a=" << a.at(i) << " b=" << b.at(i); + + return ::testing::AssertionSuccess(); +} + +// This template compares two vectors of items that support the '!=' operator +template ::value, bool>::type = true> +::testing::AssertionResult +IsEqual(const std::vector& a, const std::vector& b) { + if (a.size() != b.size()) + return ::testing::AssertionFailure() << "Size mismatch a=" << a.size() << " b=" << b.size(); + + for (int i = 0; i < a.size(); i++) + if (a.at(i) != b.at(i)) + return ::testing::AssertionFailure() + << "Element mismatch index=" << i << " a=" << a.at(i) << " b=" << b.at(i); + + return ::testing::AssertionSuccess(); +} + +inline ::testing::AssertionResult +IsEqual(const Object& lhs, const Object& rhs) { + if (lhs != rhs) + return ::testing::AssertionFailure() + << lhs.toDebugString() << " != " << rhs.toDebugString(); + + return ::testing::AssertionSuccess(); +} + +} // namespace apl + + +#endif //_APL_TEST_COMPARISONS_H diff --git a/unit/testeventloop.cpp b/unit/testeventloop.cpp index 2be1ffb..ea88744 100644 --- a/unit/testeventloop.cpp +++ b/unit/testeventloop.cpp @@ -14,6 +14,7 @@ */ #include "testeventloop.h" + #include "apl/livedata/livearrayobject.h" #include "apl/livedata/livemapobject.h" #include "apl/graphic/graphicelementcontainer.h" diff --git a/unit/testeventloop.h b/unit/testeventloop.h index b0ce896..6f2678d 100644 --- a/unit/testeventloop.h +++ b/unit/testeventloop.h @@ -16,18 +16,19 @@ #ifndef _APL_TEST_EVENT_LOOP_H #define _APL_TEST_EVENT_LOOP_H +#include #include -#include -#include #include +#include #include -#include #include +#include #include "rapidjson/error/en.h" #include "rapidjson/pointer.h" #include "gtest/gtest.h" +#include "test_comparisons.h" #include "apl/apl.h" #include "apl/command/commandfactory.h" @@ -924,82 +925,6 @@ ::testing::AssertionResult TransformComponent(const RootContextPtr& root, const return ::testing::AssertionSuccess(); } -inline -::testing::AssertionResult IsEqual(const Transform2D& lhs, const Transform2D& rhs, float epsilon=0.0001) -{ - for (int i = 0 ; i < 6 ; i++) - if (std::abs(lhs.get()[i] - rhs.get()[i]) > epsilon) { - streamer s; - s << "[" << lhs << "] != [" << rhs << "]"; - return ::testing::AssertionFailure() << s.str(); - } - - return ::testing::AssertionSuccess(); -} - -inline -::testing::AssertionResult IsEqual(const Point& lhs, const Point& rhs, float epsilon=0.0001) -{ - if (std::abs(lhs.getX() - rhs.getX()) > epsilon || - std::abs(lhs.getY() - rhs.getY()) > epsilon) - return ::testing::AssertionFailure() << lhs.toDebugString() << " != " << rhs.toDebugString(); - - return ::testing::AssertionSuccess(); -} - -inline -::testing::AssertionResult IsEqual(const Rect& lhs, const Rect& rhs, float epsilon=0.0001) -{ - if (std::abs(lhs.getX() - rhs.getX()) > epsilon || - std::abs(lhs.getY() - rhs.getY()) > epsilon || - std::abs(lhs.getWidth() - rhs.getWidth()) > epsilon || - std::abs(lhs.getHeight() - rhs.getHeight()) > epsilon) - return ::testing::AssertionFailure() << lhs.toDebugString() << " != " << rhs.toDebugString(); - - return ::testing::AssertionSuccess(); -} - -// This template compares two vectors of floating point numbers (doubles, floats, etc) -template::value, bool>::type = true> -::testing::AssertionResult IsEqual(const std::vector& a, const std::vector& b, float epsilon=1e-6) -{ - if (a.size() != b.size()) - return ::testing::AssertionFailure() << "Size mismatch a=" << a.size() << " b=" << b.size(); - - for (int i = 0; i < a.size(); i++) - if (std::abs(a.at(i)-b.at(i)) > epsilon) - return ::testing::AssertionFailure() - << "Element mismatch index=" << i << " a=" << a.at(i) << " b=" << b.at(i); - - return ::testing::AssertionSuccess(); -} - -// This template compares two vectors of items that support the '!=' operator -template::value, bool>::type = true> -::testing::AssertionResult IsEqual(const std::vector& a, const std::vector& b) -{ - if (a.size() != b.size()) - return ::testing::AssertionFailure() << "Size mismatch a=" << a.size() << " b=" << b.size(); - - for (int i = 0; i < a.size(); i++) - if (a.at(i) != b.at(i)) - return ::testing::AssertionFailure() - << "Element mismatch index=" << i << " a=" << a.at(i) << " b=" << b.at(i); - - return ::testing::AssertionSuccess(); -} - -inline -::testing::AssertionResult IsEqual(const Object& lhs, const Object& rhs) -{ - if (lhs != rhs) - return ::testing::AssertionFailure() << lhs.toDebugString() << " != " << rhs.toDebugString(); - - return ::testing::AssertionSuccess(); -} - inline ::testing::AssertionResult MatchEvent(const Event& event, EventType eventType, diff --git a/unit/utils/CMakeLists.txt b/unit/utils/CMakeLists.txt index 107dcb4..63bb216 100644 --- a/unit/utils/CMakeLists.txt +++ b/unit/utils/CMakeLists.txt @@ -24,4 +24,5 @@ target_sources_local(unittest unittest_url.cpp unittest_userdata.cpp unittest_weakcache.cpp - ) \ No newline at end of file + unittest_synchronizedweakcache.cpp + ) diff --git a/unit/utils/unittest_synchronizedweakcache.cpp b/unit/utils/unittest_synchronizedweakcache.cpp new file mode 100644 index 0000000..fd56974 --- /dev/null +++ b/unit/utils/unittest_synchronizedweakcache.cpp @@ -0,0 +1,96 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "../testeventloop.h" + +#include "apl/utils/synchronizedweakcache.h" + +using namespace apl; + +class Bar { +public: + static std::shared_ptr create(int value) { return std::make_shared(value); } + + Bar(int value) : value(value) {} + + const int value; +}; + +TEST(SynchronizedWeakCache, WrapsWeakCache) +{ + auto f1 = Bar::create(1); + auto f2 = Bar::create(2); + + SynchronizedWeakCache cache = { + {"f1", f1}, + {"f2", f2} + }; + + ASSERT_TRUE(cache.find("f1")); + ASSERT_TRUE(cache.find("f2")); + + { + auto f3 = Bar::create(3); + auto f4 = Bar::create(4); + + ASSERT_FALSE(cache.find("f3")); + ASSERT_FALSE(cache.find("f4")); + + cache.insert("f3", f3); + cache.insert("f4", f4); + + ASSERT_TRUE(cache.find("f3")); + ASSERT_TRUE(cache.find("f4")); + + ASSERT_EQ(4, cache.size()); + } + + ASSERT_EQ(2, cache.size()); + ASSERT_FALSE(cache.empty()); + + cache.clean(); + ASSERT_EQ(2, cache.size()); +} + +TEST(SynchronizedWeakCache, AutomaticallyCleansWhenDirty) +{ + SynchronizedWeakCache cache; + ASSERT_FALSE(cache.dirty()); + + auto f1 = Bar::create(1); + auto f2 = Bar::create(2); + cache.insert("f1", f1); + cache.insert("f2", f2); + + ASSERT_TRUE(cache.find("f1")); + ASSERT_TRUE(cache.find("f2")); + ASSERT_FALSE(cache.dirty()); + + { + auto f3 = Bar::create(3); + cache.insert("f3", f3); + } + cache.markDirty(); + ASSERT_TRUE(cache.dirty()); + + // The cache has been marked as dirty, so next insert should clean it + auto f4 = Bar::create(4); + cache.insert("f4", f4); + + // Cache shouldn't be dirty anymore, this is a side-effect validation + // that the cleaning has taken place, because size() also cleans. + ASSERT_FALSE(cache.dirty()); + ASSERT_EQ(3, cache.size()); +} diff --git a/unit/utils/unittest_weakcache.cpp b/unit/utils/unittest_weakcache.cpp index 6a2dec1..ad7fa4e 100644 --- a/unit/utils/unittest_weakcache.cpp +++ b/unit/utils/unittest_weakcache.cpp @@ -16,6 +16,7 @@ #include "../testeventloop.h" #include "apl/utils/weakcache.h" +#include "apl/utils/synchronizedweakcache.h" using namespace apl; @@ -71,3 +72,43 @@ TEST(WeakCache, Prepopulate) ASSERT_EQ(2, cache.size()); } + +TEST(WeakCache, CleansOnDemand) +{ + WeakCache cache; + + ASSERT_EQ(0, cache.size()); + + { + cache.insert("f1", Foo::create(1)); + cache.insert("f2", Foo::create(2)); + } + + // Cache contains two expired items (trust me) + + // Clean them up + cache.clean(); + ASSERT_EQ(0, cache.size()); + + // Add a mix of expired and non-expired items + auto f3 = Foo::create(3); + cache.insert("f3", f3); + { + cache.insert("f4", Foo::create(4)); + cache.insert("f5", Foo::create(5)); + } + auto f6 = Foo::create(6); + cache.insert("f6", f6); + + // Clean up the expired ones + cache.clean(); + + // This leaves behind exactly two non-expired ones + ASSERT_EQ(2, cache.size()); + auto f3b = cache.find("f3"); + auto f6b = cache.find("f6"); + ASSERT_TRUE(f3b); + ASSERT_TRUE(f6b); + ASSERT_EQ(3, f3b->value); + ASSERT_EQ(6, f6b->value); +}