packages feed

granite-0.5.0.0: src/Granite/Render/Terminal.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Strict #-}

{- |
Module      : Granite.Render.Terminal
Copyright   : (c) 2025
License     : MIT
Maintainer  : mschavinda@gmail.com

Terminal backend.
-}
module Granite.Render.Terminal (
    renderScene,
    Canvas (..),
    Array2D,
    newCanvas,
    setDotC,
    fillDotsC,
    lineDotsC,
    renderCanvas,
    toBit,
    getA2D,
    setA2D,
    newA2D,
) where

import Data.Bits ((.|.))
import Data.Char (chr)
import Data.List qualified as List
import Data.Text (Text)
import Data.Text qualified as Text

import Granite.Color (Color, ansiOff, ansiOn, paint)
import Granite.Internal.Util (setAt, updateAt)
import Granite.Render.Scene (
    Mark (..),
    Point (..),
    Rect (..),
    Scene (..),
    Style (..),
    TextAnchor (..),
    TextStyle (..),
    pxPerChar,
    pxPerLine,
 )

renderScene :: Scene -> Text
renderScene scene =
    let wChars = max 1 (ceiling (sceneWidth scene / pxPerChar))
        hChars = max 1 (ceiling (sceneHeight scene / pxPerLine))
        grid0 = replicate hChars (replicate wChars (' ', Nothing :: Maybe Color))
        canvas0 = newCanvas wChars hChars
        (grid1, canvas1) = List.foldl' (drawMark wChars hChars) (grid0, canvas0) (sceneMarks scene)
        grid2 = mergeCanvas grid1 canvas1
     in Text.unlines (map renderRuns grid2)

mergeCanvas :: [[(Char, Maybe Color)]] -> Canvas -> [[(Char, Maybe Color)]]
mergeCanvas grid canvas =
    [ zipWith merge row [0 :: Int ..]
    | (y, row) <- zip [0 :: Int ..] grid
    , let merge (ch, mc) x =
            case (ch, mc) of
                (' ', Nothing) ->
                    let bits = getA2D (buffer canvas) x y
                        col = getA2D (cbuf canvas) x y
                        glyph = if bits == 0 then ' ' else chr (0x2800 + bits)
                     in (glyph, col)
                _ -> (ch, mc)
    ]

drawMark ::
    Int ->
    Int ->
    ([[(Char, Maybe Color)]], Canvas) ->
    Mark ->
    ([[(Char, Maybe Color)]], Canvas)
