diff --git a/maestro-cli/src/main/java/maestro/cli/graphics/SkiaFrameRenderer.kt b/maestro-cli/src/main/java/maestro/cli/graphics/SkiaFrameRenderer.kt index 2a150aa50a..b358a3a5e0 100644 --- a/maestro-cli/src/main/java/maestro/cli/graphics/SkiaFrameRenderer.kt +++ b/maestro-cli/src/main/java/maestro/cli/graphics/SkiaFrameRenderer.kt @@ -3,14 +3,9 @@ package maestro.cli.graphics import org.jetbrains.skia.Canvas import org.jetbrains.skia.Color import org.jetbrains.skia.Font -import org.jetbrains.skia.FontMgr import org.jetbrains.skia.Paint import org.jetbrains.skia.Rect import org.jetbrains.skia.Surface -import org.jetbrains.skia.paragraph.FontCollection -import org.jetbrains.skia.paragraph.ParagraphBuilder -import org.jetbrains.skia.paragraph.ParagraphStyle -import org.jetbrains.skia.paragraph.TextStyle import org.jetbrains.skiko.toImage import java.awt.image.BufferedImage import javax.imageio.ImageIO @@ -42,13 +37,10 @@ class SkiaFrameRenderer : FrameRenderer { private val footerText = "maestro.mobile.dev" private val terminalBgColor = Color.makeARGB(220, 0, 0, 0) - private val terminalTextStyle = TextStyle().apply { - fontFamilies = SkiaFonts.MONOSPACE_FONT_FAMILIES.toTypedArray() - fontSize = 24f - color = Color.WHITE - } private val terminalContentPadding = 40f + private val textClipper = SkiaTextClipper() + override fun render( outputWidthPx: Int, outputHeightPx: Int, @@ -154,14 +146,24 @@ class SkiaFrameRenderer : FrameRenderer { val contentRect = Rect.makeLTRB(terminalRect.left, headerRect.bottom, terminalRect.right, footerRect.top) canvas.drawRect(contentRect, Paint().apply { color = terminalBgColor }) - val paddedContentRect = contentRect.inflate(-terminalContentPadding) + val paddedContentRect = Rect.makeLTRB( + l = contentRect.left + terminalContentPadding, + t = contentRect.top + terminalContentPadding, + r = contentRect.right - terminalContentPadding, + b = contentRect.bottom - terminalContentPadding / 4f, + ) + + val focusedLineIndex = getFocusedLineIndex(string) + val focusedLinePadding = 5 + textClipper.renderClippedText(canvas, paddedContentRect, string, focusedLineIndex + focusedLinePadding) + } - val fontCollection = FontCollection().setDefaultFontManager(FontMgr.default) - val p = ParagraphBuilder(ParagraphStyle(), fontCollection) - .pushStyle(terminalTextStyle) - .addText(string) - .build() - p.layout(paddedContentRect.width) - p.paint(canvas, paddedContentRect.left, paddedContentRect.top) + private fun getFocusedLineIndex(text: String): Int { + val lines = text.lines() + val indexOfFirstPendingLine = lines.indexOfFirst { it.contains("\uD83D\uDD32") } + if (indexOfFirstPendingLine != -1) return indexOfFirstPendingLine + val indexOfLastCheck = lines.indexOfLast { it.contains("✅") } + if (indexOfLastCheck != -1) return indexOfLastCheck + return 0 } } diff --git a/maestro-cli/src/main/java/maestro/cli/graphics/SkiaTextClipper.kt b/maestro-cli/src/main/java/maestro/cli/graphics/SkiaTextClipper.kt new file mode 100644 index 0000000000..e23795a9b3 --- /dev/null +++ b/maestro-cli/src/main/java/maestro/cli/graphics/SkiaTextClipper.kt @@ -0,0 +1,64 @@ +package maestro.cli.graphics + +import org.jetbrains.skia.Canvas +import org.jetbrains.skia.Color +import org.jetbrains.skia.FontMgr +import org.jetbrains.skia.Rect +import org.jetbrains.skia.paragraph.FontCollection +import org.jetbrains.skia.paragraph.Paragraph +import org.jetbrains.skia.paragraph.ParagraphBuilder +import org.jetbrains.skia.paragraph.ParagraphStyle +import org.jetbrains.skia.paragraph.RectHeightMode +import org.jetbrains.skia.paragraph.RectWidthMode +import org.jetbrains.skia.paragraph.TextStyle +import kotlin.math.min + +class SkiaTextClipper { + + private val terminalTextStyle = TextStyle().apply { + fontFamilies = SkiaFonts.MONOSPACE_FONT_FAMILIES.toTypedArray() + fontSize = 24f + color = Color.WHITE + } + + fun renderClippedText(canvas: Canvas, rect: Rect, text: String, focusedLine: Int) { + val p = createParagraph(text, rect.width) + val focusedLineRange = getRangeForLine(text, focusedLine) + val focusedLineBottom = p.getRectsForRange( + start = focusedLineRange.first, + end = focusedLineRange.second, + rectHeightMode = RectHeightMode.MAX, + rectWidthMode = RectWidthMode.MAX + ).maxOf { it.rect.bottom } + val offsetY = min(0f, rect.height - focusedLineBottom) + canvas.save() + canvas.clipRect(rect) + p.paint(canvas, rect.left, rect.top + offsetY) + canvas.restore() + } + + private fun getRangeForLine(text: String, lineIndex: Int): Pair { + var start = 0 + var end = 0 + var currentLine = 0 + while (currentLine <= lineIndex) { + start = end + end = text.indexOf('\n', start + 1) + if (end == -1) { + end = text.length + break + } + currentLine++ + } + return Pair(start, end) + } + + private fun createParagraph(text: String, width: Float): Paragraph { + val fontCollection = FontCollection().setDefaultFontManager(FontMgr.default) + return ParagraphBuilder(ParagraphStyle(), fontCollection) + .pushStyle(terminalTextStyle) + .addText(text) + .build() + .apply { layout(width) } + } +}