diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..aee53c804 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +/.vs +.DS_Store +.Thumbs.db +*.sublime* +.idea/ +debug.log +package-lock.json +.vscode/settings.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 000000000..36ad2b169 --- /dev/null +++ b/.npmignore @@ -0,0 +1,11 @@ +test +bindings +docs +lib +node_modules +spec +ui +.git +.github +.idea +SECURITY.md diff --git a/LICENSE b/LICENSE index c123e6407..76b8255d3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Quasar Framework +Copyright (c) 2017 - Present Quasar Framework Contributors, Boscop, Serge Zaitsev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c330dd8f7..f9ed9448f 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,8 @@ This has been done with our best attempt at due diligence and in respect of the original authors. Thankyou - this project would never have been possible without your amazing contribution to open-source and we are honoured to carry the torch further. Of special note: -- [zserge](https://github.com/zserge) for the original webview approach +- [zserge](https://github.com/zserge) for the original webview approach and +go bindings - [Boscop](https://github.com/Boscop) for the Rust Bindings ## Documentation @@ -149,13 +150,9 @@ Thank you to all the people who already +#include +#define WEBVIEW_STATIC +#define WEBVIEW_IMPLEMENTATION +#include "webview.h" + +extern void _webviewExternalInvokeCallback(void *, void *); + +static inline void CgoWebViewFree(void *w) { + free((void *)((struct webview *)w)->title); + free((void *)((struct webview *)w)->url); + free(w); +} + +static inline void *CgoWebViewCreate(int width, int height, char *title, char *url, int resizable, int debug) { + struct webview *w = (struct webview *) calloc(1, sizeof(*w)); + w->width = width; + w->height = height; + w->title = title; + w->url = url; + w->resizable = resizable; + w->debug = debug; + w->external_invoke_cb = (webview_external_invoke_cb_t) _webviewExternalInvokeCallback; + if (webview_init(w) != 0) { + CgoWebViewFree(w); + return NULL; + } + return (void *)w; +} + +static inline int CgoWebViewLoop(void *w, int blocking) { + return webview_loop((struct webview *)w, blocking); +} + +static inline void CgoWebViewTerminate(void *w) { + webview_terminate((struct webview *)w); +} + +static inline void CgoWebViewExit(void *w) { + webview_exit((struct webview *)w); +} + +static inline void CgoWebViewSetTitle(void *w, char *title) { + webview_set_title((struct webview *)w, title); +} + +static inline void CgoWebViewSetFullscreen(void *w, int fullscreen) { + webview_set_fullscreen((struct webview *)w, fullscreen); +} + +static inline void CgoWebViewSetColor(void *w, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { + webview_set_color((struct webview *)w, r, g, b, a); +} + +static inline void CgoDialog(void *w, int dlgtype, int flags, + char *title, char *arg, char *res, size_t ressz) { + webview_dialog(w, dlgtype, flags, + (const char*)title, (const char*) arg, res, ressz); +} + +static inline int CgoWebViewEval(void *w, char *js) { + return webview_eval((struct webview *)w, js); +} + +static inline void CgoWebViewInjectCSS(void *w, char *css) { + webview_inject_css((struct webview *)w, css); +} + +extern void _webviewDispatchGoCallback(void *); +static inline void _webview_dispatch_cb(struct webview *w, void *arg) { + _webviewDispatchGoCallback(arg); +} +static inline void CgoWebViewDispatch(void *w, uintptr_t arg) { + webview_dispatch((struct webview *)w, _webview_dispatch_cb, (void *)arg); +} +*/ +import "C" +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "html/template" + "log" + "reflect" + "runtime" + "sync" + "unicode" + "unsafe" +) + +func init() { + // Ensure that main.main is called from the main thread + runtime.LockOSThread() +} + +// Open is a simplified API to open a single native window with a full-size webview in +// it. It can be helpful if you want to communicate with the core app using XHR +// or WebSockets (as opposed to using JavaScript bindings). +// +// Window appearance can be customized using title, width, height and resizable parameters. +// URL must be provided and can user either a http or https protocol, or be a +// local file:// URL. On some platforms "data:" URLs are also supported +// (Linux/MacOS). +func Open(title, url string, w, h int, resizable bool) error { + titleStr := C.CString(title) + defer C.free(unsafe.Pointer(titleStr)) + urlStr := C.CString(url) + defer C.free(unsafe.Pointer(urlStr)) + resize := C.int(0) + if resizable { + resize = C.int(1) + } + + r := C.webview(titleStr, urlStr, C.int(w), C.int(h), resize) + if r != 0 { + return errors.New("failed to create webview") + } + return nil +} + +// Debug prints a debug string using stderr on Linux/BSD, NSLog on MacOS and +// OutputDebugString on Windows. +func Debug(a ...interface{}) { + s := C.CString(fmt.Sprint(a...)) + defer C.free(unsafe.Pointer(s)) + C.webview_print_log(s) +} + +// Debugf prints a formatted debug string using stderr on Linux/BSD, NSLog on +// MacOS and OutputDebugString on Windows. +func Debugf(format string, a ...interface{}) { + s := C.CString(fmt.Sprintf(format, a...)) + defer C.free(unsafe.Pointer(s)) + C.webview_print_log(s) +} + +// ExternalInvokeCallbackFunc is a function type that is called every time +// "window.external.invoke()" is called from JavaScript. Data is the only +// obligatory string parameter passed into the "invoke(data)" function from +// JavaScript. To pass more complex data serialized JSON or base64 encoded +// string can be used. +type ExternalInvokeCallbackFunc func(w WebView, data string) + +// Settings is a set of parameters to customize the initial WebView appearance +// and behavior. It is passed into the webview.New() constructor. +type Settings struct { + // WebView main window title + Title string + // URL to open in a webview + URL string + // Window width in pixels + Width int + // Window height in pixels + Height int + // Allows/disallows window resizing + Resizable bool + // Enable debugging tools (Linux/BSD/MacOS, on Windows use Firebug) + Debug bool + // A callback that is executed when JavaScript calls "window.external.invoke()" + ExternalInvokeCallback ExternalInvokeCallbackFunc +} + +// WebView is an interface that wraps the basic methods for controlling the UI +// loop, handling multithreading and providing JavaScript bindings. +type WebView interface { + // Run() starts the main UI loop until the user closes the webview window or + // Terminate() is called. + Run() + // Loop() runs a single iteration of the main UI. + Loop(blocking bool) bool + // SetTitle() changes window title. This method must be called from the main + // thread only. See Dispatch() for more details. + SetTitle(title string) + // SetFullscreen() controls window full-screen mode. This method must be + // called from the main thread only. See Dispatch() for more details. + SetFullscreen(fullscreen bool) + // SetColor() changes window background color. This method must be called from + // the main thread only. See Dispatch() for more details. + SetColor(r, g, b, a uint8) + // Eval() evaluates an arbitrary JS code inside the webview. This method must + // be called from the main thread only. See Dispatch() for more details. + Eval(js string) error + // InjectJS() injects an arbitrary block of CSS code using the JS API. This + // method must be called from the main thread only. See Dispatch() for more + // details. + InjectCSS(css string) + // Dialog() opens a system dialog of the given type and title. String + // argument can be provided for certain dialogs, such as alert boxes. For + // alert boxes argument is a message inside the dialog box. + Dialog(dlgType DialogType, flags int, title string, arg string) string + // Terminate() breaks the main UI loop. This method must be called from the main thread + // only. See Dispatch() for more details. + Terminate() + // Dispatch() schedules some arbitrary function to be executed on the main UI + // thread. This may be helpful if you want to run some JavaScript from + // background threads/goroutines, or to terminate the app. + Dispatch(func()) + // Exit() closes the window and cleans up the resources. Use Terminate() to + // forcefully break out of the main UI loop. + Exit() + // Bind() registers a binding between a given value and a JavaScript object with the + // given name. A value must be a struct or a struct pointer. All methods are + // available under their camel-case names, starting with a lower-case letter, + // e.g. "FooBar" becomes "fooBar" in JavaScript. + // Bind() returns a function that updates JavaScript object with the current + // Go value. You only need to call it if you change Go value asynchronously. + Bind(name string, v interface{}) (sync func(), err error) +} + +// DialogType is an enumeration of all supported system dialog types +type DialogType int + +const ( + // DialogTypeOpen is a system file open dialog + DialogTypeOpen DialogType = iota + // DialogTypeSave is a system file save dialog + DialogTypeSave + // DialogTypeAlert is a system alert dialog (message box) + DialogTypeAlert +) + +const ( + // DialogFlagFile is a normal file picker dialog + DialogFlagFile = C.WEBVIEW_DIALOG_FLAG_FILE + // DialogFlagDirectory is an open directory dialog + DialogFlagDirectory = C.WEBVIEW_DIALOG_FLAG_DIRECTORY + // DialogFlagInfo is an info alert dialog + DialogFlagInfo = C.WEBVIEW_DIALOG_FLAG_INFO + // DialogFlagWarning is a warning alert dialog + DialogFlagWarning = C.WEBVIEW_DIALOG_FLAG_WARNING + // DialogFlagError is an error dialog + DialogFlagError = C.WEBVIEW_DIALOG_FLAG_ERROR +) + +var ( + m sync.Mutex + index uintptr + fns = map[uintptr]func(){} + cbs = map[WebView]ExternalInvokeCallbackFunc{} +) + +type webview struct { + w unsafe.Pointer +} + +var _ WebView = &webview{} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +// New creates and opens a new webview window using the given settings. The +// returned object implements the WebView interface. This function returns nil +// if a window can not be created. +func New(settings Settings) WebView { + if settings.Width == 0 { + settings.Width = 640 + } + if settings.Height == 0 { + settings.Height = 480 + } + if settings.Title == "" { + settings.Title = "WebView" + } + w := &webview{} + w.w = C.CgoWebViewCreate(C.int(settings.Width), C.int(settings.Height), + C.CString(settings.Title), C.CString(settings.URL), + C.int(boolToInt(settings.Resizable)), C.int(boolToInt(settings.Debug))) + m.Lock() + if settings.ExternalInvokeCallback != nil { + cbs[w] = settings.ExternalInvokeCallback + } else { + cbs[w] = func(w WebView, data string) {} + } + m.Unlock() + return w +} + +func (w *webview) Loop(blocking bool) bool { + block := C.int(0) + if blocking { + block = 1 + } + return C.CgoWebViewLoop(w.w, block) == 0 +} + +func (w *webview) Run() { + for w.Loop(true) { + } +} + +func (w *webview) Exit() { + C.CgoWebViewExit(w.w) +} + +func (w *webview) Dispatch(f func()) { + m.Lock() + for ; fns[index] != nil; index++ { + } + fns[index] = f + m.Unlock() + C.CgoWebViewDispatch(w.w, C.uintptr_t(index)) +} + +func (w *webview) SetTitle(title string) { + p := C.CString(title) + defer C.free(unsafe.Pointer(p)) + C.CgoWebViewSetTitle(w.w, p) +} + +func (w *webview) SetColor(r, g, b, a uint8) { + C.CgoWebViewSetColor(w.w, C.uint8_t(r), C.uint8_t(g), C.uint8_t(b), C.uint8_t(a)) +} + +func (w *webview) SetFullscreen(fullscreen bool) { + C.CgoWebViewSetFullscreen(w.w, C.int(boolToInt(fullscreen))) +} + +func (w *webview) Dialog(dlgType DialogType, flags int, title string, arg string) string { + const maxPath = 4096 + titlePtr := C.CString(title) + defer C.free(unsafe.Pointer(titlePtr)) + argPtr := C.CString(arg) + defer C.free(unsafe.Pointer(argPtr)) + resultPtr := (*C.char)(C.calloc((C.size_t)(unsafe.Sizeof((*C.char)(nil))), (C.size_t)(maxPath))) + defer C.free(unsafe.Pointer(resultPtr)) + C.CgoDialog(w.w, C.int(dlgType), C.int(flags), titlePtr, + argPtr, resultPtr, C.size_t(maxPath)) + return C.GoString(resultPtr) +} + +func (w *webview) Eval(js string) error { + p := C.CString(js) + defer C.free(unsafe.Pointer(p)) + switch C.CgoWebViewEval(w.w, p) { + case -1: + return errors.New("evaluation failed") + } + return nil +} + +func (w *webview) InjectCSS(css string) { + p := C.CString(css) + defer C.free(unsafe.Pointer(p)) + C.CgoWebViewInjectCSS(w.w, p) +} + +func (w *webview) Terminate() { + C.CgoWebViewTerminate(w.w) +} + +//export _webviewDispatchGoCallback +func _webviewDispatchGoCallback(index unsafe.Pointer) { + var f func() + m.Lock() + f = fns[uintptr(index)] + delete(fns, uintptr(index)) + m.Unlock() + f() +} + +//export _webviewExternalInvokeCallback +func _webviewExternalInvokeCallback(w unsafe.Pointer, data unsafe.Pointer) { + m.Lock() + var ( + cb ExternalInvokeCallbackFunc + wv WebView + ) + for wv, cb = range cbs { + if wv.(*webview).w == w { + break + } + } + m.Unlock() + cb(wv, C.GoString((*C.char)(data))) +} + +var bindTmpl = template.Must(template.New("").Parse(` +if (typeof {{.Name}} === 'undefined') { + {{.Name}} = {}; +} +{{ range .Methods }} +{{$.Name}}.{{.JSName}} = function({{.JSArgs}}) { + window.external.invoke(JSON.stringify({scope: "{{$.Name}}", method: "{{.Name}}", params: [{{.JSArgs}}]})); +}; +{{ end }} +`)) + +type binding struct { + Value interface{} + Name string + Methods []methodInfo +} + +func newBinding(name string, v interface{}) (*binding, error) { + methods, err := getMethods(v) + if err != nil { + return nil, err + } + return &binding{Name: name, Value: v, Methods: methods}, nil +} + +func (b *binding) JS() (string, error) { + js := &bytes.Buffer{} + err := bindTmpl.Execute(js, b) + return js.String(), err +} + +func (b *binding) Sync() (string, error) { + js, err := json.Marshal(b.Value) + if err == nil { + return fmt.Sprintf("%[1]s.data=%[2]s;if(%[1]s.render){%[1]s.render(%[2]s);}", b.Name, string(js)), nil + } + return "", err +} + +func (b *binding) Call(js string) bool { + type rpcCall struct { + Scope string `json:"scope"` + Method string `json:"method"` + Params []interface{} `json:"params"` + } + + rpc := rpcCall{} + if err := json.Unmarshal([]byte(js), &rpc); err != nil { + return false + } + if rpc.Scope != b.Name { + return false + } + var mi *methodInfo + for i := 0; i < len(b.Methods); i++ { + if b.Methods[i].Name == rpc.Method { + mi = &b.Methods[i] + break + } + } + if mi == nil { + return false + } + args := make([]reflect.Value, mi.Arity(), mi.Arity()) + for i := 0; i < mi.Arity(); i++ { + val := reflect.ValueOf(rpc.Params[i]) + arg := mi.Value.Type().In(i) + u := reflect.New(arg) + if b, err := json.Marshal(val.Interface()); err == nil { + if err = json.Unmarshal(b, u.Interface()); err == nil { + args[i] = reflect.Indirect(u) + } + } + if !args[i].IsValid() { + return false + } + } + mi.Value.Call(args) + return true +} + +type methodInfo struct { + Name string + Value reflect.Value +} + +func (mi methodInfo) Arity() int { return mi.Value.Type().NumIn() } + +func (mi methodInfo) JSName() string { + r := []rune(mi.Name) + if len(r) > 0 { + r[0] = unicode.ToLower(r[0]) + } + return string(r) +} + +func (mi methodInfo) JSArgs() (js string) { + for i := 0; i < mi.Arity(); i++ { + if i > 0 { + js = js + "," + } + js = js + fmt.Sprintf("a%d", i) + } + return js +} + +func getMethods(obj interface{}) ([]methodInfo, error) { + p := reflect.ValueOf(obj) + v := reflect.Indirect(p) + t := reflect.TypeOf(obj) + if t == nil { + return nil, errors.New("object can not be nil") + } + k := t.Kind() + if k == reflect.Ptr { + k = v.Type().Kind() + } + if k != reflect.Struct { + return nil, errors.New("must be a struct or a pointer to a struct") + } + + methods := []methodInfo{} + for i := 0; i < t.NumMethod(); i++ { + method := t.Method(i) + if !unicode.IsUpper([]rune(method.Name)[0]) { + continue + } + mi := methodInfo{ + Name: method.Name, + Value: p.MethodByName(method.Name), + } + methods = append(methods, mi) + } + + return methods, nil +} + +func (w *webview) Bind(name string, v interface{}) (sync func(), err error) { + b, err := newBinding(name, v) + if err != nil { + return nil, err + } + js, err := b.JS() + if err != nil { + return nil, err + } + sync = func() { + if js, err := b.Sync(); err != nil { + log.Println(err) + } else { + w.Eval(js) + } + } + + m.Lock() + cb := cbs[w] + cbs[w] = func(w WebView, data string) { + if ok := b.Call(data); ok { + sync() + } else { + cb(w, data) + } + } + m.Unlock() + + w.Eval(js) + sync() + return sync, nil +} diff --git a/bindings/go/proton_test.go b/bindings/go/proton_test.go new file mode 100644 index 000000000..49fc951e8 --- /dev/null +++ b/bindings/go/proton_test.go @@ -0,0 +1,97 @@ +package proton + +import ( + "image" + "testing" +) + +type foo struct { + Result interface{} +} + +func (f *foo) Foo1(a int, b float32) { + f.Result = float64(a) + float64(b) +} +func (f *foo) Foo2(a []int, b [3]float32, c map[int]int) { + f.Result = map[string]interface{}{"a": a, "b": b, "c": c} +} +func (f *foo) Foo3(a []image.Point, b struct{ Z int }) { + f.Result = map[string]interface{}{"a": a, "b": b} +} + +func TestBadBinding(t *testing.T) { + x := 123 + for _, v := range []interface{}{ + nil, + true, + 123, + 123.4, + "hello", + 'a', + make(chan struct{}, 0), + func() {}, + map[string]string{}, + []int{}, + [3]int{0, 0, 0}, + &x, + } { + if _, err := newBinding("test", v); err == nil { + t.Errorf("should return an error: %#v", v) + } + } +} + +func TestBindingCall(t *testing.T) { + foo := &foo{} + b, err := newBinding("test", foo) + if err != nil { + t.Fatal(err) + } + t.Run("Primitives", func(t *testing.T) { + if !b.Call(`{"scope":"test","method":"Foo1","params":[3,4.5]}`) { + t.Fatal() + } + if foo.Result.(float64) != 7.5 { + t.Fatal(foo) + } + }) + + t.Run("Collections", func(t *testing.T) { + // Call with slices, arrays and maps + if !b.Call(`{"scope":"test","method":"Foo2","params":[[1,2,3],[4.5,4.6,4.7],{"1":2,"3":4}]}`) { + t.Fatal() + } + m := foo.Result.(map[string]interface{}) + if ints := m["a"].([]int); ints[0] != 1 || ints[1] != 2 || ints[2] != 3 { + t.Fatal(foo) + } + if floats := m["b"].([3]float32); floats[0] != 4.5 || floats[1] != 4.6 || floats[2] != 4.7 { + t.Fatal(foo) + } + if dict := m["c"].(map[int]int); len(dict) != 2 || dict[1] != 2 || dict[3] != 4 { + t.Fatal(foo) + } + }) + + t.Run("Structs", func(t *testing.T) { + if !b.Call(`{"scope":"test","method":"Foo3","params":[[{"X":1,"Y":2},{"X":3,"Y":4}],{"Z":42}]}`) { + t.Fatal() + } + m := foo.Result.(map[string]interface{}) + if p := m["a"].([]image.Point); p[0].X != 1 || p[0].Y != 2 || p[1].X != 3 || p[1].Y != 4 { + t.Fatal(foo) + } + if z := m["b"].(struct{ Z int }); z.Z != 42 { + t.Fatal(foo) + } + }) + + t.Run("Errors", func(t *testing.T) { + if b.Call(`{"scope":"foo"}`) || b.Call(`{"scope":"test", "method":"Bar"}`) { + t.Fatal() + } + if b.Call(`{"scope":"test","method":"Foo1","params":["3",4.5]}`) { + t.Fatal() + } + }) +} diff --git a/bindings/rust/.gitignore b/bindings/rust/.gitignore new file mode 100644 index 000000000..ace562d6e --- /dev/null +++ b/bindings/rust/.gitignore @@ -0,0 +1,4 @@ +target/ +**/*.rs.bk +Cargo.lock +.idea diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml new file mode 100644 index 000000000..93d6250f0 --- /dev/null +++ b/bindings/rust/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "proton-ui" +version = "0.1.0" +authors = ["Boscop", "rstoenescu", "nothingismagick", "lucasfernog"] +readme = "README.md" +license = "MIT" +repository = "https://github.com/quasarframework/proton" +description = "Rust bindings for proton, a toolchain for building more secure native apps that have tiny binaries and are very fast." +keywords = ["quasar", "web", "gui", "desktop", "webkit"] +categories = ["quasar", "gui", "web-programming", "api-bindings", "rendering", "visualization"] + +[dependencies] +urlencoding = "1.0" +proton-sys = { path = "proton-sys", version = "0.1.0" } +boxfnonce = "0.1" + +[features] +default = ["V1_30"] +V1_30 = [] + +[dev-dependencies] +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" \ No newline at end of file diff --git a/bindings/rust/LICENSE b/bindings/rust/LICENSE new file mode 100644 index 000000000..c592e15be --- /dev/null +++ b/bindings/rust/LICENSE @@ -0,0 +1,45 @@ +MIT License + +Copyright (c) 2018 Boscop + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---- + +MIT License + +Copyright (c) 2019 Quasar Framework + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bindings/rust/README.md b/bindings/rust/README.md new file mode 100644 index 000000000..795f7eb8e --- /dev/null +++ b/bindings/rust/README.md @@ -0,0 +1 @@ +# rust bindings diff --git a/bindings/rust/proton-sys/Cargo.toml b/bindings/rust/proton-sys/Cargo.toml new file mode 100644 index 000000000..f63a7fb7e --- /dev/null +++ b/bindings/rust/proton-sys/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "proton-sys" +version = "0.1.0" +authors = ["Boscop", "rstoenescu", "nothingismagick", "lucasfernog"] +license = "MIT" +repository = "https://github.com/quasarframework/proton" +description = "Rust native ffi bindings for proton UI" +keywords = ["quasar", "web", "gui", "desktop", "webkit"] +categories = ["quasar", "gui", "web-programming", "api-bindings", "rendering", "visualization"] +build = "build.rs" +links = "proton" + +[lib] +name = "proton_sys" +path = "lib.rs" + +[dependencies] +bitflags = "1.0" + +[build-dependencies] +cc = "1" +pkg-config = "0.3" \ No newline at end of file diff --git a/bindings/rust/proton-sys/build.rs b/bindings/rust/proton-sys/build.rs new file mode 100644 index 000000000..f76754355 --- /dev/null +++ b/bindings/rust/proton-sys/build.rs @@ -0,0 +1,56 @@ +extern crate cc; +extern crate pkg_config; + +use std::{ + env, + path::{Path, PathBuf}, + process::Command, +}; + +fn main() { + let proton_path = PathBuf::from("../../../ui"); + + let mut build = cc::Build::new(); + + build + .include(&proton_path) + .file("proton.c") + .flag_if_supported("-std=c11") + .flag_if_supported("-w"); + + if env::var("DEBUG").is_err() { + build.define("NDEBUG", None); + } else { + build.define("DEBUG", None); + } + + let target = env::var("TARGET").unwrap(); + + if target.contains("windows") { + build.define("WEBVIEW_WINAPI", None); + for &lib in &["ole32", "comctl32", "oleaut32", "uuid", "gdi32"] { + println!("cargo:rustc-link-lib={}", lib); + } + } else if target.contains("linux") || target.contains("bsd") { + let webkit = pkg_config::Config::new() + .atleast_version("2.8") + .probe("webkit2gtk-4.0") + .unwrap(); + + for path in webkit.include_paths { + build.include(path); + } + build.define("WEBVIEW_GTK", None); + } else if target.contains("apple") { + build + .define("WEBVIEW_COCOA", None) + .flag("-x") + .flag("objective-c"); + println!("cargo:rustc-link-lib=framework=Cocoa"); + println!("cargo:rustc-link-lib=framework=WebKit"); + } else { + panic!("unsupported target"); + } + + build.compile("proton"); +} diff --git a/bindings/rust/proton-sys/lib.rs b/bindings/rust/proton-sys/lib.rs new file mode 100644 index 000000000..559040b84 --- /dev/null +++ b/bindings/rust/proton-sys/lib.rs @@ -0,0 +1,45 @@ +//! Raw FFI bindings to proton UI. + +#[macro_use] +extern crate bitflags; + +use std::os::raw::*; + +pub enum CWebView {} // opaque type, only used in ffi pointers + +type ErasedExternalInvokeFn = extern "C" fn(webview: *mut CWebView, arg: *const c_char); +type ErasedDispatchFn = extern "C" fn(webview: *mut CWebView, arg: *mut c_void); + +#[repr(C)] +pub enum DialogType { + Open = 0, + Save = 1, + Alert = 2, +} + +bitflags! { + #[repr(C)] + pub struct DialogFlags: u32 { + const FILE = 0b0000; + const DIRECTORY = 0b0001; + const INFO = 0b0010; + const WARNING = 0b0100; + const ERROR = 0b0110; + } +} + +extern { + pub fn wrapper_webview_free(this: *mut CWebView); + pub fn wrapper_webview_new(title: *const c_char, url: *const c_char, width: c_int, height: c_int, resizable: c_int, debug: c_int, external_invoke_cb: Option, userdata: *mut c_void) -> *mut CWebView; + pub fn webview_loop(this: *mut CWebView, blocking: c_int) -> c_int; + pub fn webview_terminate(this: *mut CWebView); + pub fn webview_exit(this: *mut CWebView); + pub fn wrapper_webview_get_userdata(this: *mut CWebView) -> *mut c_void; + pub fn webview_dispatch(this: *mut CWebView, f: Option, arg: *mut c_void); + pub fn webview_eval(this: *mut CWebView, js: *const c_char) -> c_int; + pub fn webview_inject_css(this: *mut CWebView, css: *const c_char) -> c_int; + pub fn webview_set_title(this: *mut CWebView, title: *const c_char); + pub fn webview_set_fullscreen(this: *mut CWebView, fullscreen: c_int); + pub fn webview_set_color(this: *mut CWebView, red: u8, green: u8, blue: u8, alpha: u8); + pub fn webview_dialog(this: *mut CWebView, dialog_type: DialogType, flags: DialogFlags, title: *const c_char, arg: *const c_char, result: *mut c_char, result_size: usize); +} diff --git a/bindings/rust/proton-sys/proton.c b/bindings/rust/proton-sys/proton.c new file mode 100644 index 000000000..a4bb108e5 --- /dev/null +++ b/bindings/rust/proton-sys/proton.c @@ -0,0 +1,28 @@ +#define WEBVIEW_IMPLEMENTATION +#include "proton.h" + +void wrapper_webview_free(struct webview* w) { + free(w); +} + +struct webview* wrapper_webview_new(const char* title, const char* url, int width, int height, int resizable, int debug, webview_external_invoke_cb_t external_invoke_cb, void* userdata) { + struct webview* w = (struct webview*)calloc(1, sizeof(*w)); + w->width = width; + w->height = height; + w->title = title; + w->url = url; + w->resizable = resizable; + w->debug = debug; + w->external_invoke_cb = external_invoke_cb; + w->userdata = userdata; + if (webview_init(w) != 0) { + wrapper_webview_free(w); + return NULL; + } + return w; +} + +void* wrapper_webview_get_userdata(struct webview* w) { + return w->userdata; +} + diff --git a/bindings/rust/rustfmt.toml b/bindings/rust/rustfmt.toml new file mode 100644 index 000000000..9da25275e --- /dev/null +++ b/bindings/rust/rustfmt.toml @@ -0,0 +1,13 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 2 +newline_style = "Auto" +use_small_heuristics = "Default" +reorder_imports = true +reorder_modules = true +remove_nested_parens = true +edition = "2015" +merge_derives = true +use_try_shorthand = false +use_field_init_shorthand = false +force_explicit_abi = true diff --git a/bindings/rust/src/color.rs b/bindings/rust/src/color.rs new file mode 100644 index 000000000..9bddcd257 --- /dev/null +++ b/bindings/rust/src/color.rs @@ -0,0 +1,52 @@ +/// An RGBA color. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl From<(u8, u8, u8, u8)> for Color { + fn from(tuple: (u8, u8, u8, u8)) -> Color { + Color { + r: tuple.0, + g: tuple.1, + b: tuple.2, + a: tuple.3, + } + } +} + +impl From<(u8, u8, u8)> for Color { + fn from(tuple: (u8, u8, u8)) -> Color { + Color { + r: tuple.0, + g: tuple.1, + b: tuple.2, + a: 255, + } + } +} + +impl From<[u8; 4]> for Color { + fn from(array: [u8; 4]) -> Color { + Color { + r: array[0], + g: array[1], + b: array[2], + a: array[3], + } + } +} + +impl From<[u8; 3]> for Color { + fn from(array: [u8; 3]) -> Color { + Color { + r: array[0], + g: array[1], + b: array[2], + a: 255, + } + } +} diff --git a/bindings/rust/src/dialog.rs b/bindings/rust/src/dialog.rs new file mode 100644 index 000000000..2a6b8e9ed --- /dev/null +++ b/bindings/rust/src/dialog.rs @@ -0,0 +1,141 @@ +use ffi::{self, DialogFlags, DialogType}; +use std::{ffi::CString, path::PathBuf}; +use {read_str, WVResult, WebView}; + +const STR_BUF_SIZE: usize = 4096; + +/// A builder for opening a new dialog window. +#[derive(Debug)] +pub struct DialogBuilder<'a: 'b, 'b, T: 'a> { + webview: &'b mut WebView<'a, T>, +} + +impl<'a: 'b, 'b, T: 'a> DialogBuilder<'a, 'b, T> { + /// Creates a new dialog builder for a WebView. + pub fn new(webview: &'b mut WebView<'a, T>) -> DialogBuilder<'a, 'b, T> { + DialogBuilder { webview } + } + + fn dialog( + &mut self, + title: String, + arg: String, + dialog_type: DialogType, + dialog_flags: DialogFlags, + ) -> WVResult { + let mut s = [0u8; STR_BUF_SIZE]; + + let title_cstr = CString::new(title)?; + let arg_cstr = CString::new(arg)?; + + unsafe { + ffi::webview_dialog( + self.webview.inner, + dialog_type, + dialog_flags, + title_cstr.as_ptr(), + arg_cstr.as_ptr(), + s.as_mut_ptr() as _, + s.len(), + ); + } + + Ok(read_str(&s)) + } + + /// Opens a new open file dialog and returns the chosen file path. + pub fn open_file(&mut self, title: S, default_file: P) -> WVResult> + where + S: Into, + P: Into, + { + self + .dialog( + title.into(), + default_file.into().to_string_lossy().into_owned(), + DialogType::Open, + DialogFlags::FILE, + ) + .map(|path| { + if path.is_empty() { + None + } else { + Some(PathBuf::from(path)) + } + }) + } + + /// Opens a new choose directory dialog as returns the chosen directory path. + pub fn choose_directory( + &mut self, + title: S, + default_directory: P, + ) -> WVResult> + where + S: Into, + P: Into, + { + self + .dialog( + title.into(), + default_directory.into().to_string_lossy().into_owned(), + DialogType::Open, + DialogFlags::DIRECTORY, + ) + .map(|path| { + if path.is_empty() { + None + } else { + Some(PathBuf::from(path)) + } + }) + } + + /// Opens an info alert dialog. + pub fn info(&mut self, title: TS, message: MS) -> WVResult + where + TS: Into, + MS: Into, + { + self + .dialog( + title.into(), + message.into(), + DialogType::Alert, + DialogFlags::INFO, + ) + .map(|_| ()) + } + + /// Opens a warning alert dialog. + pub fn warning(&mut self, title: TS, message: MS) -> WVResult + where + TS: Into, + MS: Into, + { + self + .dialog( + title.into(), + message.into(), + DialogType::Alert, + DialogFlags::WARNING, + ) + .map(|_| ()) + } + + /// Opens an error alert dialog. + pub fn error(&mut self, title: TS, message: MS) -> WVResult + where + TS: Into, + MS: Into, + { + self + .dialog( + title.into(), + message.into(), + DialogType::Alert, + DialogFlags::ERROR, + ) + .map(|_| ()) + } +} diff --git a/bindings/rust/src/error.rs b/bindings/rust/src/error.rs new file mode 100644 index 000000000..07ed11a67 --- /dev/null +++ b/bindings/rust/src/error.rs @@ -0,0 +1,79 @@ +use std::{ + error, + ffi::NulError, + fmt::{self, Debug, Display}, +}; + +pub trait CustomError: Display + Debug + Send + Sync + 'static {} + +impl CustomError for T {} + +/// A WebView error. +#[derive(Debug)] +pub enum Error { + /// While attempting to build a WebView instance, a required field was not initialized. + UninitializedField(&'static str), + /// An error occurred while initializing a WebView instance. + Initialization, + /// A nul-byte was found in a provided string. + NulByte(NulError), + /// An error occurred while evaluating JavaScript in a WebView instance. + JsEvaluation, + /// An error occurred while injecting CSS into a WebView instance. + CssInjection, + /// Failure to dispatch a closure to a WebView instance via a handle, likely because the + /// WebView was dropped. + Dispatch, + /// An user-specified error occurred. For use inside invoke and dispatch closures. + Custom(Box), +} + +impl Error { + /// Creates a custom error from a `T: Display + Debug + Send + Sync + 'static`. + pub fn custom(error: E) -> Error { + Error::Custom(Box::new(error)) + } +} + +impl error::Error for Error { + fn cause(&self) -> Option<&error::Error> { + match self { + Error::NulByte(cause) => Some(cause), + _ => None, + } + } + + #[cfg(feature = "V1_30")] + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Error::NulByte(ref cause) => Some(cause), + _ => None, + } + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::UninitializedField(field) => write!(f, "Required field uninitialized: {}.", field), + Error::Initialization => write!(f, "Webview failed to initialize."), + Error::NulByte(cause) => write!(f, "{}", cause), + Error::JsEvaluation => write!(f, "Failed to evaluate JavaScript."), + Error::CssInjection => write!(f, "Failed to inject CSS."), + Error::Dispatch => write!( + f, + "Closure could not be dispatched. WebView was likely dropped." + ), + Error::Custom(e) => write!(f, "Error: {}", e), + } + } +} + +/// A WebView result. +pub type WVResult = Result; + +impl From for Error { + fn from(e: NulError) -> Error { + Error::NulByte(e) + } +} diff --git a/bindings/rust/src/escape.rs b/bindings/rust/src/escape.rs new file mode 100644 index 000000000..e949cd9f4 --- /dev/null +++ b/bindings/rust/src/escape.rs @@ -0,0 +1,81 @@ +use std::fmt::{self, Write}; + +/// Escape a string to pass it into JavaScript. +/// +/// # Example +/// +/// ```rust,no_run +/// # use web_view::WebView; +/// # use std::mem; +/// # +/// # let mut view: WebView<()> = unsafe { mem::uninitialized() }; +/// # +/// let string = "Hello, world!"; +/// +/// // Calls the function callback with "Hello, world!" as its parameter. +/// +/// view.eval(&format!("callback({});", web_view::escape(string))); +/// ``` +pub fn escape(string: &str) -> Escaper { + Escaper(string) +} + +// "All code points may appear literally in a string literal except for the +// closing quote code points, U+005C (REVERSE SOLIDUS), U+000D (CARRIAGE +// RETURN), U+2028 (LINE SEPARATOR), U+2029 (PARAGRAPH SEPARATOR), and U+000A +// (LINE FEED)." - ES6 Specification + +pub struct Escaper<'a>(&'a str); + +const SPECIAL: &[char] = &[ + '\n', // U+000A (LINE FEED) + '\r', // U+000D (CARRIAGE RETURN) + '\'', // U+0027 (APOSTROPHE) + '\\', // U+005C (REVERSE SOLIDUS) + '\u{2028}', // U+2028 (LINE SEPARATOR) + '\u{2029}', // U+2029 (PARAGRAPH SEPARATOR) +]; + +impl<'a> fmt::Display for Escaper<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let &Escaper(mut string) = self; + + f.write_char('\'')?; + + while !string.is_empty() { + if let Some(i) = string.find(SPECIAL) { + if i > 0 { + f.write_str(&string[..i])?; + } + + let mut chars = string[i..].chars(); + + f.write_str(match chars.next().unwrap() { + '\n' => "\\n", + '\r' => "\\r", + '\'' => "\\'", + '\\' => "\\\\", + '\u{2028}' => "\\u2028", + '\u{2029}' => "\\u2029", + _ => unreachable!(), + })?; + + string = chars.as_str(); + } else { + f.write_str(string)?; + break; + } + } + + f.write_char('\'')?; + + Ok(()) + } +} + +#[test] +fn test() { + let plain = "ABC \n\r' abc \\ \u{2028} \u{2029}123"; + let escaped = escape(plain).to_string(); + assert!(escaped == "'ABC \\n\\r\\' abc \\\\ \\u2028 \\u2029123'"); +} diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs new file mode 100644 index 000000000..d0b146948 --- /dev/null +++ b/bindings/rust/src/lib.rs @@ -0,0 +1,526 @@ +extern crate boxfnonce; +extern crate proton_sys as ffi; +extern crate urlencoding; + +mod color; +mod dialog; +mod error; +mod escape; +pub use color::Color; +pub use dialog::DialogBuilder; +pub use error::{CustomError, Error, WVResult}; +pub use escape::escape; + +use boxfnonce::SendBoxFnOnce; +use ffi::*; +use std::{ + ffi::{CStr, CString}, + marker::PhantomData, + mem, + os::raw::*, + sync::{Arc, RwLock, Weak}, +}; +use urlencoding::encode; + +/// Content displayable inside a [`WebView`]. +/// +/// # Variants +/// +/// - `Url` - Content to be fetched from a URL. +/// - `Html` - A string containing literal HTML. +/// +/// [`WebView`]: struct.WebView.html +#[derive(Debug)] +pub enum Content { + Url(T), + Html(T), +} + +/// Builder for constructing a [`WebView`] instance. +/// +/// # Example +/// +/// ```no_run +/// extern crate web_view; +/// +/// use web_view::*; +/// +/// fn main() { +/// WebViewBuilder::new() +/// .title("Minimal webview example") +/// .content(Content::Url("https://en.m.wikipedia.org/wiki/Main_Page")) +/// .size(800, 600) +/// .resizable(true) +/// .debug(true) +/// .user_data(()) +/// .invoke_handler(|_webview, _arg| Ok(())) +/// .build() +/// .unwrap() +/// .run() +/// .unwrap(); +/// } +/// ``` +/// +/// [`WebView`]: struct.WebView.html +pub struct WebViewBuilder<'a, T: 'a, I, C> { + pub title: &'a str, + pub content: Option>, + pub width: i32, + pub height: i32, + pub resizable: bool, + pub debug: bool, + pub invoke_handler: Option, + pub user_data: Option, +} + +impl<'a, T: 'a, I, C> Default for WebViewBuilder<'a, T, I, C> +where + I: FnMut(&mut WebView, &str) -> WVResult + 'a, + C: AsRef, +{ + fn default() -> Self { + #[cfg(debug_assertions)] + let debug = true; + #[cfg(not(debug_assertions))] + let debug = false; + + WebViewBuilder { + title: "Application", + content: None, + width: 800, + height: 600, + resizable: true, + debug, + invoke_handler: None, + user_data: None, + } + } +} + +impl<'a, T: 'a, I, C> WebViewBuilder<'a, T, I, C> +where + I: FnMut(&mut WebView, &str) -> WVResult + 'a, + C: AsRef, +{ + /// Alias for [`WebViewBuilder::default()`]. + /// + /// [`WebViewBuilder::default()`]: struct.WebviewBuilder.html#impl-Default + pub fn new() -> Self { + WebViewBuilder::default() + } + + /// Sets the title of the WebView window. + /// + /// Defaults to `"Application"`. + pub fn title(mut self, title: &'a str) -> Self { + self.title = title; + self + } + + /// Sets the content of the WebView. Either a URL or a HTML string. + pub fn content(mut self, content: Content) -> Self { + self.content = Some(content); + self + } + + /// Sets the size of the WebView window. + /// + /// Defaults to 800 x 600. + pub fn size(mut self, width: i32, height: i32) -> Self { + self.width = width; + self.height = height; + self + } + + /// Sets the resizability of the WebView window. If set to false, the window cannot be resized. + /// + /// Defaults to `true`. + pub fn resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } + + /// Enables or disables debug mode. + /// + /// Defaults to `true` for debug builds, `false` for release builds. + pub fn debug(mut self, debug: bool) -> Self { + self.debug = debug; + self + } + + /// Sets the invoke handler callback. This will be called when a message is received from + /// JavaScript. + /// + /// # Errors + /// + /// If the closure returns an `Err`, it will be returned on the next call to [`step()`]. + /// + /// [`step()`]: struct.WebView.html#method.step + pub fn invoke_handler(mut self, invoke_handler: I) -> Self { + self.invoke_handler = Some(invoke_handler); + self + } + + /// Sets the initial state of the user data. This is an arbitrary value stored on the WebView + /// thread, accessible from dispatched closures without synchronization overhead. + pub fn user_data(mut self, user_data: T) -> Self { + self.user_data = Some(user_data); + self + } + + /// Validates provided arguments and returns a new WebView if successful. + pub fn build(self) -> WVResult> { + macro_rules! require_field { + ($name:ident) => { + self + .$name + .ok_or_else(|| Error::UninitializedField(stringify!($name)))? + }; + } + + let title = CString::new(self.title)?; + let content = require_field!(content); + let url = match content { + Content::Url(url) => CString::new(url.as_ref())?, + Content::Html(html) => CString::new(format!("data:text/html,{}", encode(html.as_ref())))?, + }; + let user_data = require_field!(user_data); + let invoke_handler = require_field!(invoke_handler); + + WebView::new( + &title, + &url, + self.width, + self.height, + self.resizable, + self.debug, + user_data, + invoke_handler, + ) + } + + /// Validates provided arguments and runs a new WebView to completion, returning the user data. + /// + /// Equivalent to `build()?.run()`. + pub fn run(self) -> WVResult { + self.build()?.run() + } +} + +/// Constructs a new builder for a [`WebView`]. +/// +/// Alias for [`WebViewBuilder::default()`]. +/// +/// [`WebView`]: struct.Webview.html +/// [`WebViewBuilder::default()`]: struct.WebviewBuilder.html#impl-Default +pub fn builder<'a, T, I, C>() -> WebViewBuilder<'a, T, I, C> +where + I: FnMut(&mut WebView, &str) -> WVResult + 'a, + C: AsRef, +{ + WebViewBuilder::new() +} + +struct UserData<'a, T> { + inner: T, + live: Arc>, + invoke_handler: Box, &str) -> WVResult + 'a>, + result: WVResult, +} + +/// An owned webview instance. +/// +/// Construct via a [`WebViewBuilder`]. +/// +/// [`WebViewBuilder`]: struct.WebViewBuilder.html +#[derive(Debug)] +pub struct WebView<'a, T: 'a> { + inner: *mut CWebView, + _phantom: PhantomData<&'a mut T>, +} + +impl<'a, T> WebView<'a, T> { + #![cfg_attr(feature = "cargo-clippy", allow(clippy::too_many_arguments))] + fn new( + title: &CStr, + url: &CStr, + width: i32, + height: i32, + resizable: bool, + debug: bool, + user_data: T, + invoke_handler: I, + ) -> WVResult> + where + I: FnMut(&mut WebView, &str) -> WVResult + 'a, + { + let user_data = Box::new(UserData { + inner: user_data, + live: Arc::new(RwLock::new(())), + invoke_handler: Box::new(invoke_handler), + result: Ok(()), + }); + let user_data_ptr = Box::into_raw(user_data); + + unsafe { + let inner = wrapper_webview_new( + title.as_ptr(), + url.as_ptr(), + width, + height, + resizable as _, + debug as _, + Some(ffi_invoke_handler::), + user_data_ptr as _, + ); + + if inner.is_null() { + Box::>::from_raw(user_data_ptr); + Err(Error::Initialization) + } else { + Ok(WebView::from_ptr(inner)) + } + } + } + + unsafe fn from_ptr(inner: *mut CWebView) -> WebView<'a, T> { + WebView { + inner, + _phantom: PhantomData, + } + } + + /// Creates a thread-safe [`Handle`] to the `WebView`, from which closures can be dispatched. + /// + /// [`Handle`]: struct.Handle.html + pub fn handle(&self) -> Handle { + Handle { + inner: self.inner, + live: Arc::downgrade(&self.user_data_wrapper().live), + _phantom: PhantomData, + } + } + + fn user_data_wrapper_ptr(&self) -> *mut UserData<'a, T> { + unsafe { wrapper_webview_get_userdata(self.inner) as _ } + } + + fn user_data_wrapper(&self) -> &UserData<'a, T> { + unsafe { &(*self.user_data_wrapper_ptr()) } + } + + fn user_data_wrapper_mut(&mut self) -> &mut UserData<'a, T> { + unsafe { &mut (*self.user_data_wrapper_ptr()) } + } + + /// Borrows the user data immutably. + pub fn user_data(&self) -> &T { + &self.user_data_wrapper().inner + } + + /// Borrows the user data mutably. + pub fn user_data_mut(&mut self) -> &mut T { + &mut self.user_data_wrapper_mut().inner + } + + /// Forces the `WebView` instance to end, without dropping. + pub fn terminate(&mut self) { + unsafe { webview_terminate(self.inner) } + } + + /// Executes the provided string as JavaScript code within the `WebView` instance. + pub fn eval(&mut self, js: &str) -> WVResult { + let js = CString::new(js)?; + let ret = unsafe { webview_eval(self.inner, js.as_ptr()) }; + if ret != 0 { + Err(Error::JsEvaluation) + } else { + Ok(()) + } + } + + /// Injects the provided string as CSS within the `WebView` instance. + pub fn inject_css(&mut self, css: &str) -> WVResult { + let css = CString::new(css)?; + let ret = unsafe { webview_inject_css(self.inner, css.as_ptr()) }; + if ret != 0 { + Err(Error::CssInjection) + } else { + Ok(()) + } + } + + /// Sets the color of the title bar. + /// + /// # Examples + /// + /// Without specifying alpha (defaults to 255): + /// ```ignore + /// webview.set_color((123, 321, 213)); + /// ``` + /// + /// Specifying alpha: + /// ```ignore + /// webview.set_color((123, 321, 213, 127)); + /// ``` + pub fn set_color>(&mut self, color: C) { + let color = color.into(); + unsafe { webview_set_color(self.inner, color.r, color.g, color.b, color.a) } + } + + /// Sets the title displayed at the top of the window. + /// + /// # Errors + /// + /// If `title` contain a nul byte, returns [`Error::NulByte`]. + /// + /// [`Error::NulByte`]: enum.Error.html#variant.NulByte + pub fn set_title(&mut self, title: &str) -> WVResult { + let title = CString::new(title)?; + unsafe { webview_set_title(self.inner, title.as_ptr()) } + Ok(()) + } + + /// Enables or disables fullscreen. + pub fn set_fullscreen(&mut self, fullscreen: bool) { + unsafe { webview_set_fullscreen(self.inner, fullscreen as _) }; + } + + /// Returns a builder for opening a new dialog window. + pub fn dialog<'b>(&'b mut self) -> DialogBuilder<'a, 'b, T> { + DialogBuilder::new(self) + } + + /// Iterates the event loop. Returns `None` if the view has been closed or terminated. + pub fn step(&mut self) -> Option { + unsafe { + match webview_loop(self.inner, 1) { + 0 => { + let closure_result = &mut self.user_data_wrapper_mut().result; + match closure_result { + Ok(_) => Some(Ok(())), + e => Some(mem::replace(e, Ok(()))), + } + } + _ => None, + } + } + } + + /// Runs the event loop to completion and returns the user data. + pub fn run(mut self) -> WVResult { + loop { + match self.step() { + Some(Ok(_)) => (), + Some(e) => e?, + None => return Ok(self.into_inner()), + } + } + } + + /// Consumes the `WebView` and returns ownership of the user data. + pub fn into_inner(mut self) -> T { + unsafe { + let user_data = self._into_inner(); + mem::forget(self); + user_data + } + } + + unsafe fn _into_inner(&mut self) -> T { + let _lock = self + .user_data_wrapper() + .live + .write() + .expect("A dispatch channel thread panicked while holding mutex to WebView."); + + let user_data_ptr = self.user_data_wrapper_ptr(); + webview_exit(self.inner); + wrapper_webview_free(self.inner); + let user_data = *Box::from_raw(user_data_ptr); + user_data.inner + } +} + +impl<'a, T> Drop for WebView<'a, T> { + fn drop(&mut self) { + unsafe { + self._into_inner(); + } + } +} + +/// A thread-safe handle to a [`WebView`] instance. Used to dispatch closures onto its task queue. +/// +/// [`WebView`]: struct.WebView.html +pub struct Handle { + inner: *mut CWebView, + live: Weak>, + _phantom: PhantomData, +} + +impl Handle { + /// Schedules a closure to be run on the [`WebView`] thread. + /// + /// # Errors + /// + /// Returns [`Error::Dispatch`] if the [`WebView`] has been dropped. + /// + /// If the closure returns an `Err`, it will be returned on the next call to [`step()`]. + /// + /// [`WebView`]: struct.WebView.html + /// [`Error::Dispatch`]: enum.Error.html#variant.Dispatch + /// [`step()`]: struct.WebView.html#method.step + pub fn dispatch(&self, f: F) -> WVResult + where + F: FnOnce(&mut WebView) -> WVResult + Send + 'static, + { + // Abort if WebView has been dropped. Otherwise, keep it alive until closure has been + // dispatched. + let mutex = self.live.upgrade().ok_or(Error::Dispatch)?; + let closure = Box::new(SendBoxFnOnce::new(f)); + let _lock = mutex.read().map_err(|_| Error::Dispatch)?; + + // Send closure to webview. + unsafe { + webview_dispatch( + self.inner, + Some(ffi_dispatch_handler:: as _), + Box::into_raw(closure) as _, + ) + } + Ok(()) + } +} + +unsafe impl Send for Handle {} +unsafe impl Sync for Handle {} + +fn read_str(s: &[u8]) -> String { + let end = s.iter().position(|&b| b == 0).map_or(0, |i| i + 1); + match CStr::from_bytes_with_nul(&s[..end]) { + Ok(s) => s.to_string_lossy().into_owned(), + Err(_) => "".to_string(), + } +} + +extern "C" fn ffi_dispatch_handler(webview: *mut CWebView, arg: *mut c_void) { + unsafe { + let mut handle = mem::ManuallyDrop::new(WebView::::from_ptr(webview)); + let result = { + let callback = + Box::,), WVResult>>::from_raw(arg as _); + callback.call(&mut handle) + }; + handle.user_data_wrapper_mut().result = result; + } +} + +extern "C" fn ffi_invoke_handler(webview: *mut CWebView, arg: *const c_char) { + unsafe { + let arg = CStr::from_ptr(arg).to_string_lossy().to_string(); + let mut handle = mem::ManuallyDrop::new(WebView::::from_ptr(webview)); + let result = ((*handle.user_data_wrapper_ptr()).invoke_handler)(&mut *handle, &arg); + handle.user_data_wrapper_mut().result = result; + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..6ddf4a4e4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +[WIP] diff --git a/lib/rust/Cargo.toml b/lib/rust/Cargo.toml new file mode 100644 index 000000000..b819a01f7 --- /dev/null +++ b/lib/rust/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "proton" +version = "0.1.0" +authors = ["Lucas Fernandes Gonçalves Nogueira "] +edition = "2018" + +[dependencies] +proton-ui = { path = "../../bindings/rust" } +serde_json = "1.0.39" +serde = "1.0" +serde_derive = "1.0" +dirs = "1.0" +ignore = "0.4.7" +phf = "0.7.21" +threadpool = "1.7" +rand = "0.7" +reqwest = "0.9" +pbr = "1" +zip = "0.5.0" +tempdir = "0.3" +semver = "0.9" +tempfile = "3" +either = "1.5.0" +tar = "0.4" +flate2 = "1" +hyper-old-types = "0.11.0" +sysinfo = "0.9" \ No newline at end of file diff --git a/lib/rust/rustfmt.toml b/lib/rust/rustfmt.toml new file mode 100644 index 000000000..9da25275e --- /dev/null +++ b/lib/rust/rustfmt.toml @@ -0,0 +1,13 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 2 +newline_style = "Auto" +use_small_heuristics = "Default" +reorder_imports = true +reorder_modules = true +remove_nested_parens = true +edition = "2015" +merge_derives = true +use_try_shorthand = false +use_field_init_shorthand = false +force_explicit_abi = true diff --git a/lib/rust/src/api/cmd.rs b/lib/rust/src/api/cmd.rs new file mode 100644 index 000000000..1891a1e77 --- /dev/null +++ b/lib/rust/src/api/cmd.rs @@ -0,0 +1,40 @@ +#[derive(Deserialize)] +#[serde(tag = "cmd", rename_all = "camelCase")] +pub enum Cmd { + Init, + ReadAsString { + path: String, + callback: String, + error: String, + }, + ReadAsBinary { + path: String, + callback: String, + error: String, + }, + Write { + file: String, + contents: String, + callback: String, + error: String, + }, + List { + path: String, + callback: String, + error: String, + }, + ListDirs { + path: String, + callback: String, + error: String, + }, + SetTitle { + title: String, + }, + Call { + command: String, + args: Vec, + callback: String, + error: String, + }, +} diff --git a/lib/rust/src/api/mod.rs b/lib/rust/src/api/mod.rs new file mode 100644 index 000000000..a7dbf6d11 --- /dev/null +++ b/lib/rust/src/api/mod.rs @@ -0,0 +1,63 @@ +mod cmd; + +use proton_ui::WebView; + +pub fn handler(webview: &mut WebView, arg: &str) -> bool { + use cmd::Cmd::*; + match serde_json::from_str(arg) { + Err(_) => false, + Ok(command) => { + match command { + Init => (), + ReadAsString { + path, + callback, + error, + } => { + super::file_system::read_text_file(webview, path, callback, error); + } + ReadAsBinary { + path, + callback, + error, + } => { + super::file_system::read_binary_file(webview, path, callback, error); + } + Write { + file, + contents, + callback, + error, + } => { + super::file_system::write_file(webview, file, contents, callback, error); + } + ListDirs { + path, + callback, + error, + } => { + super::file_system::list_dirs(webview, path, callback, error); + } + List { + path, + callback, + error, + } => { + super::file_system::list(webview, path, callback, error); + } + SetTitle { title } => { + webview.set_title(&title).unwrap(); + } + Call { + command, + args, + callback, + error, + } => { + super::command::call(webview, command, args, callback, error); + } + } + true + } + } +} diff --git a/lib/rust/src/command.rs b/lib/rust/src/command.rs new file mode 100755 index 000000000..d4ad38a7a --- /dev/null +++ b/lib/rust/src/command.rs @@ -0,0 +1,78 @@ +use proton_ui::WebView; + +use std::process::{Child, Command, Stdio}; + +use super::run_async; + +pub fn get_output(cmd: String, args: Vec, stdout: Stdio) -> Result { + Command::new(cmd) + .args(args) + .stdout(stdout) + .output() + .map_err(|err| err.to_string()) + .and_then(|output| { + if output.status.success() { + return Result::Ok(String::from_utf8_lossy(&output.stdout).to_string()); + } else { + return Result::Err(String::from_utf8_lossy(&output.stderr).to_string()); + } + }) +} + +// TODO use .exe for windows builds +pub fn format_command(path: String, command: String) -> String { + return format!("{}/./{}", path, command); +} + +pub fn relative_command(command: String) -> Result { + match std::env::current_exe()?.parent() { + Some(exe_dir) => return Ok(format_command(exe_dir.display().to_string(), command)), + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Could not evaluate executable dir".to_string(), + )) + } + } +} + +// TODO append .exe for windows builds +pub fn command_path(command: String) -> Result { + match std::env::current_exe()?.parent() { + Some(exe_dir) => return Ok(format!("{}/{}", exe_dir.display().to_string(), command)), + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Could not evaluate executable dir".to_string(), + )) + } + } +} + +pub fn spawn_relative_command( + command: String, + args: Vec, + stdout: Stdio, +) -> Result { + let cmd = relative_command(command)?; + Ok(Command::new(cmd).args(args).stdout(stdout).spawn()?) +} + +pub fn call( + webview: &mut WebView, + command: String, + args: Vec, + callback: String, + error: String, +) { + run_async( + webview, + || { + get_output(command, args, Stdio::piped()) + .map_err(|err| format!("`{}`", err)) + .map(|output| format!("`{}`", output)) + }, + callback, + error, + ); +} diff --git a/lib/rust/src/dir/mod.rs b/lib/rust/src/dir/mod.rs new file mode 100755 index 000000000..079e2675a --- /dev/null +++ b/lib/rust/src/dir/mod.rs @@ -0,0 +1,77 @@ +extern crate dirs; +extern crate tempfile; + +mod utils; +use ignore::Walk; +use std::fs; +use std::fs::metadata; +use utils::get_dir_name_from_path; + +use tempfile::tempdir; + +#[derive(Serialize)] +pub struct DiskEntry { + pub path: String, + pub is_dir: bool, + pub name: String, +} + +fn is_dir(file_name: String) -> Result { + match metadata(file_name.to_string()) { + Ok(md) => return Result::Ok(md.is_dir()), + Err(err) => return Result::Err(err.to_string()), + }; +} + +pub fn walk_dir(path_copy: String) -> Result, String> { + println!("Trying to walk: {}", path_copy.as_str()); + let mut files_and_dirs: Vec = vec![]; + for result in Walk::new(path_copy) { + match result { + Ok(entry) => { + let display_value = entry.path().display(); + let _dir_name = display_value.to_string(); + + match is_dir(display_value.to_string()) { + Ok(flag) => { + files_and_dirs.push(DiskEntry { + path: display_value.to_string(), + is_dir: flag, + name: display_value.to_string(), + }); + } + Err(_) => {} + } + } + Err(_) => {} + } + } + return Result::Ok(files_and_dirs); +} + +pub fn list_dir_contents(dir_path: &String) -> Result, String> { + fs::read_dir(dir_path) + .map_err(|err| err.to_string()) + .and_then(|paths| { + let mut dirs: Vec = vec![]; + for path in paths { + let dir_path = path.expect("dirpath error").path(); + let _dir_name = dir_path.display(); + dirs.push(DiskEntry { + path: format!("{}", _dir_name), + is_dir: true, + name: get_dir_name_from_path(_dir_name.to_string()), + }); + } + Ok(dirs) + }) +} + +pub fn with_temp_dir ()>( + callback: F, +) -> Result<(), std::io::Error> { + let dir = tempdir()?; + callback(&dir); + dir.close()?; + Ok(()) +} diff --git a/lib/rust/src/dir/utils.rs b/lib/rust/src/dir/utils.rs new file mode 100755 index 000000000..bd0f6c993 --- /dev/null +++ b/lib/rust/src/dir/utils.rs @@ -0,0 +1,4 @@ +pub fn get_dir_name_from_path(path: String) -> String { + let path_collect: Vec<&str> = path.split("/").collect(); + return path_collect[path_collect.len() - 1].to_string(); +} diff --git a/lib/rust/src/file/error.rs b/lib/rust/src/file/error.rs new file mode 100644 index 000000000..d9a198516 --- /dev/null +++ b/lib/rust/src/file/error.rs @@ -0,0 +1,46 @@ +use std; +use zip::result::ZipError; + +#[derive(Debug)] +pub enum Error { + Extract(String), + Io(std::io::Error), + Zip(ZipError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use Error::*; + match *self { + Extract(ref s) => write!(f, "ExtractError: {}", s), + Io(ref e) => write!(f, "IoError: {}", e), + Zip(ref e) => write!(f, "ZipError: {}", e), + } + } +} + +impl std::error::Error for Error { + fn description(&self) -> &str { + "File Error" + } + + fn cause(&self) -> Option<&std::error::Error> { + use Error::*; + Some(match *self { + Io(ref e) => e, + _ => return None, + }) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: ZipError) -> Self { + Error::Zip(e) + } +} diff --git a/lib/rust/src/file/extract.rs b/lib/rust/src/file/extract.rs new file mode 100644 index 000000000..1e80bf759 --- /dev/null +++ b/lib/rust/src/file/extract.rs @@ -0,0 +1,189 @@ +extern crate either; +extern crate flate2; +extern crate tar; +extern crate zip; + +use super::error::*; +use either::Either; +use std::fs; +use std::io; +use std::path; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ArchiveFormat { + Tar(Option), + Plain(Option), + Zip, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Compression { + Gz, +} + +#[derive(Debug)] +pub struct Extract<'a> { + source: &'a path::Path, + archive_format: Option, +} + +fn detect_archive_type(path: &path::Path) -> ArchiveFormat { + match path.extension() { + Some(extension) if extension == std::ffi::OsStr::new("zip") => ArchiveFormat::Zip, + Some(extension) if extension == std::ffi::OsStr::new("tar") => ArchiveFormat::Tar(None), + Some(extension) if extension == std::ffi::OsStr::new("gz") => match path + .file_stem() + .map(|e| path::Path::new(e)) + .and_then(|f| f.extension()) + { + Some(extension) if extension == std::ffi::OsStr::new("tar") => { + ArchiveFormat::Tar(Some(Compression::Gz)) + } + _ => ArchiveFormat::Plain(Some(Compression::Gz)), + }, + _ => ArchiveFormat::Plain(None), + } +} + +impl<'a> Extract<'a> { + /// Create an `Extractor from a source path + pub fn from_source(source: &'a path::Path) -> Extract<'a> { + Self { + source, + archive_format: None, + } + } + + /// Specify an archive format of the source being extracted. If not specified, the + /// archive format will determined from the file extension. + pub fn archive_format(&mut self, format: ArchiveFormat) -> &mut Self { + self.archive_format = Some(format); + self + } + + fn get_archive_reader( + source: fs::File, + compression: Option, + ) -> Either> { + match compression { + Some(Compression::Gz) => Either::Right(flate2::read::GzDecoder::new(source)), + None => Either::Left(source), + } + } + + /// Extract an entire source archive into a specified path. If the source is a single compressed + /// file and not an archive, it will be extracted into a file with the same name inside of + /// `into_dir`. + pub fn extract_into(&self, into_dir: &path::Path) -> Result<(), Error> { + let source = fs::File::open(self.source)?; + let archive = self + .archive_format + .unwrap_or_else(|| detect_archive_type(&self.source)); + + match archive { + ArchiveFormat::Plain(compression) | ArchiveFormat::Tar(compression) => { + let mut reader = Self::get_archive_reader(source, compression); + + match archive { + ArchiveFormat::Plain(_) => { + match fs::create_dir_all(into_dir) { + Ok(_) => (), + Err(e) => { + if e.kind() != io::ErrorKind::AlreadyExists { + return Err(Error::Io(e)); + } + } + } + let file_name = self + .source + .file_name() + .ok_or_else(|| Error::Extract("Extractor source has no file-name".into()))?; + let mut out_path = into_dir.join(file_name); + out_path.set_extension(""); + let mut out_file = fs::File::create(&out_path)?; + io::copy(&mut reader, &mut out_file)?; + } + ArchiveFormat::Tar(_) => { + let mut archive = tar::Archive::new(reader); + archive.unpack(into_dir)?; + } + _ => unreachable!(), + }; + } + ArchiveFormat::Zip => { + let mut archive = zip::ZipArchive::new(source)?; + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let path = into_dir.join(file.name()); + let mut output = fs::File::create(path)?; + io::copy(&mut file, &mut output)?; + } + } + }; + Ok(()) + } + + /// Extract a single file from a source and save to a file of the same name in `into_dir`. + /// If the source is a single compressed file, it will be saved with the name `file_to_extract` + /// in the specified `into_dir`. + pub fn extract_file>( + &self, + into_dir: &path::Path, + file_to_extract: T, + ) -> Result<(), Error> { + let file_to_extract = file_to_extract.as_ref(); + let source = fs::File::open(self.source)?; + let archive = self + .archive_format + .unwrap_or_else(|| detect_archive_type(&self.source)); + + match archive { + ArchiveFormat::Plain(compression) | ArchiveFormat::Tar(compression) => { + let mut reader = Self::get_archive_reader(source, compression); + + match archive { + ArchiveFormat::Plain(_) => { + match fs::create_dir_all(into_dir) { + Ok(_) => (), + Err(e) => { + if e.kind() != io::ErrorKind::AlreadyExists { + return Err(Error::Io(e)); + } + } + } + let file_name = file_to_extract + .file_name() + .ok_or_else(|| Error::Extract("Extractor source has no file-name".into()))?; + let out_path = into_dir.join(file_name); + let mut out_file = fs::File::create(&out_path)?; + io::copy(&mut reader, &mut out_file)?; + } + ArchiveFormat::Tar(_) => { + let mut archive = tar::Archive::new(reader); + let mut entry = archive + .entries()? + .filter_map(|e| e.ok()) + .find(|e| e.path().ok().filter(|p| p == file_to_extract).is_some()) + .ok_or_else(|| { + Error::Extract(format!( + "Could not find the required path in the archive: {:?}", + file_to_extract + )) + })?; + entry.unpack_in(into_dir)?; + } + _ => { + panic!("Unreasonable code"); + } + }; + } + ArchiveFormat::Zip => { + let mut archive = zip::ZipArchive::new(source)?; + let mut file = archive.by_name(file_to_extract.to_str().unwrap())?; + let mut output = fs::File::create(into_dir.join(file.name()))?; + io::copy(&mut file, &mut output)?; + } + }; + Ok(()) + } +} diff --git a/lib/rust/src/file/file_move.rs b/lib/rust/src/file/file_move.rs new file mode 100644 index 000000000..c11c622d1 --- /dev/null +++ b/lib/rust/src/file/file_move.rs @@ -0,0 +1,62 @@ +use std::fs; +use std::path; + +use super::error::*; + +/// Moves a file from the given path to the specified destination. +/// +/// `source` and `dest` must be on the same filesystem. +/// If `replace_using_temp` is specified, the destination file will be +/// replaced using the given temporary path. +/// +/// * Errors: +/// * Io - copying / renaming +#[derive(Debug)] +pub struct Move<'a> { + source: &'a path::Path, + temp: Option<&'a path::Path>, +} +impl<'a> Move<'a> { + /// Specify source file + pub fn from_source(source: &'a path::Path) -> Move<'a> { + Self { source, temp: None } + } + + /// If specified and the destination file already exists, the "destination" + /// file will be moved to the given temporary location before the "source" + /// file is moved to the "destination" file. + /// + /// In the event of an `io` error while renaming "source" to "destination", + /// the temporary file will be moved back to "destination". + /// + /// The `temp` dir must be explicitly provided since `rename` operations require + /// files to live on the same filesystem. + pub fn replace_using_temp(&mut self, temp: &'a path::Path) -> &mut Self { + self.temp = Some(temp); + self + } + + /// Move source file to specified destination + pub fn to_dest(&self, dest: &path::Path) -> Result<(), Error> { + match self.temp { + None => { + fs::rename(self.source, dest)?; + } + Some(temp) => { + println!("dest {}", dest.to_str().unwrap()); + println!("temp {}", temp.to_str().unwrap()); + println!("source {}", self.source.to_str().unwrap()); + if dest.exists() { + fs::rename(dest, temp)?; + if let Err(e) = fs::rename(self.source, dest) { + fs::rename(temp, dest)?; + return Err(Error::from(e)); + } + } else { + fs::rename(self.source, dest)?; + } + } + }; + Ok(()) + } +} diff --git a/lib/rust/src/file/mod.rs b/lib/rust/src/file/mod.rs new file mode 100644 index 000000000..c81d4040d --- /dev/null +++ b/lib/rust/src/file/mod.rs @@ -0,0 +1,21 @@ +use std::fs; + +extern crate serde_json; + +mod error; +mod extract; +mod file_move; + +pub use error::Error; +pub use extract::*; +pub use file_move::*; + +pub fn read_string(file: String) -> Result { + fs::read_to_string(file) + .map_err(|err| err.to_string()) + .map(|c| c) +} + +pub fn read_binary(file: String) -> Result, String> { + fs::read(file).map_err(|err| err.to_string()).map(|b| b) +} diff --git a/lib/rust/src/file_system.rs b/lib/rust/src/file_system.rs new file mode 100755 index 000000000..11450d58c --- /dev/null +++ b/lib/rust/src/file_system.rs @@ -0,0 +1,100 @@ +use proton_ui::WebView; + +use super::dir; +use super::file; +use super::run_async; + +use std::fs::File; +use std::io::Write; + +pub fn list(webview: &mut WebView, path: String, callback: String, error: String) { + run_async( + webview, + move || { + dir::walk_dir(path.to_string()) + .and_then(|f| serde_json::to_string(&f).map_err(|err| err.to_string())) + }, + callback, + error, + ); +} + +pub fn list_dirs( + webview: &mut WebView, + path: String, + callback: String, + error: String, +) { + run_async( + webview, + move || { + dir::list_dir_contents(&path) + .and_then(|f| serde_json::to_string(&f).map_err(|err| err.to_string())) + }, + callback, + error, + ); +} + +pub fn write_file( + webview: &mut WebView, + file: String, + contents: String, + callback: String, + error: String, +) { + run_async( + webview, + move || { + File::create(file) + .map_err(|err| err.to_string()) + .and_then(|mut f| { + f.write_all(contents.as_bytes()) + .map_err(|err| err.to_string()) + .map(|_| "".to_string()) + }) + }, + callback, + error, + ); +} + +pub fn read_text_file( + webview: &mut WebView, + path: String, + callback: String, + error: String, +) { + run_async( + webview, + move || { + file::read_string(path).and_then(|f| { + serde_json::to_string(&f) + .map_err(|err| err.to_string()) + .map(|s| s.to_string()) + }) + }, + callback, + error, + ); +} + +pub fn read_binary_file( + webview: &mut WebView, + path: String, + callback: String, + error: String, +) { + run_async( + webview, + move || { + file::read_binary(path).and_then(|f| { + serde_json::to_string(&f) + .map_err(|err| err.to_string()) + .map(|s| s.to_string()) + }) + }, + callback, + error, + ); +} diff --git a/lib/rust/src/http/error.rs b/lib/rust/src/http/error.rs new file mode 100644 index 000000000..4ac9ffe91 --- /dev/null +++ b/lib/rust/src/http/error.rs @@ -0,0 +1,57 @@ +use reqwest; +use serde_json; +use std; + +#[derive(Debug)] +pub enum Error { + Download(String), + Json(serde_json::Error), + Reqwest(reqwest::Error), + Io(std::io::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use Error::*; + match *self { + Download(ref s) => write!(f, "DownloadError: {}", s), + Json(ref e) => write!(f, "JsonError: {}", e), + Reqwest(ref e) => write!(f, "ReqwestError: {}", e), + Io(ref e) => write!(f, "IoError: {}", e), + } + } +} + +impl std::error::Error for Error { + fn description(&self) -> &str { + "Http Error" + } + + fn cause(&self) -> Option<&std::error::Error> { + use Error::*; + Some(match *self { + Json(ref e) => e, + Reqwest(ref e) => e, + Io(ref e) => e, + _ => return None, + }) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Json(e) + } +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Error::Reqwest(e) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} diff --git a/lib/rust/src/http/mod.rs b/lib/rust/src/http/mod.rs new file mode 100644 index 000000000..0097e64ee --- /dev/null +++ b/lib/rust/src/http/mod.rs @@ -0,0 +1,80 @@ +extern crate pbr; +extern crate reqwest; + +use serde::Serialize; +use std::io; +mod error; +pub use self::error::Error; + +pub fn get(url: &String) -> Result { + let response = reqwest::Client::new().get(url).send()?; + Ok(response) +} + +pub fn post_as_json( + url: &String, + payload: &T, +) -> Result { + let response = reqwest::Client::new().post(url).json(payload).send()?; + Ok(response) +} + +pub fn download( + url: &String, + mut dest: T, + display_progress: bool, +) -> Result<(), Error> { + use io::BufRead; + + set_ssl_vars!(); + + let resp = get(url)?; + let size = resp + .headers() + .get(reqwest::header::CONTENT_LENGTH) + .map(|val| { + val + .to_str() + .map(|s| s.parse::().unwrap_or(0)) + .unwrap_or(0) + }) + .unwrap_or(0); + + if !resp.status().is_success() { + bail!( + Error::Download, + "Download request failed with status: {:?}", + resp.status() + ) + } + + let show_progress = if size == 0 { false } else { display_progress }; + + let mut src = io::BufReader::new(resp); + let mut bar = if show_progress { + let mut bar = pbr::ProgressBar::new(size); + bar.set_units(pbr::Units::Bytes); + bar.format("[=> ]"); + Some(bar) + } else { + None + }; + loop { + let n = { + let buf = src.fill_buf()?; + dest.write_all(&buf)?; + buf.len() + }; + if n == 0 { + break; + } + src.consume(n); + if let Some(ref mut bar) = bar { + bar.add(n as u64); + } + } + if show_progress { + println!(" ... Done"); + } + Ok(()) +} diff --git a/lib/rust/src/lib.rs b/lib/rust/src/lib.rs new file mode 100644 index 000000000..2ad400f4b --- /dev/null +++ b/lib/rust/src/lib.rs @@ -0,0 +1,43 @@ +extern crate threadpool; +#[macro_use] +extern crate serde_derive; + +#[macro_use] +mod macros; + +pub mod api; +pub mod command; +pub mod dir; +pub mod file; +pub mod file_system; +pub mod http; +pub mod platform; +pub mod process; +pub mod rpc; +pub mod tcp; +pub mod updater; +pub mod version; + +extern crate proton_ui; +use proton_ui::WebView; + +use threadpool::ThreadPool; + +thread_local!(static POOL: ThreadPool = ThreadPool::new(4)); + +pub fn run_async Result + Send + 'static>( + webview: &mut WebView, + what: F, + callback: String, + error: String, +) { + let handle = webview.handle(); + POOL.with(|thread| { + thread.execute(move || { + let callback_string = rpc::format_callback_result(what(), callback, error); + handle + .dispatch(move |_webview| _webview.eval(callback_string.as_str())) + .unwrap() + }); + }); +} diff --git a/lib/rust/src/macros.rs b/lib/rust/src/macros.rs new file mode 100644 index 000000000..4012fd566 --- /dev/null +++ b/lib/rust/src/macros.rs @@ -0,0 +1,46 @@ +/// Helper for formatting `errors::Error`s +macro_rules! format_err { + ($e_type:expr, $literal:expr) => { + $e_type(format!($literal)) + }; + ($e_type:expr, $literal:expr, $($arg:expr),*) => { + $e_type(format!($literal, $($arg),*)) + }; +} + +/// Helper for formatting `errors::Error`s and returning early +macro_rules! bail { + ($e_type:expr, $literal:expr) => { + return Err(format_err!($e_type, $literal)) + }; + ($e_type:expr, $literal:expr, $($arg:expr),*) => { + return Err(format_err!($e_type, $literal, $($arg),*)) + }; +} + +/// Helper to `print!` and immediately `flush` `stdout` +macro_rules! print_flush { + ($literal:expr) => { + print!($literal); + ::std::io::Write::flush(&mut ::std::io::stdout())?; + }; + ($literal:expr, $($arg:expr),*) => { + print!($literal, $($arg),*); + ::std::io::Write::flush(&mut ::std::io::stdout())?; + } +} + +/// Set ssl cert env. vars to make sure openssl can find required files +macro_rules! set_ssl_vars { + () => { + #[cfg(target_os = "linux")] + { + if ::std::env::var_os("SSL_CERT_FILE").is_none() { + ::std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt"); + } + if ::std::env::var_os("SSL_CERT_DIR").is_none() { + ::std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs"); + } + } + }; +} diff --git a/lib/rust/src/platform/error.rs b/lib/rust/src/platform/error.rs new file mode 100644 index 000000000..e8413d5e0 --- /dev/null +++ b/lib/rust/src/platform/error.rs @@ -0,0 +1,29 @@ +use std; + +#[derive(Debug)] +pub enum Error { + Arch(String), + Target(String), + Abi(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use Error::*; + match *self { + Arch(ref s) => write!(f, "ArchError: {}", s), + Target(ref e) => write!(f, "TargetError: {}", e), + Abi(ref e) => write!(f, "AbiError: {}", e), + } + } +} + +impl std::error::Error for Error { + fn description(&self) -> &str { + "Platform Error" + } + + fn cause(&self) -> Option<&std::error::Error> { + return None; + } +} diff --git a/lib/rust/src/platform/mod.rs b/lib/rust/src/platform/mod.rs new file mode 100644 index 000000000..bbc17d7f9 --- /dev/null +++ b/lib/rust/src/platform/mod.rs @@ -0,0 +1,54 @@ +pub mod error; +use error::*; + +/// Try to determine the current target triple. +/// +/// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) or an +/// `Error::Config` if the current config cannot be determined or is not some combination of the +/// following values: +/// `linux, mac, windows` -- `i686, x86, armv7` -- `gnu, musl, msvc` +/// +/// * Errors: +/// * Unexpected system config +pub fn target_triple() -> Result { + let arch = if cfg!(target_arch = "x86") { + "i686" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else if cfg!(target_arch = "arm") { + "armv7" + } else { + bail!(Error::Arch, "Unable to determine target-architecture") + }; + + let os = if cfg!(target_os = "linux") { + "unknown-linux" + } else if cfg!(target_os = "macos") { + "apple-darwin" + } else if cfg!(target_os = "windows") { + "pc-windows" + } else if cfg!(target_os = "freebsd") { + "unknown-freebsd" + } else { + bail!(Error::Target, "Unable to determine target-os"); + }; + + let s; + let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") { + os + } else { + let env = if cfg!(target_env = "gnu") { + "gnu" + } else if cfg!(target_env = "gnu") { + "musl" + } else if cfg!(target_env = "msvc") { + "msvc" + } else { + bail!(Error::Abi, "Unable to determine target-environment") + }; + s = format!("{}-{}", os, env); + &s + }; + + Ok(format!("{}-{}", arch, os)) +} diff --git a/lib/rust/src/process.rs b/lib/rust/src/process.rs new file mode 100644 index 000000000..dcaa7dca1 --- /dev/null +++ b/lib/rust/src/process.rs @@ -0,0 +1,18 @@ +extern crate sysinfo; + +pub use sysinfo::{Process, ProcessExt, Signal, System, SystemExt}; + +pub fn get_parent_process(system: &mut sysinfo::System) -> Result<&Process, String> { + let pid = sysinfo::get_current_pid().unwrap(); + system.refresh_process(pid); + let current_process = system + .get_process(pid) + .ok_or("Could not get current process")?; + let parent_pid = current_process.parent().ok_or("Could not get parent PID")?; + let parent_process = system + .get_process(parent_pid) + .ok_or("Could not get parent process")?; + + println!("{}", pid); + Ok(parent_process) +} diff --git a/lib/rust/src/rpc.rs b/lib/rust/src/rpc.rs new file mode 100755 index 000000000..f44220338 --- /dev/null +++ b/lib/rust/src/rpc.rs @@ -0,0 +1,15 @@ +pub fn format_callback(function_name: String, arg: String) -> String { + let formatted_string = &format!("window[\"{}\"]({})", function_name, arg); + return formatted_string.to_string(); +} + +pub fn format_callback_result( + result: Result, + callback: String, + error_callback: String, +) -> String { + match result { + Ok(res) => return format_callback(callback, res), + Err(err) => return format_callback(error_callback, format!("\"{}\"", err)), + } +} diff --git a/lib/rust/src/tcp.rs b/lib/rust/src/tcp.rs new file mode 100644 index 000000000..433413004 --- /dev/null +++ b/lib/rust/src/tcp.rs @@ -0,0 +1,25 @@ +use std::net::TcpListener; + +extern crate rand; + +use rand::distributions::{Distribution, Uniform}; + +pub fn get_available_port() -> Option { + let mut rng = rand::thread_rng(); + let die = Uniform::from(8000..9000); + + for _i in 0..100 { + let port = die.sample(&mut rng); + if port_is_available(port) { + return Some(port); + } + } + None +} + +pub fn port_is_available(port: u16) -> bool { + match TcpListener::bind(("127.0.0.1", port)) { + Ok(_) => true, + Err(_) => false, + } +} diff --git a/lib/rust/src/updater/error.rs b/lib/rust/src/updater/error.rs new file mode 100644 index 000000000..5476ecccd --- /dev/null +++ b/lib/rust/src/updater/error.rs @@ -0,0 +1,78 @@ +use super::super::file; +use super::super::http; +use super::super::version; +use reqwest; +use std; +use zip::result::ZipError; + +#[derive(Debug)] +pub enum Error { + Updater(String), + Release(String), + Network(String), + Config(String), + Io(std::io::Error), + Zip(ZipError), + File(file::Error), + Version(version::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use Error::*; + match *self { + Updater(ref s) => write!(f, "UpdaterError: {}", s), + Release(ref s) => write!(f, "ReleaseError: {}", s), + Network(ref s) => write!(f, "NetworkError: {}", s), + Config(ref s) => write!(f, "ConfigError: {}", s), + Io(ref e) => write!(f, "IoError: {}", e), + Zip(ref e) => write!(f, "ZipError: {}", e), + File(ref e) => write!(f, "FileError: {}", e), + Version(ref e) => write!(f, "VersionError: {}", e), + } + } +} + +impl std::error::Error for Error { + fn description(&self) -> &str { + "Updater Error" + } + + fn cause(&self) -> Option<&std::error::Error> { + use Error::*; + Some(match *self { + Io(ref e) => e, + _ => return None, + }) + } +} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: file::Error) -> Self { + Error::File(e) + } +} + +impl From for Error { + fn from(e: http::Error) -> Self { + Error::Network(e.to_string()) + } +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Error::Network(e.to_string()) + } +} + +impl From for Error { + fn from(e: version::Error) -> Self { + Error::Version(e) + } +} diff --git a/lib/rust/src/updater/github/mod.rs b/lib/rust/src/updater/github/mod.rs new file mode 100644 index 000000000..ba41cf1c0 --- /dev/null +++ b/lib/rust/src/updater/github/mod.rs @@ -0,0 +1,43 @@ +mod release; +pub use super::error::Error; +pub use release::*; + +use super::super::http; + +pub fn get_latest_release(repo_owner: &str, repo_name: &str) -> Result { + set_ssl_vars!(); + let api_url = format!( + "https://api.github.com/repos/{}/{}/releases/latest", + repo_owner, repo_name + ); + let mut resp = http::get(&api_url)?; + if !resp.status().is_success() { + bail!( + Error::Network, + "api request failed with status: {:?} - for: {:?}", + resp.status(), + api_url + ) + } + let json = resp.json::()?; + Ok(Release::parse(&json)?) +} + +pub fn get_release_version(repo_owner: &str, repo_name: &str, ver: &str) -> Result { + set_ssl_vars!(); + let api_url = format!( + "https://api.github.com/repos/{}/{}/releases/tags/{}", + repo_owner, repo_name, ver + ); + let mut resp = http::get(&api_url)?; + if !resp.status().is_success() { + bail!( + Error::Network, + "api request failed with status: {:?} - for: {:?}", + resp.status(), + api_url + ) + } + let json = resp.json::()?; + Ok(Release::parse(&json)?) +} diff --git a/lib/rust/src/updater/github/release.rs b/lib/rust/src/updater/github/release.rs new file mode 100644 index 000000000..79c98469e --- /dev/null +++ b/lib/rust/src/updater/github/release.rs @@ -0,0 +1,215 @@ +use super::super::error::*; +use hyper_old_types::header::{LinkValue, RelationType}; +use serde_json; + +/// GitHub release-asset information +#[derive(Clone, Debug)] +pub struct ReleaseAsset { + pub download_url: String, + pub name: String, +} +impl ReleaseAsset { + /// Parse a release-asset json object + /// + /// Errors: + /// * Missing required name & browser_download_url keys + fn from_asset(asset: &serde_json::Value) -> Result { + let download_url = asset["browser_download_url"] + .as_str() + .ok_or_else(|| format_err!(Error::Release, "Asset missing `browser_download_url`"))?; + let name = asset["name"] + .as_str() + .ok_or_else(|| format_err!(Error::Release, "Asset missing `name`"))?; + Ok(ReleaseAsset { + download_url: download_url.to_owned(), + name: name.to_owned(), + }) + } +} + +#[derive(Clone, Debug)] +pub struct Release { + pub name: String, + pub body: String, + pub tag: String, + pub date_created: String, + pub assets: Vec, +} +impl Release { + pub fn parse(release: &serde_json::Value) -> Result { + let tag = release["tag_name"] + .as_str() + .ok_or_else(|| format_err!(Error::Release, "Release missing `tag_name`"))?; + let date_created = release["created_at"] + .as_str() + .ok_or_else(|| format_err!(Error::Release, "Release missing `created_at`"))?; + let name = release["name"].as_str().unwrap_or(tag); + let body = release["body"].as_str().unwrap_or(""); + let assets = release["assets"] + .as_array() + .ok_or_else(|| format_err!(Error::Release, "No assets found"))?; + let assets = assets + .iter() + .map(ReleaseAsset::from_asset) + .collect::, Error>>()?; + Ok(Release { + name: name.to_owned(), + body: body.to_owned(), + tag: tag.to_owned(), + date_created: date_created.to_owned(), + assets, + }) + } + + /// Check if release has an asset who's name contains the specified `target` + pub fn has_target_asset(&self, target: &str) -> bool { + self.assets.iter().any(|asset| asset.name.contains(target)) + } + + /// Return the first `ReleaseAsset` for the current release who's name + /// contains the specified `target` + pub fn asset_for(&self, target: &str) -> Option { + self + .assets + .iter() + .filter(|asset| asset.name.contains(target)) + .cloned() + .nth(0) + } + + pub fn version(&self) -> &str { + self.tag.trim_start_matches('v') + } +} + +/// `ReleaseList` Builder +#[derive(Clone, Debug)] +pub struct ReleaseListBuilder { + repo_owner: Option, + repo_name: Option, + target: Option, +} +impl ReleaseListBuilder { + /// Set the repo owner, used to build a github api url + pub fn repo_owner(&mut self, owner: &str) -> &mut Self { + self.repo_owner = Some(owner.to_owned()); + self + } + + /// Set the repo name, used to build a github api url + pub fn repo_name(&mut self, name: &str) -> &mut Self { + self.repo_name = Some(name.to_owned()); + self + } + + /// Set the optional arch `target` name, used to filter available releases + pub fn target(&mut self, target: &str) -> &mut Self { + self.target = Some(target.to_owned()); + self + } + + /// Verify builder args, returning a `ReleaseList` + pub fn build(&self) -> Result { + Ok(ReleaseList { + repo_owner: if let Some(ref owner) = self.repo_owner { + owner.to_owned() + } else { + bail!(Error::Config, "`repo_owner` required") + }, + repo_name: if let Some(ref name) = self.repo_name { + name.to_owned() + } else { + bail!(Error::Config, "`repo_name` required") + }, + target: self.target.clone(), + }) + } +} + +/// `ReleaseList` provides a builder api for querying a GitHub repo, +/// returning a `Vec` of available `Release`s +#[derive(Clone, Debug)] +pub struct ReleaseList { + repo_owner: String, + repo_name: String, + target: Option, +} +impl ReleaseList { + /// Initialize a ReleaseListBuilder + pub fn configure() -> ReleaseListBuilder { + ReleaseListBuilder { + repo_owner: None, + repo_name: None, + target: None, + } + } + + /// Retrieve a list of `Release`s. + /// If specified, filter for those containing a specified `target` + pub fn fetch(self) -> Result, Error> { + set_ssl_vars!(); + let api_url = format!( + "https://api.github.com/repos/{}/{}/releases", + self.repo_owner, self.repo_name + ); + let releases = Self::fetch_releases(&api_url)?; + let releases = match self.target { + None => releases, + Some(ref target) => releases + .into_iter() + .filter(|r| r.has_target_asset(target)) + .collect::>(), + }; + Ok(releases) + } + + fn fetch_releases(url: &str) -> Result, Error> { + let mut resp = reqwest::get(url)?; + if !resp.status().is_success() { + bail!( + Error::Network, + "api request failed with status: {:?} - for: {:?}", + resp.status(), + url + ) + } + let releases = resp.json::()?; + let releases = releases + .as_array() + .ok_or_else(|| format_err!(Error::Release, "No releases found"))?; + let mut releases = releases + .iter() + .map(Release::parse) + .collect::, Error>>()?; + + // handle paged responses containing `Link` header: + // `Link: ; rel="next"` + let headers = resp.headers(); + let links = headers.get_all(reqwest::header::LINK); + + let next_link = links + .iter() + .filter_map(|link| { + if let Ok(link) = link.to_str() { + let lv = LinkValue::new(link.to_owned()); + if let Some(rels) = lv.rel() { + if rels.contains(&RelationType::Next) { + return Some(link); + } + } + None + } else { + None + } + }) + .nth(0); + + Ok(match next_link { + None => releases, + Some(link) => { + releases.extend(Self::fetch_releases(link)?); + releases + } + }) + } +} diff --git a/lib/rust/src/updater/mod.rs b/lib/rust/src/updater/mod.rs new file mode 100644 index 000000000..bbac888a4 --- /dev/null +++ b/lib/rust/src/updater/mod.rs @@ -0,0 +1,270 @@ +extern crate hyper_old_types; + +use std::env; +use std::fs; +use std::path::PathBuf; + +use super::file::{Extract, Move}; +use super::http; + +pub mod github; + +mod error; +pub use error::Error; + +/// Status returned after updating +/// +/// Wrapped `String`s are version tags +#[derive(Debug, Clone)] +pub enum Status { + UpToDate(String), + Updated(String), +} +impl Status { + /// Return the version tag + pub fn version(&self) -> &str { + use Status::*; + match *self { + UpToDate(ref s) => s, + Updated(ref s) => s, + } + } + + /// Returns `true` if `Status::UpToDate` + pub fn uptodate(&self) -> bool { + match *self { + Status::UpToDate(_) => true, + _ => false, + } + } + + /// Returns `true` if `Status::Updated` + pub fn updated(&self) -> bool { + match *self { + Status::Updated(_) => true, + _ => false, + } + } +} + +#[derive(Clone, Debug)] +pub struct Release { + pub version: String, + pub asset_name: String, + pub download_url: String, +} + +#[derive(Debug)] +pub struct UpdateBuilder { + release: Option, + bin_name: Option, + bin_install_path: Option, + bin_path_in_archive: Option, + show_download_progress: bool, + show_output: bool, + current_version: Option, +} +impl UpdateBuilder { + /// Initialize a new builder, defaulting the `bin_install_path` to the current + /// executable's path + /// + /// * Errors: + /// * Io - Determining current exe path + pub fn new() -> Result { + Ok(Self { + release: None, + bin_name: None, + bin_install_path: Some(env::current_exe()?), + bin_path_in_archive: None, + show_download_progress: false, + show_output: true, + current_version: None, + }) + } + + pub fn release(&mut self, release: Release) -> &mut Self { + self.release = Some(release); + self + } + + /// Set the current app version, used to compare against the latest available version. + /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml` + pub fn current_version(&mut self, ver: &str) -> &mut Self { + self.current_version = Some(ver.to_owned()); + self + } + + /// Set the exe's name. Also sets `bin_path_in_archive` if it hasn't already been set. + pub fn bin_name(&mut self, name: &str) -> &mut Self { + self.bin_name = Some(name.to_owned()); + if self.bin_path_in_archive.is_none() { + self.bin_path_in_archive = Some(PathBuf::from(name)); + } + self + } + + /// Set the installation path for the new exe, defaults to the current + /// executable's path + pub fn bin_install_path(&mut self, bin_install_path: &str) -> &mut Self { + self.bin_install_path = Some(PathBuf::from(bin_install_path)); + self + } + + /// Set the path of the exe inside the release tarball. This is the location + /// of the executable relative to the base of the tar'd directory and is the + /// path that will be copied to the `bin_install_path`. If not specified, this + /// will default to the value of `bin_name`. This only needs to be specified if + /// the path to the binary (from the root of the tarball) is not equal to just + /// the `bin_name`. + /// + /// # Example + /// + /// For a tarball `myapp.tar.gz` with the contents: + /// + /// ```shell + /// myapp.tar/ + /// |------- bin/ + /// | |--- myapp # <-- executable + /// ``` + /// + /// The path provided should be: + /// + /// ``` + /// # use proton::updater::Update; + /// # fn run() -> Result<(), Box<::std::error::Error>> { + /// Update::configure()? + /// .bin_path_in_archive("bin/myapp") + /// # .build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn bin_path_in_archive(&mut self, bin_path: &str) -> &mut Self { + self.bin_path_in_archive = Some(PathBuf::from(bin_path)); + self + } + + /// Toggle download progress bar, defaults to `off`. + pub fn show_download_progress(&mut self, show: bool) -> &mut Self { + self.show_download_progress = show; + self + } + + /// Toggle update output information, defaults to `true`. + pub fn show_output(&mut self, show: bool) -> &mut Self { + self.show_output = show; + self + } + + /// Confirm config and create a ready-to-use `Update` + /// + /// * Errors: + /// * Config - Invalid `Update` configuration + pub fn build(&self) -> Result { + Ok(Update { + release: if let Some(ref release) = self.release { + release.to_owned() + } else { + bail!(Error::Config, "`release` required") + }, + bin_name: if let Some(ref name) = self.bin_name { + name.to_owned() + } else { + bail!(Error::Config, "`bin_name` required") + }, + bin_install_path: if let Some(ref path) = self.bin_install_path { + path.to_owned() + } else { + bail!(Error::Config, "`bin_install_path` required") + }, + bin_path_in_archive: if let Some(ref path) = self.bin_path_in_archive { + path.to_owned() + } else { + bail!(Error::Config, "`bin_path_in_archive` required") + }, + current_version: if let Some(ref ver) = self.current_version { + ver.to_owned() + } else { + bail!(Error::Config, "`current_version` required") + }, + show_download_progress: self.show_download_progress, + show_output: self.show_output, + }) + } +} + +/// Updates to a specified or latest release distributed +#[derive(Debug)] +pub struct Update { + release: Release, + current_version: String, + bin_name: String, + bin_install_path: PathBuf, + bin_path_in_archive: PathBuf, + show_download_progress: bool, + show_output: bool, +} +impl Update { + /// Initialize a new `Update` builder + pub fn configure() -> Result { + UpdateBuilder::new() + } + + fn print_flush(&self, msg: &str) -> Result<(), Error> { + if self.show_output { + print_flush!("{}", msg); + } + Ok(()) + } + + fn println(&self, msg: &str) { + if self.show_output { + println!("{}", msg); + } + } + + pub fn update(self) -> Result { + self.println(&format!( + "Checking current version... v{}", + self.current_version + )); + + if self.show_output { + println!("\n{} release status:", self.bin_name); + println!(" * Current exe: {:?}", self.bin_install_path); + println!(" * New exe download url: {:?}", self.release.download_url); + println!( + "\nThe new release will be downloaded/extracted and the existing binary will be replaced." + ); + } + + let tmp_dir_parent = self + .bin_install_path + .parent() + .ok_or_else(|| Error::Updater("Failed to determine parent dir".into()))?; + let tmp_dir = + tempdir::TempDir::new_in(&tmp_dir_parent, &format!("{}_download", self.bin_name))?; + let tmp_archive_path = tmp_dir.path().join(&self.release.asset_name); + let mut tmp_archive = fs::File::create(&tmp_archive_path)?; + + self.println("Downloading..."); + http::download( + &self.release.download_url, + &mut tmp_archive, + self.show_download_progress, + )?; + + self.print_flush("Extracting archive... ")?; + Extract::from_source(&tmp_archive_path) + .extract_file(&tmp_dir.path(), &self.bin_path_in_archive)?; + let new_exe = tmp_dir.path().join(&self.bin_path_in_archive); + self.println("Done"); + + self.print_flush("Replacing binary file... ")?; + let tmp_file = tmp_dir.path().join(&format!("__{}_backup", self.bin_name)); + Move::from_source(&new_exe) + .replace_using_temp(&tmp_file) + .to_dest(&self.bin_install_path)?; + self.println("Done"); + Ok(Status::Updated(self.release.version)) + } +} diff --git a/lib/rust/src/version/error.rs b/lib/rust/src/version/error.rs new file mode 100644 index 000000000..1b084e60c --- /dev/null +++ b/lib/rust/src/version/error.rs @@ -0,0 +1,35 @@ +use semver; +use std; + +#[derive(Debug)] +pub enum Error { + SemVer(semver::SemVerError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use Error::*; + match *self { + SemVer(ref e) => write!(f, "SemVerError: {}", e), + } + } +} + +impl std::error::Error for Error { + fn description(&self) -> &str { + "Version Error" + } + + fn cause(&self) -> Option<&std::error::Error> { + use Error::*; + Some(match *self { + SemVer(ref e) => e, + }) + } +} + +impl From for Error { + fn from(e: semver::SemVerError) -> Self { + Error::SemVer(e) + } +} diff --git a/lib/rust/src/version/mod.rs b/lib/rust/src/version/mod.rs new file mode 100644 index 000000000..2082c23e8 --- /dev/null +++ b/lib/rust/src/version/mod.rs @@ -0,0 +1,53 @@ +use semver::Version; + +mod error; +pub use self::error::Error; + +/// Compare two semver versions +pub fn compare(first: &str, second: &str) -> Result { + let v1 = Version::parse(first)?; + let v2 = Version::parse(second)?; + if v1 > v2 { + Ok(-1) + } else if v1 == v2 { + Ok(0) + } else { + Ok(1) + } +} + +/// Check if the "second" semver is compatible with the "first" +pub fn is_compatible(first: &str, second: &str) -> Result { + let first = Version::parse(first)?; + let second = Version::parse(second)?; + Ok(if second.major == 0 && first.major == 0 { + first.minor == second.minor && second.patch > first.patch + } else if second.major > 0 { + first.major == second.major + && ((second.minor > first.minor) + || (first.minor == second.minor && second.patch > first.patch)) + } else { + false + }) +} + +/// Check if a the "other" version is a major bump from the "current" +pub fn is_major(current: &str, other: &str) -> Result { + let current = Version::parse(current)?; + let other = Version::parse(other)?; + Ok(other.major > current.major) +} + +/// Check if a the "other" version is a minor bump from the "current" +pub fn is_minor(current: &str, other: &str) -> Result { + let current = Version::parse(current)?; + let other = Version::parse(other)?; + Ok(current.major == other.major && other.minor > current.minor) +} + +/// Check if a the "other" version is a patch bump from the "current" +pub fn is_patch(current: &str, other: &str) -> Result { + let current = Version::parse(current)?; + let other = Version::parse(other)?; + Ok(current.major == other.major && current.minor == other.minor && other.patch > current.patch) +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..ead99ac9f --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "@quasar/proton", + "version": "1.0.0-alpha.1", + "description": "Multi-binding collection of libraries and templates for building Proton", + "main": "templates/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/quasarframework/proton.git" + }, + "author": "Quasar Framework", + "license": "MIT", + "bugs": { + "url": "https://github.com/quasarframework/proton/issues" + }, + "homepage": "https://github.com/quasarframework/proton#readme", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">= 10.16.0", + "npm": ">= 6.6.0", + "yarn": ">= 1.17.3" + } +} diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 000000000..6ddf4a4e4 --- /dev/null +++ b/spec/README.md @@ -0,0 +1 @@ +[WIP] diff --git a/templates/rust/Cargo.toml b/templates/rust/Cargo.toml new file mode 100755 index 000000000..78f8b2f21 --- /dev/null +++ b/templates/rust/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "app" +version = "0.1.0" +description = "A Quasar app" +authors = ["Lucas Fernandes Nogueira "] +edition = "2018" +build = "build.rs" +include = ["data"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proton-ui = { path = "../../proton/bindings/rust" } +serde_json = "1.0.39" +serde = "1.0" +serde_derive = "1.0" +tiny_http = "0.6" +clap = {version = "2.33", features = ["yaml"]} +phf = "0.7.21" +includedir = "0.5.0" +proton = { path = "../../proton/lib/rust" } + +[build-dependencies] +includedir_codegen = "0.5.0" + +[features] +dev = [] # has no explicit dependencies + +[package.metadata.bundle] +identifier = "com.quasar.dev" +icon = ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"] + +[[bin]] +name = "updater" +path = "src/updater.rs" + +[[bin]] +name = "app" +path = "src/main.rs" diff --git a/templates/rust/_gitignore b/templates/rust/_gitignore new file mode 100755 index 000000000..50c83018e --- /dev/null +++ b/templates/rust/_gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk \ No newline at end of file diff --git a/templates/rust/build.rs b/templates/rust/build.rs new file mode 100755 index 000000000..07829cd86 --- /dev/null +++ b/templates/rust/build.rs @@ -0,0 +1,10 @@ +extern crate includedir_codegen; + +use includedir_codegen::Compression; + +fn main() { + includedir_codegen::start("ASSETS") + .dir("./target/compiled-web", Compression::Gzip) + .build("data.rs") + .unwrap(); +} diff --git a/templates/rust/icons/128x128.png b/templates/rust/icons/128x128.png new file mode 100644 index 000000000..4ab8a828e Binary files /dev/null and b/templates/rust/icons/128x128.png differ diff --git a/templates/rust/icons/128x128@2x.png b/templates/rust/icons/128x128@2x.png new file mode 100644 index 000000000..018c12cd8 Binary files /dev/null and b/templates/rust/icons/128x128@2x.png differ diff --git a/templates/rust/icons/32x32.png b/templates/rust/icons/32x32.png new file mode 100644 index 000000000..a21c3721d Binary files /dev/null and b/templates/rust/icons/32x32.png differ diff --git a/templates/rust/icons/icon.icns b/templates/rust/icons/icon.icns new file mode 100644 index 000000000..cf57cfb49 Binary files /dev/null and b/templates/rust/icons/icon.icns differ diff --git a/templates/rust/icons/icon.ico b/templates/rust/icons/icon.ico new file mode 100644 index 000000000..2ae4b7a1f Binary files /dev/null and b/templates/rust/icons/icon.ico differ diff --git a/templates/rust/rustfmt.toml b/templates/rust/rustfmt.toml new file mode 100644 index 000000000..9da25275e --- /dev/null +++ b/templates/rust/rustfmt.toml @@ -0,0 +1,13 @@ +max_width = 100 +hard_tabs = false +tab_spaces = 2 +newline_style = "Auto" +use_small_heuristics = "Default" +reorder_imports = true +reorder_modules = true +remove_nested_parens = true +edition = "2015" +merge_derives = true +use_try_shorthand = false +use_field_init_shorthand = false +force_explicit_abi = true diff --git a/templates/rust/src/cmd.rs b/templates/rust/src/cmd.rs new file mode 100755 index 000000000..53b696461 --- /dev/null +++ b/templates/rust/src/cmd.rs @@ -0,0 +1,8 @@ +#[derive(Deserialize)] +#[serde(tag = "cmd", rename_all = "camelCase")] +pub enum Cmd { + // your custom commands + // multiple arguments are allowed + // note that rename_all = "camelCase": you need to use "myCustomCommand" on JS + MyCustomCommand { argument: String }, +} diff --git a/templates/rust/src/main.rs b/templates/rust/src/main.rs new file mode 100755 index 000000000..41c5f4fa5 --- /dev/null +++ b/templates/rust/src/main.rs @@ -0,0 +1,112 @@ +#[macro_use] +extern crate serde_derive; +extern crate clap; +extern crate proton; +extern crate proton_ui; +extern crate serde_json; + +#[cfg(not(feature = "dev"))] +extern crate tiny_http; + +#[cfg(feature = "dev")] +use clap::{App, Arg}; + +#[cfg(not(feature = "dev"))] +use std::thread; + +mod cmd; + +#[cfg(not(feature = "dev"))] +mod server; + +fn main() { + let debug; + let content; + let _matches: clap::ArgMatches; + + #[cfg(not(feature = "dev"))] + { + thread::spawn(|| { + proton::command::spawn_relative_command( + "updater".to_string(), + Vec::new(), + std::process::Stdio::inherit(), + ) + .unwrap(); + }); + } + + #[cfg(feature = "dev")] + { + let app = App::new("app") + .version("1.0.0") + .author("Author") + .about("About") + .arg( + Arg::with_name("url") + .short("u") + .long("url") + .value_name("URL") + .help("Loads the specified URL into webview") + .required(true) + .takes_value(true), + ); + + _matches = app.get_matches(); + content = proton_ui::Content::Url(_matches.value_of("url").unwrap()); + debug = true; + } + #[cfg(not(feature = "dev"))] + { + if let Some(available_port) = proton::tcp::get_available_port() { + let server_url = format!("{}:{}", "127.0.0.1", available_port); + content = proton_ui::Content::Url(format!("http://{}", server_url)); + debug = cfg!(debug_assertions); + + thread::spawn(move || { + let server = tiny_http::Server::http(server_url).unwrap(); + for request in server.incoming_requests() { + let mut url = request.url().to_string(); + if url == "/" { + url = "/index.html".to_string(); + } + request.respond(server::asset_response(&url)).unwrap(); + } + }); + } else { + panic!("Could not find an open port"); + } + } + + let webview = proton_ui::builder() + .title("MyAppTitle") + .content(content) + .size(800, 600) // TODO:Resolution is fixed right now, change this later to be dynamic + .resizable(true) + .debug(debug) + .user_data(()) + .invoke_handler(|_webview, arg| { + // leave this as is to use the proton API from your JS code + if !proton::api::handler(_webview, arg) { + use cmd::Cmd::*; + match serde_json::from_str(arg) { + Err(_) => {} + Ok(command) => { + match command { + // definitions for your custom commands from Cmd here + MyCustomCommand { argument } => { + // your command code + println!("{}", argument); + } + } + } + } + } + + Ok(()) + }) + .build() + .unwrap(); + + webview.run().unwrap(); +} diff --git a/templates/rust/src/server.rs b/templates/rust/src/server.rs new file mode 100644 index 000000000..5a7534d15 --- /dev/null +++ b/templates/rust/src/server.rs @@ -0,0 +1,26 @@ +use tiny_http::{Header, Response}; + +include!(concat!(env!("OUT_DIR"), "/data.rs")); + +pub fn asset_response(path: &str) -> Response>> { + let asset = ASSETS + .get(&format!("./target/compiled-web{}", path)) + .unwrap() + .into_owned(); + let mut response = Response::from_data(asset); + let header; + + if path.ends_with(".svg") { + header = Header::from_bytes(&b"Content-Type"[..], &b"image/svg+xml"[..]).unwrap(); + } else if path.ends_with(".css") { + header = Header::from_bytes(&b"Content-Type"[..], &b"text/css"[..]).unwrap(); + } else if path.ends_with(".html") { + header = Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..]).unwrap(); + } else { + header = Header::from_bytes(&b"Content-Type"[..], &b"appication/octet-stream"[..]).unwrap(); + } + + response.add_header(header); + + response +} diff --git a/templates/rust/src/updater.rs b/templates/rust/src/updater.rs new file mode 100644 index 000000000..d8e66ae80 --- /dev/null +++ b/templates/rust/src/updater.rs @@ -0,0 +1,73 @@ +extern crate proton; +extern crate serde_derive; +extern crate serde_json; + +use crate::proton::process::{ProcessExt, Signal, SystemExt}; + +fn update() -> Result<(), String> { + let target = proton::platform::target_triple().map_err(|_| "Could not determine target")?; + let github_release = proton::updater::github::get_latest_release("jaemk", "self_update") + .map_err(|_| "Could not fetch latest release")?; + match github_release.asset_for(&target) { + Some(github_release_asset) => { + let release = proton::updater::Release { + version: github_release.tag.trim_start_matches('v').to_string(), + download_url: github_release_asset.download_url, + asset_name: github_release_asset.name, + }; + + let status = proton::updater::Update::configure() + .unwrap() + .release(release) + .bin_path_in_archive("github") + .bin_name("app") + .bin_install_path(&proton::command::command_path("app".to_string()).unwrap()) + .show_download_progress(true) + .current_version(env!("CARGO_PKG_VERSION")) + .build() + .unwrap() + .update() + .unwrap(); + + println!("found release: {}", status.version()); + + /*let tmp_dir = proton::dir::with_temp_dir(|dir| { + let file_path = dir.path().join("my-temporary-note.pdf"); + let mut tmp_archive = std::fs::File::create(file_path).unwrap(); + proton::http::download(&"https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf".to_string(), &mut tmp_archive, true).unwrap(); + });*/ + + Ok(()) + } + None => Err(format!("Could not find release for target {}", target)), + } +} + +fn restart_app(app_command: String) -> Result<(), String> { + let mut system = proton::process::System::new(); + let parent_process = proton::process::get_parent_process(&mut system) + .map_err(|_| "Could not determine parent process")?; + if parent_process.name() == "app" { + parent_process.kill(Signal::Kill); + std::thread::sleep(std::time::Duration::from_secs(1)); + std::process::Command::new(app_command) + .spawn() + .map_err(|_| "Could not start app")?; + } + Ok(()) +} + +fn run_updater() -> Result<(), String> { + let app_command = proton::command::relative_command("app".to_string()) + .map_err(|_| "Could not determine app path")?; + update()?; + restart_app(app_command)?; + Ok(()) +} + +fn main() { + match run_updater() { + Ok(_) => {} + Err(err) => panic!(err), + }; +} diff --git a/ui/.gitattributes b/ui/.gitattributes new file mode 100644 index 000000000..5170675f3 --- /dev/null +++ b/ui/.gitattributes @@ -0,0 +1 @@ +*.h linguist-language=c diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..27a766120 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,2 @@ +# Build atrifacts +/build diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt new file mode 100755 index 000000000..a616430bb --- /dev/null +++ b/ui/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 2.8) + +project(webview) + +if(APPLE) + set(WEBVIEW_COMPILE_DEFS "-DWEBVIEW_COCOA=1") + set(WEBVIEW_LIBS "-framework WebKit") +elseif(WIN32) + set(WEBVIEW_COMPILE_DEFS "-DWEBVIEW_WINAPI=1") + set(WEBVIEW_LIBS "ole32 comctl32 oleaut32 uuid") +else() + set(WEBVIEW_COMPILE_DEFS "-DWEBVIEW_GTK=1") + find_package(PkgConfig REQUIRED) + pkg_check_modules(GTK3 REQUIRED gtk+-3.0) + pkg_check_modules(WEBKIT2 REQUIRED webkit2gtk-4.0) + set(WEBVIEW_COMPILE_INCS ${GTK3_INCLUDE_DIRS} ${WEBKIT2_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}) + set(WEBVIEW_LIBS ${GTK3_LIBRARIES} ${WEBKIT2_LIBRARIES}) +endif() + +add_library(proton ${CMAKE_CURRENT_BINARY_DIR}/proton.c) +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/proton.c "#define WEBVIEW_IMPLEMENTATION\n#include ") +target_include_directories(proton PUBLIC ${PROJECT_SOURCE_DIR} ${WEBVIEW_COMPILE_INCS}) +target_compile_definitions(proton PUBLIC ${WEBVIEW_COMPILE_DEFS}) +target_compile_options(proton PRIVATE ${WEBVIEW_COMPILE_OPTS}) +target_link_libraries(proton ${WEBVIEW_LIBS}) + +add_executable(proton_test WIN32 MACOSX_BUNDLE proton_test.cc) +set_target_properties(proton_test PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO) +find_package(Threads) +target_link_libraries(proton_test PRIVATE proton ${CMAKE_THREAD_LIBS_INIT}) +enable_testing () +add_test(NAME proton_test COMMAND proton_test) diff --git a/ui/LICENSE b/ui/LICENSE new file mode 100644 index 000000000..a12fe0483 --- /dev/null +++ b/ui/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 - present Serge Zaitsev & Quasar Framework Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ui/README.md b/ui/README.md new file mode 100755 index 000000000..95d9ca04e --- /dev/null +++ b/ui/README.md @@ -0,0 +1,3 @@ +# PROTON WEBVIEW + +Documentation forthcoming. diff --git a/ui/proton.h b/ui/proton.h new file mode 100644 index 000000000..b2a6f83d5 --- /dev/null +++ b/ui/proton.h @@ -0,0 +1,2265 @@ +/* + * MIT License + * + * Copyright (c) 2017 Serge Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef WEBVIEW_H +#define WEBVIEW_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef WEBVIEW_STATIC +#define WEBVIEW_API static +#else +#define WEBVIEW_API extern +#endif + +#include +#include +#include + +#if defined(WEBVIEW_GTK) +#include +#include +#include + +struct webview_priv { + GtkWidget *window; + GtkWidget *scroller; + GtkWidget *webview; + GtkWidget *inspector_window; + GAsyncQueue *queue; + int ready; + int js_busy; + int should_exit; +}; +#elif defined(WEBVIEW_WINAPI) +#define CINTERFACE +#include + +#include +#include +#include +#include +#include + +#include + +struct webview_priv { + HWND hwnd; + IOleObject **browser; + BOOL is_fullscreen; + DWORD saved_style; + DWORD saved_ex_style; + RECT saved_rect; +}; +#elif defined(WEBVIEW_COCOA) +#include +#include +#include + +struct webview_priv { + id pool; + id window; + id webview; + id windowDelegate; + int should_exit; +}; +#else +#error "Define one of: WEBVIEW_GTK, WEBVIEW_COCOA or WEBVIEW_WINAPI" +#endif + +struct webview; + +typedef void (*webview_external_invoke_cb_t)(struct webview *w, + const char *arg); + +struct webview { + const char *url; + const char *title; + int width; + int height; + int resizable; + int debug; + webview_external_invoke_cb_t external_invoke_cb; + struct webview_priv priv; + void *userdata; +}; + +enum webview_dialog_type { + WEBVIEW_DIALOG_TYPE_OPEN = 0, + WEBVIEW_DIALOG_TYPE_SAVE = 1, + WEBVIEW_DIALOG_TYPE_ALERT = 2 +}; + +#define WEBVIEW_DIALOG_FLAG_FILE (0 << 0) +#define WEBVIEW_DIALOG_FLAG_DIRECTORY (1 << 0) + +#define WEBVIEW_DIALOG_FLAG_INFO (1 << 1) +#define WEBVIEW_DIALOG_FLAG_WARNING (2 << 1) +#define WEBVIEW_DIALOG_FLAG_ERROR (3 << 1) +#define WEBVIEW_DIALOG_FLAG_ALERT_MASK (3 << 1) + +typedef void (*webview_dispatch_fn)(struct webview *w, void *arg); + +struct webview_dispatch_arg { + webview_dispatch_fn fn; + struct webview *w; + void *arg; +}; + +#define DEFAULT_URL \ + "data:text/" \ + "html,%3C%21DOCTYPE%20html%3E%0A%3Chtml%20lang=%22en%22%3E%0A%3Chead%3E%" \ + "3Cmeta%20charset=%22utf-8%22%3E%3Cmeta%20http-equiv=%22X-UA-Compatible%22%" \ + "20content=%22IE=edge%22%3E%3C%2Fhead%3E%0A%3Cbody%3E%3Cdiv%20id=%22app%22%" \ + "3E%3C%2Fdiv%3E%3Cscript%20type=%22text%2Fjavascript%22%3E%3C%2Fscript%3E%" \ + "3C%2Fbody%3E%0A%3C%2Fhtml%3E" + +#define CSS_INJECT_FUNCTION \ + "(function(e){var " \ + "t=document.createElement('style'),d=document.head||document." \ + "getElementsByTagName('head')[0];t.setAttribute('type','text/" \ + "css'),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document." \ + "createTextNode(e)),d.appendChild(t)})" + +static const char *webview_check_url(const char *url) { + if (url == NULL || strlen(url) == 0) { + return DEFAULT_URL; + } + return url; +} + +WEBVIEW_API int webview(const char *title, const char *url, int width, + int height, int resizable); + +WEBVIEW_API int webview_init(struct webview *w); +WEBVIEW_API int webview_loop(struct webview *w, int blocking); +WEBVIEW_API int webview_eval(struct webview *w, const char *js); +WEBVIEW_API int webview_inject_css(struct webview *w, const char *css); +WEBVIEW_API void webview_set_title(struct webview *w, const char *title); +WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen); +WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g, + uint8_t b, uint8_t a); +WEBVIEW_API void webview_dialog(struct webview *w, + enum webview_dialog_type dlgtype, int flags, + const char *title, const char *arg, + char *result, size_t resultsz); +WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn, + void *arg); +WEBVIEW_API void webview_terminate(struct webview *w); +WEBVIEW_API void webview_exit(struct webview *w); +WEBVIEW_API void webview_debug(const char *format, ...); +WEBVIEW_API void webview_print_log(const char *s); + +#ifdef WEBVIEW_IMPLEMENTATION +#undef WEBVIEW_IMPLEMENTATION + +WEBVIEW_API int webview(const char *title, const char *url, int width, + int height, int resizable) { + struct webview webview; + memset(&webview, 0, sizeof(webview)); + webview.title = title; + webview.url = url; + webview.width = width; + webview.height = height; + webview.resizable = resizable; + int r = webview_init(&webview); + if (r != 0) { + return r; + } + while (webview_loop(&webview, 1) == 0) { + } + webview_exit(&webview); + return 0; +} + +WEBVIEW_API void webview_debug(const char *format, ...) { + char buf[4096]; + va_list ap; + va_start(ap, format); + vsnprintf(buf, sizeof(buf), format, ap); + webview_print_log(buf); + va_end(ap); +} + +static int webview_js_encode(const char *s, char *esc, size_t n) { + int r = 1; /* At least one byte for trailing zero */ + for (; *s; s++) { + const unsigned char c = *s; + if (c >= 0x20 && c < 0x80 && strchr("<>\\'\"", c) == NULL) { + if (n > 0) { + *esc++ = c; + n--; + } + r++; + } else { + if (n > 0) { + snprintf(esc, n, "\\x%02x", (int)c); + esc += 4; + n -= 4; + } + r += 4; + } + } + return r; +} + +WEBVIEW_API int webview_inject_css(struct webview *w, const char *css) { + int n = webview_js_encode(css, NULL, 0); + char *esc = (char *)calloc(1, sizeof(CSS_INJECT_FUNCTION) + n + 4); + if (esc == NULL) { + return -1; + } + char *js = (char *)calloc(1, n); + webview_js_encode(css, js, n); + snprintf(esc, sizeof(CSS_INJECT_FUNCTION) + n + 4, "%s(\"%s\")", + CSS_INJECT_FUNCTION, js); + int r = webview_eval(w, esc); + free(js); + free(esc); + return r; +} + +#if defined(WEBVIEW_GTK) +static void external_message_received_cb(WebKitUserContentManager *m, + WebKitJavascriptResult *r, + gpointer arg) { + (void)m; + struct webview *w = (struct webview *)arg; + if (w->external_invoke_cb == NULL) { + return; + } + JSGlobalContextRef context = webkit_javascript_result_get_global_context(r); + JSValueRef value = webkit_javascript_result_get_value(r); + JSStringRef js = JSValueToStringCopy(context, value, NULL); + size_t n = JSStringGetMaximumUTF8CStringSize(js); + char *s = g_new(char, n); + JSStringGetUTF8CString(js, s, n); + w->external_invoke_cb(w, s); + JSStringRelease(js); + g_free(s); +} + +static void webview_load_changed_cb(WebKitWebView *webview, + WebKitLoadEvent event, gpointer arg) { + (void)webview; + struct webview *w = (struct webview *)arg; + if (event == WEBKIT_LOAD_FINISHED) { + w->priv.ready = 1; + } +} + +static void webview_destroy_cb(GtkWidget *widget, gpointer arg) { + (void)widget; + struct webview *w = (struct webview *)arg; + webview_terminate(w); +} + +static gboolean webview_context_menu_cb(WebKitWebView *webview, + GtkWidget *default_menu, + WebKitHitTestResult *hit_test_result, + gboolean triggered_with_keyboard, + gpointer userdata) { + (void)webview; + (void)default_menu; + (void)hit_test_result; + (void)triggered_with_keyboard; + (void)userdata; + return TRUE; +} + +WEBVIEW_API int webview_init(struct webview *w) { + if (gtk_init_check(0, NULL) == FALSE) { + return -1; + } + + w->priv.ready = 0; + w->priv.should_exit = 0; + w->priv.queue = g_async_queue_new(); + w->priv.window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(w->priv.window), w->title); + + if (w->resizable) { + gtk_window_set_default_size(GTK_WINDOW(w->priv.window), w->width, + w->height); + } else { + gtk_widget_set_size_request(w->priv.window, w->width, w->height); + } + gtk_window_set_resizable(GTK_WINDOW(w->priv.window), !!w->resizable); + gtk_window_set_position(GTK_WINDOW(w->priv.window), GTK_WIN_POS_CENTER); + + w->priv.scroller = gtk_scrolled_window_new(NULL, NULL); + gtk_container_add(GTK_CONTAINER(w->priv.window), w->priv.scroller); + + WebKitUserContentManager *m = webkit_user_content_manager_new(); + webkit_user_content_manager_register_script_message_handler(m, "external"); + g_signal_connect(m, "script-message-received::external", + G_CALLBACK(external_message_received_cb), w); + + w->priv.webview = webkit_web_view_new_with_user_content_manager(m); + webkit_web_view_load_uri(WEBKIT_WEB_VIEW(w->priv.webview), + webview_check_url(w->url)); + g_signal_connect(G_OBJECT(w->priv.webview), "load-changed", + G_CALLBACK(webview_load_changed_cb), w); + gtk_container_add(GTK_CONTAINER(w->priv.scroller), w->priv.webview); + + if (w->debug) { + WebKitSettings *settings = + webkit_web_view_get_settings(WEBKIT_WEB_VIEW(w->priv.webview)); + webkit_settings_set_enable_write_console_messages_to_stdout(settings, true); + webkit_settings_set_enable_developer_extras(settings, true); + } else { + g_signal_connect(G_OBJECT(w->priv.webview), "context-menu", + G_CALLBACK(webview_context_menu_cb), w); + } + + gtk_widget_show_all(w->priv.window); + + webkit_web_view_run_javascript( + WEBKIT_WEB_VIEW(w->priv.webview), + "window.external={invoke:function(x){" + "window.webkit.messageHandlers.external.postMessage(x);}}", + NULL, NULL, NULL); + + g_signal_connect(G_OBJECT(w->priv.window), "destroy", + G_CALLBACK(webview_destroy_cb), w); + return 0; +} + +WEBVIEW_API int webview_loop(struct webview *w, int blocking) { + gtk_main_iteration_do(blocking); + return w->priv.should_exit; +} + +WEBVIEW_API void webview_set_title(struct webview *w, const char *title) { + gtk_window_set_title(GTK_WINDOW(w->priv.window), title); +} + +WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen) { + if (fullscreen) { + gtk_window_fullscreen(GTK_WINDOW(w->priv.window)); + } else { + gtk_window_unfullscreen(GTK_WINDOW(w->priv.window)); + } +} + +WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g, + uint8_t b, uint8_t a) { + GdkRGBA color = {r / 255.0, g / 255.0, b / 255.0, a / 255.0}; + webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w->priv.webview), + &color); +} + +WEBVIEW_API void webview_dialog(struct webview *w, + enum webview_dialog_type dlgtype, int flags, + const char *title, const char *arg, + char *result, size_t resultsz) { + GtkWidget *dlg; + if (result != NULL) { + result[0] = '\0'; + } + if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN || + dlgtype == WEBVIEW_DIALOG_TYPE_SAVE) { + dlg = gtk_file_chooser_dialog_new( + title, GTK_WINDOW(w->priv.window), + (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN + ? (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY + ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER + : GTK_FILE_CHOOSER_ACTION_OPEN) + : GTK_FILE_CHOOSER_ACTION_SAVE), + "_Cancel", GTK_RESPONSE_CANCEL, + (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN ? "_Open" : "_Save"), + GTK_RESPONSE_ACCEPT, NULL); + gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dlg), FALSE); + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dlg), FALSE); + gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dlg), TRUE); + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE); + gtk_file_chooser_set_create_folders(GTK_FILE_CHOOSER(dlg), TRUE); + gint response = gtk_dialog_run(GTK_DIALOG(dlg)); + if (response == GTK_RESPONSE_ACCEPT) { + gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg)); + g_strlcpy(result, filename, resultsz); + g_free(filename); + } + gtk_widget_destroy(dlg); + } else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT) { + GtkMessageType type = GTK_MESSAGE_OTHER; + switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK) { + case WEBVIEW_DIALOG_FLAG_INFO: + type = GTK_MESSAGE_INFO; + break; + case WEBVIEW_DIALOG_FLAG_WARNING: + type = GTK_MESSAGE_WARNING; + break; + case WEBVIEW_DIALOG_FLAG_ERROR: + type = GTK_MESSAGE_ERROR; + break; + } + dlg = gtk_message_dialog_new(GTK_WINDOW(w->priv.window), GTK_DIALOG_MODAL, + type, GTK_BUTTONS_OK, "%s", title); + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dlg), "%s", + arg); + gtk_dialog_run(GTK_DIALOG(dlg)); + gtk_widget_destroy(dlg); + } +} + +static void webview_eval_finished(GObject *object, GAsyncResult *result, + gpointer userdata) { + (void)object; + (void)result; + struct webview *w = (struct webview *)userdata; + w->priv.js_busy = 0; +} + +WEBVIEW_API int webview_eval(struct webview *w, const char *js) { + while (w->priv.ready == 0) { + g_main_context_iteration(NULL, TRUE); + } + w->priv.js_busy = 1; + webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(w->priv.webview), js, NULL, + webview_eval_finished, w); + while (w->priv.js_busy) { + g_main_context_iteration(NULL, TRUE); + } + return 0; +} + +static gboolean webview_dispatch_wrapper(gpointer userdata) { + struct webview *w = (struct webview *)userdata; + for (;;) { + struct webview_dispatch_arg *arg = + (struct webview_dispatch_arg *)g_async_queue_try_pop(w->priv.queue); + if (arg == NULL) { + break; + } + (arg->fn)(w, arg->arg); + g_free(arg); + } + return FALSE; +} + +WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn, + void *arg) { + struct webview_dispatch_arg *context = + (struct webview_dispatch_arg *)g_new(struct webview_dispatch_arg, 1); + context->w = w; + context->arg = arg; + context->fn = fn; + g_async_queue_lock(w->priv.queue); + g_async_queue_push_unlocked(w->priv.queue, context); + if (g_async_queue_length_unlocked(w->priv.queue) == 1) { + gdk_threads_add_idle(webview_dispatch_wrapper, w); + } + g_async_queue_unlock(w->priv.queue); +} + +WEBVIEW_API void webview_terminate(struct webview *w) { + w->priv.should_exit = 1; +} + +WEBVIEW_API void webview_exit(struct webview *w) { (void)w; } +WEBVIEW_API void webview_print_log(const char *s) { + fprintf(stderr, "%s\n", s); +} + +#endif /* WEBVIEW_GTK */ + +#if defined(WEBVIEW_WINAPI) + +#pragma comment(lib, "user32.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "oleaut32.lib") + +#define WM_WEBVIEW_DISPATCH (WM_APP + 1) + +typedef struct { + IOleInPlaceFrame frame; + HWND window; +} _IOleInPlaceFrameEx; + +typedef struct { + IOleInPlaceSite inplace; + _IOleInPlaceFrameEx frame; +} _IOleInPlaceSiteEx; + +typedef struct { + IDocHostUIHandler ui; +} _IDocHostUIHandlerEx; + +typedef struct { + IInternetSecurityManager mgr; +} _IInternetSecurityManagerEx; + +typedef struct { + IServiceProvider provider; + _IInternetSecurityManagerEx mgr; +} _IServiceProviderEx; + +typedef struct { + IOleClientSite client; + _IOleInPlaceSiteEx inplace; + _IDocHostUIHandlerEx ui; + IDispatch external; + _IServiceProviderEx provider; +} _IOleClientSiteEx; + +#ifdef __cplusplus +#define iid_ref(x) &(x) +#define iid_unref(x) *(x) +#else +#define iid_ref(x) (x) +#define iid_unref(x) (x) +#endif + +static inline WCHAR *webview_to_utf16(const char *s) { + DWORD size = MultiByteToWideChar(CP_UTF8, 0, s, -1, 0, 0); + WCHAR *ws = (WCHAR *)GlobalAlloc(GMEM_FIXED, sizeof(WCHAR) * size); + if (ws == NULL) { + return NULL; + } + MultiByteToWideChar(CP_UTF8, 0, s, -1, ws, size); + return ws; +} + +static inline char *webview_from_utf16(WCHAR *ws) { + int n = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); + char *s = (char *)GlobalAlloc(GMEM_FIXED, n); + if (s == NULL) { + return NULL; + } + WideCharToMultiByte(CP_UTF8, 0, ws, -1, s, n, NULL, NULL); + return s; +} + +static int iid_eq(REFIID a, const IID *b) { + return memcmp((const void *)iid_ref(a), (const void *)b, sizeof(GUID)) == 0; +} + +static HRESULT STDMETHODCALLTYPE JS_QueryInterface(IDispatch FAR *This, + REFIID riid, + LPVOID FAR *ppvObj) { + if (iid_eq(riid, &IID_IDispatch)) { + *ppvObj = This; + return S_OK; + } + *ppvObj = 0; + return E_NOINTERFACE; +} +static ULONG STDMETHODCALLTYPE JS_AddRef(IDispatch FAR *This) { return 1; } +static ULONG STDMETHODCALLTYPE JS_Release(IDispatch FAR *This) { return 1; } +static HRESULT STDMETHODCALLTYPE JS_GetTypeInfoCount(IDispatch FAR *This, + UINT *pctinfo) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE JS_GetTypeInfo(IDispatch FAR *This, + UINT iTInfo, LCID lcid, + ITypeInfo **ppTInfo) { + return S_OK; +} +#define WEBVIEW_JS_INVOKE_ID 0x1000 +static HRESULT STDMETHODCALLTYPE JS_GetIDsOfNames(IDispatch FAR *This, + REFIID riid, + LPOLESTR *rgszNames, + UINT cNames, LCID lcid, + DISPID *rgDispId) { + if (cNames != 1) { + return S_FALSE; + } + if (wcscmp(rgszNames[0], L"invoke") == 0) { + rgDispId[0] = WEBVIEW_JS_INVOKE_ID; + return S_OK; + } + return S_FALSE; +} + +static HRESULT STDMETHODCALLTYPE +JS_Invoke(IDispatch FAR *This, DISPID dispIdMember, REFIID riid, LCID lcid, + WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, + EXCEPINFO *pExcepInfo, UINT *puArgErr) { + size_t offset = (size_t) & ((_IOleClientSiteEx *)NULL)->external; + _IOleClientSiteEx *ex = (_IOleClientSiteEx *)((char *)(This)-offset); + struct webview *w = (struct webview *)GetWindowLongPtr( + ex->inplace.frame.window, GWLP_USERDATA); + if (pDispParams->cArgs == 1 && pDispParams->rgvarg[0].vt == VT_BSTR) { + BSTR bstr = pDispParams->rgvarg[0].bstrVal; + char *s = webview_from_utf16(bstr); + if (s != NULL) { + if (dispIdMember == WEBVIEW_JS_INVOKE_ID) { + if (w->external_invoke_cb != NULL) { + w->external_invoke_cb(w, s); + } + } else { + return S_FALSE; + } + GlobalFree(s); + } + } + return S_OK; +} + +static IDispatchVtbl ExternalDispatchTable = { + JS_QueryInterface, JS_AddRef, JS_Release, JS_GetTypeInfoCount, + JS_GetTypeInfo, JS_GetIDsOfNames, JS_Invoke}; + +static ULONG STDMETHODCALLTYPE Site_AddRef(IOleClientSite FAR *This) { + return 1; +} +static ULONG STDMETHODCALLTYPE Site_Release(IOleClientSite FAR *This) { + return 1; +} +static HRESULT STDMETHODCALLTYPE Site_SaveObject(IOleClientSite FAR *This) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Site_GetMoniker(IOleClientSite FAR *This, + DWORD dwAssign, + DWORD dwWhichMoniker, + IMoniker **ppmk) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +Site_GetContainer(IOleClientSite FAR *This, LPOLECONTAINER FAR *ppContainer) { + *ppContainer = 0; + return E_NOINTERFACE; +} +static HRESULT STDMETHODCALLTYPE Site_ShowObject(IOleClientSite FAR *This) { + return NOERROR; +} +static HRESULT STDMETHODCALLTYPE Site_OnShowWindow(IOleClientSite FAR *This, + BOOL fShow) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +Site_RequestNewObjectLayout(IOleClientSite FAR *This) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Site_QueryInterface(IOleClientSite FAR *This, + REFIID riid, + void **ppvObject) { + if (iid_eq(riid, &IID_IUnknown) || iid_eq(riid, &IID_IOleClientSite)) { + *ppvObject = &((_IOleClientSiteEx *)This)->client; + } else if (iid_eq(riid, &IID_IOleInPlaceSite)) { + *ppvObject = &((_IOleClientSiteEx *)This)->inplace; + } else if (iid_eq(riid, &IID_IDocHostUIHandler)) { + *ppvObject = &((_IOleClientSiteEx *)This)->ui; + } else if (iid_eq(riid, &IID_IServiceProvider)) { + *ppvObject = &((_IOleClientSiteEx *)This)->provider; + } else { + *ppvObject = 0; + return (E_NOINTERFACE); + } + return S_OK; +} +static HRESULT STDMETHODCALLTYPE InPlace_QueryInterface( + IOleInPlaceSite FAR *This, REFIID riid, LPVOID FAR *ppvObj) { + return (Site_QueryInterface( + (IOleClientSite *)((char *)This - sizeof(IOleClientSite)), riid, ppvObj)); +} +static ULONG STDMETHODCALLTYPE InPlace_AddRef(IOleInPlaceSite FAR *This) { + return 1; +} +static ULONG STDMETHODCALLTYPE InPlace_Release(IOleInPlaceSite FAR *This) { + return 1; +} +static HRESULT STDMETHODCALLTYPE InPlace_GetWindow(IOleInPlaceSite FAR *This, + HWND FAR *lphwnd) { + *lphwnd = ((_IOleInPlaceSiteEx FAR *)This)->frame.window; + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +InPlace_ContextSensitiveHelp(IOleInPlaceSite FAR *This, BOOL fEnterMode) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +InPlace_CanInPlaceActivate(IOleInPlaceSite FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +InPlace_OnInPlaceActivate(IOleInPlaceSite FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +InPlace_OnUIActivate(IOleInPlaceSite FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE InPlace_GetWindowContext( + IOleInPlaceSite FAR *This, LPOLEINPLACEFRAME FAR *lplpFrame, + LPOLEINPLACEUIWINDOW FAR *lplpDoc, LPRECT lprcPosRect, LPRECT lprcClipRect, + LPOLEINPLACEFRAMEINFO lpFrameInfo) { + *lplpFrame = (LPOLEINPLACEFRAME) & ((_IOleInPlaceSiteEx *)This)->frame; + *lplpDoc = 0; + lpFrameInfo->fMDIApp = FALSE; + lpFrameInfo->hwndFrame = ((_IOleInPlaceFrameEx *)*lplpFrame)->window; + lpFrameInfo->haccel = 0; + lpFrameInfo->cAccelEntries = 0; + return S_OK; +} +static HRESULT STDMETHODCALLTYPE InPlace_Scroll(IOleInPlaceSite FAR *This, + SIZE scrollExtent) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +InPlace_OnUIDeactivate(IOleInPlaceSite FAR *This, BOOL fUndoable) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +InPlace_OnInPlaceDeactivate(IOleInPlaceSite FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +InPlace_DiscardUndoState(IOleInPlaceSite FAR *This) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +InPlace_DeactivateAndUndo(IOleInPlaceSite FAR *This) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE +InPlace_OnPosRectChange(IOleInPlaceSite FAR *This, LPCRECT lprcPosRect) { + IOleObject *browserObject; + IOleInPlaceObject *inplace; + browserObject = *((IOleObject **)((char *)This - sizeof(IOleObject *) - + sizeof(IOleClientSite))); + if (!browserObject->lpVtbl->QueryInterface(browserObject, + iid_unref(&IID_IOleInPlaceObject), + (void **)&inplace)) { + inplace->lpVtbl->SetObjectRects(inplace, lprcPosRect, lprcPosRect); + inplace->lpVtbl->Release(inplace); + } + return S_OK; +} +static HRESULT STDMETHODCALLTYPE Frame_QueryInterface( + IOleInPlaceFrame FAR *This, REFIID riid, LPVOID FAR *ppvObj) { + return E_NOTIMPL; +} +static ULONG STDMETHODCALLTYPE Frame_AddRef(IOleInPlaceFrame FAR *This) { + return 1; +} +static ULONG STDMETHODCALLTYPE Frame_Release(IOleInPlaceFrame FAR *This) { + return 1; +} +static HRESULT STDMETHODCALLTYPE Frame_GetWindow(IOleInPlaceFrame FAR *This, + HWND FAR *lphwnd) { + *lphwnd = ((_IOleInPlaceFrameEx *)This)->window; + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +Frame_ContextSensitiveHelp(IOleInPlaceFrame FAR *This, BOOL fEnterMode) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_GetBorder(IOleInPlaceFrame FAR *This, + LPRECT lprectBorder) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_RequestBorderSpace( + IOleInPlaceFrame FAR *This, LPCBORDERWIDTHS pborderwidths) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_SetBorderSpace( + IOleInPlaceFrame FAR *This, LPCBORDERWIDTHS pborderwidths) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_SetActiveObject( + IOleInPlaceFrame FAR *This, IOleInPlaceActiveObject *pActiveObject, + LPCOLESTR pszObjName) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +Frame_InsertMenus(IOleInPlaceFrame FAR *This, HMENU hmenuShared, + LPOLEMENUGROUPWIDTHS lpMenuWidths) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_SetMenu(IOleInPlaceFrame FAR *This, + HMENU hmenuShared, + HOLEMENU holemenu, + HWND hwndActiveObject) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE Frame_RemoveMenus(IOleInPlaceFrame FAR *This, + HMENU hmenuShared) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE Frame_SetStatusText(IOleInPlaceFrame FAR *This, + LPCOLESTR pszStatusText) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +Frame_EnableModeless(IOleInPlaceFrame FAR *This, BOOL fEnable) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +Frame_TranslateAccelerator(IOleInPlaceFrame FAR *This, LPMSG lpmsg, WORD wID) { + return E_NOTIMPL; +} +static HRESULT STDMETHODCALLTYPE UI_QueryInterface(IDocHostUIHandler FAR *This, + REFIID riid, + LPVOID FAR *ppvObj) { + return (Site_QueryInterface((IOleClientSite *)((char *)This - + sizeof(IOleClientSite) - + sizeof(_IOleInPlaceSiteEx)), + riid, ppvObj)); +} +static ULONG STDMETHODCALLTYPE UI_AddRef(IDocHostUIHandler FAR *This) { + return 1; +} +static ULONG STDMETHODCALLTYPE UI_Release(IDocHostUIHandler FAR *This) { + return 1; +} +static HRESULT STDMETHODCALLTYPE UI_ShowContextMenu( + IDocHostUIHandler FAR *This, DWORD dwID, POINT __RPC_FAR *ppt, + IUnknown __RPC_FAR *pcmdtReserved, IDispatch __RPC_FAR *pdispReserved) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +UI_GetHostInfo(IDocHostUIHandler FAR *This, DOCHOSTUIINFO __RPC_FAR *pInfo) { + pInfo->cbSize = sizeof(DOCHOSTUIINFO); + pInfo->dwFlags = DOCHOSTUIFLAG_NO3DBORDER; + pInfo->dwDoubleClick = DOCHOSTUIDBLCLK_DEFAULT; + return S_OK; +} +static HRESULT STDMETHODCALLTYPE UI_ShowUI( + IDocHostUIHandler FAR *This, DWORD dwID, + IOleInPlaceActiveObject __RPC_FAR *pActiveObject, + IOleCommandTarget __RPC_FAR *pCommandTarget, + IOleInPlaceFrame __RPC_FAR *pFrame, IOleInPlaceUIWindow __RPC_FAR *pDoc) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE UI_HideUI(IDocHostUIHandler FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE UI_UpdateUI(IDocHostUIHandler FAR *This) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE UI_EnableModeless(IDocHostUIHandler FAR *This, + BOOL fEnable) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +UI_OnDocWindowActivate(IDocHostUIHandler FAR *This, BOOL fActivate) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +UI_OnFrameWindowActivate(IDocHostUIHandler FAR *This, BOOL fActivate) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +UI_ResizeBorder(IDocHostUIHandler FAR *This, LPCRECT prcBorder, + IOleInPlaceUIWindow __RPC_FAR *pUIWindow, BOOL fRameWindow) { + return S_OK; +} +static HRESULT STDMETHODCALLTYPE +UI_TranslateAccelerator(IDocHostUIHandler FAR *This, LPMSG lpMsg, + const GUID __RPC_FAR *pguidCmdGroup, DWORD nCmdID) { + return S_FALSE; +} +static HRESULT STDMETHODCALLTYPE UI_GetOptionKeyPath( + IDocHostUIHandler FAR *This, LPOLESTR __RPC_FAR *pchKey, DWORD dw) { + return S_FALSE; +} +static HRESULT STDMETHODCALLTYPE UI_GetDropTarget( + IDocHostUIHandler FAR *This, IDropTarget __RPC_FAR *pDropTarget, + IDropTarget __RPC_FAR *__RPC_FAR *ppDropTarget) { + return S_FALSE; +} +static HRESULT STDMETHODCALLTYPE UI_GetExternal( + IDocHostUIHandler FAR *This, IDispatch __RPC_FAR *__RPC_FAR *ppDispatch) { + *ppDispatch = (IDispatch *)(This + 1); + return S_OK; +} +static HRESULT STDMETHODCALLTYPE UI_TranslateUrl( + IDocHostUIHandler FAR *This, DWORD dwTranslate, OLECHAR __RPC_FAR *pchURLIn, + OLECHAR __RPC_FAR *__RPC_FAR *ppchURLOut) { + *ppchURLOut = 0; + return S_FALSE; +} +static HRESULT STDMETHODCALLTYPE +UI_FilterDataObject(IDocHostUIHandler FAR *This, IDataObject __RPC_FAR *pDO, + IDataObject __RPC_FAR *__RPC_FAR *ppDORet) { + *ppDORet = 0; + return S_FALSE; +} + +static const TCHAR *classname = "WebView"; +static const SAFEARRAYBOUND ArrayBound = {1, 0}; + +static IOleClientSiteVtbl MyIOleClientSiteTable = { + Site_QueryInterface, Site_AddRef, Site_Release, + Site_SaveObject, Site_GetMoniker, Site_GetContainer, + Site_ShowObject, Site_OnShowWindow, Site_RequestNewObjectLayout}; +static IOleInPlaceSiteVtbl MyIOleInPlaceSiteTable = { + InPlace_QueryInterface, + InPlace_AddRef, + InPlace_Release, + InPlace_GetWindow, + InPlace_ContextSensitiveHelp, + InPlace_CanInPlaceActivate, + InPlace_OnInPlaceActivate, + InPlace_OnUIActivate, + InPlace_GetWindowContext, + InPlace_Scroll, + InPlace_OnUIDeactivate, + InPlace_OnInPlaceDeactivate, + InPlace_DiscardUndoState, + InPlace_DeactivateAndUndo, + InPlace_OnPosRectChange}; + +static IOleInPlaceFrameVtbl MyIOleInPlaceFrameTable = { + Frame_QueryInterface, + Frame_AddRef, + Frame_Release, + Frame_GetWindow, + Frame_ContextSensitiveHelp, + Frame_GetBorder, + Frame_RequestBorderSpace, + Frame_SetBorderSpace, + Frame_SetActiveObject, + Frame_InsertMenus, + Frame_SetMenu, + Frame_RemoveMenus, + Frame_SetStatusText, + Frame_EnableModeless, + Frame_TranslateAccelerator}; + +static IDocHostUIHandlerVtbl MyIDocHostUIHandlerTable = { + UI_QueryInterface, + UI_AddRef, + UI_Release, + UI_ShowContextMenu, + UI_GetHostInfo, + UI_ShowUI, + UI_HideUI, + UI_UpdateUI, + UI_EnableModeless, + UI_OnDocWindowActivate, + UI_OnFrameWindowActivate, + UI_ResizeBorder, + UI_TranslateAccelerator, + UI_GetOptionKeyPath, + UI_GetDropTarget, + UI_GetExternal, + UI_TranslateUrl, + UI_FilterDataObject}; + + + +static HRESULT STDMETHODCALLTYPE IS_QueryInterface(IInternetSecurityManager FAR *This, REFIID riid, void **ppvObject) { + return E_NOTIMPL; +} +static ULONG STDMETHODCALLTYPE IS_AddRef(IInternetSecurityManager FAR *This) { return 1; } +static ULONG STDMETHODCALLTYPE IS_Release(IInternetSecurityManager FAR *This) { return 1; } +static HRESULT STDMETHODCALLTYPE IS_SetSecuritySite(IInternetSecurityManager FAR *This, IInternetSecurityMgrSite *pSited) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_GetSecuritySite(IInternetSecurityManager FAR *This, IInternetSecurityMgrSite **ppSite) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_MapUrlToZone(IInternetSecurityManager FAR *This, LPCWSTR pwszUrl, DWORD *pdwZone, DWORD dwFlags) { + *pdwZone = URLZONE_LOCAL_MACHINE; + return S_OK; +} +static HRESULT STDMETHODCALLTYPE IS_GetSecurityId(IInternetSecurityManager FAR *This, LPCWSTR pwszUrl, BYTE *pbSecurityId, DWORD *pcbSecurityId, DWORD_PTR dwReserved) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_ProcessUrlAction(IInternetSecurityManager FAR *This, LPCWSTR pwszUrl, DWORD dwAction, BYTE *pPolicy, DWORD cbPolicy, BYTE *pContext, DWORD cbContext, DWORD dwFlags, DWORD dwReserved) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_QueryCustomPolicy(IInternetSecurityManager FAR *This, LPCWSTR pwszUrl, REFGUID guidKey, BYTE **ppPolicy, DWORD *pcbPolicy, BYTE *pContext, DWORD cbContext, DWORD dwReserved) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_SetZoneMapping(IInternetSecurityManager FAR *This, DWORD dwZone, LPCWSTR lpszPattern, DWORD dwFlags) { + return INET_E_DEFAULT_ACTION; +} +static HRESULT STDMETHODCALLTYPE IS_GetZoneMappings(IInternetSecurityManager FAR *This, DWORD dwZone, IEnumString **ppenumString, DWORD dwFlags) { + return INET_E_DEFAULT_ACTION; +} +static IInternetSecurityManagerVtbl MyInternetSecurityManagerTable = {IS_QueryInterface, IS_AddRef, IS_Release, IS_SetSecuritySite, IS_GetSecuritySite, IS_MapUrlToZone, IS_GetSecurityId, IS_ProcessUrlAction, IS_QueryCustomPolicy, IS_SetZoneMapping, IS_GetZoneMappings}; + +static HRESULT STDMETHODCALLTYPE SP_QueryInterface(IServiceProvider FAR *This, REFIID riid, void **ppvObject) { + return (Site_QueryInterface( + (IOleClientSite *)((char *)This - sizeof(IOleClientSite) - sizeof(_IOleInPlaceSiteEx) - sizeof(_IDocHostUIHandlerEx) - sizeof(IDispatch)), riid, ppvObject)); +} +static ULONG STDMETHODCALLTYPE SP_AddRef(IServiceProvider FAR *This) { return 1; } +static ULONG STDMETHODCALLTYPE SP_Release(IServiceProvider FAR *This) { return 1; } +static HRESULT STDMETHODCALLTYPE SP_QueryService(IServiceProvider FAR *This, REFGUID siid, REFIID riid, void **ppvObject) { + if (iid_eq(siid, &IID_IInternetSecurityManager) && iid_eq(riid, &IID_IInternetSecurityManager)) { + *ppvObject = &((_IServiceProviderEx *)This)->mgr; + } else { + *ppvObject = 0; + return (E_NOINTERFACE); + } + return S_OK; +} +static IServiceProviderVtbl MyServiceProviderTable = {SP_QueryInterface, SP_AddRef, SP_Release, SP_QueryService}; + +static void UnEmbedBrowserObject(struct webview *w) { + if (w->priv.browser != NULL) { + (*w->priv.browser)->lpVtbl->Close(*w->priv.browser, OLECLOSE_NOSAVE); + (*w->priv.browser)->lpVtbl->Release(*w->priv.browser); + GlobalFree(w->priv.browser); + w->priv.browser = NULL; + } +} + +static int EmbedBrowserObject(struct webview *w) { + RECT rect; + IWebBrowser2 *webBrowser2 = NULL; + LPCLASSFACTORY pClassFactory = NULL; + _IOleClientSiteEx *_iOleClientSiteEx = NULL; + IOleObject **browser = (IOleObject **)GlobalAlloc( + GMEM_FIXED, sizeof(IOleObject *) + sizeof(_IOleClientSiteEx)); + if (browser == NULL) { + goto error; + } + w->priv.browser = browser; + + _iOleClientSiteEx = (_IOleClientSiteEx *)(browser + 1); + _iOleClientSiteEx->client.lpVtbl = &MyIOleClientSiteTable; + _iOleClientSiteEx->inplace.inplace.lpVtbl = &MyIOleInPlaceSiteTable; + _iOleClientSiteEx->inplace.frame.frame.lpVtbl = &MyIOleInPlaceFrameTable; + _iOleClientSiteEx->inplace.frame.window = w->priv.hwnd; + _iOleClientSiteEx->ui.ui.lpVtbl = &MyIDocHostUIHandlerTable; + _iOleClientSiteEx->external.lpVtbl = &ExternalDispatchTable; + _iOleClientSiteEx->provider.provider.lpVtbl = &MyServiceProviderTable; + _iOleClientSiteEx->provider.mgr.mgr.lpVtbl = &MyInternetSecurityManagerTable; + + if (CoGetClassObject(iid_unref(&CLSID_WebBrowser), + CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, NULL, + iid_unref(&IID_IClassFactory), + (void **)&pClassFactory) != S_OK) { + goto error; + } + + if (pClassFactory == NULL) { + goto error; + } + + if (pClassFactory->lpVtbl->CreateInstance(pClassFactory, 0, + iid_unref(&IID_IOleObject), + (void **)browser) != S_OK) { + goto error; + } + pClassFactory->lpVtbl->Release(pClassFactory); + if ((*browser)->lpVtbl->SetClientSite( + *browser, (IOleClientSite *)_iOleClientSiteEx) != S_OK) { + goto error; + } + (*browser)->lpVtbl->SetHostNames(*browser, L"My Host Name", 0); + + if (OleSetContainedObject((struct IUnknown *)(*browser), TRUE) != S_OK) { + goto error; + } + GetClientRect(w->priv.hwnd, &rect); + if ((*browser)->lpVtbl->DoVerb((*browser), OLEIVERB_SHOW, NULL, + (IOleClientSite *)_iOleClientSiteEx, -1, + w->priv.hwnd, &rect) != S_OK) { + goto error; + } + if ((*browser)->lpVtbl->QueryInterface((*browser), + iid_unref(&IID_IWebBrowser2), + (void **)&webBrowser2) != S_OK) { + goto error; + } + + webBrowser2->lpVtbl->put_Left(webBrowser2, 0); + webBrowser2->lpVtbl->put_Top(webBrowser2, 0); + webBrowser2->lpVtbl->put_Width(webBrowser2, rect.right); + webBrowser2->lpVtbl->put_Height(webBrowser2, rect.bottom); + webBrowser2->lpVtbl->Release(webBrowser2); + + return 0; +error: + UnEmbedBrowserObject(w); + if (pClassFactory != NULL) { + pClassFactory->lpVtbl->Release(pClassFactory); + } + if (browser != NULL) { + GlobalFree(browser); + } + return -1; +} + +#define WEBVIEW_DATA_URL_PREFIX "data:text/html," +static int DisplayHTMLPage(struct webview *w) { + IWebBrowser2 *webBrowser2; + VARIANT myURL; + LPDISPATCH lpDispatch; + IHTMLDocument2 *htmlDoc2; + BSTR bstr; + IOleObject *browserObject; + SAFEARRAY *sfArray; + VARIANT *pVar; + browserObject = *w->priv.browser; + int isDataURL = 0; + const char *webview_url = webview_check_url(w->url); + if (!browserObject->lpVtbl->QueryInterface( + browserObject, iid_unref(&IID_IWebBrowser2), (void **)&webBrowser2)) { + LPCSTR webPageName; + isDataURL = (strncmp(webview_url, WEBVIEW_DATA_URL_PREFIX, + strlen(WEBVIEW_DATA_URL_PREFIX)) == 0); + if (isDataURL) { + webPageName = "about:blank"; + } else { + webPageName = (LPCSTR)webview_url; + } + VariantInit(&myURL); + myURL.vt = VT_BSTR; +#ifndef UNICODE + { + wchar_t *buffer = webview_to_utf16(webPageName); + if (buffer == NULL) { + goto badalloc; + } + myURL.bstrVal = SysAllocString(buffer); + GlobalFree(buffer); + } +#else + myURL.bstrVal = SysAllocString(webPageName); +#endif + if (!myURL.bstrVal) { + badalloc: + webBrowser2->lpVtbl->Release(webBrowser2); + return (-6); + } + webBrowser2->lpVtbl->Navigate2(webBrowser2, &myURL, 0, 0, 0, 0); + VariantClear(&myURL); + if (!isDataURL) { + return 0; + } + + char *url = (char *)calloc(1, strlen(webview_url) + 1); + char *q = url; + for (const char *p = webview_url + strlen(WEBVIEW_DATA_URL_PREFIX); *q = *p; + p++, q++) { + if (*q == '%' && *(p + 1) && *(p + 2)) { + sscanf(p + 1, "%02x", q); + p = p + 2; + } + } + + if (webBrowser2->lpVtbl->get_Document(webBrowser2, &lpDispatch) == S_OK) { + if (lpDispatch->lpVtbl->QueryInterface(lpDispatch, + iid_unref(&IID_IHTMLDocument2), + (void **)&htmlDoc2) == S_OK) { + if ((sfArray = SafeArrayCreate(VT_VARIANT, 1, + (SAFEARRAYBOUND *)&ArrayBound))) { + if (!SafeArrayAccessData(sfArray, (void **)&pVar)) { + pVar->vt = VT_BSTR; +#ifndef UNICODE + { + wchar_t *buffer = webview_to_utf16(url); + if (buffer == NULL) { + goto release; + } + bstr = SysAllocString(buffer); + GlobalFree(buffer); + } +#else + bstr = SysAllocString(string); +#endif + if ((pVar->bstrVal = bstr)) { + htmlDoc2->lpVtbl->write(htmlDoc2, sfArray); + htmlDoc2->lpVtbl->close(htmlDoc2); + } + } + SafeArrayDestroy(sfArray); + } + release: + free(url); + htmlDoc2->lpVtbl->Release(htmlDoc2); + } + lpDispatch->lpVtbl->Release(lpDispatch); + } + webBrowser2->lpVtbl->Release(webBrowser2); + return (0); + } + return (-5); +} + +static LRESULT CALLBACK wndproc(HWND hwnd, UINT uMsg, WPARAM wParam, + LPARAM lParam) { + struct webview *w = (struct webview *)GetWindowLongPtr(hwnd, GWLP_USERDATA); + switch (uMsg) { + case WM_CREATE: + w = (struct webview *)((CREATESTRUCT *)lParam)->lpCreateParams; + w->priv.hwnd = hwnd; + return EmbedBrowserObject(w); + case WM_DESTROY: + UnEmbedBrowserObject(w); + PostQuitMessage(0); + return TRUE; + case WM_SIZE: { + IWebBrowser2 *webBrowser2; + IOleObject *browser = *w->priv.browser; + if (browser->lpVtbl->QueryInterface(browser, iid_unref(&IID_IWebBrowser2), + (void **)&webBrowser2) == S_OK) { + RECT rect; + GetClientRect(hwnd, &rect); + webBrowser2->lpVtbl->put_Width(webBrowser2, rect.right); + webBrowser2->lpVtbl->put_Height(webBrowser2, rect.bottom); + } + return TRUE; + } + case WM_WEBVIEW_DISPATCH: { + webview_dispatch_fn f = (webview_dispatch_fn)wParam; + void *arg = (void *)lParam; + (*f)(w, arg); + return TRUE; + } + } + return DefWindowProc(hwnd, uMsg, wParam, lParam); +} + +#define WEBVIEW_KEY_FEATURE_BROWSER_EMULATION \ + "Software\\Microsoft\\Internet " \ + "Explorer\\Main\\FeatureControl\\FEATURE_BROWSER_EMULATION" + +static int webview_fix_ie_compat_mode() { + HKEY hKey; + DWORD ie_version = 11000; + TCHAR appname[MAX_PATH + 1]; + TCHAR *p; + if (GetModuleFileName(NULL, appname, MAX_PATH + 1) == 0) { + return -1; + } + for (p = &appname[strlen(appname) - 1]; p != appname && *p != '\\'; p--) { + } + p++; + if (RegCreateKey(HKEY_CURRENT_USER, WEBVIEW_KEY_FEATURE_BROWSER_EMULATION, + &hKey) != ERROR_SUCCESS) { + return -1; + } + if (RegSetValueEx(hKey, p, 0, REG_DWORD, (BYTE *)&ie_version, + sizeof(ie_version)) != ERROR_SUCCESS) { + RegCloseKey(hKey); + return -1; + } + RegCloseKey(hKey); + return 0; +} + +WEBVIEW_API int webview_init(struct webview *w) { + WNDCLASSEX wc; + HINSTANCE hInstance; + DWORD style; + RECT clientRect; + RECT rect; + + if (webview_fix_ie_compat_mode() < 0) { + return -1; + } + + hInstance = GetModuleHandle(NULL); + if (hInstance == NULL) { + return -1; + } + if (OleInitialize(NULL) != S_OK) { + return -1; + } + ZeroMemory(&wc, sizeof(WNDCLASSEX)); + wc.cbSize = sizeof(WNDCLASSEX); + wc.hInstance = hInstance; + wc.lpfnWndProc = wndproc; + wc.lpszClassName = classname; + RegisterClassEx(&wc); + + style = WS_OVERLAPPEDWINDOW; + if (!w->resizable) { + style = WS_OVERLAPPED | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU; + } + + rect.left = 0; + rect.top = 0; + rect.right = w->width; + rect.bottom = w->height; + AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, 0); + + GetClientRect(GetDesktopWindow(), &clientRect); + int left = (clientRect.right / 2) - ((rect.right - rect.left) / 2); + int top = (clientRect.bottom / 2) - ((rect.bottom - rect.top) / 2); + rect.right = rect.right - rect.left + left; + rect.left = left; + rect.bottom = rect.bottom - rect.top + top; + rect.top = top; + + w->priv.hwnd = + CreateWindowEx(0, classname, w->title, style, rect.left, rect.top, + rect.right - rect.left, rect.bottom - rect.top, + HWND_DESKTOP, NULL, hInstance, (void *)w); + if (w->priv.hwnd == 0) { + OleUninitialize(); + return -1; + } + + SetWindowLongPtr(w->priv.hwnd, GWLP_USERDATA, (LONG_PTR)w); + + DisplayHTMLPage(w); + + SetWindowText(w->priv.hwnd, w->title); + ShowWindow(w->priv.hwnd, SW_SHOWDEFAULT); + UpdateWindow(w->priv.hwnd); + SetFocus(w->priv.hwnd); + + return 0; +} + +WEBVIEW_API int webview_loop(struct webview *w, int blocking) { + MSG msg; + if (blocking) { + GetMessage(&msg, 0, 0, 0); + } else { + PeekMessage(&msg, 0, 0, 0, PM_REMOVE); + } + switch (msg.message) { + case WM_QUIT: + return -1; + case WM_COMMAND: + case WM_KEYDOWN: + case WM_KEYUP: { + HRESULT r = S_OK; + IWebBrowser2 *webBrowser2; + IOleObject *browser = *w->priv.browser; + if (browser->lpVtbl->QueryInterface(browser, iid_unref(&IID_IWebBrowser2), + (void **)&webBrowser2) == S_OK) { + IOleInPlaceActiveObject *pIOIPAO; + if (browser->lpVtbl->QueryInterface( + browser, iid_unref(&IID_IOleInPlaceActiveObject), + (void **)&pIOIPAO) == S_OK) { + r = pIOIPAO->lpVtbl->TranslateAccelerator(pIOIPAO, &msg); + pIOIPAO->lpVtbl->Release(pIOIPAO); + } + webBrowser2->lpVtbl->Release(webBrowser2); + } + if (r != S_FALSE) { + break; + } + } + default: + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return 0; +} + +WEBVIEW_API int webview_eval(struct webview *w, const char *js) { + IWebBrowser2 *webBrowser2; + IHTMLDocument2 *htmlDoc2; + IDispatch *docDispatch; + IDispatch *scriptDispatch; + if ((*w->priv.browser) + ->lpVtbl->QueryInterface((*w->priv.browser), + iid_unref(&IID_IWebBrowser2), + (void **)&webBrowser2) != S_OK) { + return -1; + } + + if (webBrowser2->lpVtbl->get_Document(webBrowser2, &docDispatch) != S_OK) { + return -1; + } + if (docDispatch->lpVtbl->QueryInterface(docDispatch, + iid_unref(&IID_IHTMLDocument2), + (void **)&htmlDoc2) != S_OK) { + return -1; + } + if (htmlDoc2->lpVtbl->get_Script(htmlDoc2, &scriptDispatch) != S_OK) { + return -1; + } + DISPID dispid; + BSTR evalStr = SysAllocString(L"eval"); + if (scriptDispatch->lpVtbl->GetIDsOfNames( + scriptDispatch, iid_unref(&IID_NULL), &evalStr, 1, + LOCALE_SYSTEM_DEFAULT, &dispid) != S_OK) { + SysFreeString(evalStr); + return -1; + } + SysFreeString(evalStr); + + DISPPARAMS params; + VARIANT arg; + VARIANT result; + EXCEPINFO excepInfo; + UINT nArgErr = (UINT)-1; + params.cArgs = 1; + params.cNamedArgs = 0; + params.rgvarg = &arg; + arg.vt = VT_BSTR; + static const char *prologue = "(function(){"; + static const char *epilogue = ";})();"; + int n = strlen(prologue) + strlen(epilogue) + strlen(js) + 1; + char *eval = (char *)malloc(n); + snprintf(eval, n, "%s%s%s", prologue, js, epilogue); + wchar_t *buf = webview_to_utf16(eval); + if (buf == NULL) { + return -1; + } + arg.bstrVal = SysAllocString(buf); + if (scriptDispatch->lpVtbl->Invoke( + scriptDispatch, dispid, iid_unref(&IID_NULL), 0, DISPATCH_METHOD, + ¶ms, &result, &excepInfo, &nArgErr) != S_OK) { + return -1; + } + SysFreeString(arg.bstrVal); + free(eval); + scriptDispatch->lpVtbl->Release(scriptDispatch); + htmlDoc2->lpVtbl->Release(htmlDoc2); + docDispatch->lpVtbl->Release(docDispatch); + return 0; +} + +WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn, + void *arg) { + PostMessageW(w->priv.hwnd, WM_WEBVIEW_DISPATCH, (WPARAM)fn, (LPARAM)arg); +} + +WEBVIEW_API void webview_set_title(struct webview *w, const char *title) { + SetWindowText(w->priv.hwnd, title); +} + +WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen) { + if (w->priv.is_fullscreen == !!fullscreen) { + return; + } + if (w->priv.is_fullscreen == 0) { + w->priv.saved_style = GetWindowLong(w->priv.hwnd, GWL_STYLE); + w->priv.saved_ex_style = GetWindowLong(w->priv.hwnd, GWL_EXSTYLE); + GetWindowRect(w->priv.hwnd, &w->priv.saved_rect); + } + w->priv.is_fullscreen = !!fullscreen; + if (fullscreen) { + MONITORINFO monitor_info; + SetWindowLong(w->priv.hwnd, GWL_STYLE, + w->priv.saved_style & ~(WS_CAPTION | WS_THICKFRAME)); + SetWindowLong(w->priv.hwnd, GWL_EXSTYLE, + w->priv.saved_ex_style & + ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | + WS_EX_CLIENTEDGE | WS_EX_STATICEDGE)); + monitor_info.cbSize = sizeof(monitor_info); + GetMonitorInfo(MonitorFromWindow(w->priv.hwnd, MONITOR_DEFAULTTONEAREST), + &monitor_info); + RECT r; + r.left = monitor_info.rcMonitor.left; + r.top = monitor_info.rcMonitor.top; + r.right = monitor_info.rcMonitor.right; + r.bottom = monitor_info.rcMonitor.bottom; + SetWindowPos(w->priv.hwnd, NULL, r.left, r.top, r.right - r.left, + r.bottom - r.top, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } else { + SetWindowLong(w->priv.hwnd, GWL_STYLE, w->priv.saved_style); + SetWindowLong(w->priv.hwnd, GWL_EXSTYLE, w->priv.saved_ex_style); + SetWindowPos(w->priv.hwnd, NULL, w->priv.saved_rect.left, + w->priv.saved_rect.top, + w->priv.saved_rect.right - w->priv.saved_rect.left, + w->priv.saved_rect.bottom - w->priv.saved_rect.top, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } +} + +WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g, + uint8_t b, uint8_t a) { + HBRUSH brush = CreateSolidBrush(RGB(r, g, b)); + SetClassLongPtr(w->priv.hwnd, GCLP_HBRBACKGROUND, (LONG_PTR)brush); +} + +/* These are missing parts from MinGW */ +#ifndef __IFileDialog_INTERFACE_DEFINED__ +#define __IFileDialog_INTERFACE_DEFINED__ +enum _FILEOPENDIALOGOPTIONS { + FOS_OVERWRITEPROMPT = 0x2, + FOS_STRICTFILETYPES = 0x4, + FOS_NOCHANGEDIR = 0x8, + FOS_PICKFOLDERS = 0x20, + FOS_FORCEFILESYSTEM = 0x40, + FOS_ALLNONSTORAGEITEMS = 0x80, + FOS_NOVALIDATE = 0x100, + FOS_ALLOWMULTISELECT = 0x200, + FOS_PATHMUSTEXIST = 0x800, + FOS_FILEMUSTEXIST = 0x1000, + FOS_CREATEPROMPT = 0x2000, + FOS_SHAREAWARE = 0x4000, + FOS_NOREADONLYRETURN = 0x8000, + FOS_NOTESTFILECREATE = 0x10000, + FOS_HIDEMRUPLACES = 0x20000, + FOS_HIDEPINNEDPLACES = 0x40000, + FOS_NODEREFERENCELINKS = 0x100000, + FOS_DONTADDTORECENT = 0x2000000, + FOS_FORCESHOWHIDDEN = 0x10000000, + FOS_DEFAULTNOMINIMODE = 0x20000000, + FOS_FORCEPREVIEWPANEON = 0x40000000 +}; +typedef DWORD FILEOPENDIALOGOPTIONS; +typedef enum FDAP { FDAP_BOTTOM = 0, FDAP_TOP = 1 } FDAP; +DEFINE_GUID(IID_IFileDialog, 0x42f85136, 0xdb7e, 0x439c, 0x85, 0xf1, 0xe4, 0x07, + 0x5d, 0x13, 0x5f, 0xc8); +typedef struct IFileDialogVtbl { + BEGIN_INTERFACE + HRESULT(STDMETHODCALLTYPE *QueryInterface) + (IFileDialog *This, REFIID riid, void **ppvObject); + ULONG(STDMETHODCALLTYPE *AddRef)(IFileDialog *This); + ULONG(STDMETHODCALLTYPE *Release)(IFileDialog *This); + HRESULT(STDMETHODCALLTYPE *Show)(IFileDialog *This, HWND hwndOwner); + HRESULT(STDMETHODCALLTYPE *SetFileTypes) + (IFileDialog *This, UINT cFileTypes, const COMDLG_FILTERSPEC *rgFilterSpec); + HRESULT(STDMETHODCALLTYPE *SetFileTypeIndex) + (IFileDialog *This, UINT iFileType); + HRESULT(STDMETHODCALLTYPE *GetFileTypeIndex) + (IFileDialog *This, UINT *piFileType); + HRESULT(STDMETHODCALLTYPE *Advise) + (IFileDialog *This, IFileDialogEvents *pfde, DWORD *pdwCookie); + HRESULT(STDMETHODCALLTYPE *Unadvise)(IFileDialog *This, DWORD dwCookie); + HRESULT(STDMETHODCALLTYPE *SetOptions) + (IFileDialog *This, FILEOPENDIALOGOPTIONS fos); + HRESULT(STDMETHODCALLTYPE *GetOptions) + (IFileDialog *This, FILEOPENDIALOGOPTIONS *pfos); + HRESULT(STDMETHODCALLTYPE *SetDefaultFolder) + (IFileDialog *This, IShellItem *psi); + HRESULT(STDMETHODCALLTYPE *SetFolder)(IFileDialog *This, IShellItem *psi); + HRESULT(STDMETHODCALLTYPE *GetFolder)(IFileDialog *This, IShellItem **ppsi); + HRESULT(STDMETHODCALLTYPE *GetCurrentSelection) + (IFileDialog *This, IShellItem **ppsi); + HRESULT(STDMETHODCALLTYPE *SetFileName)(IFileDialog *This, LPCWSTR pszName); + HRESULT(STDMETHODCALLTYPE *GetFileName)(IFileDialog *This, LPWSTR *pszName); + HRESULT(STDMETHODCALLTYPE *SetTitle)(IFileDialog *This, LPCWSTR pszTitle); + HRESULT(STDMETHODCALLTYPE *SetOkButtonLabel) + (IFileDialog *This, LPCWSTR pszText); + HRESULT(STDMETHODCALLTYPE *SetFileNameLabel) + (IFileDialog *This, LPCWSTR pszLabel); + HRESULT(STDMETHODCALLTYPE *GetResult)(IFileDialog *This, IShellItem **ppsi); + HRESULT(STDMETHODCALLTYPE *AddPlace) + (IFileDialog *This, IShellItem *psi, FDAP fdap); + HRESULT(STDMETHODCALLTYPE *SetDefaultExtension) + (IFileDialog *This, LPCWSTR pszDefaultExtension); + HRESULT(STDMETHODCALLTYPE *Close)(IFileDialog *This, HRESULT hr); + HRESULT(STDMETHODCALLTYPE *SetClientGuid)(IFileDialog *This, REFGUID guid); + HRESULT(STDMETHODCALLTYPE *ClearClientData)(IFileDialog *This); + HRESULT(STDMETHODCALLTYPE *SetFilter) + (IFileDialog *This, IShellItemFilter *pFilter); + END_INTERFACE +} IFileDialogVtbl; +interface IFileDialog { + CONST_VTBL IFileDialogVtbl *lpVtbl; +}; +DEFINE_GUID(IID_IFileOpenDialog, 0xd57c7288, 0xd4ad, 0x4768, 0xbe, 0x02, 0x9d, + 0x96, 0x95, 0x32, 0xd9, 0x60); +DEFINE_GUID(IID_IFileSaveDialog, 0x84bccd23, 0x5fde, 0x4cdb, 0xae, 0xa4, 0xaf, + 0x64, 0xb8, 0x3d, 0x78, 0xab); +#endif + +WEBVIEW_API void webview_dialog(struct webview *w, + enum webview_dialog_type dlgtype, int flags, + const char *title, const char *arg, + char *result, size_t resultsz) { + if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN || + dlgtype == WEBVIEW_DIALOG_TYPE_SAVE) { + IFileDialog *dlg = NULL; + IShellItem *res = NULL; + WCHAR *ws = NULL; + char *s = NULL; + FILEOPENDIALOGOPTIONS opts, add_opts; + if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN) { + if (CoCreateInstance( + iid_unref(&CLSID_FileOpenDialog), NULL, CLSCTX_INPROC_SERVER, + iid_unref(&IID_IFileOpenDialog), (void **)&dlg) != S_OK) { + goto error_dlg; + } + if (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY) { + add_opts |= FOS_PICKFOLDERS; + } + add_opts |= FOS_NOCHANGEDIR | FOS_ALLNONSTORAGEITEMS | FOS_NOVALIDATE | + FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_SHAREAWARE | + FOS_NOTESTFILECREATE | FOS_NODEREFERENCELINKS | + FOS_FORCESHOWHIDDEN | FOS_DEFAULTNOMINIMODE; + } else { + if (CoCreateInstance( + iid_unref(&CLSID_FileSaveDialog), NULL, CLSCTX_INPROC_SERVER, + iid_unref(&IID_IFileSaveDialog), (void **)&dlg) != S_OK) { + goto error_dlg; + } + add_opts |= FOS_OVERWRITEPROMPT | FOS_NOCHANGEDIR | + FOS_ALLNONSTORAGEITEMS | FOS_NOVALIDATE | FOS_SHAREAWARE | + FOS_NOTESTFILECREATE | FOS_NODEREFERENCELINKS | + FOS_FORCESHOWHIDDEN | FOS_DEFAULTNOMINIMODE; + } + if (dlg->lpVtbl->GetOptions(dlg, &opts) != S_OK) { + goto error_dlg; + } + opts &= ~FOS_NOREADONLYRETURN; + opts |= add_opts; + if (dlg->lpVtbl->SetOptions(dlg, opts) != S_OK) { + goto error_dlg; + } + if (dlg->lpVtbl->Show(dlg, w->priv.hwnd) != S_OK) { + goto error_dlg; + } + if (dlg->lpVtbl->GetResult(dlg, &res) != S_OK) { + goto error_dlg; + } + if (res->lpVtbl->GetDisplayName(res, SIGDN_FILESYSPATH, &ws) != S_OK) { + goto error_result; + } + s = webview_from_utf16(ws); + strncpy(result, s, resultsz); + result[resultsz - 1] = '\0'; + CoTaskMemFree(ws); + error_result: + res->lpVtbl->Release(res); + error_dlg: + dlg->lpVtbl->Release(dlg); + return; + } else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT) { +#if 0 + /* MinGW often doesn't contain TaskDialog, we'll use MessageBox for now */ + WCHAR *wtitle = webview_to_utf16(title); + WCHAR *warg = webview_to_utf16(arg); + TaskDialog(w->priv.hwnd, NULL, NULL, wtitle, warg, 0, NULL, NULL); + GlobalFree(warg); + GlobalFree(wtitle); +#else + UINT type = MB_OK; + switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK) { + case WEBVIEW_DIALOG_FLAG_INFO: + type |= MB_ICONINFORMATION; + break; + case WEBVIEW_DIALOG_FLAG_WARNING: + type |= MB_ICONWARNING; + break; + case WEBVIEW_DIALOG_FLAG_ERROR: + type |= MB_ICONERROR; + break; + } + MessageBox(w->priv.hwnd, arg, title, type); +#endif + } +} + +WEBVIEW_API void webview_terminate(struct webview *w) { PostQuitMessage(0); } + +WEBVIEW_API void webview_exit(struct webview *w) { + DestroyWindow(w->priv.hwnd); + OleUninitialize(); +} + +WEBVIEW_API void webview_print_log(const char *s) { OutputDebugString(s); } + +#endif /* WEBVIEW_WINAPI */ + +#if defined(WEBVIEW_COCOA) +#define NSAlertStyleWarning 0 +#define NSAlertStyleCritical 2 +#define NSWindowStyleMaskResizable 8 +#define NSWindowStyleMaskMiniaturizable 4 +#define NSWindowStyleMaskTitled 1 +#define NSWindowStyleMaskClosable 2 +#define NSWindowStyleMaskFullScreen (1 << 14) +#define NSViewWidthSizable 2 +#define NSViewHeightSizable 16 +#define NSBackingStoreBuffered 2 +#define NSEventMaskAny ULONG_MAX +#define NSEventModifierFlagCommand (1 << 20) +#define NSEventModifierFlagOption (1 << 19) +#define NSAlertStyleInformational 1 +#define NSAlertFirstButtonReturn 1000 +#define WKNavigationActionPolicyDownload 2 +#define NSModalResponseOK 1 +#define WKNavigationActionPolicyDownload 2 +#define WKNavigationResponsePolicyAllow 1 +#define WKUserScriptInjectionTimeAtDocumentStart 0 +#define NSApplicationActivationPolicyRegular 0 + +static id get_nsstring(const char *c_str) { + return objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), c_str); +} + +static id create_menu_item(id title, const char *action, const char *key) { + id item = + objc_msgSend((id)objc_getClass("NSMenuItem"), sel_registerName("alloc")); + objc_msgSend(item, sel_registerName("initWithTitle:action:keyEquivalent:"), + title, sel_registerName(action), get_nsstring(key)); + objc_msgSend(item, sel_registerName("autorelease")); + + return item; +} + +static void webview_window_will_close(id self, SEL cmd, id notification) { + struct webview *w = + (struct webview *)objc_getAssociatedObject(self, "webview"); + webview_terminate(w); +} + +static void webview_external_invoke(id self, SEL cmd, id contentController, + id message) { + struct webview *w = + (struct webview *)objc_getAssociatedObject(contentController, "webview"); + if (w == NULL || w->external_invoke_cb == NULL) { + return; + } + + w->external_invoke_cb(w, (const char *)objc_msgSend( + objc_msgSend(message, sel_registerName("body")), + sel_registerName("UTF8String"))); +} + +static void run_open_panel(id self, SEL cmd, id webView, id parameters, + id frame, void (^completionHandler)(id)) { + + id openPanel = objc_msgSend((id)objc_getClass("NSOpenPanel"), + sel_registerName("openPanel")); + + objc_msgSend( + openPanel, sel_registerName("setAllowsMultipleSelection:"), + objc_msgSend(parameters, sel_registerName("allowsMultipleSelection"))); + + objc_msgSend(openPanel, sel_registerName("setCanChooseFiles:"), 1); + objc_msgSend( + openPanel, sel_registerName("beginWithCompletionHandler:"), ^(id result) { + if (result == (id)NSModalResponseOK) { + completionHandler(objc_msgSend(openPanel, sel_registerName("URLs"))); + } else { + completionHandler(nil); + } + }); +} + +static void run_save_panel(id self, SEL cmd, id download, id filename, + void (^completionHandler)(int allowOverwrite, + id destination)) { + id savePanel = objc_msgSend((id)objc_getClass("NSSavePanel"), + sel_registerName("savePanel")); + objc_msgSend(savePanel, sel_registerName("setCanCreateDirectories:"), 1); + objc_msgSend(savePanel, sel_registerName("setNameFieldStringValue:"), + filename); + objc_msgSend(savePanel, sel_registerName("beginWithCompletionHandler:"), + ^(id result) { + if (result == (id)NSModalResponseOK) { + id url = objc_msgSend(savePanel, sel_registerName("URL")); + id path = objc_msgSend(url, sel_registerName("path")); + completionHandler(1, path); + } else { + completionHandler(NO, nil); + } + }); +} + +static void run_confirmation_panel(id self, SEL cmd, id webView, id message, + id frame, void (^completionHandler)(bool)) { + + id alert = + objc_msgSend((id)objc_getClass("NSAlert"), sel_registerName("new")); + objc_msgSend(alert, sel_registerName("setIcon:"), + objc_msgSend((id)objc_getClass("NSImage"), + sel_registerName("imageNamed:"), + get_nsstring("NSCaution"))); + objc_msgSend(alert, sel_registerName("setShowsHelp:"), 0); + objc_msgSend(alert, sel_registerName("setInformativeText:"), message); + objc_msgSend(alert, sel_registerName("addButtonWithTitle:"), + get_nsstring("OK")); + objc_msgSend(alert, sel_registerName("addButtonWithTitle:"), + get_nsstring("Cancel")); + if (objc_msgSend(alert, sel_registerName("runModal")) == + (id)NSAlertFirstButtonReturn) { + completionHandler(true); + } else { + completionHandler(false); + } + objc_msgSend(alert, sel_registerName("release")); +} + +static void run_alert_panel(id self, SEL cmd, id webView, id message, id frame, + void (^completionHandler)(void)) { + id alert = + objc_msgSend((id)objc_getClass("NSAlert"), sel_registerName("new")); + objc_msgSend(alert, sel_registerName("setIcon:"), + objc_msgSend((id)objc_getClass("NSImage"), + sel_registerName("imageNamed:"), + get_nsstring("NSCaution"))); + objc_msgSend(alert, sel_registerName("setShowsHelp:"), 0); + objc_msgSend(alert, sel_registerName("setInformativeText:"), message); + objc_msgSend(alert, sel_registerName("addButtonWithTitle:"), + get_nsstring("OK")); + objc_msgSend(alert, sel_registerName("runModal")); + objc_msgSend(alert, sel_registerName("release")); + completionHandler(); +} + +static void download_failed(id self, SEL cmd, id download, id error) { + printf("%s", + (const char *)objc_msgSend( + objc_msgSend(error, sel_registerName("localizedDescription")), + sel_registerName("UTF8String"))); +} + +static void make_nav_policy_decision(id self, SEL cmd, id webView, id response, + void (^decisionHandler)(int)) { + if (objc_msgSend(response, sel_registerName("canShowMIMEType")) == 0) { + decisionHandler(WKNavigationActionPolicyDownload); + } else { + decisionHandler(WKNavigationResponsePolicyAllow); + } +} + +WEBVIEW_API int webview_init(struct webview *w) { + w->priv.pool = objc_msgSend((id)objc_getClass("NSAutoreleasePool"), + sel_registerName("new")); + objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")); + + Class __WKScriptMessageHandler = objc_allocateClassPair( + objc_getClass("NSObject"), "__WKScriptMessageHandler", 0); + class_addMethod( + __WKScriptMessageHandler, + sel_registerName("userContentController:didReceiveScriptMessage:"), + (IMP)webview_external_invoke, "v@:@@"); + objc_registerClassPair(__WKScriptMessageHandler); + + id scriptMessageHandler = + objc_msgSend((id)__WKScriptMessageHandler, sel_registerName("new")); + + /*** + _WKDownloadDelegate is an undocumented/private protocol with methods called + from WKNavigationDelegate + References: + https://github.com/WebKit/webkit/blob/master/Source/WebKit/UIProcess/API/Cocoa/_WKDownload.h + https://github.com/WebKit/webkit/blob/master/Source/WebKit/UIProcess/API/Cocoa/_WKDownloadDelegate.h + https://github.com/WebKit/webkit/blob/master/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm + ***/ + + Class __WKDownloadDelegate = objc_allocateClassPair( + objc_getClass("NSObject"), "__WKDownloadDelegate", 0); + class_addMethod( + __WKDownloadDelegate, + sel_registerName("_download:decideDestinationWithSuggestedFilename:" + "completionHandler:"), + (IMP)run_save_panel, "v@:@@?"); + class_addMethod(__WKDownloadDelegate, + sel_registerName("_download:didFailWithError:"), + (IMP)download_failed, "v@:@@"); + objc_registerClassPair(__WKDownloadDelegate); + id downloadDelegate = + objc_msgSend((id)__WKDownloadDelegate, sel_registerName("new")); + + Class __WKPreferences = objc_allocateClassPair(objc_getClass("WKPreferences"), + "__WKPreferences", 0); + objc_property_attribute_t type = {"T", "c"}; + objc_property_attribute_t ownership = {"N", ""}; + objc_property_attribute_t attrs[] = {type, ownership}; + class_replaceProperty(__WKPreferences, "developerExtrasEnabled", attrs, 2); + objc_registerClassPair(__WKPreferences); + id wkPref = objc_msgSend((id)__WKPreferences, sel_registerName("new")); + objc_msgSend(wkPref, sel_registerName("setValue:forKey:"), + objc_msgSend((id)objc_getClass("NSNumber"), + sel_registerName("numberWithBool:"), !!w->debug), + objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), + "developerExtrasEnabled")); + + id userController = objc_msgSend((id)objc_getClass("WKUserContentController"), + sel_registerName("new")); + objc_setAssociatedObject(userController, "webview", (id)(w), + OBJC_ASSOCIATION_ASSIGN); + objc_msgSend( + userController, sel_registerName("addScriptMessageHandler:name:"), + scriptMessageHandler, + objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), "invoke")); + + /*** + In order to maintain compatibility with the other 'webviews' we need to + override window.external.invoke to call + webkit.messageHandlers.invoke.postMessage + ***/ + + id windowExternalOverrideScript = objc_msgSend( + (id)objc_getClass("WKUserScript"), sel_registerName("alloc")); + objc_msgSend( + windowExternalOverrideScript, + sel_registerName("initWithSource:injectionTime:forMainFrameOnly:"), + get_nsstring("window.external = this; invoke = function(arg){ " + "webkit.messageHandlers.invoke.postMessage(arg); };"), + WKUserScriptInjectionTimeAtDocumentStart, 0); + + objc_msgSend(userController, sel_registerName("addUserScript:"), + windowExternalOverrideScript); + + id config = objc_msgSend((id)objc_getClass("WKWebViewConfiguration"), + sel_registerName("new")); + id processPool = objc_msgSend(config, sel_registerName("processPool")); + objc_msgSend(processPool, sel_registerName("_setDownloadDelegate:"), + downloadDelegate); + objc_msgSend(config, sel_registerName("setProcessPool:"), processPool); + objc_msgSend(config, sel_registerName("setUserContentController:"), + userController); + objc_msgSend(config, sel_registerName("setPreferences:"), wkPref); + + Class __NSWindowDelegate = objc_allocateClassPair(objc_getClass("NSObject"), + "__NSWindowDelegate", 0); + class_addProtocol(__NSWindowDelegate, objc_getProtocol("NSWindowDelegate")); + class_replaceMethod(__NSWindowDelegate, sel_registerName("windowWillClose:"), + (IMP)webview_window_will_close, "v@:@"); + objc_registerClassPair(__NSWindowDelegate); + + w->priv.windowDelegate = + objc_msgSend((id)__NSWindowDelegate, sel_registerName("new")); + + objc_setAssociatedObject(w->priv.windowDelegate, "webview", (id)(w), + OBJC_ASSOCIATION_ASSIGN); + + id nsTitle = + objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), w->title); + + CGRect r = CGRectMake(0, 0, w->width, w->height); + + unsigned int style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable; + if (w->resizable) { + style = style | NSWindowStyleMaskResizable; + } + + w->priv.window = + objc_msgSend((id)objc_getClass("NSWindow"), sel_registerName("alloc")); + objc_msgSend(w->priv.window, + sel_registerName("initWithContentRect:styleMask:backing:defer:"), + r, style, NSBackingStoreBuffered, 0); + + objc_msgSend(w->priv.window, sel_registerName("autorelease")); + objc_msgSend(w->priv.window, sel_registerName("setTitle:"), nsTitle); + objc_msgSend(w->priv.window, sel_registerName("setDelegate:"), + w->priv.windowDelegate); + objc_msgSend(w->priv.window, sel_registerName("center")); + + Class __WKUIDelegate = + objc_allocateClassPair(objc_getClass("NSObject"), "__WKUIDelegate", 0); + class_addProtocol(__WKUIDelegate, objc_getProtocol("WKUIDelegate")); + class_addMethod(__WKUIDelegate, + sel_registerName("webView:runOpenPanelWithParameters:" + "initiatedByFrame:completionHandler:"), + (IMP)run_open_panel, "v@:@@@?"); + class_addMethod(__WKUIDelegate, + sel_registerName("webView:runJavaScriptAlertPanelWithMessage:" + "initiatedByFrame:completionHandler:"), + (IMP)run_alert_panel, "v@:@@@?"); + class_addMethod( + __WKUIDelegate, + sel_registerName("webView:runJavaScriptConfirmPanelWithMessage:" + "initiatedByFrame:completionHandler:"), + (IMP)run_confirmation_panel, "v@:@@@?"); + objc_registerClassPair(__WKUIDelegate); + id uiDel = objc_msgSend((id)__WKUIDelegate, sel_registerName("new")); + + Class __WKNavigationDelegate = objc_allocateClassPair( + objc_getClass("NSObject"), "__WKNavigationDelegate", 0); + class_addProtocol(__WKNavigationDelegate, + objc_getProtocol("WKNavigationDelegate")); + class_addMethod( + __WKNavigationDelegate, + sel_registerName( + "webView:decidePolicyForNavigationResponse:decisionHandler:"), + (IMP)make_nav_policy_decision, "v@:@@?"); + objc_registerClassPair(__WKNavigationDelegate); + id navDel = objc_msgSend((id)__WKNavigationDelegate, sel_registerName("new")); + + w->priv.webview = + objc_msgSend((id)objc_getClass("WKWebView"), sel_registerName("alloc")); + objc_msgSend(w->priv.webview, + sel_registerName("initWithFrame:configuration:"), r, config); + objc_msgSend(w->priv.webview, sel_registerName("setUIDelegate:"), uiDel); + objc_msgSend(w->priv.webview, sel_registerName("setNavigationDelegate:"), + navDel); + + id nsURL = objc_msgSend((id)objc_getClass("NSURL"), + sel_registerName("URLWithString:"), + get_nsstring(webview_check_url(w->url))); + + objc_msgSend(w->priv.webview, sel_registerName("loadRequest:"), + objc_msgSend((id)objc_getClass("NSURLRequest"), + sel_registerName("requestWithURL:"), nsURL)); + objc_msgSend(w->priv.webview, sel_registerName("setAutoresizesSubviews:"), 1); + objc_msgSend(w->priv.webview, sel_registerName("setAutoresizingMask:"), + (NSViewWidthSizable | NSViewHeightSizable)); + objc_msgSend(objc_msgSend(w->priv.window, sel_registerName("contentView")), + sel_registerName("addSubview:"), w->priv.webview); + objc_msgSend(w->priv.window, sel_registerName("orderFrontRegardless")); + + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("setActivationPolicy:"), + NSApplicationActivationPolicyRegular); + + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("finishLaunching")); + + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("activateIgnoringOtherApps:"), 1); + + id menubar = + objc_msgSend((id)objc_getClass("NSMenu"), sel_registerName("alloc")); + objc_msgSend(menubar, sel_registerName("initWithTitle:"), get_nsstring("")); + objc_msgSend(menubar, sel_registerName("autorelease")); + + id appName = objc_msgSend(objc_msgSend((id)objc_getClass("NSProcessInfo"), + sel_registerName("processInfo")), + sel_registerName("processName")); + + id appMenuItem = + objc_msgSend((id)objc_getClass("NSMenuItem"), sel_registerName("alloc")); + objc_msgSend(appMenuItem, + sel_registerName("initWithTitle:action:keyEquivalent:"), appName, + NULL, get_nsstring("")); + + id appMenu = + objc_msgSend((id)objc_getClass("NSMenu"), sel_registerName("alloc")); + objc_msgSend(appMenu, sel_registerName("initWithTitle:"), appName); + objc_msgSend(appMenu, sel_registerName("autorelease")); + + objc_msgSend(appMenuItem, sel_registerName("setSubmenu:"), appMenu); + objc_msgSend(menubar, sel_registerName("addItem:"), appMenuItem); + + id title = + objc_msgSend(get_nsstring("Hide "), + sel_registerName("stringByAppendingString:"), appName); + id item = create_menu_item(title, "hide:", "h"); + objc_msgSend(appMenu, sel_registerName("addItem:"), item); + + item = create_menu_item(get_nsstring("Hide Others"), + "hideOtherApplications:", "h"); + objc_msgSend(item, sel_registerName("setKeyEquivalentModifierMask:"), + (NSEventModifierFlagOption | NSEventModifierFlagCommand)); + objc_msgSend(appMenu, sel_registerName("addItem:"), item); + + item = + create_menu_item(get_nsstring("Show All"), "unhideAllApplications:", ""); + objc_msgSend(appMenu, sel_registerName("addItem:"), item); + + objc_msgSend(appMenu, sel_registerName("addItem:"), + objc_msgSend((id)objc_getClass("NSMenuItem"), + sel_registerName("separatorItem"))); + + title = objc_msgSend(get_nsstring("Quit "), + sel_registerName("stringByAppendingString:"), appName); + item = create_menu_item(title, "terminate:", "q"); + objc_msgSend(appMenu, sel_registerName("addItem:"), item); + + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("setMainMenu:"), menubar); + + w->priv.should_exit = 0; + return 0; +} + +WEBVIEW_API int webview_loop(struct webview *w, int blocking) { + id until = (blocking ? objc_msgSend((id)objc_getClass("NSDate"), + sel_registerName("distantFuture")) + : objc_msgSend((id)objc_getClass("NSDate"), + sel_registerName("distantPast"))); + + id event = objc_msgSend( + objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("nextEventMatchingMask:untilDate:inMode:dequeue:"), + ULONG_MAX, until, + objc_msgSend((id)objc_getClass("NSString"), + sel_registerName("stringWithUTF8String:"), + "kCFRunLoopDefaultMode"), + true); + + if (event) { + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("sendEvent:"), event); + } + + return w->priv.should_exit; +} + +WEBVIEW_API int webview_eval(struct webview *w, const char *js) { + objc_msgSend(w->priv.webview, + sel_registerName("evaluateJavaScript:completionHandler:"), + get_nsstring(js), NULL); + + return 0; +} + +WEBVIEW_API void webview_set_title(struct webview *w, const char *title) { + objc_msgSend(w->priv.window, sel_registerName("setTitle"), + get_nsstring(title)); +} + +WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen) { + unsigned long windowStyleMask = (unsigned long)objc_msgSend( + w->priv.window, sel_registerName("styleMask")); + int b = (((windowStyleMask & NSWindowStyleMaskFullScreen) == + NSWindowStyleMaskFullScreen) + ? 1 + : 0); + if (b != fullscreen) { + objc_msgSend(w->priv.window, sel_registerName("toggleFullScreen:"), NULL); + } +} + +WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g, + uint8_t b, uint8_t a) { + + id color = objc_msgSend((id)objc_getClass("NSColor"), + sel_registerName("colorWithRed:green:blue:alpha:"), + (float)r / 255.0, (float)g / 255.0, (float)b / 255.0, + (float)a / 255.0); + + objc_msgSend(w->priv.window, sel_registerName("setBackgroundColor:"), color); + + if (0.5 >= ((r / 255.0 * 299.0) + (g / 255.0 * 587.0) + (b / 255.0 * 114.0)) / + 1000.0) { + objc_msgSend(w->priv.window, sel_registerName("setAppearance:"), + objc_msgSend((id)objc_getClass("NSAppearance"), + sel_registerName("appearanceNamed:"), + get_nsstring("NSAppearanceNameVibrantDark"))); + } else { + objc_msgSend(w->priv.window, sel_registerName("setAppearance:"), + objc_msgSend((id)objc_getClass("NSAppearance"), + sel_registerName("appearanceNamed:"), + get_nsstring("NSAppearanceNameVibrantLight"))); + } + objc_msgSend(w->priv.window, sel_registerName("setOpaque:"), 0); + objc_msgSend(w->priv.window, + sel_registerName("setTitlebarAppearsTransparent:"), 1); +} + +WEBVIEW_API void webview_dialog(struct webview *w, + enum webview_dialog_type dlgtype, int flags, + const char *title, const char *arg, + char *result, size_t resultsz) { + if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN || + dlgtype == WEBVIEW_DIALOG_TYPE_SAVE) { + id panel = (id)objc_getClass("NSSavePanel"); + if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN) { + id openPanel = objc_msgSend((id)objc_getClass("NSOpenPanel"), + sel_registerName("openPanel")); + if (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY) { + objc_msgSend(openPanel, sel_registerName("setCanChooseFiles:"), 0); + objc_msgSend(openPanel, sel_registerName("setCanChooseDirectories:"), + 1); + } else { + objc_msgSend(openPanel, sel_registerName("setCanChooseFiles:"), 1); + objc_msgSend(openPanel, sel_registerName("setCanChooseDirectories:"), + 0); + } + objc_msgSend(openPanel, sel_registerName("setResolvesAliases:"), 0); + objc_msgSend(openPanel, sel_registerName("setAllowsMultipleSelection:"), + 0); + panel = openPanel; + } else { + panel = objc_msgSend((id)objc_getClass("NSSavePanel"), + sel_registerName("savePanel")); + } + + objc_msgSend(panel, sel_registerName("setCanCreateDirectories:"), 1); + objc_msgSend(panel, sel_registerName("setShowsHiddenFiles:"), 1); + objc_msgSend(panel, sel_registerName("setExtensionHidden:"), 0); + objc_msgSend(panel, sel_registerName("setCanSelectHiddenExtension:"), 0); + objc_msgSend(panel, sel_registerName("setTreatsFilePackagesAsDirectories:"), + 1); + objc_msgSend( + panel, sel_registerName("beginSheetModalForWindow:completionHandler:"), + w->priv.window, ^(id result) { + objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("stopModalWithCode:"), result); + }); + + if (objc_msgSend(objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")), + sel_registerName("runModalForWindow:"), + panel) == (id)NSModalResponseOK) { + id url = objc_msgSend(panel, sel_registerName("URL")); + id path = objc_msgSend(url, sel_registerName("path")); + const char *filename = + (const char *)objc_msgSend(path, sel_registerName("UTF8String")); + strlcpy(result, filename, resultsz); + } + } else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT) { + id a = objc_msgSend((id)objc_getClass("NSAlert"), sel_registerName("new")); + switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK) { + case WEBVIEW_DIALOG_FLAG_INFO: + objc_msgSend(a, sel_registerName("setAlertStyle:"), + NSAlertStyleInformational); + break; + case WEBVIEW_DIALOG_FLAG_WARNING: + printf("Warning\n"); + objc_msgSend(a, sel_registerName("setAlertStyle:"), NSAlertStyleWarning); + break; + case WEBVIEW_DIALOG_FLAG_ERROR: + printf("Error\n"); + objc_msgSend(a, sel_registerName("setAlertStyle:"), NSAlertStyleCritical); + break; + } + objc_msgSend(a, sel_registerName("setShowsHelp:"), 0); + objc_msgSend(a, sel_registerName("setShowsSuppressionButton:"), 0); + objc_msgSend(a, sel_registerName("setMessageText:"), get_nsstring(title)); + objc_msgSend(a, sel_registerName("setInformativeText:"), get_nsstring(arg)); + objc_msgSend(a, sel_registerName("addButtonWithTitle:"), + get_nsstring("OK")); + objc_msgSend(a, sel_registerName("runModal")); + objc_msgSend(a, sel_registerName("release")); + } +} + +static void webview_dispatch_cb(void *arg) { + struct webview_dispatch_arg *context = (struct webview_dispatch_arg *)arg; + (context->fn)(context->w, context->arg); + free(context); +} + +WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn, + void *arg) { + struct webview_dispatch_arg *context = (struct webview_dispatch_arg *)malloc( + sizeof(struct webview_dispatch_arg)); + context->w = w; + context->arg = arg; + context->fn = fn; + dispatch_async_f(dispatch_get_main_queue(), context, webview_dispatch_cb); +} + +WEBVIEW_API void webview_terminate(struct webview *w) { + w->priv.should_exit = 1; +} + +WEBVIEW_API void webview_exit(struct webview *w) { + id app = objc_msgSend((id)objc_getClass("NSApplication"), + sel_registerName("sharedApplication")); + objc_msgSend(app, sel_registerName("terminate:"), app); +} + +WEBVIEW_API void webview_print_log(const char *s) { printf("%s\n", s); } + +#endif /* WEBVIEW_COCOA */ + +#endif /* WEBVIEW_IMPLEMENTATION */ + +#ifdef __cplusplus +} +#endif + +#endif /* WEBVIEW_H */ diff --git a/ui/proton_test.cc b/ui/proton_test.cc new file mode 100644 index 000000000..2074cc84a --- /dev/null +++ b/ui/proton_test.cc @@ -0,0 +1,176 @@ +// +build ignore + +#include +#include +#include +#include +#include +#include +#include +#include + +#define WEBVIEW_IMPLEMENTATION +#include "proton.h" + +extern "C" void webview_dispatch_proxy(struct webview *w, void *arg) { + (*static_cast *>(arg))(w); +} + +class runner { +public: + runner(struct webview *w) : w(w) { webview_init(this->w); } + ~runner() { webview_exit(this->w); } + runner &then(std::function fn) { + auto arg = new std::pair, void *>( + fn, nullptr); + this->queue.push_back([=](struct webview *w) { + webview_dispatch( + w, + [](struct webview *w, void *arg) { + auto dispatch_arg = reinterpret_cast< + std::pair, void *> *>( + arg); + dispatch_arg->first(w); + delete dispatch_arg; + }, + reinterpret_cast(arg)); + }); + return *this; + } + runner &sleep(const int millis) { + this->queue.push_back([=](struct webview *w) { + (void)w; + std::this_thread::sleep_for(std::chrono::milliseconds(millis)); + }); + return *this; + } + void wait() { + this->then([](struct webview *w) { webview_terminate(w); }); + auto q = this->queue; + auto w = this->w; + std::thread bg_thread([w, q]() { + for (auto f : q) { + f(w); + } + }); + while (webview_loop(w, 1) == 0) { + } + bg_thread.join(); + } + +private: + struct webview *w; + std::vector> queue; +}; + +static void test_minimal() { + struct webview w = {}; + std::cout << "TEST: minimal" << std::endl; + w.title = "Minimal test"; + w.width = 480; + w.height = 320; + webview_init(&w); + webview_dispatch(&w, + [](struct webview *w, void *arg) { + (void)arg; + webview_terminate(w); + }, + nullptr); + while (webview_loop(&w, 1) == 0) { + } + webview_exit(&w); +} + +static void test_window_size() { + struct webview w = {}; + std::vector results; + std::cout << "TEST: window size" << std::endl; + w.width = 480; + w.height = 320; + w.resizable = 1; + w.userdata = static_cast(&results); + w.external_invoke_cb = [](struct webview *w, const char *arg) { + auto *v = static_cast *>(w->userdata); + v->push_back(std::string(arg)); + }; + runner(&w) + .then([](struct webview *w) { + webview_eval(w, "window.external.invoke(''+window.screen.width+' ' + " + "window.screen.height)"); + webview_eval(w, "window.external.invoke(''+window.innerWidth+' ' + " + "window.innerHeight)"); + }) + .sleep(200) + .then([](struct webview *w) { webview_set_fullscreen(w, 1); }) + .sleep(500) + .then([](struct webview *w) { + webview_eval(w, "window.external.invoke(''+window.innerWidth+' ' + " + "window.innerHeight)"); + }) + .sleep(200) + .then([](struct webview *w) { webview_set_fullscreen(w, 0); }) + .sleep(500) + .then([](struct webview *w) { + webview_eval(w, "window.external.invoke(''+window.innerWidth+' ' + " + "window.innerHeight)"); + }) + .wait(); + assert(results.size() == 4); + assert(results[1] == "480 320"); + assert(results[0] == results[2]); + assert(results[1] == results[3]); +} + +static void test_inject_js() { + struct webview w = {}; + std::vector results; + std::cout << "TEST: inject JS" << std::endl; + w.width = 480; + w.height = 320; + w.userdata = static_cast(&results); + w.external_invoke_cb = [](struct webview *w, const char *arg) { + auto *v = static_cast *>(w->userdata); + v->push_back(std::string(arg)); + }; + runner(&w) + .then([](struct webview *w) { + webview_eval(w, + R"(document.body.innerHTML = '
Foo
';)"); + webview_eval( + w, + "window.external.invoke(document.getElementById('foo').innerText)"); + }) + .wait(); + assert(results.size() == 1); + assert(results[0] == "Foo"); +} + +static void test_inject_css() { + struct webview w = {}; + std::vector results; + std::cout << "TEST: inject CSS" << std::endl; + w.width = 480; + w.height = 320; + w.userdata = static_cast(&results); + w.external_invoke_cb = [](struct webview *w, const char *arg) { + auto *v = static_cast *>(w->userdata); + v->push_back(std::string(arg)); + }; + runner(&w) + .then([](struct webview *w) { + webview_inject_css(w, "#app { margin-left: 4px; }"); + webview_eval(w, "window.external.invoke(getComputedStyle(document." + "getElementById('app')).marginLeft)"); + }) + .wait(); + assert(results.size() == 1); + assert(results[0] == "4px"); +} + +int main() { + test_minimal(); + test_window_size(); + test_inject_js(); + test_inject_css(); + return 0; +}