///|
fn rich_text_editor_paint(
  ctx : @core.ViewPaintContext,
  value~ : 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,
) -> @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(FillRoundedRectBrush(rounded, background))
  append_rich_text_border(
    commands,
    rounded,
    if ctx.focused {
      focused_border
    } else {
      border
    },
  )
  let text = value
  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 = visible_scroll_y
  let document_content = rich_text_scrolled_content_rect(
    content,
    scroll_y~,
    visible_viewport_height~,
  )
  commands.push(PushClip(content))
  let child_layers : Array[@core.ViewPaintLayer] = []
  if display_text == "" {
    commands.push(
      DrawText(
        @core.TextRun::new(
          text=placeholder,
          frame=content,
          font~,
          color=placeholder_color,
          align=TextStart,
        ),
      ),
    )
  } else {
    let document = format(display_text)
    // `document_content` is already shifted by `-scroll_y`, so blocks are in
    // paint coordinates. Windowed culling must NOT add `scroll_y` again —
    // that double-count made the visible band start near `clip.y + scroll_y`
    // while blocks sat near `clip.y - scroll_y`, which culls the whole
    // viewport (blank editor) and leaves stale/out-of-band layers (overlap).
    append_rich_text_document_layers(
      child_layers,
      document,
      document_content,
      font,
      foreground,
      text_system=ctx.text_system,
      visible_scroll_y=0.0,
      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: [PopClip],
    overlay_commands,
    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(
          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 (may already include -scroll_y
  // or a window top_padding). `clip` is the untranslated viewport used for
  // culling. When blocks are pre-positioned into paint space, pass
  // `visible_scroll_y=0` so this band is just the clip plus overscan.
  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,
      block_width=content.size.width,
    )
    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~,
      visible_top~,
      visible_bottom~,
      windowed~,
    )
    // A block shorter than the visible band paints every line no matter where
    // it sits, so its cached raster is final and the retained-layer cache can
    // keep it across scroll frames. A taller block is culled line by line, so
    // its raster depends on where it sat in the band when painted — the first
    // admission (at the band's bottom edge, lines still off-screen) would
    // otherwise freeze a partial, often textless, raster that every later
    // frame re-pasted while scrolling. Those layers opt out of the cache and
    // draw directly; the line culling still bounds their per-frame cost.
    let band_height = visible_bottom - visible_top
    let cull_lines = windowed &&
      band_height > 0.0 &&
      block.table is None &&
      !block.rule &&
      line_height > band_height
    layers.push(
      @core.ViewPaintLayer::new(
        frame~,
        cache_key=if cull_lines {
          ""
        } else {
          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,
  // Visible band in the same space as `block_frame.origin.y`, plus whether
  // windowing is active at all. A block taller than the viewport (a
  // soft-wrapped paragraph holding a whole novel, say) is never culled by the
  // block loop, so its off-screen lines have to be skipped here.
  visible_top? : Double = 0.0,
  visible_bottom? : Double = 0.0,
  windowed? : Bool = false,
) -> Unit {
  match block.background {
    Some(background) =>
      commands.push(
        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(
          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(
      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,
      text_system~,
    )
  } 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~,
      visible_top~,
      visible_bottom~,
      windowed~,
    )
  }
}

///|
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,
  visible_top? : Double = 0.0,
  visible_bottom? : Double = 0.0,
  windowed? : Bool = false,
) -> 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 line_heights = rich_text_block_line_heights(
    block,
    base_font,
    text_system,
    frame.size.width,
  )
  let line_widths = rich_text_block_line_widths(
    block,
    base_font,
    text_system,
    frame.size.width,
  )
  let mut line = 0
  // Running vertical offset of the current visual line. The loop walks lines
  // in order, so the offset advances by one line height per step. Re-summing
  // `line_heights[0..line]` for every line instead made painting a block
  // quadratic in its visual line count — which is exactly the shape of a long
  // soft-wrapped paragraph (a novel with no blank lines is one block of
  // hundreds of thousands of lines, and the sum alone ran for hours).
  let mut line_offset = 0.0
  let mut x = rich_text_block_line_start_x(block, frame, line_widths, line)
  // Line culling only pays for blocks taller than the visible band: those
  // never leave the block-level cull, so their off-screen lines must be
  // skipped here. A block that fits the band paints completely wherever it
  // sits, which is what keeps its cached layer raster independent of scroll
  // position.
  let mut content_height = 0.0
  for height in line_heights {
    content_height = content_height + height
  }
  let band_height = visible_bottom - visible_top
  let cull_lines = windowed && band_height > 0.0 && content_height > band_height
  if block.prefix != "" {
    let width = rich_text_block_marker_lane(block, block_font, text_system)
    let lane = @core.Rect::new(
      x~,
      y=frame.origin.y,
      width~,
      height=line_heights.get(0).unwrap_or(line_step),
    )
    match block.marker {
      Some(marker) =>
        append_rich_text_marker_commands(commands, marker, lane, block_font)
      None => {
        let prefix_frame = rich_text_text_paint_frame(
          x~,
          y=frame.origin.y,
          width~,
          height=line_heights.get(0).unwrap_or(line_step),
          line_right=frame.origin.x + frame.size.width,
          font=block_font,
        )
        commands.push(
          DrawText(
            @core.TextRun::new(
              text=block.prefix,
              frame=prefix_frame,
              font=block_font,
              color=block_color,
              align=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 line_height = line_heights.get(line).unwrap_or(line_step)
      let line_y = frame.origin.y + line_offset
      // A line outside the visible band is skipped, not drawn. The cursor
      // still advances so the following lines keep their positions. Only a
      // block taller than the band gets here (see `cull_lines`), so this is
      // the only thing standing between a novel-sized paragraph and one draw
      // command per visual line for the whole document.
      let on_screen = !cull_lines ||
        (line_y + line_height >= visible_top && line_y <= visible_bottom)
      let last_segment = index + 1 >= segments.length()
      // Shaping is the expensive part, so an off-screen line only pays for it
      // when its width is needed to place the next run on the same line.
      let width = if on_screen || last_segment {
        rich_text_run_segment_width(
          run,
          segment,
          font,
          text_system,
          available_width=frame.size.width,
        )
      } else {
        0.0
      }
      if on_screen {
        let run_frame = rich_text_run_segment_paint_frame(
          run,
          x~,
          y=line_y,
          width~,
          height=rich_text_run_segment_height(
            run,
            line_step,
            available_width=frame.size.width,
          ),
          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() {
        match line_heights.get(line) {
          Some(height) => line_offset = line_offset + height
          None => ()
        }
        line = line + 1
        x = rich_text_block_line_start_x(block, frame, line_widths, line)
      } else {
        x = x + width
      }
    }
  }
}

///|
fn rich_text_block_line_start_x(
  block : RichTextBlock,
  frame : @core.Rect,
  line_widths : Array[Double],
  line : Int,
) -> Double {
  let width = min_double(
    frame.size.width,
    max_double(0.0, line_widths.get(line).unwrap_or(0.0)),
  )
  let remaining = max_double(0.0, frame.size.width - width)
  match block.line_align.unwrap_or(TextStart) {
    TextCenter => frame.origin.x + remaining / 2.0
    TextEnd => frame.origin.x + remaining
    TextStart => frame.origin.x
  }
}

///|
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(
        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(
        DrawImage({
          source,
          frame: run_frame,
          opacity: 1.0,
          fit: run.image_fit,
        }),
      )
    None =>
      commands.push(
        DrawText(
          @core.TextRun::new(
            text~,
            frame=run_frame,
            font~,
            color~,
            align=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(Strikethrough) => {
      let thickness = max_double(1.0, font.size / 14.0)
      commands.push(
        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,
  text_system~ : @core.TextSystem,
) -> Unit {
  match block.table {
    Some(table) => {
      let layout = rich_text_table_layout(
        table,
        block,
        base_font,
        frame_width=frame.size.width,
        text_system~,
      )
      if layout.rows.length() == 0 || layout.column_width <= 0.0 {
        return
      }
      for row_index, row in layout.rows {
        let row_y = frame.origin.y + layout.row_top(row_index)
        let table_row = table.rows[row_index]
        if row_index == 0 {
          match table.header_background {
            Some(background) =>
              commands.push(
                FillRoundedRectBrush(
                  @core.RoundedRect::new(
                    rect=@core.Rect::new(
                      x=frame.origin.x,
                      y=row_y,
                      width=frame.size.width,
                      height=row.height,
                    ),
                    radius=block.corner_radius,
                  ),
                  background,
                ),
              )
            None => ()
          }
        }
        for column_index, cell_layout in row.cells {
          let cell_x = frame.origin.x +
            layout.column_width * column_index.to_double()
          let cell_frame = @core.Rect::new(
            x=cell_x,
            y=row_y,
            width=layout.column_width,
            height=row.height,
          )
          commands.push(StrokeRect(cell_frame, table.border, 1.0))
          if column_index < table_row.length() {
            let cell = table_row[column_index]
            let align = cell.align.unwrap_or(TextStart)
            if cell_layout.segments.length() > 0 {
              // Styled cell: draw each run segment at its laid-out x with
              // its own font; the segment layout already skipped markers.
              for segment in cell_layout.segments {
                if segment.text == "" {
                  continue
                }
                let line_y = row_y +
                  table.cell_padding.top +
                  layout.line_step * segment.line.to_double()
                commands.push(
                  DrawText(
                    @core.TextRun::new(
                      text=segment.text,
                      frame=@core.Rect::new(
                        x=cell_x + table.cell_padding.left + segment.x,
                        y=line_y,
                        width=max_double(0.0, segment.width),
                        height=layout.line_step,
                      ),
                      font=segment.run.font.unwrap_or(cell_layout.font),
                      color=segment.run.color.unwrap_or(
                        rich_text_block_color(block, foreground),
                      ),
                      align=TextStart,
                    ),
                  ),
                )
              }
              continue
            }
            for line_index, line in cell_layout.lines {
              if line.text == "" {
                continue
              }
              let line_y = row_y +
                table.cell_padding.top +
                layout.line_step * line_index.to_double()
              commands.push(
                DrawText(
                  @core.TextRun::new(
                    text=line.text,
                    frame=@core.Rect::new(
                      x=cell_x + table.cell_padding.left,
                      y=line_y,
                      width=layout.cell_inner_width,
                      height=layout.line_step,
                    ),
                    font=cell_layout.font,
                    color=rich_text_block_color(block, foreground),
                    align~,
                  ),
                ),
              )
            }
          }
        }
      }
    }
    None => ()
  }
}

///|
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,
  // Precomputed caret rect for a caret the window document does not
  // contain (the visible band is clipped and the caret sits outside it):
  // the document walk would pin such a caret to the first rendered line,
  // so the caller supplies the session-level estimate instead.
  caret_rect_override? : @core.Rect? = None,
) -> Unit {
  let clip = match clip_rect {
    Some(r) => r
    None => content
  }
  commands.push(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 = match caret_rect_override {
    Some(rect) => rect
    None =>
      rich_text_document_caret_rect_or_plain(
        document, caret, content, font, text_system, display_text,
      )
  }
  commands.push(FillRect(caret_rect, cursor_color))
  commands.push(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(
            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 => ()
  }
}