summaryrefslogtreecommitdiffstats
path: root/TextViewport.hs
blob: 927ba8b78a60baa9b1ea94234eca14903f2c3985 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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