summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--TextViewport.hs621
-rw-r--r--src/TextViewport/Buffer/Buffer.hs35
-rw-r--r--src/TextViewport/Buffer/Item.hs27
-rw-r--r--src/TextViewport/Render/CachedRender.hs15
-rw-r--r--src/TextViewport/Render/RenderBuffer.hs31
-rw-r--r--src/TextViewport/Render/RenderCache.hs28
-rw-r--r--src/TextViewport/Render/RenderItem.hs23
-rw-r--r--src/TextViewport/Render/RenderState.hs132
-rw-r--r--src/TextViewport/Render/RenderedBuffer.hs19
-rw-r--r--src/TextViewport/Render/RenderedItem.hs9
-rw-r--r--src/TextViewport/Render/RenderedLine.hs11
-rw-r--r--src/TextViewport/Render/Segmentation.hs200
-rw-r--r--src/TextViewport/Viewport/Instance.hs76
-rw-r--r--src/TextViewport/Viewport/Position.hs23
-rw-r--r--src/TextViewport/Viewport/Viewport.hs57
15 files changed, 686 insertions, 621 deletions
diff --git a/TextViewport.hs b/TextViewport.hs
deleted file mode 100644
index 764a846..0000000
--- a/TextViewport.hs
+++ /dev/null
@@ -1,621 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module TextViewport
- ( Item(..)
- , Buffer(..)
- , WrapStrategy(..)
- , RenderedLine(..)
- , RenderedItem(..)
- , CachedRender(..)
- , RenderCache(..)
- , RenderedBuffer
- , renderBuffer
- , flatten
- , modifyItem
- , updateRenderedItem
- , Viewport(..)
- , defaultViewport
- , scrollUp
- , scrollDown
- , lookupPosition
-
- , RenderState(..)
- , mkRenderState
- , updateRenderState
-
- -- Viewport Instance ("simple" façade)
- , ViewportInstance(..)
- , mkViewportInstance
- , visibleLines
- , applyToInstance
- , applyToInstanceRS
- , scrollByI
- , scrollUpI
- , scrollDownI
- , pageUpI
- , pageDownI
- , alignTopI
- , alignBottomI
- , modifyItemI
- , lookupPositionI
-
- ) where
-
-import Data.DList qualified as DL
-import Data.Foldable qualified as F
-import Data.HashMap.Strict qualified as HM
-import Data.List (minimumBy)
-import Data.Ord (comparing)
-import Data.Sequence (Seq)
-import Data.Sequence qualified as Seq
-import Data.Text (Text)
-import Data.Text qualified as T
-import Data.Vector (Vector)
-import Data.Vector qualified as V
-import Text.Hyphenation qualified as H
-
---------------------------------------------------------------------------------
--- Logical model
---------------------------------------------------------------------------------
-
-data WrapStrategy
- = NoWrap
- | FixedWidthWrap
- | HyphenateWrap
- { hwDict :: H.Hyphenator
- , hwCache :: HM.HashMap Text [(Text, Text)]
- }
-
-data Item = Item
- { itemText :: Text
- , itemWrap :: WrapStrategy
- }
-
-newtype Buffer = Buffer { unBuffer :: Seq Item }
-
-modifyItem :: Int -> (Item -> Item) -> Buffer -> Buffer
-modifyItem ix f (Buffer xs) =
- Buffer (Seq.adjust' f ix xs)
-
---------------------------------------------------------------------------------
--- Rendering with provenance
---------------------------------------------------------------------------------
-
-data RenderedLine = RenderedLine
- { rlText :: !Text
- , rlItemIx :: !Int
- , rlLineIx :: !Int
- , rlCharStart :: !Int
- } deriving (Show)
-
-type RenderedBuffer = Seq RenderedItem
-
-data RenderedItem = RenderedItem
- { riLines :: !(Vector RenderedLine)
- }
-
-flatten :: RenderedBuffer -> [RenderedLine]
-flatten = concatMap (F.toList . riLines) . F.toList
-
-renderBuffer :: Int -> Buffer -> RenderCache -> (RenderCache, RenderedBuffer)
-renderBuffer width (Buffer items) (RenderCache cache) =
- let n = Seq.length items
- go i (cAcc, rAcc)
- | i >= n = (RenderCache cAcc, rAcc)
- | otherwise =
- let item = Seq.index items i
- mOld = Seq.index cache i
- newEntry = renderItem width i item mOld
- cAcc' = Seq.update i (Just newEntry) cAcc
- rAcc' = rAcc Seq.|> crRendered newEntry
- in go (i + 1) (cAcc', rAcc')
- in go 0 (cache, Seq.empty)
-
-renderItem :: Int -> Int -> Item -> Maybe CachedRender -> CachedRender
-renderItem width itemIx (Item txt strategy) mOld =
- case mOld of
- Just old
- | crWidth old == width
- , crText old == txt
- -> old
- _ ->
- let linesV = applyStrategy strategy width itemIx txt
- rendered = RenderedItem linesV
- in CachedRender
- { crWidth = width
- , crStrategy = strategy
- , crText = txt
- , crRendered = rendered
- }
-
-data CachedRender = CachedRender
- { crWidth :: !Int
- , crStrategy :: !WrapStrategy
- , crText :: !Text
- , crRendered :: !RenderedItem
- }
-
-newtype RenderCache = RenderCache { unRenderCache :: Seq (Maybe CachedRender) }
-
---emptyCache :: RenderCache
---emptyCache = RenderCache Seq.empty
-
-emptyCacheFor :: Buffer -> RenderCache
-emptyCacheFor (Buffer items) =
- RenderCache (Seq.replicate (Seq.length items) Nothing)
-
-resizeCache :: Buffer -> RenderCache -> RenderCache
-resizeCache (Buffer items) (RenderCache cache) =
- let n = Seq.length items
- m = Seq.length cache
- in RenderCache $
- if m < n then cache <> Seq.replicate (n - m) Nothing
- else Seq.take n cache
-
---------------------------------------------------------------------------------
--- Wrapping strategies
---------------------------------------------------------------------------------
-
-applyStrategy :: WrapStrategy -> Int -> Int -> Text -> Vector RenderedLine
-
-applyStrategy NoWrap width itemIx txt =
- let rawLines = T.splitOn "\n" txt
- chunks = map (T.take width) rawLines -- crop
- offsets = scanOffsetsWithNewlines chunks
- in V.fromList
- [ RenderedLine { rlText = chunk, rlItemIx = itemIx, rlLineIx = lineIx, rlCharStart = off }
- | (lineIx, (off, chunk)) <- zip [0..] (zip offsets chunks)
- ]
-
-applyStrategy FixedWidthWrap width itemIx txt =
- let rawLines = T.splitOn "\n" txt
- (dl, _) = foldl step (DL.empty, 0) rawLines
- step (acc, off0) line =
- let chunks = chunkFixed width line
- offsets = scanOffsetsFrom off0 chunks
- offNext = off0 + T.length line + 1
- acc' = acc `DL.append` DL.fromList (zip offsets chunks)
- in (acc', offNext)
- allChunks = DL.toList dl
- in V.fromList
- [ RenderedLine { rlText = chunk, rlItemIx = itemIx, rlLineIx = lineIx, rlCharStart = off }
- | (lineIx, (off, chunk)) <- zip [0..] allChunks
- ]
-
-applyStrategy (HyphenateWrap dict cache0) width itemIx txt =
- let rawLines = T.splitOn "\n" txt
-
- -- fold over each physical line, accumulating:
- -- * all rendered (offset, chunk) pairs
- -- * updated hyphenation cache (unused for now)
- -- * running character offset across lines
- (dl, _cache1, _) =
- foldl wrapOneLine (DL.empty, cache0, 0) rawLines
- allChunks = DL.toList dl
- in V.fromList
- [ RenderedLine { rlText = chunk, rlItemIx = itemIx, rlLineIx = lineIx, rlCharStart = off }
- | (lineIx, (off, chunk)) <- zip [0..] allChunks
- ]
- where
- -- Wrap a single physical line using TeX‑lite hyphenation
- --wrapOneLine
- -- :: ([(Int, Text)], HM.HashMap Text [(Text, Text)], Int)
- -- -> Text
- -- -> ([(Int, Text)], HM.HashMap Text [(Text, Text)], Int)
- wrapOneLine (acc, cache, off0) line =
- let (chunks, cache1) = wrapWithHyphenationTeXLite dict width line cache
- offsets = scanOffsetsFrom off0 chunks
- offNext = off0 + T.length line + 1
- acc' = acc `DL.append` DL.fromList (zip offsets chunks)
- in (acc', cache1, offNext)
- --wrapOneLine (acc, cache, off0) line =
- -- let chunks = wrapWithHyphenationTeXLite dict width line
- -- offsets = scanOffsetsFrom off0 chunks
- -- offNext = off0 + T.length line + 1 -- +1 for newline
- -- acc' = acc ++ zip offsets chunks
- -- in (acc', cache, offNext)
-
--- | Compute running character offsets for a list of chunks.
-scanOffsetsWithNewlines :: [Text] -> [Int]
-scanOffsetsWithNewlines = go 0
- where
- go !_ [] = []
- go !o (l:ls) =
- let off = o
- o' = o + T.length l + 1 -- +1 for newline
- in off : go o' ls
-
--- | Chunk a single line into fixed-width pieces.
-chunkFixed :: Int -> Text -> [Text]
-chunkFixed w t
- | w <= 0 = []
- | T.null t = [""]
- | otherwise =
- let (h, rest) = T.splitAt w t
- in h : if T.null rest then [] else chunkFixed w rest
-
---------------------------------------------------------------------------------
--- Hyphenation-aware wrapping (TeX-lite)
---------------------------------------------------------------------------------
-
--- Compute offsets starting from a base offset
-scanOffsetsFrom :: Int -> [Text] -> [Int]
-scanOffsetsFrom start = go start
- where
- go !_ [] = []
- go !o (t:ts) = o : go (o + T.length t) ts
-
-wrapWithHyphenationTeXLite
- :: H.Hyphenator
- -> Int
- -> Text
- -> HM.HashMap Text [(Text, Text)]
- -> ([Text], HM.HashMap Text [(Text, Text)])
-wrapWithHyphenationTeXLite dict width txt cache0 =
- go cache0 (T.words txt)
- where
- go cache [] = ([], cache)
- go cache ws =
- case lineCandidates dict width cache ws of
- ([], cache1) ->
- let chunks = breakWordSafe width ws
- in (chunks, cache1)
- (cs, cache1) ->
- let (line, rest, _) =
- minimumBy (comparing (scoreCandidate width)) cs
- (more, cache2) = go cache1 rest
- in (line : more, cache2)
-
--- | Lossless fallback: treat remaining words as one long text and
--- chunk it into width-sized pieces. Never truncates, never drops text.
-breakWordSafe :: Int -> [Text] -> [Text]
-breakWordSafe width ws =
- chunk (T.unwords ws)
- where
- chunk t
- | T.null t = []
- | T.length t <= width = [t]
- | otherwise =
- let (c, r) = T.splitAt width t
- in c : chunk r
-
-type Candidate = (Text, [Text], Bool)
-
-lineCandidates
- :: H.Hyphenator
- -> Int
- -> HM.HashMap Text [(Text, Text)]
- -> [Text]
- -> ([(Text, [Text], Bool)], HM.HashMap Text [(Text, Text)])
-lineCandidates dict width cache0 ws0 =
- go [] [] cache0 ws0
- where
- go _ acc cache [] = (acc, cache)
- go line acc cache (w:ws) =
- let space = if null line then "" else " "
- baseTxt = T.unwords line
- wholeTxt = baseTxt <> space <> w
- wholeLen = T.length wholeTxt
-
- acc1 =
- if wholeLen <= width && not (T.null wholeTxt)
- then (wholeTxt, ws, False) : acc
- else acc
-
- (hyphs, cache1) =
- case HM.lookup w cache of
- Just hs -> (hs, cache)
- Nothing ->
- let hs = hyphenateWord dict w
- in (hs, HM.insert w hs cache)
-
- hyphCands =
- [ (preTxt, suf : ws, True)
- | (pre, suf) <- hyphs
- , not (T.null pre)
- , let preTxt = baseTxt <> space <> pre <> "-"
- , T.length preTxt <= width
- ]
-
- acc2 = hyphCands ++ acc1
- in if wholeLen <= width
- then go (line ++ [w]) acc2 cache1 ws
- else (acc2, cache1)
-
-hyphenateWord :: H.Hyphenator -> Text -> [(Text, Text)]
-hyphenateWord dict word =
- let parts = H.hyphenate dict (T.unpack word)
- in [ ( T.pack (concat (take i parts))
- , T.pack (concat (drop i parts))
- )
- | i <- [1 .. length parts - 1]
- ]
-
-scoreCandidate :: Int -> Candidate -> Int
-scoreCandidate width (line, _, endsWithHyphen) =
- let len = T.length line
- remSpace = max 0 (width - len)
- badness = remSpace * remSpace * remSpace
- hyphenPenalty =
- if endsWithHyphen then 50 else 0
- shortPenalty =
- if len < width `div` 2 then 200 else 0
- in badness + hyphenPenalty + shortPenalty
-
---------------------------------------------------------------------------------
--- Incremental re-rendering
---------------------------------------------------------------------------------
-
-updateRenderedItem :: Int -> Int -> Buffer -> RenderCache -> Seq RenderedItem -> (RenderCache, Seq RenderedItem)
-updateRenderedItem width itemIx (Buffer items) (RenderCache cache) rb =
- let item = Seq.index items itemIx
- mOld = Seq.index cache itemIx
- newEntry = renderItem width itemIx item mOld
- newCache = Seq.update itemIx (Just newEntry) cache
- newRB = Seq.update itemIx (crRendered newEntry) rb
- in (RenderCache newCache, newRB)
-
---------------------------------------------------------------------------------
--- Render state
---------------------------------------------------------------------------------
-
-data RenderState = RenderState
- { rsBuffer :: Buffer -- original items
- , rsCache :: RenderCache -- per-item cached renders
- , rsRendered :: RenderedBuffer -- fully wrapped + hyphenated lines
- , rsWidth :: Int -- wrapping width
- , rsLineCount :: Int
- }
-
-mkRenderState :: Int -> Buffer -> RenderState
-mkRenderState width buf =
- let (cache1, rendered) = renderBuffer width buf (emptyCacheFor buf)
- in RenderState
- { rsBuffer = buf
- , rsCache = cache1
- , rsRendered = rendered
- , rsWidth = width
- , rsLineCount = length (flatten rendered)
- }
-
--- RenderState has to be rebuilt whenever the buffer or the width changes.
-updateRenderState :: Int -> Buffer -> RenderState -> RenderState
-updateRenderState width buf rs =
- let (cache1, rendered) = renderBuffer width buf (rsCache rs)
- in rs
- { rsBuffer = buf
- , rsCache = cache1
- , rsRendered = rendered
- , rsWidth = width
- , rsLineCount = length (flatten rendered)
- }
-
-modifyItemRS :: Int -> (Item -> Item) -> RenderState -> RenderState
-modifyItemRS ix f st =
- let buf' = modifyItem ix f (rsBuffer st)
- cache' = resizeCache buf' (rsCache st)
- (cache1, rendered1) = renderBuffer (rsWidth st) buf' cache'
- in st { rsBuffer = buf'
- , rsCache = cache1
- , rsRendered = rendered1
- , rsLineCount = length (flatten rendered1)
- }
-
-insertItem :: Int -> Item -> RenderState -> RenderState
-insertItem i newItem st =
- let Buffer items = rsBuffer st
- items' = Seq.insertAt i newItem items
- buf' = Buffer items'
- cache' = resizeCache buf' (rsCache st)
- (cache1, rendered1) = renderBuffer (rsWidth st) buf' cache'
- in st { rsBuffer = buf'
- , rsCache = cache1
- , rsRendered = rendered1
- , rsLineCount = length (flatten rendered1)
- }
-
-deleteItem :: Int -> RenderState -> RenderState
-deleteItem i st =
- let Buffer items = rsBuffer st
- items' = Seq.deleteAt i items
- buf' = Buffer items'
- cache' = resizeCache buf' (rsCache st)
- (cache1, rendered1) = renderBuffer (rsWidth st) buf' cache'
- in st { rsBuffer = buf'
- , rsCache = cache1
- , rsRendered = rendered1
- , rsLineCount = length (flatten rendered1)
- }
-
-replaceItem :: Int -> Item -> RenderState -> RenderState
-replaceItem i newItem st =
- let Buffer items = rsBuffer st
- items' = Seq.update i newItem items
- buf' = Buffer items'
- cache' = resizeCache buf' (rsCache st)
- (cache1, rendered1) = renderBuffer (rsWidth st) buf' cache'
- in st { rsBuffer = buf'
- , rsCache = cache1
- , rsRendered = rendered1
- , rsLineCount = length (flatten rendered1)
- }
-
-appendItem :: Item -> RenderState -> RenderState
-appendItem newItem st =
- insertItem (Seq.length (let Buffer xs = rsBuffer st in xs)) newItem st
-
-clearBuffer :: RenderState -> RenderState
-clearBuffer st =
- let buf' = Buffer Seq.empty
- cache' = RenderCache Seq.empty
- in st { rsBuffer = buf'
- , rsCache = cache'
- , rsRendered = Seq.empty
- , rsLineCount = 0
- }
-
-fromList :: Int -> [Item] -> RenderState
-fromList width xs =
- let buf = Buffer (Seq.fromList xs)
- cache0 = RenderCache (Seq.replicate (length xs) Nothing)
- (cache1, rendered) = renderBuffer width buf cache0
- in RenderState
- { rsBuffer = buf
- , rsCache = cache1
- , rsRendered = rendered
- , rsWidth = width
- , rsLineCount = length (flatten rendered)
- }
-
-fromSeq :: Int -> Seq Item -> RenderState
-fromSeq width items =
- let buf = Buffer items
- cache0 = RenderCache (Seq.replicate (Seq.length items) Nothing)
- (cache1, rendered) = renderBuffer width buf cache0
- in RenderState
- { rsBuffer = buf
- , rsCache = cache1
- , rsRendered = rendered
- , rsWidth = width
- , rsLineCount = length (flatten rendered)
- }
-
-
-
---------------------------------------------------------------------------------
--- Viewport
---------------------------------------------------------------------------------
-
-data Viewport = Viewport
- { vpWidth :: !Int
- , vpHeight :: !Int
- , vpOffset :: !Int
- } deriving (Show)
-
-mkViewport :: Int -> Int -> Viewport
-mkViewport width height =
- Viewport
- { vpWidth = width
- , vpHeight = height
- , vpOffset = 0
- }
-
-defaultViewport :: Int -> Int -> RenderState -> Viewport
-defaultViewport width height rs =
- alignBottom rs (mkViewport width height)
-
--- any function that sets vpOffset and can overshoot should use clampViewport
-clampViewport :: RenderState -> Viewport -> Viewport
-clampViewport rs vp =
- let total = rsLineCount rs
- maxOff = max 0 (total - vpHeight vp)
- off = vpOffset vp
- in vp { vpOffset = max 0 (min maxOff off) }
-
-scrollBy :: Int -> RenderState -> Viewport -> Viewport
-scrollBy delta rs vp =
- clampViewport rs vp { vpOffset = vpOffset vp + delta }
-
-scrollUp :: Int -> RenderState -> Viewport -> Viewport
-scrollUp n = scrollBy (-n)
-
-scrollDown :: Int -> RenderState -> Viewport -> Viewport
-scrollDown n = scrollBy n
-
-pageUp :: RenderState -> Viewport -> Viewport
-pageUp rs vp =
- scrollBy (-(vpHeight vp)) rs vp
-
-pageDown :: RenderState -> Viewport -> Viewport
-pageDown rs vp =
- scrollBy (vpHeight vp) rs vp
-
-alignTop :: Viewport -> Viewport
-alignTop vp =
- vp { vpOffset = 0 }
-
-alignBottom :: RenderState -> Viewport -> Viewport
-alignBottom rs vp =
- let total = rsLineCount rs
- off = max 0 (total - vpHeight vp)
- in vp { vpOffset = off }
-
-
---------------------------------------------------------------------------------
--- Coordinate lookup
---------------------------------------------------------------------------------
-
-lookupPosition
- :: Int
- -> Int
- -> Viewport
- -> RenderedBuffer
- -> Maybe (Int, Int)
-lookupPosition x y vp rb =
- let allLines = flatten rb
- idx = vpOffset vp + y
- in case drop idx allLines of
- (rl:_) ->
- let charIx = rlCharStart rl + x
- in Just (rlItemIx rl, charIx)
- [] -> Nothing
-
---------------------------------------------------------------------------------
--- Viewport Instance ("simple" façade)
---------------------------------------------------------------------------------
-
-data ViewportInstance = ViewportInstance
- { viRender :: RenderState
- , viView :: Viewport
- }
-
-mkViewportInstance :: Int -> Int -> Buffer -> ViewportInstance
-mkViewportInstance width height buf =
- let rs = mkRenderState width buf
- vp = clampViewport rs (mkViewport width height)
- in ViewportInstance rs vp
-
-
-visibleLines :: ViewportInstance -> [RenderedLine]
-visibleLines (ViewportInstance rs vp) =
- take (vpHeight vp) . drop (vpOffset vp) . flatten $ rsRendered rs
-
-applyToInstance :: (Viewport -> Viewport) -> ViewportInstance -> ViewportInstance
-applyToInstance f (ViewportInstance rs vp) =
- ViewportInstance rs (f vp)
-
-applyToInstanceRS :: (RenderState -> Viewport -> Viewport) -> ViewportInstance -> ViewportInstance
-applyToInstanceRS f (ViewportInstance rs vp) =
- ViewportInstance rs (f rs vp)
-
-scrollByI :: Int -> ViewportInstance -> ViewportInstance
-scrollByI delta = applyToInstanceRS (scrollBy delta)
-
-scrollUpI :: Int -> ViewportInstance -> ViewportInstance
-scrollUpI delta = applyToInstanceRS (scrollUp delta)
-
-scrollDownI :: Int -> ViewportInstance -> ViewportInstance
-scrollDownI delta = applyToInstanceRS (scrollDown delta)
-
-pageUpI :: ViewportInstance -> ViewportInstance
-pageUpI = applyToInstanceRS pageUp
-
-pageDownI :: ViewportInstance -> ViewportInstance
-pageDownI = applyToInstanceRS pageDown
-
-alignTopI :: ViewportInstance -> ViewportInstance
-alignTopI = applyToInstance alignTop
-
-alignBottomI :: ViewportInstance -> ViewportInstance
-alignBottomI = applyToInstanceRS alignBottom
-
-modifyItemI :: Int -> (Item -> Item) -> ViewportInstance -> ViewportInstance
-modifyItemI ix f (ViewportInstance rs vp) =
- let buf' = modifyItem ix f (rsBuffer rs)
- rs' = mkRenderState (rsWidth rs) buf'
- vp' = clampViewport rs' vp
- in ViewportInstance rs' vp'
-
-lookupPositionI :: Int -> Int -> ViewportInstance -> Maybe (Int, Int)
-lookupPositionI x y (ViewportInstance rs vp) =
- lookupPosition x y vp (rsRendered rs)
diff --git a/src/TextViewport/Buffer/Buffer.hs b/src/TextViewport/Buffer/Buffer.hs
new file mode 100644
index 0000000..7b9582f
--- /dev/null
+++ b/src/TextViewport/Buffer/Buffer.hs
@@ -0,0 +1,35 @@
+module TextViewport.Buffer.Buffer where
+
+import Data.Sequence qualified as Seq
+import TextViewport.Buffer.Item (Item)
+
+newtype Buffer = Buffer { unBuffer :: Seq.Seq Item }
+ deriving (Eq, Show)
+
+-- | Build a buffer from a list
+fromList :: [Item] -> Buffer
+fromList xs = Buffer (Seq.fromList xs)
+
+-- | Modify an item at index
+modifyItem :: Int -> (Item -> Item) -> Buffer -> Buffer
+modifyItem ix f (Buffer xs) =
+ Buffer (Seq.adjust' f ix xs)
+
+-- | Insert an item
+insertItem :: Int -> Item -> Buffer -> Buffer
+insertItem ix x (Buffer xs) =
+ Buffer (Seq.insertAt ix x xs)
+
+-- | Delete an item
+deleteItem :: Int -> Buffer -> Buffer
+deleteItem ix (Buffer xs) =
+ Buffer (Seq.deleteAt ix xs)
+
+-- | Append an item
+appendItem :: Item -> Buffer -> Buffer
+appendItem x (Buffer xs) =
+ Buffer (xs Seq.|> x)
+
+-- | Extract underlying Seq (if needed)
+toSeq :: Buffer -> Seq.Seq Item
+toSeq (Buffer xs) = xs
diff --git a/src/TextViewport/Buffer/Item.hs b/src/TextViewport/Buffer/Item.hs
new file mode 100644
index 0000000..b08aaf4
--- /dev/null
+++ b/src/TextViewport/Buffer/Item.hs
@@ -0,0 +1,27 @@
+module TextViewport.Buffer.Item where
+
+import Data.Text (Text)
+import Data.HashMap.Strict qualified as HM
+import Text.Hyphenation qualified as H
+
+
+data Item = Item
+ { itemText :: Text
+ , itemSegments :: SegmentStrategy
+ }
+ deriving (Eq, Show)
+
+data SegmentStrategy
+ = NoSegments
+ | FixedWidthSegments
+ | HyphenateSegments
+ { hsDict :: H.Hyphenator
+ , hsCache :: HM.HashMap Text [(Text, Text)]
+ }
+ deriving (Eq, Show)
+
+instance Show H.Hyphenator where
+ show _ = "<Hyphenator>"
+
+instance Eq H.Hyphenator where
+ a == b = False
diff --git a/src/TextViewport/Render/CachedRender.hs b/src/TextViewport/Render/CachedRender.hs
new file mode 100644
index 0000000..c4b6cf2
--- /dev/null
+++ b/src/TextViewport/Render/CachedRender.hs
@@ -0,0 +1,15 @@
+module TextViewport.Render.CachedRender where
+
+import Data.Text (Text)
+import TextViewport.Buffer.Item (SegmentStrategy)
+import TextViewport.Render.RenderedItem (RenderedItem)
+
+
+data CachedRender = CachedRender
+ { crWidth :: !Int
+ , crStrategy :: !SegmentStrategy
+ , crText :: !Text
+ , crRendered :: !RenderedItem
+ }
+ deriving (Eq, Show)
+
diff --git a/src/TextViewport/Render/RenderBuffer.hs b/src/TextViewport/Render/RenderBuffer.hs
new file mode 100644
index 0000000..a1122a4
--- /dev/null
+++ b/src/TextViewport/Render/RenderBuffer.hs
@@ -0,0 +1,31 @@
+module TextViewport.Render.RenderBuffer where
+
+import Data.Sequence qualified as Seq
+import TextViewport.Buffer.Buffer (Buffer(..))
+import TextViewport.Render.CachedRender
+import TextViewport.Render.RenderCache
+import TextViewport.Render.RenderItem (renderItem)
+import TextViewport.Render.RenderedBuffer
+
+renderBuffer :: Int -> Buffer -> RenderCache -> (RenderCache, RenderedBuffer)
+renderBuffer width (Buffer items) (RenderCache cache) =
+ let n = Seq.length items
+ go i (cAcc, rAcc)
+ | i >= n = (RenderCache cAcc, RenderedBuffer rAcc)
+ | otherwise =
+ let item = Seq.index items i
+ mOld = Seq.index cache i
+ newEntry = renderItem width i item mOld
+ cAcc' = Seq.update i (Just newEntry) cAcc
+ rAcc' = rAcc Seq.|> crRendered newEntry
+ in go (i + 1) (cAcc', rAcc')
+ in go 0 (cache, Seq.empty)
+
+updateRenderedItem :: Int -> Int -> Buffer -> RenderCache -> RenderedBuffer -> (RenderCache, RenderedBuffer)
+updateRenderedItem width itemIx (Buffer items) (RenderCache cache) (RenderedBuffer rb) =
+ let item = Seq.index items itemIx
+ mOld = Seq.index cache itemIx
+ newEntry = renderItem width itemIx item mOld
+ newCache = Seq.update itemIx (Just newEntry) cache
+ newRB = Seq.update itemIx (crRendered newEntry) rb
+ in (RenderCache newCache, RenderedBuffer newRB)
diff --git a/src/TextViewport/Render/RenderCache.hs b/src/TextViewport/Render/RenderCache.hs
new file mode 100644
index 0000000..1db32fe
--- /dev/null
+++ b/src/TextViewport/Render/RenderCache.hs
@@ -0,0 +1,28 @@
+module TextViewport.Render.RenderCache where
+
+import Data.Sequence (Seq)
+import Data.Sequence qualified as Seq
+import TextViewport.Buffer.Buffer (Buffer(Buffer))
+import TextViewport.Render.CachedRender (CachedRender)
+
+
+newtype RenderCache = RenderCache { unRenderCache :: Seq (Maybe CachedRender) }
+ deriving (Eq, Show)
+
+-- | Create an empty cache matching the buffer size
+emptyRenderCacheFor :: Buffer -> RenderCache
+emptyRenderCacheFor (Buffer xs) =
+ RenderCache (Seq.replicate (Seq.length xs) Nothing)
+
+-- | Resize cache to match buffer length
+resizeCache :: Buffer -> RenderCache -> RenderCache
+resizeCache (Buffer xs) (RenderCache cache) =
+ let n = Seq.length xs
+ m = Seq.length cache
+ in RenderCache $
+ if m < n then cache <> Seq.replicate (n - m) Nothing
+ else Seq.take n cache
+
+-- | Number of cached items
+length :: RenderCache -> Int
+length (RenderCache xs) = Seq.length xs
diff --git a/src/TextViewport/Render/RenderItem.hs b/src/TextViewport/Render/RenderItem.hs
new file mode 100644
index 0000000..6c9cbc3
--- /dev/null
+++ b/src/TextViewport/Render/RenderItem.hs
@@ -0,0 +1,23 @@
+module TextViewport.Render.RenderItem where
+
+import TextViewport.Buffer.Item (Item(..))
+import TextViewport.Render.CachedRender
+import TextViewport.Render.RenderedItem
+import TextViewport.Render.Segmentation (applyStrategy)
+
+renderItem :: Int -> Int -> Item -> Maybe CachedRender -> CachedRender
+renderItem width itemIx (Item txt strategy) mOld =
+ case mOld of
+ Just old
+ | crWidth old == width
+ , crText old == txt
+ -> old
+ _ ->
+ let linesV = applyStrategy strategy width itemIx txt
+ rendered = RenderedItem linesV
+ in CachedRender
+ { crWidth = width
+ , crStrategy = strategy
+ , crText = txt
+ , crRendered = rendered
+ }
diff --git a/src/TextViewport/Render/RenderState.hs b/src/TextViewport/Render/RenderState.hs
new file mode 100644
index 0000000..26e92e1
--- /dev/null
+++ b/src/TextViewport/Render/RenderState.hs
@@ -0,0 +1,132 @@
+module TextViewport.Render.RenderState where
+
+import Data.Sequence (Seq)
+import Data.Sequence qualified as Seq
+import TextViewport.Buffer.Item
+import TextViewport.Buffer.Buffer (Buffer(Buffer))
+import TextViewport.Buffer.Buffer qualified as Buffer
+import TextViewport.Render.RenderBuffer (renderBuffer)
+import TextViewport.Render.RenderCache (RenderCache(..), emptyRenderCacheFor, resizeCache)
+import TextViewport.Render.RenderedBuffer (RenderedBuffer(RenderedBuffer))
+import TextViewport.Render.RenderedBuffer qualified as RenderedBuffer
+
+data RenderState = RenderState
+ { rsBuffer :: Buffer -- original items
+ , rsCache :: RenderCache -- per-item cached renders
+ , rsRendered :: RenderedBuffer -- fully segmented + hyphenated lines
+ , rsWidth :: Int -- segmenting width
+ , rsLineCount :: Int
+ } deriving (Eq, Show)
+
+mkRenderState :: Int -> Buffer -> RenderState
+mkRenderState width buf =
+ let (cache1, rendered) = renderBuffer width buf (emptyRenderCacheFor buf)
+ in RenderState
+ { rsBuffer = buf
+ , rsCache = cache1
+ , rsRendered = rendered
+ , rsWidth = width
+ , rsLineCount = length (RenderedBuffer.flatten rendered)
+ }
+
+-- RenderState has to be rebuilt whenever the buffer or the width changes.
+updateRenderState :: Int -> Buffer -> RenderState -> RenderState
+updateRenderState width buf rs =
+ let (cache1, rendered) = renderBuffer width buf (rsCache rs)
+ in rs
+ { rsBuffer = buf
+ , rsCache = cache1
+ , rsRendered = rendered
+ , rsWidth = width
+ , rsLineCount = length (RenderedBuffer.flatten rendered)
+ }
+
+modifyItemRS :: Int -> (Item -> Item) -> RenderState -> RenderState
+modifyItemRS ix f st =
+ let buf' = Buffer.modifyItem ix f (rsBuffer st)
+ cache' = resizeCache buf' (rsCache st)
+ (cache1, rendered1) = renderBuffer (rsWidth st) buf' cache'
+ in st { rsBuffer = buf'
+ , rsCache = cache1
+ , rsRendered = rendered1
+ , rsLineCount = length (RenderedBuffer.flatten rendered1)
+ }
+
+insertItem :: Int -> Item -> RenderState -> RenderState
+insertItem i newItem st =
+ let Buffer items = rsBuffer st
+ items' = Seq.insertAt i newItem items
+ buf' = Buffer items'
+ cache' = resizeCache buf' (rsCache st)
+ (cache1, rendered1) = renderBuffer (rsWidth st) buf' cache'
+ in st { rsBuffer = buf'
+ , rsCache = cache1
+ , rsRendered = rendered1
+ , rsLineCount = length (RenderedBuffer.flatten rendered1)
+ }
+
+deleteItem :: Int -> RenderState -> RenderState
+deleteItem i st =
+ let Buffer items = rsBuffer st
+ items' = Seq.deleteAt i items
+ buf' = Buffer items'
+ cache' = resizeCache buf' (rsCache st)
+ (cache1, rendered1) = renderBuffer (rsWidth st) buf' cache'
+ in st { rsBuffer = buf'
+ , rsCache = cache1
+ , rsRendered = rendered1
+ , rsLineCount = length (RenderedBuffer.flatten rendered1)
+ }
+
+replaceItem :: Int -> Item -> RenderState -> RenderState
+replaceItem i newItem st =
+ let Buffer items = rsBuffer st
+ items' = Seq.update i newItem items
+ buf' = Buffer items'
+ cache' = resizeCache buf' (rsCache st)
+ (cache1, rendered1) = renderBuffer (rsWidth st) buf' cache'
+ in st { rsBuffer = buf'
+ , rsCache = cache1
+ , rsRendered = rendered1
+ , rsLineCount = length (RenderedBuffer.flatten rendered1)
+ }
+
+appendItem :: Item -> RenderState -> RenderState
+appendItem newItem st =
+ insertItem (Seq.length (let Buffer xs = rsBuffer st in xs)) newItem st
+
+clearBuffer :: RenderState -> RenderState
+clearBuffer st =
+ let buf' = Buffer Seq.empty
+ cache' = RenderCache Seq.empty
+ in st { rsBuffer = buf'
+ , rsCache = cache'
+ , rsRendered = RenderedBuffer Seq.empty
+ , rsLineCount = 0
+ }
+
+fromList :: Int -> [Item] -> RenderState
+fromList width xs =
+ let buf = Buffer (Seq.fromList xs)
+ cache0 = RenderCache (Seq.replicate (length xs) Nothing)
+ (cache1, rendered) = renderBuffer width buf cache0
+ in RenderState
+ { rsBuffer = buf
+ , rsCache = cache1
+ , rsRendered = rendered
+ , rsWidth = width
+ , rsLineCount = length (RenderedBuffer.flatten rendered)
+ }
+
+fromSeq :: Int -> Seq Item -> RenderState
+fromSeq width items =
+ let buf = Buffer items
+ cache0 = RenderCache (Seq.replicate (Seq.length items) Nothing)
+ (cache1, rendered) = renderBuffer width buf cache0
+ in RenderState
+ { rsBuffer = buf
+ , rsCache = cache1
+ , rsRendered = rendered
+ , rsWidth = width
+ , rsLineCount = length (RenderedBuffer.flatten rendered)
+ }
diff --git a/src/TextViewport/Render/RenderedBuffer.hs b/src/TextViewport/Render/RenderedBuffer.hs
new file mode 100644
index 0000000..cbff8ca
--- /dev/null
+++ b/src/TextViewport/Render/RenderedBuffer.hs
@@ -0,0 +1,19 @@
+module TextViewport.Render.RenderedBuffer where
+
+import Data.Foldable qualified as F
+import Data.Sequence (Seq)
+import Data.Sequence qualified as Seq
+import TextViewport.Render.RenderedItem (RenderedItem)
+import TextViewport.Render.RenderedItem qualified as RenderedItem
+import TextViewport.Render.RenderedLine (RenderedLine