///|
fn rich_text_editor_paint(
  ctx : @core.ViewPaintContext,
  value~ : @core.Binding[String],
  placeholder~ : String,
  format~ : RichTextFormatter,
  font~ : @core.FontSpec,
  foreground~ : @core.Color,
  placeholder_color~ : @core.Color,
  background~ : @core.Brush,
  border~ : @core.BorderStyle?,
  focused_border~ : @core.BorderStyle?,
  cursor_color~ : @core.Color,
  corner_radius~ : Double,
  visible_scroll_y~ : Double,
  visible_viewport_height~ : Double,
  visible_overscan~ : Double,
  visible_scroll_state~ : @core.ScrollState?,
) -> @core.ViewPaintPlan {
  let commands : Array[@core.DrawCommand] = []
  let overlay_commands : Array[@core.DrawCommand] = []
  let rounded = @core.RoundedRect::new(rect=ctx.frame, radius=corner_radius)
  commands.push(@core.DrawCommand::FillRoundedRectBrush(rounded, background))
  append_rich_text_border(
    commands,
    rounded,
    if ctx.focused {
      focused_border
    } else {
      border
    },
  )
  let text = value.get()
  let display_text = rich_text_with_composition(
    text,
    ctx.text_control,
    focused=ctx.focused,
  )
  let content = rich_text_editor_content_rect(ctx.frame)
  let scroll_y = markdown_session_current_scroll_y(
    visible_scroll_y, visible_scroll_state,
  )
  let document_content = rich_text_scrolled_content_rect(
    content,
    scroll_y~,
    visible_viewport_height~,
  )
  commands.push(@core.DrawCommand::PushClip(content))
  let child_layers : Array[@core.ViewPaintLayer] = []
  if display_text == "" {
    commands.push(
      @core.DrawCommand::DrawText(
        @core.TextRun::new(
          text=placeholder,
          frame=content,
          font~,
          color=placeholder_color,
          align=@core.TextAlign::TextStart,
        ),
      ),
    )
  } else {
    let document = format(display_text)
    append_rich_text_document_layers(
      child_layers,
      document,
      document_content,
      font,
      foreground,
      text_system=ctx.text_system,
      visible_scroll_y=scroll_y,
      visible_viewport_height~,
      visible_overscan~,
      clip_rect=Some(content),
    )
    if ctx.focused {
      append_rich_text_editor_overlay(
        overlay_commands,
        text~,
        display_text~,
        document~,
        content=document_content,
        font~,
        cursor_color~,
        control=ctx.text_control,
        text_system=ctx.text_system,
      )
    }
  }
  // PopClip must go in post_commands so child_layers stay inside the clip.
  {
    commands,
    child_layers,
    post_commands: [@core.DrawCommand::PopClip],
    overlay_commands,
    platform_views: [],
    repaint_boundary: false,
    animating: false,
  }
}

///|
fn append_rich_text_border(
  commands : Array[@core.DrawCommand],
  rounded : @core.RoundedRect,
  border : @core.BorderStyle?,
) -> Unit {
  match border {
    Some(border) =>
      if border.width > 0.0 {
        commands.push(
          @core.DrawCommand::StrokeRoundedRectBrush(
            rounded,
            border.brush,
            border.width,
          ),
        )
      }
    None => ()
  }
}

///|
fn rich_text_scrolled_content_rect(
  content : @core.Rect,
  scroll_y~ : Double,
  visible_viewport_height~ : Double,
) -> @core.Rect {
  if visible_viewport_height > 0.0 {
    content.offset(dx=0.0, dy=-scroll_y)
  } else {
    content
  }
}

