From 2ad2d65f06fb55366c0c96be3b571df81de4efd7 Mon Sep 17 00:00:00 2001 From: subochev Date: Tue, 29 Sep 2026 11:02:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=82=D0=B5=D1=81=D1=82=20=D0=BD=D0=B0=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=82=D0=B8=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=86=D0=B5=D0=BD=D1=82=D1=80=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=B8?= =?UTF-8?q?=20=C2=AB=D0=92=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20=D0=B4=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=C2=BB=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BD=D0=BE=D1=81=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=BA=D0=B2=D0=B0=D0=B4=D1=80=D0=B0=D1=82=D0=B0=20=D1=87=D0=B5?= =?UTF-8?q?=D0=BA=D0=B1=D0=BE=D0=BA=D1=81=D0=B0=20=D0=B8=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D1=82=D0=B0=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BF?= =?UTF-8?q?=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=B0=D1=85=20=D0=B4=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../desktop/ui/screens/DialogInfoDialog.kt | 42 +++++++---- .../desktop/ui/DialogInfoDialogTest.kt | 74 ++++++++++++++++++- 2 files changed, 101 insertions(+), 15 deletions(-) diff --git a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/screens/DialogInfoDialog.kt b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/screens/DialogInfoDialog.kt index d6ce41a..2d58072 100644 --- a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/screens/DialogInfoDialog.kt +++ b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/screens/DialogInfoDialog.kt @@ -10,8 +10,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -21,8 +20,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -32,12 +31,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -372,6 +373,7 @@ private fun TemporalCheckbox(temporal: Boolean) { Row( modifier = Modifier .fillMaxWidth() + .testTag("temporal-checkbox") .clip(RoundedCornerShape(10.dp)) .background(colors.panelElevated) .padding(10.dp), @@ -390,19 +392,31 @@ private fun TemporalCheckbox(temporal: Boolean) { Text("✓", color = onAccent, fontSize = 13.sp, fontWeight = FontWeight.Bold) } } - Box( + // Однострочный текст в Compose может «проседать» вниз, если lineHeight + // не задан явно: LineHeightStyle.Alignment.Center применяется только + // при заданном lineHeight. Рецепт ровного центра: lineHeight == fontSize + // + LineHeightStyle(Center, Trim.Both). + Text( + text = "Временный диалог", + color = colors.text, + fontSize = 13.sp, + lineHeight = 13.sp, + fontWeight = FontWeight.Medium, modifier = Modifier .weight(1f) - .height(20.dp), - contentAlignment = Alignment.CenterStart, - ) { - Text( - text = "Временный диалог", - color = colors.text, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - ) - } + // Типографская компенсация: строчные глифы (х-высота) визуально + // «проседают» на ~1.5px вниз относительно геометрического центра + // строки из-за метрик шрифта (ascent > x-height). Сдвигаем текст + // чуть вверх, чтобы его визуальная масса совпала с центром + // квадратика чекбокса. + .offset(y = (-1.5).dp), + style = LocalTextStyle.current.copy( + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.Both, + ), + ), + ) } } diff --git a/src/jvmTest/kotlin/pw/binom/agentik/desktop/ui/DialogInfoDialogTest.kt b/src/jvmTest/kotlin/pw/binom/agentik/desktop/ui/DialogInfoDialogTest.kt index 5a20875..01c0d8b 100644 --- a/src/jvmTest/kotlin/pw/binom/agentik/desktop/ui/DialogInfoDialogTest.kt +++ b/src/jvmTest/kotlin/pw/binom/agentik/desktop/ui/DialogInfoDialogTest.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.toPixelMap import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.captureToImage +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick @@ -367,8 +368,79 @@ class DialogInfoDialogTest { ) } - // ── helpers ──────────────────────────────────────────────────────────── + /** + * Регрессия на вертикальное центрирование подписи «Временный диалог» + * относительно квадратика чекбокса. Рендерит строку в битмап и сравнивает + * вертикальные центры синего квадратика (accent) и белых глифов текста. + */ + @OptIn(ExperimentalTestApi::class) + @Test + fun `temporal checkbox label is vertically centered with the box`() = runComposeUiTest { + val conversation = sampleConversation(title = "X", temporal = true) + setContent { + withAvatarStorage { + ProvideAgentikTheme("dark".toAgentikTheme()) { + Box(Modifier.fillMaxSize().background(darkAgentikColors.background)) { + DialogInfoDialog( + conversation = conversation, + agentName = null, + agentAddress = null, + agentColor = null, + online = false, + onClose = {}, + onRename = { _, _ -> }, + onDelete = {}, + ) + } + } + } + } + val pixels = onNodeWithTag("temporal-checkbox").captureToImage().toPixelMap() + + var sqMinY = Int.MAX_VALUE; var sqMaxY = -1 + var sqMaxX = -1 + var txMinY = Int.MAX_VALUE; var txMaxY = -1 + var sawText = false + for (y in 0 until pixels.height) { + for (x in 0 until pixels.width) { + val c = pixels[x, y] + val isAccent = kotlin.math.abs(c.red - 0.416f) < 0.12f && + kotlin.math.abs(c.green - 0.698f) < 0.12f && + kotlin.math.abs(c.blue - 0.949f) < 0.12f + if (isAccent) { + if (y < sqMinY) sqMinY = y + if (y > sqMaxY) sqMaxY = y + if (x > sqMaxX) sqMaxX = x + } + } + } + // Текст ищем только правее квадратика, чтобы ✓ внутри квадрата не мешал. + for (y in 0 until pixels.height) { + for (x in (sqMaxX + 3) until pixels.width) { + val c = pixels[x, y] + val isText = c.red > 0.55f && c.green > 0.55f && c.blue > 0.55f && + (maxOf(c.red, c.green, c.blue) - minOf(c.red, c.green, c.blue)) < 0.2f + if (isText) { + sawText = true + if (y < txMinY) txMinY = y + if (y > txMaxY) txMaxY = y + } + } + } + + assertTrue(sawText, "глифы подписи не найдены в битмапе") + val sqCenter = (sqMinY + sqMaxY) / 2f + val txCenter = (txMinY + txMaxY) / 2f + val delta = txCenter - sqCenter + assertTrue( + kotlin.math.abs(delta) <= 1.0f, + "центр подписи (y=$txCenter) должен совпадать с центром квадратика " + + "(y=$sqCenter); смещение=${delta}px (sqY=$sqMinY..$sqMaxY, txY=$txMinY..$txMaxY)", + ) + } + + // ── helpers ──────────────────────────────────────────────────────────── private fun sampleConversation( title: String = "Диалог", temporal: Boolean = false,