blob: 520b0b4edb698d75eaa0059dc2dd96572739cd2b [file] [log] [blame]
/*
* Copyright 2023 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.
*/
package androidx.wear.protolayout.renderer.helper;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;
import androidx.annotation.Nullable;
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicString;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedString;
import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignment;
import androidx.wear.protolayout.proto.AlignmentProto.HorizontalAlignmentProp;
import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignment;
import androidx.wear.protolayout.proto.AlignmentProto.VerticalAlignmentProp;
import androidx.wear.protolayout.proto.ColorProto.ColorProp;
import androidx.wear.protolayout.proto.DimensionProto.ContainerDimension;
import androidx.wear.protolayout.proto.DimensionProto.DegreesProp;
import androidx.wear.protolayout.proto.DimensionProto.DpProp;
import androidx.wear.protolayout.proto.DimensionProto.ImageDimension;
import androidx.wear.protolayout.proto.DimensionProto.SpProp;
import androidx.wear.protolayout.proto.DimensionProto.SpacerDimension;
import androidx.wear.protolayout.proto.FingerprintProto.NodeFingerprint;
import androidx.wear.protolayout.proto.FingerprintProto.TreeFingerprint;
import androidx.wear.protolayout.proto.LayoutElementProto;
import androidx.wear.protolayout.proto.LayoutElementProto.Arc;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcAdapter;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.ArcText;
import androidx.wear.protolayout.proto.LayoutElementProto.Box;
import androidx.wear.protolayout.proto.LayoutElementProto.Column;
import androidx.wear.protolayout.proto.LayoutElementProto.Image;
import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement;
import androidx.wear.protolayout.proto.LayoutElementProto.Row;
import androidx.wear.protolayout.proto.LayoutElementProto.Spacer;
import androidx.wear.protolayout.proto.LayoutElementProto.Span;
import androidx.wear.protolayout.proto.LayoutElementProto.SpanText;
import androidx.wear.protolayout.proto.LayoutElementProto.Spannable;
import androidx.wear.protolayout.proto.LayoutElementProto.Text;
import androidx.wear.protolayout.proto.ModifiersProto;
import androidx.wear.protolayout.proto.TypesProto.BoolProp;
import androidx.wear.protolayout.proto.TypesProto.Int32Prop;
import androidx.wear.protolayout.proto.TypesProto.StringProp;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* A simple DSL for more easily producing layout protos in tests.
*
* <p>Example usage:
*
* <pre>{@code
* layout(
* column(
* props -> {
* props.heightDp = 20;
* props.modifiers.border.widthDp = 2;
* },
* row(
* text("Foo"),
* text("Bar")
* )
* )
* )
* }</pre>
*/
public class TestDsl {
private TestDsl() {}
/** An intermediate opaque layout node produced by the builders in this class. */
public static final class LayoutNode {
private LayoutElement.Builder mLayoutElement;
private ArcLayoutElement.Builder mArcLayoutElement;
private Span.Builder mSpanElement;
private NodeFingerprint mFingerprint;
}
/** Corresponds to {@link ModifiersProto.Border} */
public static final class Border {
public int widthDp;
public int colorArgb;
private ModifiersProto.Border toProto() {
return ModifiersProto.Border.newBuilder()
.setWidth(DpProp.newBuilder().setValue(widthDp))
.setColor(ColorProp.newBuilder().setArgb(colorArgb))
.build();
}
}
/** Corresponds to {@link ModifiersProto.Modifiers} */
public static final class Modifiers {
public Border border = new Border();
private ModifiersProto.Modifiers toProto() {
ModifiersProto.Modifiers.Builder proto = ModifiersProto.Modifiers.newBuilder();
proto.setBorder(border.toProto());
return proto.build();
}
}
/** Corresponds to {@link LayoutElementProto.FontStyle} */
public static final class FontStyle {
public float sizeSp;
public boolean italic;
public int colorArgb;
private LayoutElementProto.FontStyle toProto() {
return LayoutElementProto.FontStyle.newBuilder()
.addSize(sp(sizeSp))
.setItalic(bool(italic))
.setColor(color(colorArgb))
.build();
}
}
/** Properties of a Box, with each field directly accessible for ease of use. */
public static final class BoxProps {
public Modifiers modifiers = new Modifiers();
public int widthDp;
public int heightDp;
public HorizontalAlignment horizontalAlignment;
public VerticalAlignment verticalAlignment;
private void applyTo(Box.Builder box) {
box.setModifiers(modifiers.toProto());
box.setWidth(dpContainerDim(widthDp));
box.setHeight(dpContainerDim(heightDp));
box.setHorizontalAlignment(
HorizontalAlignmentProp.newBuilder().setValue(horizontalAlignment).build());
box.setVerticalAlignment(
VerticalAlignmentProp.newBuilder().setValue(verticalAlignment).build());
}
private int fingerprint() {
return Objects.hash(
modifiers.toProto(), widthDp, heightDp, horizontalAlignment, verticalAlignment);
}
}
/** Properties of a Row, with each field directly accessible for ease of use. */
public static final class RowProps {
public Modifiers modifiers = new Modifiers();
public int widthDp;
public int heightDp;
private void applyTo(Row.Builder row) {
row.setModifiers(modifiers.toProto());
row.setWidth(dpContainerDim(widthDp));
row.setHeight(dpContainerDim(heightDp));
}
private int fingerprint() {
return Objects.hash(modifiers.toProto(), widthDp, heightDp);
}
}
/** Properties of a Column, with each field directly accessible for ease of use. */
public static final class ColumnProps {
public Modifiers modifiers = new Modifiers();
public int widthDp;
public int heightDp;
private void applyTo(Column.Builder column) {
column.setModifiers(modifiers.toProto());
column.setWidth(dpContainerDim(widthDp));
column.setHeight(dpContainerDim(heightDp));
}
private int fingerprint() {
return Objects.hash(modifiers.toProto(), widthDp, heightDp);
}
}
/** Properties of a Text object, with each field directly accessible for ease of use. */
public static final class TextProps {
public Modifiers modifiers = new Modifiers();
public int maxLines = 1;
public float lineHeightSp = 1;
public FontStyle fontStyle = new FontStyle();
private void applyTo(Text.Builder text) {
text.setModifiers(modifiers.toProto());
text.setMaxLines(int32(maxLines));
text.setLineHeight(sp(lineHeightSp));
text.setFontStyle(fontStyle.toProto());
}
private int fingerprint() {
return Objects.hash(modifiers.toProto(), maxLines, fontStyle.toProto());
}
}
/** Properties of a Image object, with each field directly accessible for ease of use. */
public static final class ImageProps {
public Modifiers modifiers = new Modifiers();
public int widthDp;
public int heightDp;
private void applyTo(Image.Builder image) {
image.setModifiers(modifiers.toProto());
image.setWidth(dpImageDim(widthDp));
image.setHeight(dpImageDim(heightDp));
}
private int fingerprint() {
return Objects.hash(modifiers.toProto(), widthDp, heightDp);
}
}
/** Properties of a Spacer object, with each field directly accessible for ease of use. */
public static final class SpacerProps {
public Modifiers modifiers = new Modifiers();
public int widthDp;
public int heightDp;
private void applyTo(Spacer.Builder spacer) {
spacer.setModifiers(modifiers.toProto());
spacer.setWidth(dpSpacerDim(widthDp));
spacer.setHeight(dpSpacerDim(heightDp));
}
private int fingerprint() {
return Objects.hash(modifiers.toProto(), widthDp, heightDp);
}
}
/** Properties of an Arc, with each field directly accessible for ease of use. */
public static final class ArcProps {
public float anchorAngleDegrees;
private void applyTo(Arc.Builder arc) {
arc.setAnchorAngle(degrees(anchorAngleDegrees));
}
private int fingerprint() {
return Float.hashCode(anchorAngleDegrees);
}
}
/** Properties of an Spannable, with each field directly accessible for ease of use. */
public static final class SpannableProps {
public int maxLines;
private void applyTo(Spannable.Builder spannable) {
spannable.setMaxLines(int32(maxLines));
}
private int fingerprint() {
return Float.hashCode(maxLines);
}
}
public static Layout layout(LayoutNode root) {
return Layout.newBuilder()
.setRoot(root.mLayoutElement)
.setFingerprint(TreeFingerprint.newBuilder().setRoot(root.mFingerprint))
.build();
}
public static LayoutNode box(Consumer<BoxProps> propsConsumer, LayoutNode... nodes) {
return boxInternal(propsConsumer, nodes);
}
public static LayoutNode box(LayoutNode... nodes) {
return boxInternal(/* propsConsumer= */ null, nodes);
}
private static LayoutNode boxInternal(
@Nullable Consumer<BoxProps> propsConsumer, LayoutNode... nodes) {
LayoutNode element = new LayoutNode();
Box.Builder builder = Box.newBuilder().addAllContents(linearContents(nodes));
int selfPropsFingerprint = 0;
if (propsConsumer != null) {
BoxProps props = new BoxProps();
propsConsumer.accept(props);
props.applyTo(builder);
selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
}
element.mLayoutElement = LayoutElement.newBuilder().setBox(builder.build());
element.mFingerprint = fingerprint("box", selfPropsFingerprint, nodes);
return element;
}
public static LayoutNode row(Consumer<RowProps> propsConsumer, LayoutNode... nodes) {
return rowInternal(propsConsumer, nodes);
}
public static LayoutNode row(LayoutNode... nodes) {
return rowInternal(/* propsConsumer= */ null, nodes);
}
private static LayoutNode rowInternal(
@Nullable Consumer<RowProps> propsConsumer, LayoutNode... nodes) {
LayoutNode element = new LayoutNode();
Row.Builder builder = Row.newBuilder().addAllContents(linearContents(nodes));
int selfPropsFingerprint = 0;
if (propsConsumer != null) {
RowProps props = new RowProps();
propsConsumer.accept(props);
props.applyTo(builder);
selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
}
element.mLayoutElement = LayoutElement.newBuilder().setRow(builder.build());
element.mFingerprint = fingerprint("row", selfPropsFingerprint, nodes);
return element;
}
public static LayoutNode column(Consumer<ColumnProps> propsConsumer, LayoutNode... nodes) {
return columnInternal(propsConsumer, nodes);
}
public static LayoutNode column(LayoutNode... nodes) {
return columnInternal(/* propsConsumer= */ null, nodes);
}
private static LayoutNode columnInternal(
@Nullable Consumer<ColumnProps> propsConsumer, LayoutNode... nodes) {
LayoutNode element = new LayoutNode();
Column.Builder builder = Column.newBuilder().addAllContents(linearContents(nodes));
int selfPropsFingerprint = 0;
if (propsConsumer != null) {
ColumnProps props = new ColumnProps();
propsConsumer.accept(props);
props.applyTo(builder);
selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
}
element.mLayoutElement = LayoutElement.newBuilder().setColumn(builder.build());
element.mFingerprint = fingerprint("column", selfPropsFingerprint, nodes);
return element;
}
public static LayoutNode dynamicFixedText(String fixedText) {
return dynamicFixedText(/* propsConsumer= */ null, fixedText, /* valueForLayout= */ null);
}
public static LayoutNode dynamicFixedText(String fixedText, @Nullable String valueForLayout) {
return dynamicFixedText(/* propsConsumer= */ null, fixedText, valueForLayout);
}
public static LayoutNode dynamicFixedText(Consumer<TextProps> propsConsumer, String fixedText) {
return textInternal(propsConsumer, dynamicStr(fixedText, /* valueForLayout= */ null));
}
public static LayoutNode dynamicFixedText(
Consumer<TextProps> propsConsumer, String fixedText, @Nullable String valueForLayout) {
return textInternal(propsConsumer, dynamicStr(fixedText, valueForLayout));
}
public static LayoutNode text(String text) {
return text(/* propsConsumer= */ null, text);
}
public static LayoutNode text(Consumer<TextProps> propsConsumer, String text) {
return textInternal(propsConsumer, str(text));
}
private static LayoutNode textInternal(
@Nullable Consumer<TextProps> propsConsumer, StringProp text) {
LayoutNode element = new LayoutNode();
Text.Builder builder = Text.newBuilder().setText(text);
int selfPropsFingerprint = text.hashCode();
if (propsConsumer != null) {
TextProps props = new TextProps();
propsConsumer.accept(props);
props.applyTo(builder);
selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
}
element.mLayoutElement = LayoutElement.newBuilder().setText(builder.build());
element.mFingerprint = fingerprint("text", selfPropsFingerprint);
return element;
}
public static LayoutNode image(Consumer<ImageProps> propsConsumer, String resourceId) {
return imageInternal(propsConsumer, resourceId);
}
public static LayoutNode image(String resourceId) {
return imageInternal(/* propsConsumer= */ null, resourceId);
}
private static LayoutNode imageInternal(
@Nullable Consumer<ImageProps> propsConsumer, String resourceId) {
LayoutNode element = new LayoutNode();
Image.Builder builder = Image.newBuilder().setResourceId(str(resourceId));
int selfPropsFingerprint = resourceId.hashCode();
if (propsConsumer != null) {
ImageProps props = new ImageProps();
propsConsumer.accept(props);
props.applyTo(builder);
selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
}
element.mLayoutElement = LayoutElement.newBuilder().setImage(builder.build());
element.mFingerprint = fingerprint("image", selfPropsFingerprint);
return element;
}
public static LayoutNode spacer(Consumer<SpacerProps> propsConsumer) {
return spacerInternal(propsConsumer);
}
public static LayoutNode spacer() {
return spacerInternal(/* propsConsumer= */ null);
}
private static LayoutNode spacerInternal(@Nullable Consumer<SpacerProps> propsConsumer) {
LayoutNode element = new LayoutNode();
Spacer.Builder builder = Spacer.newBuilder();
int selfPropsFingerprint = 0;
if (propsConsumer != null) {
SpacerProps props = new SpacerProps();
propsConsumer.accept(props);
props.applyTo(builder);
selfPropsFingerprint = props.fingerprint();
}
element.mLayoutElement = LayoutElement.newBuilder().setSpacer(builder.build());
element.mFingerprint = fingerprint("spacer", selfPropsFingerprint);
return element;
}
public static LayoutNode arc(Consumer<ArcProps> propsConsumer, LayoutNode... nodes) {
return arcInternal(propsConsumer, nodes);
}
public static LayoutNode arc(LayoutNode... nodes) {
return arcInternal(/* propsConsumer= */ null, nodes);
}
public static LayoutNode arcAdapter(LayoutNode layoutNode) {
return arcAdapterInternal(layoutNode);
}
private static LayoutNode arcInternal(
@Nullable Consumer<ArcProps> propsConsumer, LayoutNode... nodes) {
LayoutNode element = new LayoutNode();
Arc.Builder builder = Arc.newBuilder().addAllContents(radialContents(nodes));
int selfPropsFingerprint = 0;
if (propsConsumer != null) {
ArcProps props = new ArcProps();
propsConsumer.accept(props);
props.applyTo(builder);
selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
}
element.mLayoutElement = LayoutElement.newBuilder().setArc(builder.build());
element.mFingerprint = fingerprint("arc", selfPropsFingerprint, nodes);
return element;
}
private static LayoutNode arcAdapterInternal(LayoutNode node) {
LayoutNode element = new LayoutNode();
ArcAdapter.Builder builder = ArcAdapter.newBuilder().setContent(node.mLayoutElement);
int selfPropsFingerprint = 0;
element.mArcLayoutElement = ArcLayoutElement.newBuilder().setAdapter(builder.build());
element.mFingerprint = fingerprint("arcAdapter", selfPropsFingerprint, node);
return element;
}
public static LayoutNode arcText(String text) {
LayoutNode element = new LayoutNode();
element.mArcLayoutElement =
ArcLayoutElement.newBuilder().setText(ArcText.newBuilder().setText(str(text)));
element.mFingerprint = fingerprint("arcText", text.hashCode());
return element;
}
public static LayoutNode spannable(
Consumer<SpannableProps> propsConsumer, LayoutNode... nodes) {
return spannableInternal(propsConsumer, nodes);
}
public static LayoutNode spannable(LayoutNode... nodes) {
return spannableInternal(/* propsConsumer= */ null, nodes);
}
private static LayoutNode spannableInternal(
@Nullable Consumer<SpannableProps> propsConsumer, LayoutNode... nodes) {
LayoutNode element = new LayoutNode();
Spannable.Builder builder = Spannable.newBuilder().addAllSpans(spanContents(nodes));
int selfPropsFingerprint = 0;
if (propsConsumer != null) {
SpannableProps props = new SpannableProps();
propsConsumer.accept(props);
props.applyTo(builder);
selfPropsFingerprint = combine(selfPropsFingerprint, props.fingerprint());
}
// A Spannable is *not* considered a container for diffing purposes (i.e. any updated
// children will cause the entire Spannable to be updated). So we include the fingerprint of
// the nodes in the Spannable's self fingerprint. This mirrors behaviour from
// Spannable::Builder::addSpan at http://shortn/_cUyrG0M1N2
selfPropsFingerprint = combine(selfPropsFingerprint, fingerprints(nodes).hashCode());
element.mLayoutElement = LayoutElement.newBuilder().setSpannable(builder.build());
element.mFingerprint = fingerprint("spannable", selfPropsFingerprint);
return element;
}
public static LayoutNode spanText(String text) {
LayoutNode element = new LayoutNode();
element.mSpanElement = Span.newBuilder().setText(SpanText.newBuilder().setText(str(text)));
element.mFingerprint = fingerprint("spanText", text.hashCode());
return element;
}
private static NodeFingerprint fingerprint(
String selfTypeName, int selfPropsValue, LayoutNode... nodes) {
return NodeFingerprint.newBuilder()
.setSelfTypeValue(selfTypeName.hashCode())
.setSelfPropsValue(selfPropsValue)
.setChildNodesValue(fingerprints(nodes).hashCode())
.addAllChildNodes(fingerprints(nodes))
.build();
}
private static int combine(int fingerprint1, int fingerprint2) {
return 31 * fingerprint1 + fingerprint2;
}
private static List<LayoutElement> linearContents(LayoutNode[] nodes) {
return stream(nodes).map(n -> n.mLayoutElement.build()).collect(toList());
}
private static List<NodeFingerprint> fingerprints(LayoutNode[] nodes) {
return stream(nodes).map(n -> n.mFingerprint).collect(toList());
}
private static List<ArcLayoutElement> radialContents(LayoutNode[] nodes) {
return stream(nodes).map(n -> n.mArcLayoutElement.build()).collect(toList());
}
private static List<Span> spanContents(LayoutNode[] nodes) {
return stream(nodes).map(n -> n.mSpanElement.build()).collect(toList());
}
private static ContainerDimension dpContainerDim(float dp) {
return ContainerDimension.newBuilder().setLinearDimension(dp(dp)).build();
}
private static ImageDimension dpImageDim(float dp) {
return ImageDimension.newBuilder().setLinearDimension(dp(dp)).build();
}
private static SpacerDimension dpSpacerDim(float dp) {
return SpacerDimension.newBuilder().setLinearDimension(dp(dp)).build();
}
private static ColorProp color(int value) {
return ColorProp.newBuilder().setArgb(value).build();
}
private static BoolProp bool(boolean value) {
return BoolProp.newBuilder().setValue(value).build();
}
private static DpProp dp(float value) {
return DpProp.newBuilder().setValue(value).build();
}
private static SpProp sp(float value) {
return SpProp.newBuilder().setValue(value).build();
}
private static DegreesProp degrees(float degrees) {
return DegreesProp.newBuilder().setValue(degrees).build();
}
private static Int32Prop int32(int value) {
return Int32Prop.newBuilder().setValue(value).build();
}
private static StringProp str(String value) {
return StringProp.newBuilder().setValue(value).build();
}
private static StringProp dynamicStr(String fixedValue, @Nullable String valueForLayout) {
StringProp.Builder builder =
StringProp.newBuilder()
.setDynamicValue(
DynamicString.newBuilder()
.setFixed(FixedString.newBuilder().setValue(fixedValue)));
if (valueForLayout != null) {
builder.setValueForLayout(valueForLayout);
}
return builder.build();
}
}