about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-09-24 19:42:29 +0200
committerbptato <nincsnevem662@gmail.com>2024-09-24 19:43:30 +0200
commit0c738c94e14c213562f69ff6e376c19fb0487201 (patch)
tree0d61bc0651f9120bbc7245e774dab04701a3d16e
parentb42cb50ee0213deb568b6c92965e7e193122c963 (diff)
downloadchawan-0c738c94e14c213562f69ff6e376c19fb0487201.tar.gz
select: use a separate module (again)
Now we dispatch to select objects from the pager object too, just
to make things even more confusing.

Well, it works better than the previous arrangement, in that trying to
use unimplemented movements now just throws instead of moving around
the container. Yay for OOP (?)
-rw-r--r--src/local/client.nim2
-rw-r--r--src/local/container.nim402
-rw-r--r--src/local/pager.nim6
-rw-r--r--src/local/select.nim315
4 files changed, 374 insertions, 351 deletions
diff --git a/src/local/client.nim b/src/local/client.nim
index f32e758d..33bb44e3 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -34,6 +34,7 @@ import loader/response
 import local/container
 import local/lineedit
 import local/pager
+import local/select
 import local/term
 import monoucha/constcharp
 import monoucha/fromjs
@@ -820,6 +821,7 @@ proc addJSModules(client: Client; ctx: JSContext) =
   ctx.addConfigModule()
   ctx.addPagerModule()
   ctx.addContainerModule()
+  ctx.addSelectModule()
   ctx.addCookieModule()
 
 func getClient(client: Client): Client {.jsfget: "client".} =
diff --git a/src/local/container.nim b/src/local/container.nim
index 81f5ffb0..6bdfe64b 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -15,6 +15,7 @@ import loader/headers
 import loader/loaderiface
 import loader/request
 import loader/response
+import local/select
 import monoucha/javascript
 import monoucha/jsregex
 import monoucha/jstypes
@@ -122,7 +123,7 @@ type
     dispw*: int # same as CanvasImage.dispw
     erry*: int # same as CanvasImage.offy % 6
 
-  Container* = ref object
+  Container* = ref object of RootObj
     # note: this is not the same as source.request.url (but should be synced
     # with buffer.url)
     url* {.jsget.}: URL
@@ -173,7 +174,7 @@ type
     events*: Deque[ContainerEvent]
     startpos: Option[CursorPosition]
     redirectDepth*: int
-    select*: Select
+    select* {.jsget.}: Select
     currentSelection {.jsget.}: Highlight
     tmpJumpMark: PagePos
     jumpMark: PagePos
@@ -188,23 +189,6 @@ type
     luctx: LUContext
     redraw*: bool
 
-  Select = ref object
-    container: Container
-    options: seq[string]
-    multiple: bool
-    oselected: seq[int] # old selection
-    selected: seq[int] # new selection
-    cursor: int # cursor distance from y
-    maxw: int # widest option
-    maxh: int # maximum height on screen (yes the naming is dumb)
-    si: int # first index to display
-    # location on screen
-    #TODO make this absolute
-    x: int
-    y: int
-    redraw*: bool
-    bpos: seq[int]
-
 jsDestructor(Highlight)
 jsDestructor(Container)
 
@@ -214,275 +198,6 @@ proc updateCursor(container: Container)
 proc cursorLastLine*(container: Container)
 proc triggerEvent(container: Container; t: ContainerEventType)
 
