Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
59a3e12
feat(review): unify pull request and Git review across clients
jakeleventhal Oct 24, 2026
e2214d6
fix(review): show admin merge option and group pending replies
jakeleventhal Oct 28, 2026
2121f35
fix(mobile): pass review diff native static checks
jakeleventhal Oct 28, 2026
604a688
fix(review): address draft, merge, and native review findings
jakeleventhal Oct 28, 2026
f81e1c8
fix(mobile): keep PR review controls clear of navigation
jakeleventhal Oct 28, 2026
b07e317
feat(mobile): use glass tabs for pull request review sections
jakeleventhal Oct 28, 2026
777b13e
feat(mobile): name the diff tab Changes and give git actions one spli…
jakeleventhal Oct 29, 2026
017512c
feat(mobile): PR comments add to the thread's composer and use deskto…
jakeleventhal Oct 29, 2026
1a9732c
fix(mobile): git menus name the branches item instead of hiding it in…
jakeleventhal Oct 29, 2026
e375397
fix(mobile): git split button reads as one control and the workspace …
jakeleventhal Oct 29, 2026
319fb3b
fix(mobile): PR comment sheets fit their form on iPad and stay usable…
jakeleventhal Oct 29, 2026
bee6b1c
fix(review): preserve upstream behavior and repair nested patch actions
jakeleventhal Nov 2, 2026
c8120e5
test(review): align fixtures with current shared helpers
jakeleventhal Nov 2, 2026
4763189
fix(review): retain accessible tabs and comparison recovery
jakeleventhal Nov 2, 2026
b1dd709
fix(mobile): scroll selected section tabs into view
jakeleventhal Nov 2, 2026
3dcd01f
fix(review): preserve amend messages and complete review controls
jakeleventhal Nov 2, 2026
a5ee676
fix(review): refresh status and preserve accessible review controls
jakeleventhal Nov 2, 2026
10c4718
fix(review): retain commit scopes and reconcile host changes
jakeleventhal Nov 2, 2026
c96b6da
test(review): remove native startup diagnostic exception
jakeleventhal Nov 2, 2026
d292bcd
fix(review): require read access before publishing attachments
jakeleventhal Nov 2, 2026
2a4cb0a
fix(review): keep optimistic staging patches valid
maria-rcks Nov 2, 2026
ef05f42
fix(review): merge main and preserve review grouping
maria-rcks Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import kotlin.math.max
import kotlin.math.min

internal class ReviewDiffCanvasDrawing(context: Context) {
private val density = context.resources.displayMetrics.density
val density = context.resources.displayMetrics.density
var theme: DiffTheme = DiffTheme.fallback("light")

val backgroundPaint = Paint()
Expand Down Expand Up @@ -70,90 +70,186 @@ internal class ReviewDiffCanvasDrawing(context: Context) {
)
}