///|
fn append_rich_text_document_layers(
  layers : Array[@core.ViewPaintLayer],
  document : RichTextDocument,
  content : @core.Rect,
  base_font : @core.FontSpec,
  foreground : @core.Color,
  text_system~ : @core.TextSystem,
  visible_scroll_y? : Double = 0.0,
  visible_viewport_height? : Double = 0.0,
  visible_overscan? : Double = 320.0,
  clip_rect? : @core.Rect? = None,
) -> Unit {
  // `content` sets the block positioning origin; `clip` sets the culling
  // bounds. They are the same for the regular (non-session) rich text editor
  // path, but the Markdown session path positions blocks inside an offset
  // `window_content` rect while culling must respect the original viewport.
  let clip = match clip_rect {
    Some(r) => r
    None => content
  }
  let mut y = content.origin.y
  let windowed = visible_viewport_height > 0.0
  let visible_top = clip.origin.y + visible_scroll_y - 12.0 - visible_overscan
  let visible_bottom = visible_top +
    visible_viewport_height +
    visible_overscan * 2.0
  for index, block in document.blocks {
    let line_height = rich_text_block_line_height(block, base_font, text_system)
    let block_height = line_height + block.inset.top + block.inset.bottom
    let block_bottom = y + block_height
    if windowed && block_bottom < visible_top {
      y = block_bottom
      continue
    }
    if windowed && y > visible_bottom {
      return
    }
    if !windowed && y > clip.origin.y + clip.size.height {
      return
    }
    let frame = @core.Rect::new(
      x=content.origin.x,
      y~,
      width=content.size.width,
      height=block_height,
    )
    let commands : Array[@core.DrawCommand] = []
    append_rich_text_block_commands(
      commands,
      block,
      frame,
      line_height,
      base_font,
      foreground,
      text_system~,
    )
    layers.push(
      @core.ViewPaintLayer::new(
        frame~,
        cache_key=rich_text_block_cache_key(index, block, frame),
        content_revision=rich_text_block_content_revision(
          block, index, frame, base_font, foreground,
        ),
        commands~,
      ),
    )
    y = block_bottom
  }
}

///|
fn append_rich_text_block_commands(
  commands : Array[@core.DrawCommand],
  block : RichTextBlock,
  block_frame : @core.Rect,
  line_height : Double,
  base_font : @core.FontSpec,
  foreground : @core.Color,
  text_system~ : @core.TextSystem,
) -> Unit {
  match block.background {
    Some(background) =>
      commands.push(
        @core.DrawCommand::FillRoundedRectBrush(
          @core.RoundedRect::new(rect=block_frame, radius=block.corner_radius),
          background,
        ),
      )
    None => ()
  }
  match block.border_left {
    Some(border) =>
      if border.width > 0.0 {
        commands.push(
          @core.DrawCommand::FillRoundedRectBrush(
            @core.RoundedRect::new(
              rect=@core.Rect::new(
                x=block_frame.origin.x,
                y=block_frame.origin.y + block.inset.top,
                width=border.width,
                height=line_height,
              ),
              radius=border.width / 2.0,
            ),
            border.brush,
          ),
        )
      }
    None => ()
  }
  if block.rule {
    commands.push(
      @core.DrawCommand::FillRect(
        @core.Rect::new(
          x=block_frame.origin.x + block.inset.left,
          y=block_frame.origin.y + block_frame.size.height / 2.0,
          width=max_double(
            0.0,
            block_frame.size.width - block.inset.left - block.inset.right,
          ),
          height=1.5,
        ),
        rich_text_block_color(block, foreground).multiply_alpha(0.25),
      ),
    )
  } else if block.table is Some(_) {
    append_rich_text_block_table(
      commands,
      block,
      @core.Rect::new(
        x=block_frame.origin.x + block.inset.left,
        y=block_frame.origin.y + block.inset.top,
        width=max_double(
          0.0,
          block_frame.size.width - block.inset.left - block.inset.right,
        ),
        height=line_height,
      ),
      base_font,
      foreground,
    )
  } else {
    append_rich_text_block_runs(
      commands,
      block,
      @core.Rect::new(
        x=block_frame.origin.x + block.inset.left,
        y=block_frame.origin.y + block.inset.top,
        width=max_double(
          0.0,
          block_frame.size.width - block.inset.left - block.inset.right,
        ),
        height=line_height,
      ),
      base_font,
      foreground,
      text_system~,
    )
  }
}

