about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-09-24 19:36:21 +0200
committerbptato <nincsnevem662@gmail.com>2024-11-23 20:52:38 +0100
commit2138e8eb0f57bc78bfc336f8c5cd8092f61f0700 (patch)
treea04e83b67940fb45c662fc6063804e121030cf58 /src
parent23f2b3aca50c286eb1b65ef84b827826b47bd1ca (diff)
downloadchawan-2138e8eb0f57bc78bfc336f8c5cd8092f61f0700.tar.gz
pager, select: add right click menu, fix some bugs
It's *not* a context menu: items are fixed, and currently not even
customizable. Former is a feature, latter is a bug.

Also, select now has improved mouse support; its behavior is a
combination of the w3m menu (for btn1) and GTK Firefox context menu
(for btn3).

Also, fix some bugs in select:

* lines with double width chars are handled better (not perfectly,
  but at least the grid isn't completely mangled anymore)
* non-multiple select now highlights the currently selected option
* small selects at the bottom of the screen are handled correctly
* selects at the right edge of the screen are handled correctly
* select multiple no longer discards selected items on cursorLeft
Diffstat (limited to 'src')
-rw-r--r--src/local/client.nim182
-rw-r--r--src/local/container.nim3
-rw-r--r--src/local/pager.nim58
-rw-r--r--src/local/select.nim58
4 files changed, 219 insertions, 82 deletions
diff --git a/src/local/client.nim b/src/local/client.nim
index 93c9f3cd..6b7e0a67 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -58,6 +58,7 @@ type
     pressed: tuple[col, row: int]
     exitCode: int
     inEval: bool
+    blockTillRelease: bool
 
   ContainerData = ref object of MapData
     container: Container
@@ -214,6 +215,130 @@ proc evalAction(client: Client; action: string; arg0: int32): EmptyPromise =
   JS_FreeValue(ctx, ret)
   return p
 
+#TODO move mouse input handling somewhere else...
+proc handleMouseInputGeneric(client: Client; input: MouseInput) =
+  case input.button
+  of mibLeft:
+    case input.t
+    of mitPress:
+      client.pressed = (input.col, input.row)
+    of mitRelease:
+      if client.pressed != (-1, -1):
+        let diff = (input.col - client.pressed.col,
+          input.row - client.pressed.row)
+        if diff[0] > 0:
+          discard client.evalAction("cmd.buffer.scrollLeft", int32(diff[0]))
+        elif diff[0] < 0:
+          discard client.evalAction("cmd.buffer.scrollRight", -int32(diff[0]))
+        if diff[1] > 0:
+          discard client.evalAction("cmd.buffer.scrollUp", int32(diff[1]))
+        elif diff[1] < 0:
+          discard client.evalAction("cmd.buffer.scrollDown", -int32(diff[1]))
+        client.pressed = (-1, -1)
+    else: discard
+  of mibWheelUp:
+    if input.t == mitPress:
+      discard client.evalAction("cmd.buffer.scrollUp", 5)
+  of mibWheelDown:
+    if input.t == mitPress:
+      discard client.evalAction("cmd.buffer.scrollDown", 5)
+  of mibWheelLeft:
+    if input.t == mitPress:
+      discard client.evalAction("cmd.buffer.scrollLeft", 5)
+  of mibWheelRight:
+    if input.t == mitPress:
+      discard client.evalAction("cmd.buffer.scrollRight", 5)
+  else: discard
+
+proc handleMouseInput(client: Client; input: MouseInput; select: Select) =
+  let y = select.fromy + input.row - select.y - 1 # one off because of border
+  case input.button
+  of mibRight:
+    if (input.col, input.row) != client.pressed:
+      # Prevent immediate movement/submission in case the menu appeared under
+      # the cursor.
+      select.setCursorY(y)
+    case input.t
+    of mitPress:
+      # Do not include borders, so that a double right click closes the
+      # menu again.
+      if input.row notin select.y + 1 ..< select.y + select.height - 1 or
+          input.col notin select.x + 1 ..< select.x + select.width - 1:
+        client.blockTillRelease = true
+        select.cursorLeft()
+    of mitRelease:
+      if input.row in select.y + 1 ..< select.y + select.height - 1 and
+          input.col in select.x + 1 ..< select.x + select.width - 1 and
+          (input.col, input.row) != client.pressed:
+        select.click()
+      # forget about where we started once btn3 is released
+      client.pressed = (-1, -1)
+    of mitMove: discard
+  of mibLeft:
+    case input.t
+    of mitPress:
+      if input.row notin select.y ..< select.y + select.height or
+          input.col notin select.x ..< select.x + select.width:
+        # clicked outside the select
+        client.blockTillRelease = true
+        select.cursorLeft()
+    of mitRelease:
+      let at = (input.col, input.row)
+      if at == client.pressed and
+          (input.row in select.y + 1 ..< select.y + select.height - 1 and
+            input.col in select.x + 1 ..< select.x + select.width - 1 or
+          select.multiple and at == (select.x, select.y)):
+        # clicked inside the select
+        select.setCursorY(y)
+        select.click()
+    of mitMove: discard
+  else: discard
+
+proc handleMouseInput(client: Client; input: MouseInput; container: Container) =
+  case input.button
+  of mibLeft:
+    if input.t == mitRelease and client.pressed == (input.col, input.row):
+      let prevx = container.cursorx
+      let prevy = container.cursory
+      #TODO I wish we could avoid setCursorXY if we're just going to
+      # click, but that doesn't work with double-width chars
+      container.setCursorXY(container.fromx + input.col,
+        container.fromy + input.row)
+      if container.cursorx == prevx and container.cursory == prevy:
+        discard client.evalAction("cmd.buffer.click", 0)
+  of mibMiddle:
+    if input.t == mitRelease: # release, to emulate w3m
+      discard client.evalAction("cmd.pager.discardBuffer", 0)
+  of mibRight:
+    if input.t == mitPress: # w3m uses release, but I like this better
+      client.pressed = (input.col, input.row)
+      container.setCursorXY(container.fromx + input.col,
+        container.fromy + input.row)
+      client.pager.openMenu(input.col, input.row)
+  of mibThumbInner:
+    if input.t == mitPress:
+      discard client.evalAction("cmd.pager.prevBuffer", 0)
+  of mibThumbTip:
+    if input.t == mitPress:
+      discard client.evalAction("cmd.pager.nextBuffer", 0)
+  else: discard
+
+proc handleMouseInput(client: Client; input: MouseInput) =
+  if client.blockTillRelease:
+    if input.t == mitRelease:
+      client.blockTillRelease = false
+    else:
+      return
+  if client.pager.menu != nil:
+    client.handleMouseInput(input, client.pager.menu)
+  elif (let container = client.pager.container; container != nil):
+    if container.select != nil:
+      client.handleMouseInput(input, container.select)
+    else:
+      client.handleMouseInput(input, container)
+  if not client.blockTillRelease:
+    client.handleMouseInputGeneric(input)
+
 # The maximum number we are willing to accept.
 # This should be fine for 32-bit signed ints (which precnum currently is).
 # We can always increase it further (e.g. by switching to uint32, uint64...) if
@@ -243,62 +368,7 @@ proc handleCommandInput(client: Client; c: char): EmptyPromise =
       let input = client.pager.term.parseMouseInput()
       if input.isSome:
         let input = input.get
-        let container = client.pager.container
-        if container != nil:
-          case input.button
-          of mibLeft:
-            case input.t
-            of mitPress:
-              client.pressed = (input.col, input.row)
-            of mitRelease:
-              if client.pressed == (input.col, input.row):
-                let prevx = container.cursorx
-                let prevy = container.cursory
-                #TODO I wish we could avoid setCursorXY if we're just going to
-                # click, but that doesn't work with double-width chars
-                container.setCursorXY(container.fromx + input.col,
-                  container.fromy + input.row)
-                if container.cursorx == prevx and container.cursory == prevy:
-                  discard client.evalAction("cmd.buffer.click", 0)
-              else:
-                let diff = (input.col - client.pressed.col,
-                  input.row - client.pressed.row)
-                if diff[0] > 0:
-                  discard client.evalAction("cmd.buffer.scrollLeft",
-                    int32(diff[0]))
-                else:
-                  discard client.evalAction("cmd.buffer.scrollRight",
-                    -int32(diff[0]))
-                if diff[1] > 0:
-                  discard client.evalAction("cmd.buffer.scrollUp",
-                    int32(diff[1]))
-                else:
-                  discard client.evalAction("cmd.buffer.scrollDown",
-                    -int32(diff[1]))
-              client.pressed = (-1, -1)
-            else: discard
-          of mibMiddle:
-            if input.t == mitRelease: # release, to emulate w3m
-              discard client.evalAction("cmd.pager.discardBuffer", 0)
-          of mibWheelUp:
-            if input.t == mitPress:
-              discard client.evalAction("cmd.buffer.scrollUp", 5)
-          of mibWheelDown:
-            if input.t == mitPress:
-              discard client.evalAction("cmd.buffer.scrollDown", 5)
-          of mibWheelLeft:
-            if input.t == mitPress:
-              discard client.evalAction("cmd.buffer.scrollLeft", 5)
-          of mibWheelRight:
-            if input.t == mitPress:
-              discard client.evalAction("cmd.buffer.scrollRight", 5)
-          of mibThumbInner:
-            if input.t == mitPress:
-              discard client.evalAction("cmd.pager.prevBuffer", 0)
-          of mibThumbTip:
-            if input.t == mitPress:
-              discard client.evalAction("cmd.pager.nextBuffer", 0)
-          else: discard
+        client.handleMouseInput(input)
       client.pager.inputBuffer = ""
     elif "\e[<".startsWith(client.pager.inputBuffer):
       client.feednext = true
diff --git a/src/local/container.nim b/src/local/container.nim
index bee93e73..dbc510c9 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -1618,6 +1618,7 @@ proc displaySelect(container: Container; selectResult: SelectResult) =
     selectResult.selected,
     container.acursorx,
     container.acursory,
+    container.width,
     container.height,
     selectFinish,
     container
@@ -1671,7 +1672,7 @@ proc windowChange*(container: Container; attrs: WindowAttributes) =
       container.needslines = true
     )
   if container.select != nil:
