tauri.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. //
  2. // Package tauri implements Go bindings to Tauri UI.
  3. //
  4. // Bindings closely repeat the C APIs and include both, a simplified
  5. // single-function API to just open a full-screen webview window, and a more
  6. // advanced and featureful set of APIs, including Go-to-JavaScript bindings.
  7. //
  8. // The library uses gtk-webkit, Cocoa/Webkit and MSHTML (IE8..11) as a browser
  9. // engine and supports Linux, MacOS and Windows 7..10 respectively.
  10. //
  11. package tauri
  12. /*
  13. #cgo linux openbsd freebsd CFLAGS: -DWEBVIEW_GTK=1
  14. #cgo linux openbsd freebsd pkg-config: gtk+-3.0 webkit2gtk-4.0
  15. #cgo windows CFLAGS: -DWEBVIEW_WINAPI=1
  16. #cgo windows LDFLAGS: -lole32 -lcomctl32 -loleaut32 -luuid -lgdi32
  17. #cgo darwin CFLAGS: -DWEBVIEW_COCOA=1
  18. #cgo darwin LDFLAGS: -framework WebKit
  19. #include <stdlib.h>
  20. #include <stdint.h>
  21. #define WEBVIEW_STATIC
  22. #define WEBVIEW_IMPLEMENTATION
  23. #include "webview.h"
  24. extern void _webviewExternalInvokeCallback(void *, void *);
  25. static inline void CgoWebViewFree(void *w) {
  26. free((void *)((struct webview *)w)->title);
  27. free((void *)((struct webview *)w)->url);
  28. free(w);
  29. }
  30. static inline void *CgoWebViewCreate(int width, int height, char *title, char *url, int resizable, int debug) {
  31. struct webview *w = (struct webview *) calloc(1, sizeof(*w));
  32. w->width = width;
  33. w->height = height;
  34. w->title = title;
  35. w->url = url;
  36. w->resizable = resizable;
  37. w->debug = debug;
  38. w->external_invoke_cb = (webview_external_invoke_cb_t) _webviewExternalInvokeCallback;
  39. if (webview_init(w) != 0) {
  40. CgoWebViewFree(w);
  41. return NULL;
  42. }
  43. return (void *)w;
  44. }
  45. static inline int CgoWebViewLoop(void *w, int blocking) {
  46. return webview_loop((struct webview *)w, blocking);
  47. }
  48. static inline void CgoWebViewTerminate(void *w) {
  49. webview_terminate((struct webview *)w);
  50. }
  51. static inline void CgoWebViewExit(void *w) {
  52. webview_exit((struct webview *)w);
  53. }
  54. static inline void CgoWebViewSetTitle(void *w, char *title) {
  55. webview_set_title((struct webview *)w, title);
  56. }
  57. static inline void CgoWebViewSetFullscreen(void *w, int fullscreen) {
  58. webview_set_fullscreen((struct webview *)w, fullscreen);
  59. }
  60. static inline void CgoWebViewSetColor(void *w, uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
  61. webview_set_color((struct webview *)w, r, g, b, a);
  62. }
  63. static inline void CgoDialog(void *w, int dlgtype, int flags,
  64. char *title, char *arg, char *res, size_t ressz) {
  65. webview_dialog(w, dlgtype, flags,
  66. (const char*)title, (const char*) arg, res, ressz);
  67. }
  68. static inline int CgoWebViewEval(void *w, char *js) {
  69. return webview_eval((struct webview *)w, js);
  70. }
  71. static inline void CgoWebViewInjectCSS(void *w, char *css) {
  72. webview_inject_css((struct webview *)w, css);
  73. }
  74. extern void _webviewDispatchGoCallback(void *);
  75. static inline void _webview_dispatch_cb(struct webview *w, void *arg) {
  76. _webviewDispatchGoCallback(arg);
  77. }
  78. static inline void CgoWebViewDispatch(void *w, uintptr_t arg) {
  79. webview_dispatch((struct webview *)w, _webview_dispatch_cb, (void *)arg);
  80. }
  81. */
  82. import "C"
  83. import (
  84. "bytes"
  85. "encoding/json"
  86. "errors"
  87. "fmt"
  88. "html/template"
  89. "log"
  90. "reflect"
  91. "runtime"
  92. "sync"
  93. "unicode"
  94. "unsafe"
  95. )
  96. func init() {
  97. // Ensure that main.main is called from the main thread
  98. runtime.LockOSThread()
  99. }
  100. // Open is a simplified API to open a single native window with a full-size webview in
  101. // it. It can be helpful if you want to communicate with the core app using XHR
  102. // or WebSockets (as opposed to using JavaScript bindings).
  103. //
  104. // Window appearance can be customized using title, width, height and resizable parameters.
  105. // URL must be provided and can user either a http or https protocol, or be a
  106. // local file:// URL. On some platforms "data:" URLs are also supported
  107. // (Linux/MacOS).
  108. func Open(title, url string, w, h int, resizable bool) error {
  109. titleStr := C.CString(title)
  110. defer C.free(unsafe.Pointer(titleStr))
  111. urlStr := C.CString(url)
  112. defer C.free(unsafe.Pointer(urlStr))
  113. resize := C.int(0)
  114. if resizable {
  115. resize = C.int(1)
  116. }
  117. r := C.webview(titleStr, urlStr, C.int(w), C.int(h), resize)
  118. if r != 0 {
  119. return errors.New("failed to create webview")
  120. }
  121. return nil
  122. }
  123. // Debug prints a debug string using stderr on Linux/BSD, NSLog on MacOS and
  124. // OutputDebugString on Windows.
  125. func Debug(a ...interface{}) {
  126. s := C.CString(fmt.Sprint(a...))
  127. defer C.free(unsafe.Pointer(s))
  128. C.webview_print_log(s)
  129. }
  130. // Debugf prints a formatted debug string using stderr on Linux/BSD, NSLog on
  131. // MacOS and OutputDebugString on Windows.
  132. func Debugf(format string, a ...interface{}) {
  133. s := C.CString(fmt.Sprintf(format, a...))
  134. defer C.free(unsafe.Pointer(s))
  135. C.webview_print_log(s)
  136. }
  137. // ExternalInvokeCallbackFunc is a function type that is called every time
  138. // "window.external.invoke()" is called from JavaScript. Data is the only
  139. // obligatory string parameter passed into the "invoke(data)" function from
  140. // JavaScript. To pass more complex data serialized JSON or base64 encoded
  141. // string can be used.
  142. type ExternalInvokeCallbackFunc func(w WebView, data string)
  143. // Settings is a set of parameters to customize the initial WebView appearance
  144. // and behavior. It is passed into the webview.New() constructor.
  145. type Settings struct {
  146. // WebView main window title
  147. Title string
  148. // URL to open in a webview
  149. URL string
  150. // Window width in pixels
  151. Width int
  152. // Window height in pixels
  153. Height int
  154. // Allows/disallows window resizing
  155. Resizable bool
  156. // Enable debugging tools (Linux/BSD/MacOS, on Windows use Firebug)
  157. Debug bool
  158. // A callback that is executed when JavaScript calls "window.external.invoke()"
  159. ExternalInvokeCallback ExternalInvokeCallbackFunc
  160. }
  161. // WebView is an interface that wraps the basic methods for controlling the UI
  162. // loop, handling multithreading and providing JavaScript bindings.
  163. type WebView interface {
  164. // Run() starts the main UI loop until the user closes the webview window or
  165. // Terminate() is called.
  166. Run()
  167. // Loop() runs a single iteration of the main UI.
  168. Loop(blocking bool) bool
  169. // SetTitle() changes window title. This method must be called from the main
  170. // thread only. See Dispatch() for more details.
  171. SetTitle(title string)
  172. // SetFullscreen() controls window full-screen mode. This method must be
  173. // called from the main thread only. See Dispatch() for more details.
  174. SetFullscreen(fullscreen bool)
  175. // SetColor() changes window background color. This method must be called from
  176. // the main thread only. See Dispatch() for more details.
  177. SetColor(r, g, b, a uint8)
  178. // Eval() evaluates an arbitrary JS code inside the webview. This method must
  179. // be called from the main thread only. See Dispatch() for more details.
  180. Eval(js string) error
  181. // InjectJS() injects an arbitrary block of CSS code using the JS API. This
  182. // method must be called from the main thread only. See Dispatch() for more
  183. // details.
  184. InjectCSS(css string)
  185. // Dialog() opens a system dialog of the given type and title. String
  186. // argument can be provided for certain dialogs, such as alert boxes. For
  187. // alert boxes argument is a message inside the dialog box.
  188. Dialog(dlgType DialogType, flags int, title string, arg string) string
  189. // Terminate() breaks the main UI loop. This method must be called from the main thread
  190. // only. See Dispatch() for more details.
  191. Terminate()
  192. // Dispatch() schedules some arbitrary function to be executed on the main UI
  193. // thread. This may be helpful if you want to run some JavaScript from
  194. // background threads/goroutines, or to terminate the app.
  195. Dispatch(func())
  196. // Exit() closes the window and cleans up the resources. Use Terminate() to
  197. // forcefully break out of the main UI loop.
  198. Exit()
  199. // Bind() registers a binding between a given value and a JavaScript object with the
  200. // given name. A value must be a struct or a struct pointer. All methods are
  201. // available under their camel-case names, starting with a lower-case letter,
  202. // e.g. "FooBar" becomes "fooBar" in JavaScript.
  203. // Bind() returns a function that updates JavaScript object with the current
  204. // Go value. You only need to call it if you change Go value asynchronously.
  205. Bind(name string, v interface{}) (sync func(), err error)
  206. }
  207. // DialogType is an enumeration of all supported system dialog types
  208. type DialogType int
  209. const (
  210. // DialogTypeOpen is a system file open dialog
  211. DialogTypeOpen DialogType = iota
  212. // DialogTypeSave is a system file save dialog
  213. DialogTypeSave
  214. // DialogTypeAlert is a system alert dialog (message box)
  215. DialogTypeAlert
  216. )
  217. const (
  218. // DialogFlagFile is a normal file picker dialog
  219. DialogFlagFile = C.WEBVIEW_DIALOG_FLAG_FILE
  220. // DialogFlagDirectory is an open directory dialog
  221. DialogFlagDirectory = C.WEBVIEW_DIALOG_FLAG_DIRECTORY
  222. // DialogFlagInfo is an info alert dialog
  223. DialogFlagInfo = C.WEBVIEW_DIALOG_FLAG_INFO
  224. // DialogFlagWarning is a warning alert dialog
  225. DialogFlagWarning = C.WEBVIEW_DIALOG_FLAG_WARNING
  226. // DialogFlagError is an error dialog
  227. DialogFlagError = C.WEBVIEW_DIALOG_FLAG_ERROR
  228. )
  229. var (
  230. m sync.Mutex
  231. index uintptr
  232. fns = map[uintptr]func(){}
  233. cbs = map[WebView]ExternalInvokeCallbackFunc{}
  234. )
  235. type webview struct {
  236. w unsafe.Pointer
  237. }
  238. var _ WebView = &webview{}
  239. func boolToInt(b bool) int {
  240. if b {
  241. return 1
  242. }
  243. return 0
  244. }
  245. // New creates and opens a new webview window using the given settings. The
  246. // returned object implements the WebView interface. This function returns nil
  247. // if a window can not be created.
  248. func New(settings Settings) WebView {
  249. if settings.Width == 0 {
  250. settings.Width = 640
  251. }
  252. if settings.Height == 0 {
  253. settings.Height = 480
  254. }
  255. if settings.Title == "" {
  256. settings.Title = "WebView"
  257. }
  258. w := &webview{}
  259. w.w = C.CgoWebViewCreate(C.int(settings.Width), C.int(settings.Height),
  260. C.CString(settings.Title), C.CString(settings.URL),
  261. C.int(boolToInt(settings.Resizable)), C.int(boolToInt(settings.Debug)))
  262. m.Lock()
  263. if settings.ExternalInvokeCallback != nil {
  264. cbs[w] = settings.ExternalInvokeCallback
  265. } else {
  266. cbs[w] = func(w WebView, data string) {}
  267. }
  268. m.Unlock()
  269. return w
  270. }
  271. func (w *webview) Loop(blocking bool) bool {
  272. block := C.int(0)
  273. if blocking {
  274. block = 1
  275. }
  276. return C.CgoWebViewLoop(w.w, block) == 0
  277. }
  278. func (w *webview) Run() {
  279. for w.Loop(true) {
  280. }
  281. }
  282. func (w *webview) Exit() {
  283. C.CgoWebViewExit(w.w)
  284. }
  285. func (w *webview) Dispatch(f func()) {
  286. m.Lock()
  287. for ; fns[index] != nil; index++ {
  288. }
  289. fns[index] = f
  290. m.Unlock()
  291. C.CgoWebViewDispatch(w.w, C.uintptr_t(index))
  292. }
  293. func (w *webview) SetTitle(title string) {
  294. p := C.CString(title)
  295. defer C.free(unsafe.Pointer(p))
  296. C.CgoWebViewSetTitle(w.w, p)
  297. }
  298. func (w *webview) SetColor(r, g, b, a uint8) {
  299. C.CgoWebViewSetColor(w.w, C.uint8_t(r), C.uint8_t(g), C.uint8_t(b), C.uint8_t(a))
  300. }
  301. func (w *webview) SetFullscreen(fullscreen bool) {
  302. C.CgoWebViewSetFullscreen(w.w, C.int(boolToInt(fullscreen)))
  303. }
  304. func (w *webview) Dialog(dlgType DialogType, flags int, title string, arg string) string {
  305. const maxPath = 4096
  306. titlePtr := C.CString(title)
  307. defer C.free(unsafe.Pointer(titlePtr))
  308. argPtr := C.CString(arg)
  309. defer C.free(unsafe.Pointer(argPtr))
  310. resultPtr := (*C.char)(C.calloc((C.size_t)(unsafe.Sizeof((*C.char)(nil))), (C.size_t)(maxPath)))
  311. defer C.free(unsafe.Pointer(resultPtr))
  312. C.CgoDialog(w.w, C.int(dlgType), C.int(flags), titlePtr,
  313. argPtr, resultPtr, C.size_t(maxPath))
  314. return C.GoString(resultPtr)
  315. }
  316. func (w *webview) Eval(js string) error {
  317. p := C.CString(js)
  318. defer C.free(unsafe.Pointer(p))
  319. switch C.CgoWebViewEval(w.w, p) {
  320. case -1:
  321. return errors.New("evaluation failed")
  322. }
  323. return nil
  324. }
  325. func (w *webview) InjectCSS(css string) {
  326. p := C.CString(css)
  327. defer C.free(unsafe.Pointer(p))
  328. C.CgoWebViewInjectCSS(w.w, p)
  329. }
  330. func (w *webview) Terminate() {
  331. C.CgoWebViewTerminate(w.w)
  332. }
  333. //export _webviewDispatchGoCallback
  334. func _webviewDispatchGoCallback(index unsafe.Pointer) {
  335. var f func()
  336. m.Lock()
  337. f = fns[uintptr(index)]
  338. delete(fns, uintptr(index))
  339. m.Unlock()
  340. f()
  341. }
  342. //export _webviewExternalInvokeCallback
  343. func _webviewExternalInvokeCallback(w unsafe.Pointer, data unsafe.Pointer) {
  344. m.Lock()
  345. var (
  346. cb ExternalInvokeCallbackFunc
  347. wv WebView
  348. )
  349. for wv, cb = range cbs {
  350. if wv.(*webview).w == w {
  351. break
  352. }
  353. }
  354. m.Unlock()
  355. cb(wv, C.GoString((*C.char)(data)))
  356. }
  357. var bindTmpl = template.Must(template.New("").Parse(`
  358. if (typeof {{.Name}} === 'undefined') {
  359. {{.Name}} = {};
  360. }
  361. {{ range .Methods }}
  362. {{$.Name}}.{{.JSName}} = function({{.JSArgs}}) {
  363. window.external.invoke(JSON.stringify({scope: "{{$.Name}}", method: "{{.Name}}", params: [{{.JSArgs}}]}));
  364. };
  365. {{ end }}
  366. `))
  367. type binding struct {
  368. Value interface{}
  369. Name string
  370. Methods []methodInfo
  371. }
  372. func newBinding(name string, v interface{}) (*binding, error) {
  373. methods, err := getMethods(v)
  374. if err != nil {
  375. return nil, err
  376. }
  377. return &binding{Name: name, Value: v, Methods: methods}, nil
  378. }
  379. func (b *binding) JS() (string, error) {
  380. js := &bytes.Buffer{}
  381. err := bindTmpl.Execute(js, b)
  382. return js.String(), err
  383. }
  384. func (b *binding) Sync() (string, error) {
  385. js, err := json.Marshal(b.Value)
  386. if err == nil {
  387. return fmt.Sprintf("%[1]s.data=%[2]s;if(%[1]s.render){%[1]s.render(%[2]s);}", b.Name, string(js)), nil
  388. }
  389. return "", err
  390. }
  391. func (b *binding) Call(js string) bool {
  392. type rpcCall struct {
  393. Scope string `json:"scope"`
  394. Method string `json:"method"`
  395. Params []interface{} `json:"params"`
  396. }
  397. rpc := rpcCall{}
  398. if err := json.Unmarshal([]byte(js), &rpc); err != nil {
  399. return false
  400. }
  401. if rpc.Scope != b.Name {
  402. return false
  403. }
  404. var mi *methodInfo
  405. for i := 0; i < len(b.Methods); i++ {
  406. if b.Methods[i].Name == rpc.Method {
  407. mi = &b.Methods[i]
  408. break
  409. }
  410. }
  411. if mi == nil {
  412. return false
  413. }
  414. args := make([]reflect.Value, mi.Arity(), mi.Arity())
  415. for i := 0; i < mi.Arity(); i++ {
  416. val := reflect.ValueOf(rpc.Params[i])
  417. arg := mi.Value.Type().In(i)
  418. u := reflect.New(arg)
  419. if b, err := json.Marshal(val.Interface()); err == nil {
  420. if err = json.Unmarshal(b, u.Interface()); err == nil {
  421. args[i] = reflect.Indirect(u)
  422. }
  423. }
  424. if !args[i].IsValid() {
  425. return false
  426. }
  427. }
  428. mi.Value.Call(args)
  429. return true
  430. }
  431. type methodInfo struct {
  432. Name string
  433. Value reflect.Value
  434. }
  435. func (mi methodInfo) Arity() int { return mi.Value.Type().NumIn() }
  436. func (mi methodInfo) JSName() string {
  437. r := []rune(mi.Name)
  438. if len(r) > 0 {
  439. r[0] = unicode.ToLower(r[0])
  440. }
  441. return string(r)
  442. }
  443. func (mi methodInfo) JSArgs() (js string) {
  444. for i := 0; i < mi.Arity(); i++ {
  445. if i > 0 {
  446. js = js + ","
  447. }
  448. js = js + fmt.Sprintf("a%d", i)
  449. }
  450. return js
  451. }
  452. func getMethods(obj interface{}) ([]methodInfo, error) {
  453. p := reflect.ValueOf(obj)
  454. v := reflect.Indirect(p)
  455. t := reflect.TypeOf(obj)
  456. if t == nil {
  457. return nil, errors.New("object can not be nil")
  458. }
  459. k := t.Kind()
  460. if k == reflect.Ptr {
  461. k = v.Type().Kind()
  462. }
  463. if k != reflect.Struct {
  464. return nil, errors.New("must be a struct or a pointer to a struct")
  465. }
  466. methods := []methodInfo{}
  467. for i := 0; i < t.NumMethod(); i++ {
  468. method := t.Method(i)
  469. if !unicode.IsUpper([]rune(method.Name)[0]) {
  470. continue
  471. }
  472. mi := methodInfo{
  473. Name: method.Name,
  474. Value: p.MethodByName(method.Name),
  475. }
  476. methods = append(methods, mi)
  477. }
  478. return methods, nil
  479. }
  480. func (w *webview) Bind(name string, v interface{}) (sync func(), err error) {
  481. b, err := newBinding(name, v)
  482. if err != nil {
  483. return nil, err
  484. }
  485. js, err := b.JS()
  486. if err != nil {
  487. return nil, err
  488. }
  489. sync = func() {
  490. if js, err := b.Sync(); err != nil {
  491. log.Println(err)
  492. } else {
  493. w.Eval(js)
  494. }
  495. }
  496. m.Lock()
  497. cb := cbs[w]
  498. cbs[w] = func(w WebView, data string) {
  499. if ok := b.Call(data); ok {
  500. sync()
  501. } else {
  502. cb(w, data)
  503. }
  504. }
  505. m.Unlock()
  506. w.Eval(js)
  507. sync()
  508. return sync, nil
  509. }