fun drawDisclosureChevron(
fun drawFileHeader(
canvas: Canvas,
row: DiffRow,
rect: RectF,
color: Int,
collapsed: Boolean
style: DiffStyle,
state: FileHeaderDrawingState
) {
borderPaint.style = Paint.Style.STROKE
borderPaint.color = color
borderPaint.strokeWidth = 2f * density
borderPaint.strokeCap = Paint.Cap.ROUND
borderPaint.strokeJoin = Paint.Join.ROUND
val path = Path()
if (collapsed) {
path.moveTo(rect.left + rect.width() * 0.4f, rect.top + rect.height() * 0.28f)
path.lineTo(rect.left + rect.width() * 0.6f, rect.centerY())
path.lineTo(rect.left + rect.width() * 0.4f, rect.bottom - rect.height() * 0.28f)
val top = rect.top.toInt()
val bottom = rect.bottom.toInt()
val width = rect.width().toInt()
backgroundPaint.color = theme.headerBackground
canvas.drawRect(rect, backgroundPaint)

val chevronRect = fileHeaderChevronRect(top, bottom, style)
val iconRect = fileHeaderIconRect(top, bottom, style)
val checkboxRect = fileHeaderCheckboxRect(top, bottom, width, style)
drawDisclosureChevron(
canvas,
chevronRect,
theme.mutedText,
collapsed = state.collapsed,
)
drawFileIcon(canvas, iconRect, row.changeType)
if (state.showViewedCheckbox) {
val saved = canvas.saveLayerAlpha(checkboxRect, if (state.viewedCheckboxEnabled) 255 else 102)
drawViewedCheckbox(
canvas,
checkboxRect,
state.viewed,
)
canvas.restoreToCount(saved)
}

configureUiPaint(
paint = uiPaint,
color = theme.deleteText,
size = style.fileHeaderMetaFontSizePx,
weight = style.fileHeaderMetaFontWeight,
)
val deleteText = "-${row.deletions}"
val deleteWidth = uiPaint.measureText(deleteText)
val addText = "+${row.additions}"
val addWidth = uiPaint.measureText(addText)
val countGap = 4f * density
val countsRight = if (row.fileAction != null) {
width - 110f * density
} else if (state.showViewedCheckbox) {
checkboxRect.left - 10f * density
} else {
path.moveTo(rect.left + rect.width() * 0.28f, rect.top + rect.height() * 0.42f)
path.lineTo(rect.centerX(), rect.top + rect.height() * 0.62f)
path.lineTo(rect.right - rect.width() * 0.28f, rect.top + rect.height() * 0.42f)
width - 16f * density
}
canvas.drawPath(path, borderPaint)
borderPaint.style = Paint.Style.FILL
val countsX = countsRight - deleteWidth - countGap - addWidth
val baseline = centeredBaseline(top, bottom, uiPaint)
canvas.drawText(deleteText, countsX, baseline, uiPaint)
uiPaint.color = theme.addText
canvas.drawText(addText, countsX + deleteWidth + countGap, baseline, uiPaint)

val pathLayout = fileHeaderPathLayout(row, iconRect, countsX, style)
val maxPathOffset = maxHeaderPathOffset(pathLayout)
val pathOffset = state.pathOffset
.coerceIn(0, maxPathOffset)
drawFileHeaderPath(canvas, row, pathLayout, pathOffset, maxPathOffset)
drawFileHeaderAction(canvas, row, rect)
borderPaint.color = theme.border
borderPaint.strokeWidth = density
canvas.drawLine(0f, bottom - density / 2f, width.toFloat(), bottom - density / 2f, borderPaint)
}

private fun drawFileHeaderPath(
canvas: Canvas,
row: DiffRow,
pathLayout: FileHeaderPathLayout,
pathOffset: Int,
maxPathOffset: Int
) {
canvas.save()
canvas.clipRect(pathLayout.rect)
val metrics = uiPaint.fontMetrics
val baseline = pathLayout.rect.centerY() - (metrics.ascent + metrics.descent) / 2f
if (row.renameSegments.isNotEmpty()) {
drawRenamePath(canvas, row, pathLayout, pathOffset, baseline)
} else {
canvas.drawText(pathLayout.displayPath, pathLayout.rect.left - pathOffset, baseline, uiPaint)
}
canvas.restore()
drawFileHeaderPathScrollFades(canvas, pathLayout.rect, pathOffset, maxPathOffset)
}

fun drawFileIcon(canvas: Canvas, rect: RectF, changeType: String) {
val color = when (changeType) {
"new" -> theme.addText
"deleted" -> theme.deleteText
else -> theme.hunkText
private fun drawRenamePath(
canvas: Canvas,
row: DiffRow,
pathLayout: FileHeaderPathLayout,
pathOffset: Int,
baseline: Float
) {
var x = pathLayout.rect.left - pathOffset
for (segment in row.renameSegments) {
val segmentWidth = uiPaint.measureText(segment.text)
if (segment.kind == "removed" || segment.kind == "added") {
backgroundPaint.color =
if (segment.kind == "removed") theme.deleteBackground else theme.addBackground
canvas.drawRect(
x,
pathLayout.rect.top,
x + segmentWidth,
pathLayout.rect.bottom,
backgroundPaint
)
}
uiPaint.color = if (segment.kind == "context") theme.mutedText else theme.text
canvas.drawText(segment.text, x, baseline, uiPaint)
x += segmentWidth
}
borderPaint.style = Paint.Style.STROKE
borderPaint.color = color
borderPaint.strokeWidth = 2f * density
canvas.drawRoundRect(rect, 6f * density, 6f * density, borderPaint)
}

if (changeType == "rename-pure" || changeType == "rename-changed" || changeType == "renamed") {
drawRenameChevronIcon(canvas, rect, color)
} else {
backgroundPaint.color = color
canvas.drawCircle(rect.centerX(), rect.centerY(), 3f * density, backgroundPaint)
private fun drawFileHeaderAction(canvas: Canvas, row: DiffRow, rect: RectF) {
val top = rect.top.toInt()
val bottom = rect.bottom.toInt()
val width = rect.width().toInt()
row.fileAction?.let { label ->
val rect = RectF(
width - 96 * density,
top + 7 * density,
width - 10 * density,
bottom - 7 * density
)
backgroundPaint.color = theme.border
backgroundPaint.style = Paint.Style.STROKE
backgroundPaint.strokeWidth = density
canvas.drawRoundRect(rect, 6 * density, 6 * density, backgroundPaint)
backgroundPaint.style = Paint.Style.FILL
configureUiPaint(
uiPaint,
if (row.fileActionDisabled) theme.mutedText else theme.hunkText,
12 * density,
"semibold"
)
canvas.drawText(
label,
rect.centerX() - uiPaint.measureText(label) / 2,
centeredBaseline(top, bottom, uiPaint),
uiPaint
)
}
borderPaint.style = Paint.Style.FILL
}

fun drawViewedCheckbox(canvas: Canvas, rect: RectF, checked: Boolean) {
if (checked) {
backgroundPaint.color = theme.hunkText
canvas.drawRoundRect(rect, 6f * density, 6f * density, backgroundPaint)
}
borderPaint.style = Paint.Style.STROKE
borderPaint.color = if (checked) theme.hunkText else theme.mutedText
borderPaint.strokeWidth = 1.8f * density
canvas.drawRoundRect(rect, 6f * density, 6f * density, borderPaint)
if (checked) {
borderPaint.color = theme.background
borderPaint.strokeWidth = 2f * density
borderPaint.strokeCap = Paint.Cap.ROUND
borderPaint.strokeJoin = Paint.Join.ROUND
val path = Path().apply {
moveTo(rect.left + rect.width() * 0.28f, rect.centerY())
lineTo(rect.left + rect.width() * 0.44f, rect.bottom - rect.height() * 0.3f)
lineTo(rect.right - rect.width() * 0.25f, rect.top + rect.height() * 0.3f)
}
canvas.drawPath(path, borderPaint)
fun fileHeaderPathLayout(
row: DiffRow,
iconRect: RectF,
countsX: Float,
style: DiffStyle
): FileHeaderPathLayout {
configureUiPaint(
paint = uiPaint,
color = theme.text,
size = style.fileHeaderFontSizePx,
weight = style.fileHeaderFontWeight,
)
val pathX = iconRect.right + 10f * density
val pathWidth = max(24f * density, countsX - pathX - 12f * density)
val centerY = iconRect.centerY()
val displayPath = if (row.renameSegments.isNotEmpty()) {
row.renameSegments.joinToString("") { it.text }
} else {
row.filePath
}
borderPaint.style = Paint.Style.FILL
}

fun drawNoticeIcon(canvas: Canvas, rect: RectF, color: Int) {
borderPaint.style = Paint.Style.STROKE
borderPaint.color = color
borderPaint.strokeWidth = 1.7f * density
borderPaint.strokeCap = Paint.Cap.ROUND
canvas.drawOval(rect, borderPaint)
canvas.drawLine(
rect.centerX(),
rect.top + rect.height() * 0.3f,
rect.centerX(),
rect.top + rect.height() * 0.58f,
borderPaint,
return FileHeaderPathLayout(
displayPath = displayPath,
rect = RectF(pathX, centerY - 10f * density, pathX + pathWidth, centerY + 10f * density),
)
borderPaint.style = Paint.Style.FILL
canvas.drawCircle(rect.centerX(), rect.bottom - rect.height() * 0.24f, density, borderPaint)
}

private fun maxHeaderPathOffset(layout: FileHeaderPathLayout): Int = max(
0,
ceil(uiPaint.measureText(layout.displayPath) - layout.rect.width()).toInt(),
)

private fun centeredBaseline(top: Int, bottom: Int, paint: Paint): Float {
val metrics = paint.fontMetrics
return (top + bottom) / 2f - (metrics.ascent + metrics.descent) / 2f
}

fun configureUiPaint(paint: Paint, color: Int, size: Float, weight: String) {
Expand Down Expand Up @@ -467,25 +563,6 @@ internal class ReviewDiffCanvasDrawing(context: Context) {
}
}

private fun drawRenameChevronIcon(canvas: Canvas, rect: RectF, color: Int) {
borderPaint.style = Paint.Style.STROKE
borderPaint.color = color
borderPaint.strokeWidth = 1.8f * density
borderPaint.strokeCap = Paint.Cap.ROUND
borderPaint.strokeJoin = Paint.Join.ROUND
val chevronWidth = 3.6f * density
val chevronHeight = 8f * density
val gap = 2.4f * density
val startX = rect.centerX() - (chevronWidth * 2f + gap) / 2f
val path = Path()
for (x in listOf(startX, startX + chevronWidth + gap)) {
path.moveTo(x, rect.centerY() - chevronHeight / 2f)
path.lineTo(x + chevronWidth, rect.centerY())
path.lineTo(x, rect.centerY() + chevronHeight / 2f)
}
canvas.drawPath(path, borderPaint)
}

private fun configureTextPaint(paint: Paint, color: Int, size: Float) {
paint.textSize = size
paint.color = color
Expand Down Expand Up @@ -532,3 +609,16 @@ private object ReviewDiffTypefaces {
else -> regular
}
}

internal data class FileHeaderDrawingState(
val collapsed: Boolean,
val viewed: Boolean,
val showViewedCheckbox: Boolean,
val viewedCheckboxEnabled: Boolean,
val pathOffset: Int
)

internal data class FileHeaderPathLayout(
val displayPath: String,
val rect: RectF
)
Loading
Loading