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 | |
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.)
-rw-r--r-- | res/ua.css | 17 | ||||
-rw-r--r-- | src/buffer/buffer.nim | 241 | ||||
-rw-r--r-- | src/buffer/container.nim | 197 | ||||
-rw-r--r-- | src/buffer/select.nim | 308 | ||||
-rw-r--r-- | src/display/pager.nim | 66 | ||||
-rw-r--r-- | src/render/renderdocument.nim | 2 | ||||
-rw-r--r-- | src/utils/twtstr.nim | 5 | ||||
-rw-r--r-- | todo | 5 |
8 files changed, 639 insertions, 202 deletions
diff --git a/res/ua.css b/res/ua.css index 2d611737..ea5cc449 100644 --- a/res/ua.css +++ b/res/ua.css @@ -251,6 +251,10 @@ select { display: inline-block; } +select:focus { + visibility: hidden; +} + select::before { content: '['; color: initial; @@ -272,17 +276,16 @@ select > :is(option, optgroup > option):checked { color: red; } -select:focus > :is(option, optgroup > option) { - display: list-item; - color: white; +select[multiple] > :is(option, optgroup > option):first-child { + display: inline-block; } -select:focus > :is(option, optgroup > option):checked { - color: pink; +select[multiple] > :is(option, optgroup > option):not(:checked):first-child::after { + content: '(...)'; } -select:focus > :is(option, optgroup > option):hover { - color: red; +select[multiple] > :is(option, optgroup > option):not(:first-child):checked::before { + content: ','; } center { diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim index 330c7fcf..d9060d4c 100644 --- a/src/buffer/buffer.nim +++ b/src/buffer/buffer.nim @@ -57,7 +57,7 @@ type LOAD, RENDER, WINDOW_CHANGE, FIND_ANCHOR, READ_SUCCESS, READ_CANCELED, CLICK, FIND_NEXT_LINK, FIND_PREV_LINK, FIND_NEXT_MATCH, FIND_PREV_MATCH, GET_SOURCE, GET_LINES, UPDATE_HOVER, PASS_FD, CONNECT, GOTO_ANCHOR, CANCEL, - GET_TITLE + GET_TITLE, SELECT # LOADING_PAGE: istream open # LOADING_RESOURCES: istream closed, resources open @@ -963,106 +963,146 @@ type ReadLineResult* = object hide*: bool area*: bool -type ClickResult* = object - open*: Option[Request] - readline*: Option[ReadLineResult] - repaint*: bool +type + SelectResult* = object + multiple*: bool + options*: seq[string] + selected*: seq[int] + + ClickResult* = object + open*: Option[Request] + readline*: Option[ReadLineResult] + repaint*: bool + select*: Option[SelectResult] + +proc click(buffer: Buffer, clickable: Element): ClickResult + +proc click(buffer: Buffer, label: HTMLLabelElement): ClickResult = + let control = label.control + if control != nil: + return buffer.click(control) + +proc click(buffer: Buffer, select: HTMLSelectElement): ClickResult = + let repaint = buffer.setFocus(select) + var options: seq[string] + var selected: seq[int] + var i = 0 + for option in select.options: + options.add(option.textContent.stripAndCollapse()) + if option.selected: + selected.add(i) + inc i + let select = SelectResult( + multiple: select.attrb("multiple"), + options: options, + selected: selected + ) + return ClickResult( + repaint: repaint, + select: some(select) + ) + +proc click(buffer: Buffer, anchor: HTMLAnchorElement): ClickResult = + let repaint = buffer.restoreFocus() + let url = parseURL(anchor.href, some(anchor.document.baseURL)) + if url.isSome: + return ClickResult( + repaint: repaint, + open: some(newRequest(url.get, HTTP_GET)) + ) + return ClickResult( + repaint: repaint + ) + +proc click(buffer: Buffer, option: HTMLOptionElement): ClickResult = + let select = option.select + if select != nil: + return buffer.click(select) + +proc click(buffer: Buffer, button: HTMLButtonElement): ClickResult = + if button.form != nil: + case button.ctype + of BUTTON_SUBMIT: result.open = submitForm(button.form, button) + of BUTTON_RESET: + button.form.reset() + buffer.do_reshape() + return ClickResult(repaint: true) + of BUTTON_BUTTON: discard + result.repaint = buffer.setFocus(button) + +proc click(buffer: Buffer, textarea: HTMLTextAreaElement): ClickResult = + let readline = ReadLineResult( + value: textarea.value, + area: true + ) + return ClickResult(readline: some(readline)) + +proc click(buffer: Buffer, input: HTMLInputElement): ClickResult = + result.repaint = buffer.restoreFocus() + case input.inputType + of INPUT_SEARCH: + result.repaint = buffer.setFocus(input) + result.readline = some(ReadLineResult( + prompt: "SEARCH: ", + value: input.value + )) + of INPUT_TEXT, INPUT_PASSWORD: + result.repaint = buffer.setFocus(input) + result.readline = some(ReadLineResult( + prompt: "TEXT: ", + value: input.value, + hide: input.inputType == INPUT_PASSWORD + )) + of INPUT_FILE: + result.repaint = buffer.setFocus(input) + var path = if input.file.issome: + input.file.get.path.serialize_unicode() + else: + "" + result.readline = some(ReadLineResult( + prompt: "Filename: ", + value: path + )) + of INPUT_CHECKBOX: + input.checked = not input.checked + input.invalid = true + result.repaint = true + buffer.do_reshape() + of INPUT_RADIO: + for radio in input.radiogroup: + radio.checked = false + radio.invalid = true + input.checked = true + input.invalid = true + result.repaint = true + buffer.do_reshape() + of INPUT_RESET: + if input.form != nil: + input.form.reset() + result.repaint = true + buffer.do_reshape() + of INPUT_SUBMIT, INPUT_BUTTON: + if input.form != nil: + result.open = submitForm(input.form, input) + else: + result.repaint = buffer.restoreFocus() proc click(buffer: Buffer, clickable: Element): ClickResult = case clickable.tagType of TAG_LABEL: - let label = HTMLLabelElement(clickable) - let control = label.control - if control != nil: - return buffer.click(control) + return buffer.click(HTMLLabelElement(clickable)) of TAG_SELECT: - result.repaint = buffer.setFocus(clickable) + return buffer.click(HTMLSelectElement(clickable)) of TAG_A: - result.repaint = buffer.restoreFocus() - let url = parseURL(HTMLAnchorElement(clickable).href, clickable.document.baseURL.some) - if url.issome: - result.open = some(newRequest(url.get, HTTP_GET)) + return buffer.click(HTMLAnchorElement(clickable)) of TAG_OPTION: - let option = HTMLOptionElement(clickable) - let select = option.select - if select != nil: - if buffer.document.focus == select: - # select option - if not select.attrb("multiple"): - for option in select.options: - option.selected = false - option.selected = true - result.repaint = buffer.restoreFocus() - else: - # focus on select - result.repaint = buffer.setFocus(select) + return buffer.click(HTMLOptionElement(clickable)) of TAG_BUTTON: - let button = HTMLButtonElement(clickable) - if button.form != nil: - case button.ctype - of BUTTON_SUBMIT: result.open = submitForm(button.form, button) - of BUTTON_RESET: - button.form.reset() - result.repaint = true - buffer.do_reshape() - of BUTTON_BUTTON: discard + return buffer.click(HTMLButtonElement(clickable)) of TAG_TEXTAREA: - result.repaint = buffer.setFocus(clickable) - let textarea = HTMLTextAreaElement(clickable) - result.readline = some(ReadLineResult( - value: textarea.value, - area: true - )) + return buffer.click(HTMLTextAreaElement(clickable)) of TAG_INPUT: - result.repaint = buffer.restoreFocus() - let input = HTMLInputElement(clickable) - case input.inputType - of INPUT_SEARCH: - result.repaint = buffer.setFocus(input) - result.readline = some(ReadLineResult( - prompt: "SEARCH: ", - value: input.value - )) - of INPUT_TEXT, INPUT_PASSWORD: - result.repaint = buffer.setFocus(input) - result.readline = some(ReadLineResult( - prompt: "TEXT: ", - value: input.value, - hide: input.inputType == INPUT_PASSWORD - )) - of INPUT_FILE: - result.repaint = buffer.setFocus(input) - var path = if input.file.issome: - input.file.get.path.serialize_unicode() - else: - "" - result.readline = some(ReadLineResult( - prompt: "Filename: ", - value: path - )) - of INPUT_CHECKBOX: - input.checked = not input.checked - input.invalid = true - result.repaint = true - buffer.do_reshape() - of INPUT_RADIO: - for radio in input.radiogroup: - radio.checked = false - radio.invalid = true - input.checked = true - input.invalid = true - result.repaint = true - buffer.do_reshape() - of INPUT_RESET: - if input.form != nil: - input.form.reset() - result.repaint = true - buffer.do_reshape() - of INPUT_SUBMIT, INPUT_BUTTON: - if input.form != nil: - result.open = submitForm(input.form, input) - else: - result.repaint = buffer.restoreFocus() + return buffer.click(HTMLInputElement(clickable)) else: result.repaint = buffer.restoreFocus() @@ -1072,6 +1112,25 @@ proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} = if clickable != nil: return buffer.click(clickable) +proc select*(buffer: Buffer, selected: seq[int]): ClickResult {.proxy.} = + if buffer.document.focus != nil and + buffer.document.focus.tagType == TAG_SELECT: + let select = HTMLSelectElement(buffer.document.focus) + var i = 0 + var j = 0 + var repaint = false + for option in select.options: + var wasSelected = option.selected + if i < selected.len and selected[i] == j: + option.selected = true + inc i + else: + option.selected = false + if not repaint: + repaint = wasSelected != option.selected + inc j + return ClickResult(repaint: buffer.restoreFocus()) + proc readCanceled*(buffer: Buffer): bool {.proxy.} = return buffer.restoreFocus() diff --git a/src/buffer/container.nim b/src/buffer/container.nim index 21b11af6..ab5bc6f7 100644 --- a/src/buffer/container.nim +++ b/src/buffer/container.nim @@ -10,6 +10,7 @@ when defined(posix): import buffer/buffer import buffer/cell +import buffer/select import config/config import io/promise import io/request @@ -20,6 +21,7 @@ import ips/socketstream import js/javascript import js/regex import types/buffersource +import types/color import types/cookie import types/dispatcher import types/url @@ -96,6 +98,7 @@ type startpos: Option[CursorPosition] hasstart: bool redirectdepth*: int + select*: Select jsDestructor(Container) @@ -407,16 +410,28 @@ proc setCursorXY(container: Container, x, y: int) {.jsfunc.} = container.centerLine() proc cursorDown(container: Container) {.jsfunc.} = - container.setCursorY(container.cursory + 1) + if container.select.open: + container.select.cursorDown() + else: + container.setCursorY(container.cursory + 1) proc cursorUp(container: Container) {.jsfunc.} = - container.setCursorY(container.cursory - 1) + if container.select.open: + container.select.cursorUp() + else: + container.setCursorY(container.cursory - 1) proc cursorLeft(container: Container) {.jsfunc.} = - container.setCursorX(container.cursorFirstX() - 1) + if container.select.open: + container.select.cursorLeft() + else: + container.setCursorX(container.cursorFirstX() - 1) proc cursorRight(container: Container) {.jsfunc.} = - container.setCursorX(container.cursorLastX() + 1) + if container.select.open: + container.select.cursorRight() + else: + container.setCursorX(container.cursorLastX() + 1) proc cursorLineBegin(container: Container) {.jsfunc.} = container.setCursorX(0) @@ -512,10 +527,16 @@ proc halfPageDown(container: Container) {.jsfunc.} = container.restoreCursorX() proc cursorFirstLine(container: Container) {.jsfunc.} = - container.setCursorY(0) + if container.select.open: + container.select.cursorFirstLine() + else: + container.setCursorY(0) proc cursorLastLine*(container: Container) {.jsfunc.} = - container.setCursorY(container.numLines - 1) + if container.select.open: + container.select.cursorLastLine() + else: + container.setCursorY(container.numLines - 1) proc cursorTop(container: Container) {.jsfunc.} = container.setCursorY(container.fromy) @@ -592,14 +613,20 @@ proc gotoLine*[T: string|int](container: Container, s: T) = container.setCursorY(s) proc pushCursorPos*(container: Container) = - container.bpos.add(container.pos) + if container.select.open: + container.select.pushCursorPos() + else: + container.bpos.add(container.pos) proc popCursorPos*(container: Container, nojump = false) = - container.pos = container.bpos.pop() - if not nojump: - container.updateCursor() - container.sendCursorPosition() - container.needslines = true + if container.select.open: + container.select.popCursorPos(nojump) + else: + container.pos = container.bpos.pop() + if not nojump: + container.updateCursor() + container.sendCursorPosition() + container.needslines = true proc copyCursorPos*(container, c2: Container) = container.startpos = some(c2.pos) @@ -629,7 +656,7 @@ proc onMatch(container: Container, res: BufferMatch) = container.setCursorXY(res.x, res.y) if container.hlon: container.clearSearchHighlights() - let ex = res.str.twidth(res.x) - 1 + let ex = res.x + res.str.twidth(res.x) - 1 let hl = Highlight(x: res.x, y: res.y, endx: ex, endy: res.y, clear: true) container.highlights.add(hl) container.triggerEvent(UPDATE) @@ -641,16 +668,22 @@ proc onMatch(container: Container, res: BufferMatch) = container.hlon = false proc cursorNextMatch*(container: Container, regex: Regex, wrap: bool) = - container.iface - .findNextMatch(regex, container.cursorx, container.cursory, wrap) - .then(proc(res: BufferMatch) = - container.onMatch(res)) + if container.select.open: + container.select.cursorNextMatch(regex, wrap) + else: + container.iface + .findNextMatch(regex, container.cursorx, container.cursory, wrap) + .then(proc(res: BufferMatch) = + container.onMatch(res)) proc cursorPrevMatch*(container: Container, regex: Regex, wrap: bool) = - container.iface - .findPrevMatch(regex, container.cursorx, container.cursory, wrap) - .then(proc(res: BufferMatch) = - container.onMatch(res)) + if container.select.open: + container.select.cursorPrevMatch(regex, wrap) + else: + container.iface + .findPrevMatch(regex, container.cursorx, container.cursory, wrap) + .then(proc(res: BufferMatch) = + container.onMatch(res)) proc setLoadInfo(container: Container, msg: string) = container.loadinfo = msg @@ -723,8 +756,11 @@ proc load(container: Container) = container.onload(res)) proc cancel*(container: Container) {.jsfunc.} = - container.canceled = true - container.alert("Canceled loading") + if container.select.open: + container.select.cancel() + else: + container.canceled = true + container.alert("Canceled loading") proc findAnchor*(container: Container, anchor: string) = container.iface.findAnchor(anchor).then(proc(found: bool) = @@ -764,28 +800,45 @@ proc dupeBuffer*(dispatcher: Dispatcher, container: Container, config: Config, l container.pipeto = nil) return container.pipeto +proc onclick(container: Container, res: ClickResult) + +proc displaySelect(container: Container, selectResult: SelectResult) = + let submitSelect = proc(selected: seq[int]) = + container.iface.select(selected).then(proc(res: ClickResult) = + container.onclick(res)) + container.select.initSelect(selectResult, container.acursorx, + container.acursory, container.height, submitSelect) + container.triggerEvent(UPDATE) + +proc onclick(container: Container, res: ClickResult) = + if res.repaint: + container.needslines = true + if res.open.isSome: + container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get)) + if res.select.isSome: + container.displaySelect(res.select.get) + if res.readline.isSome: + let rl = res.readline.get + let event = if rl.area: + ContainerEvent( + t: READ_AREA, + tvalue: rl.value + ) + else: + ContainerEvent( + t: READ_LINE, + prompt: rl.prompt, + value: rl.value, + password: rl.hide + ) + container.triggerEvent(event) + proc click(container: Container) {.jsfunc.} = - container.iface.click(container.cursorx, container.cursory).then(proc(res: ClickResult) = - if res.repaint: - container.needslines = true - if res.open.isSome: - container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get)) - if res.readline.isSome: - let rl = res.readline.get - if rl.area: - container.triggerEvent( - ContainerEvent( - t: READ_AREA, - tvalue: rl.value - )) - else: - container.triggerEvent( - ContainerEvent( - t: READ_LINE, - prompt: rl.prompt, - value: rl.value, - password: rl.hide - ))) + if container.select.open: + container.select.click() + else: + container.iface.click(container.cursorx, container.cursory) + .then(proc(res: ClickResult) = container.onclick(res)) proc windowChange*(container: Container, attrs: WindowAttributes) = container.attrs = attrs @@ -861,6 +914,62 @@ proc readLines*(container: Container, handle: (proc(line: SimpleFlexibleLine))) # fulfill all promises container.handleCommand() +proc drawLines*(container: Container, display: var FixedGrid, + hlcolor: CellColor) = + var r: Rune + var by = 0 + let endy = min(container.fromy + display.height, container.numLines) + for line in container.ilines(container.fromy ..< endy): + var w = 0 # width of the row so far + var i = 0 # byte in line.str + # Skip cells till fromx. + while w < container.fromx and i < line.str.len: + fastRuneAt(line.str, i, r) + w += r.twidth(w) + let dls = by * display.width # starting position of row in display + # Fill in the gap in case we skipped more cells than fromx mandates (i.e. + # we encountered a double-width character.) + var k = 0 + if w > container.fromx: + while k < w - container.fromx: + display[dls + k].str &= ' ' + inc k + var cf = line.findFormat(w) + var nf = line.findNextFormat(w) + let startw = w # save this for later + # Now fill in the visible part of the row. + while i < line.str.len: + let pw = w + fastRuneAt(line.str, i, r) + let rw = r.twidth(w) + w += rw + if w > container.fromx + display.width: + break # die on exceeding the width limit + if nf.pos != -1 and nf.pos <= pw: + cf = nf + nf = line.findNextFormat(pw) + if cf.pos != -1: + display[dls + k].format = cf.format + if r == Rune('\t'): + # Needs to be replaced with spaces, otherwise bgcolor isn't displayed. + let tk = k + rw + while k < tk: + display[dls + k].str &= ' ' + inc k + else: + display[dls + k].str &= r + k += rw + # Finally, override cell formatting for highlighted cells. + let hls = container.findHighlights(container.fromy + by) + let aw = container.width - (startw - container.fromx) # actual width + for hl in hls: + let area = hl.colorArea(container.fromy + by, startw .. startw + aw) + for i in area: + var hlformat = display[dls + i - startw].format + hlformat.bgcolor = hlcolor + display[dls + i - startw].format = hlformat + inc by + proc handleEvent*(container: Container) = container.handleCommand() if container.needslines: 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 diff --git a/src/display/pager.nim b/src/display/pager.nim index 2698550c..0bbdd98f 100644 --- a/src/display/pager.nim +++ b/src/display/pager.nim @@ -11,6 +11,7 @@ when defined(posix): import buffer/cell import buffer/container +import buffer/select import config/config import data/charset import display/term @@ -205,59 +206,9 @@ proc clearDisplay(pager: Pager) = proc buffer(pager: Pager): Container {.jsfget, inline.} = pager.container proc refreshDisplay(pager: Pager, container = pager.container) = - var r: Rune - var by = 0 pager.clearDisplay() - for line in container.ilines(container.fromy ..< min(container.fromy + pager.display.height, container.numLines)): - var w = 0 # width of the row so far - var i = 0 # byte in line.str - # Skip cells till fromx. - while w < container.fromx and i < line.str.len: - fastRuneAt(line.str, i, r) - w += r.twidth(w) - let dls = by * pager.display.width # starting position of row in display - # Fill in the gap in case we skipped more cells than fromx mandates (i.e. - # we encountered a double-width character.) - var k = 0 - if w > container.fromx: - while k < w - container.fromx: - pager.display[dls + k].str &= ' ' - inc k - var cf = line.findFormat(w) - var nf = line.findNextFormat(w) - let startw = w # save this for later - # Now fill in the visible part of the row. - while i < line.str.len: - let pw = w - fastRuneAt(line.str, i, r) - let rw = r.twidth(w) - w += rw - if w > container.fromx + pager.display.width: - break # die on exceeding the width limit - if nf.pos != -1 and nf.pos <= pw: - cf = nf - nf = line.findNextFormat(pw) - if cf.pos != -1: - pager.display[dls + k].format = cf.format - if r == Rune('\t'): - # Needs to be replaced with spaces, otherwise bgcolor isn't displayed. - let tk = k + rw - while k < tk: - pager.display[dls + k].str &= ' ' - inc k - else: - pager.display[dls + k].str &= r - k += rw - # Finally, override cell formatting for highlighted cells. - let hls = container.findHighlights(container.fromy + by) - let aw = container.width - (startw - container.fromx) # actual width - for hl in hls: - let area = hl.colorArea(container.fromy + by, startw .. startw + aw) - for i in area: - var hlformat = pager.display[dls + i - startw].format - hlformat.bgcolor = pager.config.display.highlight_color.cellColor() - pager.display[dls + i - startw].format = hlformat - inc by + container.drawLines(pager.display, + cellColor(pager.config.display.highlight_color)) # Note: this function doesn't work if start < i of last written char proc writeStatusMessage(pager: Pager, str: string, @@ -356,11 +307,15 @@ proc redraw(pager: Pager) {.jsfunc.} = pager.term.clearCanvas() proc draw*(pager: Pager) = - if pager.container == nil: return + let container = pager.container + if container == nil: return pager.term.hideCursor() if pager.redraw: pager.refreshDisplay() pager.term.writeGrid(pager.display) + if container.select.open and container.select.redraw: + container.select.drawSelect(pager.display) + pager.term.writeGrid(pager.display) if pager.askpromise != nil: discard elif pager.lineedit.isSome: @@ -383,6 +338,9 @@ proc draw*(pager: Pager) = pager.lineedit.get.fullRedraw() pager.lineedit.get.isnew = false pager.term.setCursor(pager.lineedit.get.getCursorX(), pager.attrs.height - 1) + elif container.select.open: + pager.term.setCursor(container.select.getCursorX(), + container.select.getCursorY()) else: pager.term.setCursor(pager.container.acursorx, pager.container.acursory) pager.term.showCursor() @@ -735,13 +693,13 @@ proc updateReadLineISearch(pager: Pager, linemode: LineMode) = let x = $lineedit.news if x != "": pager.iregex = compileSearchRegex(x) pager.container.popCursorPos(true) + pager.container.pushCursorPos() if pager.iregex.isSome: pager.container.hlon = true if linemode == ISEARCH_F: pager.container.cursorNextMatch(pager.iregex.get, pager.config.search.wrap) else: pager.container.cursorPrevMatch(pager.iregex.get, pager.config.search.wrap) - pager.container.pushCursorPos() of FINISH: pager.regex = pager.checkRegex(pager.iregex) pager.reverseSearch = linemode == ISEARCH_B diff --git a/src/render/renderdocument.nim b/src/render/renderdocument.nim index afde059b..6f8b2a80 100644 --- a/src/render/renderdocument.nim +++ b/src/render/renderdocument.nim @@ -59,7 +59,7 @@ proc setText(lines: var FlexibleGrid, linestr: string, cformat: ComputedFormat, lines[y].str &= ' '.repeat(padwidth) lines[y].str &= linestr - let linestrwidth = linestr.twidth(x) - x + let linestrwidth = linestr.twidth(x) i = 0 var nx = x # last x of new string diff --git a/src/utils/twtstr.nim b/src/utils/twtstr.nim index f735252c..60b85cbc 100644 --- a/src/utils/twtstr.nim +++ b/src/utils/twtstr.nim @@ -1068,9 +1068,10 @@ func width*(s: seq[Rune], min: int): int = inc i func twidth*(s: string, w: int): int = - result = w + var i = 0 for r in s.runes(): - result += r.twidth(result) + i += r.twidth(result) + return i func breaksWord*(r: Rune): bool = return not (r.isDigitAscii() or r.width() == 0 or r.isAlpha()) diff --git a/todo b/todo index c23e689c..c4e6e260 100644 --- a/todo +++ b/todo @@ -74,6 +74,7 @@ javascript: * alternative idea: separate console for each buffer - implement JS access to non-ref object members (with a ptr opaque + GC_ref on parent object or something) +- buffer selection layout engine: - important: floats (way too many websites look very ugly without them) - make background-color a property of inline boxes, not words. (this would @@ -83,9 +84,7 @@ layout engine: - overflow - incremental layout & layout caching * first for tree generation, then for layout. -- more replaced elements: iframe, select - * for select we should have a built-in dropdown list, like w3m. then - we could use that for buffer selection too +- iframe - writing-mode, flexbox, grid, ruby, ... (i.e. cool new stuff) images: - sixel encoding (eventually also kitty) |