| /* | |
| * Copyright (C) 2014 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License 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. | |
| */ | |
| #pragma once | |
| #ifdef __ANDROID__ // Layoutlib does not support device info | |
| #include "DeviceInfo.h" | |
| #endif // __ANDROID__ | |
| #include "Outline.h" | |
| #include "Rect.h" | |
| #include "RevealClip.h" | |
| #include "effects/StretchEffect.h" | |
| #include "utils/MathUtils.h" | |
| #include "utils/PaintUtils.h" | |
| #include <SkBlendMode.h> | |
| #include <SkImageFilter.h> | |
| #include <SkCamera.h> | |
| #include <SkColor.h> | |
| #include <SkMatrix.h> | |
| #include <SkRegion.h> | |
| #include <androidfw/ResourceTypes.h> | |
| #include <cutils/compiler.h> | |
| #include <stddef.h> | |
| #include <utils/Log.h> | |
| #include <algorithm> | |
| #include <ostream> | |
| #include <vector> | |
| class SkBitmap; | |
| class SkColorFilter; | |
| class SkPaint; | |
| namespace android { | |
| namespace uirenderer { | |
| class Matrix4; | |
| class RenderNode; | |
| class RenderProperties; | |
| // The __VA_ARGS__ will be executed if a & b are not equal | |
| #define RP_SET(a, b, ...) ((a) != (b) ? ((a) = (b), ##__VA_ARGS__, true) : false) | |
| #define RP_SET_AND_DIRTY(a, b) RP_SET(a, b, mPrimitiveFields.mMatrixOrPivotDirty = true) | |
| // Keep in sync with View.java:LAYER_TYPE_* | |
| enum class LayerType { | |
| None = 0, | |
| // We cannot build the software layer directly (must be done at record time) and all management | |
| // of software layers is handled in Java. | |
| Software = 1, | |
| RenderLayer = 2, | |
| }; | |
| enum ClippingFlags { | |
| CLIP_TO_BOUNDS = 0x1 << 0, | |
| CLIP_TO_CLIP_BOUNDS = 0x1 << 1, | |
| }; | |
| class LayerProperties { | |
| public: | |
| bool setType(LayerType type) { | |
| if (RP_SET(mType, type)) { | |
| reset(); | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool setOpaque(bool opaque) { return RP_SET(mOpaque, opaque); } | |
| bool opaque() const { return mOpaque; } | |
| bool setAlpha(uint8_t alpha) { return RP_SET(mAlpha, alpha); } | |
| uint8_t alpha() const { return mAlpha; } | |
| bool setXferMode(SkBlendMode mode) { return RP_SET(mMode, mode); } | |
| SkBlendMode xferMode() const { return mMode; } | |
| SkColorFilter* getColorFilter() const { return mColorFilter.get(); } | |
| bool setImageFilter(SkImageFilter* imageFilter); | |
| SkImageFilter* getImageFilter() const { return mImageFilter.get(); } | |
| const StretchEffect& getStretchEffect() const { return mStretchEffect; } | |
| StretchEffect& mutableStretchEffect() { return mStretchEffect; } | |
| // Sets alpha, xfermode, and colorfilter from an SkPaint | |
| // paint may be NULL, in which case defaults will be set | |
| bool setFromPaint(const SkPaint* paint); | |
| bool needsBlending() const { return !opaque() || alpha() < 255; } | |
| LayerProperties& operator=(const LayerProperties& other); | |
| // Strongly recommend using effectiveLayerType instead | |
| LayerType type() const { return mType; } | |
| private: | |
| LayerProperties(); | |
| ~LayerProperties(); | |
| void reset(); | |
| bool setColorFilter(SkColorFilter* filter); | |
| friend class RenderProperties; | |
| LayerType mType = LayerType::None; | |
| // Whether or not that Layer's content is opaque, doesn't include alpha | |
| bool mOpaque; | |
| uint8_t mAlpha; | |
| SkBlendMode mMode; | |
| sk_sp<SkColorFilter> mColorFilter; | |
| sk_sp<SkImageFilter> mImageFilter; | |
| StretchEffect mStretchEffect; | |
| }; | |
| /* | |
| * Data structure that holds the properties for a RenderNode | |
| */ | |
| class RenderProperties { | |
| public: | |
| RenderProperties(); | |
| virtual ~RenderProperties(); | |
| static bool setFlag(int flag, bool newValue, int* outFlags) { | |
| if (newValue) { | |
| if (!(flag & *outFlags)) { | |
| *outFlags |= flag; | |
| return true; | |
| } | |
| return false; | |
| } else { | |
| if (flag & *outFlags) { | |
| *outFlags &= ~flag; | |
| return true; | |
| } | |
| return false; | |
| } | |
| } | |
| /** | |
| * Set internal layer state based on whether this layer | |
| * | |
| * Additionally, returns true if child RenderNodes with functors will need to use a layer | |
| * to support clipping. | |
| */ | |
| bool prepareForFunctorPresence(bool willHaveFunctor, bool ancestorDictatesFunctorsNeedLayer) { | |
| // parent may have already dictated that a descendant layer is needed | |
| bool functorsNeedLayer = | |
| ancestorDictatesFunctorsNeedLayer | |
| || CC_UNLIKELY(isClipMayBeComplex()) | |
| // Round rect clipping forces layer for functors | |
| || CC_UNLIKELY(getOutline().willRoundRectClip()) || | |
| CC_UNLIKELY(getRevealClip().willClip()) | |
| // Complex matrices forces layer, due to stencil clipping | |
| || CC_UNLIKELY(getTransformMatrix() && !getTransformMatrix()->isScaleTranslate()) || | |
| CC_UNLIKELY(getAnimationMatrix() && !getAnimationMatrix()->isScaleTranslate()) || | |
| CC_UNLIKELY(getStaticMatrix() && !getStaticMatrix()->isScaleTranslate()); | |
| mComputedFields.mNeedLayerForFunctors = (willHaveFunctor && functorsNeedLayer); | |
| // If on a layer, will have consumed the need for isolating functors from stencil. | |
| // Thus, it's safe to reset the flag until some descendent sets it. | |
| return CC_LIKELY(effectiveLayerType() == LayerType::None) && functorsNeedLayer; | |
| } | |
| RenderProperties& operator=(const RenderProperties& other); | |
| bool setClipToBounds(bool clipToBounds) { | |
| return setFlag(CLIP_TO_BOUNDS, clipToBounds, &mPrimitiveFields.mClippingFlags); | |
| } | |
| bool setClipBounds(const Rect& clipBounds) { | |
| bool ret = setFlag(CLIP_TO_CLIP_BOUNDS, true, &mPrimitiveFields.mClippingFlags); | |
| return RP_SET(mPrimitiveFields.mClipBounds, clipBounds) || ret; | |
| } | |
| bool setClipBoundsEmpty() { | |
| return setFlag(CLIP_TO_CLIP_BOUNDS, false, &mPrimitiveFields.mClippingFlags); | |
| } | |
| bool setProjectBackwards(bool shouldProject) { | |
| return RP_SET(mPrimitiveFields.mProjectBackwards, shouldProject); | |
| } | |
| bool setProjectionReceiver(bool shouldReceive) { | |
| return RP_SET(mPrimitiveFields.mProjectionReceiver, shouldReceive); | |
| } | |
| bool isProjectionReceiver() const { return mPrimitiveFields.mProjectionReceiver; } | |
| bool setClipMayBeComplex(bool isClipMayBeComplex) { | |
| return RP_SET(mPrimitiveFields.mClipMayBeComplex, isClipMayBeComplex); | |
| } | |
| bool isClipMayBeComplex() const { return mPrimitiveFields.mClipMayBeComplex; } | |
| bool setStaticMatrix(const SkMatrix* matrix) { | |
| delete mStaticMatrix; | |
| if (matrix) { | |
| mStaticMatrix = new SkMatrix(*matrix); | |
| } else { | |
| mStaticMatrix = nullptr; | |
| } | |
| return true; | |
| } | |
| // Can return NULL | |
| const SkMatrix* getStaticMatrix() const { return mStaticMatrix; } | |
| bool setAnimationMatrix(const SkMatrix* matrix) { | |
| delete mAnimationMatrix; | |
| if (matrix) { | |
| mAnimationMatrix = new SkMatrix(*matrix); | |
| } else { | |
| mAnimationMatrix = nullptr; | |
| } | |
| return true; | |
| } | |
| bool setAlpha(float alpha) { | |
| alpha = MathUtils::clampAlpha(alpha); | |
| return RP_SET(mPrimitiveFields.mAlpha, alpha); | |
| } | |
| float getAlpha() const { return mPrimitiveFields.mAlpha; } | |
| bool setHasOverlappingRendering(bool hasOverlappingRendering) { | |
| return RP_SET(mPrimitiveFields.mHasOverlappingRendering, hasOverlappingRendering); | |
| } | |
| bool hasOverlappingRendering() const { return mPrimitiveFields.mHasOverlappingRendering; } | |
| bool setElevation(float elevation) { | |
| return RP_SET(mPrimitiveFields.mElevation, elevation); | |
| // Don't dirty matrix/pivot, since they don't respect Z | |
| } | |
| float getElevation() const { return mPrimitiveFields.mElevation; } | |
| bool setTranslationX(float translationX) { | |
| return RP_SET_AND_DIRTY(mPrimitiveFields.mTranslationX, translationX); | |
| } | |
| float getTranslationX() const { return mPrimitiveFields.mTranslationX; } | |
| bool setTranslationY(float translationY) { | |
| return RP_SET_AND_DIRTY(mPrimitiveFields.mTranslationY, translationY); | |
| } | |
| float getTranslationY() const { return mPrimitiveFields.mTranslationY; } | |
| bool setTranslationZ(float translationZ) { | |
| return RP_SET(mPrimitiveFields.mTranslationZ, translationZ); | |
| // mMatrixOrPivotDirty not set, since matrix doesn't respect Z | |
| } | |
| float getTranslationZ() const { return mPrimitiveFields.mTranslationZ; } | |
| // Animation helper | |
| bool setX(float value) { return setTranslationX(value - getLeft()); } | |
| // Animation helper | |
| float getX() const { return getLeft() + getTranslationX(); } | |
| // Animation helper | |
| bool setY(float value) { return setTranslationY(value - getTop()); } | |
| // Animation helper | |
| float getY() const { return getTop() + getTranslationY(); } | |
| // Animation helper | |
| bool setZ(float value) { return setTranslationZ(value - getElevation()); } | |
| float getZ() const { return getElevation() + getTranslationZ(); } | |
| bool setRotation(float rotation) { | |
| return RP_SET_AND_DIRTY(mPrimitiveFields.mRotation, rotation); | |
| } | |
| float getRotation() const { return mPrimitiveFields.mRotation; } | |
| bool setRotationX(float rotationX) { | |
| return RP_SET_AND_DIRTY(mPrimitiveFields.mRotationX, rotationX); | |
| } | |
| float getRotationX() const { return mPrimitiveFields.mRotationX; } | |
| bool setRotationY(float rotationY) { | |
| return RP_SET_AND_DIRTY(mPrimitiveFields.mRotationY, rotationY); | |
| } | |
| float getRotationY() const { return mPrimitiveFields.mRotationY; } | |
| bool setScaleX(float scaleX) { return RP_SET_AND_DIRTY(mPrimitiveFields.mScaleX, scaleX); } | |
| float getScaleX() const { return mPrimitiveFields.mScaleX; } | |
| bool setScaleY(float scaleY) { return RP_SET_AND_DIRTY(mPrimitiveFields.mScaleY, scaleY); } | |
| float getScaleY() const { return mPrimitiveFields.mScaleY; } | |
| bool setPivotX(float pivotX) { | |
| if (RP_SET(mPrimitiveFields.mPivotX, pivotX) || !mPrimitiveFields.mPivotExplicitlySet) { | |
| mPrimitiveFields.mMatrixOrPivotDirty = true; | |
| mPrimitiveFields.mPivotExplicitlySet = true; | |
| return true; | |
| } | |
| return false; | |
| } | |
| /* Note that getPivotX and getPivotY are adjusted by updateMatrix(), | |
| * so the value returned may be stale if the RenderProperties has been | |
| * modified since the last call to updateMatrix() | |
| */ | |
| float getPivotX() const { return mPrimitiveFields.mPivotX; } | |
| bool setPivotY(float pivotY) { | |
| if (RP_SET(mPrimitiveFields.mPivotY, pivotY) || !mPrimitiveFields.mPivotExplicitlySet) { | |
| mPrimitiveFields.mMatrixOrPivotDirty = true; | |
| mPrimitiveFields.mPivotExplicitlySet = true; | |
| return true; | |
| } | |
| return false; | |
| } | |
| float getPivotY() const { return mPrimitiveFields.mPivotY; } | |
| bool isPivotExplicitlySet() const { return mPrimitiveFields.mPivotExplicitlySet; } | |
| bool resetPivot() { return RP_SET_AND_DIRTY(mPrimitiveFields.mPivotExplicitlySet, false); } | |
| bool setCameraDistance(float distance) { | |
| if (distance != getCameraDistance()) { | |
| mPrimitiveFields.mMatrixOrPivotDirty = true; | |
| mComputedFields.mTransformCamera.setCameraLocation(0, 0, distance); | |
| return true; | |
| } | |
| return false; | |
| } | |
| float getCameraDistance() const { | |
| // TODO: update getCameraLocationZ() to be const | |
| return const_cast<Sk3DView*>(&mComputedFields.mTransformCamera)->getCameraLocationZ(); | |
| } | |
| bool setLeft(int left) { | |
| if (RP_SET(mPrimitiveFields.mLeft, left)) { | |
| mPrimitiveFields.mWidth = mPrimitiveFields.mRight - mPrimitiveFields.mLeft; | |
| if (!mPrimitiveFields.mPivotExplicitlySet) { | |
| mPrimitiveFields.mMatrixOrPivotDirty = true; | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| int getLeft() const { return mPrimitiveFields.mLeft; } | |
| bool setTop(int top) { | |
| if (RP_SET(mPrimitiveFields.mTop, top)) { | |
| mPrimitiveFields.mHeight = mPrimitiveFields.mBottom - mPrimitiveFields.mTop; | |
| if (!mPrimitiveFields.mPivotExplicitlySet) { | |
| mPrimitiveFields.mMatrixOrPivotDirty = true; | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| int getTop() const { return mPrimitiveFields.mTop; } | |
| bool setRight(int right) { | |
| if (RP_SET(mPrimitiveFields.mRight, right)) { | |
| mPrimitiveFields.mWidth = mPrimitiveFields.mRight - mPrimitiveFields.mLeft; | |
| if (!mPrimitiveFields.mPivotExplicitlySet) { | |
| mPrimitiveFields.mMatrixOrPivotDirty = true; | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| int getRight() const { return mPrimitiveFields.mRight; } | |
| bool setBottom(int bottom) { | |
| if (RP_SET(mPrimitiveFields.mBottom, bottom)) { | |
| mPrimitiveFields.mHeight = mPrimitiveFields.mBottom - mPrimitiveFields.mTop; | |
| if (!mPrimitiveFields.mPivotExplicitlySet) { | |
| mPrimitiveFields.mMatrixOrPivotDirty = true; | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| int getBottom() const { return mPrimitiveFields.mBottom; } | |
| bool setLeftTop(int left, int top) { | |
| bool leftResult = setLeft(left); | |
| bool topResult = setTop(top); | |
| return leftResult || topResult; | |
| } | |
| bool setLeftTopRightBottom(int left, int top, int right, int bottom) { | |
| if (left != mPrimitiveFields.mLeft || top != mPrimitiveFields.mTop || | |
| right != mPrimitiveFields.mRight || bottom != mPrimitiveFields.mBottom) { | |
| mPrimitiveFields.mLeft = left; | |
| mPrimitiveFields.mTop = top; | |
| mPrimitiveFields.mRight = right; | |
| mPrimitiveFields.mBottom = bottom; | |
| mPrimitiveFields.mWidth = mPrimitiveFields.mRight - mPrimitiveFields.mLeft; | |
| mPrimitiveFields.mHeight = mPrimitiveFields.mBottom - mPrimitiveFields.mTop; | |
| if (!mPrimitiveFields.mPivotExplicitlySet) { | |
| mPrimitiveFields.mMatrixOrPivotDirty = true; | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool offsetLeftRight(int offset) { | |
| if (offset != 0) { | |
| mPrimitiveFields.mLeft += offset; | |
| mPrimitiveFields.mRight += offset; | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool offsetTopBottom(int offset) { | |
| if (offset != 0) { | |
| mPrimitiveFields.mTop += offset; | |
| mPrimitiveFields.mBottom += offset; | |
| return true; | |
| } | |
| return false; | |
| } | |
| int getWidth() const { return mPrimitiveFields.mWidth; } | |
| int getHeight() const { return mPrimitiveFields.mHeight; } | |
| const SkMatrix* getAnimationMatrix() const { return mAnimationMatrix; } | |
| bool hasTransformMatrix() const { | |
| return getTransformMatrix() && !getTransformMatrix()->isIdentity(); | |
| } | |
| // May only call this if hasTransformMatrix() is true | |
| bool isTransformTranslateOnly() const { | |
| return getTransformMatrix()->getType() == SkMatrix::kTranslate_Mask; | |
| } | |
| const SkMatrix* getTransformMatrix() const { | |
| LOG_ALWAYS_FATAL_IF(mPrimitiveFields.mMatrixOrPivotDirty, "Cannot get a dirty matrix!"); | |
| return mComputedFields.mTransformMatrix; | |
| } | |
| int getClippingFlags() const { return mPrimitiveFields.mClippingFlags; } | |
| bool getClipToBounds() const { return mPrimitiveFields.mClippingFlags & CLIP_TO_BOUNDS; } | |
| const Rect& getClipBounds() const { return mPrimitiveFields.mClipBounds; } | |
| void getClippingRectForFlags(uint32_t flags, Rect* outRect) const { | |
| if (flags & CLIP_TO_BOUNDS) { | |
| outRect->set(0, 0, getWidth(), getHeight()); | |
| if (flags & CLIP_TO_CLIP_BOUNDS) { | |
| outRect->doIntersect(mPrimitiveFields.mClipBounds); | |
| } | |
| } else { | |
| outRect->set(mPrimitiveFields.mClipBounds); | |
| } | |
| } | |
| bool getHasOverlappingRendering() const { return mPrimitiveFields.mHasOverlappingRendering; } | |
| const Outline& getOutline() const { return mPrimitiveFields.mOutline; } | |
| const RevealClip& getRevealClip() const { return mPrimitiveFields.mRevealClip; } | |
| bool getProjectBackwards() const { return mPrimitiveFields.mProjectBackwards; } | |
| void debugOutputProperties(std::ostream& output, const int level) const; | |
| void updateMatrix(); | |
| Outline& mutableOutline() { return mPrimitiveFields.mOutline; } | |
| RevealClip& mutableRevealClip() { return mPrimitiveFields.mRevealClip; } | |
| const LayerProperties& layerProperties() const { return mLayerProperties; } | |
| LayerProperties& mutateLayerProperties() { return mLayerProperties; } | |
| // Returns true if damage calculations should be clipped to bounds | |
| // TODO: Figure out something better for getZ(), as children should still be | |
| // clipped to this RP's bounds. But as we will damage -INT_MAX to INT_MAX | |
| // for this RP's getZ() anyway, this can be optimized when we have a | |
| // Z damage estimate instead of INT_MAX | |
| bool getClipDamageToBounds() const { | |
| return getClipToBounds() && (getZ() <= 0 || getOutline().isEmpty()); | |
| } | |
| bool hasShadow() const { | |
| return getZ() > 0.0f && getOutline().getPath() != nullptr && | |
| getOutline().getAlpha() != 0.0f; | |
| } | |
| SkColor getSpotShadowColor() const { return mPrimitiveFields.mSpotShadowColor; } | |
| bool setSpotShadowColor(SkColor shadowColor) { | |
| return RP_SET(mPrimitiveFields.mSpotShadowColor, shadowColor); | |
| } | |
| SkColor getAmbientShadowColor() const { return mPrimitiveFields.mAmbientShadowColor; } | |
| bool setAmbientShadowColor(SkColor shadowColor) { | |
| return RP_SET(mPrimitiveFields.mAmbientShadowColor, shadowColor); | |
| } | |
| bool fitsOnLayer() const { | |
| #ifdef __ANDROID__ // Layoutlib does not support device info | |
| const DeviceInfo* deviceInfo = DeviceInfo::get(); | |
| return mPrimitiveFields.mWidth <= deviceInfo->maxTextureSize() && | |
| mPrimitiveFields.mHeight <= deviceInfo->maxTextureSize(); | |
| #else | |
| return mPrimitiveFields.mWidth <= 4096 && mPrimitiveFields.mHeight <= 4096; | |
| #endif | |
| } | |
| bool promotedToLayer() const { | |
| return mLayerProperties.mType == LayerType::None && fitsOnLayer() && | |
| (mComputedFields.mNeedLayerForFunctors || mLayerProperties.mImageFilter != nullptr || | |
| mLayerProperties.getStretchEffect().requiresLayer() || | |
| (!MathUtils::isZero(mPrimitiveFields.mAlpha) && mPrimitiveFields.mAlpha < 1 && | |
| mPrimitiveFields.mHasOverlappingRendering)); | |
| } | |
| LayerType effectiveLayerType() const { | |
| return CC_UNLIKELY(promotedToLayer()) ? LayerType::RenderLayer : mLayerProperties.mType; | |
| } | |
| bool setAllowForceDark(bool allow) { | |
| return RP_SET(mPrimitiveFields.mAllowForceDark, allow); | |
| } | |
| bool getAllowForceDark() const { | |
| return mPrimitiveFields.mAllowForceDark; | |
| } | |
| private: | |
| // Rendering properties | |
| struct PrimitiveFields { | |
| int mLeft = 0, mTop = 0, mRight = 0, mBottom = 0; | |
| int mWidth = 0, mHeight = 0; | |
| int mClippingFlags = CLIP_TO_BOUNDS; | |
| SkColor mSpotShadowColor = SK_ColorBLACK; | |
| SkColor mAmbientShadowColor = SK_ColorBLACK; | |
| float mAlpha = 1; | |
| float mTranslationX = 0, mTranslationY = 0, mTranslationZ = 0; | |
| float mElevation = 0; | |
| float mRotation = 0, mRotationX = 0, mRotationY = 0; | |
| float mScaleX = 1, mScaleY = 1; | |
| float mPivotX = 0, mPivotY = 0; | |
| bool mHasOverlappingRendering = false; | |
| bool mPivotExplicitlySet = false; | |
| bool mMatrixOrPivotDirty = false; | |
| bool mProjectBackwards = false; | |
| bool mProjectionReceiver = false; | |
| bool mAllowForceDark = true; | |
| bool mClipMayBeComplex = false; | |
| Rect mClipBounds; | |
| Outline mOutline; | |
| RevealClip mRevealClip; | |
| } mPrimitiveFields; | |
| SkMatrix* mStaticMatrix; | |
| SkMatrix* mAnimationMatrix; | |
| LayerProperties mLayerProperties; | |
| /** | |
| * These fields are all generated from other properties and are not set directly. | |
| */ | |
| struct ComputedFields { | |
| ComputedFields(); | |
| ~ComputedFields(); | |
| /** | |
| * Stores the total transformation of the DisplayList based upon its scalar | |
| * translate/rotate/scale properties. | |
| * | |
| * In the common translation-only case, the matrix isn't necessarily allocated, | |
| * and the mTranslation properties are used directly. | |
| */ | |
| SkMatrix* mTransformMatrix; | |
| Sk3DView mTransformCamera; | |
| // Force layer on for functors to enable render features they don't yet support (clipping) | |
| bool mNeedLayerForFunctors = false; | |
| } mComputedFields; | |
| }; | |
| } /* namespace uirenderer */ | |
| } /* namespace android */ |