diff --git a/api.go b/api.go index b87722f..f776282 100644 --- a/api.go +++ b/api.go @@ -51,10 +51,9 @@ class WebkitAPI { }); } } -let api = new WebkitAPI(); +window.webkitAPI = new WebkitAPI(); {{range $api, $calls := .Calls}}window.{{$api}} = {}; -{{range $calls}}window.{{$api}}.{{.}} = (obj) => api.request("{{$api}}", "{{.}}", obj);{{end}}{{end}} -window.webkit.api = api; +{{range $calls}}window.{{$api}}.{{.}} = (obj) => window.webkitAPI.request("{{$api}}", "{{.}}", obj);{{end}}{{end}} })(document.cloneNode(),globalThis.window);`)) func apiHandler(bindings map[string]apiBinding, eval func(string), log func(interface{}, ...interface{})) func(string) { @@ -88,17 +87,17 @@ func apiHandler(bindings map[string]apiBinding, eval func(string), log func(inte log("api request", "id", id, "api", api, "fn", fn) binding, ok := bindings[api] if !ok { - eval("webkit.api.reject(" + string(id) + ",'api not found')") + eval("webkitAPI.reject(" + string(id) + ",'api not found')") return } reply, err := binding.call(fn, req[cur:]) if err != nil { log("api reject", "id", id, "error", err) - eval("webkit.api.reject(" + string(id) + ",'" + err.Error() + "')") + eval("webkitAPI.reject(" + string(id) + ",'" + err.Error() + "')") return } log("api resolve", "id", id, "reply", reply) - eval("webkit.api.resolve(" + id + ",'" + reply + "')") + eval("webkitAPI.resolve(" + id + ",'" + reply + "')") } } diff --git a/app.go b/app.go index e5aae15..9845965 100644 --- a/app.go +++ b/app.go @@ -28,6 +28,9 @@ type App struct { windowsLock sync.RWMutex runOnce runOnce + + //web context + context ptr } func New(options AppOptions) *App { @@ -41,11 +44,6 @@ func New(options AppOptions) *App { } else { options.Name = strings.ToLower(options.Name) } - if options.PanicHandler == nil { - options.PanicHandler = func(v any) { - panic(v) - } - } // Create app app := &App{ @@ -57,20 +55,17 @@ func New(options AppOptions) *App { // Setup debug logger if options.Debug { - logger := &logger{ + app.logger = &logger{ prefix: "webkit2gtk: " + options.Name, - writer: os.Stdout, - } - if options.LogWriter != nil { - logger.writer = options.LogWriter + writer: LogWriter, } - app.logger = logger } + ///////////////////////////////////// globalApplication = app // !important - return app } + func (a *App) CurrentWindow() *Window { if a.pointer == 0 { return nil @@ -103,56 +98,20 @@ func (a *App) Run() error { if err := os.Setenv("JSC_SIGNAL_FOR_GC", "20"); err != nil { return err } - // - //// 2. Connect foreground signal (USR2) - //// TODO: Is this working? - //foreground := make(chan os.Signal, 1) - //signal.Notify(foreground, syscall.SIGUSR2) - //go func() { - // for { - // <-foreground - // for _, window := range a.windows { - // windowPresent(window.pointer) - // - // } - // a.info("application foregrounded", "main_thread", mainThreadId) - // } - //}() - - // 3. Load shared libraries + + // 2. Load shared libraries if err := a.loadSharedLibs(); err != nil { return err } - // 4. Get Main Thread and create GTK Application + // 3. Get Main Thread and create GTK Application mainThreadId = lib.g.ThreadSelf() a.pointer = lib.gtk.ApplicationNew(a.ident, uint(0)) - //dbusCli, err := newDBUSClient(a.ident) - //if err != nil { - // return err - //} - //running, pid := dbusCli.IsAppRunning() - //if running { - // // send signal - // proc, err := os.FindProcess(int(pid)) - // if err != nil { - // return err - // } - // err = proc.Signal(syscall.SIGUSR2) - // if err != nil { - // return err - // } - // a.info("application already running", "pid", pid) - // return fmt.Errorf("application already running") - //} - - //lib.g.ApplicationRegister(a.pointer, 0, 0) - - // 3. Run deferred functions + // 4. Run deferred functions a.runOnce.invoke(true) - // 4. Setup activate signal ipc + // 5. Setup activate signal ipc app := ptr(a.pointer) activate := func() { a.log("application startup complete", "since_startup", time.Since(startupTime)) diff --git a/examples/sudoku/assets/main.js b/examples/sudoku/assets/main.js new file mode 100644 index 0000000..c1c5dc0 --- /dev/null +++ b/examples/sudoku/assets/main.js @@ -0,0 +1,135 @@ +class Sudoku { + constructor(elem) { + this.elem = elem; + this.board = this.createBoard(); + this.controls = this.createControls(); + this.elem.appendChild(this.board) + this.elem.appendChild(this.controls) + } + + createControls() { + let controls = document.createElement("div"); + controls.classList.add("controls"); + let genButton = this.createButton("Generate", this.fillBoard.bind(this)); + let genAmount = this.createInput("number", 20, 0, 81); + controls.append(genButton, genAmount); + return controls; + } + + createButton(text, onClick) { + let button = document.createElement("button"); + button.innerText = text; + button.onclick = onClick; + return button; + } + + createInput(type, value, min, max) { + let input = document.createElement("input"); + input.type = type; + input.value = value; + input.min = min; + input.max = max; + return input; + } + + createBoard() { + let board = document.createElement('div'); + board.className = 'board'; + for (let i = 0; i < 9; i++) { + let row = document.createElement('div'); + row.className = 'row'; + for (let j = 0; j < 9; j++) { + let cell = this.createCell(i, j); + row.appendChild(cell); + } + board.appendChild(row); + } + return board; + } + + createCell(i, j) { + let cell = document.createElement('div'); + cell.className = 'cell'; + cell.id = `cell${i}${j}`; + cell.innerHTML = ' '; + cell.onclick = this.cellOnClick.bind(cell); + cell.oncontextmenu = this.cellOnRightClick.bind(cell); + return cell; + } + + cellOnClick() { + this.innerHTML = this.innerHTML === ' ' ? 1 : (parseInt(this.innerHTML) % 9) + 1; + } + + cellOnRightClick(e) { + e.preventDefault(); + this.innerHTML = this.innerHTML === ' ' ? 9 : (parseInt(this.innerHTML) - 1) || ' '; + } + + getValues(selector) { + let values = []; + for (let i = 0; i < 9; i++) { + values.push(parseInt(this.board.querySelector(selector(i)).innerHTML)); + } + return values; + } + + getRowValues = (row) => this.getValues(i => `#cell${row}${i}`); + getColValues = (col) => this.getValues(i => `#cell${i}${col}`); + + getValidValues(row, col) { + let rowValues = this.getRowValues(row); + let colValues = this.getColValues(col); + let squareValues = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + return squareValues.filter(i => !rowValues.includes(i) && !colValues.includes(i)); + } + + getEmptyCells() { + let emptyCells = []; + for (let i = 0; i < 9; i++) { + for (let j = 0; j < 9; j++) { + if (this.board.querySelector(`#cell${i}${j}`).innerHTML === ' ') { + emptyCells.push([i, j]); + } + } + } + return emptyCells; + } + + fillCell(row, col) { + let cell = this.board.querySelector(`#cell${row}${col}`); + + let possibleValues = this.getValidValues(row, col); + if (possibleValues.length === 0) { + return false; + } + possibleValues.sort(() => Math.random() - 0.5); + cell.classList.add("cell-generated"); + cell.innerHTML = possibleValues.pop(); + return true; + } + + clearBoard() { + for (let i = 0; i < 9; i++) { + for (let j = 0; j < 9; j++) { + let cell = this.board.querySelector(`#cell${i}${j}`); + cell.classList.remove("cell-generated"); + cell.innerHTML = ' '; + } + } + } + + fillBoard() { + this.clearBoard(); + let emptyCells = this.getEmptyCells(); + emptyCells.sort(() => Math.random() - 0.5); + let amount = parseInt(this.controls.querySelector("input").value); + let n = 0; + while (n < amount) { + let [row, col] = emptyCells.pop(); + if (this.fillCell(row, col)) n++; + } + } +} + +let game = new Sudoku(document.body); \ No newline at end of file diff --git a/examples/sudoku/sudoku.go b/examples/sudoku/sudoku.go new file mode 100644 index 0000000..d9b6c55 --- /dev/null +++ b/examples/sudoku/sudoku.go @@ -0,0 +1,32 @@ +package main + +import ( + "embed" + _ "embed" + ui "github.com/malivvan/webkitgtk" + "io/fs" + "net/http" +) + +//go:embed assets +var assets embed.FS + +func main() { + assets, _ := fs.Sub(assets, "assets") + app := ui.New(ui.AppOptions{ + Name: "sudoku", + Debug: true, + Handle: map[string]http.Handler{ + "main": http.FileServer(http.FS(assets)), + }, + }) + app.Open(ui.WindowOptions{ + Title: "Sudoku", + Width: 600, + Height: 460, + URL: "app://main/", + }) + if err := app.Run(); err != nil { + panic(err) + } +} diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..60b029e --- /dev/null +++ b/handler.go @@ -0,0 +1,222 @@ +package webkitgtk + +import ( + "fmt" + "io" + "math" + "net/http" + "net/url" + "os" + "strconv" + "syscall" + "unsafe" +) + +type uriSchemeRequestBody struct { + stream ptr + closed bool +} + +func newUriSchemeRequestBody(stream ptr) *uriSchemeRequestBody { + return &uriSchemeRequestBody{stream: stream} +} + +// Read implements io.Reader +func (r *uriSchemeRequestBody) Read(p []byte) (int, error) { + if r.closed { + return 0, io.ErrClosedPipe + } + + var content unsafe.Pointer + var contentLen int + if p != nil { + content = unsafe.Pointer(&p[0]) + contentLen = len(p) + } + + var n int + var gErr *gError + if !lib.g.InputStreamReadAll(r.stream, ptr(content), contentLen, &n, 0, gErr) { + return 0, gErr.toError("stream read failed") + } + if n == 0 { + return 0, io.EOF + } + return n, nil +} + +// Close implements io.Closer +func (r *uriSchemeRequestBody) Close() error { + if r.closed { + return nil + } + r.closed = true + + var err error + var gErr *gError + if !lib.g.InputStreamClose(r.stream, 0, gErr) { + err = gErr.toError("stream close failed") + } + + lib.g.ObjectUnref(r.stream) + r.stream = 0 + return err +} + +type uriSchemeRequest struct { + pointer ptr + reqBody *uriSchemeRequestBody +} + +type uriSchemeResponseWriter struct { + request *uriSchemeRequest + header http.Header + code int + writer io.WriteCloser + writerErr error + finished bool +} + +func (r *uriSchemeRequest) toResponseWriter() *uriSchemeResponseWriter { + return &uriSchemeResponseWriter{ + request: r, + header: http.Header{}, + code: math.MinInt, + } +} + +func (rw *uriSchemeResponseWriter) Close() error { + if rw.code == math.MinInt { + rw.WriteHeader(http.StatusNotImplemented) + } + if rw.finished { + return nil + } + rw.finished = true + if rw.writer != nil { + return rw.writer.Close() + } + return nil +} + +func (rw *uriSchemeResponseWriter) Header() http.Header { + return rw.header +} + +func (rw *uriSchemeResponseWriter) WriteHeader(code int) { + if rw.finished || rw.code != math.MinInt { + return + } + rw.code = code + ////////////// + + contentLength := int64(-1) + if sLen := rw.Header().Get("Content-Length"); sLen != "" { + if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 { + contentLength = pLen + } + } + + rFD, w, err := rw.newPipe() + if err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to open pipe: %s", err)) + return + } + rw.writer = w + + stream := lib.g.UnixInputStreamNew(rFD, true) + if err := rw.finishWithResponse(code, rw.Header(), stream, contentLength); err != nil { + rw.finishWithError(http.StatusInternalServerError, fmt.Errorf("unable to finish request: %s", err)) + return + } +} +func (rw *uriSchemeResponseWriter) Write(buf []byte) (n int, err error) { + if rw.finished { + return 0, fmt.Errorf("write after finish") + } + + rw.WriteHeader(http.StatusOK) + if rw.writerErr != nil { + return 0, rw.writerErr + } + return rw.writer.Write(buf) +} + +func (rw *uriSchemeResponseWriter) newPipe() (r int, f *os.File, err error) { + var p [2]int + e := syscall.Pipe2(p[0:], 0) + if e != nil { + return 0, nil, fmt.Errorf("pipe2: %s", e) + } + return p[0], os.NewFile(uintptr(p[1]), "|1"), nil +} +func (rw *uriSchemeResponseWriter) finishWithResponse(code int, header http.Header, stream ptr, streamLength int64) error { + + resp := lib.webkit.UriSchemeResponseNew(stream, streamLength) + defer lib.g.ObjectUnref(resp) + lib.webkit.UriSchemeResponseSetStatus(resp, code, "") + lib.webkit.UriSchemeResponseSetContentType(resp, header.Get("Content-Type")) + headers := lib.soup.MessageHeadersNew(1) + for name, values := range header { + for _, value := range values { + lib.soup.MessageHeadersAppend(headers, name, value) + } + } + lib.webkit.UriSchemeResponseSetHttpHeaders(resp, headers) + lib.webkit.UriSchemeRequestFinishWithResponse(rw.request.pointer, resp) + return nil +} + +type uriSchemeResponseNopCloser struct { + io.Writer +} + +func (uriSchemeResponseNopCloser) Close() error { return nil } + +func (rw *uriSchemeResponseWriter) finishWithError(code int, err error) { + + if rw.writer != nil { + rw.writer.Close() + rw.writer = &uriSchemeResponseNopCloser{io.Discard} + } + rw.writerErr = err + + msg := err.Error() + gErr := lib.g.ErrorNewLiteral(1, msg, code, msg) + defer lib.g.ErrorFree(gErr) + lib.webkit.UriSchemeRequestFinishError(rw.request.pointer, gErr) +} + +func (r *uriSchemeRequest) toHttpRequest() (*http.Request, error) { + var req http.Request + + req.RequestURI = lib.webkit.UriSchemeRequestGetUri(r.pointer) + reqUrl, err := url.ParseRequestURI(req.RequestURI) + if err != nil { + return nil, err + } + req.URL = reqUrl + req.Method = lib.webkit.UriSchemeRequestGetHttpMethod(r.pointer) + req.Body = http.NoBody + reqBody := lib.webkit.UriSchemeRequestGetHttpBody(r.pointer) + if reqBody != 0 { + r.reqBody = newUriSchemeRequestBody(reqBody) + req.Body = r.reqBody + } + return &req, nil +} + +func newUriSchemeRequest(pointer ptr) *uriSchemeRequest { + req := &uriSchemeRequest{pointer: pointer} + lib.g.ObjectRef(req.pointer) + return req +} + +func (r *uriSchemeRequest) Close() error { + if r.reqBody != nil { + return r.reqBody.Close() + } + lib.g.ObjectUnref(r.pointer) + r.pointer = 0 + return nil +} diff --git a/jsc.go b/jsc.go new file mode 100644 index 0000000..b5c9c70 --- /dev/null +++ b/jsc.go @@ -0,0 +1,80 @@ +package webkitgtk + +import ( + "errors" + "github.com/ebitengine/purego" +) + +// TODO: check for memory leaks in jsc.go + +func (w *Window) JSPromise(js string, fn func(interface{})) func() { + js = "return new Promise((resolve, reject) => { \n" + js + "\n });" + return w.JSCall(js, fn) +} + +func (w *Window) JSCall(js string, fn func(interface{})) func() { + cancelable := lib.g.CancellableNew() + lib.webkit.WebViewCallAsyncJavascriptFunction( + w.webview, js, len(js), 0, 0, 0, cancelable, + ptr(purego.NewCallback(func(webview webviewPtr, result ptr) { + var gErr *gError + jsc := lib.webkit.WebViewCallAsyncJavascriptFunctionFinish(webview, result, gErr) + fn(parseJSC(jsc, cancelable, gErr)) + //lib.webkit.JavascriptResultUnref(result) + })), 0) + return func() { + lib.g.CancellableCancel(cancelable) + } +} + +func (w *Window) JSEval(js string, fn func(interface{})) func() { + cancelable := lib.g.CancellableNew() + lib.webkit.WebViewEvaluateJavascript( + w.webview, js, len(js), 0, 0, cancelable, + ptr(purego.NewCallback(func(webview webviewPtr, result ptr) { + var gErr *gError + jsc := lib.webkit.WebViewEvaluateJavascriptFinish(webview, result, gErr) + fn(parseJSC(jsc, cancelable, gErr)) + //lib.webkit.JavascriptResultUnref(result) + })), 0) + return func() { + lib.g.CancellableCancel(cancelable) + } +} + +type JSObject struct { + pointer ptr +} + +func (jso *JSObject) Get(name string) interface{} { + value := lib.jsc.ValueObjectGetProperty(jso.pointer, name) + return parseJSC(value, 0, nil) +} + +func parseJSC(value ptr, cancelable ptr, gErr *gError) interface{} { + if value == 0 { + if lib.g.CancellableIsCancelled(cancelable) { + return errors.New("call canceled") + } + err := errors.New("call error") + if gErr != nil { + err = gErr.toError(err.Error()) + lib.g.ErrorFree(gErr) + } + return err + } + exception := lib.jsc.ContextGetException(lib.jsc.ValueGetContext(value)) + if exception != 0 { + return errors.New(lib.jsc.ExceptionGetMessage(exception)) + } + if lib.jsc.ValueIsNumber(value) { + return lib.jsc.ValueToDouble(value) + } else if lib.jsc.ValueIsBoolean(value) { + return lib.jsc.ValueToBoolean(value) + } else if lib.jsc.ValueIsString(value) { + return lib.jsc.ValueToString(value) + } else if lib.jsc.ValueIsObject(value) { + return &JSObject{value} + } + return errors.New("jscToValue: unknown type") +} diff --git a/purego.go b/purego.go index 55e2003..977939a 100644 --- a/purego.go +++ b/purego.go @@ -1,6 +1,7 @@ package webkitgtk import ( + "errors" "fmt" "github.com/ebitengine/purego" "os" @@ -44,6 +45,21 @@ type ( } ) +type gError struct { + domain uint32 + code int + message string +} + +func (gErr *gError) toError(msg string) error { + if gErr == nil && gErr.message == "" { + return nil + } + msg += ": " + gErr.message + lib.g.ErrorFree(gErr) + return errors.New(msg) +} + const ( gSourceRemove int = 0 @@ -70,24 +86,40 @@ var lib struct { Webkit uintptr g struct { - ApplicationHold func(ptr) - ApplicationQuit func(ptr) - ApplicationRegister func(ptr, ptr, ptr) - ApplicationActivate func(ptr) - GetApplicationName func() string - ApplicationRelease func(ptr) - ApplicationRun func(ptr, int, []string) int - BytesNewStatic func(uintptr, int) uintptr - BytesUnref func(uintptr) - Free func(ptr) - IdleAdd func(uintptr) - ObjectRefSink func(ptr) - ObjectUnref func(ptr) - SignalConnectData func(ptr, string, uintptr, ptr, bool, int) int - SignalConnectObject func(ptr, string, ptr, ptr, int) uint - SignalHandlerBlock func(ptr, uint) - SignalHandlerUnblock func(ptr, uint) - ThreadSelf func() uint64 + ApplicationHold func(ptr) + ApplicationQuit func(ptr) + ApplicationRegister func(ptr, ptr, ptr) + ApplicationActivate func(ptr) + GetApplicationName func() string + ApplicationRelease func(ptr) + ApplicationRun func(ptr, int, []string) int + BytesNewStatic func(uintptr, int) uintptr + BytesUnref func(uintptr) + Free func(ptr) + IdleAdd func(uintptr) + ObjectRef func(ptr) + ObjectRefSink func(ptr) + ObjectUnref func(ptr) + ObjectSet func(ptr, string, ptr) + ObjectGet func(ptr, string, ptr) + SignalConnectData func(ptr, string, uintptr, ptr, bool, int) int + SignalConnectObject func(ptr, string, ptr, ptr, int) uint + SignalHandlerBlock func(ptr, uint) + SignalHandlerUnblock func(ptr, uint) + ThreadSelf func() uint64 + InputStreamClose func(ptr, ptr, *gError) bool + InputStreamReadAll func(ptr, ptr, int, *int, ptr, *gError) bool + ErrorFree func(*gError) + UnixInputStreamNew func(int, bool) ptr + ErrorNewLiteral func(uint32, string, int, string) *gError + QuarkFromString func(string) ptr + BuildFilename func(...string) ptr + GetHomeDir func() string + RefStringNew func(string) ptr + RefStringRelease func(ptr) + CancellableNew func() ptr + CancellableCancel func(ptr) + CancellableIsCancelled func(ptr) bool } gdk struct { DisplayGetMonitor func(ptr, int) ptr @@ -180,6 +212,10 @@ var lib struct { WindowUnfullscreen func(windowPtr) WindowUnmaximize func(windowPtr) } + soup struct { + MessageHeadersNew func(int) ptr + MessageHeadersAppend func(ptr, string, string) + } webkitSettings struct { GetEnableJavascript func(webkitSettingsPtr) bool SetEnableJavascript func(webkitSettingsPtr, bool) @@ -306,12 +342,35 @@ var lib struct { SetDisableWebSecurity func(webkitSettingsPtr, bool) } jsc struct { + ValueNewString func(string) ptr ValueToString func(ptr) string ValueToStringAsBytes func(ptr) string ValueIsString func(ptr) bool + + ValueToDouble func(ptr) float64 + ValueIsNumber func(ptr) bool + + ValueToBoolean func(ptr) bool + ValueIsBoolean func(ptr) bool + + ValueIsObject func(ptr) bool + ValueObjectInvokeMethodv func(ptr, string, int, ...ptr) ptr + ValueObjectGetProperty func(ptr, string) ptr + ValueObjectSetProperty func(ptr, string, ptr) + + ValueIsArray func(ptr) bool + + ContextGetCurrent func() ptr + ValueIsUndefined func(ptr) bool + ValueIsNull func(ptr) bool + ContextGetValue func(ptr, string) ptr + ContextGetException func(ptr) ptr + ContextGetGlobalObject func(ptr) ptr + ExceptionGetMessage func(ptr) string + ValueGetContext func(ptr) ptr } webkit struct { - //WebViewNew func() ptr + WebViewNewWithContext func(ptr) webviewPtr WebViewNewWithUserContentManager func(userContentManagerPtr) webviewPtr WebContextRegisterUriScheme func(ptr, string, ptr, int, int) //webkitSettingsGetEnableDeveloperExtras func(pointer) bool @@ -321,23 +380,26 @@ var lib struct { UserContentManagerNew func() userContentManagerPtr UserContentManagerRegisterScriptMessageHandler func(userContentManagerPtr, string) - // WebsiteDataManagerNew func(...string) ptr - //WebContextNewWithWebsiteDataManager func(ptr) ptr - //web_context_get_cookie_manager - // void webkit_cookie_manager_set_storage + WebContextGetWebsiteDataManager func(ptr) ptr CookieManagerSetPersistentStorage func(ptr, string, int) WebContextGetCookieManager func(ptr) ptr WebViewGetUserContentManager func(webviewPtr) userContentManagerPtr - WebsiteDataManagerNew func(...string) ptr - WebContextNewWithWebsiteDataManager func(ptr) ptr - WebContextGetSandboxEnabled func(ptr) bool - WebContextSetSandboxEnabled func(ptr, bool) - WebContextAddPathToSandbox func(ptr, string) - WebContextGetSpellCheckingEnabled func(ptr) bool - WebContextSetSpellCheckingEnabled func(ptr, bool) + WebsiteDataManagerNew func(string, string, string, string, string, bool, ptr) ptr + WebContextSetFaviconDatabaseDirectory func(ptr, string) + WebContextSetWebExtensionsDirectory func(ptr, string) + WebsiteDataManagerGetBaseCacheDirectory func(ptr) string + WebsiteDataManagerGetLocalStorageDirectory func(ptr) string + WebsiteDataManagerSetPersistentCredentialStorageEnabled func(ptr, bool) + WebContextNewWithWebsiteDataManager func(ptr) ptr + WebContextGetSandboxEnabled func(ptr) bool + WebContextSetSandboxEnabled func(ptr, bool) + WebContextAddPathToSandbox func(ptr, string) + WebContextGetSpellCheckingEnabled func(ptr) bool + WebContextSetSpellCheckingEnabled func(ptr, bool) JavascriptResultGetJsValue func(ptr) ptr + JavascriptResultUnref func(ptr) WebContextGetDefault func() ptr WebContextGetSecurityManager func(ptr) ptr @@ -347,15 +409,32 @@ var lib struct { SecurityManagerRegisterUriSchemeAsCorsEnabled func(ptr, string) SecurityManagerRegisterUriSchemeAsLocal func(ptr, string) - WebViewEvaluateJavascript func(webviewPtr, string, int, ptr, string, ptr, ptr, ptr) - WebViewGetSettings func(webviewPtr) webkitSettingsPtr - WebViewGetZoomLevel func(webviewPtr) float64 - WebViewLoadAlternateHtml func(webviewPtr, string, string, *string) - WebViewLoadUri func(webviewPtr, string) - WebViewSetBackgroundColor func(webviewPtr, ptr) - WebViewSetSettings func(webviewPtr, webkitSettingsPtr) - WebViewSetZoomLevel func(webviewPtr, float64) - WebViewLoadBytes func(webviewPtr, []byte, string, string, string) + WebViewEvaluateJavascript func(webviewPtr, string, int, ptr, ptr, ptr, ptr, ptr) + WebViewEvaluateJavascriptFinish func(webviewPtr, ptr, *gError) ptr + WebViewCallAsyncJavascriptFunction func(webviewPtr, string, int, ptr, ptr, ptr, ptr, ptr, ptr) + WebViewCallAsyncJavascriptFunctionFinish func(webviewPtr, ptr, *gError) ptr + WebViewGetSettings func(webviewPtr) webkitSettingsPtr + WebViewGetZoomLevel func(webviewPtr) float64 + //WebViewLoadAlternateHtml func(webviewPtr, string, string, *string) + WebViewLoadUri func(webviewPtr, string) + WebViewLoadHtml func(webviewPtr, string, string) + WebViewSetBackgroundColor func(webviewPtr, ptr) + WebViewSetSettings func(webviewPtr, webkitSettingsPtr) + WebViewSetZoomLevel func(webviewPtr, float64) + WebViewLoadBytes func(webviewPtr, []byte, string, string, string) + WebViewSetCorsAllowlist func(webviewPtr, ...ptr) + UriSchemeRequestGetUri func(ptr) string + UriSchemeRequestGetHttpMethod func(ptr) string + UriSchemeRequestGetHttpHeaders func(ptr) ptr + UriSchemeRequestGetHttpBody func(ptr) ptr + UriSchemeRequestFinish func(ptr, ptr, int, string) + UriSchemeRequestFinishError func(ptr, *gError) + UriSchemeRequestFinishWithResponse func(ptr, ptr) + + UriSchemeResponseNew func(ptr, int64) ptr + UriSchemeResponseSetStatus func(ptr, int, string) + UriSchemeResponseSetContentType func(ptr, string) + UriSchemeResponseSetHttpHeaders func(ptr, ptr) } } @@ -497,6 +576,10 @@ func (a *App) loadSharedLibs() error { if err != nil { return fmt.Errorf("unable to register gtk functions: %w", err) } + err = registerFunctions(lib.Webkit, "soup", &lib.soup) + if err != nil { + return fmt.Errorf("unable to register soup functions: %w", err) + } err = registerFunctions(lib.Webkit, "jsc", &lib.jsc) if err != nil { return fmt.Errorf("unable to register jsc functions: %w", err) diff --git a/settings.go b/settings.go index 07586f9..856abf0 100644 --- a/settings.go +++ b/settings.go @@ -1,15 +1,37 @@ package webkitgtk import ( - "io" + "net/http" ) type AppOptions struct { - Name string - Debug bool - LogWriter io.Writer - HideOnLastWindowClosed bool - PanicHandler func(any) + + // The name of the app. + Name string + + // Debug mode. + Debug bool + + // Hold the app open after the last window is closed. + Hold bool + + // Handle internal app:// requests. + Handle map[string]http.Handler + + // Ephemeral mode disables all persistent storage. + Ephemeral bool + + // DataDir is the directory where persistent data is stored. + DataDir string + + // CacheDir is the directory where persistent cache is stored. + CacheDir string + + // CacheModel is the cache model used by the webview. + CacheModel WebkitCacheModel + + // CookiePolicy is the cookie store used by the webview. + CookiePolicy WebkitCookiePolicy } type WebkitSettings struct { @@ -189,8 +211,8 @@ var defaultWebkitSettings = WebkitSettings{ EnableMediaSource: true, EnableEncryptedMedia: false, EnableMediaCapabilities: false, - AllowFileAccessFromFileUrls: true, - AllowUniversalAccessFromFileUrls: true, + AllowFileAccessFromFileUrls: false, + AllowUniversalAccessFromFileUrls: false, AllowTopNavigationToDataUrls: false, HardwareAccelerationPolicy: 1, EnableBackForwardNavigationGestures: false, diff --git a/thread.go b/thread.go index 6f86da1..1a835f9 100644 --- a/thread.go +++ b/thread.go @@ -37,63 +37,8 @@ func invokeSync(fn func()) { wg.Wait() } -func invokeSyncWithResult[T any](fn func() T) (res T) { - var wg sync.WaitGroup - wg.Add(1) - globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() - res = fn() - wg.Done() - }) - wg.Wait() - return res -} - -func invokeSyncWithError(fn func() error) (err error) { - var wg sync.WaitGroup - wg.Add(1) - globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() - err = fn() - wg.Done() - }) - wg.Wait() - return -} - -func invokeSyncWithResultAndError[T any](fn func() (T, error)) (res T, err error) { - var wg sync.WaitGroup - wg.Add(1) - globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() - res, err = fn() - wg.Done() - }) - wg.Wait() - return res, err -} - -func invokeSyncWithResultAndOther[T any, U any](fn func() (T, U)) (res T, other U) { - var wg sync.WaitGroup - wg.Add(1) - globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() - res, other = fn() - wg.Done() - }) - wg.Wait() - return res, other -} - -func invokeAsync(fn func()) { - globalApplication.dispatchOnMainThread(func() { - defer processPanicHandlerRecover() - fn() - }) -} - func processPanicHandlerRecover() { - h := globalApplication.options.PanicHandler + h := PanicHandler if h == nil { return } @@ -113,11 +58,11 @@ func (a *App) dispatchOnMainThread(fn func()) { id := generateFunctionStoreID() mainThreadFunctionStore[id] = fn mainThreadFunctionStoreLock.Unlock() + // Call platform specific dispatch function dispatchOnMainThread(id) } -// mainthread stuff func dispatchOnMainThread(id uint) { lib.g.IdleAdd(purego.NewCallback(func(ptr) int { executeOnMainThread(id) diff --git a/util.go b/util.go deleted file mode 100644 index 6c55f64..0000000 --- a/util.go +++ /dev/null @@ -1,62 +0,0 @@ -package webkitgtk - -import ( - "fmt" - "io" - "strings" - "sync" -) - -type logger struct { - prefix string - writer io.Writer -} - -func (l *logger) log(msg interface{}, keyvals ...interface{}) { - var s strings.Builder - s.WriteString(l.prefix) - s.WriteString(": ") - s.WriteString(fmt.Sprintf("%v", msg)) - for i := 0; i < len(keyvals); i += 2 { - s.WriteString(" ") - s.WriteString(fmt.Sprintf("%v", keyvals[i])) - s.WriteString("=") - s.WriteString(fmt.Sprintf("%v", keyvals[i+1])) - } - s.WriteString("\n") - l.writer.Write([]byte(s.String())) -} - -type runOnce struct { - mutex sync.Mutex - running bool - runnables []runnable -} - -type runnable interface { - run() -} - -func (r *runOnce) add(runnable runnable) { - r.mutex.Lock() - defer r.mutex.Unlock() - if r.running { - runnable.run() - } else { - r.runnables = append(r.runnables, runnable) - } -} - -func (r *runOnce) invoke(inRoutine bool) { - r.mutex.Lock() - defer r.mutex.Unlock() - r.running = true - for _, runnable := range r.runnables { - if inRoutine { - go runnable.run() - } else { - runnable.run() - } - } - r.runnables = nil -} diff --git a/window.go b/window.go index 10688bf..abcb3f0 100644 --- a/window.go +++ b/window.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" "github.com/ebitengine/purego" + "net/http" "net/url" + "path/filepath" "reflect" "strconv" "strings" @@ -14,21 +16,9 @@ import ( "unsafe" ) -var URLScheme = "app" -var UserAgent = "webkit2gtk" - var windowID uint var windowIDLock sync.RWMutex -//var showDevTools = func(pointer unsafe.Pointer) {} - -type dragInfo struct { - XRoot int - YRoot int - DragTime int - MouseButton uint -} - func getWindowID() uint { windowIDLock.Lock() defer windowIDLock.Unlock() @@ -103,12 +93,6 @@ type WindowOptions struct { // Color specified the window color as a hex string (#RGB, #RGBA, #RRGGBB, #RRGGBBAA) Color string - // BackgroundType is the type of background to use for the window. - // Default: BackgroundTypeSolid - //BackgroundType BackgroundType - - // BackgroundColour is the colour to use for the window background. - //BackgroundColour RGBA // X is the starting X position of the window. X int @@ -158,14 +142,6 @@ type Window struct { constants map[string]string } -type assetHandler struct { - registered bool -} - -func (ah *assetHandler) handleURISchemeRequest(request uintptr) { - println("handleURISchemeRequest", request) -} - // Open opens a new window with the given options. func (a *App) Open(options WindowOptions) *Window { if options.Width == 0 { @@ -219,7 +195,6 @@ func (a *App) Open(options WindowOptions) *Window { } a.runOnce.add(newWindow) - // a.runOrDeferToAppRun(newWindow) return newWindow } @@ -231,9 +206,6 @@ func (w *Window) run() { invokeSync(w.create) } -// ////////////////////////////////////////////// -var registered = false - func (w *Window) create() { id := w.ID() @@ -242,63 +214,212 @@ func (w *Window) create() { w.app.windowsLock.Unlock() openTime := time.Now() - w.log("creating pointer", "id", w.id, "name", w.options.Name) + w.log("creating window", "id", w.id, "name", w.options.Name) w.pointer = lib.gtk.ApplicationWindowNew(w.app.pointer) lib.g.ObjectRefSink(ptr(w.pointer)) + ///////////////////////////////////////////////////////////////////// + + // 1. Create the web context once. + if w.app.context == 0 { + + // 1.1. Prepare the data manager for the web context. + cacheDir := w.app.options.CacheDir + if cacheDir == "" { + cacheDir = filepath.Join(lib.g.GetHomeDir(), ".cache", "webkitgtk", w.app.options.Name) + } + dataDir := w.app.options.DataDir + if dataDir == "" { + dataDir = filepath.Join(lib.g.GetHomeDir(), ".local", "share", "webkitgtk", w.app.options.Name) + } + if w.app.options.Ephemeral { + cacheDir = "" + dataDir = "" + } + dataManager := lib.webkit.WebsiteDataManagerNew( + "base-cache-directory", cacheDir, + "base-data-directory", dataDir, + "is-ephemeral", w.app.options.Ephemeral, 0) + w.app.context = lib.webkit.WebContextNewWithWebsiteDataManager(dataManager) + + // 1.2. Configure additional data manager settings if not ephemeral. + if !w.app.options.Ephemeral { + lib.webkit.WebsiteDataManagerSetPersistentCredentialStorageEnabled(dataManager, true) + + cookieManager := lib.webkit.WebContextGetCookieManager(w.app.context) + lib.webkit.CookieManagerSetPersistentStorage(cookieManager, filepath.Join(dataDir, "cookies.db"), 1) + + lib.webkit.WebContextSetFaviconDatabaseDirectory(w.app.context, filepath.Join(dataDir, "favicons")) + lib.webkit.WebContextSetWebExtensionsDirectory(w.app.context, filepath.Join(dataDir, "extensions")) + } + + // 1.3. Configure app URI scheme and register it with the web context. + securityManager := lib.webkit.WebContextGetSecurityManager(w.app.context) + lib.webkit.SecurityManagerRegisterUriSchemeAsCorsEnabled(securityManager, uriScheme) + lib.webkit.SecurityManagerRegisterUriSchemeAsSecure(securityManager, uriScheme) + lib.webkit.WebContextRegisterUriScheme( + w.app.context, + uriScheme, + ptr(purego.NewCallback(func(request ptr) { + r := newUriSchemeRequest(request) + defer r.Close() + + req, err := r.toHttpRequest() + if err != nil { + w.log("error parsing request", "error", err) + return + } - manager := lib.webkit.UserContentManagerNew() + rw := r.toResponseWriter() + defer rw.Close() + if handler, exists := w.app.options.Handle[req.URL.Host]; exists { + handler.ServeHTTP(rw, req) + return + } + + w.log("no handler found for request", "host", req.URL.Host) + http.Error(rw, "no handler found for request", http.StatusNotFound) + })), + 0, + 0, + ) + } + + // 2. Create the webview add app URI scheme to the CORS allow list. + w.webview = lib.webkit.WebViewNewWithContext(w.app.context) + uriSchemeEntry := lib.g.RefStringNew(uriScheme + "://*/*") + defer lib.g.RefStringRelease(uriSchemeEntry) + lib.webkit.WebViewSetCorsAllowlist(w.webview, uriSchemeEntry, 0) + + // 3. Register the API handler if bindings are defined. + userContentManager := lib.webkit.WebViewGetUserContentManager(w.webview) if w.bindings != nil { - manager.registerScriptMessageHandler("api", apiHandler(w.bindings, w.ExecJS, w.log)) - } - - w.webview = lib.webkit.WebViewNewWithUserContentManager(manager) - - if !registered { - webContext := lib.webkit.WebContextGetDefault() - //dataManager := lib.webkit.WebsiteDataManagerNew() - cookieManager := lib.webkit.WebContextGetCookieManager(webContext) - lib.webkit.CookieManagerSetPersistentStorage(cookieManager, "/home/malivvan/webkitdata/xxx", 0) - - //dataManager := lib.webkit.WebsiteDataManagerNew("base-data-directory","~/xxxxxx/") - // webContext := lib.webkit.WebContextNewWithWebsiteDataManager(dataManager) - // // println("sandboxing webview", lib.webkit.WebContextGetSandboxEnabled(webContext)) - // securityManager := lib.webkit.WebContextGetSecurityManager(webContext) - // lib.webkit.SecurityManagerRegisterUriSchemeAsLocal(securityManager, URLScheme) - // lib.webkit.SecurityManagerRegisterUriSchemeAsSecure(securityManager, URLScheme) - // //webkitSecurityManagerRegisterUriSchemeAsCorsEnabled(securityManager, URLScheme) - // // webkitSecurityManagerRegisterUriSchemeAsNoAccess(securityManager, URLScheme) - // - // lib.webkit.WebContextRegisterUriScheme( - // webContext, - // URLScheme, - // ptr(purego.NewCallback(func(request uintptr) { - // // globalApplication.server.handleURISchemeRequest(request) - // - // })), - // 0, - // 0, - // ) - registered = true + userContentManager.registerScriptMessageHandler("api", apiHandler(w.bindings, w.ExecJS, w.log)) } + // 4. Apply the webkit settings to the webview. settings := lib.webkit.WebViewGetSettings(w.webview) defaultWebkitSettings.apply(settings) - lib.webkitSettings.SetUserAgentWithApplicationDetails( - settings, - UserAgent, - "") - lib.webkitSettings.SetHardwareAccelerationPolicy(settings, 1) + + //////////////////////////////////////////////////////////////////////////////// + // var webkitDefault = WebkitSettings{ + //**** EnableJavascript: true, + //**** EnableWebAudio: true, + //**** EnableWebgl: true, + //**** EnableOfflineWebApplicationCache: true, + //**** EnableHtml5LocalStorage: true, + //**** EnableHtml5Database: true, + //**** EnableMediaStream: true, + //**** EnableMediaSource: true, + //**** EnableJavascriptMarkup: true, + //**** EnableMedia: true, + //**** EnableWebRTC: false, + + //depr LoadIconsIgnoringImageLoadSetting: false, + //depr EnableXssAuditor: false, + //depr EnableFrameFlattening: false, + //depr EnablePlugins: false, + //depr EnableJava: false, + //depr EnablePrivateBrowsing: false, + //depr EnableAccelerated2DCanvas: false, + + //font DefaultFontFamily: "sans-serif", + //font MonospaceFontFamily: "monospace", + //font SerifFontFamily: "serif", + //font SansSerifFontFamily: "sans-serif", + //font CursiveFontFamily: "serif", + //font FantasyFontFamily: "serif", + //font PictographFontFamily: "serif", + //font DefaultFontSize: 16, + //font DefaultMonospaceFontSize: 13, + //font MinimumFontSize: 0, + + //conf HardwareAccelerationPolicy: 1, + //conf EnableDeveloperExtras: false, + //conf EnableTabsToLinks: true, + //conf JavascriptCanOpenWindowsAutomatically: false, + //conf EnableHyperlinkAuditing: true, + //conf EnableDnsPrefetching: false, + //conf ZoomTextOnly: false, + //conf JavascriptCanAccessClipboard: false, + //conf MediaPlaybackRequiresUserGesture: false, + //conf EnablePageCache: true, + //conf UserAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15", + + //**** DefaultCharset: "iso-8859-1", + //**** AutoLoadImages: true, + //**** EnableResizableTextAreas: true, + //**** EnableFullscreen: true, + //**** PrintBackgrounds: true, + //**** EnableSmoothScrolling: true, + //**** MediaPlaybackAllowsInline: true, + //**** EnableSiteSpecificQuirks: true, + + //---- DrawCompositingIndicators: false, + //---- EnableWriteConsoleMessagesToStdout: false, + //---- EnableEncryptedMedia: false, + //---- DisableWebSecurity: false, + //---- EnableCaretBrowsing: false, + //---- AllowModalDialogs: false, + + // EnableMockCaptureDevices: false, + // EnableSpatialNavigation: false, + // EnableMediaCapabilities: false, + // AllowFileAccessFromFileUrls: false, + // AllowUniversalAccessFromFileUrls: false, + // AllowTopNavigationToDataUrls: false, + + // EnableBackForwardNavigationGestures: false, + + // MediaContentTypesRequiringHardwareSupport: "", + + // } + + // ALWAYS ON + lib.webkitSettings.SetDefaultCharset(settings, "utf-8") + lib.webkitSettings.SetAutoLoadImages(settings, true) + lib.webkitSettings.SetEnableResizableTextAreas(settings, true) + lib.webkitSettings.SetEnableFullscreen(settings, true) + lib.webkitSettings.SetPrintBackgrounds(settings, true) + lib.webkitSettings.SetEnableSmoothScrolling(settings, true) + lib.webkitSettings.SetMediaPlaybackAllowsInline(settings, true) + lib.webkitSettings.SetEnableSiteSpecificQuirks(settings, true) + + // ALWAYS OFF + lib.webkitSettings.SetDrawCompositingIndicators(settings, false) + lib.webkitSettings.SetEnableWriteConsoleMessagesToStdout(settings, false) + lib.webkitSettings.SetEnableEncryptedMedia(settings, false) + lib.webkitSettings.SetDisableWebSecurity(settings, false) + lib.webkitSettings.SetEnableCaretBrowsing(settings, false) + lib.webkitSettings.SetAllowModalDialogs(settings, false) + + // CONFIGURABLE + // TODO: make these configurable + + // JAVASCRIPT + _enableJS := true + lib.webkitSettings.SetEnableJavascript(settings, _enableJS) + lib.webkitSettings.SetEnableJavascriptMarkup(settings, _enableJS) + lib.webkitSettings.SetEnableWebaudio(settings, _enableJS) + lib.webkitSettings.SetEnableWebgl(settings, _enableJS) + lib.webkitSettings.SetEnableOfflineWebApplicationCache(settings, _enableJS) + lib.webkitSettings.SetEnableHtml5LocalStorage(settings, _enableJS) + lib.webkitSettings.SetEnableHtml5Database(settings, _enableJS) + lib.webkitSettings.SetEnableMedia(settings, _enableJS) + lib.webkitSettings.SetEnableMediaStream(settings, _enableJS) + lib.webkitSettings.SetEnableMediasource(settings, _enableJS) + lib.webkitSettings.SetEnableWebrtc(settings, _enableJS) + lib.webkit.WebViewSetSettings(w.webview, settings) + ///////////////////////////////////////////////////////////////////////////////// + // 1. Create the window with the webview inside. w.vbox = lib.gtk.BoxNew(gtkOrientationVertical, 0) lib.gtk.ContainerAdd(w.pointer, w.vbox) lib.gtk.WidgetSetName(w.vbox, "webview-box") lib.gtk.BoxPackStart(w.vbox, ptr(w.webview), 1, 1, 0) - windowSetupSignalHandlers(w.id, w.pointer, w.webview) - w.SetTitle(w.options.Title) // only set min/max GetSize if actually set if w.options.MinWidth != 0 && @@ -312,6 +433,7 @@ func (w *Window) create() { w.options.MaxHeight, ) } + w.SetTitle(w.options.Title) w.SetSize(w.options.Width, w.options.Height) w.SetZoom(w.options.Zoom) w.SetOverlay(w.options.Overlay) @@ -329,6 +451,7 @@ func (w *Window) create() { } else { w.Center() } + switch w.options.State { case WindowStateMaximised: w.Maximise() @@ -342,9 +465,9 @@ func (w *Window) create() { w.SetURL(w.options.URL) } else { if w.options.HTML != "" { - w.setHTML(w.options.HTML) + w.SetHTML(w.options.HTML) } else { - w.setHTML("error: no url or html specified in window options") + w.SetHTML("error: no url or html specified in window options") } } @@ -360,12 +483,9 @@ func (w *Window) create() { w.ToggleDevTools() } - //w.ExecJS(ipcJS) - w.log("pointer created", "id", w.id, "name", w.options.Name, "since_open", time.Since(openTime)) + w.log("window created", "id", w.id, "name", w.options.Name, "since_open", time.Since(openTime)) } -// Window Callable Methods - func (w *Window) Focus() { windowPresent(w.pointer) } @@ -525,10 +645,9 @@ func windowExecJS(webview webviewPtr, js string) { js, len(js), 0, - "", 0, 0, - 0) + 0, 0) } func windowDestroy(window windowPtr) { @@ -683,9 +802,9 @@ func windowSetFrameless(window windowPtr, frameless bool) { } // TODO: confirm this is working properly -func windowSetHTML(webview webviewPtr, html string) { - lib.webkit.WebViewLoadAlternateHtml(webview, html, URLScheme+"://", nil) -} +//func windowSetHTML(webview webviewPtr, html string) { +// lib.webkit.WebViewLoadAlternateHtml(webview, html, uriScheme+"://", nil) +//} func windowSetKeepAbove(window windowPtr, alwaysOnTop bool) { lib.gtk.WindowSetKeepAbove(window, alwaysOnTop) @@ -737,8 +856,8 @@ func windowSetupSignalHandlers(windowId uint, window windowPtr, webview webviewP windowCount := len(globalApplication.windows) globalApplication.windowsLock.Unlock() - if windowCount == 0 && !globalApplication.options.HideOnLastWindowClosed { - globalApplication.log("last pointer closed, quitting") + if windowCount == 0 && !globalApplication.options.Hold { + globalApplication.log("last window closed, quitting") globalApplication.Quit() } } else { @@ -763,7 +882,7 @@ func windowSetupSignalHandlers(windowId uint, window windowPtr, webview webviewP for name, constant := range w.constants { w.ExecJS(fmt.Sprintf("const %s = JSON.parse('%s');", name, constant)) } - + // TODO: this is not working properly w.ExecJS(apiClient(w.bindings)) for _, css := range w.options.CSS { @@ -848,15 +967,19 @@ func (w *Window) SetURL(uri string) { url, err := url.Parse(uri) if err == nil && url.Scheme == "" && url.Host == "" { // TODO handle this in a central location, the scheme and host might be platform dependant. - url.Scheme = URLScheme - url.Host = URLScheme // TODO: maybe handle differently - //url.Host = "" + url.Scheme = uriScheme + // url.Host = uriScheme // TODO: maybe handle differently + //url.Host = "src" uri = url.String() } } windowSetURL(w.webview, uri) } +func (w *Window) SetHTML(html string) { + lib.webkit.WebViewLoadHtml(w.webview, html, uriScheme+"://") +} + func (w *Window) SetMinMaxSize(minWidth, minHeight, maxWidth, maxHeight int) { if minWidth == 0 { minWidth = -1 @@ -972,9 +1095,9 @@ func (w *Window) setEnabled(enabled bool) { widgetSetSensitive(ptr(w.pointer), enabled) } -func (w *Window) setHTML(html string) { - windowSetHTML(w.webview, html) -} +//func (w *Window) setHTML(html string) { +// windowSetHTML(w.webview, html) +//} func (w *Window) startResize(border string) error { // FIXME: what do we need to do here?