153 lines
6.9 KiB
Kotlin
153 lines
6.9 KiB
Kotlin
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 }
|