Do not enlarge images on open

This commit is contained in:
iris committed 2026-09-12 21:52:47 -04:00
1 parent 6d765ff6e4
commit e9a0f1b9da
3 files changed
+26 -15

No files matched your search

+2 -1
View File
@@ -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 system-bar regions** (2026-09-12). It hides the status and navigation bars
independently when the fitted, zoomed or panned image reaches them, and independently when the fitted, zoomed or panned image reaches them, and
restores either one when it does not. `100%` recenters at one bitmap pixel 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` - **All transcript text is selectable, from one `SelectionContainer`
around the whole list.** Not per row: a transcript is one body of text, 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 — so a selection has to run from a reply into the tool output under it —
@@ -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 * 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 * 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 @Composable
private fun ZoomableImage( private fun ZoomableImage(
@@ -328,7 +329,7 @@ private fun ZoomableImage(
Image( Image(
bitmap = image, bitmap = image,
contentDescription = "Attached image", contentDescription = "Attached image",
contentScale = ContentScale.Fit, contentScale = ContentScale.Inside,
// Zoomed in, the reader is looking at pixels on purpose. // Zoomed in, the reader is looking at pixels on purpose.
filterQuality = FilterQuality.None, filterQuality = FilterQuality.None,
modifier = modifier =
@@ -337,9 +338,8 @@ private fun ZoomableImage(
.pointerInput(nativeScale) { .pointerInput(nativeScale) {
detectTransformGestures { centroid, pan, zoom, _ -> detectTransformGestures { centroid, pan, zoom, _ ->
val oldScale = scale val oldScale = scale
val minimumScale = minOf(1f, nativeScale)
val maximumScale = maxOf(8f, nativeScale) val maximumScale = maxOf(8f, nativeScale)
val newScale = (oldScale * zoom).coerceIn(minimumScale, maximumScale) val newScale = (oldScale * zoom).coerceIn(1f, maximumScale)
if (newScale > 1f) { if (newScale > 1f) {
val offset = val offset =
zoomOffset( zoomOffset(
@@ -414,8 +414,7 @@ internal fun viewerBars(
if (imageWidth <= 0 || imageHeight <= 0 || viewportWidth <= 0 || viewportHeight <= 0) { if (imageWidth <= 0 || imageHeight <= 0 || viewportWidth <= 0 || viewportHeight <= 0) {
return ViewerBars() return ViewerBars()
} }
val fittedScale = val fittedScale = insideScale(imageWidth, imageHeight, viewportWidth, viewportHeight)
minOf(viewportWidth.toFloat() / imageWidth, viewportHeight.toFloat() / imageHeight)
val width = imageWidth * fittedScale * scale val width = imageWidth * fittedScale * scale
val height = imageHeight * fittedScale * scale val height = imageHeight * fittedScale * scale
val left = viewportWidth / 2f + offset.x - width / 2f 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( internal fun nativeScale(
imageWidth: Int, imageWidth: Int,
imageHeight: Int, imageHeight: Int,
@@ -441,11 +440,22 @@ internal fun nativeScale(
viewportHeight: Int, viewportHeight: Int,
): Float { ): Float {
if (imageWidth <= 0 || imageHeight <= 0 || viewportWidth <= 0 || viewportHeight <= 0) return 1f if (imageWidth <= 0 || imageHeight <= 0 || viewportWidth <= 0 || viewportHeight <= 0) return 1f
val fittedScale = return 1f / insideScale(imageWidth, imageHeight, viewportWidth, viewportHeight)
minOf(viewportWidth.toFloat() / imageWidth, viewportHeight.toFloat() / imageHeight)
return 1f / fittedScale
} }
/** 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. */ /** Keeps the image point beneath [centroid] beneath the fingers as its scale changes. */
internal fun zoomOffset( internal fun zoomOffset(
offset: Offset, offset: Offset,
@@ -10,8 +10,8 @@ class SessionImageTest {
assertEquals( assertEquals(
ViewerBars(status = true, navigation = true), ViewerBars(status = true, navigation = true),
viewerBars( viewerBars(
imageWidth = 500, imageWidth = 1000,
imageHeight = 1000, imageHeight = 2000,
viewportWidth = 1000, viewportWidth = 1000,
viewportHeight = 2000, viewportHeight = 2000,
scale = 1f, scale = 1f,
@@ -67,9 +67,9 @@ class SessionImageTest {
} }
@Test @Test
fun `native scale reverses enlarging a small image`() { fun `native scale leaves a small image alone`() {
assertEquals( assertEquals(
0.5f, 1f,
nativeScale( nativeScale(
imageWidth = 500, imageWidth = 500,
imageHeight = 500, imageHeight = 500,