diff --git a/PLAN.md b/PLAN.md index 2a698f4..eb317dd 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1048,7 +1048,8 @@ dev-updater (Kotlin 2.4.x, CMP 1.11.x, JDK 21). system-bar regions** (2026-09-12). It hides the status and navigation bars independently when the fitted, zoomed or panned image reaches them, and restores either one when it does not. `100%` recenters at one bitmap pixel - per screen pixel; opening still fits the whole image first. + per screen pixel; opening only shrinks an image that needs it to fit and + never enlarges a smaller one. - **All transcript text is selectable, from one `SelectionContainer` around the whole list.** Not per row: a transcript is one body of text, so a selection has to run from a reply into the tool output under it — diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionImage.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionImage.kt index 9dc8612..29290ef 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionImage.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionImage.kt @@ -292,7 +292,8 @@ private fun enlargingFilter(sourceHeight: Int, drawnHeight: Int): FilterQuality * * Inside a dialog rather than a screen -- see [SessionImageViewer] -- so the platform's back * gesture returns to the transcript instead of leaving the app. It opens fitted, with the whole - * image visible; the 100% control changes to one bitmap pixel per screen pixel and recenters it. + * image visible without enlarging a smaller one; the 100% control changes to one bitmap pixel per + * screen pixel and recenters it. */ @Composable private fun ZoomableImage( @@ -328,7 +329,7 @@ private fun ZoomableImage( Image( bitmap = image, contentDescription = "Attached image", - contentScale = ContentScale.Fit, + contentScale = ContentScale.Inside, // Zoomed in, the reader is looking at pixels on purpose. filterQuality = FilterQuality.None, modifier = @@ -337,9 +338,8 @@ private fun ZoomableImage( .pointerInput(nativeScale) { detectTransformGestures { centroid, pan, zoom, _ -> val oldScale = scale - val minimumScale = minOf(1f, nativeScale) val maximumScale = maxOf(8f, nativeScale) - val newScale = (oldScale * zoom).coerceIn(minimumScale, maximumScale) + val newScale = (oldScale * zoom).coerceIn(1f, maximumScale) if (newScale > 1f) { val offset = zoomOffset( @@ -414,8 +414,7 @@ internal fun viewerBars( if (imageWidth <= 0 || imageHeight <= 0 || viewportWidth <= 0 || viewportHeight <= 0) { return ViewerBars() } - val fittedScale = - minOf(viewportWidth.toFloat() / imageWidth, viewportHeight.toFloat() / imageHeight) + val fittedScale = insideScale(imageWidth, imageHeight, viewportWidth, viewportHeight) val width = imageWidth * fittedScale * scale val height = imageHeight * fittedScale * scale val left = viewportWidth / 2f + offset.x - width / 2f @@ -433,7 +432,7 @@ internal fun viewerBars( ) } -/** Scale relative to [ContentScale.Fit] at which bitmap and screen pixels are one-to-one. */ +/** Scale relative to [ContentScale.Inside] at which bitmap and screen pixels are one-to-one. */ internal fun nativeScale( imageWidth: Int, imageHeight: Int, @@ -441,11 +440,22 @@ internal fun nativeScale( viewportHeight: Int, ): Float { if (imageWidth <= 0 || imageHeight <= 0 || viewportWidth <= 0 || viewportHeight <= 0) return 1f - val fittedScale = - minOf(viewportWidth.toFloat() / imageWidth, viewportHeight.toFloat() / imageHeight) - return 1f / fittedScale + return 1f / insideScale(imageWidth, imageHeight, viewportWidth, viewportHeight) } +/** The downscale-only factor used by [ContentScale.Inside]. */ +private fun insideScale( + imageWidth: Int, + imageHeight: Int, + viewportWidth: Int, + viewportHeight: Int, +): Float = + minOf( + 1f, + viewportWidth.toFloat() / imageWidth, + viewportHeight.toFloat() / imageHeight, + ) + /** Keeps the image point beneath [centroid] beneath the fingers as its scale changes. */ internal fun zoomOffset( offset: Offset, diff --git a/app/androidApp/src/test/kotlin/com/example/aiapp/SessionImageTest.kt b/app/androidApp/src/test/kotlin/com/example/aiapp/SessionImageTest.kt index 62c2787..abd92e0 100644 --- a/app/androidApp/src/test/kotlin/com/example/aiapp/SessionImageTest.kt +++ b/app/androidApp/src/test/kotlin/com/example/aiapp/SessionImageTest.kt @@ -10,8 +10,8 @@ class SessionImageTest { assertEquals( ViewerBars(status = true, navigation = true), viewerBars( - imageWidth = 500, - imageHeight = 1000, + imageWidth = 1000, + imageHeight = 2000, viewportWidth = 1000, viewportHeight = 2000, scale = 1f, @@ -67,9 +67,9 @@ class SessionImageTest { } @Test - fun `native scale reverses enlarging a small image`() { + fun `native scale leaves a small image alone`() { assertEquals( - 0.5f, + 1f, nativeScale( imageWidth = 500, imageHeight = 500,