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