///|
fn append_rich_text_block_runs(
  commands : Array[@core.DrawCommand],
  block : RichTextBlock,
  frame : @core.Rect,
  base_font : @core.FontSpec,
  foreground : @core.Color,
  text_system~ : @core.TextSystem,
) -> Unit {
  let block_font = rich_text_block_font(block, base_font)
  let block_color = rich_text_block_color(block, foreground)
  let line_step = rich_text_block_line_step(block, base_font, text_system)
  let mut x = frame.origin.x
  let mut line = 0
  if block.prefix != "" {
    let width = rich_text_width(block.prefix, block_font, text_system)
    let prefix_frame = rich_text_text_paint_frame(
      x~,
      y=frame.origin.y,
      width~,
      height=frame.size.height,
      line_right=frame.origin.x + frame.size.width,
      font=block_font,
    )
    commands.push(
      @core.DrawCommand::DrawText(
        @core.TextRun::new(
          text=block.prefix,
          frame=prefix_frame,
          font=block_font,
          color=block_color,
          align=@core.TextAlign::TextStart,
        ),
      ),
    )
    x = x + width
  }
  for run in block.runs {
    let font = run.font.unwrap_or(block_font)
    let color = run.color.unwrap_or(block_color)
    let segments = rich_text_split_lines(rich_text_run_visual_text(run))
    for index, segment in segments {
      let width = rich_text_run_segment_width(run, segment, font, text_system)
      let line_y = frame.origin.y + line.to_double() * line_step
      let run_frame = rich_text_run_segment_paint_frame(
        run,
        x~,
        y=line_y,
        width~,
        height=rich_text_run_segment_height(run, line_step),
        line_right=frame.origin.x + frame.size.width,
        font~,
      )
      if segment != "" || run.image_source is Some(_) {
        append_rich_text_run_segment(
          commands, run, segment, run_frame, width, font, color,
        )
      }
      if index + 1 < segments.length() {
        line = line + 1
        x = frame.origin.x
      } else {
        x = x + width
      }
    }
  }
}

///|
fn rich_text_run_segment_paint_frame(
  run : RichTextRun,
  x~ : Double,
  y~ : Double,
  width~ : Double,
  height~ : Double,
  line_right~ : Double,
  font~ : @core.FontSpec,
) -> @core.Rect {
  match run.image_source {
    Some(_) =>
      @core.Rect::new(
        x~,
        y~,
        width=max_double(0.0, min_double(width, line_right - x)),
        height~,
      )
    None =>
      rich_text_text_paint_frame(x~, y~, width~, height~, line_right~, font~)
  }
}

///|
fn rich_text_text_paint_frame(
  x~ : Double,
  y~ : Double,
  width~ : Double,
  height~ : Double,
  line_right~ : Double,
  font~ : @core.FontSpec,
) -> @core.Rect {
  @core.Rect::new(
    x~,
    y~,
    width=max_double(
      0.0,
      min_double(width + rich_text_text_clip_bleed(font), line_right - x),
    ),
    height~,
  )
}

///|
fn rich_text_text_clip_bleed(font : @core.FontSpec) -> Double {
  max_double(2.0, font.size * 0.18)
}

///|
fn append_rich_text_run_segment(
  commands : Array[@core.DrawCommand],
  run : RichTextRun,
  text : String,
  run_frame : @core.Rect,
  width : Double,
  font : @core.FontSpec,
  color : @core.Color,
) -> Unit {
  match run.background {
    Some(background) =>
      commands.push(
        @core.DrawCommand::FillRoundedRectBrush(
          @core.RoundedRect::new(
            rect=@core.Rect::new(
              x=run_frame.origin.x - 3.0,
              y=run_frame.origin.y + 4.0,
              width=width + 6.0,
              height=max_double(12.0, run_frame.size.height - 8.0),
            ),
            radius=run.background_radius,
          ),
          background,
        ),
      )
    None => ()
  }
  match run.image_source {
    Some(source) =>
      commands.push(
        @core.DrawCommand::DrawImage({
          source,
          frame: run_frame,
          opacity: 1.0,
          fit: run.image_fit,
        }),
      )
    None =>
      commands.push(
        @core.DrawCommand::DrawText(
          @core.TextRun::new(
            text~,
            frame=run_frame,
            font~,
            color~,
            align=@core.TextAlign::TextStart,
          ),
        ),
      )
  }
  append_rich_text_run_decoration(commands, run, run_frame, width, font, color)
}

