diff options
author | bptato <nincsnevem662@gmail.com> | 2024-06-22 17:16:44 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-06-22 17:16:44 +0200 |
commit | aa3e08c3ea432b8d9a9558c1609ebae1ab75cc1e (patch) | |
tree | 9172d7ec24c1cf7d817a40f9b23da18f2874bda3 | |
parent | ca295c18cda7bbdffb42e65729f4dd969fe16d69 (diff) | |
download | chawan-aa3e08c3ea432b8d9a9558c1609ebae1ab75cc1e.tar.gz |
pager: refactor drawing code
* merge select into container * avoid unnecessary redraws in draw() for parts of the screen that haven't been updated * various image redraw fixes
-rw-r--r-- | src/local/client.nim | 26 | ||||
-rw-r--r-- | src/local/container.nim | 394 | ||||
-rw-r--r-- | src/local/lineedit.nim | 34 | ||||
-rw-r--r-- | src/local/pager.nim | 204 | ||||
-rw-r--r-- | src/local/select.nim | 307 | ||||
-rw-r--r-- | src/local/term.nim | 74 | ||||
-rw-r--r-- | todo | 1 |
7 files changed, 536 insertions, 504 deletions
diff --git a/src/local/client.nim b/src/local/client.nim index c52ea71f..78213b6e 100644 --- a/src/local/client.nim +++ b/src/local/client.nim @@ -7,8 +7,9 @@ import std/posix import std/selectors import std/strutils import std/tables -import std/unicode +import chagashi/charset +import chagashi/decoder import config/config import html/catom import html/chadombuilder @@ -55,8 +56,6 @@ import types/opt import types/url import utils/twtstr -import chagashi/charset - type Client* = ref object of Window alive: bool @@ -183,11 +182,6 @@ proc feedNext(client: Client) {.jsfunc.} = proc alert(client: Client; msg: string) {.jsfunc.} = client.pager.alert(msg) -proc handlePagerEvents(client: Client) = - let container = client.pager.container - if container != nil: - client.pager.handleEvents(container) - proc evalActionJS(client: Client; action: string): JSValue = client.config.cmd.map.withValue(action, p): return JS_DupValue(client.jsctx, p[]) @@ -244,7 +238,7 @@ proc handleCommandInput(client: Client; c: char): EmptyPromise = if not client.feednext: client.pager.precnum = 0 client.pager.notnum = false - client.handlePagerEvents() + client.pager.handleEvents() return p if client.config.input.use_mouse: if client.pager.inputBuffer == "\e[<": @@ -321,12 +315,12 @@ proc input(client: Client): EmptyPromise = client.pager.fulfillAsk(false) elif client.pager.askcharpromise != nil: buf &= c - if buf.validateUtf8() != -1: + if buf.validateUtf8Surr() != -1: continue client.pager.fulfillCharAsk(buf) - elif client.pager.lineedit.isSome: + elif client.pager.lineedit != nil: client.pager.inputBuffer &= c - let edit = client.pager.lineedit.get + let edit = client.pager.lineedit if edit.escNext: edit.escNext = false if edit.write(client.pager.inputBuffer, client.pager.term.cs): @@ -469,7 +463,7 @@ proc acceptBuffers(client: Client) = proc handleRead(client: Client; fd: int) = if client.pager.term.istream != nil and fd == client.pager.term.istream.fd: client.input().then(proc() = - client.handlePagerEvents() + client.pager.handleEvents() ) elif (let i = client.pager.findConnectingContainer(fd); i != -1): client.pager.handleConnectingContainer(i) @@ -589,8 +583,8 @@ proc inputLoop(client: Client) = if client.pager.scommand != "": client.command(client.pager.scommand) client.pager.scommand = "" - client.handlePagerEvents() - if client.pager.container == nil and client.pager.lineedit.isNone: + client.pager.handleEvents() + if client.pager.container == nil and client.pager.lineedit == nil: # No buffer to display. if not client.pager.hasload: # Failed to load every single URL the user passed us. We quit, and that @@ -800,7 +794,7 @@ proc btoa(ctx: JSContext; client: Client; data: JSValue): DOMResult[string] return btoa(ctx, data) func line(client: Client): LineEdit {.jsfget.} = - return client.pager.lineedit.get(nil) + return client.pager.lineedit proc addJSModules(client: Client; ctx: JSContext) = ctx.addWindowModule2() diff --git a/src/local/container.nim b/src/local/container.nim index 52498047..1fddb490 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -17,7 +17,6 @@ import layout/renderdocument import loader/headers import loader/loader import loader/request -import local/select import monoucha/javascript import monoucha/jsregex import monoucha/jstypes @@ -49,8 +48,8 @@ type setxsave: bool ContainerEventType* = enum - cetAnchor, cetNoAnchor, cetUpdate, cetReadLine, cetReadArea, cetReadFile, - cetOpen, cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle, cetCancel + cetAnchor, cetNoAnchor, cetReadLine, cetReadArea, cetReadFile, cetOpen, + cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle, cetCancel ContainerEvent* = object case t*: ContainerEventType @@ -68,8 +67,6 @@ type anchor*: string of cetAlert: msg*: string - of cetUpdate: - force*: bool else: discard HighlightType = enum @@ -163,10 +160,300 @@ type images*: seq[PosBitmap] cachedImages*: seq[CachedImage] 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) +# Forward declarations +proc onclick(container: Container; res: ClickResult; save: bool) +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 = $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 = 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 r: Rune + 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: + 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 + proc newContainer*(config: BufferConfig; loaderConfig: LoaderClientConfig; url: URL; request: Request; luctx: LUContext; attrs: WindowAttributes; title: string; redirectDepth: int; flags: set[ContainerFlag]; @@ -190,7 +477,8 @@ proc newContainer*(config: BufferConfig; loaderConfig: LoaderClientConfig; process: -1, mainConfig: mainConfig, flags: flags, - luctx: luctx + luctx: luctx, + redraw: true ) func location(container: Container): URL {.jsfget.} = @@ -380,7 +668,10 @@ func startx(hl: Highlight): int = hl.x2 else: min(hl.x1, hl.x2) -func starty(hl: Highlight): int = min(hl.y1, hl.y2) + +func starty(hl: Highlight): int = + return min(hl.y1, hl.y2) + func endx(hl: Highlight): int = if hl.y1 > hl.y2: hl.x1 @@ -388,7 +679,9 @@ func endx(hl: Highlight): int = hl.x2 else: max(hl.x1, hl.x2) -func endy(hl: Highlight): int = max(hl.y1, hl.y2) + +func endy(hl: Highlight): int = + return max(hl.y1, hl.y2) func colorNormal(container: Container; hl: Highlight; y: int; limitx: Slice[int]): Slice[int] = @@ -448,8 +741,6 @@ proc triggerEvent(container: Container; event: ContainerEvent) = proc triggerEvent(container: Container; t: ContainerEventType) = container.triggerEvent(ContainerEvent(t: t)) -proc updateCursor(container: Container) - proc setNumLines(container: Container; lines: int; finish = false) = if container.numLines != lines: container.numLines = lines @@ -458,7 +749,8 @@ proc setNumLines(container: Container; lines: int; finish = false) = container.startpos = none(CursorPosition) container.updateCursor() -proc cursorLastLine*(container: Container) +proc queueDraw*(container: Container) = + container.redraw = true proc requestLines(container: Container): EmptyPromise {.discardable.} = if container.iface == nil: @@ -483,13 +775,10 @@ proc requestLines(container: Container): EmptyPromise {.discardable.} = container.cursorLastLine() let cw = container.fromy ..< container.fromy + container.height if w.a in cw or w.b in cw or cw.a in w or cw.b in w or isBgNew: - container.triggerEvent(cetUpdate) + container.queueDraw() container.images = res.images ) -proc redraw(container: Container) {.jsfunc.} = - container.triggerEvent(ContainerEvent(t: cetUpdate, force: true)) - proc sendCursorPosition*(container: Container) = if container.iface == nil: return @@ -508,7 +797,7 @@ proc setFromY(container: Container; y: int) {.jsfunc.} = if container.pos.fromy != y: container.pos.fromy = max(min(y, container.maxfromy), 0) container.needslines = true - container.triggerEvent(cetUpdate) + container.queueDraw() proc setFromX(container: Container; x: int; refresh = true) {.jsfunc.} = if container.pos.fromx != x: @@ -518,7 +807,7 @@ proc setFromX(container: Container; x: int; refresh = true) {.jsfunc.} = container.currentLineWidth()) if refresh: container.sendCursorPosition() - container.triggerEvent(cetUpdate) + container.queueDraw() proc setFromXY(container: Container; x, y: int) {.jsfunc.} = container.setFromY(y) @@ -564,7 +853,7 @@ proc setCursorX(container: Container; x: int; refresh = true; save = true) if container.cursorx == x and container.currentSelection != nil and container.currentSelection.x2 != x: container.currentSelection.x2 = x - container.triggerEvent(cetUpdate) + container.queueDraw() if refresh: container.sendCursorPosition() if save: @@ -586,7 +875,7 @@ proc setCursorY(container: Container; y: int; refresh = true) {.jsfunc.} = container.setFromY(y) container.pos.cursory = y if container.currentSelection != nil and container.currentSelection.y2 != y: - container.triggerEvent(cetUpdate) + container.queueDraw() container.currentSelection.y2 = y container.restoreCursorX() if refresh: @@ -679,25 +968,25 @@ proc setCursorXYCenter(container: Container; x, y: int; refresh = true) container.centerColumn() proc cursorDown(container: Container; n = 1) {.jsfunc.} = - if container.select.open: + if container.select != nil: container.select.cursorDown() else: container.setCursorY(container.cursory + n) proc cursorUp(container: Container; n = 1) {.jsfunc.} = - if container.select.open: + if container.select != nil: container.select.cursorUp() else: container.setCursorY(container.cursory - n) proc cursorLeft(container: Container; n = 1) {.jsfunc.} = - if container.select.open: + if container.select != nil: container.select.cursorLeft() else: container.setCursorX(container.cursorFirstX() - n) proc cursorRight(container: Container; n = 1) {.jsfunc.} = - if container.select.open: + if container.select != nil: container.select.cursorRight() else: container.setCursorX(container.cursorLastX() + n) @@ -947,7 +1236,7 @@ proc markPos*(container: Container) = container.jumpMark = pos proc cursorFirstLine(container: Container) {.jsfunc.} = - if container.select.open: + if container.select != nil: container.select.cursorFirstLine() else: container.markPos0() @@ -955,7 +1244,7 @@ proc cursorFirstLine(container: Container) {.jsfunc.} = container.markPos() proc cursorLastLine*(container: Container) {.jsfunc.} = - if container.select.open: + if container.select != nil: container.select.cursorLastLine() else: container.markPos0() @@ -1041,7 +1330,7 @@ proc updateCursor(container: Container) = proc gotoLine*[T: string|int](container: Container; s: T) = when s is string: if s == "": - redraw(container) + container.redraw = true elif s[0] == '^': container.cursorFirstLine() elif s[0] == '$': @@ -1060,13 +1349,13 @@ proc gotoLine*[T: string|int](container: Container; s: T) = container.markPos() proc pushCursorPos*(container: Container) = - if container.select.open: + if container.select != nil: container.select.pushCursorPos() else: container.bpos.add(container.pos) proc popCursorPos*(container: Container; nojump = false) = - if container.select.open: + if container.select != nil: container.select.popCursorPos(nojump) else: container.pos = container.bpos.pop() @@ -1131,17 +1420,17 @@ proc setMark*(container: Container; id: string; x = none(int); let y = y.get(container.cursory) container.marks.withValue(id, p): p[] = (x, y) - container.triggerEvent(cetUpdate) + container.queueDraw() return false do: container.marks[id] = (x, y) - container.triggerEvent(cetUpdate) + container.queueDraw() return true proc clearMark*(container: Container; id: string): bool {.jsfunc.} = result = id in container.marks container.marks.del(id) - container.triggerEvent(cetUpdate) + container.queueDraw() proc getMarkPos(container: Container; id: string): Opt[PagePos] {.jsfunc.} = if id == "`" or id == "'": @@ -1237,18 +1526,18 @@ proc onMatch(container: Container; res: BufferMatch; refresh: bool) = y2: res.y ) container.highlights.add(hl) - container.triggerEvent(cetUpdate) + container.queueDraw() container.hlon = false container.needslines = true elif container.hlon: container.clearSearchHighlights() - container.triggerEvent(cetUpdate) + container.queueDraw() container.needslines = true container.hlon = false proc cursorNextMatch*(container: Container; regex: Regex; wrap, refresh: bool; n: int): EmptyPromise {.discardable.} = - if container.select.open: + if container.select != nil: #TODO for _ in 0 ..< n: container.select.cursorNextMatch(regex, wrap) @@ -1263,7 +1552,7 @@ proc cursorNextMatch*(container: Container; regex: Regex; wrap, refresh: bool; proc cursorPrevMatch*(container: Container; regex: Regex; wrap, refresh: bool; n: int): EmptyPromise {.discardable.} = - if container.select.open: + if container.select != nil: #TODO for _ in 0 ..< n: container.select.cursorPrevMatch(regex, wrap) @@ -1304,7 +1593,7 @@ proc cursorToggleSelection(container: Container; n = 1; ) container.highlights.add(hl) container.currentSelection = hl - container.triggerEvent(cetUpdate) + container.queueDraw() return container.currentSelection #TODO I don't like this API @@ -1481,7 +1770,7 @@ proc remoteCancel*(container: Container) = container.alert("Canceled loading") proc cancel*(container: Container) {.jsfunc.} = - if container.select.open: + if container.select != nil: container.select.cancel() elif container.loadState == lsLoading: container.loadState = lsCanceled @@ -1522,15 +1811,21 @@ proc reshape(container: Container): EmptyPromise {.jsfunc.} = return container.requestLines() ) -proc onclick(container: Container; res: ClickResult; save: bool) - proc displaySelect(container: Container; selectResult: SelectResult) = - let submitSelect = proc(selected: seq[int]) = - container.iface.select(selected).then(proc(res: ClickResult) = - container.onclick(res, save = false)) - container.select.initSelect(selectResult, container.acursorx, - container.acursory, container.height, submitSelect) - container.triggerEvent(cetUpdate) + container.select = Select( + container: container, + multiple: selectResult.multiple, + options: selectResult.options, + oselected: selectResult.selected, + selected: selectResult.selected, + x: container.acursorx, + y: container.acursory + ) + for opt in container.select.options.mitems: + opt.mnormalize() + container.select.maxw = max(container.select.maxw, opt.width()) + container.select.windowChange(container.height) + container.queueDraw() proc onclick(container: Container; res: ClickResult; save: bool) = if res.repaint: @@ -1547,7 +1842,7 @@ proc onclick(container: Container; res: ClickResult; save: bool) = container.onReadLine(res.readline.get) proc click*(container: Container) {.jsfunc.} = - if container.select.open: + if container.select != nil: container.select.click() else: if container.iface == nil: @@ -1663,8 +1958,7 @@ proc readLines*(container: Container; handle: proc(line: SimpleFlexibleLine)) = # fulfill all promises container.handleCommand() -proc drawLines*(container: Container; display: var FixedGrid; - hlcolor: CellColor) = +proc drawLines*(container: Container; display: var FixedGrid; hlcolor: CellColor) = let bgcolor = container.bgcolor template set_fmt(cell, cf: typed) = if cf.pos != -1: @@ -1722,12 +2016,12 @@ proc drawLines*(container: Container; display: var FixedGrid; inc k # Finally, override cell formatting for highlighted cells. let hls = container.findHighlights(container.fromy + by) - let aw = container.width - (startw - container.fromx) # actual width + let aw = display.width - (startw - container.fromx) # actual width for hl in hls: let area = container.colorArea(hl, container.fromy + by, startw .. startw + aw) for i in area: - if i - startw >= container.width: + if i - startw >= display.width: break var hlformat = display[dls + i - startw].format hlformat.bgcolor = hlcolor diff --git a/src/local/lineedit.nim b/src/local/lineedit.nim index ba02e2ae..15cbdc54 100644 --- a/src/local/lineedit.nim +++ b/src/local/lineedit.nim @@ -38,7 +38,7 @@ type hist: LineHistory histindex: int histtmp: string - invalid*: bool + redraw*: bool jsDestructor(LineEdit) @@ -135,7 +135,7 @@ proc insertCharseq(edit: LineEdit; s: string) = edit.news &= rem edit.cursori += s.len edit.cursorx += s.notwidth() - edit.invalid = true + edit.redraw = true proc cancel(edit: LineEdit) {.jsfunc.} = edit.state = lesCancel @@ -151,7 +151,7 @@ proc backspace(edit: LineEdit) {.jsfunc.} = edit.news.delete(edit.cursori - len .. edit.cursori - 1) edit.cursori -= len edit.cursorx -= r.width() - edit.invalid = true + edit.redraw = true proc write*(edit: LineEdit; s: string; cs: Charset): bool = if cs == CHARSET_UTF_8: @@ -177,7 +177,7 @@ proc delete(edit: LineEdit) {.jsfunc.} = if edit.cursori < edit.news.len: let len = edit.news.runeLenAt(edit.cursori) edit.news.delete(edit.cursori ..< edit.cursori + len) - edit.invalid = true + edit.redraw = true proc escape(edit: LineEdit) {.jsfunc.} = edit.escNext = true @@ -187,12 +187,12 @@ proc clear(edit: LineEdit) {.jsfunc.} = edit.news.delete(0..edit.cursori - 1) edit.cursori = 0 edit.cursorx = 0 - edit.invalid = true + edit.redraw = true proc kill(edit: LineEdit) {.jsfunc.} = if edit.cursori < edit.news.len: edit.news.setLen(edit.cursori) - edit.invalid = true + edit.redraw = true proc backward(edit: LineEdit) {.jsfunc.} = if edit.cursori > 0: @@ -200,7 +200,7 @@ proc backward(edit: LineEdit) {.jsfunc.} = edit.cursori -= len edit.cursorx -= r.width() if edit.cursorx < edit.shiftx: - edit.invalid = true + edit.redraw = true proc forward(edit: LineEdit) {.jsfunc.} = if edit.cursori < edit.news.len: @@ -208,7 +208,7 @@ proc forward(edit: LineEdit) {.jsfunc.} = fastRuneAt(edit.news, edit.cursori, r) edit.cursorx += r.width() if edit.cursorx >= edit.shiftx + edit.maxwidth: - edit.invalid = true + edit.redraw = true proc prevWord(edit: LineEdit) {.jsfunc.} = if edit.cursori == 0: @@ -225,7 +225,7 @@ proc prevWord(edit: LineEdit) {.jsfunc.} = edit.cursori -= len edit.cursorx -= r.width() if edit.cursorx < edit.shiftx: - edit.invalid = true + edit.redraw = true proc nextWord(edit: LineEdit) {.jsfunc.} = if edit.cursori >= edit.news.len: @@ -246,14 +246,14 @@ proc nextWord(edit: LineEdit) {.jsfunc.} = break edit.cursorx += r.width() if edit.cursorx >= edit.shiftx + edit.maxwidth: - edit.invalid = true + edit.redraw = true proc clearWord(edit: LineEdit) {.jsfunc.} = let oc = edit.cursori edit.prevWord() if oc != edit.cursori: edit.news.delete(edit.cursori .. oc - 1) - edit.invalid = true + edit.redraw = true proc killWord(edit: LineEdit) {.jsfunc.} = if edit.cursori >= edit.news.len: @@ -269,20 +269,20 @@ proc killWord(edit: LineEdit) {.jsfunc.} = edit.news.delete(oc ..< edit.cursori) edit.cursori = oc edit.cursorx = ox - edit.invalid = true + edit.redraw = true proc begin(edit: LineEdit) {.jsfunc.} = edit.cursori = 0 edit.cursorx = 0 if edit.shiftx > 0: - edit.invalid = true + edit.redraw = true proc `end`(edit: LineEdit) {.jsfunc.} = if edit.cursori < edit.news.len: edit.cursori = edit.news.len edit.cursorx = edit.news.notwidth() if edit.cursorx >= edit.shiftx + edit.maxwidth: - edit.invalid = true + edit.redraw = true proc prevHist(edit: LineEdit) {.jsfunc.} = if edit.histindex > 0: @@ -294,7 +294,7 @@ proc prevHist(edit: LineEdit) {.jsfunc.} = # the string. edit.begin() edit.end() - edit.invalid = true + edit.redraw = true proc nextHist(edit: LineEdit) {.jsfunc.} = if edit.histindex + 1 < edit.hist.lines.len: @@ -302,7 +302,7 @@ proc nextHist(edit: LineEdit) {.jsfunc.} = edit.news = edit.hist.lines[edit.histindex] edit.begin() edit.end() - edit.invalid = true + edit.redraw = true elif edit.histindex < edit.hist.lines.len: inc edit.histindex edit.news = edit.histtmp @@ -322,7 +322,7 @@ proc readLine*(prompt, current: string; termwidth: int; disallowed: set[char]; news: current, disallowed: disallowed, hide: hide, - invalid: true, + redraw: true, cursori: current.len, cursorx: current.notwidth(), # - 1, so that the cursor always has place diff --git a/src/local/pager.nim b/src/local/pager.nim index 073b941c..411ef1b1 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -26,7 +26,6 @@ import loader/loader import loader/request import local/container import local/lineedit -import local/select import local/term import monoucha/fromjs import monoucha/javascript @@ -109,6 +108,10 @@ type ndFirstChild ndAny = "any" + Surface = object + redraw: bool + grid: FixedGrid + Pager* = ref object alertState: PagerAlertState alerts*: seq[string] @@ -122,7 +125,7 @@ type container*: Container cookiejars: Table[string, CookieJar] devRandom: PosixStream - display: FixedGrid + display: Surface forkserver*: ForkServer formRequestMap*: Table[string, FormRequestType] hasload*: bool # has a page been successfully loaded since startup? @@ -132,7 +135,7 @@ type isearchpromise: EmptyPromise jsctx: JSContext lineData: LineData - lineedit*: Option[LineEdit] + lineedit*: LineEdit linehist: array[LineMode, LineHistory] linemode: LineMode loader*: FileLoader @@ -142,12 +145,11 @@ type numload*: int # number of pages currently being loaded precnum*: int32 # current number prefix (when vi-numeric-prefix is true) procmap*: seq[ProcMapItem] - redraw: bool regex: Opt[Regex] reverseSearch: bool scommand*: string selector*: Selector[int] - statusgrid*: FixedGrid + status: Surface term*: Terminal unreg*: seq[Container] @@ -164,9 +166,16 @@ func loaderPid(pager: Pager): int64 {.jsfget.} = func getRoot(container: Container): Container = var c = container - while c.parent != nil: c = c.parent + while c.parent != nil: + c = c.parent return c +func bufWidth(pager: Pager): int = + return pager.attrs.width + +func bufHeight(pager: Pager): int = + return pager.attrs.height - 1 + # depth-first descendant iterator iterator descendants(parent: Container): Container {.inline.} = var stack = newSeqOfCap[Container](parent.children.len) @@ -186,10 +195,22 @@ iterator containers*(pager: Pager): Container {.inline.} = for c in root.descendants: yield c +proc clearDisplay(pager: Pager) = + pager.display = Surface( + grid: newFixedGrid(pager.bufWidth, pager.bufHeight), + redraw: true + ) + +proc clearStatus(pager: Pager) = + pager.status = Surface( + grid: newFixedGrid(pager.attrs.width), + redraw: true + ) + proc setContainer*(pager: Pager; c: Container) {.jsfunc.} = pager.container = c - pager.redraw = true if c != nil: + c.queueDraw() pager.term.setTitle(c.getTitle()) proc hasprop(ctx: JSContext; pager: Pager; s: string): bool {.jshasprop.} = @@ -259,13 +280,12 @@ proc setLineEdit(pager: Pager; mode: LineMode; current = ""; hide = false; let hist = pager.getLineHist(mode) if pager.term.isatty() and pager.config.input.use_mouse: pager.term.disableMouse() - let edit = readLine($mode & extraPrompt, current, pager.attrs.width, {}, hide, - hist) - pager.lineedit = some(edit) + pager.lineedit = readLine($mode & extraPrompt, current, pager.attrs.width, + {}, hide, hist) pager.linemode = mode proc clearLineEdit(pager: Pager) = - pager.lineedit = none(LineEdit) + pager.lineedit = nil if pager.term.isatty() and pager.config.input.use_mouse: pager.term.enableMouse() @@ -342,52 +362,43 @@ proc launchPager*(pager: Pager; istream: PosixStream; selector: Selector[int]) = of tsrSuccess: discard of tsrDA1Fail: pager.alert("Failed to query DA1, please set display.query-da1 = false") - pager.display = newFixedGrid(pager.attrs.width, pager.attrs.height - 1) - pager.statusgrid = newFixedGrid(pager.attrs.width) - -proc clearDisplay(pager: Pager) = - pager.display = newFixedGrid(pager.display.width, pager.display.height) - -proc buffer(pager: Pager): Container {.jsfget, inline.} = pager.container - -proc refreshDisplay(pager: Pager; container = pager.container) = pager.clearDisplay() - let hlcolor = cellColor(pager.config.display.highlight_color) - container.drawLines(pager.display, hlcolor) - if pager.config.display.highlight_marks: - container.highlightMarks(pager.display, hlcolor) + pager.clearStatus() + +proc buffer(pager: Pager): Container {.jsfget, inline.} = + return pager.container # Note: this function does not work correctly if start < i of last written char proc writeStatusMessage(pager: Pager; str: string; format = Format(); start = 0; maxwidth = -1; clip = '$'): int {.discardable.} = var maxwidth = maxwidth if maxwidth == -1: - maxwidth = pager.statusgrid.len + maxwidth = pager.status.grid.len var i = start - let e = min(start + maxwidth, pager.statusgrid.width) + let e = min(start + maxwidth, pager.status.grid.width) if i >= e: return i - pager.redraw = true + pager.status.redraw = true for r in str.runes: let w = r.width() if i + w >= e: - pager.statusgrid[i].format = format - pager.statusgrid[i].str = $clip + pager.status.grid[i].format = format + pager.status.grid[i].str = $clip inc i # Note: we assume `clip' is 1 cell wide break if r.isControlChar(): - pager.statusgrid[i].str = "^" - pager.statusgrid[i + 1].str = $getControlLetter(char(r)) - pager.statusgrid[i + 1].format = format + pager.status.grid[i].str = "^" + pager.status.grid[i + 1].str = $getControlLetter(char(r)) + pager.status.grid[i + 1].format = format else: - pager.statusgrid[i].str = $r - pager.statusgrid[i].format = format + pager.status.grid[i].str = $r + pager.status.grid[i].format = format i += w result = i var def = Format() while i < e: - pager.statusgrid[i].str = "" - pager.statusgrid[i].format = def + pager.status.grid[i].str = "" + pager.status.grid[i].format = def inc i # Note: should only be called directly after user interaction. @@ -416,7 +427,7 @@ proc refreshStatusMsg*(pager: Pager) = pager.writeStatusMessage(title, format, mw) else: let hover2 = " " & hover - let maxwidth = pager.statusgrid.width - hover2.width() - mw + let maxwidth = pager.status.grid.width - hover2.width() - mw let tw = pager.writeStatusMessage(title, format, mw, maxwidth, '>') pager.writeStatusMessage(hover2, format, tw) @@ -455,8 +466,13 @@ proc drawBuffer*(pager: Pager; container: Container; ofile: File) = ofile.flushFile() proc redraw(pager: Pager) {.jsfunc.} = - pager.redraw = true pager.term.clearCanvas() + pager.display.redraw = true + pager.status.redraw = true + if pager.container != nil: + pager.container.redraw = true + if pager.container.select != nil: + pager.container.select.redraw = true proc loadCachedImage(pager: Pager; container: Container; bmp: NetworkBitmap) = #TODO this is kinda dumb, because we cannot unload cached images. @@ -478,7 +494,7 @@ proc loadCachedImage(pager: Pager; container: Container; bmp: NetworkBitmap) = return nil return res.get.saveToBitmap(bmp) ).then(proc() = - pager.redraw = true + container.redraw = true cachedImage.loaded = true pager.loader.removeCachedItem(cacheId) ) @@ -504,65 +520,72 @@ proc initImages(pager: Pager; container: Container) = imageId = pager.imageId inc pager.imageId let canvasImage = pager.term.loadImage(image.bmp, container.process, imageId, - image.x - container.fromx, image.y - container.fromy, pager.attrs.width, - pager.attrs.height - 1) + image.x - container.fromx, image.y - container.fromy, pager.bufWidth, + pager.bufHeight) if canvasImage != nil: newImages.add(canvasImage) - canvasImage.marked = true - if pager.term.imageMode == imKitty: - for image in pager.term.canvasImages: - if not image.marked: - pager.term.imagesToClear.add(image) - image.marked = false + pager.term.clearImages(pager.bufHeight) pager.term.canvasImages = newImages proc draw*(pager: Pager) = + var redraw = false let container = pager.container if container != nil: - 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 container.redraw: + pager.clearDisplay() + let hlcolor = cellColor(pager.config.display.highlight_color) + container.drawLines(pager.display.grid, hlcolor) + if pager.config.display.highlight_marks: + container.highlightMarks(pager.display.grid, hlcolor) + container.redraw = false + pager.display.redraw = true + if (let select = container.select; select != nil and select.redraw): + select.drawSelect(pager.display.grid) + select.redraw = false + pager.display.redraw = true + if pager.display.redraw: + pager.term.writeGrid(pager.display.grid) + pager.display.redraw = false + redraw = true if pager.askpromise != nil or pager.askcharpromise != nil: - discard - elif pager.lineedit.isSome: - if pager.lineedit.get.invalid: - let x = pager.lineedit.get.generateOutput() + pager.term.writeGrid(pager.status.grid, 0, pager.attrs.height - 1) + pager.status.redraw = false + redraw = true + elif pager.lineedit != nil: + if pager.lineedit.redraw: + let x = pager.lineedit.generateOutput() pager.term.writeGrid(x, 0, pager.attrs.height - 1) - pager.lineedit.get.invalid = false - pager.redraw = true - else: - pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1) - if pager.redraw: - if container != nil and pager.term.imageMode != imNone: - pager.initImages(container) + pager.lineedit.redraw = false + redraw = true + elif pager.status.redraw: + pager.term.writeGrid(pager.status.grid, 0, pager.attrs.height - 1) + pager.status.redraw = false + redraw = true + if container != nil and pager.term.imageMode != imNone: + # init images only after term canvas has been finalized + pager.initImages(container) + if redraw: pager.term.hideCursor() pager.term.outputGrid() if pager.term.imageMode != imNone: pager.term.outputImages() if pager.askpromise != nil: pager.term.setCursor(pager.askcursor, pager.attrs.height - 1) - elif pager.lineedit.isSome: - pager.term.setCursor(pager.lineedit.get.getCursorX(), - pager.attrs.height - 1) + elif pager.lineedit != nil: + pager.term.setCursor(pager.lineedit.getCursorX(), pager.attrs.height - 1) elif container != nil: - if container.select.open: - pager.term.setCursor(container.select.getCursorX(), - container.select.getCursorY()) + if (let select = container.select; select != nil): + pager.term.setCursor(select.getCursorX(), select.getCursorY()) else: - pager.term.setCursor(pager.container.acursorx, pager.container.acursory) - if pager.redraw: + pager.term.setCursor(container.acursorx, container.acursory) + if redraw: pager.term.showCursor() pager.term.flush() - pager.redraw = false proc writeAskPrompt(pager: Pager; s = "") = - let maxwidth = pager.statusgrid.width - s.len + let maxwidth = pager.status.grid.width - s.len let i = pager.writeStatusMessage(pager.askprompt, maxwidth = maxwidth) pager.askcursor = pager.writeStatusMessage(s, start = i) - pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1) proc ask(pager: Pager; prompt: string): Promise[bool] {.jsfunc.} = pager.askprompt = prompt @@ -1010,10 +1033,10 @@ proc windowChange*(pager: Pager) = if pager.attrs == oldAttrs: #TODO maybe it's more efficient to let false positives through? return - if pager.lineedit.isSome: - pager.lineedit.get.windowChange(pager.attrs) - pager.display = newFixedGrid(pager.attrs.width, pager.attrs.height - 1) - pager.statusgrid = newFixedGrid(pager.attrs.width) + if pager.lineedit != nil: + pager.lineedit.windowChange(pager.attrs) + pager.clearDisplay() + pager.clearStatus() for container in pager.containers: container.windowChange(pager.attrs) if pager.askprompt != "": @@ -1241,14 +1264,14 @@ proc compileSearchRegex(pager: Pager; s: string): Result[Regex, string] = return compileSearchRegex(s, flags) proc updateReadLineISearch(pager: Pager; linemode: LineMode) = - let lineedit = pager.lineedit.get + let lineedit = pager.lineedit pager.isearchpromise = pager.isearchpromise.then(proc(): EmptyPromise = case lineedit.state of lesCancel: pager.iregex.err() pager.container.popCursorPos() pager.container.clearSearchHighlights() - pager.redraw = true + pager.container.redraw = true pager.isearchpromise = nil of lesEdit: if lineedit.news != "": @@ -1271,7 +1294,7 @@ proc updateReadLineISearch(pager: Pager; linemode: LineMode) = pager.container.markPos() pager.container.clearSearchHighlights() pager.container.sendCursorPosition() - pager.redraw = true + pager.container.redraw = true pager.isearchpromise = nil ) @@ -1292,7 +1315,7 @@ proc saveTo(pager: Pager; data: LineDataDownload; path: string) = ) proc updateReadLine*(pager: Pager) = - let lineedit = pager.lineedit.get + let lineedit = pager.lineedit if pager.linemode in {lmISearchF, lmISearchB}: pager.updateReadLineISearch(pager.linemode) else: @@ -1359,8 +1382,7 @@ proc updateReadLine*(pager: Pager) = data.stream.sclose() else: discard pager.lineData = nil - if lineedit.state in {lesCancel, lesFinish} and - pager.lineedit.get == lineedit: + if lineedit.state in {lesCancel, lesFinish} and pager.lineedit == lineedit: pager.clearLineEdit() # Same as load(s + '\n') @@ -1773,7 +1795,6 @@ proc askDownloadPath(pager: Pager; container: Container; response: Response) = stream: response.body ) pager.deleteContainer(container, container.find(ndAny)) - pager.redraw = true pager.refreshStatusMsg() dec pager.numload @@ -1840,7 +1861,6 @@ proc connected(pager: Pager; container: Container; response: Response) = else: dec pager.numload pager.deleteContainer(container, container.find(ndAny)) - pager.redraw = true pager.refreshStatusMsg() # true if done, false if keep @@ -1915,11 +1935,6 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent): pager.dupeBuffer(container, url2) of cetNoAnchor: pager.alert("Couldn't find anchor " & event.anchor) - of cetUpdate: - if container == pager.container: - pager.redraw = true - if event.force: - pager.term.clearCanvas() of cetReadLine: if container == pager.container: pager.setLineEdit(lmBuffer, event.value, hide = event.password, @@ -1931,7 +1946,6 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent): pager.container.readSuccess(s) else: pager.container.readCanceled() - pager.redraw = true of cetReadFile: if container == pager.container: pager.setLineEdit(lmBufferFile, "") @@ -1990,6 +2004,10 @@ proc handleEvents*(pager: Pager; container: Container) = if not pager.handleEvent0(container, event): break +proc handleEvents*(pager: Pager) = + if pager.container != nil: + pager.handleEvents(pager.container) + proc handleEvent*(pager: Pager; container: Container) = try: container.handleEvent() diff --git a/src/local/select.nim b/src/local/select.nim deleted file mode 100644 index 9e2f2c22..00000000 --- a/src/local/select.nim +++ /dev/null @@ -1,307 +0,0 @@ -import std/unicode - -import monoucha/jsregex -import server/buffer -import types/cell -import utils/luwrap -import utils/strwidth - -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 = 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 r: Rune - 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: - 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/local/term.nim b/src/local/term.nim index e4fc7429..91fe4a88 100644 --- a/src/local/term.nim +++ b/src/local/term.nim @@ -68,8 +68,7 @@ type kittyId: int bmp: Bitmap - Terminal* = ref TerminalObj - TerminalObj = object + Terminal* = ref object cs*: Charset config: Config istream*: PosixStream @@ -95,6 +94,8 @@ type ibuf*: string # buffer for chars when we can't process them sixelRegisterNum: int kittyId: int # counter for kitty image (*not* placement) ids. + cursorx: int + cursory: int # control sequence introducer template CSI(s: varargs[string, `$`]): string = @@ -283,7 +284,11 @@ proc endFormat(term: Terminal; flag: FormatFlags): string = return SGR(FormatCodes[flag].e) proc setCursor*(term: Terminal; x, y: int) = - term.write(term.cursorGoto(x, y)) + assert x >= 0 and y >= 0 + if x != term.cursorx or y != term.cursory: + term.write(term.cursorGoto(x, y)) + term.cursorx = x + term.cursory = y proc enableAltScreen(term: Terminal): string = when termcap_found: @@ -608,6 +613,8 @@ proc outputGrid*(term: Terminal) = term.cleared = true else: term.outfile.write(term.generateSwapOutput()) + term.cursorx = -1 + term.cursory = -1 func findImage(term: Terminal; pid, imageId: int): CanvasImage = for it in term.canvasImages: @@ -633,25 +640,50 @@ proc positionImage(term: Terminal; image: CanvasImage; x, y, maxw, maxh: int): image.dispw = min(int(image.bmp.width) + xpx, maxwpx) - xpx image.disph = min(int(image.bmp.height) + ypx, maxhpx) - ypx image.damaged = true - return image.dispw > 0 and image.disph > 0 + return image.dispw > image.offx and image.disph > image.offy + +proc clearImage*(term: Terminal; image: CanvasImage; maxh: int) = + case term.imageMode + of imNone: discard + of imSixel: + # we must clear sixels the same way as we clear text. + var y = max(image.y, 0) + let ey = min(image.y + int(image.bmp.height), maxh) + let x = image.x + while y < ey: + term.lineDamage[y] = min(x, term.lineDamage[y]) + inc y + of imKitty: + term.imagesToClear.add(image) + +proc clearImages*(term: Terminal; maxh: int) = + for image in term.canvasImages: + if not image.marked: + term.clearImage(image, maxh) + image.marked = false proc loadImage*(term: Terminal; bmp: Bitmap; pid, imageId, x, y, maxw, maxh: int): CanvasImage = if (let image = term.findImage(pid, imageId); image != nil): # reuse image on screen if image.x != x or image.y != y: - # with sixel, we must clear the image currently on the screen the same way - # as we clear text. + # only clear sixels; with kitty we just move the existing image if term.imageMode == imSixel: - var y = max(image.y, 0) - let ey = min(image.y + int(image.bmp.height), maxh) - let x = image.x - while y < ey: - term.lineDamage[y] = min(x, term.lineDamage[y]) - inc y + term.clearImage(image, maxh) if not term.positionImage(image, x, y, maxw, maxh): # no longer on screen return nil + elif term.imageMode == imSixel: + # check if any line our image is on is damaged + let ey = min(image.y + int(image.bmp.height), maxh) + let mx = (image.offx + image.dispw) div term.attrs.ppc + for y in max(image.y, 0) ..< ey: + if term.lineDamage[y] < mx: + image.damaged = true + break + # only mark old images; new images will not be checked until the next + # initImages call. + image.marked = true return image # new image let image = CanvasImage(bmp: bmp, pid: pid, imageId: imageId) @@ -807,14 +839,14 @@ proc clearCanvas*(term: Terminal) = term.cleared = false let maxw = term.attrs.width let maxh = term.attrs.height - 1 - var toRemove: seq[int] = @[] - for i, image in term.canvasImages: - if not term.positionImage(image, image.x, image.y, maxw, maxh): - toRemove.add(i) - if term.imageMode == imKitty: - term.imagesToClear.add(image) - for i in countdown(toRemove.high, 0): - term.canvasImages.delete(toRemove[i]) + var newImages: seq[CanvasImage] = @[] + for image in term.canvasImages: + if term.positionImage(image, image.x, image.y, maxw, maxh): + image.damaged = true + image.marked = true + newImages.add(image) + term.clearImages(maxh) + term.canvasImages = newImages # see https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html proc disableRawMode(term: Terminal) = @@ -1216,6 +1248,8 @@ proc initScreen(term: Terminal) = term.write(term.enableAltScreen()) if term.config.input.use_mouse: term.enableMouse() + term.cursorx = -1 + term.cursory = -1 proc start*(term: Terminal; istream: PosixStream): TermStartResult = term.istream = istream diff --git a/todo b/todo index fab985f5..84be5079 100644 --- a/todo +++ b/todo @@ -74,7 +74,6 @@ layout engine: - writing-mode, grid, ruby, ... (i.e. cool new stuff) images: - more efficient sixel display (store encoded images) -- more efficient display in general (why are we repainting twice per keypress?) - document it (when performance is acceptable) - proper sixel color register allocation, dithering - fix race condition where images decoded after buffer load won't display until |