Files
agentik-desktop/src/jvmMain/kotlin/pw/binom/agentik/desktop/ui/MarkdownStyle.kt
T

153 lines
6.9 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pw.binom.agentik.desktop.ui
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
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.darkAgentikColors
/**
* Стиль отрисовки markdown: цвета, шрифты и отступы, из которых рендер
* ([renderMarkdownSegments]) берёт решительно всё.
*
* Зачем отдельный контракт: рендер не должен знать ни про тему приложения, ни
* про конкретные числа. Раньше он получал только `textColor` + `fontSize`, а
* остальное (фон код-блока, цвет ссылки, отступы, фон таблицы) тянул из
* `AgentikTheme`/`MaterialTheme` напрямую — из-за чего часть слотов жила вне
* темы, а тело сообщения могло уехать в цвет по умолчанию (чёрный).
*
* Реализация под наш клиент — [AgentikMarkdownStyle]; текущий стиль лежит в
* [LocalMarkdownStyle] (его подкладывает `AgentikTheme`), поэтому вызову
* достаточно `renderMarkdownSegments(content)`.
*/
@Immutable
interface MarkdownStyle {
/** Цвет основного текста сообщения (в тёмной теме — светлый). */
val textColor: Color
/** Кегль тела сообщения. */
val bodyFontSize: TextUnit
val bodyFontFamily: FontFamily
/** Интервал строк тела; [TextUnit.Unspecified] — на усмотрение движка. */
val bodyLineHeight: TextUnit
/** Ссылки: цвет + подчёркивание. */
val linkColor: Color
/** Inline-код: `code`. */
val monospaceFamily: FontFamily
val inlineCodeFontSize: TextUnit
val inlineCodeBackground: Color
/** Блок кода (``` … ```). */
val codeBackground: Color
val codeTextColor: Color
val codeHeaderBackground: Color
val codeFontSize: TextUnit
/** Таблица. */
val tableSurface: Color
val tableHeaderBackground: Color
val tableStripeBackground: Color
val tableFontSize: TextUnit
/** Мелкие подписи (язык код-блока, иконка копирования) и разделитель `---`. */
val labelColor: Color
val labelFontSize: TextUnit
val divider: Color
/** Отступы. */
val paragraphSpacing: Dp
val blockSpacing: Dp
val tableSpacing: Dp
val codePadding: Dp
val codeHeaderPaddingHorizontal: Dp
val codeHeaderPaddingVertical: Dp
val tableCellPaddingHorizontal: Dp
val tableCellPaddingVertical: Dp
val copyIconSize: Dp
/** Ширина контента, когда сообщение лежит в горизонтально скроллящемся пузыре. */
val maxContentWidth: Dp
}
/**
* База, накладываемая на ВЕСЬ текстовый сегмент.
*
* Парсер заводит стилевые спаны только для размеченных участков (жирный/код/
* ссылка), поэтому без этой базы «простой» текст остаётся без цвета и рисуется
* цветом по умолчанию (чёрным) — это и был баг «текст чёрный вместо белого».
*
* Именно функция, а не свойство интерфейса: с делегированием
* (`object : MarkdownStyle by otherStyle`) свойство с реализацией по умолчанию
* уезжает в делегат и перекрытый `textColor` в базе не учитывается.
* [bodyLineHeight] сюда не входит — интервал строк задаётся у абзаца
* (`Text(lineHeight = …)`), в `SpanStyle` такого слота нет.
*/
fun MarkdownStyle.bodySpan(): SpanStyle = SpanStyle(
color = textColor,
fontSize = bodyFontSize,
fontFamily = bodyFontFamily,
)
/**
* Наш стиль: цвета — из [AgentikColors] активной темы, типографика и отступы —
* из макетов клиента (`sketches/index.html`).
*/
@Immutable
class AgentikMarkdownStyle(private val colors: AgentikColors) : MarkdownStyle {
override val textColor: Color get() = colors.text
override val bodyFontSize: TextUnit = 13.5.sp
override val bodyFontFamily: FontFamily = FontFamily.Default
override val bodyLineHeight: TextUnit = 20.sp
override val linkColor: Color get() = colors.accent
override val monospaceFamily: FontFamily = FontFamily.Monospace
override val inlineCodeFontSize: TextUnit = 13.sp
override val inlineCodeBackground: Color get() = colors.panelElevated
override val codeBackground: Color get() = colors.codeBackground
override val codeTextColor: Color get() = colors.codeText
override val codeHeaderBackground: Color get() = colors.panelElevated
override val codeFontSize: TextUnit = 13.sp
override val tableSurface: Color get() = colors.panel.copy(alpha = 0.3f)
override val tableHeaderBackground: Color get() = colors.accent.copy(alpha = 0.1f)
override val tableStripeBackground: Color get() = colors.text.copy(alpha = 0.03f)
override val tableFontSize: TextUnit = 13.5.sp
override val labelColor: Color get() = colors.textDim
override val labelFontSize: TextUnit = 12.sp
override val divider: Color get() = colors.text.copy(alpha = 0.25f)
override val paragraphSpacing: Dp = 12.dp
override val blockSpacing: Dp = 4.dp
override val tableSpacing: Dp = 6.dp
override val codePadding: Dp = 12.dp
override val codeHeaderPaddingHorizontal: Dp = 12.dp
override val codeHeaderPaddingVertical: Dp = 6.dp
override val tableCellPaddingHorizontal: Dp = 8.dp
override val tableCellPaddingVertical: Dp = 6.dp
override val copyIconSize: Dp = 16.dp
override val maxContentWidth: Dp = 600.dp
}
/** Стиль по умолчанию — тёмная тема (эталонная); совпадает с дефолтом [AgentikColors]. */
private val DefaultMarkdownStyle: MarkdownStyle = AgentikMarkdownStyle(darkAgentikColors)
/**
* Текущий стиль markdown. Подкладывается `AgentikTheme`, поэтому экраны читают
* его неявно: `renderMarkdownSegments(content)`.
*/
val LocalMarkdownStyle = staticCompositionLocalOf<MarkdownStyle> { DefaultMarkdownStyle }