-    container.select.windowChange(container.height)
+    container.select.windowChange(container.width, container.height)
 
 proc peek(container: Container) {.jsfunc.} =
   container.alert($container.url)
diff --git a/src/local/pager.nim b/src/local/pager.nim
index fbfd3710..eec61715 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -116,7 +116,7 @@ type
     container*: Container
     prev*: Container
 
-  Pager* = ref object
+  Pager* = ref object of RootObj
     alertState: PagerAlertState
     alerts*: seq[string]
     askcharpromise*: Promise[string]
@@ -142,6 +142,7 @@ type
     linemode: LineMode
     loader*: FileLoader
     luctx: LUContext
+    menu*: Select
     navDirection {.jsget.}: NavDirection
     notnum*: bool # has a non-numeric character been input already?
     numload*: int # number of pages currently being loaded
@@ -235,7 +236,9 @@ proc reflect(ctx: JSContext; this_val: JSValue; argc: cint;
 
 proc getter(ctx: JSContext; pager: Pager; a: JSAtom): JSValue {.jsgetownprop.} =
   if pager.container != nil:
-    let cval = if pager.container.select != nil:
+    let cval = if pager.menu != nil:
+      ctx.toJS(pager.menu)
+    elif pager.container.select != nil:
       ctx.toJS(pager.container.select)
     else:
       ctx.toJS(pager.container)
@@ -715,10 +718,17 @@ proc draw*(pager: Pager) =
       imageRedraw = true
       if container.select != nil:
         container.select.redraw = true
-    if (let select = container.select; select != nil and select.redraw):
+    if (let select = container.select; select != nil and
+        (select.redraw or pager.display.redraw)):
       select.drawSelect(pager.display.grid)
       select.redraw = false
       pager.display.redraw = true
+  if (let menu = pager.menu; menu != nil and
+      (menu.redraw or pager.display.redraw)):
+    menu.drawSelect(pager.display.grid)
+    menu.redraw = false
+    pager.display.redraw = true
+    imageRedraw = false
   if pager.display.redraw:
     pager.term.writeGrid(pager.display.grid)
     pager.display.redraw = false
@@ -749,6 +759,8 @@ proc draw*(pager: Pager) =
     pager.term.setCursor(pager.askcursor, pager.attrs.height - 1)
   elif pager.lineedit != nil:
     pager.term.setCursor(pager.lineedit.getCursorX(), pager.attrs.height - 1)
+  elif (let menu = pager.menu; menu != nil):
+    pager.term.setCursor(menu.getCursorX(), menu.getCursorY())
   elif container != nil:
     if (let select = container.select; select != nil):
       pager.term.setCursor(select.getCursorX(), select.getCursorY())
@@ -2237,6 +2249,46 @@ proc metaRefresh(pager: Pager; container: Container; n: int; url: URL) =
   for arg in args:
     JS_FreeValue(ctx, arg)
 
+const MenuMap = [
+  ("Previous buffer (,)", "pager.prevBuffer"),
+  ("Next buffer (.)", "pager.nextBuffer"),
+  ("Discard buffer (D)", "pager.discardBuffer"),
+  ("View source (\\)", "pager.toggleSource"),
+  ("Edit source (sE)", "buffer.sourceEdit"),
+  ("Save source (sS)", "buffer.saveSource"),
+  ("Reload (U)", "pager.reloadBuffer"),
+  ("Save link (s<Enter>)", "buffer.saveLink"),
+  ("View image (I)", "buffer.viewImage")
+]
+
+proc menuFinish(opaque: RootRef; select: Select; sr: SubmitResult) =
+  let pager = Pager(opaque)
+  case sr
+  of srCancel: discard
+  of srSubmit:
+    let action = MenuMap[select.selected[0]][1]
+    let fun = pager.config.cmd.map.getOrDefault(action, JS_UNDEFINED)
+    discard pager.timeouts.setTimeout(ttTimeout, fun, 0, [])
+  pager.menu = nil
+  if pager.container != nil:
+    pager.container.queueDraw()
+  pager.draw()
+
+proc openMenu*(pager: Pager; x = -1; y = -1) {.jsfunc.} =
+  let x = if x == -1 and pager.container != nil:
+    pager.container.acursorx
+  else:
+    max(x, 0)
+  let y = if y == -1 and pager.container != nil:
+    pager.container.acursory
+  else:
+    max(y, 0)
+  var options: seq[string] = @[]
+  for (s, _) in MenuMap:
+    options.add(s)
+  pager.menu = newSelect(false, options, @[], x, y, pager.bufWidth,
+    pager.bufHeight, menuFinish, pager)
+
 proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
     bool =
   case event.t
diff --git a/src/local/select.nim b/src/local/select.nim
index 24330257..bf4d5500 100644
--- a/src/local/select.nim
+++ b/src/local/select.nim
@@ -11,17 +11,17 @@ type
 
   Select* = ref object
     options: seq[string]
-    multiple: bool
+    multiple* {.jsget.}: bool
     oselected*: seq[int] # old selection
     selected*: seq[int] # new selection
-    fromy {.jsget.}: int # first index to display
+    fromy* {.jsget.}: int # first index to display
     cursory {.jsget.}: int # selected index
     maxw: int # widest option
     maxh: int # maximum height on screen (yes the naming is dumb)
     # location on screen
     #TODO make this absolute
-    x: int
-    y: int
+    x*: int
+    y*: int
     redraw*: bool
     bpos: seq[int]
     opaque: RootRef
@@ -41,15 +41,22 @@ func dispheight(select: Select): int =
 proc setFromY(select: Select; y: int) =
   select.fromy = max(min(y, select.options.len - select.maxh), 0)
 
-proc setCursorY(select: Select; y: int) =
-  let y = clamp(y, 0, select.options.high)
+func width*(select: Select): int =
+  return select.maxw + 2
+
+func height*(select: Select): int =
+  return select.maxh + 2
+
+proc setCursorY*(select: Select; y: int) =
+  var y = clamp(y, -1, select.options.high)
+  if not select.multiple:
+    y = max(y, 0)
   if select.fromy > y:
     select.setFromY(y)
   if select.fromy + select.dispheight <= y:
     select.setFromY(y - select.dispheight + 1)
   select.cursory = y
   select.queueDraw()
-  assert select.cursory >= select.fromy
 
 proc getCursorX*(select: Select): int =
   if select.cursory == -1:
@@ -93,15 +100,19 @@ proc cursorPrevLink(select: Select) {.jsfunc.} =
 proc cursorNextLink(select: Select) {.jsfunc.} =
   select.cursorDown()
 
+proc gotoLine(select: Select; n: int) {.jsfunc.} =
+  select.setCursorY(n + 1)
+
 proc cancel(select: Select) {.jsfunc.} =
   select.finishImpl(select.opaque, select, srCancel)
 
 proc submit(select: Select) {.jsfunc.} =
+  if not select.multiple:
+    select.selected = @[select.cursory]
   select.finishImpl(select.opaque, select, srSubmit)
 
-proc click(select: Select) {.jsfunc.} =
+proc click*(select: Select) {.jsfunc.} =
   if not select.multiple:
-    select.selected = @[select.cursory]
     select.submit()
   elif select.cursory == -1:
     select.submit()
@@ -118,8 +129,11 @@ proc click(select: Select) {.jsfunc.} =
       select.selected.insert(i, k)
     select.queueDraw()
 
-proc cursorLeft(select: Select) {.jsfunc.} =
-  select.submit()
+proc cursorLeft*(select: Select) {.jsfunc.} =
+  if select.multiple:
+    select.submit()
+  else:
+    select.cancel()
 
 proc cursorRight(select: Select) {.jsfunc.} =
   select.click()
@@ -205,8 +219,7 @@ proc drawBorders(display: var FixedGrid; sx, ex, sy, ey: int;
         display[y * display.width + x].str = " "
         inc x
       else:
-        #x = display[y * display.width + x].str.width()
-        inc x
+        x += display[y * display.width + x].str.width()
   # Draw corners.
   let tl = if upmore: VerticalBar else: CornerTopLeft
   let tr = if upmore: VerticalBar else: CornerTopRight
@@ -275,7 +288,9 @@ proc drawSelect*(select: Select; display: var FixedGrid) =
     var j = 0
     var x = sx
     let dls = y * display.width
-    if k < select.selected.len and select.selected[k] == i:
+    if select.multiple and k < select.selected.len and
+          select.selected[k] == i or
+        not select.multiple and select.getCursorY() == y:
       format.flags.incl(ffReverse)
       inc k
     else:
@@ -298,17 +313,18 @@ proc drawSelect*(select: Select; display: var FixedGrid) =
       display[dls + x].format = format
       inc x
 
-proc windowChange*(select: Select; height: int) =
+proc windowChange*(select: Select; width, 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.y = 0
+    select.y = max(select.maxh - select.options.len, 0)
+  if select.x + select.maxw + 2 > width:
+    select.x = max(width - select.maxw, 0)
   select.setCursorY(select.cursory)
   select.queueDraw()
 
 proc newSelect*(multiple: bool; options: seq[string]; selected: seq[int];
-    x, y, height: int; finishImpl: SelectFinish; opaque: RootRef): Select =
+    x, y, width, height: int; finishImpl: SelectFinish; opaque: RootRef):
+    Select =
   let select = Select(
     multiple: multiple,
     options: options,
@@ -319,12 +335,10 @@ proc newSelect*(multiple: bool; options: seq[string]; selected: seq[int];
     finishImpl: finishImpl,
     opaque: opaque
   )
-  select.windowChange(height)
   for opt in select.options.mitems:
     opt.mnormalize()
     select.maxw = max(select.maxw, opt.width())
-  select.windowChange(height)
-  select.queueDraw()
+  select.windowChange(width, height)
   return select
 
 proc addSelectModule*(ctx: JSContext) =