• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.internal.widget.remotecompose.core.operations.layout.managers;
17 
18 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.FLOAT;
19 import static com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation.INT;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 
24 import com.android.internal.widget.remotecompose.core.Operation;
25 import com.android.internal.widget.remotecompose.core.Operations;
26 import com.android.internal.widget.remotecompose.core.PaintContext;
27 import com.android.internal.widget.remotecompose.core.Platform;
28 import com.android.internal.widget.remotecompose.core.RemoteContext;
29 import com.android.internal.widget.remotecompose.core.VariableSupport;
30 import com.android.internal.widget.remotecompose.core.WireBuffer;
31 import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder;
32 import com.android.internal.widget.remotecompose.core.operations.Utils;
33 import com.android.internal.widget.remotecompose.core.operations.layout.Component;
34 import com.android.internal.widget.remotecompose.core.operations.layout.measure.ComponentMeasure;
35 import com.android.internal.widget.remotecompose.core.operations.layout.measure.MeasurePass;
36 import com.android.internal.widget.remotecompose.core.operations.layout.measure.Size;
37 import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
38 import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer;
39 import com.android.internal.widget.remotecompose.core.semantics.AccessibleComponent;
40 import com.android.internal.widget.remotecompose.core.serialize.MapSerializer;
41 
42 import java.util.List;
43 
44 /** Text component, referencing a text id */
45 public class TextLayout extends LayoutManager implements VariableSupport, AccessibleComponent {
46 
47     public static final int TEXT_ALIGN_LEFT = 1;
48     public static final int TEXT_ALIGN_RIGHT = 2;
49     public static final int TEXT_ALIGN_CENTER = 3;
50     public static final int TEXT_ALIGN_JUSTIFY = 4;
51     public static final int TEXT_ALIGN_START = 5;
52     public static final int TEXT_ALIGN_END = 6;
53 
54     public static final int OVERFLOW_CLIP = 1;
55     public static final int OVERFLOW_VISIBLE = 2;
56     public static final int OVERFLOW_ELLIPSIS = 3;
57     public static final int OVERFLOW_START_ELLIPSIS = 4;
58     public static final int OVERFLOW_MIDDLE_ELLIPSIS = 5;
59 
60     private static final boolean DEBUG = false;
61     private int mTextId = -1;
62     private int mColor = 0;
63     private float mFontSize = 16f;
64     private int mFontStyle = 0;
65     private float mFontWeight = 400f;
66     private int mFontFamilyId = -1;
67     private int mTextAlign = -1;
68     private int mOverflow = 1;
69     private int mMaxLines = Integer.MAX_VALUE;
70 
71     private int mType = -1;
72     private float mTextX;
73     private float mTextY;
74     private float mTextW = -1;
75     private float mTextH = -1;
76 
77     private final Size mCachedSize = new Size(0f, 0f);
78 
79     @Nullable private String mCachedString = "";
80     @Nullable private String mNewString;
81 
82     Platform.ComputedTextLayout mComputedTextLayout;
83 
84     @Nullable
85     @Override
getTextId()86     public Integer getTextId() {
87         return mTextId;
88     }
89 
90     @Override
registerListening(@onNull RemoteContext context)91     public void registerListening(@NonNull RemoteContext context) {
92         if (mTextId != -1) {
93             context.listensTo(mTextId, this);
94         }
95     }
96 
97     @Override
updateVariables(@onNull RemoteContext context)98     public void updateVariables(@NonNull RemoteContext context) {
99         String cachedString = context.getText(mTextId);
100         if (cachedString != null && cachedString.equalsIgnoreCase(mCachedString)) {
101             return;
102         }
103         mNewString = cachedString;
104         if (mType == -1) {
105             if (mFontFamilyId != -1) {
106                 String fontFamily = context.getText(mFontFamilyId);
107                 if (fontFamily != null) {
108                     mType = 0; // default
109                     if (fontFamily.equalsIgnoreCase("default")) {
110                         mType = 0;
111                     } else if (fontFamily.equalsIgnoreCase("sans-serif")) {
112                         mType = 1;
113                     } else if (fontFamily.equalsIgnoreCase("serif")) {
114                         mType = 2;
115                     } else if (fontFamily.equalsIgnoreCase("monospace")) {
116                         mType = 3;
117                     }
118                 }
119             } else {
120                 mType = 0;
121             }
122         }
123 
124         if (mHorizontalScrollDelegate != null) {
125             mHorizontalScrollDelegate.reset();
126         }
127         if (mVerticalScrollDelegate != null) {
128             mVerticalScrollDelegate.reset();
129         }
130         invalidateMeasure();
131     }
132 
TextLayout( @ullable Component parent, int componentId, int animationId, float x, float y, float width, float height, int textId, int color, float fontSize, int fontStyle, float fontWeight, int fontFamilyId, int textAlign, int overflow, int maxLines)133     public TextLayout(
134             @Nullable Component parent,
135             int componentId,
136             int animationId,
137             float x,
138             float y,
139             float width,
140             float height,
141             int textId,
142             int color,
143             float fontSize,
144             int fontStyle,
145             float fontWeight,
146             int fontFamilyId,
147             int textAlign,
148             int overflow,
149             int maxLines) {
150         super(parent, componentId, animationId, x, y, width, height);
151         mTextId = textId;
152         mColor = color;
153         mFontSize = fontSize;
154         mFontStyle = fontStyle;
155         mFontWeight = fontWeight;
156         mFontFamilyId = fontFamilyId;
157         mTextAlign = textAlign;
158         mOverflow = overflow;
159         mMaxLines = maxLines;
160     }
161 
TextLayout( @ullable Component parent, int componentId, int animationId, int textId, int color, float fontSize, int fontStyle, float fontWeight, int fontFamilyId, int textAlign, int overflow, int maxLines)162     public TextLayout(
163             @Nullable Component parent,
164             int componentId,
165             int animationId,
166             int textId,
167             int color,
168             float fontSize,
169             int fontStyle,
170             float fontWeight,
171             int fontFamilyId,
172             int textAlign,
173             int overflow,
174             int maxLines) {
175         this(
176                 parent,
177                 componentId,
178                 animationId,
179                 0,
180                 0,
181                 0,
182                 0,
183                 textId,
184                 color,
185                 fontSize,
186                 fontStyle,
187                 fontWeight,
188                 fontFamilyId,
189                 textAlign,
190                 overflow,
191                 maxLines);
192     }
193 
194     @NonNull public PaintBundle mPaint = new PaintBundle();
195 
196     @Override
paintingComponent(@onNull PaintContext context)197     public void paintingComponent(@NonNull PaintContext context) {
198         Component prev = context.getContext().mLastComponent;
199         RemoteContext remoteContext = context.getContext();
200         remoteContext.mLastComponent = this;
201 
202         context.save();
203         context.translate(mX, mY);
204         if (mGraphicsLayerModifier != null) {
205             context.startGraphicsLayer((int) getWidth(), (int) getHeight());
206             mCachedAttributes.clear();
207             mGraphicsLayerModifier.fillInAttributes(mCachedAttributes);
208             context.setGraphicsLayer(mCachedAttributes);
209         }
210         mComponentModifiers.paint(context);
211         float tx = mPaddingLeft;
212         float ty = mPaddingTop;
213         context.translate(tx, ty);
214 
215         //////////////////////////////////////////////////////////
216         // Text content
217         //////////////////////////////////////////////////////////
218         context.savePaint();
219         mPaint.reset();
220         mPaint.setStyle(PaintBundle.STYLE_FILL);
221         mPaint.setColor(mColor);
222         mPaint.setTextSize(mFontSize);
223         mPaint.setTextStyle(mType, (int) mFontWeight, mFontStyle == 1);
224         context.replacePaint(mPaint);
225         if (mCachedString == null) {
226             return;
227         }
228         int length = mCachedString.length();
229         if (mComputedTextLayout != null) {
230             context.drawComplexText(mComputedTextLayout);
231         } else {
232             float px = mTextX;
233             switch (mTextAlign) {
234                 case TEXT_ALIGN_CENTER:
235                     px = (mWidth - mPaddingLeft - mPaddingRight - mTextW) / 2f;
236                     break;
237                 case TEXT_ALIGN_RIGHT:
238                 case TEXT_ALIGN_END:
239                     px = (mWidth - mPaddingLeft - mPaddingRight - mTextW);
240                     break;
241                 case TEXT_ALIGN_LEFT:
242                 case TEXT_ALIGN_START:
243                 default:
244             }
245 
246             if (mTextW > (mWidth - mPaddingLeft - mPaddingRight)) {
247                 context.save();
248                 context.clipRect(
249                         0f,
250                         0f,
251                         mWidth - mPaddingLeft - mPaddingRight,
252                         mHeight - mPaddingTop - mPaddingBottom);
253                 context.translate(getScrollX(), getScrollY());
254                 context.drawTextRun(mTextId, 0, length, 0, 0, px, mTextY, false);
255                 context.restore();
256             } else {
257                 context.drawTextRun(mTextId, 0, length, 0, 0, px, mTextY, false);
258             }
259         }
260         if (DEBUG) {
261             mPaint.setStyle(PaintBundle.STYLE_FILL_AND_STROKE);
262             mPaint.setColor(1f, 1F, 1F, 1F);
263             mPaint.setStrokeWidth(3f);
264             context.applyPaint(mPaint);
265             context.drawLine(0f, 0f, mWidth, mHeight);
266             context.drawLine(0f, mHeight, mWidth, 0f);
267             mPaint.setColor(1f, 0F, 0F, 1F);
268             mPaint.setStrokeWidth(1f);
269             context.applyPaint(mPaint);
270             context.drawLine(0f, 0f, mWidth, mHeight);
271             context.drawLine(0f, mHeight, mWidth, 0f);
272         }
273         context.restorePaint();
274         //////////////////////////////////////////////////////////
275 
276         if (mGraphicsLayerModifier != null) {
277             context.endGraphicsLayer();
278         }
279 
280         context.translate(-tx, -ty);
281         context.restore();
282         context.getContext().mLastComponent = prev;
283     }
284 
285     @NonNull
286     @Override
toString()287     public String toString() {
288         return "TEXT_LAYOUT ["
289                 + mComponentId
290                 + ":"
291                 + mAnimationId
292                 + "] ("
293                 + mX
294                 + ", "
295                 + mY
296                 + " - "
297                 + mWidth
298                 + " x "
299                 + mHeight
300                 + ") "
301                 + Visibility.toString(mVisibility);
302     }
303 
304     @NonNull
305     @Override
getSerializedName()306     protected String getSerializedName() {
307         return "TEXT_LAYOUT";
308     }
309 
310     @Override
serializeToString(int indent, @NonNull StringSerializer serializer)311     public void serializeToString(int indent, @NonNull StringSerializer serializer) {
312         serializer.append(
313                 indent,
314                 getSerializedName()
315                         + " ["
316                         + mComponentId
317                         + ":"
318                         + mAnimationId
319                         + "] = "
320                         + "["
321                         + mX
322                         + ", "
323                         + mY
324                         + ", "
325                         + mWidth
326                         + ", "
327                         + mHeight
328                         + "] "
329                         + Visibility.toString(mVisibility)
330                         + " ("
331                         + mTextId
332                         + ":\""
333                         + mCachedString
334                         + "\")");
335     }
336 
337     @Override
computeSize( @onNull PaintContext context, float minWidth, float maxWidth, float minHeight, float maxHeight, @NonNull MeasurePass measure)338     public void computeSize(
339             @NonNull PaintContext context,
340             float minWidth,
341             float maxWidth,
342             float minHeight,
343             float maxHeight,
344             @NonNull MeasurePass measure) {
345         super.computeSize(context, minWidth, maxWidth, minHeight, maxHeight, measure);
346         computeWrapSize(context, maxWidth, maxHeight, true, true, measure, mCachedSize);
347         ComponentMeasure m = measure.get(this);
348         m.setW(mCachedSize.getWidth());
349         m.setH(mCachedSize.getHeight());
350     }
351 
352     @Override
computeWrapSize( @onNull PaintContext context, float maxWidth, float maxHeight, boolean horizontalWrap, boolean verticalWrap, @NonNull MeasurePass measure, @NonNull Size size)353     public void computeWrapSize(
354             @NonNull PaintContext context,
355             float maxWidth,
356             float maxHeight,
357             boolean horizontalWrap,
358             boolean verticalWrap,
359             @NonNull MeasurePass measure,
360             @NonNull Size size) {
361         context.savePaint();
362         mPaint.reset();
363         mPaint.setTextSize(mFontSize);
364         mPaint.setTextStyle(mType, (int) mFontWeight, mFontStyle == 1);
365         mPaint.setColor(mColor);
366         context.replacePaint(mPaint);
367         float[] bounds = new float[4];
368         if (mNewString != null && !mNewString.equals(mCachedString)) {
369             mCachedString = mNewString;
370         }
371         if (mCachedString == null) {
372             return;
373         }
374 
375         boolean forceComplex = false;
376         int flags = PaintContext.TEXT_MEASURE_FONT_HEIGHT | PaintContext.TEXT_MEASURE_SPACES;
377         if (mMaxLines == 1
378                 && (mOverflow == OVERFLOW_START_ELLIPSIS
379                         || mOverflow == OVERFLOW_MIDDLE_ELLIPSIS
380                         || mOverflow == OVERFLOW_ELLIPSIS)) {
381             flags |= PaintContext.TEXT_COMPLEX;
382         }
383         if ((flags & PaintContext.TEXT_COMPLEX) != PaintContext.TEXT_COMPLEX) {
384             for (int i = 0; i < mCachedString.length(); i++) {
385                 char c = mCachedString.charAt(i);
386                 if ((c == '\n') || (c == '\t')) {
387                     flags |= PaintContext.TEXT_COMPLEX;
388                     forceComplex = true;
389                     break;
390                 }
391             }
392         }
393         if (!forceComplex) {
394             context.getTextBounds(mTextId, 0, mCachedString.length(), flags, bounds);
395         }
396         if (forceComplex || bounds[2] - bounds[1] > maxWidth && mMaxLines > 1 && maxWidth > 0f) {
397             mComputedTextLayout =
398                     context.layoutComplexText(
399                             mTextId,
400                             0,
401                             mCachedString.length(),
402                             mTextAlign,
403                             mOverflow,
404                             mMaxLines,
405                             maxWidth,
406                             flags);
407             if (mComputedTextLayout != null) {
408                 bounds[0] = 0f;
409                 bounds[1] = 0f;
410                 bounds[2] = mComputedTextLayout.getWidth();
411                 bounds[3] = mComputedTextLayout.getHeight();
412             }
413         } else {
414             mComputedTextLayout = null;
415         }
416         context.restorePaint();
417         float w = bounds[2] - bounds[0];
418         float h = bounds[3] - bounds[1];
419         size.setWidth(Math.min(maxWidth, w));
420         mTextX = -bounds[0];
421         size.setHeight(Math.min(maxHeight, h));
422         mTextY = -bounds[1];
423         mTextW = w;
424         mTextH = h;
425     }
426 
427     @Override
minIntrinsicHeight(@ullable RemoteContext context)428     public float minIntrinsicHeight(@Nullable RemoteContext context) {
429         return mTextH;
430     }
431 
432     @Override
minIntrinsicWidth(@ullable RemoteContext context)433     public float minIntrinsicWidth(@Nullable RemoteContext context) {
434         return mTextW;
435     }
436 
437     /**
438      * The name of the class
439      *
440      * @return the name
441      */
442     @NonNull
name()443     public static String name() {
444         return "TextLayout";
445     }
446 
447     /**
448      * The OP_CODE for this command
449      *
450      * @return the opcode
451      */
id()452     public static int id() {
453         return Operations.LAYOUT_TEXT;
454     }
455 
456     /**
457      * Write the operation in the buffer
458      *
459      * @param buffer the WireBuffer we write on
460      * @param componentId the component id
461      * @param animationId the animation id (-1 if not set)
462      * @param textId the text id
463      * @param color the text color
464      * @param fontSize the font size
465      * @param fontStyle the font style
466      * @param fontWeight the font weight
467      * @param fontFamilyId the font family id
468      * @param textAlign the alignment rules
469      * @param overflow
470      * @param maxLines
471      */
apply( @onNull WireBuffer buffer, int componentId, int animationId, int textId, int color, float fontSize, int fontStyle, float fontWeight, int fontFamilyId, int textAlign, int overflow, int maxLines)472     public static void apply(
473             @NonNull WireBuffer buffer,
474             int componentId,
475             int animationId,
476             int textId,
477             int color,
478             float fontSize,
479             int fontStyle,
480             float fontWeight,
481             int fontFamilyId,
482             int textAlign,
483             int overflow,
484             int maxLines) {
485         buffer.start(id());
486         buffer.writeInt(componentId);
487         buffer.writeInt(animationId);
488         buffer.writeInt(textId);
489         buffer.writeInt(color);
490         buffer.writeFloat(fontSize);
491         buffer.writeInt(fontStyle);
492         buffer.writeFloat(fontWeight);
493         buffer.writeInt(fontFamilyId);
494         buffer.writeInt(textAlign);
495         buffer.writeInt(overflow);
496         buffer.writeInt(maxLines);
497     }
498 
499     /**
500      * Read this operation and add it to the list of operations
501      *
502      * @param buffer the buffer to read
503      * @param operations the list of operations that will be added to
504      */
read(@onNull WireBuffer buffer, @NonNull List<Operation> operations)505     public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) {
506         int componentId = buffer.readInt();
507         int animationId = buffer.readInt();
508         int textId = buffer.readInt();
509         int color = buffer.readInt();
510         float fontSize = buffer.readFloat();
511         int fontStyle = buffer.readInt();
512         float fontWeight = buffer.readFloat();
513         int fontFamilyId = buffer.readInt();
514         int textAlign = buffer.readInt();
515         int overflow = buffer.readInt();
516         int maxLines = buffer.readInt();
517         operations.add(
518                 new TextLayout(
519                         null,
520                         componentId,
521                         animationId,
522                         textId,
523                         color,
524                         fontSize,
525                         fontStyle,
526                         fontWeight,
527                         fontFamilyId,
528                         textAlign,
529                         overflow,
530                         maxLines));
531     }
532 
533     /**
534      * Populate the documentation with a description of this operation
535      *
536      * @param doc to append the description to.
537      */
documentation(@onNull DocumentationBuilder doc)538     public static void documentation(@NonNull DocumentationBuilder doc) {
539         doc.operation("Layout Operations", id(), name())
540                 .description("Text layout implementation.\n\n")
541                 .field(INT, "COMPONENT_ID", "unique id for this component")
542                 .field(
543                         INT,
544                         "ANIMATION_ID",
545                         "id used to match components," + " for animation purposes")
546                 .field(INT, "COLOR", "text color")
547                 .field(FLOAT, "FONT_SIZE", "font size")
548                 .field(INT, "FONT_STYLE", "font style (0 = normal, 1 = italic)")
549                 .field(FLOAT, "FONT_WEIGHT", "font weight (1-1000, normal = 400)")
550                 .field(INT, "FONT_FAMILY_ID", "font family id");
551     }
552 
553     @Override
write(@onNull WireBuffer buffer)554     public void write(@NonNull WireBuffer buffer) {
555         apply(
556                 buffer,
557                 mComponentId,
558                 mAnimationId,
559                 mTextId,
560                 mColor,
561                 mFontSize,
562                 mFontStyle,
563                 mFontWeight,
564                 mFontFamilyId,
565                 mTextAlign,
566                 mOverflow,
567                 mMaxLines);
568     }
569 
570     @Override
serialize(MapSerializer serializer)571     public void serialize(MapSerializer serializer) {
572         super.serialize(serializer);
573         serializer.add("textId", mTextId);
574         serializer.add("color", Utils.colorInt(mColor));
575         serializer.add("fontSize", mFontSize);
576         serializer.add("fontStyle", mFontStyle);
577         serializer.add("fontWeight", mFontWeight);
578         serializer.add("fontFamilyId", mFontFamilyId);
579         serializer.add("textAlign", mTextAlign);
580     }
581 }
582