about summary refs log blame commit diff stats
path: root/src/local/select.nim
blob: ed5436382f6d18be0265813073edfcde8c920fd1 (plain) (tree)
1
2
3
4
5

              
               

                    































































































































































































                                                                     
                                  











































































































                                                                          
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