summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-03-05 17:47:38 +0100
committertv <tv@krebsco.de>2026-03-06 20:42:17 +0100
commitc6a68227069d33fdcaa07295322974ab324000c0 (patch)
treea9abefbdc4c634a1d19b005662ffdc70ef14a509
parente66e5ee8aa62153b83bc2ae201a8ad44d90e08b8 (diff)
bump
-rw-r--r--TextViewport.hs69
1 files changed, 35 insertions, 34 deletions
diff --git a/TextViewport.hs b/TextViewport.hs
index 927ba8b..ea075f4 100644
--- a/TextViewport.hs
+++ b/TextViewport.hs
@@ -3,6 +3,7 @@
module TextViewport
( Item(..)
, Buffer(..)
+ , WrapMode(..)
, Viewport(..)
, RenderedLine(..)
, renderBuffer
@@ -19,77 +20,77 @@ 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
+-- Rendering
--------------------------------------------------------------------------------
--- | A single line after splitting on hard breaks and cropping to viewport width.
--- This is a *physical* line: no wrapping, only truncation.
+-- | Rendering mode: either truncate long lines or wrap them.
+data WrapMode
+ = NoWrap -- ^ Hard truncation at viewport width.
+ | Wrap -- ^ Soft line breaks inserted at viewport width.
+ deriving (Eq, Show)
+
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) =
+-- | Render the buffer into a flat list of physical lines.
+-- Wrapping expands logical lines; truncation keeps one line per logical line.
+renderBuffer :: WrapMode -> Int -> Buffer -> [RenderedLine]
+renderBuffer mode 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
+ concatMap renderLogicalLine (T.splitOn "\n" t)
+
+ renderLogicalLine :: Text -> [RenderedLine]
+ renderLogicalLine line =
+ case mode of
+ NoWrap -> [RenderedLine (T.take w line)]
+ Wrap -> map RenderedLine (wrapText w line)
+
+-- | Split a line into chunks of at most width characters.
+-- This is soft wrapping: no hyphenation, no word-boundary logic.
+wrapText :: Int -> Text -> [Text]
+wrapText w t
+ | T.null t = [""]
+ | otherwise = go t
+ where
+ go s
+ | T.null s = []
+ | otherwise =
+ let (chunk, rest) = T.splitAt w s
+ in chunk : go rest
--------------------------------------------------------------------------------
--- Viewport state
+-- Viewport
--------------------------------------------------------------------------------
--- | 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
+ , vpOffset :: !Int
} 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
+ 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