Canvas

use "collections"

actor Canvas is CompositeWidget
  """
  An absolute-positioning container. Children are placed at explicit
  (x, y) coordinates with fixed sizes. The user implements
  render_background() for custom drawing; children are composited
  on top in registration order (later on top).

  Children keep their positions when the canvas resizes — they are
  clipped at the canvas bounds.
  """
  let _state: WidgetState
  let _placements: Array[(Any tag, USize, USize, USize, USize)]

  new create(p: WidgetParent tag) =>
    _state = WidgetState(p)
    _placements = Array[(Any tag, USize, USize, USize, USize)]

  // -- Widget + CompositeWidget required helpers --

  fun ref state(): WidgetState => _state

  // -- Override render: composite children at their placed positions --

  fun ref render(): Grid =>
    """
    Render background, then blit each child at its (x, y) position.
    Children are composited in registration order (later on top).
    Clipped at canvas bounds.
    """
    let bg = render_background()
    let w = _state.width
    let h = _state.height

    if _state.child_grids.size() == 0 then
      return bg
    end

    let empty = _state.empty_cell()
    let cells: Array[Cell] iso = recover iso
      let size = w * h
      let arr = Array[Cell](size)
      for row in Range(0, h) do
        for col in Range(0, w) do
          match bg(col, row)
          | let c: Cell => arr.push(c)
          | GridCellOutOfBounds => arr.push(empty)
          end
        end
      end
      arr
    end

    // Blit each child at its placement position
    for pi in Range(0, _placements.size()) do
      try
        (let id, let px, let py, _, _) = _placements(pi)?
        // Find the matching grid
        for gi in Range(0, _state.child_grids.size()) do
          try
            (let gid, let grid) = _state.child_grids(gi)?
            if gid is id then
              for row in Range(0, grid.height) do
                let dest_row = py + row
                if dest_row >= h then break end
                for col in Range(0, grid.width) do
                  let dest_col = px + col
                  if dest_col >= w then break end
                  match grid(col, row)
                  | let c: Cell =>
                    try cells((dest_row * w) + dest_col)? = c end
                  end
                end
              end
              break
            end
          end
        end
      end
    end

    let cells_val: Array[Cell] val = consume cells
    GridFactory(w, h, cells_val)

  // -- Container-specific --

  be add(widget: Widget tag, x: USize, y: USize, w: USize, h: USize) =>
    """
    Place a child widget at (x, y) with the given size. The child is
    resized to (w, h) when the canvas receives its own resize.
    Later-added children draw on top of earlier ones.
    """
    _placements.push((widget, x, y, w, h))
    _state.child_grids.push((widget, Grid.filled(w, h, Cell.empty())))
    widget.resize(w, h)

  be move(widget: Widget tag, x: USize, y: USize) =>
    """
    Reposition a child widget. Size is unchanged.
    """
    for i in Range(0, _placements.size()) do
      try
        (let id, _, _, let pw, let ph) = _placements(i)?
        if id is widget then
          _placements(i)? = (id, x, y, pw, ph)
          render_and_send()
          return
        end
      end
    end

  // -- Override resize: resize children to their fixed sizes --

  be resize(w: USize, h: USize) =>
    """
    Update canvas size and re-render. Children keep their fixed sizes
    and positions — they are clipped at the new bounds.
    """
    _state.width = w
    _state.height = h
    // Resize children to their assigned sizes (not the canvas size)
    for placement in _placements.values() do
      (let id, _, _, let pw, let ph) = placement
      match id
      | let r: Widget tag => r.resize(pw, ph)
      end
    end
    render_and_send()