diff options
author | bptato <nincsnevem662@gmail.com> | 2023-07-05 17:54:22 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2023-07-05 18:01:00 +0200 |
commit | 4511c956bf62983f1d3f0252805fadaac040123e (patch) | |
tree | 9a04a74fda5ff70316948c775c7e133d5112b426 /src/buffer/select.nim | |
parent | 76cb8dee5e2066a7409f53edf74b2d3275ff4a7e (diff) | |
download | chawan-4511c956bf62983f1d3f0252805fadaac040123e.tar.gz |
Add popup menu for select element
Replaces the weird CSS implementation we have had until now with a searchable popup menu similar to that of w3m. (The previous implementation broke on websites that do not expect <select> to expand on click, had no separate search, and was ugly.)
Diffstat (limited to 'src/buffer/select.nim')
-rw-r--r-- | src/buffer/select.nim | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/src/buffer/select.nim b/src/buffer/select.nim new file mode 100644 index 00000000..1b1993c5 --- /dev/null +++ b/src/buffer/select.nim @@ -0,0 +1,308 @@ +import unicode + +import buffer/buffer +import buffer/cell +import js/regex +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) = + let was = select.hover + let got = select.bpos.pop() + select.hover = got + 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 |