summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--TextViewport.hs95
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