about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-08-31 22:52:28 +0200
committerbptato <nincsnevem662@gmail.com>2023-08-31 22:52:28 +0200
commit3a7d189d88747d119ab475c96e067b86c4cd2957 (patch)
tree044978abafa0f2eb288bdaf6c0e840b530fadd2f
parent40aef0b310bb84628b759bdb3f17ff7229afc634 (diff)
downloadchawan-3a7d189d88747d119ab475c96e067b86c4cd2957.tar.gz
buffer: basic click event support
Mostly a proof of concept. Just bubble it unconditionally for now
and never prevent default.

Also, fix fromJSFunction by Dup'ing its closure JSValue.
-rw-r--r--src/buffer/buffer.nim52
-rw-r--r--src/html/event.nim22
-rw-r--r--src/js/fromjs.nim17
-rw-r--r--src/js/javascript.nim2
-rw-r--r--src/js/tojs.nim4
5 files changed, 81 insertions, 16 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index c53641e9..0b3c5d95 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -20,6 +20,7 @@ import css/values
 import html/chadombuilder
 import html/dom
 import html/env
+import html/event
 import img/png
 import io/connecterror
 import io/loader
@@ -111,6 +112,7 @@ type
     tasks: array[BufferCommand, int] #TODO this should have arguments
     savetask: bool
     hovertext: array[HoverType, string]
+    estream: Stream # error stream
 
   InterfaceOpaque = ref object
     stream: Stream
@@ -329,10 +331,23 @@ func getClickHover(styledNode: StyledNode): string =
       return "<textarea>"
     else: discard
 
-func getCursorClickable(buffer: Buffer, cursorx, cursory: int): Element =
+func getCursorStyledNode(buffer: Buffer, cursorx, cursory: int): StyledNode =
   let i = buffer.lines[cursory].findFormatN(cursorx) - 1
   if i >= 0:
-    return buffer.lines[cursory].formats[i].node.getClickable()
+    return buffer.lines[cursory].formats[i].node
+
+func getCursorElement(buffer: Buffer, cursorx, cursory: int): Element =
+  let styledNode = buffer.getCursorStyledNode(cursorx, cursory)
+  if styledNode == nil or styledNode.node == nil:
+    return nil
+  if styledNode.t == STYLED_ELEMENT:
+    return Element(styledNode.node)
+  return styledNode.node.parentElement
+
+func getCursorClickable(buffer: Buffer, cursorx, cursory: int): Element =
+  let styledNode = buffer.getCursorStyledNode(cursorx, cursory)
+  if styledNode != nil:
+    return styledNode.getClickable()
 
 func cursorBytes(buffer: Buffer, y: int, cc: int): int =
   let line = buffer.lines[y].str
@@ -1176,11 +1191,39 @@ proc click(buffer: Buffer, clickable: Element): ClickResult =
   else:
     result.repaint = buffer.restoreFocus()
 
+proc dispatchEvent(buffer: Buffer, ctype: string, elem: Element): bool =
+  var called = false
+  for a in elem.branch:
+    var stop = false
+    for el in a.eventListeners:
+      if el.ctype == "click":
+        let event = newEvent(buffer.window.jsctx, ctype, elem, a)
+        let e = el.callback(event)
+        called = true
+        if e.isErr:
+          buffer.window.jsctx.writeException(buffer.estream)
+        if FLAG_STOP_IMMEDIATE_PROPAGATION in event.flags:
+          stop = true
+          break
+        if FLAG_STOP_PROPAGATION in event.flags:
+          stop = true
+    if stop:
+      break
+  return called
+
 proc click*(buffer: Buffer, cursorx, cursory: int): ClickResult {.proxy.} =
   if buffer.lines.len <= cursory: return
+  var called = false
+  if buffer.config.scripting:
+    let elem = buffer.getCursorElement(cursorx, cursory)
+    if buffer.dispatchEvent("click", elem):
+      buffer.do_reshape()
   let clickable = buffer.getCursorClickable(cursorx, cursory)
   if clickable != nil:
