diff --git a/.gitignore b/.gitignore index 39a4f9c..ae13504 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ compiled/ # Dependency tracking files *.dep +# Other +webview +rktwebview/build diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..6c2232d --- /dev/null +++ b/example/index.html @@ -0,0 +1,17 @@ + + +
+ ++Please press this for something to happen. +
++Some input text: +
+ + diff --git a/example/styles.css b/example/styles.css new file mode 100644 index 0000000..68be6c5 --- /dev/null +++ b/example/styles.css @@ -0,0 +1,4 @@ +body { + font-family: Arial; + font-size: 11pt; +} \ No newline at end of file diff --git a/js/boilerplate.js b/js/boilerplate.js new file mode 100644 index 0000000..a8246dd --- /dev/null +++ b/js/boilerplate.js @@ -0,0 +1,123 @@ + +window._web_wire_evt_queue = []; + +window._web_wire_queue_worker = function() { + if (typeof web_ui_wire_handle_event === 'function') { + while (window._web_wire_evt_queue.length > 0) { + let evt = window._web_wire_evt_queue.shift(); + web_ui_wire_handle_event(JSON.stringify(evt)); + } + } + window.setTimeout(window._web_wire_queue_worker, 5); +}; +window.setTimeout(window._web_wire_queue_worker, 15); + +window._web_wire_put_evt = function(evt) { window._web_wire_evt_queue.push(evt); }; + +window._web_wire_event_info = function(e, id, evt) { + let obj = {}; + if (e == 'input') { + obj['data'] = evt.data; + obj['dataTransfer'] = evt.dataTransfer; + obj['inputType'] = evt.inputType; + obj['isComposing'] = evt.isComposing; + obj['value'] = document.getElementById(id).value; + } else if (e == 'change') { + obj['value'] = document.getElementById(id).value; + } else if (e == 'mousemove' || e == 'mouseover' || e == 'mouseenter' || + e == 'mouseleave' || e == 'click' || e == 'dblclick' || e == 'contextmenu' || + e == 'mousedown' || e == 'mouseup' ) { + obj['altKey'] = evt.altKey; + obj['buttons'] = evt.buttons; + obj['clientX'] = evt.clientX; + obj['clientY'] = evt.clientY; + obj['ctrlKey'] = evt.ctrlKey; + obj['metaKey'] = evt.metaKey; + obj['movementX'] = evt.movementX; + obj['movementY'] = evt.movementY; + obj['screenX'] = evt.screenX; + obj['screenY'] = evt.screenY; + obj['shiftKey'] = evt.shiftKey; + obj['x'] = evt.x; + obj['y'] = evt.y; + } else if (e == 'keydown' || e == 'keyup' || e == 'keypress') { + obj['key'] = evt.key; + obj['code'] = evt.code; + obj['altKey'] = evt.altKey; + obj['ctrlKey'] = evt.ctrlKey; + obj['metaKey'] = evt.metaKey; + obj['repeat'] = evt.repeat; + obj['shiftKey'] = evt.shiftKey; + } + // More events can be added like pointerEvent, clipboardEvent, etc. + return obj; +}; + +window._web_wire_get_evts = function() { + let v = _web_wire_evt_queue; + _web_wire_evt_queue = []; + return JSON.stringify(v); // This needs no extra type info, as it is internally used only +}; + +window._web_wire_bind_evt_ids = function(win_nr, selector, event_kind) { + try { + let nodelist = document.querySelectorAll(selector); + if (nodelist === undefined || nodelist === null) { + return 'json:[]'; + } + let ids = []; + nodelist.forEach(function(el) { + let el_id = el.getAttribute('id'); + let el_tag = el.nodeName; + let el_type = el.getAttribute('type'); + if (el_type === null) { el_type = ''; } + if (el_id !== null) { + el.addEventListener(event_kind, + function(e) { + let obj = {evt: event_kind, id: el_id, selector: selector, window: win_nr, + js_evt: window._web_wire_event_info(event_kind, el_id, e) }; + window._web_wire_put_evt(obj); + } + ); + let info = [ el_id, el_tag, el_type ]; + ids.push(info); + } + }); + return 'json:' + JSON.stringify(ids); + } catch(e) { + return 'json:[]'; + } +}; + +window._web_wire_resize_timeout = false; +window.addEventListener('resize', function() { + clearTimeout(window._web_wire_resize_timeout); + let f = function() { + let obj = { selector: 'global', evt: 'window-resize', h: window.outerWidth, w: window.outerHeight }; + window._web_wire_put_evt(obj); + }; + window._web_wire_resize_timeout = setTimeout(f, 250); +}); + +window._web_wire_x = window.screenX; +window._web_wire_y = window.screenY; +window._web_wire_move_interval = setInterval(function() { + let x = window.screenX; + let y = window.screenY; + if (x != window._web_wire_x || y != window._web_wire_y) { + window._web_wire_x = x; + window._web_wire_y = y; + let obj = { selector: 'global', evt: 'window-move', x: x, y: y }; + window._web_wire_put_evt(obj); + } +}, 500); + +document.addEventListener('readystatechange', event => { + + // When window loaded ( external resources are loaded too- `css`,`src`, etc...) + if (event.target.readyState === "complete") { + let obj = { selector: 'global', evt: 'html-loaded' }; + window._web_wire_put_evt(obj); + } +}); + diff --git a/lib/dll/webview.dll b/lib/dll/webview.dll new file mode 100644 index 0000000..9343d55 Binary files /dev/null and b/lib/dll/webview.dll differ diff --git a/racket-webview-ffi.rkt b/racket-webview-ffi.rkt new file mode 100644 index 0000000..a67e7c3 --- /dev/null +++ b/racket-webview-ffi.rkt @@ -0,0 +1,194 @@ +#lang racket/base + +(require ffi/unsafe + ffi/unsafe/define + ffi/unsafe/atomic + ffi/unsafe/os-thread + racket/async-channel + racket/runtime-path + racket/port + data/queue + json + racket/string + racket/path + ) + +(provide rkt_create_webview + rkt_webview_navigate + rkt_webview_set_html + rkt_webview_valid + rkt_webview_run_js + rkt_webview_call_js + rkt_webview_pending_events + rkt_webview_get_event + rkt_webview_set_event_callback! + rkt_webview_clear_event_callback! + rkt_webview_devtools + rkt_webview_last_reason + rkt_webview_destroy_item + rkt_webview_item_data + ) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; FFI Library +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define lib-type 'release) + +(define-runtime-path lib-dir "lib") + +(define libname (let ((os (system-type 'os*))) + (cond ((eq? os 'windows) (format "rktwebview.dll")) + (else (error (format "OS ~a not supported" os))))) + ) +(set! libname "../rtkwebview/build/Release/rktwebview.dll") +(define webview-lib-file (build-path lib-dir libname)) + +(define webview-lib (ffi-lib webview-lib-file)) +(define-ffi-definer define-rktwebview webview-lib) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Types +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define _rkt_webview_t (_cpointer 'rkt-webview-t)) +(define _char _int8) + +(define _result_t + (_enum '(oke = 0 + error = 1 + ) + ) + ) + +(define _context_t + (_enum '(context-invalid = 0 + bound-event = 1 + window-resize = 2 + window-move = 3 + window-can-close = 4 + window-closed = 5 + ) + ) + ) + + +(define _reason_t + (_enum '( + reason_no_result_yet = -1 + reason_oke = 0 + reason_set_html_failed + reason_set_navigate_failed + reason_eval_js_failed + reason_no_devtools_on_platform + reason_no_delegate_for_context + ))) + +(define-cstruct _rkt_item_t + ( + [context _int] + [data _string*/utf-8] + ) + ) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; ASync apply +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define item-queue (make-queue)) +(define callback-hash (make-hash)) + +(define callback-box (box '())) + +(define (apply-async thunk) + (thunk)) + +(define (std-callback id item) + (enqueue! item-queue (list id item))) + +(thread (λ () + (letrec ((dispatch (λ () + (if (> (queue-length item-queue) 0) + (let* ((entry (dequeue! item-queue)) + (id (car entry)) + (item (cadr entry)) + (cb (hash-ref callback-hash id + (λ args #f))) + (context (rkt_item_t-context item)) + (data (rkt_item_t-data item)) + ) + (cb context data) + (rkt_webview_destroy_item item) + (dispatch) + ) + (begin + (sleep 0.05) + (dispatch)))))) + (dispatch)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Functions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;RKTWEBVIEW_EXPORT rkt_webview_t *rkt_create_webview() +(define-rktwebview rkt_create_webview + (_fun -> _rkt_webview_t)) + +;RKTWEBVIEW_EXPORT result_t rkt_navigate(rkt_webview_t *wv, const char *url) +(define-rktwebview rkt_webview_navigate + (_fun _rkt_webview_t _string/utf-8 -> _result_t)) + +;RKTWEBVIEW_EXPORT result_t rkt_set_html(rkt_webview_t *wv, const char *html) +(define-rktwebview rkt_webview_set_html + (_fun _rkt_webview_t _string/utf-8 -> _result_t)) + +;RKTWEBVIEW_EXPORT bool rkt_webview_valid(rkt_webview_t *handle) +(define-rktwebview rkt_webview_valid + (_fun _rkt_webview_t -> _int)) + +;RKTWEBVIEW_EXPORT result_t rkt_run_js(rkt_webview_t *handle, const char *js); +(define-rktwebview rkt_webview_run_js + (_fun _rkt_webview_t _string/utf-8 -> _result_t)) + +;RKTWEBVIEW_EXPORT char *rkt_webview_call_js(rkt_webview_t *handle, const char *js); +;(define-rktwebview rkt_webview_call_js +; (_fun _rkt_webview_t _string/utf-8 -> _rkt_item_t)) + +;RKTWEBVIEW_EXPORT int rkt_webview_pending_events(rkt_webview_t *wv); +(define-rktwebview rkt_webview_pending_events + (_fun _rkt_webview_t -> _int)) + +;RKTWEBVIEW_EXPORT item_t rkt_webview_get_event(rkt_webview_t *wv); +(define-rktwebview rkt_webview_get_event + (_fun _rkt_webview_t -> _rkt_item_t)) + +;RKTWEBVIEW_EXPORT void rkt_webview_register_queue_callback(rkt_webview_t *wv, void(*cb)(item_t item)); +(define-rktwebview rkt_webview_register_queue_callback + (_fun _rkt_webview_t _int + (_fun #:keep callback-box #:async-apply apply-async + _int _rkt_item_t -> _void) -> _void)) + +(define (rkt_webview_set_event_callback! wv id cb) + (hash-set! callback-hash id cb) + (rkt_webview_register_queue_callback wv id std-callback) + ) + +(define (rkt_webview_clear_event_callback! wv id) + (hash-remove! callback-hash id) + (rkt_webview_register_queue_callback wv #f)) + +;RKTWEBVIEW_EXPORT void (item_t item) +(define-rktwebview rkt_webview_destroy_item + (_fun _rkt_item_t -> _void)) + +;RKTWEBVIEW_EXPORT result_t rkt_webview_devtools(rkt_webview_t *wv) +(define-rktwebview rkt_webview_devtools + (_fun _rkt_webview_t -> _result_t)) + +;RKTWEBVIEW_EXPORT reason_t rkt_webview_last_reason(rkt_webview_t *wv) +(define-rktwebview rkt_webview_last_reason + (_fun _rkt_webview_t -> _reason_t)) + +(define (rkt_webview_item_data item) + (rkt_item_t-data item)) + diff --git a/racket-webview.rkt b/racket-webview.rkt new file mode 100644 index 0000000..e1d1582 --- /dev/null +++ b/racket-webview.rkt @@ -0,0 +1,196 @@ +#lang racket/base + +(require "racket-webview-ffi.rkt" + "utils.rkt" + web-server/servlet + web-server/servlet-env + web-server/http + net/url + racket/runtime-path + racket/file + racket/string + racket/path + racket/port + json + ) + +(provide webview-create + webview-devtools + webview-has-events? + webview-event-count + webview-get-event + webview-set-event-callback! + webview-clear-event-callback! + webview-bind + webview-run-js + ;webview-call-js + ) + +(define current-servlet-port 8083) +(define current-window-nr 1) + +(define-runtime-path js-path "js") + +(define (default-boilerplate-js) + (let ((file (build-path js-path "boilerplate.js"))) + (file->string file))) + +(define-struct wv + (handle port window-nr + [file-getter #:mutable] + [boilerplate-js #:mutable] + [webserver-thread #:mutable])) + +(define (process-html wv-handle path out) + (let ((html (file->string path)) + (boilerplate-js (wv-boilerplate-js wv-handle))) + (set! html (string-replace html "" + (string-append "" "\n" + "" "\n"))) + (display html out))) + +(define (process-file wv-handle ext path out) + (let ((content (file->bytes path))) + (write bytes out))) + +(define (web-serve wv-handle req) + (let* ((path (url->string (request-uri req))) + (file-getter (wv-file-getter wv-handle))) + (let* ((file-to-serve (build-path (file-getter path))) + (ext-bytes (path-get-extension file-to-serve)) + (ext (if (eq? ext-bytes #f) #f + (string->symbol (string-downcase (substring (bytes->string/utf-8 ext-bytes) 1))))) + ) + (if (file-exists? file-to-serve) + (response/output + (λ (out) + (if (or (eq? ext 'html) (eq? ext 'htm)) + (process-html wv-handle file-to-serve out) + (process-file wv-handle ext file-to-serve out)) + )) + (response/output + #:code 404 + (λ (out) + (displayln (format "~a not found" path) out))) + ) + ) + ) + ) + +(define (start-web-server h) + (thread (λ () + (serve/servlet + (λ (req) (web-serve h req)) + #:listen-ip "127.0.0.1" + #:port (wv-port h) + #:command-line? #t + #:servlet-path "" + #:stateless? #t + ;#:launch-browser #f + #:servlet-regexp #rx"")))) + +(define (webview-create file-getter #:boilerplate-js [bj (default-boilerplate-js)]) + (let* ((wv (rkt_create_webview)) + (h (make-wv wv current-servlet-port current-window-nr file-getter bj #f)) + (base-req (format "http://127.0.0.1:~a" + (wv-port h))) + ) + (set-wv-webserver-thread! h (start-web-server h)) + (rkt_webview_navigate (wv-handle h) base-req) + (set! current-servlet-port (+ current-servlet-port 1)) + (set! current-window-nr (+ current-window-nr 1)) + h)) + +(define (webview-devtools wv) + (rkt_webview_devtools (wv-handle wv))) + +(define (webview-parse-event evt) + (let ((wv-d0 (with-input-from-string evt read-json))) + ;(displayln wv-d0) + (let ((wv-d1 (with-input-from-string (hash-ref wv-d0 'data) read-json))) + ;(displayln wv-d1) + (let ((wv-d2 (with-input-from-string (car wv-d1) read-json))) + ;(displayln wv-d2) + wv-d2)))) + +(define (webview-set-event-callback! wv cb) + (rkt_webview_set_event_callback! (wv-handle wv) (wv-window-nr wv) + (λ (context data) + (let ((e (webview-parse-event data))) + (cb context e))))) + +(define (webview-clear-event-callback! wv) + (rkt_webview_clear_event_callback! (wv-handle wv) (wv-window-nr wv))) + + +(define (webview-get-event wv) + (if (> (rkt_webview_pending_events (wv-handle wv)) 0) + (let ((item (rkt_webview_get_event (wv-handle wv)))) + (let ((data (rkt_webview_item_data item))) + (rkt_webview_destroy_item item) + (let ((wv-d0 (with-input-from-string data read-json))) + ;(displayln wv-d0) + (let ((wv-d1 (with-input-from-string (hash-ref wv-d0 'data) read-json))) + ;(displayln wv-d1) + (let ((wv-d2 (with-input-from-string (car wv-d1) read-json))) + ;(displayln wv-d2) + wv-d2 + ) + ) + ) + ) + ) + #f) + ) + +(define (webview-has-events? wv) + (>= (rkt_webview_pending_events (wv-handle wv)) 1) + ) + +(define (webview-event-count wv) + (rkt_webview_pending_events (wv-handle wv))) + +(define (webview-bind wv selector event) + (let ((sel (if (symbol? selector) + (format "#~a" selector) + selector)) + (evt (format "~a" event))) + (rkt_webview_run_js (wv-handle wv) + (format "window._web_wire_bind_evt_ids(~a, '~a', '~a')" + (wv-window-nr wv) sel evt)))) + +(define (webview-run-js wv js) + (rkt_webview_run_js (wv-handle wv) js)) + +(define (webview-call-js wv js) + (let ((result (rkt_webview_call_js (wv-handle wv) js))) + result)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; testing +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define-runtime-path example-path "example") + +(define (file-getter file) + (displayln (format "file-getter: ~a" file)) + (let ((f (if (string=? file "/") "index.html" file))) + (when (string-prefix? f "/") + (set! f (substring f 1))) + (let ((p (build-path example-path f))) + (displayln p) + p))) + +(define (test) + (let ((h (webview-create file-getter))) + (displayln (webview-has-events? h)) + + (while (not (webview-has-events? h)) + (displayln "Waiting...") + (sleep 1)) + (let ((evt (webview-get-event h))) + (when (string=? (hash-ref evt 'evt) "html-loaded") + (webview-bind h "button" "click"))) + h)) \ No newline at end of file diff --git a/rtkwebview/CMakeLists.txt b/rtkwebview/CMakeLists.txt new file mode 100644 index 0000000..eb5b5c7 --- /dev/null +++ b/rtkwebview/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.16) + +project(rktwebview LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include_directories(../webview/core/include) +include_directories(../webview/build/Release/_deps/microsoft_web_webview2-src/build/native/include) +link_directories(../webview/build/Release/core) +link_directories(build/Release) + +add_library(rktwebview SHARED + rktwebview_global.h + rktwebview.cpp + rktwebview.h + + + json.h + json.cpp + main.cpp +) + +add_executable(rktwebview_test + main.cpp +) + + +target_link_libraries(rktwebview webview_static) +target_link_libraries(rktwebview_test rktwebview) + +target_compile_definitions(rktwebview PRIVATE RKTWEBVIEW_LIBRARY)