///|
type RichTextFormatter = (String) -> RichTextDocument

///|
pub(all) struct RichTextInputTransform {
  text : String
  caret : Int
  selection : @core.TextRange?
  // Char range in the previous source replaced by this transform. None means
  // the range is unknown (from_input_transform then falls back to the O(doc)
  // common-prefix diff in from_sources).
  edit_range : MarkdownSourceRange?
} derive(Eq, Debug, ToJson)

///|
pub extend RichTextInputTransform with Debug::{to_repr}

///|
pub extend RichTextInputTransform with Eq::{not_equal, equal}

///|
pub extend RichTextInputTransform with ToJson::{to_json}

///|
pub(all) struct RichTextSourceRange {
  start : Int
  end : Int
} derive(Eq, Debug, ToJson)

///|
pub extend RichTextSourceRange with Debug::{to_repr}

///|
pub extend RichTextSourceRange with Eq::{not_equal, equal}

///|
pub extend RichTextSourceRange with ToJson::{to_json}

///|
pub(all) enum RichTextDecoration {
  Strikethrough
} derive(Eq, Debug, ToJson)

///|
pub extend RichTextDecoration with Debug::{to_repr}

///|
pub extend RichTextDecoration with Eq::{not_equal, equal}

///|
pub extend RichTextDecoration with ToJson::{to_json}

///|
/// Shape-and-state marker drawn in a block's prefix lane instead of a glyph.
///
/// Task list items render a real rounded box with a vector checkmark; a font
/// glyph (`☑`/`☐`) depends on which installed face happens to cover U+2610,
/// so its shape, weight, and color drift per machine. The block keeps a
/// text-shaped `prefix` so caret, selection, and hit-test arithmetic stay
/// character-indexed; only the painted lane swaps the glyph for the marker.
pub(all) enum RichTextMarker {
  TaskCheckbox(Bool)
} derive(Eq, Debug, ToJson)

///|
pub extend RichTextMarker with Debug::{to_repr}

///|
pub extend RichTextMarker with Eq::{not_equal, equal}

///|
pub extend RichTextMarker with ToJson::{to_json}

///|
pub(all) struct RichTextRun {
  text : String
  visual_text : String?
  image_source : String?
  image_size : @core.Size?
  image_width_fraction : Double?
  image_fit : @core.ImageFit
  font : @core.FontSpec?
  color : @core.Color?
  background : @core.Brush?
  background_radius : Double
  decoration : RichTextDecoration?
  source_range : RichTextSourceRange?
} derive(Eq, Debug, ToJson)

///|
pub extend RichTextRun with Debug::{to_repr}

///|
pub extend RichTextRun with Eq::{not_equal, equal}

///|
pub extend RichTextRun with ToJson::{to_json}

///|
/// One table cell. `text` is the cell's raw source (markers included);
/// `runs` is the styled projection (markers hidden, one run per inline
/// span) used by paint/geometry. Empty `runs` means "render `text` plain",
/// which keeps viewer-side tables and any legacy construction unstyled but
/// correct.
pub(all) struct RichTextTableCell {
  text : String
  runs : Array[RichTextRun]
  header : Bool
  align : @core.TextAlign?
  source_range : RichTextSourceRange?
} derive(Eq, Debug, ToJson)

///|
pub extend RichTextTableCell with Debug::{to_repr}

///|
pub extend RichTextTableCell with Eq::{not_equal, equal}

///|
pub extend RichTextTableCell with ToJson::{to_json}

///|
pub(all) struct RichTextTable {
  rows : Array[Array[RichTextTableCell]]
  row_height : Double
  cell_padding : @core.Insets
  border : @core.Color
  header_background : @core.Brush?
} derive(Eq, Debug, ToJson)

///|
pub extend RichTextTable with Debug::{to_repr}

///|
pub extend RichTextTable with Eq::{not_equal, equal}

///|
pub extend RichTextTable with ToJson::{to_json}

///|
pub(all) struct RichTextBlock {
  runs : Array[RichTextRun]
  table : RichTextTable?
  prefix : String
  marker : RichTextMarker?
  font : @core.FontSpec?
  color : @core.Color?
  background : @core.Brush?
  border_left : @core.BorderStyle?
  inset : @core.Insets
  line_height : Double?
  corner_radius : Double
  rule : Bool
  line_align : @core.TextAlign?
  source_range : RichTextSourceRange?
  content_range : RichTextSourceRange?
} derive(Eq, Debug, ToJson)

///|
pub extend RichTextBlock with Debug::{to_repr}

///|
pub extend RichTextBlock with Eq::{not_equal, equal}

///|
pub extend RichTextBlock with ToJson::{to_json}

///|
pub(all) struct RichTextDocument {
  blocks : Array[RichTextBlock]
} derive(Eq, Debug, ToJson)

///|
pub extend RichTextDocument with Debug::{to_repr}

///|
pub extend RichTextDocument with Eq::{not_equal, equal}

///|
pub extend RichTextDocument with ToJson::{to_json}

///|
pub fn RichTextRun::new(
  text~ : String,
  visual_text? : String? = None,
  image_source? : String? = None,
  image_size? : @core.Size? = None,
  image_fit? : @core.ImageFit = Contain,
  font? : @core.FontSpec? = None,
  color? : @core.Color? = None,
  background? : @core.Brush? = None,
  background_radius? : Double = 4.0,
  decoration? : RichTextDecoration? = None,
  source_range? : RichTextSourceRange? = None,
  image_width_fraction? : Double? = None,
) -> RichTextRun {
  {
    text,
    visual_text,
    image_source,
    image_size,
    image_width_fraction,
    image_fit,
    font,
    color,
    background,
    background_radius,
    decoration,
    source_range,
  }
}

///|
pub fn RichTextInputTransform::new(
  text~ : String,
  caret~ : Int,
  selection? : @core.TextRange? = None,
  edit_range? : MarkdownSourceRange? = None,
) -> RichTextInputTransform {
  { text, caret, selection, edit_range, }
}

///|
pub fn RichTextBlock::new(
  runs~ : Array[RichTextRun],
  table? : RichTextTable? = None,
  prefix? : String = "",
  marker? : RichTextMarker? = None,
  font? : @core.FontSpec? = None,
  color? : @core.Color? = None,
  background? : @core.Brush? = None,
  border_left? : @core.BorderStyle? = None,
  inset? : @core.Insets = @core.Insets::symmetric(),
  line_height? : Double? = None,
  corner_radius? : Double = 0.0,
  rule? : Bool = false,
  source_range? : RichTextSourceRange? = None,
  content_range? : RichTextSourceRange? = None,
  line_align? : @core.TextAlign? = None,
) -> RichTextBlock {
  {
    runs,
    table,
    prefix,
    marker,
    font,
    color,
    background,
    border_left,
    inset,
    line_height,
    corner_radius,
    rule,
    line_align,
    source_range,
    content_range,
  }
}

///|
pub fn RichTextDocument::new(
  blocks~ : Array[RichTextBlock],
) -> RichTextDocument {
  { blocks, }
}

///|
pub fn RichTextDocument::plain(text : String) -> RichTextDocument {
  let blocks : Array[RichTextBlock] = []
  let current : Array[Char] = []
  for ch in text {
    if ch == '\n' {
      blocks.push(
        RichTextBlock::new(runs=[
          RichTextRun::new(text=String::from_array(current)),
        ]),
      )
      current.clear()
    } else {
      current.push(ch)
    }
  }
  blocks.push(
    RichTextBlock::new(runs=[RichTextRun::new(text=String::from_array(current))]),
  )
  RichTextDocument::new(blocks~)
}