diff --git a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/App.kt b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/App.kt index 27da795..d4a33c7 100644 --- a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/App.kt +++ b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/App.kt @@ -38,6 +38,8 @@ import pw.binom.agentik.desktop.ui.screens.ConversationList import pw.binom.agentik.desktop.ui.screens.GroupsDialog import pw.binom.agentik.desktop.ui.screens.NewChatDialog import pw.binom.agentik.desktop.ui.theme.AgentikTheme +import pw.binom.agentik.desktop.ui.theme.ProvideAgentikTheme +import pw.binom.agentik.desktop.ui.theme.toAgentikTheme private sealed interface AppDialog { data object Agents : AppDialog @@ -52,7 +54,7 @@ private sealed interface AppDialog { */ @Composable fun App(state: AppState) { - AgentikTheme(themeId = state.settingsState.themeId) { + ProvideAgentikTheme(theme = state.settingsState.themeId.toAgentikTheme()) { CompositionLocalProvider(LocalAvatarStorage provides state.avatars) { LaunchedEffect(Unit) { state.start() } Root(state) diff --git a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/ChatImage.kt b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/ChatImage.kt index ff417f9..8725b3f 100644 --- a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/ChatImage.kt +++ b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/ChatImage.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import pw.binom.agentik.desktop.ui.ClientImageSupport +import pw.binom.agentik.desktop.ui.theme.AgentikTheme import java.util.Base64 private enum class ImageLoadState { Loading, Ready, Error } @@ -41,6 +42,7 @@ fun ChatImage(src: String, alt: String?) { // Декодер не зарегистрирован — fallback: alt (или ссылка) текстом. Text( text = alt?.takeIf { it.isNotBlank() } ?: src, + color = AgentikTheme.colors.text, modifier = Modifier.widthIn(max = 280.dp), ) return diff --git a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownRendering.kt b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownRendering.kt index a4956f6..18acaa1 100644 --- a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownRendering.kt +++ b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownRendering.kt @@ -18,12 +18,9 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.ClickableText -import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -38,52 +35,55 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle -import pw.binom.agentik.desktop.ui.theme.AgentikColors -import pw.binom.agentik.desktop.ui.theme.AgentikTheme -import pw.binom.agentik.desktop.ui.theme.darkAgentikColors import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import pw.binom.agentik.desktop.markdown.MarkdownSegment import pw.binom.agentik.desktop.markdown.TextStyleSpan import pw.binom.agentik.desktop.markdown.parseMarkdown +/** Скругление у блоков (код, таблица). Геометрия, а не тема — поэтому константа. */ +private val RoundedBlockShape = RoundedCornerShape(8.dp) + +/** Ширина колонки таблицы ≈ [TableCharWidth] на символ, но не уже [TableMinColumnWidth]. */ +private val TableCharWidth = 9.dp +private val TableMinColumnWidth = 60.dp + // ───────────────────────────────────────────────────────────── // Public rendering entry point // ───────────────────────────────────────────────────────────── /** - * Parse markdown and render segments with the given base text style. + * Разбирает markdown и рисует сегменты стилем [style] (по умолчанию — текущий + * стиль темы из [LocalMarkdownStyle]). */ @Composable fun renderMarkdownSegments( content: String, - textColor: Color, - fontSize: TextUnit, isBubbleScrolled: Boolean = false, onUrlClick: (String) -> Unit = {}, + style: MarkdownStyle = LocalMarkdownStyle.current, ) { - val baseStyle = SpanStyle(color = textColor, fontSize = fontSize) - val colors = AgentikTheme.colors - val segments = parseMarkdown(content) + val segments = remember(content) { parseMarkdown(content) } var previousWasText = false for (segment in segments) { when (segment) { is MarkdownSegment.Text -> { - val annotated = segment.toAnnotatedString(baseStyle, colors) + val annotated = segment.toAnnotatedString(style) if (annotated.text.isNotEmpty()) { // Между соседними абзацами (последовательными текстовыми сегментами) // оставляем вертикальный отступ, иначе абзацы склеиваются в сплошной блок. if (previousWasText) { - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(style.paragraphSpacing)) } clickableMarkdownText( annotatedString = annotated, + lineHeight = style.bodyLineHeight, modifier = Modifier - .then(if (isBubbleScrolled) Modifier.widthIn(max = 600.dp) else Modifier), + .then(if (isBubbleScrolled) Modifier.widthIn(max = style.maxContentWidth) else Modifier), onUrlClick = onUrlClick, ) previousWasText = true @@ -92,76 +92,79 @@ fun renderMarkdownSegments( is MarkdownSegment.CodeBlock -> { previousWasText = false val clipboardManager = LocalClipboardManager.current - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(style.blockSpacing)) Column( modifier = Modifier .then( - if (isBubbleScrolled) Modifier.widthIn(max = 600.dp) + if (isBubbleScrolled) Modifier.widthIn(max = style.maxContentWidth) else Modifier.fillMaxWidth() ) - .clip(RoundedCornerShape(8.dp)) - .background(colors.codeBackground), + .clip(RoundedBlockShape) + .background(style.codeBackground), ) { Row( modifier = Modifier .fillMaxWidth() - .background(AgentikTheme.colors.panelElevated) - .padding(horizontal = 12.dp, vertical = 6.dp), + .background(style.codeHeaderBackground) + .padding( + horizontal = style.codeHeaderPaddingHorizontal, + vertical = style.codeHeaderPaddingVertical, + ), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Text( text = segment.language ?: "Code", - fontSize = 12.sp, - fontFamily = FontFamily.Monospace, - color = colors.textDim, + fontSize = style.labelFontSize, + fontFamily = style.monospaceFamily, + color = style.labelColor, ) Icon( imageVector = Icons.Default.ContentCopy, contentDescription = "Скопировать", modifier = Modifier - .size(16.dp) + .size(style.copyIconSize) .clickable { clipboardManager.setText(AnnotatedString(segment.code)) }, - tint = colors.textDim, + tint = style.labelColor, ) } Box( modifier = Modifier - .padding(12.dp) + .padding(style.codePadding) .horizontalScroll(rememberScrollState()), ) { Text( text = segment.code, - fontFamily = FontFamily.Monospace, - fontSize = 13.sp, - color = colors.codeText, + fontFamily = style.monospaceFamily, + fontSize = style.codeFontSize, + color = style.codeTextColor, ) } } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(style.blockSpacing)) } is MarkdownSegment.Table -> { previousWasText = false - renderTable(segment, textColor, fontSize, isBubbleScrolled) + renderTable(segment, style, isBubbleScrolled) } is MarkdownSegment.Image -> { previousWasText = false - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(style.blockSpacing)) ChatImage(src = segment.src, alt = segment.alt) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(style.blockSpacing)) } is MarkdownSegment.ThematicBreak -> { previousWasText = false - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(style.blockSpacing)) Box( modifier = Modifier .fillMaxWidth() .height(1.dp) - .background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.25f)), + .background(style.divider), ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(style.blockSpacing)) } } } @@ -174,11 +177,10 @@ fun renderMarkdownSegments( @Composable private fun renderTable( segment: MarkdownSegment.Table, - textColor: Color, - fontSize: TextUnit, + style: MarkdownStyle, isBubbleScrolled: Boolean, ) { - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(style.tableSpacing)) val columnWidths = remember(segment) { val columnCount = segment.headers.size @@ -203,15 +205,14 @@ private fun renderTable( Column( modifier = Modifier .then(if (isBubbleScrolled) Modifier.wrapContentWidth() else Modifier.fillMaxWidth()) - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.3f)) + .clip(RoundedBlockShape) + .background(style.tableSurface) ) { if (isBubbleScrolled) { TableRows( segment = segment, columnWidths = columnWidths, - textColor = textColor, - fontSize = fontSize, + style = style, ) } else { Box( @@ -223,30 +224,27 @@ private fun renderTable( TableRows( segment = segment, columnWidths = columnWidths, - textColor = textColor, - fontSize = fontSize, + style = style, ) } } } } - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(style.tableSpacing)) } @Composable private fun TableRows( segment: MarkdownSegment.Table, columnWidths: List, - textColor: Color, - fontSize: TextUnit, + style: MarkdownStyle, ) { // Header row TableRow( cells = segment.headers, widths = columnWidths, isHeader = true, - textColor = textColor, - fontSize = fontSize, + style = style, ) // Data rows segment.rows.forEachIndexed { idx, row -> @@ -254,8 +252,7 @@ private fun TableRows( cells = row, widths = columnWidths, isHeader = false, - textColor = textColor, - fontSize = fontSize, + style = style, rowIndex = idx, ) } @@ -266,14 +263,13 @@ private fun TableRow( cells: List, widths: List, isHeader: Boolean, - textColor: Color, - fontSize: TextUnit, + style: MarkdownStyle, rowIndex: Int = 0, ) { val bgColor = when { - isHeader -> MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) + isHeader -> style.tableHeaderBackground rowIndex % 2 == 0 -> Color.Transparent - else -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.03f) + else -> style.tableStripeBackground } Row( modifier = Modifier @@ -283,13 +279,19 @@ private fun TableRow( cells.forEachIndexed { i, cell -> Box( modifier = Modifier - .width((widths.getOrElse(i) { 10 } * 9).dp.coerceAtLeast(60.dp)) - .padding(horizontal = 8.dp, vertical = 6.dp), + .width( + (TableCharWidth * widths.getOrElse(i) { 10 }) + .coerceAtLeast(TableMinColumnWidth) + ) + .padding( + horizontal = style.tableCellPaddingHorizontal, + vertical = style.tableCellPaddingVertical, + ), ) { Text( text = cell, - color = textColor, - fontSize = fontSize, + color = style.textColor, + fontSize = style.tableFontSize, fontWeight = if (isHeader) FontWeight.SemiBold else FontWeight.Normal, softWrap = true, ) @@ -306,6 +308,7 @@ private fun TableRow( private fun clickableMarkdownText( annotatedString: AnnotatedString, modifier: Modifier = Modifier, + lineHeight: TextUnit = TextUnit.Unspecified, onUrlClick: (String) -> Unit = {}, ) { val urlAnnotations = annotatedString.getStringAnnotations("URL", 0, annotatedString.length) @@ -317,6 +320,7 @@ private fun clickableMarkdownText( var textLayoutResult by remember { mutableStateOf(null) } Text( text = annotatedString, + lineHeight = lineHeight, modifier = modifier.pointerInput(annotatedString) { detectTapGestures { offset -> val layout = textLayoutResult ?: return@detectTapGestures @@ -334,7 +338,7 @@ private fun clickableMarkdownText( onTextLayout = { textLayoutResult = it }, ) } else { - Text(text = annotatedString, modifier = modifier) + Text(text = annotatedString, lineHeight = lineHeight, modifier = modifier) } } @@ -342,16 +346,27 @@ private fun clickableMarkdownText( // Convert MarkdownSegment.Text to Compose AnnotatedString // ───────────────────────────────────────────────────────────── +/** + * Текст сегмента → [AnnotatedString]. + * + * База ([baseStyle], по умолчанию [MarkdownStyle.bodySpan]) накладывается на ВЕСЬ + * текст, а не только на размеченные спаны: парсер заводит [TextStyleSpan] лишь + * для жирного/курсива/кода/ссылок, поэтому «простой» текст без базы оставался + * без цвета и рисовался цветом по умолчанию (чёрным) — это и был баг «текст + * чёрный вместо белого». + */ fun MarkdownSegment.Text.toAnnotatedString( - baseStyle: SpanStyle = SpanStyle(), - colors: AgentikColors = darkAgentikColors, + style: MarkdownStyle, + baseStyle: SpanStyle = style.bodySpan(), ): AnnotatedString { val builder = AnnotatedString.Builder(text) + if (text.isNotEmpty()) { + builder.addStyle(baseStyle, 0, text.length) + } for (span in spans) { - val style = buildSpanStyle(span, colors) - val merged = baseStyle.merge(style) - builder.addStyle(merged, span.start, span.end) + val spanStyle = buildSpanStyle(span, style) + builder.addStyle(baseStyle.merge(spanStyle), span.start, span.end) val link = span.link if (link != null) { builder.addStringAnnotation("URL", link, span.start, span.end) @@ -360,23 +375,23 @@ fun MarkdownSegment.Text.toAnnotatedString( return builder.toAnnotatedString() } -private fun buildSpanStyle(span: TextStyleSpan, colors: AgentikColors): SpanStyle { - var style = SpanStyle() - if (span.bold) style = style.copy(fontWeight = FontWeight.Bold) - if (span.italic) style = style.copy(fontStyle = androidx.compose.ui.text.font.FontStyle.Italic) +private fun buildSpanStyle(span: TextStyleSpan, style: MarkdownStyle): SpanStyle { + var out = SpanStyle() + if (span.bold) out = out.copy(fontWeight = FontWeight.Bold) + if (span.italic) out = out.copy(fontStyle = FontStyle.Italic) if (span.code) { - style = style.copy( - fontFamily = FontFamily.Monospace, - fontSize = 13.sp, - background = colors.panelElevated, + out = out.copy( + fontFamily = style.monospaceFamily, + fontSize = style.inlineCodeFontSize, + background = style.inlineCodeBackground, ) } - if (span.strikethrough) style = style.copy(textDecoration = androidx.compose.ui.text.style.TextDecoration.LineThrough) + if (span.strikethrough) out = out.copy(textDecoration = TextDecoration.LineThrough) if (span.link != null) { - style = style.copy( - color = colors.accent, - textDecoration = androidx.compose.ui.text.style.TextDecoration.Underline, + out = out.copy( + color = style.linkColor, + textDecoration = TextDecoration.Underline, ) } - return style + return out } diff --git a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/screens/MessageStream.kt b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/screens/MessageStream.kt index 48f3b62..8828397 100644 --- a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/screens/MessageStream.kt +++ b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/screens/MessageStream.kt @@ -205,15 +205,10 @@ private fun TextContent(content: List, textColor: Color, fontSize: an @Composable private fun MarkdownContent(content: List, agentColor: Color) { - val colors = AgentikTheme.colors Column { content.forEach { part -> when (part) { - is UiContent.Text -> renderMarkdownSegments( - content = part.body, - textColor = colors.text, - fontSize = 13.5.sp, - ) + is UiContent.Text -> renderMarkdownSegments(content = part.body) is UiContent.Image -> { ChatImage(src = dataUrl(part), alt = null) diff --git a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/theme/Theme.kt b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/theme/Theme.kt index 0c2b422..cd4b747 100644 --- a/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/theme/Theme.kt +++ b/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/theme/Theme.kt @@ -3,6 +3,7 @@ package pw.binom.agentik.desktop.ui.theme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable @@ -10,6 +11,9 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import pw.binom.agentik.desktop.settings.ThemeId +import pw.binom.agentik.desktop.ui.AgentikMarkdownStyle +import pw.binom.agentik.desktop.ui.LocalMarkdownStyle +import pw.binom.agentik.desktop.ui.MarkdownStyle /** * Семантические цвета интерфейса. **Ни один цвет не задаётся числом вне @@ -146,30 +150,83 @@ val lightAgentikColors = AgentikColors( codeText = Color(0xFF1A1D23), ) -private val LocalAgentikColors = staticCompositionLocalOf { darkAgentikColors } - -/** Доступ к семантическим цветам из экранов: `AgentikTheme.colors.panel`. */ -object AgentikTheme { +/** + * Контракт темы приложения. + * + * Все параметры, которые в UI зависят от выбранной темы, живут здесь: + * - [colors] — семантические цвета (`AgentikColors`); + * - [markdown] — стиль отрисовки md (`MarkdownStyle`, см. `MarkdownStyle.kt`); + * - [dark] — для построения Material-схемы (`toMaterialScheme(dark)`). + * + * Чтобы добавить новую тему, достаточно положить ещё один `object` сюда и + * поднять его в настройках через [ThemeId] + [ThemeId.toTheme]. Экраны и + * рендер md не меняются — они читают только [AgentikTheme.current]. + */ +interface AgentikTheme { val colors: AgentikColors - @Composable - @ReadOnlyComposable - get() = LocalAgentikColors.current + val markdown: MarkdownStyle + val dark: Boolean + + companion object { + /** Активная тема (то, что положили в [ProvideAgentikTheme]). */ + val current: AgentikTheme + @Composable + @ReadOnlyComposable + get() = LocalAgentikTheme.current + + /** Короткий доступ к [AgentikTheme.colors] — для кода вида `val colors = AgentikTheme.colors`. */ + val colors: AgentikColors + @Composable + @ReadOnlyComposable + get() = current.colors + + /** Короткий доступ к [AgentikTheme.markdown] — для `renderMarkdownSegments` и т. п. */ + val markdown: MarkdownStyle + @Composable + @ReadOnlyComposable + get() = current.markdown + } } /** - * Корневой провайдер темы. Оборачивает всё приложение; [themeId] — значение - * из настроек (`ThemeId.DARK` / `ThemeId.LIGHT`). + * Локаль, через которую [AgentikTheme.current] доходит до экранов. + * Объявлен на уровне файла, чтобы [ProvideAgentikTheme] мог им пользоваться + * напрямую (внутри companion-объекта интерфейса он был бы недоступен снаружи). + */ +private val LocalAgentikTheme = staticCompositionLocalOf { DarkAgentikTheme } + +/** Тёмная тема — значения `darkAgentikColors` + её markdown-стиль. */ +object DarkAgentikTheme : AgentikTheme { + override val colors: AgentikColors = darkAgentikColors + override val markdown: MarkdownStyle = AgentikMarkdownStyle(darkAgentikColors) + override val dark: Boolean = true +} + +/** Светлая тема — значения `lightAgentikColors` + её markdown-стиль. */ +object LightAgentikTheme : AgentikTheme { + override val colors: AgentikColors = lightAgentikColors + override val markdown: MarkdownStyle = AgentikMarkdownStyle(lightAgentikColors) + override val dark: Boolean = false +} + +/** Преобразует значение [ThemeId] из настроек (`AgentikTheme.colors.themeId` хранится как `String`) в активный [AgentikTheme]. */ +fun String.toAgentikTheme(): AgentikTheme = if (this == ThemeId.LIGHT) LightAgentikTheme else DarkAgentikTheme + +/** + * Корневой провайдер темы. Оборачивает всё приложение; принимает готовый + * экземпляр [AgentikTheme] (обычно результат [ThemeId.toTheme]). */ @Composable -fun AgentikTheme( - themeId: String, - content: @Composable () -> Unit, -) { - val dark = themeId != ThemeId.LIGHT - val colors = if (dark) darkAgentikColors else lightAgentikColors - CompositionLocalProvider(LocalAgentikColors provides colors) { +fun ProvideAgentikTheme(theme: AgentikTheme, content: @Composable () -> Unit) { + CompositionLocalProvider( + LocalAgentikTheme provides theme, + // MaterialTheme сам LocalContentColor не подкладывает: без этого любой + // Text без явного color рисовался бы чёрным (дефолт локала), а не текстом темы. + LocalContentColor provides theme.colors.text, + LocalMarkdownStyle provides theme.markdown, + ) { MaterialTheme( - colorScheme = colors.toMaterialScheme(dark = dark), + colorScheme = theme.colors.toMaterialScheme(dark = theme.dark), content = content, ) } diff --git a/src/jvmTest/kotlin/pw/binom/agentik/desktop/ui/ThemeRenderTest.kt b/src/jvmTest/kotlin/pw/binom/agentik/desktop/ui/ThemeRenderTest.kt index 15b5a8b..7556511 100644 --- a/src/jvmTest/kotlin/pw/binom/agentik/desktop/ui/ThemeRenderTest.kt +++ b/src/jvmTest/kotlin/pw/binom/agentik/desktop/ui/ThemeRenderTest.kt @@ -13,11 +13,12 @@ import androidx.compose.ui.test.captureToImage import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import pw.binom.agentik.desktop.ui.theme.AgentikColors import pw.binom.agentik.desktop.ui.theme.AgentikTheme import pw.binom.agentik.desktop.ui.theme.darkAgentikColors import pw.binom.agentik.desktop.ui.theme.lightAgentikColors +import pw.binom.agentik.desktop.ui.theme.ProvideAgentikTheme +import pw.binom.agentik.desktop.ui.theme.toAgentikTheme import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -80,14 +81,10 @@ class ThemeRenderTest { @Test fun codeBlockPaintsThemeCodeBackgroundInLightTheme() = runComposeUiTest { setContent { - AgentikTheme("light") { + ProvideAgentikTheme("light".toAgentikTheme()) { Box(Modifier.fillMaxSize().background(AgentikTheme.colors.botBubble).padding(8.dp)) { Column { - renderMarkdownSegments( - content = "```kotlin\nval x = 1\n```", - textColor = AgentikTheme.colors.text, - fontSize = 14.sp, - ) + renderMarkdownSegments(content = "```kotlin\nval x = 1\n```") } } } @@ -116,14 +113,10 @@ class ThemeRenderTest { @Test fun darkThemeStillRendersCodeBlockDark() = runComposeUiTest { setContent { - AgentikTheme("dark") { + ProvideAgentikTheme("dark".toAgentikTheme()) { Box(Modifier.fillMaxSize().background(AgentikTheme.colors.botBubble).padding(8.dp)) { Column { - renderMarkdownSegments( - content = "```kotlin\nval x = 1\n```", - textColor = AgentikTheme.colors.text, - fontSize = 14.sp, - ) + renderMarkdownSegments(content = "```kotlin\nval x = 1\n```") } } } diff --git a/src/jvmTest/kotlin/pw/binom/agentik/desktop/voice/RealSpeechIT.kt b/src/jvmTest/kotlin/pw/binom/agentik/desktop/voice/RealSpeechIT.kt index bf54472..a7cf55d 100644 --- a/src/jvmTest/kotlin/pw/binom/agentik/desktop/voice/RealSpeechIT.kt +++ b/src/jvmTest/kotlin/pw/binom/agentik/desktop/voice/RealSpeechIT.kt @@ -6,9 +6,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.runComposeUiTest -import androidx.compose.ui.unit.sp import pw.binom.agentik.desktop.ui.renderMarkdownSegments import pw.binom.agentik.desktop.ui.theme.AgentikTheme +import pw.binom.agentik.desktop.ui.theme.ProvideAgentikTheme +import pw.binom.agentik.desktop.ui.theme.toAgentikTheme import kotlin.test.Test import kotlin.test.assertTrue @@ -38,12 +39,10 @@ class RealSpeechIT { // 2. skiko/Compose инициализируется и реально рисует (как окно приложения). runComposeUiTest { setContent { - AgentikTheme("dark") { + ProvideAgentikTheme("dark".toAgentikTheme()) { Box(Modifier.fillMaxSize().background(AgentikTheme.colors.botBubble)) { renderMarkdownSegments( content = "проверка: **окно** уже отрисовано", - textColor = AgentikTheme.colors.text, - fontSize = 14.sp, ) } }