diff --git a/src/main/java/org/pubvm/a3270/ui/TerminalView.kt b/src/main/java/org/pubvm/a3270/ui/TerminalView.kt index 97a33ae..304d978 100644 --- a/src/main/java/org/pubvm/a3270/ui/TerminalView.kt +++ b/src/main/java/org/pubvm/a3270/ui/TerminalView.kt @@ -142,11 +142,10 @@ fun TerminalView( showContextMenu = false selectionStart = null selectionEnd = null - val cellWidth = size.width / cols - val cellHeight = size.height / rows - if (cellWidth > 0 && cellHeight > 0) { - val col = (startPos.x / cellWidth).toInt().coerceIn(0, cols - 1) - val row = (startPos.y / cellHeight).toInt().coerceIn(0, rows - 1) + val metrics = calculateGridMetrics(size.width.toFloat(), size.height.toFloat(), cols, rows) + if (metrics.cellWidth > 0 && metrics.cellHeight > 0) { + val col = ((startPos.x - metrics.offsetX) / metrics.cellWidth).toInt().coerceIn(0, cols - 1) + val row = ((startPos.y - metrics.offsetY) / metrics.cellHeight).toInt().coerceIn(0, rows - 1) var addr = row * cols + col val buf = screenBuffer @@ -167,17 +166,26 @@ fun TerminalView( val activeCursorAddr = currentCursorAddr if (version == -1L) return@Canvas - val width = size.width - val height = size.height - val cellWidth = width / cols - val cellHeight = height / rows + val metrics = calculateGridMetrics(size.width, size.height, cols, rows) + if (metrics.cellWidth <= 0f || metrics.cellHeight <= 0f) return@Canvas - if (cellWidth <= 0 || cellHeight <= 0) return@Canvas + val cellWidth = metrics.cellWidth + val cellHeight = metrics.cellHeight + val offsetX = metrics.offsetX + val offsetY = metrics.offsetY val paint = Paint().apply { isAntiAlias = true typeface = Typeface.MONOSPACE - textSize = cellHeight * 0.85f + textAlign = Paint.Align.CENTER + } + + // Calculate optimal font size ensuring characters fit cleanly in cell without crowding + val baseTextSize = cellHeight * 0.78f + paint.textSize = baseTextSize + val measuredW = paint.measureText("W") + if (measuredW > cellWidth * 0.85f && measuredW > 0f) { + paint.textSize = baseTextSize * (cellWidth * 0.85f / measuredW) } val buf = screenBuffer @@ -194,11 +202,11 @@ fun TerminalView( val start = selectionStart val end = selectionEnd - if (start != null && end != null) { - val startCol = (start.x / cellWidth).toInt().coerceIn(0, cols - 1) - val startRow = (start.y / cellHeight).toInt().coerceIn(0, rows - 1) - val endCol = (end.x / cellWidth).toInt().coerceIn(0, cols - 1) - val endRow = (end.y / cellHeight).toInt().coerceIn(0, rows - 1) + if (start != null && end != null && cellWidth > 0f && cellHeight > 0f) { + val startCol = ((start.x - offsetX) / cellWidth).toInt().coerceIn(0, cols - 1) + val startRow = ((start.y - offsetY) / cellHeight).toInt().coerceIn(0, rows - 1) + val endCol = ((end.x - offsetX) / cellWidth).toInt().coerceIn(0, cols - 1) + val endRow = ((end.y - offsetY) / cellHeight).toInt().coerceIn(0, rows - 1) selMinRow = minOf(startRow, endRow) selMaxRow = maxOf(startRow, endRow) @@ -211,8 +219,8 @@ fun TerminalView( val addr = r * cols + c if (addr >= totalCells) break - val left = c * cellWidth - val top = r * cellHeight + val left = offsetX + c * cellWidth + val top = offsetY + r * cellHeight var charVal = ' ' var fgColor: Color @@ -286,7 +294,7 @@ fun TerminalView( ) } - // Highlight selected block range + // Draw selection box highlight if selected val isSelected = r in selMinRow..selMaxRow && c in selMinCol..selMaxCol if (isSelected) { drawRect( @@ -314,7 +322,7 @@ fun TerminalView( val symHeight = slot.height val rgbArray = slot.getRgbPixels(fgColor.toArgb(), bgColor.toArgb()) if (rgbArray != null && symWidth > 0 && symHeight > 0) { - val bmp = android.graphics.Bitmap.createBitmap(rgbArray, symWidth, symHeight, android.graphics.Bitmap.Config.ARGB_8888) + val bmp = android.graphics.Bitmap.createBitmap(rgbArray as IntArray, symWidth, symHeight, android.graphics.Bitmap.Config.ARGB_8888) drawContext.canvas.nativeCanvas.drawBitmap( bmp, null, @@ -330,11 +338,12 @@ fun TerminalView( paint.color = fgColor.toArgb() paint.isFakeBoldText = isBold val fontMetrics = paint.fontMetrics - val textY = top + (cellHeight - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top + val textX = left + cellWidth / 2f + val textY = top + (cellHeight - fontMetrics.bottom + fontMetrics.top) / 2f - fontMetrics.top drawContext.canvas.nativeCanvas.drawText( charVal.toString(), - left + (cellWidth / 4), + textX, textY, paint ) @@ -343,8 +352,8 @@ fun TerminalView( if (isUnderline) { drawLine( color = fgColor, - start = Offset(left, top + cellHeight - 2), - end = Offset(left + cellWidth, top + cellHeight - 2), + start = Offset(left, top + cellHeight - 2f), + end = Offset(left + cellWidth, top + cellHeight - 2f), strokeWidth = 2f ) } @@ -353,8 +362,8 @@ fun TerminalView( // Draw Vector Graphics Plane overlay if present if (graphicsPlane != null && graphicsPlane.hasContent()) { - val gridW = (cols * cellWidth).toInt() - val gridH = (rows * cellHeight).toInt() + val gridW = metrics.gridWidth.toInt() + val gridH = metrics.gridHeight.toInt() if (gridW > 0 && gridH > 0) { graphicsPlane.resize(gridW, gridH) val rgb = graphicsPlane.rgbBuffer @@ -363,7 +372,7 @@ fun TerminalView( drawContext.canvas.nativeCanvas.drawBitmap( bmp, null, - android.graphics.RectF(0f, 0f, gridW.toFloat(), gridH.toFloat()), + android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()), null ) } @@ -425,6 +434,57 @@ fun TerminalView( } } +data class TerminalGridMetrics( + val cellWidth: Float, + val cellHeight: Float, + val gridWidth: Float, + val gridHeight: Float, + val offsetX: Float, + val offsetY: Float +) + +fun calculateGridMetrics( + viewWidth: Float, + viewHeight: Float, + cols: Int, + rows: Int +): TerminalGridMetrics { + if (viewWidth <= 0f || viewHeight <= 0f || cols <= 0 || rows <= 0) { + return TerminalGridMetrics(0f, 0f, 0f, 0f, 0f, 0f) + } + + val rawCellW = viewWidth / cols + val rawCellH = viewHeight / rows + val currentRatio = rawCellW / rawCellH + + // Target terminal cell aspect ratio is roughly 0.50 to 0.60 (width / height) + val targetMinRatio = 0.48f + val targetMaxRatio = 0.60f + + val cellW: Float + val cellH: Float + + if (currentRatio > targetMaxRatio) { + // Screen is wider than ideal aspect ratio -> clamp width, pillarbox (equal blank bars left/right) + cellH = rawCellH + cellW = rawCellH * targetMaxRatio + } else if (currentRatio < targetMinRatio) { + // Screen is taller than ideal aspect ratio -> clamp height, letterbox (equal blank bars top/bottom) + cellW = rawCellW + cellH = rawCellW / targetMinRatio + } else { + cellW = rawCellW + cellH = rawCellH + } + + val gridW = cols * cellW + val gridH = rows * cellH + val offX = (viewWidth - gridW) / 2f + val offY = (viewHeight - gridH) / 2f + + return TerminalGridMetrics(cellW, cellH, gridW, gridH, offX, offY) +} + private fun copySelection( start: Offset?, end: Offset?, @@ -436,13 +496,15 @@ private fun copySelection( ) { if (start != null && end != null && screenBuffer != null && cols > 0 && rows > 0) { val displayMetrics = context.resources.displayMetrics - val cellWidth = displayMetrics.widthPixels.toFloat() / cols - val cellHeight = displayMetrics.heightPixels.toFloat() / rows + val viewW = displayMetrics.widthPixels.toFloat() + val viewH = displayMetrics.heightPixels.toFloat() + val metrics = calculateGridMetrics(viewW, viewH, cols, rows) + if (metrics.cellWidth <= 0f || metrics.cellHeight <= 0f) return - val startCol = (start.x / cellWidth).toInt().coerceIn(0, cols - 1) - val startRow = (start.y / cellHeight).toInt().coerceIn(0, rows - 1) - val endCol = (end.x / cellWidth).toInt().coerceIn(0, cols - 1) - val endRow = (end.y / cellHeight).toInt().coerceIn(0, rows - 1) + val startCol = ((start.x - metrics.offsetX) / metrics.cellWidth).toInt().coerceIn(0, cols - 1) + val startRow = ((start.y - metrics.offsetY) / metrics.cellHeight).toInt().coerceIn(0, rows - 1) + val endCol = ((end.x - metrics.offsetX) / metrics.cellWidth).toInt().coerceIn(0, cols - 1) + val endRow = ((end.y - metrics.offsetY) / metrics.cellHeight).toInt().coerceIn(0, rows - 1) val minRow = minOf(startRow, endRow) val maxRow = maxOf(startRow, endRow)