base webview project
This commit is contained in:
170
README_webview.md
Normal file
170
README_webview.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# webview — низкоуровневый WinAPI webview на Go (Windows XP)
|
||||||
|
|
||||||
|
Пакет `nati-launch/webview` создаёт нативное окно Windows, целиком занятое
|
||||||
|
веб-вью, средствами **чистого WinAPI/OLE** через стандартный пакет `syscall`.
|
||||||
|
Никаких внешних зависимостей и DLL: используется встроенный в Windows
|
||||||
|
управляющий элемент Internet Explorer (`shdocvw` / `IWebBrowser2`).
|
||||||
|
|
||||||
|
Это Go-порт примера `WinAPI-Embed-Browser/embed-browser.c`, упрощённый до
|
||||||
|
«сплошного» окна (без кнопок и прочих Win32-контролов — браузер растягивается
|
||||||
|
на весь клиентский прямоугольник).
|
||||||
|
|
||||||
|
## Совместимость
|
||||||
|
|
||||||
|
- **Целевая платформа:** Windows XP и новее.
|
||||||
|
- **Тулчейн:** Go **1.10.8** — последняя версия с поддержкой XP. ⚠️ Это
|
||||||
|
**обязательно**: бинарники, собранные Go 1.11+, на XP не запускаются и дают
|
||||||
|
ошибку «не является допустимым приложением Win32» — независимо от `GOARCH`.
|
||||||
|
Системный `go` (если он новее 1.10) использовать **нельзя**.
|
||||||
|
- **Архитектура:** `GOARCH=386` (32-бит, работает и на x64 через WOW64).
|
||||||
|
- Go 1.10.8 **не поддерживает модули** — сборка ведётся в режиме **GOPATH**.
|
||||||
|
|
||||||
|
> Компилировать **на самой XP не нужно** — это обычная кросс-компиляция с
|
||||||
|
> Windows 10/11 старым тулчейном.
|
||||||
|
|
||||||
|
## Установка Go 1.10.8
|
||||||
|
|
||||||
|
1. Скачать `go1.10.8.windows-amd64.zip` со страницы релизов https://go.dev/dl/
|
||||||
|
(есть в архиве старых версий).
|
||||||
|
2. Распаковать, например, в `C:\go1.10.8` (там должен оказаться
|
||||||
|
`C:\go1.10.8\bin\go.exe`).
|
||||||
|
|
||||||
|
## GOPATH
|
||||||
|
|
||||||
|
Исходники уже лежат в стандартной структуре `src/nati-launch`. `build_xp.bat`
|
||||||
|
назначает корень проекта как `GOPATH` и собирает код прямо на месте — без
|
||||||
|
копирования, временных папок и симлинков. Кэш Go сохраняется между сборками.
|
||||||
|
|
||||||
|
## Сборка и запуск
|
||||||
|
|
||||||
|
`build_xp.bat` уже всё делает: проверяет наличие Go 1.10.8, выставляет
|
||||||
|
`GOROOT/GOOS/GOARCH/CGO_ENABLED` и собирает GUI-приложение.
|
||||||
|
|
||||||
|
```bat
|
||||||
|
set GO110_ROOT=C:\go1.10.8
|
||||||
|
build_xp.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
`build_xp.bat` собирает `nati-launch.exe` как 32-битное GUI-приложение
|
||||||
|
(`-ldflags "-H windowsgui"`, без консольного окна).
|
||||||
|
|
||||||
|
### Проверить, каким Go собран .exe
|
||||||
|
|
||||||
|
```bat
|
||||||
|
findstr /M /C:"go1.10.8" nati-launch.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Команда должна вывести имя `nati-launch.exe`. Старый Go ещё не записывал
|
||||||
|
современный блок build info, поэтому `go version nati-launch.exe` его не читает.
|
||||||
|
|
||||||
|
## Пример использования
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "nati-launch/webview"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
w, err := webview.New(webview.Options{
|
||||||
|
Title: "My App",
|
||||||
|
Width: 1024,
|
||||||
|
Height: 768,
|
||||||
|
Resizable: true,
|
||||||
|
DisableMenu: true,
|
||||||
|
URL: "https://example.com",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
w.Show()
|
||||||
|
w.Run() // блокирует до закрытия окна
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Функция | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `New(Options) (*Window, error)` | регистрация класса, создание окна, встраивание браузера |
|
||||||
|
| `(*Window) Run()` | цикл сообщений Win32 (блокирует) |
|
||||||
|
| `(*Window) Navigate(url string) error` | перейти по URL |
|
||||||
|
| `(*Window) LoadHTML(html string) error` | показать HTML-строку (через временный файл) |
|
||||||
|
| `(*Window) GoBack() / GoForward() / Refresh() / Stop()` | команды навигации |
|
||||||
|
| `(*Window) Show() / Hide()` | показать / скрыть окно |
|
||||||
|
| `(*Window) SetTitle(s) / Resize(w,h) / Center() / Close()` | управление окном |
|
||||||
|
| `(*Window) Bind(name, fn)` | пробросить Go-функцию в JS как `window.external.<name>(...)` |
|
||||||
|
|
||||||
|
### `Options`
|
||||||
|
|
||||||
|
| Поле | Описание |
|
||||||
|
|---|---|
|
||||||
|
| `Title` | заголовок окна |
|
||||||
|
| `Width`, `Height` | размер клиентской области (0 → 1024×768) |
|
||||||
|
| `X`, `Y` | позиция (0,0 → по центру экрана) |
|
||||||
|
| `URL` | URL для загрузки при старте |
|
||||||
|
| `HTML` | HTML для показа при старте (приоритетнее `URL`) |
|
||||||
|
| `Resizable` | разрешить изменение размера |
|
||||||
|
| `DisableMenu` | отключить контекстное меню IE |
|
||||||
|
| `HideScrollbars` | скрыть полосы прокрутки страницы |
|
||||||
|
|
||||||
|
## Мост JS → Go
|
||||||
|
|
||||||
|
Go-функции пробрасываются в страницу через `IDocHostUIHandler::GetExternal`
|
||||||
|
(реализован свой `IDispatch`). Из JS они доступны как
|
||||||
|
`window.external.<имя>(аргументы)`. Аргументы приходят в Go строками, а
|
||||||
|
возвращённая строка отдаётся обратно в JS как результат вызова.
|
||||||
|
|
||||||
|
```go
|
||||||
|
w, _ := webview.New(webview.Options{Width: 900, Height: 600})
|
||||||
|
|
||||||
|
// привязать ДО загрузки страницы, которая этим пользуется
|
||||||
|
w.Bind("report", func(args []string) string {
|
||||||
|
// args[0] — то, что передали из JS
|
||||||
|
return "ack: " + args[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
w.LoadHTML(`<button onclick="
|
||||||
|
var r = window.external.report('hello from JS');
|
||||||
|
alert(r); // -> ack: hello from JS
|
||||||
|
">call Go</button>`)
|
||||||
|
|
||||||
|
w.Show()
|
||||||
|
w.Run()
|
||||||
|
```
|
||||||
|
|
||||||
|
Готовый пример (счётчик на JS + кнопка, вызывающая Go) — в
|
||||||
|
`src/nati-launch/cmd/nati-launch/main.go`.
|
||||||
|
JS должен быть совместим с IE (используйте `onclick`/`attachEvent`,
|
||||||
|
`setInterval`, без `addEventListener`).
|
||||||
|
|
||||||
|
## Встроенный frontend
|
||||||
|
|
||||||
|
HTML, CSS, JavaScript, изображения и другие ресурсы лежат в папке
|
||||||
|
`src/nati-launch/frontend/`.
|
||||||
|
Перед обычной сборкой обновите сгенерированный Go-файл:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd src/nati-launch
|
||||||
|
go generate ./cmd/nati-launch
|
||||||
|
go build -o ../../nati-launch.exe ./cmd/nati-launch
|
||||||
|
```
|
||||||
|
|
||||||
|
Генератор `src/nati-launch/tools/frontendgen` рекурсивно упаковывает все файлы в
|
||||||
|
`src/nati-launch/cmd/nati-launch/frontend_gen.go`. При запуске `LoadFiles` отдаёт ресурсы прямо
|
||||||
|
из памяти через зарегистрированный внутри процесса URLMon-протокол `nati...://`.
|
||||||
|
На диск ничего не распаковывается и сетевой порт не открывается. Относительные
|
||||||
|
ссылки внутри frontend продолжают работать, а рядом с готовым EXE папка
|
||||||
|
`frontend/` не нужна. URLMon и `IInternetProtocol` доступны в Windows XP;
|
||||||
|
`build_xp.bat` запускает кодген автоматически.
|
||||||
|
|
||||||
|
## Важные замечания
|
||||||
|
|
||||||
|
- **Один поток / STA.** `New` вызывает `runtime.LockOSThread` и `OleInitialize`;
|
||||||
|
вызывайте `New` и `Run` из одной и той же горутины.
|
||||||
|
- **Движок Trident.** На XP это IE6–IE8; современные сайты могут рендериться в
|
||||||
|
режиме совместимости. Для локального HTML и простых страниц это ожидаемо.
|
||||||
|
- По умолчанию IE рендерит во встроенном приложении в режиме IE7. При
|
||||||
|
необходимости задайте режим эмуляции через реестр
|
||||||
|
(`HKCU\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION`,
|
||||||
|
значение = имя вашего `.exe`, dword `8888` для IE8 на XP).
|
||||||
|
```
|
||||||
57
build_xp.bat
Normal file
57
build_xp.bat
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
@echo off
|
||||||
|
REM ===========================================================================
|
||||||
|
REM Build nati-launch for Windows XP.
|
||||||
|
REM
|
||||||
|
REM IMPORTANT: Windows XP is only supported by the Go 1.10.x toolchain.
|
||||||
|
REM Binaries produced by Go 1.11+ fail on XP with "is not a valid Win32
|
||||||
|
REM application", regardless of GOARCH. So the SYSTEM `go` (likely a modern
|
||||||
|
REM version) MUST NOT be used here.
|
||||||
|
REM
|
||||||
|
REM Requirements:
|
||||||
|
REM * Go 1.10.8 unpacked somewhere, e.g. C:\go1.10.8
|
||||||
|
REM * Sources located at .\src\nati-launch (standard GOPATH layout)
|
||||||
|
REM
|
||||||
|
REM Set GO110_ROOT to your Go 1.10.8 folder (or edit the default below), then
|
||||||
|
REM just run build_xp.bat.
|
||||||
|
REM ===========================================================================
|
||||||
|
|
||||||
|
if "%GO110_ROOT%"=="" set GO110_ROOT=C:\go1.10.8
|
||||||
|
|
||||||
|
if not exist "%GO110_ROOT%\bin\go.exe" (
|
||||||
|
echo ERROR: Go 1.10.8 not found at "%GO110_ROOT%".
|
||||||
|
echo Download go1.10.8.windows-amd64.zip from https://go.dev/dl/ and unpack it,
|
||||||
|
echo then set GO110_ROOT to that folder, e.g.: set GO110_ROOT=C:\go1.10.8
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%~dp0src\nati-launch\cmd\nati-launch\main.go" (
|
||||||
|
echo ERROR: Sources not found at "%~dp0src\nati-launch".
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
set GOPATH=%~dp0
|
||||||
|
|
||||||
|
set GOROOT=%GO110_ROOT%
|
||||||
|
set PATH=%GO110_ROOT%\bin;%PATH%
|
||||||
|
set GOOS=windows
|
||||||
|
set GOARCH=386
|
||||||
|
set CGO_ENABLED=0
|
||||||
|
|
||||||
|
echo Using toolchain:
|
||||||
|
"%GO110_ROOT%\bin\go.exe" version
|
||||||
|
echo GOPATH=%GOPATH%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
"%GO110_ROOT%\bin\go.exe" generate nati-launch/cmd/nati-launch
|
||||||
|
if errorlevel 1 goto fail
|
||||||
|
|
||||||
|
"%GO110_ROOT%\bin\go.exe" build -ldflags "-H windowsgui" -o "%~dp0nati-launch.exe" nati-launch/cmd/nati-launch
|
||||||
|
if errorlevel 1 goto fail
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Built nati-launch.exe (32-bit, GUI subsystem, Go 1.10.8 for Windows XP)
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:fail
|
||||||
|
echo Build failed.
|
||||||
|
exit /b 1
|
||||||
9
src/nati-launch/cmd/nati-launch/frontend_gen.go
Normal file
9
src/nati-launch/cmd/nati-launch/frontend_gen.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Code generated by frontendgen; DO NOT EDIT.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
var frontendFiles = map[string][]byte{
|
||||||
|
"app.js": []byte("// Keep this file compatible with the IE engine used by the native WebView.\nvar counter = 0;\n\nfunction tick() {\n counter = counter + 1;\n document.getElementById('count').innerHTML = counter;\n}\n\nsetInterval(tick, 1000);\n\nfunction sendToGo() {\n try {\n var reply = window.external.report(counter);\n document.getElementById('ack').innerHTML = 'Go replied: ' + reply;\n } catch (e) {\n document.getElementById('ack').innerHTML = 'bridge error: ' + e.message;\n }\n}\n"),
|
||||||
|
"index.html": []byte("<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>nati-launch</title>\n <link rel=\"stylesheet\" href=\"styles.css\">\n</head>\n<body>\n <h1>JS ↔ Go bridge</h1>\n\n <p>JS counter (tick every second):</p>\n <div id=\"count\">0</div>\n\n <button onclick=\"sendToGo()\">Send counter to Go</button>\n <div id=\"ack\"> </div>\n\n <script src=\"app.js\"></script>\n</body>\n</html>\n"),
|
||||||
|
"styles.css": []byte("html,\nbody {\n overflow: hidden;\n}\n\nbody {\n margin: 40px;\n font-family: Tahoma, sans-serif;\n}\n\n#count {\n color: #225;\n font-size: 48px;\n font-weight: bold;\n}\n\nbutton {\n margin-top: 12px;\n padding: 6px 14px;\n font-size: 16px;\n}\n\n#ack {\n margin-top: 16px;\n color: #060;\n}\n"),
|
||||||
|
}
|
||||||
46
src/nati-launch/cmd/nati-launch/main.go
Normal file
46
src/nati-launch/cmd/nati-launch/main.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"nati-launch/webview"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go run ../../tools/frontendgen/main.go -src ../../frontend -out frontend_gen.go -package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
w, err := webview.New(webview.Options{
|
||||||
|
Title: "nati-launch",
|
||||||
|
Width: 900,
|
||||||
|
Height: 600,
|
||||||
|
Resizable: true,
|
||||||
|
DisableMenu: true,
|
||||||
|
HideScrollbars: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
calls := 0
|
||||||
|
// Register a Go function reachable from JS as window.external.report(...).
|
||||||
|
w.Bind("report", func(args []string) string {
|
||||||
|
calls++
|
||||||
|
value := ""
|
||||||
|
if len(args) > 0 {
|
||||||
|
value = args[0]
|
||||||
|
}
|
||||||
|
// Visible proof on the Go side: update the window title.
|
||||||
|
w.SetTitle("nati-launch — counter=" + value + " (call #" + strconv.Itoa(calls) + ")")
|
||||||
|
fmt.Printf("[go] report() called: counter=%s, call #%d\n", value, calls)
|
||||||
|
return "ack #" + strconv.Itoa(calls) + " for counter " + value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load the generated frontend bundle after binding so JS can call Go.
|
||||||
|
if err := w.LoadFiles(frontendFiles, "index.html"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Show()
|
||||||
|
w.Run()
|
||||||
|
}
|
||||||
18
src/nati-launch/frontend/app.js
Normal file
18
src/nati-launch/frontend/app.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Keep this file compatible with the IE engine used by the native WebView.
|
||||||
|
var counter = 0;
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
counter = counter + 1;
|
||||||
|
document.getElementById('count').innerHTML = counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(tick, 1000);
|
||||||
|
|
||||||
|
function sendToGo() {
|
||||||
|
try {
|
||||||
|
var reply = window.external.report(counter);
|
||||||
|
document.getElementById('ack').innerHTML = 'Go replied: ' + reply;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('ack').innerHTML = 'bridge error: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/nati-launch/frontend/index.html
Normal file
19
src/nati-launch/frontend/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>nati-launch</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>JS ↔ Go bridge</h1>
|
||||||
|
|
||||||
|
<p>JS counter (tick every second):</p>
|
||||||
|
<div id="count">0</div>
|
||||||
|
|
||||||
|
<button onclick="sendToGo()">Send counter to Go</button>
|
||||||
|
<div id="ack"> </div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
src/nati-launch/frontend/styles.css
Normal file
26
src/nati-launch/frontend/styles.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
html,
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 40px;
|
||||||
|
font-family: Tahoma, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#count {
|
||||||
|
color: #225;
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ack {
|
||||||
|
margin-top: 16px;
|
||||||
|
color: #060;
|
||||||
|
}
|
||||||
83
src/nati-launch/tools/frontendgen/main.go
Normal file
83
src/nati-launch/tools/frontendgen/main.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// Command frontendgen turns a directory into a Go map of embedded files.
|
||||||
|
// It intentionally uses only APIs available in Go 1.10.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"go/format"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
src := flag.String("src", "", "directory to bundle")
|
||||||
|
out := flag.String("out", "", "generated Go file")
|
||||||
|
pkg := flag.String("package", "main", "generated package name")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *src == "" || *out == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "frontendgen: -src and -out are required")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
files := make(map[string][]byte)
|
||||||
|
var names []string
|
||||||
|
err := filepath.Walk(*src, func(name string, info os.FileInfo, walkErr error) error {
|
||||||
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rel, err := filepath.Rel(*src, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rel = filepath.ToSlash(rel)
|
||||||
|
data, err := ioutil.ReadFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
files[rel] = data
|
||||||
|
names = append(names, rel)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "frontendgen:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if len(names) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "frontendgen: source directory is empty")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(names)
|
||||||
|
var generated bytes.Buffer
|
||||||
|
fmt.Fprintln(&generated, "// Code generated by frontendgen; DO NOT EDIT.")
|
||||||
|
fmt.Fprintln(&generated)
|
||||||
|
fmt.Fprintf(&generated, "package %s\n\n", *pkg)
|
||||||
|
fmt.Fprintln(&generated, "var frontendFiles = map[string][]byte{")
|
||||||
|
for _, name := range names {
|
||||||
|
fmt.Fprintf(&generated, "\t%s: []byte(%s),\n", strconv.Quote(name), strconv.Quote(string(files[name])))
|
||||||
|
}
|
||||||
|
fmt.Fprintln(&generated, "}")
|
||||||
|
|
||||||
|
formatted, err := format.Source(generated.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "frontendgen: format generated source:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
current, _ := ioutil.ReadFile(*out)
|
||||||
|
if bytes.Equal(current, formatted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(*out, formatted, 0644); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "frontendgen:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
320
src/nati-launch/webview/com.go
Normal file
320
src/nati-launch/webview/com.go
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
package webview
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HRESULT values.
|
||||||
|
const (
|
||||||
|
sOK uintptr = 0x00000000
|
||||||
|
sFalse uintptr = 0x00000001
|
||||||
|
eNotImpl uintptr = 0x80004001
|
||||||
|
eNoInterface uintptr = 0x80004002
|
||||||
|
ePointer uintptr = 0x80004003
|
||||||
|
classENoAggregation uintptr = 0x80040110
|
||||||
|
inetEResourceNotFound uintptr = 0x800C0005
|
||||||
|
)
|
||||||
|
|
||||||
|
const ptrSize = unsafe.Sizeof(uintptr(0))
|
||||||
|
|
||||||
|
// comCall invokes method number idx in the vtable of the COM object pointed to
|
||||||
|
// by this. The first argument passed to the method is always this itself.
|
||||||
|
func comCall(this uintptr, idx int, args ...uintptr) uintptr {
|
||||||
|
vtbl := *(*uintptr)(unsafe.Pointer(this))
|
||||||
|
fn := *(*uintptr)(unsafe.Pointer(vtbl + uintptr(idx)*ptrSize))
|
||||||
|
|
||||||
|
full := make([]uintptr, 0, len(args)+1)
|
||||||
|
full = append(full, this)
|
||||||
|
full = append(full, args...)
|
||||||
|
|
||||||
|
nargs := uintptr(len(full))
|
||||||
|
for len(full) < 3 {
|
||||||
|
full = append(full, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r uintptr
|
||||||
|
switch {
|
||||||
|
case nargs <= 3:
|
||||||
|
r, _, _ = syscall.Syscall(fn, nargs, full[0], full[1], full[2])
|
||||||
|
case nargs <= 6:
|
||||||
|
for len(full) < 6 {
|
||||||
|
full = append(full, 0)
|
||||||
|
}
|
||||||
|
r, _, _ = syscall.Syscall6(fn, nargs, full[0], full[1], full[2], full[3], full[4], full[5])
|
||||||
|
default:
|
||||||
|
for len(full) < 9 {
|
||||||
|
full = append(full, 0)
|
||||||
|
}
|
||||||
|
r, _, _ = syscall.Syscall9(fn, nargs, full[0], full[1], full[2], full[3], full[4], full[5], full[6], full[7], full[8])
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// release is a convenience wrapper for IUnknown::Release (vtable index 2).
|
||||||
|
func release(this uintptr) {
|
||||||
|
if this != 0 {
|
||||||
|
comCall(this, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The host-side COM objects. Each starts with a pointer to its vtable so the
|
||||||
|
// browser can call our functions, followed by a back-pointer to the owning
|
||||||
|
// Window so the callbacks can recover their context. (Go's GC does not move
|
||||||
|
// heap objects, so the addresses we hand to the browser stay valid as long as
|
||||||
|
// the Window keeps these structs alive.)
|
||||||
|
type oleClientSite struct {
|
||||||
|
vtbl uintptr
|
||||||
|
w *Window
|
||||||
|
}
|
||||||
|
|
||||||
|
type oleInPlaceSite struct {
|
||||||
|
vtbl uintptr
|
||||||
|
w *Window
|
||||||
|
}
|
||||||
|
|
||||||
|
type oleInPlaceFrame struct {
|
||||||
|
vtbl uintptr
|
||||||
|
w *Window
|
||||||
|
}
|
||||||
|
|
||||||
|
type docHostUIHandler struct {
|
||||||
|
vtbl uintptr
|
||||||
|
w *Window
|
||||||
|
}
|
||||||
|
|
||||||
|
// oleInPlaceFrameInfo mirrors OLEINPLACEFRAMEINFO.
|
||||||
|
type oleInPlaceFrameInfo struct {
|
||||||
|
cb uint32
|
||||||
|
fMDIApp int32
|
||||||
|
hwndFrame uintptr
|
||||||
|
haccel uintptr
|
||||||
|
cAccelEntries uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// docHostUIInfo mirrors DOCHOSTUIINFO.
|
||||||
|
type docHostUIInfo struct {
|
||||||
|
cbSize uint32
|
||||||
|
dwFlags uint32
|
||||||
|
dwDoubleClick uint32
|
||||||
|
pchHostCss *uint16
|
||||||
|
pchHostNS *uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
dochostuiflagNo3DBorder = 0x00000004
|
||||||
|
dochostuiflagScrollNo = 0x00000040 // turns off scrollbars (content is clipped, not scrollable)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Vtables are built once and kept alive for the lifetime of the process.
|
||||||
|
var (
|
||||||
|
clientSiteVtbl []uintptr
|
||||||
|
inPlaceSiteVtbl []uintptr
|
||||||
|
inPlaceFrameVtbl []uintptr
|
||||||
|
docHostVtbl []uintptr
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
buildVtables()
|
||||||
|
buildExternalVtbl()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cb(fn interface{}) uintptr { return syscall.NewCallback(fn) }
|
||||||
|
|
||||||
|
func buildVtables() {
|
||||||
|
// IOleClientSite : IUnknown
|
||||||
|
clientSiteVtbl = []uintptr{
|
||||||
|
cb(csQueryInterface),
|
||||||
|
cb(addRef),
|
||||||
|
cb(siteRelease),
|
||||||
|
cb(func(this uintptr) uintptr { return eNotImpl }), // SaveObject
|
||||||
|
cb(func(this, a, b, c uintptr) uintptr { return eNotImpl }), // GetMoniker
|
||||||
|
cb(func(this, ppContainer uintptr) uintptr {
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppContainer)) = 0
|
||||||
|
return eNoInterface
|
||||||
|
}), // GetContainer
|
||||||
|
cb(func(this uintptr) uintptr { return sOK }), // ShowObject
|
||||||
|
cb(func(this, fShow uintptr) uintptr { return sOK }), // OnShowWindow
|
||||||
|
cb(func(this uintptr) uintptr { return eNotImpl }), // RequestNewObjectLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
// IOleInPlaceSite : IOleWindow : IUnknown
|
||||||
|
inPlaceSiteVtbl = []uintptr{
|
||||||
|
cb(ipsQueryInterface),
|
||||||
|
cb(addRef),
|
||||||
|
cb(siteRelease),
|
||||||
|
cb(ipsGetWindow), // GetWindow
|
||||||
|
cb(func(this, b uintptr) uintptr { return sOK }), // ContextSensitiveHelp
|
||||||
|
cb(func(this uintptr) uintptr { return sOK }), // CanInPlaceActivate
|
||||||
|
cb(func(this uintptr) uintptr { return sOK }), // OnInPlaceActivate
|
||||||
|
cb(func(this uintptr) uintptr { return sOK }), // OnUIActivate
|
||||||
|
cb(ipsGetWindowContext), // GetWindowContext
|
||||||
|
cb(func(this, cx, cy uintptr) uintptr { return sOK }), // Scroll (SIZE passed by value = 2 dwords)
|
||||||
|
cb(func(this, a uintptr) uintptr { return sOK }), // OnUIDeactivate
|
||||||
|
cb(func(this uintptr) uintptr { return sOK }), // OnInPlaceDeactivate
|
||||||
|
cb(func(this uintptr) uintptr { return sOK }), // DiscardUndoState
|
||||||
|
cb(func(this uintptr) uintptr { return sOK }), // DeactivateAndUndo
|
||||||
|
cb(func(this, a uintptr) uintptr { return sOK }), // OnPosRectChange
|
||||||
|
}
|
||||||
|
|
||||||
|
// IOleInPlaceFrame : IOleInPlaceUIWindow : IOleWindow : IUnknown
|
||||||
|
inPlaceFrameVtbl = []uintptr{
|
||||||
|
cb(ipfQueryInterface),
|
||||||
|
cb(addRef),
|
||||||
|
cb(siteRelease),
|
||||||
|
cb(ipfGetWindow), // GetWindow
|
||||||
|
cb(func(this, b uintptr) uintptr { return sOK }), // ContextSensitiveHelp
|
||||||
|
cb(func(this, a uintptr) uintptr { return eNotImpl }), // GetBorder
|
||||||
|
cb(func(this, a uintptr) uintptr { return eNotImpl }), // RequestBorderSpace
|
||||||
|
cb(func(this, a uintptr) uintptr { return sOK }), // SetBorderSpace
|
||||||
|
cb(func(this, a, b uintptr) uintptr { return sOK }), // SetActiveObject
|
||||||
|
cb(func(this, a, b uintptr) uintptr { return sOK }), // InsertMenus
|
||||||
|
cb(func(this, a, b, c uintptr) uintptr { return sOK }), // SetMenu
|
||||||
|
cb(func(this, a uintptr) uintptr { return sOK }), // RemoveMenus
|
||||||
|
cb(func(this, a uintptr) uintptr { return sOK }), // SetStatusText
|
||||||
|
cb(func(this, a uintptr) uintptr { return sOK }), // EnableModeless
|
||||||
|
cb(func(this, a, b uintptr) uintptr { return sFalse }), // TranslateAccelerator
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDocHostUIHandler : IUnknown
|
||||||
|
docHostVtbl = []uintptr{
|
||||||
|
cb(duiQueryInterface),
|
||||||
|
cb(addRef),
|
||||||
|
cb(siteRelease),
|
||||||
|
cb(duiShowContextMenu), // ShowContextMenu (this + 4)
|
||||||
|
cb(duiGetHostInfo), // GetHostInfo (this + 1)
|
||||||
|
cb(func(this, a, b, c, d, e uintptr) uintptr { return sOK }), // ShowUI (this + 5)
|
||||||
|
cb(func(this uintptr) uintptr { return sOK }), // HideUI
|
||||||
|
cb(func(this uintptr) uintptr { return sOK }), // UpdateUI
|
||||||
|
cb(func(this, a uintptr) uintptr { return sOK }), // EnableModeless
|
||||||
|
cb(func(this, a uintptr) uintptr { return sOK }), // OnDocWindowActivate
|
||||||
|
cb(func(this, a uintptr) uintptr { return sOK }), // OnFrameWindowActivate
|
||||||
|
cb(func(this, a, b, c uintptr) uintptr { return sOK }), // ResizeBorder (this + 3)
|
||||||
|
cb(func(this, a, b, c uintptr) uintptr { return sFalse }), // TranslateAccelerator (this + 3)
|
||||||
|
cb(func(this, a, b uintptr) uintptr { return sFalse }), // GetOptionKeyPath (this + 2)
|
||||||
|
cb(func(this, a, b uintptr) uintptr { return eNotImpl }), // GetDropTarget (this + 2)
|
||||||
|
cb(duiGetExternal), // GetExternal (this + 1)
|
||||||
|
cb(duiTranslateUrl), // TranslateUrl (this + 3)
|
||||||
|
cb(func(this, a, b uintptr) uintptr { *(*uintptr)(unsafe.Pointer(b)) = 0; return sFalse }), // FilterDataObject (this + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- shared IUnknown callbacks ---
|
||||||
|
|
||||||
|
func addRef(this uintptr) uintptr { return 1 }
|
||||||
|
func siteRelease(this uintptr) uintptr { return 1 }
|
||||||
|
|
||||||
|
func vtblAddr(s []uintptr) uintptr { return uintptr(unsafe.Pointer(&s[0])) }
|
||||||
|
|
||||||
|
// queryInterface implements the shared logic: every one of our host objects can
|
||||||
|
// hand back any of the interfaces the browser asks for, all rooted at the owning
|
||||||
|
// Window's site objects.
|
||||||
|
func queryInterface(w *Window, riid, ppv uintptr) uintptr {
|
||||||
|
iid := (*GUID)(unsafe.Pointer(riid))
|
||||||
|
out := (*uintptr)(unsafe.Pointer(ppv))
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case guidEqual(iid, &IID_IUnknown), guidEqual(iid, &IID_IOleClientSite):
|
||||||
|
*out = uintptr(unsafe.Pointer(w.clientSite))
|
||||||
|
case guidEqual(iid, &IID_IOleInPlaceSite):
|
||||||
|
*out = uintptr(unsafe.Pointer(w.inPlaceSite))
|
||||||
|
case guidEqual(iid, &IID_IOleInPlaceFrame):
|
||||||
|
*out = uintptr(unsafe.Pointer(w.inPlaceFrame))
|
||||||
|
case guidEqual(iid, &IID_IDocHostUIHandler):
|
||||||
|
*out = uintptr(unsafe.Pointer(w.docHost))
|
||||||
|
default:
|
||||||
|
*out = 0
|
||||||
|
return eNoInterface
|
||||||
|
}
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func csQueryInterface(this, riid, ppv uintptr) uintptr {
|
||||||
|
return queryInterface((*oleClientSite)(unsafe.Pointer(this)).w, riid, ppv)
|
||||||
|
}
|
||||||
|
func ipsQueryInterface(this, riid, ppv uintptr) uintptr {
|
||||||
|
return queryInterface((*oleInPlaceSite)(unsafe.Pointer(this)).w, riid, ppv)
|
||||||
|
}
|
||||||
|
func ipfQueryInterface(this, riid, ppv uintptr) uintptr {
|
||||||
|
return queryInterface((*oleInPlaceFrame)(unsafe.Pointer(this)).w, riid, ppv)
|
||||||
|
}
|
||||||
|
func duiQueryInterface(this, riid, ppv uintptr) uintptr {
|
||||||
|
return queryInterface((*docHostUIHandler)(unsafe.Pointer(this)).w, riid, ppv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- IOleInPlaceSite ---
|
||||||
|
|
||||||
|
func ipsGetWindow(this, phwnd uintptr) uintptr {
|
||||||
|
w := (*oleInPlaceSite)(unsafe.Pointer(this)).w
|
||||||
|
*(*uintptr)(unsafe.Pointer(phwnd)) = w.hwnd
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipsGetWindowContext(this, ppFrame, ppDoc, lprcPosRect, lprcClipRect, pInfo uintptr) uintptr {
|
||||||
|
w := (*oleInPlaceSite)(unsafe.Pointer(this)).w
|
||||||
|
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppFrame)) = uintptr(unsafe.Pointer(w.inPlaceFrame))
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppDoc)) = 0
|
||||||
|
|
||||||
|
rc := getClientRect(w.hwnd)
|
||||||
|
if lprcPosRect != 0 {
|
||||||
|
*(*rect)(unsafe.Pointer(lprcPosRect)) = rc
|
||||||
|
}
|
||||||
|
if lprcClipRect != 0 {
|
||||||
|
*(*rect)(unsafe.Pointer(lprcClipRect)) = rc
|
||||||
|
}
|
||||||
|
if pInfo != 0 {
|
||||||
|
info := (*oleInPlaceFrameInfo)(unsafe.Pointer(pInfo))
|
||||||
|
info.cb = uint32(unsafe.Sizeof(oleInPlaceFrameInfo{}))
|
||||||
|
info.fMDIApp = 0
|
||||||
|
info.hwndFrame = w.hwnd
|
||||||
|
info.haccel = 0
|
||||||
|
info.cAccelEntries = 0
|
||||||
|
}
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- IOleInPlaceFrame ---
|
||||||
|
|
||||||
|
func ipfGetWindow(this, phwnd uintptr) uintptr {
|
||||||
|
w := (*oleInPlaceFrame)(unsafe.Pointer(this)).w
|
||||||
|
*(*uintptr)(unsafe.Pointer(phwnd)) = w.hwnd
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- IDocHostUIHandler ---
|
||||||
|
|
||||||
|
func duiShowContextMenu(this, dwID, ppt, pcmdReserved, pdispReserved uintptr) uintptr {
|
||||||
|
w := (*docHostUIHandler)(unsafe.Pointer(this)).w
|
||||||
|
if w.disableMenu {
|
||||||
|
return sOK // we "handled" it -> default IE context menu is suppressed
|
||||||
|
}
|
||||||
|
return sFalse // let the browser show its own menu
|
||||||
|
}
|
||||||
|
|
||||||
|
func duiGetHostInfo(this, pInfo uintptr) uintptr {
|
||||||
|
w := (*docHostUIHandler)(unsafe.Pointer(this)).w
|
||||||
|
info := (*docHostUIInfo)(unsafe.Pointer(pInfo))
|
||||||
|
info.cbSize = uint32(unsafe.Sizeof(docHostUIInfo{}))
|
||||||
|
info.dwFlags = dochostuiflagNo3DBorder
|
||||||
|
if w.hideScroll {
|
||||||
|
info.dwFlags |= dochostuiflagScrollNo
|
||||||
|
}
|
||||||
|
info.dwDoubleClick = 0
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func duiTranslateUrl(this, dwTranslate, pchURLIn, ppchURLOut uintptr) uintptr {
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppchURLOut)) = 0
|
||||||
|
return sFalse // do not modify the URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func duiGetExternal(this, ppDisp uintptr) uintptr {
|
||||||
|
w := (*docHostUIHandler)(unsafe.Pointer(this)).w
|
||||||
|
if w.external != nil {
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppDisp)) = uintptr(unsafe.Pointer(w.external))
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppDisp)) = 0
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
153
src/nati-launch/webview/external.go
Normal file
153
src/nati-launch/webview/external.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package webview
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// external.go implements a minimal IDispatch object that the hosted page can
|
||||||
|
// reach as window.external.<name>(...). Go functions are registered with
|
||||||
|
// Window.Bind; the browser resolves a name to a DISPID via GetIDsOfNames and
|
||||||
|
// then calls Invoke, which we route to the bound Go function.
|
||||||
|
|
||||||
|
// VARIANT change/clear helpers (oleaut32).
|
||||||
|
var (
|
||||||
|
procVariantInit = oleaut32.NewProc("VariantInit")
|
||||||
|
procVariantClear = oleaut32.NewProc("VariantClear")
|
||||||
|
procVariantChangeType = oleaut32.NewProc("VariantChangeType")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
vtBSTR = 8
|
||||||
|
|
||||||
|
dispErrUnknownName = 0x80020006
|
||||||
|
dispErrMemberNotFound = 0x80020003
|
||||||
|
)
|
||||||
|
|
||||||
|
// externalDispatch is the COM object handed to the browser via GetExternal.
|
||||||
|
type externalDispatch struct {
|
||||||
|
vtbl uintptr
|
||||||
|
w *Window
|
||||||
|
}
|
||||||
|
|
||||||
|
var externalVtbl []uintptr
|
||||||
|
|
||||||
|
func buildExternalVtbl() {
|
||||||
|
externalVtbl = []uintptr{
|
||||||
|
cb(extQueryInterface), // 0 QueryInterface
|
||||||
|
cb(addRef), // 1 AddRef
|
||||||
|
cb(siteRelease), // 2 Release
|
||||||
|
cb(func(this, pctinfo uintptr) uintptr { // 3 GetTypeInfoCount
|
||||||
|
*(*uint32)(unsafe.Pointer(pctinfo)) = 0
|
||||||
|
return sOK
|
||||||
|
}),
|
||||||
|
cb(func(this, a, b, c uintptr) uintptr { return eNotImpl }), // 4 GetTypeInfo
|
||||||
|
cb(extGetIDsOfNames), // 5 GetIDsOfNames
|
||||||
|
cb(extInvoke), // 6 Invoke
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extQueryInterface(this, riid, ppv uintptr) uintptr {
|
||||||
|
iid := (*GUID)(unsafe.Pointer(riid))
|
||||||
|
if guidEqual(iid, &IID_IUnknown) || guidEqual(iid, &IID_IDispatch) {
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppv)) = this
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppv)) = 0
|
||||||
|
return eNoInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func extGetIDsOfNames(this, riid, rgszNames, cNames, lcid, rgDispId uintptr) uintptr {
|
||||||
|
w := (*externalDispatch)(unsafe.Pointer(this)).w
|
||||||
|
name := utf16PtrToString(*(*uintptr)(unsafe.Pointer(rgszNames)))
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
id, ok := w.bindIDs[name]
|
||||||
|
w.mu.Unlock()
|
||||||
|
if !ok {
|
||||||
|
*(*int32)(unsafe.Pointer(rgDispId)) = -1
|
||||||
|
return dispErrUnknownName
|
||||||
|
}
|
||||||
|
*(*int32)(unsafe.Pointer(rgDispId)) = id
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func extInvoke(this, dispid, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr uintptr) uintptr {
|
||||||
|
w := (*externalDispatch)(unsafe.Pointer(this)).w
|
||||||
|
id := int32(dispid)
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
fn := w.bindings[w.idNames[id]]
|
||||||
|
w.mu.Unlock()
|
||||||
|
if fn == nil {
|
||||||
|
return dispErrMemberNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
if pDispParams != 0 {
|
||||||
|
dp := (*dispParams)(unsafe.Pointer(pDispParams))
|
||||||
|
n := dp.cArgs
|
||||||
|
args = make([]string, n)
|
||||||
|
// Arguments arrive in reverse order in rgvarg.
|
||||||
|
for i := uint32(0); i < n; i++ {
|
||||||
|
vptr := dp.rgvarg + uintptr(n-1-i)*unsafe.Sizeof(variant{})
|
||||||
|
args[i] = variantToString(vptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := fn(args)
|
||||||
|
|
||||||
|
if pVarResult != 0 {
|
||||||
|
vr := (*variant)(unsafe.Pointer(pVarResult))
|
||||||
|
vr.vt = vtBSTR
|
||||||
|
vr.val = sysAllocString(res)
|
||||||
|
}
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
// variantToString coerces a VARIANT to a Go string.
|
||||||
|
func variantToString(v uintptr) string {
|
||||||
|
src := (*variant)(unsafe.Pointer(v))
|
||||||
|
if src.vt == vtBSTR {
|
||||||
|
return bstrToString(src.val)
|
||||||
|
}
|
||||||
|
var dest variant
|
||||||
|
procVariantInit.Call(uintptr(unsafe.Pointer(&dest)))
|
||||||
|
hr, _, _ := procVariantChangeType.Call(uintptr(unsafe.Pointer(&dest)), v, 0, vtBSTR)
|
||||||
|
if hr != 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
s := bstrToString(dest.val)
|
||||||
|
procVariantClear.Call(uintptr(unsafe.Pointer(&dest)))
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func bstrToString(b uintptr) string {
|
||||||
|
if b == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
n := *(*uint32)(unsafe.Pointer(b - 4)) / 2 // byte length prefix -> char count
|
||||||
|
if n == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
buf := make([]uint16, n)
|
||||||
|
for i := uint32(0); i < n; i++ {
|
||||||
|
buf[i] = *(*uint16)(unsafe.Pointer(b + uintptr(i)*2))
|
||||||
|
}
|
||||||
|
return syscall.UTF16ToString(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func utf16PtrToString(p uintptr) string {
|
||||||
|
if p == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var buf []uint16
|
||||||
|
for i := uintptr(0); ; i += 2 {
|
||||||
|
c := *(*uint16)(unsafe.Pointer(p + i))
|
||||||
|
if c == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf = append(buf, c)
|
||||||
|
}
|
||||||
|
return syscall.UTF16ToString(buf)
|
||||||
|
}
|
||||||
43
src/nati-launch/webview/guids.go
Normal file
43
src/nati-launch/webview/guids.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package webview
|
||||||
|
|
||||||
|
// GUID is the Win32 GUID / IID / CLSID structure.
|
||||||
|
type GUID struct {
|
||||||
|
Data1 uint32
|
||||||
|
Data2 uint16
|
||||||
|
Data3 uint16
|
||||||
|
Data4 [8]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGUID(d1 uint32, d2, d3 uint16, b0, b1, b2, b3, b4, b5, b6, b7 byte) GUID {
|
||||||
|
return GUID{d1, d2, d3, [8]byte{b0, b1, b2, b3, b4, b5, b6, b7}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func guidEqual(a, b *GUID) bool {
|
||||||
|
if a.Data1 != b.Data1 || a.Data2 != b.Data2 || a.Data3 != b.Data3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
if a.Data4[i] != b.Data4[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// COM identifiers needed to host the Internet Explorer WebBrowser control.
|
||||||
|
var (
|
||||||
|
CLSID_WebBrowser = newGUID(0x8856F961, 0x340A, 0x11D0, 0xA9, 0x6B, 0x00, 0xC0, 0x4F, 0xD7, 0x05, 0xA2)
|
||||||
|
|
||||||
|
IID_IUnknown = newGUID(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
|
||||||
|
IID_IClassFactory = newGUID(0x00000001, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
|
||||||
|
IID_IOleObject = newGUID(0x00000112, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
|
||||||
|
IID_IDispatch = newGUID(0x00020400, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
|
||||||
|
IID_IOleClientSite = newGUID(0x00000118, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
|
||||||
|
IID_IOleInPlaceSite = newGUID(0x00000119, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
|
||||||
|
IID_IOleInPlaceFrame = newGUID(0x00000116, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
|
||||||
|
IID_IOleInPlaceObject = newGUID(0x00000113, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46)
|
||||||
|
IID_IDocHostUIHandler = newGUID(0xBD3F23C0, 0xD43E, 0x11CF, 0x89, 0x3B, 0x00, 0xAA, 0x00, 0xBD, 0xCE, 0x1A)
|
||||||
|
IID_IWebBrowser2 = newGUID(0xD30C1661, 0xCDAF, 0x11D0, 0x8A, 0x3E, 0x00, 0xC0, 0x4F, 0xC9, 0xE2, 0x6E)
|
||||||
|
IID_IInternetProtocolRoot = newGUID(0x79EAC9E3, 0xBAF9, 0x11CE, 0x8C, 0x82, 0x00, 0xAA, 0x00, 0x4B, 0xA9, 0x0B)
|
||||||
|
IID_IInternetProtocol = newGUID(0x79EAC9E4, 0xBAF9, 0x11CE, 0x8C, 0x82, 0x00, 0xAA, 0x00, 0x4B, 0xA9, 0x0B)
|
||||||
|
)
|
||||||
249
src/nati-launch/webview/host.go
Normal file
249
src/nati-launch/webview/host.go
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
package webview
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// oleaut32 is used for BSTR allocation.
|
||||||
|
var (
|
||||||
|
oleaut32 = syscall.NewLazyDLL("oleaut32.dll")
|
||||||
|
procSysAllocString = oleaut32.NewProc("SysAllocString")
|
||||||
|
procSysFreeString = oleaut32.NewProc("SysFreeString")
|
||||||
|
)
|
||||||
|
|
||||||
|
// IOleObject vtable indices.
|
||||||
|
const (
|
||||||
|
idxSetClientSite = 3
|
||||||
|
idxSetHostNames = 5
|
||||||
|
idxOleClose = 6
|
||||||
|
idxDoVerb = 11
|
||||||
|
)
|
||||||
|
|
||||||
|
// IWebBrowser2 vtable indices used for sizing (property setters).
|
||||||
|
const (
|
||||||
|
idxWBPutLeft = 22
|
||||||
|
idxWBPutTop = 24
|
||||||
|
idxWBPutWidth = 26
|
||||||
|
idxWBPutHeight = 28
|
||||||
|
)
|
||||||
|
|
||||||
|
const oleCloseNoSave = 0
|
||||||
|
|
||||||
|
var htmlCounter struct {
|
||||||
|
sync.Mutex
|
||||||
|
n int
|
||||||
|
}
|
||||||
|
|
||||||
|
// embed creates the WebBrowser OLE object and activates it in-place inside the
|
||||||
|
// window. Port of EmbedBrowserObject() from embed-browser.c.
|
||||||
|
func (w *Window) embed() error {
|
||||||
|
var browser uintptr
|
||||||
|
hr, _, _ := procCoCreateInstance.Call(
|
||||||
|
uintptr(unsafe.Pointer(&CLSID_WebBrowser)),
|
||||||
|
0,
|
||||||
|
clsctxInprocServer,
|
||||||
|
uintptr(unsafe.Pointer(&IID_IOleObject)),
|
||||||
|
uintptr(unsafe.Pointer(&browser)),
|
||||||
|
)
|
||||||
|
if hr != 0 || browser == 0 {
|
||||||
|
return errors.New("webview: CoCreateInstance(WebBrowser) failed: 0x" + strconv.FormatUint(uint64(hr), 16))
|
||||||
|
}
|
||||||
|
w.oleObject = browser
|
||||||
|
|
||||||
|
comCall(browser, idxSetClientSite, uintptr(unsafe.Pointer(w.clientSite)))
|
||||||
|
comCall(browser, idxSetHostNames, uintptr(unsafe.Pointer(utf16("Go WebView"))), 0)
|
||||||
|
|
||||||
|
procOleSetContainedObject.Call(browser, 1)
|
||||||
|
|
||||||
|
rc := getClientRect(w.hwnd)
|
||||||
|
comCall(browser, idxDoVerb,
|
||||||
|
oleiVerbShow, // iVerb = OLEIVERB_SHOW (-1)
|
||||||
|
0, // lpmsg
|
||||||
|
uintptr(unsafe.Pointer(w.clientSite)), // active site
|
||||||
|
^uintptr(0), // lindex = -1
|
||||||
|
w.hwnd, // hwndParent
|
||||||
|
uintptr(unsafe.Pointer(&rc)), // lprcPosRect
|
||||||
|
)
|
||||||
|
|
||||||
|
var wb uintptr
|
||||||
|
comCall(browser, 0 /*QueryInterface*/, uintptr(unsafe.Pointer(&IID_IWebBrowser2)), uintptr(unsafe.Pointer(&wb)))
|
||||||
|
if wb == 0 {
|
||||||
|
w.unembed()
|
||||||
|
return errors.New("webview: QueryInterface(IWebBrowser2) failed")
|
||||||
|
}
|
||||||
|
w.browser = wb
|
||||||
|
|
||||||
|
w.resizeBrowser(int(rc.right), int(rc.bottom))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unembed releases the browser. Port of UnEmbedBrowserObject().
|
||||||
|
func (w *Window) unembed() {
|
||||||
|
if w.browser != 0 {
|
||||||
|
release(w.browser)
|
||||||
|
w.browser = 0
|
||||||
|
}
|
||||||
|
if w.oleObject != 0 {
|
||||||
|
comCall(w.oleObject, idxOleClose, oleCloseNoSave)
|
||||||
|
comCall(w.oleObject, idxSetClientSite, 0)
|
||||||
|
release(w.oleObject)
|
||||||
|
w.oleObject = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resizeBrowser stretches the browser over the whole client area. Port of
|
||||||
|
// ResizeBrowser().
|
||||||
|
func (w *Window) resizeBrowser(width, height int) {
|
||||||
|
if w.browser == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
comCall(w.browser, idxWBPutLeft, 0)
|
||||||
|
comCall(w.browser, idxWBPutTop, 0)
|
||||||
|
comCall(w.browser, idxWBPutWidth, uintptr(int32(width)))
|
||||||
|
comCall(w.browser, idxWBPutHeight, uintptr(int32(height)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func sysAllocString(s string) uintptr {
|
||||||
|
b, _, _ := procSysAllocString.Call(uintptr(unsafe.Pointer(utf16(s))))
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func sysFreeString(b uintptr) {
|
||||||
|
if b != 0 {
|
||||||
|
procSysFreeString.Call(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate loads the given URL. Port of the Navigate path of DoPageAction().
|
||||||
|
func (w *Window) Navigate(url string) error {
|
||||||
|
if w.browser == 0 {
|
||||||
|
return errors.New("webview: browser not initialised")
|
||||||
|
}
|
||||||
|
return w.invoke(dispidNavigate, dispatchMethod, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// variant mirrors the Win32 VARIANT structure. Two trailing machine words after
|
||||||
|
// the 8-byte header make it 16 bytes on 386 and 24 bytes on amd64, matching the
|
||||||
|
// platform VARIANT size.
|
||||||
|
type variant struct {
|
||||||
|
vt uint16
|
||||||
|
wReserved1 uint16
|
||||||
|
wReserved2 uint16
|
||||||
|
wReserved3 uint16
|
||||||
|
val uintptr
|
||||||
|
_ uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type dispParams struct {
|
||||||
|
rgvarg uintptr
|
||||||
|
rgdispidNamedArgs uintptr
|
||||||
|
cArgs uint32
|
||||||
|
cNamedArgs uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDispatch / IWebBrowser DISPIDs.
|
||||||
|
const (
|
||||||
|
idxInvoke = 6 // IDispatch::Invoke
|
||||||
|
|
||||||
|
dispatchMethod = 1
|
||||||
|
|
||||||
|
dispidGoBack int32 = 100
|
||||||
|
dispidGoForward int32 = 101
|
||||||
|
dispidRefresh int32 = -550 // DISPID_REFRESH
|
||||||
|
dispidStop int32 = 106
|
||||||
|
dispidNavigate int32 = 104
|
||||||
|
)
|
||||||
|
|
||||||
|
var iidNull GUID
|
||||||
|
|
||||||
|
// invoke calls a method on the IWebBrowser2 IDispatch by DISPID. If a single
|
||||||
|
// string argument is supplied it is passed as a BSTR VARIANT. This is layout
|
||||||
|
// independent and avoids relying on exact IWebBrowser2 vtable slot ordering.
|
||||||
|
func (w *Window) invoke(dispid int32, flags uint16, strArg ...string) error {
|
||||||
|
if w.browser == 0 {
|
||||||
|
return errors.New("webview: browser not initialised")
|
||||||
|
}
|
||||||
|
|
||||||
|
var dp dispParams
|
||||||
|
var arg variant
|
||||||
|
if len(strArg) > 0 {
|
||||||
|
b := sysAllocString(strArg[0])
|
||||||
|
defer sysFreeString(b)
|
||||||
|
arg.vt = 8 // VT_BSTR
|
||||||
|
arg.val = b
|
||||||
|
dp.rgvarg = uintptr(unsafe.Pointer(&arg))
|
||||||
|
dp.cArgs = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
comCall(w.browser, idxInvoke,
|
||||||
|
uintptr(uint32(dispid)),
|
||||||
|
uintptr(unsafe.Pointer(&iidNull)),
|
||||||
|
0, // lcid
|
||||||
|
uintptr(flags),
|
||||||
|
uintptr(unsafe.Pointer(&dp)),
|
||||||
|
0, // pVarResult
|
||||||
|
0, // pExcepInfo
|
||||||
|
0, // puArgErr
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadHTML displays an HTML string. It writes the markup to a temporary file
|
||||||
|
// and navigates to it, which is robust against the asynchronous nature of the
|
||||||
|
// IE control (more reliable than the in-memory document.write path used by
|
||||||
|
// DisplayHTMLStr in the C example).
|
||||||
|
func (w *Window) LoadHTML(html string) error {
|
||||||
|
htmlCounter.Lock()
|
||||||
|
htmlCounter.n++
|
||||||
|
n := htmlCounter.n
|
||||||
|
htmlCounter.Unlock()
|
||||||
|
|
||||||
|
// DOCHOSTUIFLAG_SCROLL_NO is honored on the XP/IE target but ignored by some
|
||||||
|
// newer IE control builds, so for self-authored content we also enforce it
|
||||||
|
// with CSS, which every engine respects.
|
||||||
|
if w.hideScroll {
|
||||||
|
html = "<style>html,body{overflow:hidden !important;}</style>" + html
|
||||||
|
}
|
||||||
|
|
||||||
|
name := filepath.Join(os.TempDir(), "gowebview_"+strconv.Itoa(os.Getpid())+"_"+strconv.Itoa(n)+".html")
|
||||||
|
if err := ioutil.WriteFile(name, []byte(html), 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.tempFiles = append(w.tempFiles, name)
|
||||||
|
return w.Navigate("file:///" + filepath.ToSlash(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFiles registers an in-process URLMon protocol backed by the supplied
|
||||||
|
// bundle and navigates to entry. Nothing is written to disk, no socket is
|
||||||
|
// opened, and relative references between bundled files are preserved.
|
||||||
|
func (w *Window) LoadFiles(files map[string][]byte, entry string) error {
|
||||||
|
registration, bundleURL, err := registerBundleProtocol(files, entry, w.hideScroll)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := w.Navigate(bundleURL); err != nil {
|
||||||
|
registration.close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.protocols = append(w.protocols, registration)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoBack navigates to the previous page.
|
||||||
|
func (w *Window) GoBack() { w.invoke(dispidGoBack, dispatchMethod) }
|
||||||
|
|
||||||
|
// GoForward navigates to the next page.
|
||||||
|
func (w *Window) GoForward() { w.invoke(dispidGoForward, dispatchMethod) }
|
||||||
|
|
||||||
|
// Refresh reloads the current page.
|
||||||
|
func (w *Window) Refresh() { w.invoke(dispidRefresh, dispatchMethod) }
|
||||||
|
|
||||||
|
// Stop aborts the current navigation.
|
||||||
|
func (w *Window) Stop() { w.invoke(dispidStop, dispatchMethod) }
|
||||||
401
src/nati-launch/webview/protocol.go
Normal file
401
src/nati-launch/webview/protocol.go
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
package webview
|
||||||
|
|
||||||
|
// This file implements an asynchronous pluggable protocol (URLMon APP) backed
|
||||||
|
// by Go memory. All APIs used here are present on Windows XP.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bindingStatusMIMETypeAvailable = 13
|
||||||
|
bscfFirstDataNotification = 0x01
|
||||||
|
bscfLastDataNotification = 0x04
|
||||||
|
bscfDataFullyAvailable = 0x08
|
||||||
|
)
|
||||||
|
|
||||||
|
type protocolRegistration struct {
|
||||||
|
session uintptr
|
||||||
|
factory *protocolFactory
|
||||||
|
scheme string
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type protocolFactory struct {
|
||||||
|
vtbl uintptr
|
||||||
|
classID GUID
|
||||||
|
scheme string
|
||||||
|
files map[string][]byte
|
||||||
|
mu sync.Mutex
|
||||||
|
instances map[uintptr]*internetProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
type internetProtocol struct {
|
||||||
|
vtbl uintptr
|
||||||
|
factory *protocolFactory
|
||||||
|
refs int32
|
||||||
|
mu sync.Mutex
|
||||||
|
data []byte
|
||||||
|
position int
|
||||||
|
sink uintptr
|
||||||
|
finished bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
protocolFactoryVtbl []uintptr
|
||||||
|
internetProtocolVtbl []uintptr
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
protocolFactoryVtbl = []uintptr{
|
||||||
|
cb(factoryQueryInterface),
|
||||||
|
cb(factoryAddRef),
|
||||||
|
cb(factoryRelease),
|
||||||
|
cb(factoryCreateInstance),
|
||||||
|
cb(factoryLockServer),
|
||||||
|
}
|
||||||
|
internetProtocolVtbl = []uintptr{
|
||||||
|
cb(protocolQueryInterface),
|
||||||
|
cb(protocolAddRef),
|
||||||
|
cb(protocolRelease),
|
||||||
|
cb(protocolStart),
|
||||||
|
cb(protocolContinue),
|
||||||
|
cb(protocolAbort),
|
||||||
|
cb(protocolTerminate),
|
||||||
|
cb(protocolSuspend),
|
||||||
|
cb(protocolResume),
|
||||||
|
cb(protocolRead),
|
||||||
|
protocolSeekCallback(),
|
||||||
|
cb(protocolLockRequest),
|
||||||
|
cb(protocolUnlockRequest),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerBundleProtocol(files map[string][]byte, entry string, hideScroll bool) (*protocolRegistration, string, error) {
|
||||||
|
entry, ok := cleanBundlePath(entry)
|
||||||
|
if !ok {
|
||||||
|
return nil, "", errors.New("webview: invalid bundle entry path")
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle := make(map[string][]byte, len(files))
|
||||||
|
for name, data := range files {
|
||||||
|
clean, valid := cleanBundlePath(name)
|
||||||
|
if !valid {
|
||||||
|
return nil, "", errors.New("webview: invalid file path in bundle")
|
||||||
|
}
|
||||||
|
copied := append([]byte(nil), data...)
|
||||||
|
if hideScroll && clean == entry {
|
||||||
|
copied = append([]byte("<style>html,body{overflow:hidden !important;}</style>"), copied...)
|
||||||
|
}
|
||||||
|
bundle[clean] = copied
|
||||||
|
}
|
||||||
|
if _, exists := bundle[entry]; !exists {
|
||||||
|
return nil, "", errors.New("webview: bundle entry not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
random := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(random); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
// URLMon limits protocol scheme names to INTERNET_MAX_SCHEME_LENGTH.
|
||||||
|
// Eight random bytes keep the complete name short enough for XP/IE.
|
||||||
|
scheme := "nati" + hex.EncodeToString(random[:8])
|
||||||
|
classID := GUID{
|
||||||
|
Data1: binary.LittleEndian.Uint32(random[0:4]),
|
||||||
|
Data2: binary.LittleEndian.Uint16(random[4:6]),
|
||||||
|
Data3: binary.LittleEndian.Uint16(random[6:8]),
|
||||||
|
}
|
||||||
|
copy(classID.Data4[:], random[8:16])
|
||||||
|
|
||||||
|
factory := &protocolFactory{
|
||||||
|
vtbl: vtblAddr(protocolFactoryVtbl),
|
||||||
|
classID: classID,
|
||||||
|
scheme: scheme,
|
||||||
|
files: bundle,
|
||||||
|
instances: make(map[uintptr]*internetProtocol),
|
||||||
|
}
|
||||||
|
var session uintptr
|
||||||
|
hr, _, _ := procCoInternetGetSession.Call(0, uintptr(unsafe.Pointer(&session)), 0)
|
||||||
|
if hresultFailed(hr) || session == 0 {
|
||||||
|
return nil, "", errors.New("webview: CoInternetGetSession failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = comCall(session, 3,
|
||||||
|
uintptr(unsafe.Pointer(factory)),
|
||||||
|
uintptr(unsafe.Pointer(&factory.classID)),
|
||||||
|
uintptr(unsafe.Pointer(utf16(scheme))),
|
||||||
|
0, 0, 0,
|
||||||
|
)
|
||||||
|
if hresultFailed(hr) {
|
||||||
|
release(session)
|
||||||
|
return nil, "", errors.New("webview: RegisterNameSpace failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
registration := &protocolRegistration{session: session, factory: factory, scheme: scheme}
|
||||||
|
return registration, scheme + "://app/" + escapeBundlePath(entry), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *protocolRegistration) close() {
|
||||||
|
if r == nil || r.closed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.closed = true
|
||||||
|
if r.session != 0 && r.factory != nil {
|
||||||
|
comCall(r.session, 4,
|
||||||
|
uintptr(unsafe.Pointer(r.factory)),
|
||||||
|
uintptr(unsafe.Pointer(utf16(r.scheme))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
release(r.session)
|
||||||
|
r.session = 0
|
||||||
|
r.factory = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func factoryQueryInterface(this, riid, ppv uintptr) uintptr {
|
||||||
|
if ppv == 0 {
|
||||||
|
return ePointer
|
||||||
|
}
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppv)) = 0
|
||||||
|
iid := (*GUID)(unsafe.Pointer(riid))
|
||||||
|
if guidEqual(iid, &IID_IUnknown) || guidEqual(iid, &IID_IClassFactory) {
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppv)) = this
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
return eNoInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func factoryAddRef(this uintptr) uintptr { return 2 }
|
||||||
|
func factoryRelease(this uintptr) uintptr { return 1 }
|
||||||
|
func factoryLockServer(this, lock uintptr) uintptr { return sOK }
|
||||||
|
|
||||||
|
func factoryCreateInstance(this, outer, riid, ppv uintptr) uintptr {
|
||||||
|
if ppv == 0 {
|
||||||
|
return ePointer
|
||||||
|
}
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppv)) = 0
|
||||||
|
if outer != 0 {
|
||||||
|
return classENoAggregation
|
||||||
|
}
|
||||||
|
iid := (*GUID)(unsafe.Pointer(riid))
|
||||||
|
if !guidEqual(iid, &IID_IUnknown) && !guidEqual(iid, &IID_IInternetProtocolRoot) && !guidEqual(iid, &IID_IInternetProtocol) {
|
||||||
|
return eNoInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
factory := (*protocolFactory)(unsafe.Pointer(this))
|
||||||
|
protocol := &internetProtocol{
|
||||||
|
vtbl: vtblAddr(internetProtocolVtbl),
|
||||||
|
factory: factory,
|
||||||
|
refs: 1,
|
||||||
|
}
|
||||||
|
pointer := uintptr(unsafe.Pointer(protocol))
|
||||||
|
factory.mu.Lock()
|
||||||
|
factory.instances[pointer] = protocol
|
||||||
|
factory.mu.Unlock()
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppv)) = pointer
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolQueryInterface(this, riid, ppv uintptr) uintptr {
|
||||||
|
if ppv == 0 {
|
||||||
|
return ePointer
|
||||||
|
}
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppv)) = 0
|
||||||
|
iid := (*GUID)(unsafe.Pointer(riid))
|
||||||
|
if guidEqual(iid, &IID_IUnknown) || guidEqual(iid, &IID_IInternetProtocolRoot) || guidEqual(iid, &IID_IInternetProtocol) {
|
||||||
|
*(*uintptr)(unsafe.Pointer(ppv)) = this
|
||||||
|
protocolAddRef(this)
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
return eNoInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolAddRef(this uintptr) uintptr {
|
||||||
|
protocol := (*internetProtocol)(unsafe.Pointer(this))
|
||||||
|
return uintptr(atomic.AddInt32(&protocol.refs, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolRelease(this uintptr) uintptr {
|
||||||
|
protocol := (*internetProtocol)(unsafe.Pointer(this))
|
||||||
|
refs := atomic.AddInt32(&protocol.refs, -1)
|
||||||
|
if refs <= 0 {
|
||||||
|
protocol.releaseSink()
|
||||||
|
factory := protocol.factory
|
||||||
|
factory.mu.Lock()
|
||||||
|
delete(factory.instances, this)
|
||||||
|
factory.mu.Unlock()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uintptr(refs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolStart(this, rawURL, sink, bindInfo, flags, reserved uintptr) uintptr {
|
||||||
|
protocol := (*internetProtocol)(unsafe.Pointer(this))
|
||||||
|
parsed, err := url.Parse(utf16PtrToString(rawURL))
|
||||||
|
if err != nil || parsed.Scheme != protocol.factory.scheme || !strings.EqualFold(parsed.Host, "app") {
|
||||||
|
protocol.reportFailure(sink, inetEResourceNotFound)
|
||||||
|
return inetEResourceNotFound
|
||||||
|
}
|
||||||
|
name, valid := cleanBundlePath(strings.TrimPrefix(parsed.Path, "/"))
|
||||||
|
data, exists := protocol.factory.files[name]
|
||||||
|
if !valid || !exists {
|
||||||
|
protocol.reportFailure(sink, inetEResourceNotFound)
|
||||||
|
return inetEResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol.mu.Lock()
|
||||||
|
protocol.data = data
|
||||||
|
protocol.position = 0
|
||||||
|
protocol.finished = false
|
||||||
|
protocol.sink = sink
|
||||||
|
if sink != 0 {
|
||||||
|
comCall(sink, 1)
|
||||||
|
}
|
||||||
|
protocol.mu.Unlock()
|
||||||
|
|
||||||
|
if sink != 0 {
|
||||||
|
contentType := bundleContentType(name)
|
||||||
|
comCall(sink, 4, bindingStatusMIMETypeAvailable, uintptr(unsafe.Pointer(utf16(contentType))))
|
||||||
|
size := uintptr(len(data))
|
||||||
|
comCall(sink, 5, bscfFirstDataNotification|bscfLastDataNotification|bscfDataFullyAvailable, size, size)
|
||||||
|
}
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolContinue(this, protocolData uintptr) uintptr { return eNotImpl }
|
||||||
|
|
||||||
|
func protocolAbort(this, reason, options uintptr) uintptr {
|
||||||
|
protocol := (*internetProtocol)(unsafe.Pointer(this))
|
||||||
|
protocol.finish(reason)
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolTerminate(this, options uintptr) uintptr {
|
||||||
|
protocol := (*internetProtocol)(unsafe.Pointer(this))
|
||||||
|
protocol.releaseSink()
|
||||||
|
protocol.mu.Lock()
|
||||||
|
protocol.data = nil
|
||||||
|
protocol.position = 0
|
||||||
|
protocol.mu.Unlock()
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolSuspend(this uintptr) uintptr { return eNotImpl }
|
||||||
|
func protocolResume(this uintptr) uintptr { return eNotImpl }
|
||||||
|
|
||||||
|
func protocolRead(this, destination, count, read uintptr) uintptr {
|
||||||
|
if read != 0 {
|
||||||
|
*(*uint32)(unsafe.Pointer(read)) = 0
|
||||||
|
}
|
||||||
|
protocol := (*internetProtocol)(unsafe.Pointer(this))
|
||||||
|
protocol.mu.Lock()
|
||||||
|
remaining := len(protocol.data) - protocol.position
|
||||||
|
if remaining <= 0 {
|
||||||
|
protocol.mu.Unlock()
|
||||||
|
protocol.finish(sOK)
|
||||||
|
return sFalse
|
||||||
|
}
|
||||||
|
n := int(count)
|
||||||
|
if n > remaining {
|
||||||
|
n = remaining
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
*(*byte)(unsafe.Pointer(destination + uintptr(i))) = protocol.data[protocol.position+i]
|
||||||
|
}
|
||||||
|
protocol.position += n
|
||||||
|
protocol.mu.Unlock()
|
||||||
|
if read != 0 {
|
||||||
|
*(*uint32)(unsafe.Pointer(read)) = uint32(n)
|
||||||
|
}
|
||||||
|
return sOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolLockRequest(this, options uintptr) uintptr { return sOK }
|
||||||
|
func protocolUnlockRequest(this uintptr) uintptr { return sOK }
|
||||||
|
|
||||||
|
func (p *internetProtocol) finish(result uintptr) {
|
||||||
|
p.mu.Lock()
|
||||||
|
if p.finished {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.finished = true
|
||||||
|
sink := p.sink
|
||||||
|
p.mu.Unlock()
|
||||||
|
if sink != 0 {
|
||||||
|
comCall(sink, 6, result, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *internetProtocol) reportFailure(sink, result uintptr) {
|
||||||
|
if sink != 0 {
|
||||||
|
comCall(sink, 6, result, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *internetProtocol) releaseSink() {
|
||||||
|
p.mu.Lock()
|
||||||
|
sink := p.sink
|
||||||
|
p.sink = 0
|
||||||
|
p.mu.Unlock()
|
||||||
|
release(sink)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanBundlePath(name string) (string, bool) {
|
||||||
|
name = strings.Replace(name, "\\", "/", -1)
|
||||||
|
if name == "" || strings.HasPrefix(name, "/") || strings.IndexByte(name, 0) >= 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
clean := path.Clean(name)
|
||||||
|
if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return clean, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeBundlePath(name string) string {
|
||||||
|
parts := strings.Split(name, "/")
|
||||||
|
for i, part := range parts {
|
||||||
|
parts[i] = url.PathEscape(part)
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func bundleContentType(name string) string {
|
||||||
|
switch strings.ToLower(path.Ext(name)) {
|
||||||
|
case ".html", ".htm":
|
||||||
|
return "text/html; charset=utf-8"
|
||||||
|
case ".css":
|
||||||
|
return "text/css; charset=utf-8"
|
||||||
|
case ".js":
|
||||||
|
return "text/javascript; charset=utf-8"
|
||||||
|
case ".json":
|
||||||
|
return "application/json; charset=utf-8"
|
||||||
|
case ".svg":
|
||||||
|
return "image/svg+xml"
|
||||||
|
case ".png":
|
||||||
|
return "image/png"
|
||||||
|
case ".jpg", ".jpeg":
|
||||||
|
return "image/jpeg"
|
||||||
|
case ".gif":
|
||||||
|
return "image/gif"
|
||||||
|
case ".ico":
|
||||||
|
return "image/x-icon"
|
||||||
|
case ".woff":
|
||||||
|
return "font/woff"
|
||||||
|
case ".woff2":
|
||||||
|
return "font/woff2"
|
||||||
|
default:
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hresultFailed(result uintptr) bool { return int32(result) < 0 }
|
||||||
8
src/nati-launch/webview/protocol_386.go
Normal file
8
src/nati-launch/webview/protocol_386.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package webview
|
||||||
|
|
||||||
|
func protocolSeekCallback() uintptr { return cb(protocolSeek) }
|
||||||
|
|
||||||
|
// LARGE_INTEGER is passed as two machine words on 386.
|
||||||
|
func protocolSeek(this, moveLow, moveHigh, origin, newPosition uintptr) uintptr {
|
||||||
|
return eNotImpl
|
||||||
|
}
|
||||||
8
src/nati-launch/webview/protocol_amd64.go
Normal file
8
src/nati-launch/webview/protocol_amd64.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package webview
|
||||||
|
|
||||||
|
func protocolSeekCallback() uintptr { return cb(protocolSeek) }
|
||||||
|
|
||||||
|
// LARGE_INTEGER fits in one machine word on amd64.
|
||||||
|
func protocolSeek(this, move, origin, newPosition uintptr) uintptr {
|
||||||
|
return eNotImpl
|
||||||
|
}
|
||||||
144
src/nati-launch/webview/syscalls.go
Normal file
144
src/nati-launch/webview/syscalls.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package webview
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Low-level WinAPI bindings. Pure stdlib syscall, no external dependencies, so
|
||||||
|
// the package builds on Go 1.10.8 (last toolchain supporting Windows XP).
|
||||||
|
|
||||||
|
var (
|
||||||
|
user32 = syscall.NewLazyDLL("user32.dll")
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
ole32 = syscall.NewLazyDLL("ole32.dll")
|
||||||
|
urlmon = syscall.NewLazyDLL("urlmon.dll")
|
||||||
|
|
||||||
|
procRegisterClassExW = user32.NewProc("RegisterClassExW")
|
||||||
|
procCreateWindowExW = user32.NewProc("CreateWindowExW")
|
||||||
|
procDestroyWindow = user32.NewProc("DestroyWindow")
|
||||||
|
procDefWindowProcW = user32.NewProc("DefWindowProcW")
|
||||||
|
procShowWindow = user32.NewProc("ShowWindow")
|
||||||
|
procUpdateWindow = user32.NewProc("UpdateWindow")
|
||||||
|
procGetMessageW = user32.NewProc("GetMessageW")
|
||||||
|
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||||||
|
procDispatchMessageW = user32.NewProc("DispatchMessageW")
|
||||||
|
procPostQuitMessage = user32.NewProc("PostQuitMessage")
|
||||||
|
procPostMessageW = user32.NewProc("PostMessageW")
|
||||||
|
procGetClientRect = user32.NewProc("GetClientRect")
|
||||||
|
procMoveWindow = user32.NewProc("MoveWindow")
|
||||||
|
procSetWindowTextW = user32.NewProc("SetWindowTextW")
|
||||||
|
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
||||||
|
procSetWindowPos = user32.NewProc("SetWindowPos")
|
||||||
|
procLoadCursorW = user32.NewProc("LoadCursorW")
|
||||||
|
|
||||||
|
procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
|
||||||
|
|
||||||
|
procOleInitialize = ole32.NewProc("OleInitialize")
|
||||||
|
procOleUninitialize = ole32.NewProc("OleUninitialize")
|
||||||
|
procCoCreateInstance = ole32.NewProc("CoCreateInstance")
|
||||||
|
procOleSetContainedObject = ole32.NewProc("OleSetContainedObject")
|
||||||
|
procCoInternetGetSession = urlmon.NewProc("CoInternetGetSession")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Win32 constants.
|
||||||
|
const (
|
||||||
|
wsOverlappedWindow = 0x00CF0000
|
||||||
|
wsThickFrame = 0x00040000
|
||||||
|
wsMaximizeBox = 0x00010000
|
||||||
|
wsVisible = 0x10000000
|
||||||
|
wsClipChildren = 0x02000000
|
||||||
|
|
||||||
|
swShow = 5
|
||||||
|
swHide = 0
|
||||||
|
|
||||||
|
wmDestroy = 0x0002
|
||||||
|
wmSize = 0x0005
|
||||||
|
wmClose = 0x0010
|
||||||
|
|
||||||
|
cwUseDefault = 0x80000000
|
||||||
|
|
||||||
|
smCXScreen = 0
|
||||||
|
smCYScreen = 1
|
||||||
|
|
||||||
|
idcArrow = 32512
|
||||||
|
|
||||||
|
csHRedraw = 0x0002
|
||||||
|
csVRedraw = 0x0001
|
||||||
|
|
||||||
|
swpNoZOrder = 0x0004
|
||||||
|
swpNoActivate = 0x0010
|
||||||
|
swpNoMove = 0x0002
|
||||||
|
|
||||||
|
clsctxInprocServer = 0x1
|
||||||
|
clsctxInprocAll = 0x3
|
||||||
|
|
||||||
|
oleiVerbShow = ^uintptr(0) // OLEIVERB_SHOW = -1
|
||||||
|
)
|
||||||
|
|
||||||
|
// wndClassExW mirrors the Win32 WNDCLASSEXW structure.
|
||||||
|
type wndClassExW struct {
|
||||||
|
cbSize uint32
|
||||||
|
style uint32
|
||||||
|
lpfnWndProc uintptr
|
||||||
|
cbClsExtra int32
|
||||||
|
cbWndExtra int32
|
||||||
|
hInstance uintptr
|
||||||
|
hIcon uintptr
|
||||||
|
hCursor uintptr
|
||||||
|
hbrBackground uintptr
|
||||||
|
lpszMenuName *uint16
|
||||||
|
lpszClassName *uint16
|
||||||
|
hIconSm uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type msg struct {
|
||||||
|
hwnd uintptr
|
||||||
|
message uint32
|
||||||
|
wParam uintptr
|
||||||
|
lParam uintptr
|
||||||
|
time uint32
|
||||||
|
pt point
|
||||||
|
}
|
||||||
|
|
||||||
|
type point struct {
|
||||||
|
x, y int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type rect struct {
|
||||||
|
left, top, right, bottom int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func getModuleHandle() uintptr {
|
||||||
|
r, _, _ := procGetModuleHandleW.Call(0)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCursor(id uintptr) uintptr {
|
||||||
|
r, _, _ := procLoadCursorW.Call(0, id)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func defWindowProc(hwnd uintptr, m uint32, w, l uintptr) uintptr {
|
||||||
|
r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(m), w, l)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClientRect(hwnd uintptr) rect {
|
||||||
|
var rc rect
|
||||||
|
procGetClientRect.Call(hwnd, uintptr(unsafe.Pointer(&rc)))
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystemMetric(index int) int {
|
||||||
|
r, _, _ := procGetSystemMetrics.Call(uintptr(index))
|
||||||
|
return int(int32(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func utf16(s string) *uint16 {
|
||||||
|
p, err := syscall.UTF16PtrFromString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
263
src/nati-launch/webview/webview.go
Normal file
263
src/nati-launch/webview/webview.go
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
// Package webview embeds the Internet Explorer WebBrowser control in a native
|
||||||
|
// Win32 window using nothing but the standard library's syscall package. It is
|
||||||
|
// a Go port of the classic embed-browser.c example and targets Go 1.10.8 /
|
||||||
|
// Windows XP (build with GOARCH=386).
|
||||||
|
//
|
||||||
|
// The window is a "solid" webview: the browser fills the entire client area and
|
||||||
|
// there are no separate Win32 child controls.
|
||||||
|
package webview
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options configures a Window created by New.
|
||||||
|
type Options struct {
|
||||||
|
Title string // window caption
|
||||||
|
Width int // client width; 0 -> 1024
|
||||||
|
Height int // client height; 0 -> 768
|
||||||
|
X, Y int // top-left position; 0,0 -> centered on screen
|
||||||
|
URL string // if set, navigated to on creation
|
||||||
|
HTML string // if set, displayed on creation (takes precedence over URL)
|
||||||
|
Resizable bool // allow the user to resize the window
|
||||||
|
DisableMenu bool // suppress the default IE right-click context menu
|
||||||
|
HideScrollbars bool // hide the page scrollbars (content is clipped, not scrollable)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window is a native window hosting the WebBrowser control.
|
||||||
|
type Window struct {
|
||||||
|
hwnd uintptr
|
||||||
|
oleObject uintptr // IOleObject*
|
||||||
|
browser uintptr // IWebBrowser2*
|
||||||
|
|
||||||
|
clientSite *oleClientSite
|
||||||
|
inPlaceSite *oleInPlaceSite
|
||||||
|
inPlaceFrame *oleInPlaceFrame
|
||||||
|
docHost *docHostUIHandler
|
||||||
|
external *externalDispatch
|
||||||
|
|
||||||
|
disableMenu bool
|
||||||
|
hideScroll bool
|
||||||
|
tempFiles []string
|
||||||
|
protocols []*protocolRegistration
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
bindings map[string]func([]string) string
|
||||||
|
bindIDs map[string]int32
|
||||||
|
idNames map[int32]string
|
||||||
|
nextID int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind exposes a Go function to JavaScript as window.external.<name>(...).
|
||||||
|
// Arguments from JS are passed as strings; the returned string is given back to
|
||||||
|
// JS as the call result. Bind before navigating to a page that uses it.
|
||||||
|
func (w *Window) Bind(name string, fn func(args []string) string) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
id, ok := w.bindIDs[name]
|
||||||
|
if !ok {
|
||||||
|
w.nextID++
|
||||||
|
id = w.nextID
|
||||||
|
w.bindIDs[name] = id
|
||||||
|
w.idNames[id] = name
|
||||||
|
}
|
||||||
|
w.bindings[name] = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = "GoWebViewWindow"
|
||||||
|
|
||||||
|
var (
|
||||||
|
registryMu sync.Mutex
|
||||||
|
windows = make(map[uintptr]*Window)
|
||||||
|
|
||||||
|
classOnce sync.Once
|
||||||
|
wndProcPtr uintptr
|
||||||
|
oleOnce sync.Once
|
||||||
|
oleOK bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func loword(v uintptr) int { return int(v & 0xffff) }
|
||||||
|
func hiword(v uintptr) int { return int((v >> 16) & 0xffff) }
|
||||||
|
|
||||||
|
func wndProc(hwnd, m, wParam, lParam uintptr) uintptr {
|
||||||
|
switch uint32(m) {
|
||||||
|
case wmSize:
|
||||||
|
registryMu.Lock()
|
||||||
|
w := windows[hwnd]
|
||||||
|
registryMu.Unlock()
|
||||||
|
if w != nil {
|
||||||
|
w.resizeBrowser(loword(lParam), hiword(lParam))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
|
||||||
|
case wmDestroy:
|
||||||
|
registryMu.Lock()
|
||||||
|
w := windows[hwnd]
|
||||||
|
delete(windows, hwnd)
|
||||||
|
registryMu.Unlock()
|
||||||
|
if w != nil {
|
||||||
|
w.unembed()
|
||||||
|
w.cleanup()
|
||||||
|
}
|
||||||
|
procPostQuitMessage.Call(0)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return defWindowProc(hwnd, uint32(m), wParam, lParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerClass() {
|
||||||
|
wndProcPtr = cb(wndProc)
|
||||||
|
wc := wndClassExW{
|
||||||
|
style: csHRedraw | csVRedraw,
|
||||||
|
lpfnWndProc: wndProcPtr,
|
||||||
|
hInstance: getModuleHandle(),
|
||||||
|
hCursor: loadCursor(idcArrow),
|
||||||
|
hbrBackground: 6, // COLOR_WINDOW+1
|
||||||
|
lpszClassName: utf16(className),
|
||||||
|
}
|
||||||
|
wc.cbSize = uint32(unsafe.Sizeof(wc))
|
||||||
|
procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a window, embeds the browser, and (optionally) navigates. It must
|
||||||
|
// be called from the same goroutine that will later call Run, because the IE
|
||||||
|
// control requires a single-threaded COM apartment bound to one OS thread.
|
||||||
|
func New(opts Options) (*Window, error) {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
|
||||||
|
oleOnce.Do(func() {
|
||||||
|
hr, _, _ := procOleInitialize.Call(0)
|
||||||
|
oleOK = hr == 0 || hr == 1 // S_OK or S_FALSE (already initialised)
|
||||||
|
})
|
||||||
|
if !oleOK {
|
||||||
|
return nil, errors.New("webview: OleInitialize failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
classOnce.Do(registerClass)
|
||||||
|
|
||||||
|
if opts.Width <= 0 {
|
||||||
|
opts.Width = 1024
|
||||||
|
}
|
||||||
|
if opts.Height <= 0 {
|
||||||
|
opts.Height = 768
|
||||||
|
}
|
||||||
|
|
||||||
|
style := uintptr(wsOverlappedWindow | wsClipChildren)
|
||||||
|
if !opts.Resizable {
|
||||||
|
style &^= wsThickFrame | wsMaximizeBox
|
||||||
|
}
|
||||||
|
|
||||||
|
x, y := uintptr(opts.X), uintptr(opts.Y)
|
||||||
|
if opts.X == 0 && opts.Y == 0 {
|
||||||
|
x = uintptr(int32((getSystemMetric(smCXScreen) - opts.Width) / 2))
|
||||||
|
y = uintptr(int32((getSystemMetric(smCYScreen) - opts.Height) / 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &Window{
|
||||||
|
disableMenu: opts.DisableMenu,
|
||||||
|
hideScroll: opts.HideScrollbars,
|
||||||
|
bindings: make(map[string]func([]string) string),
|
||||||
|
bindIDs: make(map[string]int32),
|
||||||
|
idNames: make(map[int32]string),
|
||||||
|
}
|
||||||
|
w.clientSite = &oleClientSite{vtbl: vtblAddr(clientSiteVtbl), w: w}
|
||||||
|
w.inPlaceSite = &oleInPlaceSite{vtbl: vtblAddr(inPlaceSiteVtbl), w: w}
|
||||||
|
w.inPlaceFrame = &oleInPlaceFrame{vtbl: vtblAddr(inPlaceFrameVtbl), w: w}
|
||||||
|
w.docHost = &docHostUIHandler{vtbl: vtblAddr(docHostVtbl), w: w}
|
||||||
|
w.external = &externalDispatch{vtbl: vtblAddr(externalVtbl), w: w}
|
||||||
|
|
||||||
|
hwnd, _, _ := procCreateWindowExW.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(utf16(className))),
|
||||||
|
uintptr(unsafe.Pointer(utf16(opts.Title))),
|
||||||
|
style,
|
||||||
|
x, y,
|
||||||
|
uintptr(int32(opts.Width)), uintptr(int32(opts.Height)),
|
||||||
|
0, 0,
|
||||||
|
getModuleHandle(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if hwnd == 0 {
|
||||||
|
return nil, errors.New("webview: CreateWindowEx failed")
|
||||||
|
}
|
||||||
|
w.hwnd = hwnd
|
||||||
|
|
||||||
|
registryMu.Lock()
|
||||||
|
windows[hwnd] = w
|
||||||
|
registryMu.Unlock()
|
||||||
|
|
||||||
|
if err := w.embed(); err != nil {
|
||||||
|
procDestroyWindow.Call(hwnd)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.HTML != "" {
|
||||||
|
w.LoadHTML(opts.HTML)
|
||||||
|
} else if opts.URL != "" {
|
||||||
|
w.Navigate(opts.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run pumps the Win32 message loop until the window is closed. Blocks the
|
||||||
|
// calling goroutine.
|
||||||
|
func (w *Window) Run() {
|
||||||
|
var m msg
|
||||||
|
for {
|
||||||
|
r, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
|
||||||
|
if int32(r) <= 0 { // 0 = WM_QUIT, -1 = error
|
||||||
|
break
|
||||||
|
}
|
||||||
|
procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
|
||||||
|
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show makes the window visible.
|
||||||
|
func (w *Window) Show() {
|
||||||
|
procShowWindow.Call(w.hwnd, swShow)
|
||||||
|
procUpdateWindow.Call(w.hwnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide hides the window.
|
||||||
|
func (w *Window) Hide() { procShowWindow.Call(w.hwnd, swHide) }
|
||||||
|
|
||||||
|
// SetTitle changes the window caption.
|
||||||
|
func (w *Window) SetTitle(title string) {
|
||||||
|
procSetWindowTextW.Call(w.hwnd, uintptr(unsafe.Pointer(utf16(title))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize sets the window size (in pixels).
|
||||||
|
func (w *Window) Resize(width, height int) {
|
||||||
|
procSetWindowPos.Call(w.hwnd, 0, 0, 0, uintptr(int32(width)), uintptr(int32(height)), swpNoZOrder|swpNoMove|swpNoActivate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center positions the window in the middle of the screen.
|
||||||
|
func (w *Window) Center() {
|
||||||
|
rc := getClientRect(w.hwnd)
|
||||||
|
cw, ch := int(rc.right), int(rc.bottom)
|
||||||
|
x := (getSystemMetric(smCXScreen) - cw) / 2
|
||||||
|
y := (getSystemMetric(smCYScreen) - ch) / 2
|
||||||
|
procSetWindowPos.Call(w.hwnd, 0, uintptr(int32(x)), uintptr(int32(y)), 0, 0, swpNoZOrder|swpNoActivate|0x0001 /*SWP_NOSIZE*/)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close requests the window to close (triggers WM_DESTROY and ends Run).
|
||||||
|
func (w *Window) Close() {
|
||||||
|
procPostMessageW.Call(w.hwnd, wmClose, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Window) cleanup() {
|
||||||
|
for _, f := range w.tempFiles {
|
||||||
|
os.Remove(f)
|
||||||
|
}
|
||||||
|
for _, registration := range w.protocols {
|
||||||
|
registration.close()
|
||||||
|
}
|
||||||
|
w.tempFiles = nil
|
||||||
|
w.protocols = nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user