drawMark wChars hChars (grid, canvas) mark = case mark of
    MRect (Rect x y w h) sty ->
        let col = styleFill sty
            cx0 = clampInt 0 (wChars - 1) (floor (x / pxPerChar))
            cy0 = clampInt 0 (hChars - 1) (floor (y / pxPerLine))
            cx1 = clampInt 0 (wChars - 1) (floor ((x + w - 1) / pxPerChar))
            cy1 = clampInt 0 (hChars - 1) (floor ((y + h - 1) / pxPerLine))
            grid' =
                List.foldl'
                    ( \g cy ->
                        List.foldl'
                            (\g' cx -> setCell g' cx cy ('█', col))
                            g
                            [cx0 .. cx1]
                    )
                    grid
                    [cy0 .. cy1]
         in (grid', canvas)
    MText (Point x y) txt ts ->
        let cy = clampInt 0 (hChars - 1) (floor (y / pxPerLine))
            width = Text.length txt
            startCol = case textAnchor ts of
                AnchorStart -> floor (x / pxPerChar)
                AnchorMiddle -> floor (x / pxPerChar) - width `div` 2
                AnchorEnd -> floor (x / pxPerChar) - width
            grid' = placeChars grid wChars cy startCol (Text.unpack txt) (textFill ts)
         in (grid', canvas)
    MCircle (Point x y) r sty ->
        let col = styleFill sty
            xDotC = round (x * 2 / pxPerChar)
            yDotC = round (y * 4 / pxPerLine)
            rDotX = max 1 (round (r * 2 / pxPerChar))
            rDotY = max 1 (round (r * 4 / pxPerLine))
            canvas' =
                fillDotsC
                    (xDotC - rDotX, yDotC - rDotY)
                    (xDotC + rDotX, yDotC + rDotY)
                    ( \dx dy ->
                        let ddx = fromIntegral (dx - xDotC) / fromIntegral rDotX :: Double
                            ddy = fromIntegral (dy - yDotC) / fromIntegral rDotY :: Double
                         in ddx * ddx + ddy * ddy <= 1
                    )
                    col
                    canvas
         in (grid, canvas')
    MPolyline pts sty ->
        let col = styleFill sty
            toDot (Point x y) =
                ( round (x * 2 / pxPerChar)
                , round (y * 4 / pxPerLine)
                )
            dots = map toDot pts
            pairs = zip dots (drop 1 dots)
            canvas' =
                List.foldl'
                    (\c ((x0, y0), (x1, y1)) -> lineDotsC (x0, y0) (x1, y1) col c)
                    canvas
                    pairs
         in (grid, canvas')
    MPolygon pts sty ->
        let stroke = case styleStroke sty of
                Just c -> Just c
                Nothing -> styleFill sty
            outline =
                if null pts
                    then pts
                    else pts <> [head pts]
         in drawMark
                wChars
                hChars
                (grid, canvas)
                (MPolyline outline sty{styleStroke = stroke})
    MAxisLine (Point x1 y1) (Point x2 y2) sty ->
        let col = styleStroke sty
            grid' = drawAxisLine wChars hChars x1 y1 x2 y2 col grid
         in (grid', canvas)
    MArc (Point cx cy) r a0 a1 sty ->
        let nSeg = 32 :: Int
            ang i = a0 + (a1 - a0) * fromIntegral i / fromIntegral nSeg
            pts =
                [ Point (cx + r * cos (ang i)) (cy + r * sin (ang i))
                | i <- [0 .. nSeg]
                ]
         in drawMark wChars hChars (grid, canvas) (MPolyline pts sty)
    MPath _ _ -> (grid, canvas)
    MGroup ms ->
        List.foldl' (drawMark wChars hChars) (grid, canvas) ms

placeChars ::
    [[(Char, Maybe Color)]] ->
    Int ->
    Int ->
    Int ->
    String ->
    Color ->
    [[(Char, Maybe Color)]]
placeChars grid wChars cy startCol s col
    | cy < 0 || cy >= length grid = grid
    | otherwise =
        let row0 = grid !! cy
            row' = applyChars row0 wChars startCol s
         in setAt grid cy row'
  where
    applyChars row _ _ [] = row
    applyChars row w c (ch : rest)
        | c < 0 || c >= w = applyChars row w (c + 1) rest
        | otherwise =
            let row' = setAt row c (ch, Just col)
             in applyChars row' w (c + 1) rest

setCell ::
    [[(Char, Maybe Color)]] ->
    Int ->
    Int ->
    (Char, Maybe Color) ->
    [[(Char, Maybe Color)]]
setCell grid cx cy cell =
    updateAt grid cy (\row -> setAt row cx cell)

clampInt :: Int -> Int -> Int -> Int
clampInt low high x = max low (min high x)

drawAxisLine ::
    Int ->
    Int ->
    Double ->
    Double ->
    Double ->
    Double ->
    Maybe Color ->
    [[(Char, Maybe Color)]] ->
    [[(Char, Maybe Color)]]
drawAxisLine wChars hChars x1 y1 x2 y2 col grid
    | abs (y2 - y1) < 1e-6 =
        let cy = clampInt 0 (hChars - 1) (floor (y1 / pxPerLine))
            cxL = clampInt 0 (wChars - 1) (floor (min x1 x2 / pxPerChar))
            cxR = clampInt 0 (wChars - 1) (floor (max x1 x2 / pxPerChar))
         in List.foldl' (\g cx -> writeAxisCell g cx cy '─' col) grid [cxL .. cxR]
    | abs (x2 - x1) < 1e-6 =
        let cx = clampInt 0 (wChars - 1) (floor (x1 / pxPerChar))
            cyT = clampInt 0 (hChars - 1) (floor (min y1 y2 / pxPerLine))
            cyB = clampInt 0 (hChars - 1) (floor (max y1 y2 / pxPerLine))
         in List.foldl' (\g cy -> writeAxisCell g cx cy '│' col) grid [cyT .. cyB]
    | otherwise = grid

writeAxisCell ::
    [[(Char, Maybe Color)]] ->
    Int ->
    Int ->
    Char ->
    Maybe Color ->
    [[(Char, Maybe Color)]]
writeAxisCell grid cx cy ch col =
    let existing = case grid `atRow` cy >>= (`atCol` cx) of
            Just (c, _) -> c
            Nothing -> ' '
        finalCh = combineAxis existing ch
     in setCell grid cx cy (finalCh, col)
  where
    atRow rs i
        | i < 0 || i >= length rs = Nothing
        | otherwise = Just (rs !! i)
    atCol cs i
        | i < 0 || i >= length cs = Nothing
        | otherwise = Just (cs !! i)

combineAxis :: Char -> Char -> Char
combineAxis existing new
    | existing == '│' && new == '─' = '┼'
    | existing == '─' && new == '│' = '┼'
    | existing == '┼' = '┼'
    | otherwise = new

renderRuns :: [(Char, Maybe Color)] -> Text
renderRuns = go
  where
    go [] = Text.empty
    go xs@((_, Nothing) : _) =
        let (plain, rest) = span (\(_, mc) -> case mc of Nothing -> True; _ -> False) xs
         in Text.pack (map fst plain) <> go rest
    go ((ch, Just c) : rest) =
        let (run, after) =
                span (\(_, mc) -> mc == Just c || isNothing mc) rest
            chunk = ch : map fst run
         in if all (== ' ') chunk
                then Text.pack chunk <> go after
                else ansiOn c <> Text.pack chunk <> ansiOff <> go after

data Array2D a = A2D Int Int (Arr a)

getA2D :: Array2D a -> Int -> Int -> a
getA2D (A2D w _ xs) x y = indexA xs (y * w + x)

setA2D :: Array2D a -> Int -> Int -> a -> Array2D a
setA2D (A2D w h xs) x y v =
    let i = y * w + x
     in A2D w h (setArr xs i v)

newA2D :: Int -> Int -> a -> Array2D a
newA2D w h v = A2D w h (fromList (replicate (w * h) v))

toBit :: Int -> Int -> Int
toBit ry rx = case (ry, rx) of
    (0, 0) -> 1
    (1, 0) -> 2
    (2, 0) -> 4
    (3, 0) -> 64
    (0, 1) -> 8
    (1, 1) -> 16
    (2, 1) -> 32
    (3, 1) -> 128
    _ -> 0

data Canvas = Canvas
    { cW :: Int
    , cH :: Int
    , buffer :: Array2D Int
    , cbuf :: Array2D (Maybe Color)
    }

newCanvas :: Int -> Int -> Canvas
newCanvas w h = Canvas w h (newA2D w h 0) (newA2D w h Nothing)

setDotC :: Canvas -> Int -> Int -> Maybe Color -> Canvas
setDotC c xDot yDot mcol
    | xDot < 0 || yDot < 0 || xDot >= cW c * 2 || yDot >= cH c * 4 = c
    | otherwise =
        let (cx, rx) = xDot `divMod` 2
            (cy, ry) = yDot `divMod` 4
            b = toBit ry rx
            m = getA2D (buffer c) cx cy
            c' = c{buffer = setA2D (buffer c) cx cy (m .|. b)}
         in case mcol of
                Nothing -> c'
                Just col -> c'{cbuf = setA2D (cbuf c) cx cy (Just col)}

fillDotsC ::
    (Int, Int) ->
    (Int, Int) ->
    (Int -> Int -> Bool) ->
    Maybe Color ->
    Canvas ->
    Canvas
fillDotsC (x0, y0) (x1, y1) p mcol c0 =
    let xs = [max 0 x0 .. min (cW c0 * 2 - 1) x1]
        ys = [max 0 y0 .. min (cH c0 * 4 - 1) y1]
     in List.foldl'
            (\c y -> List.foldl' (\c' x -> if p x y then setDotC c' x y mcol else c') c xs)
            c0
            ys

lineDotsC :: (Int, Int) -> (Int, Int) -> Maybe Color -> Canvas -> Canvas
lineDotsC (x0, y0) (x1, y1) mcol c0 =
    let dx = abs (x1 - x0)
        sx = if x0 < x1 then 1 else -1
        dy = negate (abs (y1 - y0))
        sy = if y0 < y1 then 1 else -1
        go x y err c
            | x == x1 && y == y1 = setDotC c x y mcol
            | otherwise =
                let e2 = 2 * err
                    (x', err') = if e2 >= dy then (x + sx, err + dy) else (x, err)
                    (y', err'') = if e2 <= dx then (y + sy, err' + dx) else (y, err')
                 in go x' y' err'' (setDotC c x y mcol)
     in go x0 y0 (dx + dy) c0

renderCanvas :: Canvas -> Text
renderCanvas (Canvas w h a colA) =
    let glyph 0 = ' '
        glyph m = chr (0x2800 + m)
        rows =
            fmap
                ( \y -> flip fmap [0 .. w - 1] $ \x ->
                    let m = getA2D a x y
                        ch = glyph m
                        mc = getA2D colA x y
                     in maybe (Text.singleton ch) (`paint` ch) mc
                )
                [0 .. h - 1]
     in Text.unlines (fmap Text.concat rows)

data Arr a
    = E
    | N Int Int (Arr a) a (Arr a)

sizeA :: Arr a -> Int
sizeA E = 0
sizeA (N sz _ _ _ _) = sz

heightA :: Arr a -> Int
heightA E = 0
heightA (N _ h _ _ _) = h

mk :: Arr a -> a -> Arr a -> Arr a
mk l x r = N sz h l x r
  where
    sl = sizeA l
    sr = sizeA r
    hl = heightA l
    hr = heightA r
    sz = 1 + sl + sr
    h = 1 + max hl hr

rotateL :: Arr a -> Arr a
rotateL (N _ _ l x (N _ _ rl y rr)) = mk (mk l x rl) y rr
rotateL _ = error "rotateL: malformed tree"

rotateR :: Arr a -> Arr a
rotateR (N _ _ (N _ _ ll y lr) x r) = mk ll y (mk lr x r)
rotateR _ = error "rotateR: malformed tree"

balance :: Arr a -> Arr a
balance t@(N _ _ l x r)
    | heightA l > heightA r + 1 =
        case l of
            N _ _ ll _ lr ->
                if heightA ll >= heightA lr
                    then rotateR t
                    else rotateR (mk (rotateL l) x r)
            _ -> t
    | heightA r > heightA l + 1 =
        case r of
            N _ _ rl _ rr ->
                if heightA rr >= heightA rl
                    then rotateL t
                    else rotateL (mk l x (rotateR r))
            _ -> t
    | otherwise = mk l x r
balance t = t

indexA :: Arr a -> Int -> a
indexA t i =
    case t of
        E -> error ("index out of bounds: " <> show i)
        N _ _ l x r ->
            let sl = sizeA l
             in if i < 0 || i >= 1 + sl + sizeA r
                    then error ("index out of bounds: " <> show i)
                    else
                        if i < sl
                            then indexA l i
                            else
                                if i == sl
                                    then x
                                    else indexA r (i - sl - 1)

setArr :: Arr a -> Int -> a -> Arr a
setArr t i y =
    case t of
        E -> error ("index out of bounds when setting: " <> show i)
        N _ _ l x r ->
            let sl = sizeA l
             in if i < 0 || i >= 1 + sl + sizeA r
                    then error ("index out of bounds: " <> show i)
                    else
                        if i < sl
                            then balance (mk (setArr l i y) x r)
                            else
                                if i == sl
                                    then mk l y r
                                    else balance (mk l x (setArr r (i - sl - 1) y))

fromList :: [a] -> Arr a
fromList xs = fst (build (length xs) xs)
  where
    build :: Int -> [a] -> (Arr a, [a])
    build 0 ys = (E, ys)
    build n ys =
        let (l, ys1) = build (n `div` 2) ys
            (x, ys2) = case ys1 of
                [] -> error "IMPOSSIBLE"
                (v : vs) -> (v, vs)
            (r, ys3) = build (n - n `div` 2 - 1) ys2
         in (mk l x r, ys3)