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 { DefaultMarkdownStyle }