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
|