import unicode
import js/regex
import server/buffer
import types/cell
import utils/twtstr
type
SubmitSelect* = proc(selected: seq[int])
CloseSelect* = proc()
Select* = object
open*: bool
options: seq[string]
multiple: bool
# old selection
oselected*: seq[int]
# new selection
selected*: seq[int]
# cursor distance from y
cursor: int
# widest option
maxw: int
# maximum height on screen (yes the naming is dumb)
maxh: int
# first index to display
si: int
# location on screen
x: int
y: int
redraw*: bool
submitFun: SubmitSelect
bpos: seq[int]
proc windowChange*(select: var Select, height: int) =
select.maxh = height - 2
if select.y + select.options.len >= select.maxh:
select.y = height - select.options.len
if select.y < 0:
select.si = -select.y
select.y = 0
if select.selected.len > 0:
let i = select.selected[0]
if select.si > i:
select.si = i
elif select.si + select.maxh < i:
select.si = max(i - select.maxh, 0)
select.redraw = true
proc initSelect*(select: var Select, selectResult: SelectResult,
x, y, height: int, submitFun: SubmitSelect) =
select.open = true
select.multiple = selectResult.multiple
select.options = selectResult.options
select.oselected = selectResult.selected
select.selected = selectResult.selected
select.submitFun = submitFun
for opt in select.options.mitems:
opt.mnormalize()
select.maxw = max(select.maxw, opt.width())
select.x = x
select.y = y
select.windowChange(height)
# index of option currently under cursor
func hover(select: Select): int =
return select.cursor + select.si
func dispheight(select: Select): int =
return select.maxh - select.y
proc `hover=`(select: var Select, i: int) =
let i = clamp(i, 0, select.options.high)
if i >= select.si + select.dispheight:
select.si = i - select.dispheight + 1
select.cursor = select.dispheight - 1
elif i < select.si:
select.si = i
select.cursor = 0
else:
select.cursor = i - select.si
proc cursorDown*(select: var Select) =
if select.hover < select.options.high and
select.cursor + select.y < select.maxh - 1:
inc select.cursor
select.redraw = true
elif select.si < select.options.len - select.maxh:
inc select.si
select.redraw = true
proc cursorUp*(select: var Select) =
if select.cursor > 0:
dec select.cursor
select.redraw = true
elif select.si > 0:
dec select.si
select.redraw = true
elif select.multiple and select.cursor > -1:
select.cursor = -1
proc close(select: var Select) =
select = Select()
proc cancel*(select: var Select) =
select.submitFun(select.oselected)
select.close()
proc submit(select: var Select) =
select.submitFun(select.selected)
select.close()
proc click*(select: var Select) =
if not select.multiple:
select.selected = @[select.hover]
select.submit()
elif select.cursor == -1:
select.submit()
else:
var k = select.selected.len
let i = select.hover
for j in 0 ..< select.selected.len:
if select.selected[j] >= i:
k = j
break
if k < select.selected.len and select.selected[k] == i:
select.selected.delete(k)
else:
select.selected.insert(i, k)
select.redraw = true
proc cursorLeft*(select: var Select) =
select.submit()
proc cursorRight*(select: var Select) =
select.click()
proc getCursorX*(select: var Select): int =
if select.cursor == -1:
return select.x
return select.x + 1
proc getCursorY*(select: var Select): int =
return select.y + 1 + select.cursor
proc cursorFirstLine*(select: var Select) =
if select.cursor != 0 or select.si != 0:
select.cursor = 0
select.si = 0
select.redraw = true
proc cursorLastLine*(select: var Select) =
if select.hover < select.options.len:
select.cursor = select.dispheight - 1
select.si = max(select.options.len - select.maxh, 0)
select.redraw = true
proc cursorNextMatch*(select: var Select, regex: Regex, wrap: bool) =
var j = -1
for i in select.hover + 1 ..< select.options.len:
if regex.exec(select.options[i]).success:
j = i
break
if j != -1:
select.hover = j
select.redraw = true
elif wrap:
for i in 0 ..< select.hover:
if regex.exec(select.options[i]).success:
j = i
break
if j != -1:
select.hover = j
select.redraw = true
proc cursorPrevMatch*(select: var Select, regex: Regex, wrap: bool) =
var j = -1
for i in countdown(select.hover - 1, 0):
if regex.exec(select.options[i]).success:
j = i
break
if j != -1:
select.hover = j
select.redraw = true
elif wrap:
for i in countdown(select.options.high, select.hover):
if regex.exec(select.options[i]).success:
j = i
break
if j != -1:
select.hover = j
select.redraw = true
proc pushCursorPos*(select: var Select) =
select.bpos.add(select.hover)
proc popCursorPos*(select: var Select, nojump = false) =
select.hover = select.bpos.pop()
if not nojump:
select.redraw = true
const HorizontalBar = $Rune(0x2500)
const VerticalBar = $Rune(0x2502)
const CornerTopLeft = $Rune(0x250C)
const CornerTopRight = $Rune(0x2510)
const CornerBottomLeft = $Rune(0x2514)
const CornerBottomRight = $Rune(0x2518)
proc drawBorders(display: var FixedGrid, sx, ex, sy, ey: int,
upmore, downmore: bool) =
for y in sy .. ey:
var x = 0
while x < sx:
if display[y * display.width + x].str == "":
display[y * display.width + x].str = " "
inc x
else:
#x = display[y * display.width + x].str.twidth(x)
inc x
# Draw corners.
let tl = if upmore: VerticalBar else: CornerTopLeft
let tr = if upmore: VerticalBar else: CornerTopRight
let bl = if downmore: VerticalBar else: CornerBottomLeft
let br = if downmore: VerticalBar else: CornerBottomRight
const fmt = newFormat()
display[sy * display.width + sx].str = tl
display[sy * display.width + ex].str = tr
display[ey * display.width + sx].str = bl
display[ey * display.width + ex].str = br
display[sy * display.width + sx].format = fmt
display[sy * display.width + ex].format = fmt
display[ey * display.width + sx].format = fmt
display[ey * display.width + ex].format = fmt
# Draw top, bottom borders.
let ups = if upmore: " " else: HorizontalBar
let downs = if downmore: " " else: HorizontalBar
for x in sx + 1 .. ex - 1:
display[sy * display.width + x].str = ups
display[ey * display.width + x].str = downs
display[sy * display.width + x].format = fmt
display[ey * display.width + x].format = fmt
if upmore:
display[sy * display.width + sx + (ex - sx) div 2].str = ":"
if downmore:
display[ey * display.width + sx + (ex - sx) div 2].str = ":"
# Draw left, right borders.
for y in sy + 1 .. ey - 1:
display[y * display.width + sx].str = VerticalBar
display[y * display.width + ex].str = VerticalBar
display[y * display.width + sx].format = fmt
display[y * display.width + ex].format = fmt
proc drawSelect*(select: Select, display: var FixedGrid) =
if display.width < 2 or display.height < 2:
return # border does not fit...
# Max width, height with one row/column on the sides.
let mw = display.width - 2
let mh = display.height - 2
var sy = select.y
let si = select.si
var ey = min(sy + select.options.len, mh) + 1
var sx = select.x
if sx + select.maxw >= mw:
sx = display.width - select.maxw
if sx < 0:
# This means the widest option is wider than the available screen.
# w3m simply cuts off the part that doesn't fit, and we do that too,
# but I feel like this may not be the best solution.
sx = 0
var ex = min(sx + select.maxw, mw) + 1
let upmore = select.si > 0
let downmore = select.si + mh < select.options.len
drawBorders(display, sx, ex, sy, ey, upmore, downmore)
if select.multiple and not upmore:
display[sy * display.width + sx].str = "X"
# move inside border
inc sy
inc sx
var r: Rune
var k = 0
var format = newFormat()
while k < select.selected.len and select.selected[k] < si:
inc k
for y in sy ..< ey:
let i = y - sy + si
var j = 0
var x = sx
let dls = y * display.width
if k < select.selected.len and select.selected[k] == i:
format.reverse = true
inc k
else:
format.reverse = false
while j < select.options[i].len:
fastRuneAt(select.options[i], j, r)
let rw = r.twidth(x)
let ox = x
x += rw
if x > ex:
break
display[dls + ox].str = $r
display[dls + ox].format = format
while x < ex:
display[dls + x].str = " "
display[dls + x].format = format
inc x