{-# LANGUAGE OverloadedStrings #-} module TextViewport ( Item(..) , Buffer(..) , Viewport(..) , RenderedLine(..) , renderBuffer , defaultViewport , scrollUp , scrollDown , visibleLines ) where import Data.Text (Text) import qualified Data.Text as T -------------------------------------------------------------------------------- -- Logical model -------------------------------------------------------------------------------- -- | A single buffer item. Arbitrary text, may contain hard line breaks. newtype Item = Item { unItem :: Text } -- | Oldest item first, newest last. No rendering assumptions here. newtype Buffer = Buffer { unBuffer :: [Item] } -------------------------------------------------------------------------------- -- Rendered representation -------------------------------------------------------------------------------- -- | A single line after splitting on hard breaks and cropping to viewport width. -- This is a *physical* line: no wrapping, only truncation. newtype RenderedLine = RenderedLine { unRenderedLine :: Text } deriving (Eq, Show) -- | Render the entire buffer into a flat list of cropped lines. -- This is conceptually stable: scrolling operates only on this list. renderBuffer :: Int -- ^ viewport width (characters) -> Buffer -> [RenderedLine] renderBuffer w (Buffer items) = concatMap renderItem items where renderItem (Item t) = let ls = T.splitOn "\n" t in map (RenderedLine . crop) ls crop = T.take w -- hard truncation, no wrapping -------------------------------------------------------------------------------- -- Viewport state -------------------------------------------------------------------------------- -- | Viewport is defined by width, height, and a scroll offset into the -- rendered line stream. Offset is always a line index, never an item index. data Viewport = Viewport { vpWidth :: !Int , vpHeight :: !Int , vpOffset :: !Int -- ^ index into rendered lines; 0 = top of buffer } deriving (Eq, Show) -- | Construct a viewport positioned at the bottom (newest content). defaultViewport :: Int -> Int -> [RenderedLine] -> Viewport defaultViewport w h rendered = let total = length rendered off = max 0 (total - h) in Viewport w h off -------------------------------------------------------------------------------- -- Scrolling -------------------------------------------------------------------------------- -- | Scroll upward by k lines. Clamped at 0. scrollUp :: Int -> Viewport -> Viewport scrollUp k vp = vp { vpOffset = max 0 (vpOffset vp - k) } -- | Scroll downward by k lines. Clamped at the last fully visible window. scrollDown :: Int -> [RenderedLine] -> Viewport -> Viewport scrollDown k rendered vp = let total = length rendered maxOff = max 0 (total - vpHeight vp) newOff = min maxOff (vpOffset vp + k) in vp { vpOffset = newOff } -------------------------------------------------------------------------------- -- Visibility -------------------------------------------------------------------------------- -- | Extract the currently visible slice of rendered lines. -- The viewport height determines the slice length. visibleLines :: [RenderedLine] -> Viewport -> [RenderedLine] visibleLines rendered vp = take (vpHeight vp) . drop (vpOffset vp) $ rendered