about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-11-12 18:52:26 +0100
committerbptato <nincsnevem662@gmail.com>2024-11-12 18:52:26 +0100
commita4133432e47d9e45159f0ed1d4e7f6a2c02dc5b6 (patch)
tree679ee33faf91877685a89e3bfe8743bf2b013eea /src
parent66db97a494d4605c66ba0da049accf0375f647b2 (diff)
downloadchawan-a4133432e47d9e45159f0ed1d4e7f6a2c02dc5b6.tar.gz
buffer: fix broken gotoAnchor behavior
23beebe6 introduced a regression that broke gotoAnchor. This fixes that,
plus a couple other long-standing gotoAnchor bugs:

* If no anchor is found, do not dupe the buffer.
  Desktop browsers still add a history entry, while w3m prints an
  error. I've copied the latter because it makes more sense as a user,
  but this will have to be refined for the navigation API at some point.
* If the anchor *is* found, then always jump to it, even if it's not
  visible.
  This was a limitation of relying on the line array, so now we rely on
  the box tree instead. (Sooner or later, the former must go anyway.)

Also, fix `U' reload not restoring the position (hopefully this time for
good).
Diffstat (limited to 'src')
-rw-r--r--src/css/box.nim5
-rw-r--r--src/css/render.nim11
-rw-r--r--src/local/container.nim68
-rw-r--r--src/local/pager.nim20
-rw-r--r--src/server/buffer.nim109
5 files changed, 123 insertions, 90 deletions
diff --git a/src/css/box.nim b/src/css/box.nim
index a40cd4af..3d35b2ab 100644
--- a/src/css/box.nim
+++ b/src/css/box.nim
@@ -58,6 +58,7 @@ type
 
   InlineFragment* = ref object
     state*: InlineFragmentState
+    render*: BoxRenderState
     computed*: CSSComputedValues
     node*: StyledNode
     splitType*: set[SplitType]
@@ -79,8 +80,12 @@ type
 
   RelativeRect* = array[DimensionType, Span]
 
+  BoxRenderState* = object
+    offset*: Offset
+
   BlockBox* = ref object
     state*: BoxLayoutState
+    render*: BoxRenderState
     computed*: CSSComputedValues
     node*: StyledNode
     inline*: InlineFragment
diff --git a/src/css/render.nim b/src/css/render.nim
index 6db0a9de..9688ff30 100644
--- a/src/css/render.nim
+++ b/src/css/render.nim
@@ -2,7 +2,6 @@ import std/algorithm
 
 import css/box
 import css/cssvalues
-import css/layout
 import css/lunit
 import css/stylednode
 import types/bitmap
@@ -345,8 +344,10 @@ proc renderInlineFragment(grid: var FlexibleGrid; state: var RenderState;
     if bgcolor0.a > 0:
       grid.paintInlineFragment(state, fragment, offset,
         bgcolor0.rgb.cellColor())
+  let startOffset = offset + fragment.state.startOffset
+  fragment.render.offset = startOffset
   if position notin PositionStaticLike and stSplitStart in fragment.splitType:
-    state.absolutePos.add(offset + fragment.state.startOffset)
+    state.absolutePos.add(startOffset)
   if fragment.t == iftParent:
     for child in fragment.children:
       grid.renderInlineFragment(state, child, offset, bgcolor0)
@@ -396,6 +397,7 @@ proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
     if not box.computed{"top"}.auto or not box.computed{"bottom"}.auto:
       offset.y = state.absolutePos[^1].y
   offset += box.state.offset
+  box.render.offset = offset
   if position notin PositionStaticLike:
     state.absolutePos.add(offset)
   if box.computed{"visibility"} == VisibilityVisible:
@@ -438,13 +440,12 @@ proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
     discard state.absolutePos.pop()
 
 proc renderDocument*(grid: var FlexibleGrid; bgcolor: var CellColor;
-    styledRoot: StyledNode; attrsp: ptr WindowAttributes;
+    rootBox: BlockBox; attrsp: ptr WindowAttributes;
     images: var seq[PosBitmap]) =
   grid.setLen(0)
-  if styledRoot == nil:
+  if rootBox == nil:
     # no HTML element when we run cascade; just clear all lines.
     return
-  let rootBox = styledRoot.layout(attrsp)
   var state = RenderState(
     absolutePos: @[offset(0, 0)],
     attrsp: attrsp,
diff --git a/src/local/container.nim b/src/local/container.nim
index a5cdd630..862ae0ed 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -46,9 +46,8 @@ type
     setxsave: bool
 
   ContainerEventType* = enum
-    cetAnchor, cetNoAnchor, cetReadLine, cetReadArea, cetReadFile, cetOpen,
-    cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle, cetCancel,
-    cetMetaRefresh
+    cetReadLine, cetReadArea, cetReadFile, cetOpen, cetSetLoadInfo, cetStatus,
+    cetAlert, cetLoaded, cetTitle, cetCancel, cetMetaRefresh
 
   ContainerEvent* = object
     case t*: ContainerEventType
@@ -62,8 +61,6 @@ type
       request*: Request
       save*: bool
       url*: URL
-    of cetAnchor, cetNoAnchor:
-      anchor*: string
     of cetAlert:
       msg*: string
     of cetMetaRefresh:
@@ -151,7 +148,7 @@ type
     # if set, this *overrides* any content type received from the network. (this
     # is because it stores the content type from the -T flag.)
     contentType* {.jsget.}: Option[string]
-    pos: CursorPosition
+    pos*: CursorPosition
     bpos: seq[CursorPosition]
     highlights: seq[Highlight]
     process* {.jsget.}: int
@@ -489,13 +486,16 @@ proc triggerEvent(container: Container; event: ContainerEvent) =
 proc triggerEvent(container: Container; t: ContainerEventType) =
   container.triggerEvent(ContainerEvent(t: t))
 
-proc setNumLines(container: Container; lines: int; finish = false) =
+proc gotoStart(container: Container) =
+  container.pos = container.startpos.get
+  container.startpos = none(CursorPosition)
+  container.needslines = true
+
+proc setNumLines(container: Container; lines: int) =
   if container.numLines != lines:
     container.numLines = lines
-    if container.startpos.isSome and finish:
-      container.pos = container.startpos.get
-      container.startpos = none(CursorPosition)
-      container.needslines = true
+    if container.startpos.isSome and lines >= container.startpos.get.cursory:
+      container.gotoStart()
     container.updateCursor()
 
 proc queueDraw*(container: Container) =
@@ -514,7 +514,7 @@ proc requestLines(container: Container): EmptyPromise {.discardable.} =
     if isBgNew:
       container.bgcolor = res.bgcolor
     if res.numLines != container.numLines:
-      container.setNumLines(res.numLines, true)
+      container.setNumLines(res.numLines)
       if container.loadState != lsLoading:
         container.triggerEvent(cetStatus)
     if res.numLines > 0:
@@ -706,7 +706,7 @@ proc setCursorYCenter(container: Container; y: int; refresh = true)
   if fy != container.fromy:
     container.centerLine()
 
-proc setCursorXYCenter(container: Container; x, y: int; refresh = true)
+proc setCursorXYCenter*(container: Container; x, y: int; refresh = true)
     {.jsfunc.} =
   let fy = container.fromy
   let fx = container.fromx
@@ -1102,10 +1102,13 @@ proc popCursorPos*(container: Container; nojump = false) =
       container.sendCursorPosition()
       container.needslines = true
 
-proc copyCursorPos*(container, c2: Container) =
-  container.startpos = some(c2.pos)
+proc setStartingPos*(container: Container; pos: CursorPosition) =
+  container.startpos = some(pos)
   container.flags.incl(cfHasStart)
 
+proc setStartingPos*(container: Container; x, y: int) =
+  container.setStartingPos(CursorPosition(setx: -1, cursorx: x, cursory: y))
+
 proc cursorNextLink*(container: Container; n = 1) {.jsfunc.} =
   if container.iface == nil:
     return
@@ -1427,18 +1430,21 @@ proc onload(container: Container; res: int) =
     container.setLoadInfo("")
     container.triggerEvent(cetStatus)
     container.triggerEvent(cetLoaded)
-    if cfHasStart notin container.flags and (container.url.hash != "" or
-        container.config.autofocus):
-      container.requestLines().then(proc(): Promise[GotoAnchorResult] =
-        return container.iface.gotoAnchor()
-      ).then(proc(res: GotoAnchorResult) =
-        if res.found:
-          container.setCursorXYCenter(res.x, res.y)
-          if res.focus != nil:
-            container.onReadLine(res.focus)
-      )
+    if cfHasStart in container.flags:
+      if container.startpos.isSome:
+        container.gotoStart()
     else:
-      container.needslines = true
+      let anchor = container.url.hash.substr(1)
+      if anchor != "" or container.config.autofocus:
+        container.requestLines().then(proc(): Promise[GotoAnchorResult] =
+          return container.iface.gotoAnchor(anchor, container.config.autofocus)
+        ).then(proc(res: GotoAnchorResult) =
+          if res.found:
+            container.setCursorXYCenter(res.x, res.y)
+            if res.focus != nil:
+              container.onReadLine(res.focus)
+        )
+    container.needslines = true
     if container.config.metaRefresh != mrNever:
       container.iface.checkRefresh().then(proc(res: CheckRefreshResult) =
         if res.n >= 0:
@@ -1520,14 +1526,6 @@ proc cancel*(container: Container) {.jsfunc.} =
     else:
       container.triggerEvent(cetCancel)
 
-proc findAnchor*(container: Container; anchor: string) =
-  container.iface.findAnchor(anchor).then(proc(found: bool) =
-    if found:
-      container.triggerEvent(ContainerEvent(t: cetAnchor, anchor: anchor))
-    else:
-      container.triggerEvent(ContainerEvent(t: cetNoAnchor, anchor: anchor))
-  )
-
 proc readCanceled*(container: Container) =
   container.iface.readCanceled().then(proc(repaint: bool) =
     if repaint:
@@ -1694,7 +1692,7 @@ proc onreadline(container: Container; w: Slice[int];
     container.iface.getLines(w).then(proc(res: GetLinesResult) =
       container.onreadline(w, handle, res))
   else:
-    container.setNumLines(res.numLines, true)
+    container.setNumLines(res.numLines)
 
 # Synchronously read all lines in the buffer.
 proc readLines*(container: Container; handle: proc(line: SimpleFlexibleLine)) =
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 07244408..ee799705 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -865,7 +865,7 @@ proc dupeBuffer(pager: Pager; container: Container; url: URL) =
   if p == nil:
     pager.alert("Failed to duplicate buffer.")
   else:
-    p.then(proc(container: Container) =
+    p.then(proc(container: Container): Container =
       if container == nil:
         pager.alert("Failed to duplicate buffer.")
       else:
@@ -1423,13 +1423,21 @@ proc gotoURL(pager: Pager; request: Request; prevurl = none(URL);
       else:
         container.replaceBackup = replaceBackup
         replaceBackup.replaceRef = container
-      container.copyCursorPos(replace)
+      container.setStartingPos(replace.pos)
     else:
       pager.addContainer(container)
     inc pager.numload
     return container
   else:
-    pager.container.findAnchor(request.url.hash)
+    let container = pager.container
+    let url = request.url
+    let anchor = url.hash.substr(1)
+    container.iface.gotoAnchor(anchor, false).then(proc(res: GotoAnchorResult) =
+      if res.found:
+        pager.dupeBuffer(container, url)
+      else:
+        pager.alert("Anchor " & url.hash & " not found")
+    )
     return nil
 
 proc omniRewrite(pager: Pager; s: string): string =
@@ -2231,12 +2239,6 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
   case event.t
   of cetLoaded:
     dec pager.numload
-  of cetAnchor:
-    let url2 = newURL(container.url)
-    url2.setHash(event.anchor)
-    pager.dupeBuffer(container, url2)
-  of cetNoAnchor:
-    pager.alert("Couldn't find anchor " & event.anchor)
   of cetReadLine:
     if container == pager.container:
       pager.setLineEdit(lmBuffer, event.value, hide = event.password,
diff --git a/src/server/buffer.nim b/src/server/buffer.nim
index 0386b996..0aa8b545 100644
--- a/src/server/buffer.nim
+++ b/src/server/buffer.nim
@@ -11,9 +11,12 @@ import chagashi/decoder
 import chagashi/decodercore
 import chame/tags
 import config/config
+import css/box
 import css/cascade
 import css/cssparser
 import css/cssvalues
+import css/layout
+import css/lunit
 import css/render
 import css/sheet
 import css/stylednode
@@ -51,12 +54,11 @@ import utils/twtstr
 
 type
   BufferCommand* = enum
-    bcLoad, bcForceRender, bcWindowChange, bcFindAnchor, bcReadSuccess,
-    bcReadCanceled, bcClick, bcFindNextLink, bcFindPrevLink, bcFindNthLink,
-    bcFindRevNthLink, bcFindNextMatch, bcFindPrevMatch, bcGetLines,
-    bcUpdateHover, bcGotoAnchor, bcCancel, bcGetTitle, bcSelect, bcClone,
-    bcFindPrevParagraph, bcFindNextParagraph, bcMarkURL, bcToggleImages,
-    bcCheckRefresh
+    bcLoad, bcForceRender, bcWindowChange, bcReadSuccess, bcReadCanceled,
+    bcClick, bcFindNextLink, bcFindPrevLink, bcFindNthLink, bcFindRevNthLink,
+    bcFindNextMatch, bcFindPrevMatch, bcGetLines, bcUpdateHover, bcGotoAnchor,
+    bcCancel, bcGetTitle, bcSelect, bcClone, bcFindPrevParagraph,
+    bcFindNextParagraph, bcMarkURL, bcToggleImages, bcCheckRefresh
 
   BufferState = enum
     bsLoadingPage, bsLoadingResources, bsLoaded
@@ -98,6 +100,7 @@ type
     pollData: PollData
     prevStyled: StyledNode
     prevnode: StyledNode
+    rootBox: BlockBox
     pstream: SocketStream # control stream
     quirkstyle: CSSStylesheet
     reportedBytesRead: int
@@ -463,12 +466,12 @@ proc findPrevLink*(buffer: Buffer; cursorx, cursory, n: int):
 
   var ly = 0 #last y
   var lx = 0 #last x
-  template link_beginning() =
-    #go to beginning of link
+  template link_beginning(y: int) =
+    # go to beginning of link
     ly = y #last y
     lx = format.pos #last x
 
-    #on the current line
+    # on the current line
     let line = buffer.lines[y]
     while i >= 0:
       let format = line.formats[i]
@@ -477,7 +480,7 @@ proc findPrevLink*(buffer: Buffer; cursorx, cursory, n: int):
         lx = format.pos
       dec i
 
-    #on previous lines
+    # on previous lines
     for iy in countdown(ly - 1, 0):
       let line = buffer.lines[iy]
       i = line.formats.len - 1
@@ -507,8 +510,7 @@ proc findPrevLink*(buffer: Buffer; cursorx, cursory, n: int):
     let format = line.formats[i]
     let fl = format.node.getClickable()
     if fl != nil and fl != link:
-      let y = cursory
-      link_beginning
+      link_beginning cursory
       found_pos lx, ly, fl
     dec i
 
@@ -519,7 +521,7 @@ proc findPrevLink*(buffer: Buffer; cursorx, cursory, n: int):
       let format = line.formats[i]
       let fl = format.node.getClickable()
       if fl != nil and fl != link:
-        link_beginning
+        link_beginning y
         found_pos lx, ly, fl
       dec i
   return (-1, -1)
@@ -704,12 +706,44 @@ type GotoAnchorResult* = object
   y*: int
   focus*: ReadLineResult
 
-proc gotoAnchor*(buffer: Buffer): GotoAnchorResult {.proxy.} =
+proc findAnchor(box: BlockBox; anchor: Element): Offset
+
+proc findAnchor(fragment: InlineFragment; anchor: Element): Offset =
+  if fragment.t == iftBox:
+    let off = fragment.box.findAnchor(anchor)
+    if off.y >= 0:
+      return off
+  elif fragment.t == iftParent:
+    for child in fragment.children:
+      let off = child.findAnchor(anchor)
+      if off.y >= 0:
+        return off
+  if fragment.node != nil and fragment.node.node == anchor:
+    return fragment.render.offset
+  return offset(-1, -1)
+
+proc findAnchor(box: BlockBox; anchor: Element): Offset =
+  if box.inline != nil:
+    let off = box.inline.findAnchor(anchor)
+    if off.y >= 0:
+      return off
+  for child in box.children:
+    let off = child.findAnchor(anchor)
+    if off.y >= 0:
+      return off
+  if box.node != nil and box.node.node == anchor:
+    return box.render.offset
+  return offset(-1, -1)
+
+proc gotoAnchor*(buffer: Buffer; anchor: string; autofocus: bool):
+    GotoAnchorResult {.proxy.} =
   if buffer.document == nil:
     return GotoAnchorResult(found: false)
-  var anchor = buffer.document.findAnchor(buffer.url.hash)
+  var anchor = buffer.document.findAnchor(anchor)
   var focus: ReadLineResult = nil
-  if buffer.config.autofocus:
+  # Do not use buffer.config.autofocus when we just want to check if the
+  # anchor can be found.
+  if autofocus:
     let autofocus = buffer.document.findAutoFocus()
     if autofocus != nil:
       if anchor == nil:
@@ -718,18 +752,10 @@ proc gotoAnchor*(buffer: Buffer): GotoAnchorResult {.proxy.} =
       focus = res.readline.get(nil)
   if anchor == nil:
     return GotoAnchorResult(found: false)
-  for y in 0 ..< buffer.lines.len:
-    let line = buffer.lines[y]
-    for i in 0 ..< line.formats.len:
-      let format = line.formats[i]
-      if format.node != nil and format.node.node in anchor:
-        return GotoAnchorResult(
-          found: true,
-          x: format.pos,
-          y: y,
-          focus: focus
-        )
-  return GotoAnchorResult(found: false)
+  let offset = buffer.rootBox.findAnchor(anchor)
+  let x = max(offset.x div buffer.attrs.ppc, 0).toInt
+  let y = max(offset.y div buffer.attrs.ppl, 0).toInt
+  return GotoAnchorResult(found: true, x: x, y: y, focus: focus)
 
 type CheckRefreshResult* = object
   # n is timeout in millis. -1 => not found
@@ -788,8 +814,12 @@ proc reshape(buffer: Buffer) =
     buffer.prevStyled = nil
   let styledRoot = buffer.document.applyStylesheets(uastyle,
     buffer.userstyle, buffer.prevStyled)
-  buffer.lines.renderDocument(buffer.bgcolor, styledRoot, addr buffer.attrs,
-    buffer.images)
+  # applyStylesheets may return nil if there is no <html> element.
+  buffer.rootBox = nil
+  if styledRoot != nil:
+    buffer.rootBox = styledRoot.layout(addr buffer.attrs)
+  buffer.lines.renderDocument(buffer.bgcolor, buffer.rootBox,
+    addr buffer.attrs, buffer.images)
   buffer.prevStyled = styledRoot
 
 proc maybeReshape(buffer: Buffer) =
@@ -1088,9 +1118,9 @@ proc hasTask(buffer: Buffer; cmd: BufferCommand): bool =
 proc resolveTask[T](buffer: Buffer; cmd: BufferCommand; res: T) =
   let packetid = buffer.tasks[cmd]
   assert packetid != 0
-  buffer.pstream.withPacketWriter w:
-    w.swrite(packetid)
-    w.swrite(res)
+  buffer.pstream.withPacketWriter wt:
+    wt.swrite(packetid)
+    wt.swrite(res)
   buffer.tasks[cmd] = 0
 
 proc onload(buffer: Buffer) =
@@ -1610,9 +1640,6 @@ proc select*(buffer: Buffer; selected: seq[int]): ClickResult {.proxy.} =
 proc readCanceled*(buffer: Buffer): bool {.proxy.} =
   return buffer.restoreFocus()
 
-proc findAnchor*(buffer: Buffer; anchor: string): bool {.proxy.} =
-  return buffer.document != nil and buffer.document.findAnchor(anchor) != nil
-
 type GetLinesResult* = tuple
   numLines: int
   lines: seq[SimpleFlexibleLine]
@@ -1741,14 +1768,14 @@ macro bufferDispatcher(funs: static ProxyMap; buffer: Buffer;
     var resolve = newStmtList()
     if rval == nil:
       resolve.add(quote do:
-        buffer.pstream.withPacketWriter w:
-          w.swrite(`packetid`)
+        buffer.pstream.withPacketWriter wt:
+          wt.swrite(`packetid`)
       )
     else:
       resolve.add(quote do:
-        buffer.pstream.withPacketWriter w:
-          w.swrite(`packetid`)
-          w.swrite(`rval`)
+        buffer.pstream.withPacketWriter wt:
+          wt.swrite(`packetid`)
+          wt.swrite(`rval`)
       )
     if v.istask:
       let en = v.ename