-    return buffer.click(clickable)
+    var res = buffer.click(clickable)
+    res.repaint = called
+    return res
+  return ClickResult(repaint: called)
 
 proc select*(buffer: Buffer, selected: seq[int]): ClickResult {.proxy.} =
   if buffer.document.focus != nil and
@@ -1347,7 +1390,7 @@ proc runBuffer(buffer: Buffer, rfd: int) =
         buffer.handleError(event.fd, event.errorCode)
       if not buffer.alive:
         break
-      if Event.Timer in event.events:
+      if selectors.Event.Timer in event.events:
         assert buffer.window != nil
         assert buffer.window.timeouts.runTimeoutFd(event.fd)
         buffer.window.runJSJobs()
@@ -1379,6 +1422,7 @@ proc launchBuffer*(config: BufferConfig, source: BufferSource,
     buffer.window = newWindow(buffer.config.scripting, buffer.selector,
       buffer.attrs, proc(url: URL) = buffer.navigate(url), some(buffer.loader))
   let socks = connectSocketStream(mainproc, false)
+  buffer.estream = newFileStream(stderr)
   socks.swrite(getpid())
   buffer.pstream = socks
   let rfd = int(socks.source.getFd())
diff --git a/src/html/event.nim b/src/html/event.nim
index 5f841fd1..a6ab2e36 100644
--- a/src/html/event.nim
+++ b/src/html/event.nim
@@ -15,7 +15,7 @@ type
     AT_TARGET = 2u16
     BUBBLING_PHASE = 3u16
 
-  EventFlag = enum
+  EventFlag* = enum
     FLAG_STOP_PROPAGATION
     FLAG_STOP_IMMEDIATE_PROPAGATION
     FLAG_CANCELED
@@ -26,14 +26,14 @@ type
 
   Event* = ref object of RootObj
     ctype {.jsget: "type".}: string
-    target {.jsget.}: EventTarget
+    target* {.jsget.}: EventTarget
     currentTarget {.jsget.}: EventTarget
     eventPhase {.jsget.}: uint16
     bubbles {.jsget.}: bool
     cancelable {.jsget.}: bool
     #TODO DOMHighResTimeStamp?
     timeStamp {.jsget.}: float64
-    flags: set[EventFlag]
+    flags*: set[EventFlag]
     isTrusted {.jsufget.}: bool
 
   CustomEvent* = ref object of Event
@@ -47,9 +47,9 @@ type
 
   EventListenerCallback = proc (event: Event): Err[JSError]
 
-  EventListener = ref object
-    ctype: string
-    callback: EventListenerCallback
+  EventListener* = ref object
+    ctype*: string
+    callback*: EventListenerCallback
     capture: bool
     passive: Opt[bool]
     once: bool
@@ -93,6 +93,13 @@ proc newEvent(ctx: JSContext, ctype: string, eventInitDict = JS_UNDEFINED):
   event.ctype = ctype
   return ok(event)
 
+proc newEvent*(ctx: JSContext, ctype: string, target, currentTarget: EventTarget): Event =
+  return Event(
+    ctype: ctype,
+    target: target,
+    currentTarget: currentTarget
+  )
+
 proc initialize(this: Event, ctype: string, bubbles, cancelable: bool) =
   this.flags.incl(FLAG_INITIALIZED)
   this.isTrusted = false
@@ -231,10 +238,11 @@ proc flattenMore(ctx: JSContext, options: JSValue):
       passive = opt(x.get)
   return (capture, once, passive)
 
-proc addEventListener(ctx: JSContext, eventTarget: EventTarget,
+proc addEventListener(ctx: JSContext, eventTarget: EventTarget, ctype: string,
     callback: EventListenerCallback, options = JS_UNDEFINED) {.jsfunc.} =
   let (capture, once, passive) = flattenMore(ctx, options)
   let listener = EventListener(
+    ctype: ctype,
     capture: capture,
     passive: passive,
     once: once,
diff --git a/src/js/fromjs.nim b/src/js/fromjs.nim
index c7e27110..310550e4 100644
--- a/src/js/fromjs.nim
+++ b/src/js/fromjs.nim
@@ -242,6 +242,8 @@ proc fromJSSet[T](ctx: JSContext, val: JSValue): Opt[set[T]] =
   return ok(s)
 
 proc fromJSTable[A, B](ctx: JSContext, val: JSValue): JSResult[Table[A, B]] =
+  if not JS_IsObject(val):
+    return err(newTypeError("object expected"))
   var ptab: ptr UncheckedArray[JSPropertyEnum]
   var plen: uint32
   let flags = cint(JS_GPN_STRING_MASK)
@@ -267,14 +269,13 @@ proc fromJSTable[A, B](ctx: JSContext, val: JSValue): JSResult[Table[A, B]] =
 #TODO varargs
 proc fromJSFunction1*[T, U](ctx: JSContext, val: JSValue):
     proc(x: U): JSResult[T] =
+  #TODO this leaks memory!
   let dupval = JS_DupValue(ctx, JS_DupValue(ctx, val)) # save
   return proc(x: U): JSResult[T] =
     var arg1 = toJS(ctx, x)
     #TODO exceptions?
     let ret = JS_Call(ctx, dupval, JS_UNDEFINED, 1, addr arg1)
-    when T isnot void:
-      result = fromJS[T](ctx, ret)
-    JS_FreeValue(ctx, dupval)
+    result = fromJS[T](ctx, ret)
     JS_FreeValue(ctx, ret)
 
 proc isErrType(rt: NimNode): bool =
@@ -312,6 +313,8 @@ macro unpackArg0(f: typed) =
 proc fromJSFunction[T](ctx: JSContext, val: JSValue):
     JSResult[T] =
   #TODO all args...
+  if not JS_IsFunction(ctx, val):
+    return err(newTypeError("function expected"))
   return ok(
     fromJSFunction1[
       typeof(unpackReturnType(T)),
@@ -394,6 +397,12 @@ proc fromJSPObj0(ctx: JSContext, val: JSValue, t: string):
 proc fromJSObject[T: ref object](ctx: JSContext, val: JSValue): JSResult[T] =
   return ok(cast[T](?fromJSPObj0(ctx, val, $T)))
 
+proc fromJSVoid(ctx: JSContext, val: JSValue): JSResult[void] =
+  if JS_IsException(val):
+    #TODO maybe wrap or something
+    return err()
+  return ok()
+
 type FromJSAllowedT = (object and not (Result|Option|Table|JSValue))
 
 proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T] =
@@ -430,6 +439,8 @@ proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T] =
     return ok(val)
   elif T is ref object:
     return fromJSObject[T](ctx, val)
+  elif T is void:
+    return fromJSVoid(ctx, val)
   elif compiles(fromJS2(ctx, val, result)):
     fromJS2(ctx, val, result)
   else:
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index 06d1a577..b1d16734 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -170,7 +170,7 @@ proc setInterruptHandler*(rt: JSRuntime, cb: JSInterruptHandler, opaque: pointer
 proc writeException*(ctx: JSContext, s: Stream) =
   let ex = JS_GetException(ctx)
   let str = fromJS[string](ctx, ex)
-  if str.issome:
+  if str.isSome:
     s.write(str.get & '\n')
   let stack = JS_GetPropertyStr(ctx, ex, cstring("stack"));
   if not JS_IsUndefined(stack):
diff --git a/src/js/tojs.nim b/src/js/tojs.nim
index 4cde2e15..050af5e6 100644
--- a/src/js/tojs.nim
+++ b/src/js/tojs.nim
@@ -243,7 +243,9 @@ proc toJS*(ctx: JSContext, err: JSError): JSValue =
   if JS_IsException(msg):
     return msg
   let ctor = ctx.getOpaque().err_ctors[err.e]
-  return JS_CallConstructor(ctx, ctor, 1, addr msg)
+  let ret = JS_CallConstructor(ctx, ctor, 1, addr msg)
+  JS_FreeValue(ctx, msg)
+  return ret
 
 proc toJS*(ctx: JSContext, f: JSCFunction): JSValue =
   return JS_NewCFunction(ctx, f, cstring"", 0)