-proc queueDraw(select: Select) =
-  select.redraw = true
-
-proc windowChange(select: 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.queueDraw()
-
-# 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: 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: Select) =
-  if select.hover < select.options.high and
-      select.cursor + select.y < select.maxh - 1:
-    inc select.cursor
-    select.queueDraw()
-  elif select.si < select.options.len - select.maxh:
-    inc select.si
-    select.queueDraw()
-
-proc cursorUp(select: Select) =
-  if select.cursor > 0:
-    dec select.cursor
-    select.queueDraw()
-  elif select.si > 0:
-    dec select.si
-    select.queueDraw()
-  elif select.multiple and select.cursor > -1:
-    select.cursor = -1
-
-proc close(select: Select) =
-  let container = select.container
-  container.select = nil
-
-proc cancel(select: Select) =
-  let container = select.container
-  container.iface.select(select.oselected).then(proc(res: ClickResult) =
-    container.onclick(res, save = false))
-  select.close()
-
-proc submit(select: Select) =
-  let container = select.container
-  container.iface.select(select.selected).then(proc(res: ClickResult) =
-    container.onclick(res, save = false))
-  select.close()
-
-proc click(select: 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.queueDraw()
-
-proc cursorLeft(select: Select) =
-  select.submit()
-
-proc cursorRight(select: Select) =
-  select.click()
-
-proc getCursorX*(select: Select): int =
-  if select.cursor == -1:
-    return select.x
-  return select.x + 1
-
-proc getCursorY*(select: Select): int =
-  return select.y + 1 + select.cursor
-
-proc cursorFirstLine(select: Select) =
-  if select.cursor != 0 or select.si != 0:
-    select.cursor = 0
-    select.si = 0
-    select.queueDraw()
-
-proc cursorLastLine(select: Select) =
-  if select.hover < select.options.len:
-    select.cursor = select.dispheight - 1
-    select.si = max(select.options.len - select.maxh, 0)
-    select.queueDraw()
-
-proc cursorNextMatch(select: 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.queueDraw()
-  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.queueDraw()
-
-proc cursorPrevMatch(select: 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.queueDraw()
-  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.queueDraw()
-
-proc pushCursorPos(select: Select) =
-  select.bpos.add(select.hover)
-
-proc popCursorPos(select: Select; nojump = false) =
-  select.hover = select.bpos.pop()
-  if not nojump:
-    select.queueDraw()
-
-const HorizontalBar = "\u2500"
-const VerticalBar = "\u2502"
-const CornerTopLeft = "\u250C"
-const CornerTopRight = "\u2510"
-const CornerBottomLeft = "\u2514"
-const CornerBottomRight = "\u2518"
-
-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 = Format()
-  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 k = 0
-  var format = Format()
-  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.flags.incl(ffReverse)
-      inc k
-    else:
-      format.flags.excl(ffReverse)
-    while j < select.options[i].len:
-      let pj = j
-      let u = select.options[i].nextUTF8(j)
-      let nx = x + u.twidth(x)
-      if nx > ex:
-        break
-      display[dls + x].str = select.options[i].substr(pj, j - 1)
-      display[dls + x].format = format
-      inc x
-      while x < nx:
-        display[dls + x].str = ""
-        display[dls + x].format = format
-        inc x
-    while x < ex:
-      display[dls + x].str = " "
-      display[dls + x].format = format
-      inc x
-
 proc newContainer*(config: BufferConfig; loaderConfig: LoaderClientConfig;
     url: URL; request: Request; luctx: LUContext; attrs: WindowAttributes;
     title: string; redirectDepth: int; flags: set[ContainerFlag];
@@ -1000,28 +715,16 @@ proc setCursorXYCenter(container: Container; x, y: int; refresh = true)
     container.centerColumn()
 
 proc cursorDown(container: Container; n = 1) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorDown()
-  else:
-    container.setCursorY(container.cursory + n)
+  container.setCursorY(container.cursory + n)
 
 proc cursorUp(container: Container; n = 1) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorUp()
-  else:
-    container.setCursorY(container.cursory - n)
+  container.setCursorY(container.cursory - n)
 
 proc cursorLeft(container: Container; n = 1) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorLeft()
-  else:
-    container.setCursorX(container.cursorFirstX() - n)
+  container.setCursorX(container.cursorFirstX() - n)
 
 proc cursorRight(container: Container; n = 1) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorRight()
-  else:
-    container.setCursorX(container.cursorLastX() + n)
+  container.setCursorX(container.cursorLastX() + n)
 
 proc cursorLineBegin(container: Container) {.jsfunc.} =
   container.setCursorX(-1)
@@ -1275,20 +978,14 @@ proc markPos*(container: Container) =
     container.jumpMark = pos
 
 proc cursorFirstLine(container: Container) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorFirstLine()
-  else:
-    container.markPos0()
-    container.setCursorY(0)
-    container.markPos()
+  container.markPos0()
+  container.setCursorY(0)
+  container.markPos()
 
 proc cursorLastLine*(container: Container) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorLastLine()
-  else:
-    container.markPos0()
-    container.setCursorY(container.numLines - 1)
-    container.markPos()
+  container.markPos0()
+  container.setCursorY(container.numLines - 1)
+  container.markPos()
 
 proc cursorTop(container: Container; i = 1) {.jsfunc.} =
   container.markPos0()
@@ -1815,9 +1512,7 @@ proc remoteCancel*(container: Container) =
   container.alert("Canceled loading")
 
 proc cancel*(container: Container) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cancel()
-  elif container.loadState == lsLoading:
+  if container.loadState == lsLoading:
     container.loadState = lsCanceled
     if container.iface != nil:
       container.remoteCancel()
@@ -1856,21 +1551,30 @@ proc reshape(container: Container): EmptyPromise {.jsfunc.} =
     return container.requestLines()
   )
 
+proc selectFinish(opaque: RootRef; select: Select; res: SubmitResult) =
+  let container = Container(opaque)
+  case res
+  of srCancel:
+    container.iface.select(select.oselected).then(proc(res: ClickResult) =
+      container.onclick(res, save = false)
+    )
+  of srSubmit:
+    container.iface.select(select.selected).then(proc(res: ClickResult) =
+      container.onclick(res, save = false)
+    )
+  container.select = nil
+
 proc displaySelect(container: Container; selectResult: SelectResult) =
-  container.select = Select(
-    container: container,
-    multiple: selectResult.multiple,
-    options: selectResult.options,
-    oselected: selectResult.selected,
-    selected: selectResult.selected,
-    x: container.acursorx,
-    y: container.acursory
+  container.select = newSelect(
+    selectResult.multiple,
+    selectResult.options,
+    selectResult.selected,
+    container.acursorx,
+    container.acursory,
+    container.height,
+    selectFinish,
+    container
   )
-  for opt in container.select.options.mitems:
-    opt.mnormalize()
-    container.select.maxw = max(container.select.maxw, opt.width())
-  container.select.windowChange(container.height)
-  container.select.queueDraw()
 
 proc onclick(container: Container; res: ClickResult; save: bool) =
   if res.repaint:
@@ -1887,13 +1591,10 @@ proc onclick(container: Container; res: ClickResult; save: bool) =
     container.onReadLine(res.readline.get)
 
 proc click*(container: Container) {.jsfunc.} =
-  if container.select != nil:
-    container.select.click()
-  else:
-    if container.iface == nil:
-      return
-    container.iface.click(container.cursorx, container.cursory)
-      .then(proc(res: ClickResult) = container.onclick(res, save = false))
+  if container.iface == nil:
+    return
+  container.iface.click(container.cursorx, container.cursory)
+    .then(proc(res: ClickResult) = container.onclick(res, save = false))
 
 proc saveLink*(container: Container) {.jsfunc.} =
   if container.iface == nil:
@@ -1912,17 +1613,18 @@ proc saveSource*(container: Container) {.jsfunc.} =
   ))
 
 proc windowChange*(container: Container; attrs: WindowAttributes) =
-  if attrs.width != container.width or attrs.height - 1 != container.height:
-    container.width = attrs.width
-    container.height = attrs.height - 1
-    if container.iface != nil:
-      var attrs = attrs
-      # subtract status line height
-      attrs.height -= 1
-      attrs.heightPx -= attrs.ppl
-      container.iface.windowChange(attrs).then(proc() =
-        container.needslines = true
-      )
+  container.width = attrs.width
+  container.height = attrs.height - 1
+  if container.iface != nil:
+    var attrs = attrs
+    # subtract status line height
+    attrs.height -= 1
+    attrs.heightPx -= attrs.ppl
+    container.iface.windowChange(attrs).then(proc() =
+      container.needslines = true
+    )
+  if container.select != nil:
+    container.select.windowChange(container.height)
 
 proc peek(container: Container) {.jsfunc.} =
   container.alert($container.url)
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 3e0c21a0..ec242472 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -27,6 +27,7 @@ import loader/request
 import loader/response
 import local/container
 import local/lineedit
+import local/select
 import local/term
 import monoucha/fromjs
 import monoucha/javascript
@@ -231,7 +232,10 @@ proc reflect(ctx: JSContext; this_val: JSValue; argc: cint;
 
 proc getter(ctx: JSContext; pager: Pager; a: JSAtom): JSValue {.jsgetprop.} =
   if pager.container != nil:
-    let cval = ctx.toJS(pager.container)
+    let cval = if pager.container.select != nil:
+      ctx.toJS(pager.container.select)
+    else:
+      ctx.toJS(pager.container)
     let val = JS_GetProperty(ctx, cval, a)
     if JS_IsFunction(ctx, val):
       let func_data = @[cval, val]
diff --git a/src/local/select.nim b/src/local/select.nim
new file mode 100644
index 00000000..70726e7c
--- /dev/null
+++ b/src/local/select.nim
@@ -0,0 +1,315 @@
+import monoucha/javascript
+import monoucha/jsregex
+import types/cell
+import utils/luwrap
+import utils/strwidth
+import utils/twtuni
+
+type
+  SelectFinish* = proc(opaque: RootRef; select: Select; res: SubmitResult)
+    {.nimcall.}
+
+  Select* = ref object
+    options: seq[string]
+    multiple: bool
+    oselected*: seq[int] # old selection
+    selected*: seq[int] # new selection
+    cursor: int # cursor distance from y
+    maxw: int # widest option
+    maxh: int # maximum height on screen (yes the naming is dumb)
+    si: int # first index to display
+    # location on screen
+    #TODO make this absolute
+    x: int
+    y: int
+    redraw*: bool
+    bpos: seq[int]
+    opaque: RootRef
+    finishImpl: SelectFinish
+
+  SubmitResult* = enum
+    srCancel, srSubmit
+
+jsDestructor(Select)
+
+proc queueDraw(select: Select) =
+  select.redraw = true
+
+# 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: 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: Select) {.jsfunc.} =
+  if select.hover < select.options.high and
+      select.cursor + select.y < select.maxh - 1:
+    inc select.cursor
+    select.queueDraw()
+  elif select.si < select.options.len - select.maxh:
+    inc select.si
+    select.queueDraw()
+
+proc cursorUp(select: Select) {.jsfunc.} =
+  if select.cursor > 0:
+    dec select.cursor
+    select.queueDraw()
+  elif select.si > 0:
+    dec select.si
+    select.queueDraw()
+  elif select.multiple and select.cursor > -1:
+    select.cursor = -1
+
+proc cancel(select: Select) {.jsfunc.} =
+  select.finishImpl(select.opaque, select, srCancel)
+
+proc submit(select: Select) {.jsfunc.} =
+  select.finishImpl(select.opaque, select, srSubmit)
+
+proc click(select: Select) {.jsfunc.} =
+  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.queueDraw()
+
+proc cursorLeft(select: Select) {.jsfunc.} =
+  select.submit()
+
+proc cursorRight(select: Select) {.jsfunc.} =
+  select.click()
+
+proc getCursorX*(select: Select): int =
+  if select.cursor == -1:
+    return select.x
+  return select.x + 1
+
+proc getCursorY*(select: Select): int =
+  return select.y + 1 + select.cursor
+
+proc cursorFirstLine(select: Select) {.jsfunc.} =
+  if select.cursor != 0 or select.si != 0:
+    select.cursor = 0
+    select.si = 0
+    select.queueDraw()
+
+proc cursorLastLine(select: Select) {.jsfunc.} =
+  if select.hover < select.options.len:
+    select.cursor = select.dispheight - 1
+    select.si = max(select.options.len - select.maxh, 0)
+    select.queueDraw()
+
+proc cursorNextMatch*(select: 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.queueDraw()
+  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.queueDraw()
+
+proc cursorPrevMatch*(select: 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.queueDraw()
+  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.queueDraw()
+
+proc pushCursorPos*(select: Select) =
+  select.bpos.add(select.hover)
+
+proc popCursorPos*(select: Select; nojump = false) =
+  select.hover = select.bpos.pop()
+  if not nojump:
+    select.queueDraw()
+
+const HorizontalBar = "\u2500"
+const VerticalBar = "\u2502"
+const CornerTopLeft = "\u250C"
+const CornerTopRight = "\u2510"
+const CornerBottomLeft = "\u2514"
+const CornerBottomRight = "\u2518"
+
+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 = Format()
+  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 k = 0
+  var format = Format()
+  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.flags.incl(ffReverse)
+      inc k
+    else:
+      format.flags.excl(ffReverse)
+    while j < select.options[i].len:
+      let pj = j
+      let u = select.options[i].nextUTF8(j)
+      let nx = x + u.twidth(x)
+      if nx > ex:
+        break
+      display[dls + x].str = select.options[i].substr(pj, j - 1)
+      display[dls + x].format = format
+      inc x
+      while x < nx:
+        display[dls + x].str = ""
+        display[dls + x].format = format
+        inc x
+    while x < ex:
+      display[dls + x].str = " "
+      display[dls + x].format = format
+      inc x
+
+proc windowChange*(select: 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.queueDraw()
+
+proc newSelect*(multiple: bool; options: seq[string]; selected: seq[int];
+    x, y, height: int; finishImpl: SelectFinish; opaque: RootRef): Select =
+  let select = Select(
+    multiple: multiple,
+    options: options,
+    oselected: selected,
+    selected: selected,
+    x: x,
+    y: y,
+    finishImpl: finishImpl,
+    opaque: opaque
+  )
+  select.windowChange(height)
+  for opt in select.options.mitems:
+    opt.mnormalize()
+    select.maxw = max(select.maxw, opt.width())
+  select.windowChange(height)
+  select.queueDraw()
+  return select
+
+proc addSelectModule*(ctx: JSContext) =
+  ctx.registerType(Select)