///|
fn append_rich_text_run_decoration(
  commands : Array[@core.DrawCommand],
  run : RichTextRun,
  frame : @core.Rect,
  width : Double,
  font : @core.FontSpec,
  color : @core.Color,
) -> Unit {
  match run.decoration {
    Some(RichTextDecoration::Strikethrough) => {
      let thickness = max_double(1.0, font.size / 14.0)
      commands.push(
        @core.DrawCommand::FillRect(
          @core.Rect::new(
            x=frame.origin.x,
            y=frame.origin.y + frame.size.height * 0.52,
            width~,
            height=thickness,
          ),
          color.multiply_alpha(0.72),
        ),
      )
    }
    None => ()
  }
}

///|
fn append_rich_text_block_table(
  commands : Array[@core.DrawCommand],
  block : RichTextBlock,
  frame : @core.Rect,
  base_font : @core.FontSpec,
  foreground : @core.Color,
) -> Unit {
  match block.table {
    Some(table) => {
      let row_count = table.rows.length()
      let column_count = rich_text_table_column_count(table)
      if row_count == 0 || column_count == 0 {
        return
      }
      let column_width = frame.size.width / column_count.to_double()
      for row_index in 0..
              commands.push(
                @core.DrawCommand::FillRoundedRectBrush(
                  @core.RoundedRect::new(
                    rect=@core.Rect::new(
                      x=frame.origin.x,
                      y=row_y,
                      width=frame.size.width,
                      height=table.row_height,
                    ),
                    radius=block.corner_radius,
                  ),
                  background,
                ),
              )
            None => ()
          }
        }
        let row = table.rows[row_index]
        for column_index in 0.. ()
  }
}

///|
fn append_rich_text_editor_overlay(
  commands : Array[@core.DrawCommand],
  text~ : String,
  display_text~ : String,
  document~ : RichTextDocument,
  content~ : @core.Rect,
  font~ : @core.FontSpec,
  cursor_color~ : @core.Color,
  control~ : @core.TextControlStateContext,
  text_system~ : @core.TextSystem,
  clip_rect? : @core.Rect? = None,
) -> Unit {
  let clip = match clip_rect {
    Some(r) => r
    None => content
  }
  commands.push(@core.DrawCommand::PushClip(clip))
  if control.composition is None {
    append_rich_text_selection_commands(
      commands,
      document,
      content,
      font,
      text_system,
      control.selection,
    )
  } else {
    append_rich_text_composition_underline(
      commands,
      document,
      text~,
      display_text~,
      content~,
      font~,
      text_system~,
      cursor_color~,
      control~,
    )
  }
  let caret = @core.clamp_int(
    rich_text_visual_caret(text, control, focused=true),
    0,
    text_length(display_text),
  )
  let caret_rect = rich_text_document_caret_rect_or_plain(
    document, caret, content, font, text_system, display_text,
  )
  commands.push(@core.DrawCommand::FillRect(caret_rect, cursor_color))
  commands.push(@core.DrawCommand::PopClip)
}

///|
fn append_rich_text_composition_underline(
  commands : Array[@core.DrawCommand],
  document : RichTextDocument,
  text~ : String,
  display_text~ : String,
  content~ : @core.Rect,
  font~ : @core.FontSpec,
  text_system~ : @core.TextSystem,
  cursor_color~ : @core.Color,
  control~ : @core.TextControlStateContext,
) -> Unit {
  match control.composition {
    Some(composition) =>
      if composition != "" {
        let start = @core.composition_start_index(
          text,
          control.caret,
          control.selection,
        )
        let end = start + text_length(composition)
        let start_rect = rich_text_document_caret_rect_or_plain(
          document, start, content, font, text_system, display_text,
        )
        let end_rect = rich_text_document_caret_rect_or_plain(
          document, end, content, font, text_system, display_text,
        )
        if end_rect.origin.x > start_rect.origin.x {
          commands.push(
            @core.DrawCommand::FillRect(
              @core.Rect::new(
                x=start_rect.origin.x,
                y=start_rect.origin.y + start_rect.size.height - 5.0,
                width=end_rect.origin.x - start_rect.origin.x,
                height=1.5,
              ),
              cursor_color.with_alpha(0.72),
            ),
          )
        }
      }
    None => ()
  }
}