import Bliku.Decoration
namespace Bliku.Widget
open Bliku
structure TextViewProps where
lines : Array String
scrollCol : Nat := 0
byteDecorations : Array ByteDecoration := #[]
cellDecorations : Array CellDecoration := #[]
cursor : Option CursorDecoration := none
resetStyle : String := "\x1b[0m"
deriving Inhabited
structure VisibleCell where
text : String
col : Nat
startByte : Nat
endByte : Nat
deriving Repr, Inhabited
private def collectVisibleCells (line : String) (scrollCol width : Nat) : Array VisibleCell := Id.run do
let mut out : Array VisibleCell := #[]
let mut bytePos := 0
let mut col := 0
for ch in line.toList do
let nextByte := bytePos + ch.utf8Size
if scrollCol <= col && col < scrollCol + width then
out := out.push {
text := ch.toString
col := col
startByte := bytePos
endByte := nextByte
}
bytePos := nextByte
col := col + 1
out
private def lineDisplayLength (line : String) : Nat :=
line.toList.length
private def faceForByteRange
(decos : Array ByteDecoration) (row byteStart byteEnd : Nat) : Option String :=
let rec loop (i : Nat) (best : Option ByteDecoration) : Option String :=
if i >= decos.size then
best.map ByteDecoration.style
else
let deco := decos[i]!
let best' :=
if deco.row == row && byteStart < deco.endByte && byteEnd > deco.startByte then
match best with
| none => some deco
| some current => if current.priority <= deco.priority then some deco else best
else
best
loop (i + 1) best'
loop 0 none
private def faceForCell
(decos : Array CellDecoration) (row col : Nat) : Option String :=
let rec loop (i : Nat) (best : Option CellDecoration) : Option String :=
if i >= decos.size then
best.map CellDecoration.style
else
let deco := decos[i]!
let best' :=
if deco.row == row && deco.startCol <= col && col < deco.endCol then
match best with
| none => some deco
| some current => if current.priority <= deco.priority then some deco else best
else
best
loop (i + 1) best'
loop 0 none
def renderVisibleLine (props : TextViewProps) (row width : Nat) : String := Id.run do
let line := props.lines[row]?.getD ""
let visibleCells := collectVisibleCells line props.scrollCol width
let mut out : Array String := #[]
let mut activeStyle : Option String := none
for cell in visibleCells do
let desiredStyle : Option String :=
match props.cursor with
| some cur =>
if cur.row == row && cur.col == cell.col then
some (if cell.text == " " then cur.spaceStyle else cur.charStyle)
else
(faceForCell props.cellDecorations row cell.col).orElse
(fun _ => faceForByteRange props.byteDecorations row cell.startByte cell.endByte)
| none =>
(faceForCell props.cellDecorations row cell.col).orElse
(fun _ => faceForByteRange props.byteDecorations row cell.startByte cell.endByte)
if desiredStyle != activeStyle then
match activeStyle with
| some _ => out := out.push props.resetStyle
| none => pure ()
match desiredStyle with
| some style => out := out.push style
| none => pure ()
activeStyle := desiredStyle
out := out.push cell.text
if activeStyle.isSome then
out := out.push props.resetStyle
return String.intercalate "" out.toList
def appendCursorOnBlankCell (props : TextViewProps) (row width : Nat) (lineToDraw : String) : String := Id.run do
let some cur := props.cursor | return lineToDraw
if cur.row != row || cur.col < props.scrollCol then
return lineToDraw
let line := props.lines[row]?.getD ""
let visCol := cur.col - props.scrollCol
let visLen := min width (lineDisplayLength line - props.scrollCol)
if visCol < visLen || visCol >= width then
return lineToDraw
let padCount := visCol - visLen
return lineToDraw ++ ("".pushn ' ' padCount) ++ cur.spaceStyle ++ " " ++ props.resetStyle
end Bliku.Widget