module TextUI.ItemField.Layout where
import Data.Monoid ( (<>) )
import Data.Either
import Data.Maybe
import Data.List (mapAccumL)
import qualified Data.Text as T
import Brick.AttrMap
import TextUI.ItemField.Types
import TextUI.ItemField.Attrs
data RenderData = RenderData Int [LinePosRange]
deriving (Eq, Show)
renderedLines :: RenderData -> [LinePosRange]
renderedLines (RenderData _ l) = l
renderWidth :: RenderData -> Int
renderWidth (RenderData w _) = w
data LinePosRange = LinePosRange Int Int
deriving (Show, Eq)
lineStart, lineWidth, lineEnd :: LinePosRange -> Int
lineStart (LinePosRange s _) = s
lineWidth (LinePosRange _ w) = w
lineEnd (LinePosRange s w) = s + w
data LineIndexRange = LineIndexRange Int Int | EmptyLine deriving (Show, Eq)
lineFirstIndex, lineLastIndex :: LineIndexRange -> Maybe Int
lineFirstIndex (LineIndexRange f _) = Just f
lineFirstIndex EmptyLine = Nothing
lineLastIndex (LineIndexRange _ l) = Just l
lineLastIndex EmptyLine = Nothing
lineIndices :: [LinePosRange] -> [LineIndexRange]
lineIndices =
let cvtFun prevEnd (LinePosRange _ w) = (prevEnd + w, nlr prevEnd w)
nlr p l = if l == 0
then EmptyLine
else LineIndexRange p $ p + max 0 (l 1)
in snd . mapAccumL cvtFun 0
ssRep :: ItemState -> (T.Text, Brick.AttrMap.AttrName)
ssRep Free = (T.singleton '.', itemFreeAttr)
ssRep Marked = (T.singleton '*', itemMarkedAttr)
ssRep Good = (T.singleton '+', itemGoodAttr)
ssRep Bad = (T.singleton '!', itemBadAttr)
ssRep CaVa = (T.singleton '~', itemCaVaAttr)
groupSepText :: T.Text
groupSepText = T.singleton ' '
groupSepImg :: (T.Text, AttrName)
groupSepImg = (groupSepText, itemBlankAttr)
aSpace :: (T.Text, AttrName)
aSpace = (T.singleton ' ', itemBlankAttr)
aSpaces :: Int -> (T.Text, AttrName)
aSpaces n = (T.replicate n (T.singleton ' '), itemBlankAttr)
computeLinePosRanges :: Int -> ItemField -> RenderData
computeLinePosRanges dispWidth field =
case partitionEithers $ layout (items field) of
([], l) -> RenderData dispWidth $ concat l
(reqWidths, _) -> if maximum reqWidths == dispWidth
then abort
else computeLinePosRanges (maximum reqWidths) field
where layout = snd . mapAccumL lay 0
lay ind (ItemGroup nm gs) =
(ind, snd $ lay (ind + T.length nm + sepwidth) gs)
lay ind (Items 0) | ind <= dispWidth =
(ind, Right [LinePosRange ind 0])
lay ind (Items n) =
let rwidth = dispWidth ind
(a,b) = n `divMod` rwidth
rng = map (LinePosRange ind) $ replicate a rwidth <> endrng
endrng = [b | a == 0 || b > 0 ]
in if ind >= dispWidth
then (ind, Left $ ind + n)
else (ind, Right rng)
sepwidth = T.length groupSepText
abort = error $ "line computation cycling with " <> show dispWidth
redrawSt8Lines :: Int -> [Items] -> [ItemState] -> RenderData -> [PlotLine]
redrawSt8Lines nLines si ss rdata =
let sl = plot si ss rdata
nCols = renderWidth rdata
moreMsg = T.pack $ take nCols "+++ more +++"
noneMsg = T.pack $ take nCols "[None identified yet]"
centerTextLn t a = let ls = (nCols T.length t) `div` 2
rs = nCols ls T.length t
in [ aSpaces ls, (t, a), aSpaces rs]
slPlusMore = take (nLines1) sl <>
[centerTextLn moreMsg itemMoreMessageAttr]
in if null si || cntItems si == 0
then [centerTextLn noneMsg itemNoneMessageAttr]
else if length sl <= nLines then sl else slPlusMore
type PlotLine = [(T.Text, Brick.AttrMap.AttrName)]
plot :: [Items] -> [ItemState] -> RenderData -> [PlotLine]
plot ss st8st8 rdata = concat $ snd $ mapAccumL plt (st8st8, renderedLines rdata) ss
where plt (si,rl) (Items 0) = ((si, tail rl), [[]])
plt (si,rl) (Items n) =
let (xx,yy) = splitAt curw $ si <> repeat Free
rng = head rl
curw = lineWidth rng
thisl = map ssRep xx
pltAcc' = (yy, tail rl)
(pltAcc'', cons) = plt pltAcc' $ Items $ n curw
in if n <= curw
then (pltAcc', [thisl])
else (pltAcc'', thisl : cons)
plt pltAcc (ItemGroup nm gs) =
let (pltAcc', grpl) = plt pltAcc gs
title = [(nm, itemHeaderAttr), groupSepImg]
prefixes = title : repeat [aSpaces $ T.length nm + 1]
glines = zipWith (<>) prefixes grpl
in (pltAcc', glines)
curLine :: ItemField -> RenderData -> (Int, Int)
curLine st rdata =
let cs = curSel st
lens = renderedLines rdata
findLC n _ [] = (n,0)
findLC n c (l:ls) = if c < l
then (n, c)
else findLC (n+1) (cl) ls
in findLC 0 cs $ map lineWidth lens
coordinatesSel :: ItemField -> RenderData -> (Int, Int) -> Maybe ItemLocation
coordinatesSel field rdata (col,row) =
let lens = renderedLines rdata
indices = lineIndices lens
li = indices !! row
lr = lens !! row
itm = (col lineStart lr) + fromJust (lineFirstIndex li)
in if row >= length lens
then Nothing
else case lineFirstIndex li of
Nothing -> Nothing
Just l -> if col >= lineEnd lr
then Nothing
else if col < lineStart lr
then coordinatesGroup field col l
else Just $ ItemLocNum itm
coordinatesGroup :: ItemField -> Int -> Int -> Maybe ItemLocation
coordinatesGroup field col atItemNum =
let itms = items field
walkToGroup _ [] = Nothing
walkToGroup (curIdx,grps) (ItemGroup nm ii : is) =
walkToGroup (curIdx, (T.length nm, nm) : grps) (ii : is)
walkToGroup (curIdx,grps) (Items n : is)
| curIdx + n 1 < atItemNum = walkToGroup (curIdx + n, []) is
| curIdx /= atItemNum = Nothing
| otherwise = unwind (0,[]) $ reverse grps
unwind _ [] = Nothing
unwind (al,ag) ((l,g):gs) =
if col < al + l
then Just $ ItemLocGroup atItemNum $ T.intercalate (T.singleton '.') $ reverse $ g : ag
else unwind (al + l + 1, g : ag) gs
in walkToGroup (0,[]) itms
data ItemLocation = ItemLocNum Int | ItemLocGroup Int T.Text deriving Show
pos_coordinates :: ItemField -> RenderData -> Maybe (Int, Int)
pos_coordinates field rdata =
let (cl,cc) = curLine field rdata
lens = renderedLines rdata
lrange = lens !! cl
curX = toEnum $ cc + lineStart lrange
curY = toEnum cl
in if cl >= length lens then Nothing else Just (curX, curY)
update_position :: ([ItemState] -> Int -> Int) -> ItemField -> ([Int], ItemField)
update_position update_func field =
let oldsel = curSel field
newsel = update_func (itemst8 field) oldsel
newfield = field { curSel = newsel }
sel_range = if oldsel < newsel
then [oldsel .. newsel]
else [newsel .. oldsel]
in (sel_range, newfield)
bounded :: Ord b => (a -> b) -> b -> b -> a -> b
bounded f minV maxV = max minV . min maxV . f
boundedIdx :: Foldable t => (a -> Int) -> t c -> a -> Int
boundedIdx f l = bounded f 0 (length l 1)
type FieldUpdateFunc = ItemField -> ([Int], ItemField)
select_next, select_prev :: FieldUpdateFunc
select_next = update_position (boundedIdx succ)
select_prev = update_position $ boundedIdx pred
select_forward_n, select_backward_n :: Int -> FieldUpdateFunc
select_forward_n n = update_position $ boundedIdx (n +)
select_backward_n n = update_position $ boundedIdx (\x -> x n)
select_next_line, select_prev_line :: RenderData -> FieldUpdateFunc
select_next_line rdata field =
let lens = renderedLines rdata
(cl, cc) = curLine field rdata
cc' = if (cl + 1) < length lens
then min cc $ nextLW (cl + 1) 1
else cc
nextLW ln = if ln == length lens then 0
else let tlw = lineWidth (lens !! ln)
in if tlw > 0 then tlw
else nextLW $ ln + 1
inc = cc' + (lineWidth (lens !! cl) cc)
in update_position (boundedIdx (inc +)) field
select_prev_line rdata field =
let lens = renderedLines rdata
(cl,cc) = curLine field rdata
prevLW ln = if ln == 0 then 0
else let tlw = lineWidth $ lens !! ln
in if tlw > 0 then tlw
else prevLW $ ln 1
cc' = if cl > 0
then prevLW (cl1) min cc (prevLW (cl1) 1)
else 0
dec = cc + cc'
in update_position (boundedIdx (\n -> n dec)) field