diff options
| -rw-r--r-- | TextViewport.hs | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/TextViewport.hs b/TextViewport.hs new file mode 100644 index 0000000..927ba8b --- /dev/null +++ b/TextViewport.hs @@ -0,0 +1,95 @@ +{-# 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 |
