packages feed

hpdft-0.4.6.0: TuiScroll.hs

module TuiScroll
  ( ScrollState(..)
  , initialScrollState
  , clampScrollTop
  , scrollBy
  , scrollToTop
  , scrollToEnd
  , scrollHalfPageDown
  , scrollHalfPageUp
  , searchForwardFrom
  , searchBackwardFrom
  , visibleLineRange
  , statusLineNumber
  , charDisplayWidth
  , stringDisplayWidth
  , clipToDisplayWidth
  , padToDisplayWidth
  ) where

import Data.Char (ord)
import Data.List (findIndex)

data ScrollState = ScrollState
  { scrollTopLine  :: !Int
  , scrollTotalLines :: !Int
  , scrollTextRows :: !Int
  } deriving (Eq, Show)

initialScrollState :: Int -> ScrollState
initialScrollState textRows =
  ScrollState {scrollTopLine = 0, scrollTotalLines = 0, scrollTextRows = textRows}

maxScrollTop :: ScrollState -> Int
maxScrollTop st =
  max 0 (scrollTotalLines st - scrollTextRows st)

clampScrollTop :: Int -> ScrollState -> ScrollState
clampScrollTop n st = st {scrollTopLine = clamp 0 (maxScrollTop st) n}
  where
    clamp lo hi x = max lo (min hi x)

scrollBy :: Int -> ScrollState -> ScrollState
scrollBy delta st =
  clampScrollTop (scrollTopLine st + delta) st

scrollToTop :: ScrollState -> ScrollState
scrollToTop st = st {scrollTopLine = 0}

scrollToEnd :: ScrollState -> ScrollState
scrollToEnd st = clampScrollTop maxBound st

scrollHalfPageDown :: ScrollState -> ScrollState
scrollHalfPageDown st =
  scrollBy (max 1 (scrollTextRows st `div` 2)) st

scrollHalfPageUp :: ScrollState -> ScrollState
scrollHalfPageUp st =
  scrollBy (-max 1 (scrollTextRows st `div` 2)) st

-- | Index of the first line at or after @start@ satisfying the predicate.
searchForwardFrom :: (a -> Bool) -> Int -> [a] -> Maybe Int
searchForwardFrom p start ls =
  let from = max 0 start
   in (+ from) <$> findIndex p (drop from ls)

-- | Index of the last line at or before @start@ satisfying the predicate.
searchBackwardFrom :: (a -> Bool) -> Int -> [a] -> Maybe Int
searchBackwardFrom p start ls =
  let upto = min (length ls - 1) start
      hits = [i | (i, l) <- zip [0 ..] (take (upto + 1) ls), p l]
   in if upto < 0 || null hits then Nothing else Just (last hits)

visibleLineRange :: ScrollState -> (Int, Int)
visibleLineRange st =
  let top = scrollTopLine st
      end = min (scrollTotalLines st) (top + scrollTextRows st)
   in (top, end)

statusLineNumber :: ScrollState -> Int
statusLineNumber st =
  if scrollTotalLines st == 0
    then 0
    else scrollTopLine st + 1

-- | Terminal display width: East Asian wide/fullwidth characters occupy
-- two columns. Covers the common CJK blocks; combining marks are not handled.
charDisplayWidth :: Char -> Int
charDisplayWidth c
  | w >= 0x1100 && w <= 0x115F = 2   -- Hangul Jamo
  | w >= 0x2E80 && w <= 0xA4CF = 2   -- CJK radicals .. Yi
  | w >= 0xAC00 && w <= 0xD7A3 = 2   -- Hangul syllables
  | w >= 0xF900 && w <= 0xFAFF = 2   -- CJK compatibility ideographs
  | w >= 0xFE30 && w <= 0xFE4F = 2   -- CJK compatibility forms
  | w >= 0xFF00 && w <= 0xFF60 = 2   -- fullwidth forms
  | w >= 0xFFE0 && w <= 0xFFE6 = 2
  | w >= 0x20000 && w <= 0x3FFFD = 2 -- CJK extensions
  | otherwise = 1
  where w = ord c

stringDisplayWidth :: String -> Int
stringDisplayWidth = sum . map charDisplayWidth

-- | Longest prefix that fits in @width@ display columns.
clipToDisplayWidth :: Int -> String -> String
clipToDisplayWidth width = go width
  where
    go _ [] = []
    go left (c : cs)
      | cw <= left = c : go (left - cw) cs
      | otherwise = []
      where cw = charDisplayWidth c

-- | Clip and right-pad with spaces to exactly @width@ display columns
-- (may fall one column short when a wide character straddles the edge).
padToDisplayWidth :: Int -> String -> String
padToDisplayWidth width s =
  let clipped = clipToDisplayWidth width s
   in clipped ++ replicate (width - stringDisplayWidth clipped) ' '