├── GoUI.iml ├── LICENSE ├── README.md ├── config.go ├── config_prod.go ├── context.go ├── example ├── chatbot │ ├── main.go │ ├── screenshots │ │ ├── chatbot-mac.png │ │ └── chatbot-ubuntu.jpeg │ └── ui │ │ ├── css │ │ └── chat.css │ │ ├── index.html │ │ └── js │ │ ├── goui.js │ │ ├── index.js │ │ └── jquery.js ├── filepicker │ ├── filepicker │ ├── main.go │ └── ui │ │ ├── css │ │ └── index.css │ │ ├── index.html │ │ └── js │ │ ├── g-filepicker.js │ │ ├── goui.js │ │ ├── index.js │ │ └── jquery.js ├── hello │ ├── main.go │ └── ui │ │ ├── css │ │ └── index.css │ │ ├── img │ │ └── goui.png │ │ ├── index.html │ │ └── js │ │ ├── goui.js │ │ └── index.js └── texteditor │ ├── main.go │ ├── texteditor │ └── ui │ ├── css │ └── editor.css │ ├── index.html │ └── js │ └── editor.js ├── file ├── file_android.go ├── file_iOS.go ├── file_linux.go ├── file_macOS.go └── filepicker_macOS.go ├── goui.go ├── goui_test.go ├── js └── goui.js ├── log.go ├── menu.go ├── menu.h ├── menu_macos.c ├── menu_macos.go ├── menu_macos.h ├── menu_test.go ├── platform.go ├── provider.go ├── provider.h ├── provider_android.go ├── provider_android_invoke.go ├── provider_android_invoke ├── invoke.go ├── invoke_386.s ├── invoke_amd64.s ├── invoke_arm.s └── invoke_arm64.s ├── provider_ios.go ├── provider_linux.go ├── provider_macos.go ├── provider_windows.cpp ├── provider_windows.go ├── provider_windows.h ├── provider_windows_mock.go ├── request.go ├── router.go ├── router_test.go ├── screenshots ├── debug-go.jpeg ├── debug-web-android.jpeg ├── debug-web.png ├── editor-mac.png ├── editor-ubuntu.jpeg ├── hello-android.jpeg ├── hello-ios.jpeg ├── hello-mac.jpeg └── hello-ubuntu.jpeg ├── storage.go ├── util.c ├── util.h ├── util_cocoa.h ├── widget └── filepicker │ ├── filepicker.go │ ├── filepicker_macOS.go │ └── js │ └── g-filepicker.js ├── window.go ├── window.h ├── window_macos.go └── window_macos.h /GoUI.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 FIPress 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoUI 2 | 3 | > We are currently refactoring the project, and it is not ready to use. 4 | 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/FIPress/GoUI)](https://goreportcard.com/report/github.com/FIPress/GoUI) 6 | 7 | [GoUI](https://fipress.org/project/goui) is a lightweight cross-platform Go "GUI" framework. It is not really a *GUI* library, instead, it displays things through web view. 8 | That is, you can write cross-platform applications using JavaScript, HTML and CSS, and Go of cause. 9 | 10 | The basic idea is: 11 | 1. Write your app with HTML, JavaScript, and CSS like it is a single page web application. 12 | You may adopt whatever javascript framework you like, angular, backbone, etc. 13 | 2. Wrap the web application to desktop or mobile application. 14 | 3. Plus, write some backend services with Go, then your Javascript code can access them like make an AJAX request 15 | 4. Plus plus, you may also write some frontend services with Javascript for your Go code to access. 16 | 17 | That's how GoUI came from. And now, it has some unique merits compares to other library. 18 | 19 | 1. It provides two way bindings between Go and Javascript. Both sides can register services for the other side to access. 20 | 21 | 2. Your application won't get too large because you don't need to include Chromium or any other browser into your package, since it uses Cocoa/WebKit for macOS, MSHTML for Windows and Gtk/WebKit for Linux. 22 | The macOS package "hello.app" of example "hello" only takes **2.6MB**. 23 | 24 | 3. It is extremely easy To Use. The api is super simple, and intentionally designed adapts to web developers. Register a service is like registering a REST service and request one is like making a jquery AJAX call. 25 | 26 | 4. It is powerful, not because itself but because it brings two powerful tools together. 27 | 28 | - **The UI** You can use any frontend technologies to make your web page shinning. And conveniently, you can open the Web Inspector to help you. 29 | 30 | - **The Logic** With Javascript + Go, you can do anything. 31 | 32 | 33 | ## Basic Usage 34 | ### The backend - Go 35 | 1. Install the package by `go get` 36 | ``` 37 | go get github.com/fipress/GoUI 38 | ``` 39 | 40 | 2. import the package 41 | ``` 42 | import ( 43 | "github.com/fipress/GoUI" 44 | ) 45 | ``` 46 | 47 | 3. Register services with `goui.Service`, for Javascript to access 48 | ``` 49 | goui.Service("hello", func(context *goui.Context) { 50 | context.Success("Hello world!") 51 | }) 52 | ``` 53 | 54 | 4. Create window with `goui.Create` 55 | ``` 56 | goui.Create(goui.Settings{Title: "Hello", 57 | Left: 200, 58 | Top: 50, 59 | Width: 400, 60 | Height: 510, 61 | Resizable: true, 62 | Debug: true}) 63 | ``` 64 | 65 | ### The frontend - Javascript 66 | 1. add `goui.js` in your web page 67 | 68 | 2. request backend services 69 | ``` 70 | goui.request({url: "hello", 71 | success: function(data) { 72 | document.getElementById("result").innerText = data; 73 | }}); 74 | ``` 75 | 76 | ### The backend can access frontend services 77 | 78 | 1. The frontend register services with `goui.service` 79 | ``` 80 | goui.service("chat/:msg",function() { 81 | console.log(msg); 82 | }) 83 | ``` 84 | 85 | 2. The backend invoke frontend service by `goui.RequestJSService` 86 | ``` 87 | goui.RequestJSService(goui.JSServiceOptions{ 88 | Url: "chat/"+msg, 89 | }) 90 | ``` 91 | 92 | ### Menu 93 | It is extremely easy to create menu with GoUI: just define your menuDefs, and create window with them, 94 | 95 | ``` 96 | menuDefs = []goui.MenuDef{{Title: "File", Type: goui.Container, Children: []goui.MenuDef{ 97 | {Title: "New", Type: goui.Custom, Action: "new", Handler: func() { 98 | println("new file") 99 | }}, 100 | {Title: "Open", Type: goui.Custom, Action: "open"}, 101 | {Type: goui.Separator}, 102 | {Title: "Quit", Type: goui.Standard, Action: "quit"}, 103 | ... 104 | } 105 | } 106 | 107 | goui.CreateWithMenu(settings,menuDefs) 108 | ``` 109 | 110 | For a complete demonstration, please check out the example "texteditor". 111 | 112 | ## Debugging and packaging 113 | ### Debugging 114 | I personally recommend [GoLand](https://www.jetbrains.com/go) to debug Go code, or IntelliJ IDEA which contains GoLand. You may need to set the output directory to your working folder so that your debugging binary can read the web folder. Or other solution to make sure the binary can read the page you provided. 115 | 116 | To debug javascript code or check web page elements, you should set `Debug` settings to `true`, then you can open the inspector from the context menu. 117 | 118 | ![Inspector](https://github.com/FIPress/GoUI/raw/master/screenshots/debug-web.png) 119 | 120 | **Note** 121 | You may use remote debugging of Safari or Chrome to debug iOS or Android web page. 122 | 123 | ![Inspector](https://github.com/FIPress/GoUI/raw/master/screenshots/debug-web-android.png) 124 | 125 | ### Packaging 126 | The easiest way to package GoUI applications would be through [GoUI-CLI](https://github.com/FIPress/GoUI-CLI), which will packaging native applications for all the supported platforms, macOS, Ubuntu, Windows, iOS and Android. 127 | 128 | ## Examples 129 | Under the `example` directory, there are some examples for you to get started with. 130 | 131 | ### hello 132 | This is the GoUI version of "Hello world". It's pretty much a starting point for you to go, so let's take a look at it in detail. 133 | 134 | The project only contains 3 files. 135 | 136 | ``` 137 | ├─ui 138 | | ├─css 139 | | | └─index.css 140 | | ├─img 141 | | | └─goui.png 142 | | ├─js 143 | | | └─goui.js 144 | | | └─index.js 145 | | └─index.html 146 | └─main.go 147 | ``` 148 | 149 | `goui.js` - The frontend library GoUI provides for you. 150 | 151 | `main.go` - provides a service named `hello`, and then create a window. 152 | ``` 153 | goui.Service("hello", func(context *goui.Context) { 154 | context.Success("Hello world!") 155 | }) 156 | 157 | //create and open a window 158 | goui.Create(goui.Settings{Title: "Hello", 159 | Url: "./ui/hello.html", 160 | Left: 20, 161 | Top: 30, 162 | Width: 300, 163 | Height: 200, 164 | Resizable: true, 165 | Debug: true}) 166 | ``` 167 | 168 | `index.html` - has a button on it, if a user clicks the button, it will request the above `hello` service. 169 | `index.js` - frontend logic of `index.html`. 170 | ``` 171 | goui.request({url: "hello", 172 | success: function(data) { 173 | document.getElementById("result").innerText = data; 174 | }}); 175 | ``` 176 | 177 | Here are some screenshots. 178 | 179 | macOS 180 | ![hello-macOS](https://github.com/FIPress/GoUI/raw/master/screenshots/hello-mac.jpeg) 181 | 182 | Ubuntu 183 | ![hello-ubuntu](https://github.com/FIPress/GoUI/raw/master/screenshots/hello-ubuntu.jpeg) 184 | 185 | iOS 186 | ![hello-ios](https://github.com/FIPress/GoUI/raw/master/screenshots/hello-ios.jpeg) 187 | 188 | Android 189 | ![hello-android](https://github.com/FIPress/GoUI/raw/master/screenshots/hello-android.jpeg) 190 | 191 | ### chat 192 | Demonstrate how backend requests frontend service. 193 | 194 | ### texteditor 195 | Demonstrate how to setup menu for desktop applications, on macOS, ubuntu and windows. 196 | 197 | macOS 198 | ![editor-macOS](https://github.com/FIPress/GoUI/raw/master/screenshots/editor-mac.png) 199 | 200 | Ubuntu 201 | ![editor-ubuntu](https://github.com/FIPress/GoUI/raw/master/screenshots/editor-ubuntu.jpeg) 202 | 203 | ## Progress 204 | Currently, the basic functions are working on macOS, Ubuntu, iOS and Android. We are working on Windows now, and it will be done soon. Then we will start adding features. If you have any suggestion, please don't hesitate to tell us. -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fipress/go-rj" 6 | ) 7 | 8 | var configFile = "config/dev.rj" 9 | 10 | type config struct { 11 | *rj.Node 12 | } 13 | 14 | var Config = initConfig() 15 | 16 | func initConfig() *config { 17 | cfg := new(config) 18 | 19 | var err error 20 | cfg.Node, err = rj.Load(configFile) 21 | 22 | if err != nil { 23 | fmt.Println("Open config file failed, filename:", configFile, ", error:", err.Error()) 24 | } 25 | 26 | return cfg 27 | } 28 | -------------------------------------------------------------------------------- /config_prod.go: -------------------------------------------------------------------------------- 1 | // +build prod 2 | 3 | package goui 4 | 5 | func init() { 6 | configFile = "./config/prod.rj" 7 | } 8 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "strconv" 8 | ) 9 | 10 | type Context struct { 11 | Data string `json:"data"` 12 | SuccessCallback string `json:"success"` 13 | ErrorCallback string `json:"error"` 14 | params map[string]string 15 | } 16 | 17 | // GetParam get a string parameter from the url 18 | func (ctx *Context) GetParam(name string) (val string) { 19 | val, _ = url.PathUnescape(ctx.params[name]) 20 | return 21 | } 22 | 23 | // GetBoolParam get a bool parameter from the url 24 | func (ctx *Context) GetBoolParam(name string) (b bool, err error) { 25 | str := ctx.GetParam(name) 26 | b, err = strconv.ParseBool(str) 27 | if err != nil { 28 | Log("convert data to bool failed:", err) 29 | } 30 | return 31 | } 32 | 33 | // GetIntParam get a int parameter from the url 34 | func (ctx *Context) GetIntParam(name string) (i int, err error) { 35 | str := ctx.GetParam(name) 36 | i, err = strconv.Atoi(str) 37 | if err != nil { 38 | Log("convert data to int failed:", err) 39 | } 40 | return 41 | } 42 | 43 | func (ctx *Context) GetIntParamOr(name string, defaultVal int) (i int) { 44 | str := ctx.GetParam(name) 45 | var err error 46 | i, err = strconv.Atoi(str) 47 | if err != nil { 48 | i = defaultVal 49 | } 50 | return 51 | 52 | } 53 | 54 | // GetFloatParam get a float parameter from the url 55 | func (ctx *Context) GetFloatParam(name string) (f float64, err error) { 56 | str := ctx.GetParam(name) 57 | f, err = strconv.ParseFloat(str, 32) 58 | if err != nil { 59 | Log("convert data to float failed:", err) 60 | } 61 | return 62 | } 63 | 64 | // GetParam get an entity from the requested data 65 | func (ctx *Context) GetEntity(v interface{}) (err error) { 66 | err = json.Unmarshal([]byte(ctx.Data), v) 67 | if err != nil { 68 | Log("get entity failed:", err) 69 | } 70 | return 71 | } 72 | 73 | func (ctx *Context) Success(feedback interface{}) { 74 | if ctx.SuccessCallback != "" { 75 | if feedback == nil { 76 | invokeJS(ctx.SuccessCallback+"()", true) 77 | } else { 78 | invokeJS(fmt.Sprintf("%s(\"%v\")", ctx.SuccessCallback, feedback), true) 79 | } 80 | } 81 | } 82 | 83 | func (ctx *Context) Error(err interface{}) { 84 | if ctx.ErrorCallback != "" { 85 | if err == nil { 86 | invokeJS(ctx.ErrorCallback+"()", true) 87 | } else { 88 | invokeJS(fmt.Sprintf("%s('%v')", ctx.ErrorCallback, err), true) 89 | } 90 | } 91 | } 92 | 93 | func (ctx *Context) Ok() { 94 | ctx.Success(nil) 95 | } 96 | 97 | func (ctx *Context) Fail() { 98 | ctx.Error(nil) 99 | } 100 | -------------------------------------------------------------------------------- /example/chatbot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fipress/GoUI" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | const ( 10 | greetings = "Hello, I'm your pal B. How may I help you today?" 11 | boast = "You can ask me anything. There is nothing I don't know." 12 | truth = "Uh-oh~~~ you got me! I'm just a dummy robot." 13 | knock = "Knock knock..." 14 | please = "Say something please..." 15 | ) 16 | 17 | var timer *time.Timer 18 | 19 | func main() { 20 | goui.Service("chat/:msg", chatService) 21 | timer = time.NewTimer(time.Minute) 22 | resetTimer() 23 | goui.Create(goui.Settings{Title: "Chatbot", Top: 30, Left: 100, Width: 300, Height: 440}) 24 | } 25 | 26 | func resetTimer() { 27 | timer.Reset(time.Minute) 28 | 29 | go func() { 30 | select { 31 | case <-timer.C: 32 | println("timeout") 33 | goui.RequestJSServiceFromBackground(goui.JSServiceOptions{ 34 | Url: "chat/" + knock, 35 | }) 36 | resetTimer() 37 | } 38 | }() 39 | } 40 | 41 | func chatService(ctx *goui.Context) { 42 | msg := ctx.GetParam("msg") 43 | resetTimer() 44 | msg = strings.ToLower(msg) 45 | switch { 46 | case msg == "": 47 | ctx.Success(please) 48 | case msg == "ready": 49 | ctx.Success(greetings) 50 | goui.RequestJSService(goui.JSServiceOptions{ 51 | Url: "chat/" + greetings, 52 | }) 53 | case strings.HasSuffix(msg, "?"): 54 | ctx.Success(truth) 55 | default: 56 | ctx.Success(boast) 57 | } 58 | } 59 | 60 | /* 61 | func receive(msg string) { 62 | resetTimer() 63 | msg = strings.ToLower(msg) 64 | switch { 65 | case msg == "": 66 | send(please) 67 | case msg == "ready": 68 | send(greetings) 69 | case strings.HasSuffix(msg, "?"): 70 | send(truth) 71 | default: 72 | send(boast) 73 | } 74 | } 75 | 76 | func send(msg string) { 77 | goui.RequestJSService(goui.JSServiceOptions{ 78 | Url: "chat/" + msg, 79 | }) 80 | }*/ 81 | -------------------------------------------------------------------------------- /example/chatbot/screenshots/chatbot-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/example/chatbot/screenshots/chatbot-mac.png -------------------------------------------------------------------------------- /example/chatbot/screenshots/chatbot-ubuntu.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/example/chatbot/screenshots/chatbot-ubuntu.jpeg -------------------------------------------------------------------------------- /example/chatbot/ui/css/chat.css: -------------------------------------------------------------------------------- 1 | body { 2 | box-sizing: border-box; 3 | color: #555555; 4 | margin:0; 5 | } 6 | 7 | #box { 8 | width: 300px; 9 | border: 1px #999999 solid; 10 | border-radius: 6px; 11 | background-color: #efefde; 12 | } 13 | 14 | #head { 15 | padding: 8px; 16 | border-top-left-radius: 6px; 17 | border-top-right-radius: 6px; 18 | border-bottom: 1px #aaaaaa solid; 19 | text-align: center; 20 | } 21 | 22 | #chat { 23 | width:100%; 24 | height: 363px; 25 | padding: 6px; 26 | box-sizing: border-box; 27 | } 28 | 29 | #input { 30 | width: 100%; 31 | height: 40px; 32 | margin: 0; 33 | padding: 5px; 34 | border: none; 35 | resize: none; 36 | border-top: 1px #aaaaaa solid; 37 | box-sizing: border-box; 38 | background: inherit; 39 | outline: none; 40 | font-size: inherit; 41 | color: inherit; 42 | border-bottom-left-radius: 6px; 43 | border-bottom-right-radius: 6px; 44 | } 45 | 46 | .bot, .user { 47 | padding: 5px 0; 48 | display: grid; 49 | } 50 | 51 | .bot { 52 | //float: left; 53 | width: 100%; 54 | grid-template-columns: 30px 220px; 55 | } 56 | 57 | .msg { 58 | padding: 5px; 59 | border-radius: 4px; 60 | //max-width: 240px; 61 | display: inline-block; 62 | } 63 | 64 | .user { 65 | //float: right; 66 | grid-template-columns: auto 220px 30px; 67 | 68 | } 69 | 70 | .bot .msg { 71 | background-color: #ffffee; 72 | //margin-left: 5px; 73 | } 74 | 75 | .user .msg { 76 | background-color: #7fff00; 77 | //margin-right: 5px; 78 | float: right; 79 | } 80 | 81 | .avatar { 82 | display: inline-block; 83 | padding: 2px 5px; 84 | border-radius: 10px; 85 | } 86 | 87 | .bot .avatar { 88 | border: 2px solid #c96cff; 89 | } 90 | 91 | .user .avatar { 92 | border: 2px solid #ff801d; 93 | margin-left: 5px; 94 | } -------------------------------------------------------------------------------- /example/chatbot/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /example/chatbot/ui/js/goui.js: -------------------------------------------------------------------------------- 1 | window.goui = (function() { 2 | let obj = {}; 3 | let invokeBackend; 4 | 5 | let osConsts = { 6 | linux:{pathSeparator:'/',pathListSeparator:':'}, 7 | windows:{pathSeparator:'\\',pathListSeparator: ';'} 8 | }; 9 | 10 | let Context = { 11 | create: function (options) { 12 | let obj = {}; 13 | 14 | obj.error = function (msg) { 15 | if(options.error) { 16 | let data = {url:options.error,data:msg}; 17 | invokeBackend(data); 18 | } 19 | }; 20 | 21 | obj.success = function(data) { 22 | if(options.success) { 23 | data = {url:options.success,data:data}; 24 | invokeBackend(data); 25 | } 26 | }; 27 | 28 | return obj; 29 | } 30 | }; 31 | 32 | let Router = { 33 | create: function () { 34 | let obj = {}; 35 | 36 | let parsedRoutes = []; 37 | 38 | let optionalParam = /\((.*?)\)/g; 39 | let namedParam = /(\(\?)?:\w+/g; 40 | let splatParam = /\*\w+/g; 41 | let escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 42 | 43 | let routeStripper = /^[#\/]|\s+$/g; 44 | 45 | let pathToRegExp = function (path) { 46 | path = path.replace(escapeRegExp, '\\$&') 47 | .replace(optionalParam, '(?:$1)?') 48 | .replace(namedParam, function (match, optional) { 49 | return optional ? match : '([^/?]+)'; 50 | }) 51 | .replace(splatParam, '([^?]*?)'); 52 | return new RegExp('^' + path + '(?:\\?([\\s\\S]*))?$'); 53 | }; 54 | 55 | obj.parse = function (path, handler) { 56 | var route = {}; 57 | route.regexp = pathToRegExp(path); 58 | route.handler = handler; 59 | parsedRoutes.push(route); 60 | }; 61 | 62 | obj.dispatch = function (url) { 63 | var matched; 64 | parsedRoutes.some(function (route) { 65 | var args = url.match(route.regexp); 66 | if (args) { 67 | route.args = args.slice(1); 68 | matched = route; 69 | return true; 70 | } 71 | }); 72 | return matched; 73 | }; 74 | 75 | return obj; 76 | } 77 | }; 78 | 79 | if (window.webkit) { 80 | obj.os = osConsts.linux ; 81 | invokeBackend = function(data) { 82 | window.webkit.messageHandlers.goui.postMessage(JSON.stringify(data)); 83 | }; 84 | } else if (window.gouiAndroid){ 85 | obj.os = osConsts.linux ; 86 | invokeBackend = function(data) { 87 | window.gouiAndroid.handleMessage(JSON.stringify(data)); 88 | }; 89 | } else if(window.external) { 90 | obj.os = osConsts.windows ; 91 | invokeBackend = function(data) { 92 | window.external.notify(JSON.stringify(data)); 93 | }; 94 | } 95 | 96 | let router = Router.create(); 97 | 98 | let seq = 0; 99 | 100 | let getName = function () { 101 | let name = "f" + seq; 102 | seq++; 103 | return name; 104 | }; 105 | 106 | //Request is to send request to the backend 107 | // 108 | //options: { 109 | // url:"service function", 110 | // data: data, 111 | // dataType: "json, text, html or xml" ? 112 | // context: callback context 113 | // success: callback function on success, 114 | // error: callback function on erroe 115 | // } 116 | 117 | obj.request = function (options) { 118 | var successName, errorName; 119 | 120 | if (options.success) { 121 | successName = getName(); 122 | obj[successName] = function (data) { 123 | options.success.call(options.context, data); 124 | delete obj[successName]; 125 | if (errorName) { 126 | delete obj[errorName]; 127 | } 128 | }; 129 | } 130 | 131 | if (options.error) { 132 | errorName = getName(); 133 | obj[errorName] = function (err) { 134 | options.error.call(options.context, err); 135 | delete obj[errorName]; 136 | if (successName) { 137 | delete obj[successName]; 138 | } 139 | }; 140 | } 141 | 142 | var req = { url: options.url}; 143 | if(options.data) { 144 | req.data = JSON.stringify(options.data); 145 | } 146 | if(successName) { 147 | req.success = "goui." + successName; 148 | } 149 | if(errorName) { 150 | req.error = "goui." + errorName; 151 | } 152 | 153 | invokeBackend(req); 154 | }; 155 | 156 | // service is to register a frontend service the backend can request 157 | obj.service = function (path,handler) { 158 | router.parse(path,handler); 159 | }; 160 | 161 | //options: { 162 | // url, 163 | // data, 164 | // success, 165 | // error 166 | // } 167 | obj.handleRequest = function (options) { 168 | var ctx = Context.create(options); 169 | 170 | if (!options || !options.url) { 171 | ctx.error("Invalid request."); 172 | return; 173 | } 174 | 175 | var route = router.dispatch(options.url); 176 | if (route) { 177 | route.args.push(ctx); 178 | route.handler.apply(null, route.args); 179 | } else { 180 | ctx.error("Service not found: ", options.url) 181 | } 182 | }; 183 | 184 | obj.escapeRegExp = function(text) { 185 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); 186 | }; 187 | 188 | 189 | return obj; 190 | })(); 191 | -------------------------------------------------------------------------------- /example/chatbot/ui/js/index.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var $input = $("#input"); 3 | var $box = $("#chat"); 4 | 5 | $input.on("keydown", function (e) { 6 | if(e.which == 13 || e.keyCode == 13) { 7 | var msg = $input.val(); 8 | if(msg.length != 0) { 9 | sendMessage(msg) 10 | $input.val(""); 11 | } 12 | } 13 | }); 14 | 15 | var sendMessage = function(msg) { 16 | $box.append('
' + msg + '
U
') 17 | send(msg); 18 | }; 19 | 20 | var send = function(msg) { 21 | goui.request({url: "chat/"+msg,success: receiveMessage}); 22 | }; 23 | 24 | var receiveMessage = function(msg) { 25 | $box.append('
B
' + msg + '
') 26 | }; 27 | 28 | goui.service("chat/:msg",receiveMessage) 29 | 30 | send("ready"); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /example/filepicker/filepicker: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/example/filepicker/filepicker -------------------------------------------------------------------------------- /example/filepicker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fipress/GoUI" 5 | "github.com/fipress/GoUI/widgets/filepicker" 6 | ) 7 | 8 | func main() { 9 | //goui.Service("chat/:msg", chatService) 10 | goui.RegisterWidgets(new(filepicker.FilePicker)) 11 | 12 | goui.Create(goui.Settings{Title: "FilePicker", Top: 30, Left: 100, Width: 300, Height: 440}) 13 | } 14 | -------------------------------------------------------------------------------- /example/filepicker/ui/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | box-sizing: border-box; 3 | color: #555555; 4 | margin:0; 5 | } 6 | 7 | #box { 8 | width: 300px; 9 | border: 1px #999999 solid; 10 | border-radius: 6px; 11 | background-color: #efefde; 12 | } 13 | 14 | #head { 15 | padding: 8px; 16 | border-top-left-radius: 6px; 17 | border-top-right-radius: 6px; 18 | border-bottom: 1px #aaaaaa solid; 19 | text-align: center; 20 | } 21 | 22 | #chat { 23 | width:100%; 24 | height: 363px; 25 | padding: 6px; 26 | box-sizing: border-box; 27 | } 28 | 29 | #input { 30 | width: 100%; 31 | height: 40px; 32 | margin: 0; 33 | padding: 5px; 34 | border: none; 35 | resize: none; 36 | border-top: 1px #aaaaaa solid; 37 | box-sizing: border-box; 38 | background: inherit; 39 | outline: none; 40 | font-size: inherit; 41 | color: inherit; 42 | border-bottom-left-radius: 6px; 43 | border-bottom-right-radius: 6px; 44 | } 45 | 46 | .bot, .user { 47 | padding: 5px 0; 48 | display: grid; 49 | } 50 | 51 | .bot { 52 | //float: left; 53 | width: 100%; 54 | grid-template-columns: 30px 220px; 55 | } 56 | 57 | .msg { 58 | padding: 5px; 59 | border-radius: 4px; 60 | //max-width: 240px; 61 | display: inline-block; 62 | } 63 | 64 | .user { 65 | //float: right; 66 | grid-template-columns: auto 220px 30px; 67 | 68 | } 69 | 70 | .bot .msg { 71 | background-color: #ffffee; 72 | //margin-left: 5px; 73 | } 74 | 75 | .user .msg { 76 | background-color: #7fff00; 77 | //margin-right: 5px; 78 | float: right; 79 | } 80 | 81 | .avatar { 82 | display: inline-block; 83 | padding: 2px 5px; 84 | border-radius: 10px; 85 | } 86 | 87 | .bot .avatar { 88 | border: 2px solid #c96cff; 89 | } 90 | 91 | .user .avatar { 92 | border: 2px solid #ff801d; 93 | margin-left: 5px; 94 | } -------------------------------------------------------------------------------- /example/filepicker/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
File Picker
15 |
Open File Picker
16 | 17 |
Save File Picker
18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /example/filepicker/ui/js/g-filepicker.js: -------------------------------------------------------------------------------- 1 | class GoUIFilePicker extends HTMLElement { 2 | static get observedAttributes() { 3 | return ['accept','multiple', 'editable','style','class']; 4 | } 5 | 6 | constructor() { 7 | super(); 8 | 9 | this.settings = { 10 | //isSave: false, 11 | //message: "", 12 | //fileTypes: "", 13 | //startLocation:"", 14 | //suggestedFilename:"", 15 | //multiple:false, 16 | //fileOnly:false, 17 | //dirOnly:false, 18 | //allowsOtherFileTypes:false, 19 | //canCreateDir:false, 20 | //showsHiddenFiles:false, 21 | }; 22 | } 23 | 24 | get editable() { 25 | return !this.editor.getAttribute("readonly"); 26 | } 27 | 28 | set editable(v) { 29 | const isEditable = Boolean(v); 30 | this.editor.setAttribute("readonly",!isEditable); 31 | } 32 | 33 | set accept(v) { 34 | //this.fileBtn.setAttribute("accept",v); 35 | this.settings.accept = v; 36 | } 37 | 38 | set multiple(v) { 39 | const isMulti = Boolean(v); 40 | //this.fileBtn.setAttribute("multiple",v); 41 | this.settings.multiple = isMulti; 42 | } 43 | 44 | set action(v) { 45 | this.settings.isSave = v == "save"; 46 | } 47 | 48 | get value() { 49 | //return this.fileBtn.value; 50 | return this.editor.value; 51 | } 52 | 53 | set value(v) { 54 | this.editor.text = v; 55 | } 56 | 57 | get files() { 58 | //split 59 | return this.editor.value; 60 | } 61 | 62 | connectedCallback() { 63 | const el = document.createElement('div'); 64 | const shadow = el.attachShadow({mode: 'open'}); 65 | const wrapper = document.createElement('span'); 66 | 67 | this.editor = document.createElement('input'); 68 | this.editor.setAttribute('type', 'text'); 69 | wrapper.appendChild(this.editor); 70 | 71 | this.btn = document.createElement('button'); 72 | this.btn.innerText = 'Browse...'; 73 | wrapper.appendChild(this.btn); 74 | 75 | //this.fileBtn = document.createElement('input'); 76 | //this.fileBtn.setAttribute('type', 'file'); 77 | //this.fileBtn.setAttribute('style','opacity:0'); 78 | //wrapper.appendChild(this.fileBtn); 79 | 80 | // Create some CSS to apply to the shadow dom 81 | const style = document.createElement('style'); 82 | 83 | style.textContent = ` 84 | span { 85 | display:grid; 86 | grid-template-columns: auto min-content; 87 | grid-gap: 0; 88 | } 89 | `; 90 | 91 | // Attach the created elements to the shadow dom 92 | shadow.appendChild(style); 93 | //console.log(style.isConnected); 94 | shadow.appendChild(wrapper); 95 | this.appendChild(el); 96 | 97 | let _this=this; 98 | 99 | this.btn.onclick = function() { 100 | //console.log("request goui service"); 101 | goui.request({url:"filepicker", 102 | data:_this.settings, 103 | success:function(path) { 104 | _this.editor.value = path; 105 | }}); 106 | }; 107 | 108 | 109 | if(this.hasAttribute("style")) { 110 | el.setAttribute("style",this.getAttribute("style")); 111 | } 112 | 113 | if(this.hasAttribute("class")) { 114 | //const s = getComputedStyle(this); 115 | el.setAttribute("class",this.getAttribute("class")); 116 | } 117 | 118 | if(this.hasAttribute("multiple")) { 119 | //_this.fileBtn.setAttribute("multiple",this.getAttribute("multiple")); 120 | const isMulti = Boolean(this.getAttribute("multiple")); 121 | //this.fileBtn.setAttribute("multiple",v); 122 | _this.settings.multiple = isMulti; 123 | } 124 | 125 | if(this.hasAttribute("accept")) { 126 | //_this.fileBtn.setAttribute("accept",this.getAttribute("accept")); 127 | _this.settings.accept = this.getAttribute("accept"); 128 | } 129 | 130 | if(this.hasAttribute("action")) { 131 | _this.settings.isSave = this.getAttribute("action") == "save"; 132 | } 133 | 134 | const readonly = !this.getAttribute("editable"); 135 | this.editor.setAttribute("readonly",readonly); 136 | 137 | } 138 | 139 | disconnectedCallback() { 140 | } 141 | 142 | attributeChangedCallback(name, oldValue, newValue) { 143 | switch(name) { 144 | 145 | } 146 | } 147 | 148 | } 149 | 150 | // Define the new element 151 | customElements.define('g-filepicker', GoUIFilePicker); -------------------------------------------------------------------------------- /example/filepicker/ui/js/goui.js: -------------------------------------------------------------------------------- 1 | window.goui = (function() { 2 | 3 | var Context = { 4 | create: function (options) { 5 | var obj = {}; 6 | 7 | obj.error = function (msg) { 8 | if(options.error) { 9 | var data = {url:options.error,data:msg}; 10 | agent.invokeBackend(JSON.stringify(data)); 11 | } 12 | }; 13 | 14 | obj.success = function(data) { 15 | if(options.success) { 16 | var data = {url:options.success,data:data}; 17 | agent.invokeBackend(JSON.stringify(data)); 18 | } 19 | } 20 | 21 | return obj; 22 | } 23 | } 24 | 25 | var Agent = { 26 | create: function () { 27 | var obj = {}; 28 | 29 | obj.invokeBackend = function (data) { 30 | if(typeof data === 'object') { 31 | data = JSON.stringify(data); 32 | } 33 | if (window.webkit) { 34 | window.webkit.messageHandlers.goui.postMessage(data); 35 | } else if (1==2){ 36 | //todo: windows 37 | } else { 38 | //todo: linux 39 | } 40 | 41 | } 42 | 43 | 44 | 45 | return obj; 46 | } 47 | } 48 | 49 | 50 | var Router = { 51 | create: function () { 52 | var obj = {}; 53 | 54 | var parsedRoutes = []; 55 | 56 | var optionalParam = /\((.*?)\)/g; 57 | var namedParam = /(\(\?)?:\w+/g; 58 | var splatParam = /\*\w+/g; 59 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 60 | 61 | var routeStripper = /^[#\/]|\s+$/g; 62 | 63 | var pathToRegExp = function (path) { 64 | path = path.replace(escapeRegExp, '\\$&') 65 | .replace(optionalParam, '(?:$1)?') 66 | .replace(namedParam, function (match, optional) { 67 | return optional ? match : '([^/?]+)'; 68 | }) 69 | .replace(splatParam, '([^?]*?)'); 70 | return new RegExp('^' + path + '(?:\\?([\\s\\S]*))?$'); 71 | }; 72 | 73 | obj.parse = function (path, handler) { 74 | var route = {}; 75 | route.regexp = pathToRegExp(path); 76 | route.handler = handler; 77 | parsedRoutes.push(route); 78 | } 79 | 80 | obj.dispatch = function (url) { 81 | var matched; 82 | parsedRoutes.some(function (route) { 83 | var args = url.match(route.regexp); 84 | if (args) { 85 | route.args = args.slice(1); 86 | matched = route; 87 | return true; 88 | } 89 | }) 90 | return matched; 91 | } 92 | 93 | return obj; 94 | } 95 | } 96 | 97 | var obj = {}; 98 | 99 | var agent = Agent.create(); 100 | var router = Router.create(); 101 | 102 | var seq = 0; 103 | 104 | var getName = function () { 105 | var name = "f" + seq; 106 | seq++; 107 | return name; 108 | }; 109 | 110 | //Request is to send request to the backend 111 | // 112 | //options: { 113 | // url:"service function", 114 | // data: data, 115 | // dataType: "json, text, html or xml" ? 116 | // context: callback context 117 | // success: callback function on success, 118 | // error: callback function on erroe 119 | // } 120 | 121 | obj.request = function (options) { 122 | var successName, errorName; 123 | 124 | if (options.success) { 125 | successName = getName(); 126 | obj[successName] = function (data) { 127 | options.success.call(options.context, data); 128 | delete obj[successName]; 129 | if (errorName) { 130 | delete obj[errorName]; 131 | } 132 | }; 133 | } 134 | 135 | if (options.error) { 136 | errorName = getName(); 137 | obj[errorName] = function (err) { 138 | options.error.call(options.context, err); 139 | delete obj[errorName]; 140 | if (successName) { 141 | delete obj[successName]; 142 | } 143 | }; 144 | } 145 | 146 | var req = { url: options.url}; 147 | if(options.data) { 148 | req.data = JSON.stringify(options.data); 149 | } 150 | if(successName) { 151 | req.success = "goui." + successName; 152 | } 153 | if(errorName) { 154 | req.error = "goui." + errorName; 155 | } 156 | 157 | agent.invokeBackend(JSON.stringify(req)); 158 | } 159 | 160 | // service is to register a frontend service the backend can request 161 | obj.service = function (path,handler) { 162 | router.parse(path,handler); 163 | } 164 | 165 | //options: { 166 | // url, 167 | // data, 168 | // success, 169 | // error 170 | // } 171 | obj.handleRequest = function (options) { 172 | //var ops = JSON.parse(options); 173 | var ctx = Context.create(options); 174 | 175 | if (!options || !options.url) { 176 | ctx.error("Invalid request."); 177 | return; 178 | } 179 | 180 | var route = router.dispatch(options.url); 181 | if (route) { 182 | route.args.push(ctx); 183 | route.handler.apply(null, route.args); 184 | } else { 185 | ctx.error("Service not found: ", options.url) 186 | } 187 | 188 | }; 189 | 190 | obj.escapeRegExp = function(text) { 191 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); 192 | } 193 | 194 | 195 | return obj; 196 | })(); 197 | -------------------------------------------------------------------------------- /example/filepicker/ui/js/index.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | }); 4 | -------------------------------------------------------------------------------- /example/hello/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fipress/GoUI" 5 | ) 6 | 7 | var on = true 8 | 9 | func getMsg() string { 10 | if on { 11 | on = false 12 | return "Hello world :-)" 13 | } else { 14 | on = true 15 | return "And hello dear developer :-)" 16 | } 17 | } 18 | 19 | func main() { 20 | //register a service 21 | goui.Service("hello", func(context *goui.Context) { 22 | context.Success(getMsg()) 23 | }) 24 | 25 | //create and open a window 26 | goui.Create(goui.Settings{Title: "Hello", 27 | Left: 200, 28 | Top: 50, 29 | Width: 400, 30 | Height: 510, 31 | Resizable: true, 32 | Debug: true}) 33 | } 34 | -------------------------------------------------------------------------------- /example/hello/ui/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | box-sizing: border-box; 3 | color: #555555; 4 | margin:0; 5 | background-color: azure ; 6 | } 7 | 8 | img { 9 | width: 100%; 10 | } 11 | 12 | .center { 13 | text-align: center; 14 | } 15 | 16 | #box { 17 | width:25em; 18 | margin: 0 auto; 19 | } 20 | 21 | #logo { 22 | width: 10em; 23 | margin: 1em auto .3em auto; 24 | } 25 | 26 | #btn,#result { 27 | margin: .4em 1em; 28 | border: 1px solid #21a823; 29 | border-radius: .3em; 30 | } 31 | 32 | #btn { 33 | width: 16em; 34 | 35 | font: inherit; 36 | padding: .5em 2em; 37 | color: #eeeeee; 38 | background-color: #19a823; 39 | } 40 | 41 | #result { 42 | width: 15em; 43 | height: 15em; 44 | resize: none; 45 | padding: .5em; 46 | font-size: 1em; 47 | line-height: 1.5em; 48 | color: #333333; 49 | background-color: #efefef; 50 | } -------------------------------------------------------------------------------- /example/hello/ui/img/goui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/example/hello/ui/img/goui.png -------------------------------------------------------------------------------- /example/hello/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /example/hello/ui/js/goui.js: -------------------------------------------------------------------------------- 1 | window.goui = (function() { 2 | var Context = { 3 | create: function (options) { 4 | var obj = {}; 5 | 6 | obj.error = function (msg) { 7 | if(options.error) { 8 | var data = {url:options.error,data:msg}; 9 | agent.invokeBackend(JSON.stringify(data)); 10 | } 11 | }; 12 | 13 | obj.success = function(data) { 14 | if(options.success) { 15 | var data = {url:options.success,data:data}; 16 | agent.invokeBackend(JSON.stringify(data)); 17 | } 18 | }; 19 | 20 | return obj; 21 | } 22 | }; 23 | 24 | var Agent = { 25 | create: function () { 26 | var obj = {}; 27 | 28 | obj.invokeBackend = function (data) { 29 | if(typeof data === 'object') { 30 | data = JSON.stringify(data); 31 | } 32 | if (window.webkit) { 33 | window.webkit.messageHandlers.goui.postMessage(data); 34 | } else if (1==2){ 35 | //todo: windows 36 | } else { 37 | //todo: linux 38 | } 39 | 40 | }; 41 | 42 | return obj; 43 | } 44 | }; 45 | 46 | 47 | var Router = { 48 | create: function () { 49 | var obj = {}; 50 | 51 | var parsedRoutes = []; 52 | 53 | var optionalParam = /\((.*?)\)/g; 54 | var namedParam = /(\(\?)?:\w+/g; 55 | var splatParam = /\*\w+/g; 56 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 57 | 58 | var routeStripper = /^[#\/]|\s+$/g; 59 | 60 | var pathToRegExp = function (path) { 61 | path = path.replace(escapeRegExp, '\\$&') 62 | .replace(optionalParam, '(?:$1)?') 63 | .replace(namedParam, function (match, optional) { 64 | return optional ? match : '([^/?]+)'; 65 | }) 66 | .replace(splatParam, '([^?]*?)'); 67 | return new RegExp('^' + path + '(?:\\?([\\s\\S]*))?$'); 68 | }; 69 | 70 | obj.parse = function (path, handler) { 71 | var route = {}; 72 | route.regexp = pathToRegExp(path); 73 | route.handler = handler; 74 | parsedRoutes.push(route); 75 | }; 76 | 77 | obj.dispatch = function (url) { 78 | var matched; 79 | parsedRoutes.some(function (route) { 80 | var args = url.match(route.regexp); 81 | if (args) { 82 | route.args = args.slice(1); 83 | matched = route; 84 | return true; 85 | } 86 | }); 87 | return matched; 88 | }; 89 | 90 | return obj; 91 | } 92 | }; 93 | 94 | var obj = {}; 95 | 96 | var agent = Agent.create(); 97 | var router = Router.create(); 98 | 99 | var seq = 0; 100 | 101 | var getName = function () { 102 | var name = "f" + seq; 103 | seq++; 104 | return name; 105 | }; 106 | 107 | //Request is to send request to the backend 108 | // 109 | //options: { 110 | // url:"service function", 111 | // data: data, 112 | // dataType: "json, text, html or xml" ? 113 | // context: callback context 114 | // success: callback function on success, 115 | // error: callback function on erroe 116 | // } 117 | 118 | obj.request = function (options) { 119 | var successName, errorName; 120 | 121 | if (options.success) { 122 | successName = getName(); 123 | obj[successName] = function (data) { 124 | options.success.call(options.context, data); 125 | delete obj[successName]; 126 | if (errorName) { 127 | delete obj[errorName]; 128 | } 129 | }; 130 | } 131 | 132 | if (options.error) { 133 | errorName = getName(); 134 | obj[errorName] = function (err) { 135 | options.error.call(options.context, err); 136 | delete obj[errorName]; 137 | if (successName) { 138 | delete obj[successName]; 139 | } 140 | }; 141 | } 142 | 143 | var req = { url: options.url}; 144 | if(options.data) { 145 | req.data = JSON.stringify(options.data); 146 | } 147 | if(successName) { 148 | req.success = "goui." + successName; 149 | } 150 | if(errorName) { 151 | req.error = "goui." + errorName; 152 | } 153 | 154 | agent.invokeBackend(req); 155 | }; 156 | 157 | // service is to register a frontend service the backend can request 158 | obj.service = function (path,handler) { 159 | router.parse(path,handler); 160 | }; 161 | 162 | //options: { 163 | // url, 164 | // data, 165 | // success, 166 | // error 167 | // } 168 | obj.handleRequest = function (options) { 169 | var ctx = Context.create(options); 170 | 171 | if (!options || !options.url) { 172 | ctx.error("Invalid request."); 173 | return; 174 | } 175 | 176 | var route = router.dispatch(options.url); 177 | if (route) { 178 | route.args.push(ctx); 179 | route.handler.apply(null, route.args); 180 | } else { 181 | ctx.error("Service not found: ", options.url) 182 | } 183 | }; 184 | 185 | obj.escapeRegExp = function(text) { 186 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); 187 | }; 188 | 189 | 190 | return obj; 191 | })(); 192 | -------------------------------------------------------------------------------- /example/hello/ui/js/index.js: -------------------------------------------------------------------------------- 1 | document.onreadystatechange = function () { 2 | if(document.readyState == "complete") { 3 | var result = ""; 4 | document.getElementById("btn").onclick = function() { 5 | goui.request({url: "hello", 6 | success: function(data) { 7 | result = result + data + "\n" 8 | document.getElementById("result").innerText = result; 9 | }}); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /example/texteditor/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fipress/GoUI" 5 | "runtime" 6 | ) 7 | 8 | func main() { 9 | var menuDefs []goui.MenuDef 10 | switch runtime.GOOS { 11 | case "darwin": 12 | menuDefs = []goui.MenuDef{{Title: "AppMenu", Type: goui.Container, Children: []goui.MenuDef{ 13 | {Title: "About", Type: goui.Standard, Action: "about"}, 14 | {Type: goui.Separator}, 15 | {Title: "Hide", Type: goui.Standard, Action: "hide"}, 16 | {Type: goui.Separator}, 17 | {Title: "Quit", Type: goui.Standard, Action: "quit"}, 18 | }}, 19 | {Title: "File", Type: goui.Container, Children: []goui.MenuDef{ 20 | {Title: "New", Type: goui.Custom, Action: "new", Handler: func() { 21 | println("new file") 22 | }}, 23 | {Title: "Open", Type: goui.Custom, Action: "open"}, 24 | {Type: goui.Separator}, 25 | {Title: "Save", Type: goui.Custom, Action: "save"}, 26 | {Title: "Save as", Type: goui.Custom, Action: "saveAs"}, 27 | }}, 28 | {Title: "Edit", Type: goui.Container, Children: []goui.MenuDef{ 29 | {Title: "Undo", Type: goui.Custom, Action: "undo"}, 30 | {Title: "Redo", Type: goui.Custom, Action: "redo"}, 31 | {Type: goui.Separator}, 32 | {Title: "Cut", Type: goui.Custom, Action: "cut"}, 33 | {Title: "Copy", Type: goui.Custom, Action: "copy"}, 34 | {Title: "Paste", Type: goui.Custom, Action: "paste"}, 35 | }}, 36 | } 37 | case "windows": 38 | println("windows") 39 | default: 40 | menuDefs = []goui.MenuDef{{Title: "File", Type: goui.Container, Children: []goui.MenuDef{ 41 | {Title: "New", Type: goui.Custom, Action: "new", Handler: func() { 42 | println("new file") 43 | }}, 44 | {Title: "Open", Type: goui.Custom, Action: "open"}, 45 | {Type: goui.Separator}, 46 | {Title: "Save", Type: goui.Custom, Action: "save"}, 47 | {Title: "Save as", Type: goui.Custom, Action: "saveAs"}, 48 | {Type: goui.Separator}, 49 | {Title: "Quit", Type: goui.Standard, Action: "quit"}, 50 | }}, 51 | {Title: "Edit", Type: goui.Container, Children: []goui.MenuDef{ 52 | {Title: "Undo", Type: goui.Custom, Action: "undo"}, 53 | {Title: "Redo", Type: goui.Custom, Action: "redo"}, 54 | {Type: goui.Separator}, 55 | {Title: "Cut", Type: goui.Custom, Action: "cut"}, 56 | {Title: "Copy", Type: goui.Custom, Action: "copy"}, 57 | {Title: "Paste", Type: goui.Custom, Action: "paste"}, 58 | }}, 59 | } 60 | } 61 | 62 | goui.CreateWithMenu(goui.Settings{ 63 | Title: "Text Editor", 64 | Top: 30, 65 | Left: 100, 66 | Width: 320, 67 | Height: 400, 68 | Debug: true, 69 | }, 70 | menuDefs) 71 | } 72 | -------------------------------------------------------------------------------- /example/texteditor/texteditor: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/example/texteditor/texteditor -------------------------------------------------------------------------------- /example/texteditor/ui/css/editor.css: -------------------------------------------------------------------------------- 1 | textarea { 2 | width: 300px; 3 | height: 350px; 4 | } -------------------------------------------------------------------------------- /example/texteditor/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/texteditor/ui/js/editor.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/example/texteditor/ui/js/editor.js -------------------------------------------------------------------------------- /file/file_android.go: -------------------------------------------------------------------------------- 1 | package file 2 | -------------------------------------------------------------------------------- /file/file_iOS.go: -------------------------------------------------------------------------------- 1 | package file 2 | -------------------------------------------------------------------------------- /file/file_linux.go: -------------------------------------------------------------------------------- 1 | package file 2 | -------------------------------------------------------------------------------- /file/file_macOS.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | /* 4 | 5 | */ 6 | import "C" 7 | 8 | func OpenFilePicker() { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /file/filepicker_macOS.go: -------------------------------------------------------------------------------- 1 | // +build darwin,amd64,!sim 2 | 3 | package file 4 | 5 | /* 6 | 7 | #cgo darwin CFLAGS: -x objective-c 8 | #cgo darwin LDFLAGS: -framework Cocoa -framework WebKit 9 | 10 | //title, nameLabel, message, canCreateDir, showsHidden, can 11 | 12 | char* openFilePicker(bool multiple, bool dirOnly, char* defaultDir) { 13 | NSOpenPanel *panel = [NSOpenPanel openPanel]; 14 | [panel directoryURL:NSHomeDirectory()]; 15 | [panel setAllowsMultipleSelection:NO]; 16 | [panel setCanChooseDirectories:YES]; 17 | [panel setCanChooseFiles:YES]; 18 | [panel setAllowedFileTypes:@[@"onecodego"]]; 19 | [panel setAllowsOtherFileTypes:YES]; 20 | if ([panel runModal] == NSOKButton) { 21 | NSString *path = [panel.URLs.firstObject path]; 22 | return [path UTF8String]; 23 | } 24 | return ""; 25 | } 26 | 27 | char* saveFilePicker() { 28 | NSSavePanel* panel = [NSSavePanel savePanel]; 29 | [panel setNameFieldStringValue:@"Untitle.onecodego"]; 30 | [panel setMessage:@"Choose the path to save the document"]; 31 | [panel setAllowsOtherFileTypes:YES]; 32 | [panel setAllowedFileTypes:@[@"onecodego"]]; 33 | [panel setExtensionHidden:YES]; 34 | [panel setCanCreateDirectories:YES]; 35 | [panel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result){ 36 | if (result == NSFileHandlingPanelOKButton) 37 | { 38 | NSString *path = [[panel URL] path]; 39 | [@"onecodego" writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]; 40 | } 41 | }]; 42 | } 43 | */ 44 | import "C" 45 | -------------------------------------------------------------------------------- /goui.go: -------------------------------------------------------------------------------- 1 | // 2 | // Package goui provides a cross platform GUI solution for Go developers. 3 | // 4 | // It uses Cocoa/WebKit for macOS, MSHTML for Windows and Gtk/WebKit for Linux. 5 | // 6 | // It provides two way bindings between Go and Javascript. 7 | // 8 | // 9 | package goui 10 | 11 | import "C" 12 | import ( 13 | "encoding/json" 14 | ) 15 | 16 | /*type iWindow interface { 17 | create(Settings) 18 | exit() 19 | activate() 20 | invokeJS(string) 21 | }*/ 22 | 23 | // Settings is to configure the window's appearance 24 | type Settings struct { 25 | Title string //Title of the application window 26 | UIDir string //Directory of the UI/Web related files, default: "ui" 27 | Index string //Index html file, default: "index.html" 28 | Url string //Full url address if you don't use WebDir + Index 29 | Left int 30 | Top int 31 | Width int 32 | Height int 33 | Resizable bool 34 | Debug bool 35 | } 36 | 37 | type Widget interface { 38 | Register() 39 | } 40 | 41 | //as goui designed to support only single-page application, it is reasonable to hold a window globally 42 | 43 | // Create is to create a native window with a webview 44 | // 45 | func Create(settings Settings) (err error) { 46 | return CreateWithMenu(settings, nil) 47 | } 48 | 49 | func CreateWithMenu(settings Settings, menuDefs []MenuDef) (err error) { 50 | create(settings, menuDefs) 51 | defer exit() 52 | 53 | return 54 | } 55 | 56 | // Service is to add a backend service for frontend to invoke. 57 | // params: 58 | // url - the url act as an unique identifier of a service, for example, "user/login", "blog/get/:id". 59 | // handler - the function that handle the client request. 60 | func Service(url string, handler func(*Context)) { 61 | route := new(route) 62 | route.handler = handler 63 | parseRoute(url, route) 64 | } 65 | 66 | func RegisterWidgets(widgets ...Widget) { 67 | for _, w := range widgets { 68 | w.Register() 69 | } 70 | } 71 | 72 | type JSServiceOptions struct { 73 | Url string `json:"url"` 74 | Data interface{} `json:"data"` 75 | Success string `json:"success"` 76 | Error string `json:"error"` 77 | } 78 | 79 | // RequestJSService is to send a request to the front end from the main thread 80 | func RequestJSService(options JSServiceOptions) (err error) { 81 | ops, err := json.Marshal(options) 82 | if err != nil { 83 | return 84 | } 85 | 86 | invokeJS("goui.handleRequest("+string(ops)+")", true) 87 | return 88 | } 89 | 90 | // RequestJSServiceFromBackground is to send a request to the front end from a background thread 91 | func RequestJSServiceFromBackground(options JSServiceOptions) (err error) { 92 | ops, err := json.Marshal(options) 93 | if err != nil { 94 | return 95 | } 96 | 97 | invokeJS("goui.handleRequest("+string(ops)+")", false) 98 | return 99 | } 100 | 101 | func ActivateWindow() { 102 | //window.activate() 103 | } 104 | 105 | //InvokeJavascriptFunc is for the backend to invoke frontend javascript directly. 106 | //params: 107 | // name - javascript function name 108 | // params - the parameters 109 | /*func InvokeJavascriptFunc(name string, params ...interface{}) { 110 | js := fiputil.MkString(params,name + "(",",",")") 111 | worker.invokeJS(js) 112 | } 113 | */ 114 | func OpenDevTools() { 115 | 116 | } 117 | -------------------------------------------------------------------------------- /goui_test.go: -------------------------------------------------------------------------------- 1 | package goui 2 | -------------------------------------------------------------------------------- /js/goui.js: -------------------------------------------------------------------------------- 1 | window.goui = (function() { 2 | let obj = {}; 3 | let invokeBackend; 4 | 5 | let osConsts = { 6 | linux:{pathSeparator:'/',pathListSeparator:':'}, 7 | windows:{pathSeparator:'\\',pathListSeparator: ';'} 8 | }; 9 | 10 | let Context = { 11 | create: function (options) { 12 | let obj = {}; 13 | 14 | obj.error = function (msg) { 15 | if(options.error) { 16 | let data = {url:options.error,data:msg}; 17 | invokeBackend(data); 18 | } 19 | }; 20 | 21 | obj.success = function(data) { 22 | if(options.success) { 23 | data = {url:options.success,data:data}; 24 | invokeBackend(data); 25 | } 26 | }; 27 | 28 | return obj; 29 | } 30 | }; 31 | 32 | let Router = { 33 | create: function () { 34 | let obj = {}; 35 | 36 | let parsedRoutes = []; 37 | 38 | let optionalParam = /\((.*?)\)/g; 39 | let namedParam = /(\(\?)?:\w+/g; 40 | let splatParam = /\*\w+/g; 41 | let escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 42 | 43 | let routeStripper = /^[#\/]|\s+$/g; 44 | 45 | let pathToRegExp = function (path) { 46 | path = path.replace(escapeRegExp, '\\$&') 47 | .replace(optionalParam, '(?:$1)?') 48 | .replace(namedParam, function (match, optional) { 49 | return optional ? match : '([^/?]+)'; 50 | }) 51 | .replace(splatParam, '([^?]*?)'); 52 | return new RegExp('^' + path + '(?:\\?([\\s\\S]*))?$'); 53 | }; 54 | 55 | obj.parse = function (path, handler) { 56 | var route = {}; 57 | route.regexp = pathToRegExp(path); 58 | route.handler = handler; 59 | parsedRoutes.push(route); 60 | }; 61 | 62 | obj.dispatch = function (url) { 63 | var matched; 64 | parsedRoutes.some(function (route) { 65 | var args = url.match(route.regexp); 66 | if (args) { 67 | route.args = args.slice(1); 68 | matched = route; 69 | return true; 70 | } 71 | }); 72 | return matched; 73 | }; 74 | 75 | return obj; 76 | } 77 | }; 78 | 79 | if (window.webkit) { 80 | obj.os = osConsts.linux ; 81 | invokeBackend = function(data) { 82 | window.webkit.messageHandlers.goui.postMessage(JSON.stringify(data)); 83 | }; 84 | } else if (window.gouiAndroid){ 85 | obj.os = osConsts.linux ; 86 | invokeBackend = function(data) { 87 | window.gouiAndroid.handleMessage(JSON.stringify(data)); 88 | }; 89 | } else if(window.external) { 90 | obj.os = osConsts.windows ; 91 | invokeBackend = function(data) { 92 | window.external.notify(JSON.stringify(data)); 93 | }; 94 | } 95 | 96 | let router = Router.create(); 97 | 98 | let seq = 0; 99 | 100 | let getName = function () { 101 | let name = "f" + seq; 102 | seq++; 103 | return name; 104 | }; 105 | 106 | //Request is to send request to the backend 107 | // 108 | //options: { 109 | // url:"service function", 110 | // data: data, 111 | // dataType: "json, text, html or xml" ? 112 | // context: callback context 113 | // success: callback function on success, 114 | // error: callback function on erroe 115 | // } 116 | 117 | obj.request = function (options) { 118 | var successName, errorName; 119 | 120 | if (options.success) { 121 | successName = getName(); 122 | obj[successName] = function (data) { 123 | options.success.call(options.context, data); 124 | delete obj[successName]; 125 | if (errorName) { 126 | delete obj[errorName]; 127 | } 128 | }; 129 | } 130 | 131 | if (options.error) { 132 | errorName = getName(); 133 | obj[errorName] = function (err) { 134 | options.error.call(options.context, err); 135 | delete obj[errorName]; 136 | if (successName) { 137 | delete obj[successName]; 138 | } 139 | }; 140 | } 141 | 142 | var req = { url: options.url}; 143 | if(options.data) { 144 | req.data = JSON.stringify(options.data); 145 | } 146 | if(successName) { 147 | req.success = "goui." + successName; 148 | } 149 | if(errorName) { 150 | req.error = "goui." + errorName; 151 | } 152 | 153 | invokeBackend(req); 154 | }; 155 | 156 | // service is to register a frontend service the backend can request 157 | obj.service = function (path,handler) { 158 | router.parse(path,handler); 159 | }; 160 | 161 | //options: { 162 | // url, 163 | // data, 164 | // success, 165 | // error 166 | // } 167 | obj.handleRequest = function (options) { 168 | var ctx = Context.create(options); 169 | 170 | if (!options || !options.url) { 171 | ctx.error("Invalid request."); 172 | return; 173 | } 174 | 175 | var route = router.dispatch(options.url); 176 | if (route) { 177 | route.args.push(ctx); 178 | route.handler.apply(null, route.args); 179 | } else { 180 | ctx.error("Service not found: ", options.url) 181 | } 182 | }; 183 | 184 | obj.escapeRegExp = function(text) { 185 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); 186 | }; 187 | 188 | 189 | return obj; 190 | })(); 191 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | // +build !android 2 | 3 | package goui 4 | 5 | import "C" 6 | import ( 7 | "fmt" 8 | "io" 9 | "os" 10 | ) 11 | 12 | //export goLog 13 | func goLog(msg *C.char) { 14 | s := fmt.Sprintln(C.GoString(msg)) 15 | doLog(s) 16 | } 17 | 18 | func Log(args ...interface{}) { 19 | s := fmt.Sprintln(args...) 20 | doLog(s) 21 | } 22 | 23 | func Logf(format string, args ...interface{}) { 24 | s := fmt.Sprintf(format, args...) 25 | doLog(s) 26 | } 27 | 28 | var logger io.WriteCloser 29 | 30 | func doLog(msg string) { 31 | if logger == nil { 32 | filename := Config.GetString("logFile") 33 | if filename != "" { 34 | file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.FileMode(0644)) 35 | if err == nil { 36 | logger = file 37 | } else { 38 | logger = os.Stdout 39 | } 40 | } else { 41 | logger = os.Stdout 42 | } 43 | } 44 | go logger.Write([]byte(msg)) 45 | } 46 | 47 | func closeLogger() { 48 | if logger != nil { 49 | logger.Close() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /menu.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | /* 4 | #include "menu.h" 5 | */ 6 | import "C" 7 | 8 | // MenuType is an enum of menu type 9 | type MenuType int 10 | 11 | const ( 12 | Container MenuType = iota //just a container item for sub items 13 | Custom 14 | Standard 15 | Separator 16 | ) 17 | 18 | // MenuDef is to define a menu item 19 | type MenuDef struct { 20 | Type MenuType 21 | Title string 22 | HotKey string 23 | Action string 24 | Handler func() 25 | Children []MenuDef 26 | } 27 | 28 | func convertMenuDef(def MenuDef) (cMenuDef C.MenuDef) { 29 | cMenuDef = C.MenuDef{} 30 | cMenuDef.title = C.CString(def.Title) 31 | cMenuDef.action = C.CString(def.Action) 32 | cMenuDef.key = C.CString(def.HotKey) 33 | cMenuDef.menuType = C.MenuType(def.Type) 34 | cMenuDef.children, cMenuDef.childrenCount = convertMenuDefs(def.Children) 35 | 36 | return 37 | } 38 | 39 | func convertMenuDefs(defs []MenuDef) (array *C.MenuDef, count C.int) { 40 | l := len(defs) 41 | if l == 0 { 42 | return 43 | } 44 | 45 | count = C.int(l) 46 | 47 | array = C.allocMenuDefArray(count) 48 | for i := 0; i < l; i++ { 49 | cMenuDef := convertMenuDef(defs[i]) 50 | C.addChildMenu(array, cMenuDef, C.int(i)) 51 | } 52 | 53 | return 54 | } 55 | 56 | var actionMap map[string]func() 57 | 58 | //export menuClicked 59 | func menuClicked(action *C.char) { 60 | a := C.GoString(action) 61 | println("menu clicked", a) 62 | f := actionMap[a] 63 | if f != nil { 64 | f() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /menu.h: -------------------------------------------------------------------------------- 1 | #ifndef _GOUI_MENU_ 2 | #define _GOUI_MENU_ 3 | 4 | #include 5 | #include "util.h" 6 | 7 | typedef enum MenuType { 8 | container, //just a container item for sub items 9 | custom, 10 | standard, 11 | separator 12 | } MenuType; 13 | 14 | typedef struct MenuDef{ 15 | const char* title; 16 | const char* action; 17 | const char* key; 18 | struct MenuDef* children; 19 | int childrenCount; 20 | MenuType menuType; 21 | } MenuDef; 22 | 23 | static MenuDef* allocMenuDefArray(int count) { 24 | if(count == 0) { 25 | return 0; 26 | } 27 | return (MenuDef*)malloc(sizeof(MenuDef)*count); 28 | } 29 | 30 | static void addChildMenu(MenuDef* children, MenuDef child, int index) { 31 | children[index] = child; 32 | } 33 | 34 | #endif -------------------------------------------------------------------------------- /menu_macos.c: -------------------------------------------------------------------------------- 1 | #include "menu_macos.h" 2 | 3 | extern void menuClicked(const char * action); 4 | 5 | @implementation CustomAction 6 | - (void)action:(id)sender { 7 | //NSLog(@"click menu:%@",[sender representedObject]); 8 | const char* str = [[sender representedObject] UTF8String]; 9 | //menuClicked((_GoString_){str, strlen(str)}); 10 | menuClicked(str); 11 | goUILog("click menu: %s\n",str); 12 | } 13 | @end 14 | 15 | 16 | @implementation GoUIMenu 17 | static CustomAction* customAction; 18 | static NSDictionary* actionMap; 19 | 20 | + (void)initialize { 21 | customAction = [[CustomAction alloc] init]; 22 | actionMap = [NSDictionary dictionaryWithObjectsAndKeys: 23 | //app menu 24 | [NSValue valueWithPointer:@selector(orderFrontStandardAboutPanel:)], @"about", 25 | [NSValue valueWithPointer:@selector(hide:)], @"hide", 26 | [NSValue valueWithPointer:@selector(hideOtherApplications:)], @"hideothers", 27 | [NSValue valueWithPointer:@selector(unhideAllApplications:)], @"unhide", 28 | [NSValue valueWithPointer:@selector(terminate:)], @"quit", 29 | nil]; 30 | } 31 | 32 | NSMenuItem* createMenuItem(MenuDef def) { 33 | NSString *title = utf8(def.title); 34 | NSString *key = utf8(def.key); 35 | 36 | SEL act = NULL; 37 | if(def.menuType == standard) { 38 | id pointer = [actionMap objectForKey:[NSString stringWithUTF8String:def.action]]; 39 | if(pointer) { 40 | act = [pointer pointerValue]; 41 | } 42 | } else if(def.menuType == custom) { 43 | act = @selector(action:); 44 | } 45 | NSMenuItem *item = [[[NSMenuItem alloc] initWithTitle:title 46 | action: act 47 | keyEquivalent:key] 48 | autorelease]; 49 | if(def.menuType == custom) { 50 | [item setRepresentedObject:[NSString stringWithUTF8String:def.action]]; 51 | [item setTarget:customAction]; 52 | } 53 | return item; 54 | } 55 | 56 | NSMenuItem* createMenu(MenuDef def) { 57 | NSString *title = utf8(def.title); 58 | 59 | NSMenu *menu = [[[NSMenu alloc] initWithTitle:title] autorelease]; 60 | NSMenuItem *item = createMenuItem(def); 61 | [item setSubmenu:menu]; 62 | 63 | for(int i=0;i 2 | #include "menu.h" 3 | #include "util_cocoa.h" 4 | 5 | @interface CustomAction : NSObject 6 | @end 7 | 8 | @interface GoUIMenu : NSObject 9 | +(void)buildMenu:(struct MenuDef[])defs count: (int)count; 10 | @end 11 | 12 | -------------------------------------------------------------------------------- /menu_test.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | const def = `[{"title":"File",}]` 4 | -------------------------------------------------------------------------------- /platform.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | //Platform is for packager to inject for different platforms 4 | var Platform string 5 | -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | /* 4 | #include 5 | #include "provider.h" 6 | 7 | */ 8 | import "C" 9 | import ( 10 | "os" 11 | "path" 12 | "runtime" 13 | "unsafe" 14 | ) 15 | 16 | const defaultDir = "ui" 17 | const defaultIndex = "index.html" 18 | 19 | func BoolToCInt(b bool) (i C.int) { 20 | if b { 21 | i = 1 22 | } 23 | return 24 | } 25 | 26 | func convertSettings(settings Settings) C.WindowSettings { 27 | //dir := path.Dir(settings.Url) 28 | if settings.UIDir == "" { 29 | settings.UIDir = defaultDir 30 | } 31 | 32 | if settings.Index == "" { 33 | settings.Index = defaultIndex 34 | } 35 | 36 | if settings.Url == "" { 37 | settings.Url = path.Join(settings.UIDir, settings.Index) 38 | if runtime.GOOS == "linux" { 39 | wd, _ := os.Getwd() 40 | settings.Url = path.Join("file://", wd, settings.Url) 41 | } else if runtime.GOOS == "android" { 42 | settings.Url = path.Join("file:///android_asset/", settings.Url) 43 | } 44 | } 45 | 46 | // windows needs WebDir and Index 47 | // macOS and iOS need Url 48 | 49 | return C.WindowSettings{C.CString(settings.Title), 50 | C.CString(settings.UIDir), 51 | //C.CString(abs), 52 | C.CString(settings.Index), 53 | C.CString(settings.Url), 54 | C.int(settings.Left), 55 | C.int(settings.Top), 56 | C.int(settings.Width), 57 | C.int(settings.Height), 58 | BoolToCInt(settings.Resizable), 59 | BoolToCInt(settings.Debug), 60 | } 61 | } 62 | 63 | func convertMenuDef(def MenuDef) (cMenuDef C.MenuDef) { 64 | cMenuDef = C.MenuDef{} 65 | cMenuDef.title = C.CString(def.Title) 66 | cMenuDef.action = C.CString(def.Action) 67 | cMenuDef.key = C.CString(def.HotKey) 68 | cMenuDef.menuType = C.MenuType(def.Type) 69 | cMenuDef.children, cMenuDef.childrenCount = convertMenuDefs(def.Children) 70 | 71 | return 72 | } 73 | 74 | func convertMenuDefs(defs []MenuDef) (array *C.MenuDef, count C.int) { 75 | l := len(defs) 76 | if l == 0 { 77 | return 78 | } 79 | 80 | count = C.int(l) 81 | 82 | array = C.allocMenuDefArray(count) 83 | for i := 0; i < l; i++ { 84 | cMenuDef := convertMenuDef(defs[i]) 85 | C.addChildMenu(array, cMenuDef, C.int(i)) 86 | } 87 | 88 | return 89 | } 90 | 91 | func create(settings Settings, menuDefs []MenuDef) { 92 | //C.Create((*C.WindowSettings)(unsafe.Pointer(settings))) 93 | cs := convertSettings(settings) 94 | cMenuDefs, count := convertMenuDefs(menuDefs) 95 | cCreate(cs, cMenuDefs, count) 96 | } 97 | 98 | func activate() { 99 | 100 | } 101 | 102 | func invokeJS(js string, fromMainThread bool) { 103 | cJs := C.CString(js) 104 | Log("invoke:", js) 105 | defer C.free(unsafe.Pointer(cJs)) 106 | 107 | cInvokeJS(cJs, BoolToCInt(fromMainThread)) 108 | } 109 | 110 | func exit() { 111 | cExit() 112 | } 113 | -------------------------------------------------------------------------------- /provider.h: -------------------------------------------------------------------------------- 1 | #ifndef _BRIDGE_ 2 | #define _BRIDGE_ 3 | 4 | #include "c/common.h" 5 | 6 | typedef enum MenuType { 7 | container, //just a container item for sub items 8 | custom, 9 | standard, 10 | separator 11 | } MenuType; 12 | 13 | typedef struct WindowSettings{ 14 | const char* title; 15 | const char* webDir; 16 | const char* index; 17 | const char* url; 18 | int left; 19 | int top; 20 | int width; 21 | int height; 22 | int resizable; 23 | int debug; 24 | } WindowSettings; 25 | 26 | typedef struct MenuDef{ 27 | const char* title; 28 | const char* action; 29 | const char* key; 30 | struct MenuDef* children; 31 | int childrenCount; 32 | MenuType menuType; 33 | } MenuDef; 34 | 35 | static MenuDef* allocMenuDefArray(int count) { 36 | if(count == 0) { 37 | return NULL; 38 | } 39 | return (MenuDef*)malloc(sizeof(MenuDef)*count); 40 | } 41 | 42 | static void addChildMenu(MenuDef* children, MenuDef child, int index) { 43 | children[index] = child; 44 | } 45 | 46 | 47 | #endif 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /provider_android.go: -------------------------------------------------------------------------------- 1 | // +build android 2 | // +build arm 386 amd64 arm64 3 | 4 | package goui 5 | 6 | /* 7 | #cgo LDFLAGS: -landroid -llog 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "provider.h" 14 | 15 | #define loge(...) __android_log_print(ANDROID_LOG_ERROR, "GoUI", __VA_ARGS__); 16 | #define logd(...) __android_log_print(ANDROID_LOG_DEBUG, "GoUI", __VA_ARGS__); 17 | 18 | static void alogd(const char *msg) { 19 | __android_log_print(ANDROID_LOG_DEBUG, "GoUI", "%s",msg); 20 | } 21 | 22 | extern void handleClientReq(const char* s); 23 | extern void invokeMain(uintptr_t ptr); 24 | 25 | JavaVM* jvm=0; 26 | jobject mainActivity = 0; 27 | jmethodID evalJSID = 0; 28 | jmethodID createWebViewID = 0; 29 | 30 | JNIEXPORT jint JNICALL 31 | JNI_OnLoad(JavaVM* vm, void* reserved) { 32 | JNIEnv* env; 33 | if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { 34 | return -1; 35 | } 36 | 37 | return JNI_VERSION_1_6; 38 | } 39 | 40 | void callCreateWebView(WindowSettings settings) { 41 | logd("url:%s",settings.url); 42 | if(createWebViewID!=NULL) { 43 | JNIEnv *env; 44 | (*jvm)->AttachCurrentThread(jvm,&env,NULL); 45 | (*env)->CallVoidMethod(env,mainActivity, createWebViewID, (*env)->NewStringUTF(env,settings.url)); 46 | } 47 | } 48 | 49 | 50 | JNIEXPORT jboolean JNICALL 51 | Java_org_fipress_goui_android_GoUIActivity_invokeGoMain( 52 | JNIEnv *env, 53 | jobject jobj) { 54 | (*env)->GetJavaVM(env,&jvm); 55 | mainActivity = (*env)->NewGlobalRef(env, jobj); 56 | jclass jcls = (*env)->GetObjectClass(env,jobj); 57 | evalJSID = (*env)->GetMethodID(env,jcls,"evalJavaScript","(Ljava/lang/String;)V"); 58 | createWebViewID = (*env)->GetMethodID(env,jcls,"loadWebView","(Ljava/lang/String;)V"); 59 | const char *dlsym_error = dlerror(); 60 | 61 | uintptr_t goMainPtr = (uintptr_t)dlsym(RTLD_DEFAULT, "main.main"); 62 | dlsym_error = dlerror(); 63 | if(dlsym_error) { 64 | loge("dlsym_error:%s",dlsym_error); 65 | return 0; 66 | } 67 | invokeMain(goMainPtr); 68 | return 1; 69 | } 70 | 71 | //to invoke java 72 | void invokeJS(const char* js) { 73 | if(evalJSID!=NULL) { 74 | JNIEnv *env; 75 | (*jvm)->AttachCurrentThread(jvm,&env,NULL); 76 | (*env)->CallVoidMethod(env,mainActivity, evalJSID, (*env)->NewStringUTF(env,js)); 77 | } 78 | } 79 | 80 | JNIEXPORT void JNICALL 81 | Java_org_fipress_goui_android_ScriptHandler_postMessage( 82 | JNIEnv *env, 83 | jobject jobj, 84 | jstring message) { 85 | const char *msg = (*env)->GetStringUTFChars(env,message,0); 86 | handleClientReq(msg); 87 | (*env)->ReleaseStringUTFChars(env,message,msg); 88 | } 89 | 90 | void create(WindowSettings settings) { 91 | callCreateWebView(settings); 92 | } 93 | 94 | void exitApp() { 95 | 96 | } 97 | */ 98 | import "C" 99 | import ( 100 | "unsafe" 101 | ) 102 | 103 | func cCreate(cs C.WindowSettings, cMenuDefs *C.MenuDef, count C.int) { 104 | C.create(cs) 105 | } 106 | 107 | func cActivate() { 108 | 109 | } 110 | 111 | func cInvokeJS(js *C.char) { 112 | C.invokeJS(js) 113 | } 114 | 115 | func cExit() { 116 | C.exitApp() 117 | } 118 | 119 | func Log(arg ...interface{}) { 120 | go func() { 121 | msg := fmt.Sprint(arg...) 122 | cMsg := C.CString(msg) 123 | defer C.free(unsafe.Pointer(cMsg)) 124 | C.alogd(cMsg) 125 | }() 126 | } 127 | -------------------------------------------------------------------------------- /provider_android_invoke.go: -------------------------------------------------------------------------------- 1 | // +build android 2 | // +build arm 386 amd64 arm64 3 | 4 | package goui 5 | 6 | import "C" 7 | 8 | //export invokeMain 9 | func invokeMain(ptr uintptr) { 10 | Log("invoke main") 11 | invoke.InvokeMain(ptr) 12 | } 13 | -------------------------------------------------------------------------------- /provider_android_invoke/invoke.go: -------------------------------------------------------------------------------- 1 | // +build android 2 | // +build arm 386 amd64 arm64 3 | 4 | // Package invoke is for invoking main.main 5 | // 6 | package provider_android_invoke 7 | 8 | // InvokeMain calls main.main by its address. 9 | func InvokeMain(ptr uintptr) 10 | -------------------------------------------------------------------------------- /provider_android_invoke/invoke_386.s: -------------------------------------------------------------------------------- 1 | #include "textflag.h" 2 | #include "funcdata.h" 3 | 4 | TEXT ·InvokeMain(SB),$0-4 5 | MOVL fn+0(FP), AX 6 | CALL AX 7 | RET 8 | -------------------------------------------------------------------------------- /provider_android_invoke/invoke_amd64.s: -------------------------------------------------------------------------------- 1 | #include "textflag.h" 2 | #include "funcdata.h" 3 | 4 | TEXT ·InvokeMain(SB),$0-8 5 | MOVQ fn+0(FP), AX 6 | CALL AX 7 | RET 8 | -------------------------------------------------------------------------------- /provider_android_invoke/invoke_arm.s: -------------------------------------------------------------------------------- 1 | #include "textflag.h" 2 | #include "funcdata.h" 3 | 4 | TEXT ·InvokeMain(SB),$0-4 5 | MOVW fn+0(FP), R0 6 | BL (R0) 7 | RET 8 | -------------------------------------------------------------------------------- /provider_android_invoke/invoke_arm64.s: -------------------------------------------------------------------------------- 1 | #include "textflag.h" 2 | #include "funcdata.h" 3 | 4 | TEXT ·InvokeMain(SB),$0-8 5 | MOVD fn+0(FP), R0 6 | BL (R0) 7 | RET 8 | -------------------------------------------------------------------------------- /provider_ios.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | // +build arm arm64 sim 3 | 4 | package goui 5 | 6 | /* 7 | #cgo darwin CFLAGS: -x objective-c 8 | #cgo darwin LDFLAGS: -framework WebKit -framework Foundation -framework UIKit 9 | 10 | #include 11 | #include 12 | #import 13 | #include 14 | #include 15 | #include "provider.h" 16 | 17 | extern void handleClientReq(const char* s); 18 | //extern void goLog(_GoString_ s); 19 | 20 | @interface GoUIMessageHandler : NSObject { 21 | } 22 | @end 23 | 24 | @implementation GoUIMessageHandler 25 | - (void)userContentController:(WKUserContentController *)userContentController 26 | didReceiveScriptMessage:(WKScriptMessage *)message { 27 | goUILog("didReceiveScriptMessage: %s\n",[message.name UTF8String]); 28 | if ([message.name isEqualToString:@"goui"]) { 29 | const char* str = [message.body UTF8String]; 30 | goUILog("Received event %s\n", str); 31 | handleClientReq(str); 32 | //(_GoString_){str, strlen(str)} 33 | } 34 | } 35 | @end 36 | 37 | WKWebView *webView; 38 | 39 | @interface ViewController : UIViewController 40 | 41 | @end 42 | 43 | @implementation ViewController 44 | 45 | - (void)viewDidLoad { 46 | [super viewDidLoad]; 47 | GoUIMessageHandler* handler = [[GoUIMessageHandler alloc] init]; 48 | WKUserContentController *userContentController = [[WKUserContentController alloc] init]; 49 | [userContentController addScriptMessageHandler:handler name:@"goui"]; 50 | 51 | WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; 52 | configuration.userContentController = userContentController; 53 | webView = [[WKWebView alloc] initWithFrame:[[UIScreen mainScreen] bounds] configuration:configuration]; 54 | [webView.configuration.preferences setValue:@YES forKey:@"developerExtrasEnabled"]; 55 | 56 | NSBundle *bundle = [NSBundle mainBundle]; 57 | NSString *path = [bundle pathForResource:@"ui/index" ofType:@"html"]; 58 | NSURL *nsURL = [NSURL fileURLWithPath:path isDirectory:false]; 59 | NSString *bundlePath = [[NSBundle mainBundle] resourcePath]; 60 | NSString *webPath = [bundlePath stringByAppendingString:@"/ui"]; 61 | NSURL *nsDir = [NSURL fileURLWithPath:webPath isDirectory:true]; 62 | NSLog(@"dir:%@",nsDir); 63 | [webView loadFileURL:nsURL allowingReadAccessToURL:nsDir]; 64 | [self.view addSubview:webView]; 65 | } 66 | 67 | @end 68 | 69 | 70 | @interface AppDelegate : UIResponder 71 | 72 | @property (strong, nonatomic) UIWindow *window; 73 | 74 | @end 75 | 76 | @implementation AppDelegate 77 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 78 | // Override point for customization after application launch. 79 | self.window = [[UIWindow alloc]initWithFrame:[[UIScreen mainScreen] bounds]]; 80 | //UIView *view = [[UIView alloc]initWithFrame:[[UIScreen mainScreen] bounds]]; 81 | ViewController *vc = [[ViewController alloc] init]; 82 | [self.window setRootViewController:vc]; 83 | [self.window makeKeyAndVisible]; 84 | 85 | return YES; 86 | } 87 | 88 | 89 | - (void)applicationWillResignActive:(UIApplication *)application { 90 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 91 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 92 | } 93 | 94 | 95 | - (void)applicationDidEnterBackground:(UIApplication *)application { 96 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 97 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 98 | } 99 | 100 | 101 | - (void)applicationWillEnterForeground:(UIApplication *)application { 102 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 103 | } 104 | 105 | 106 | - (void)applicationDidBecomeActive:(UIApplication *)application { 107 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 108 | } 109 | 110 | 111 | - (void)applicationWillTerminate:(UIApplication *)application { 112 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 113 | } 114 | 115 | 116 | @end 117 | 118 | void create() { 119 | @autoreleasepool { 120 | char* arg[] = {""}; 121 | UIApplicationMain(1, arg, nil, NSStringFromClass([AppDelegate class])); 122 | } 123 | } 124 | 125 | void invokeJS(const char *js) { 126 | [webView evaluateJavaScript:[NSString stringWithUTF8String:js] completionHandler:^(id _Nullable response, NSError * _Nullable error) { 127 | //goUILog("response:%s,error:%s",[response UTF8String],[error UTF8String]); 128 | }]; 129 | } 130 | 131 | void exitApp() { 132 | 133 | } 134 | */ 135 | import "C" 136 | 137 | func cCreate(cs C.WindowSettings, cMenuDefs *C.MenuDef, count C.int) { 138 | //cs := convertSettings(settings) 139 | C.create() 140 | } 141 | 142 | func cActivate() { 143 | 144 | } 145 | 146 | func cInvokeJS(js *C.char) { 147 | C.invokeJS(js) 148 | } 149 | 150 | func cExit() { 151 | //not supported 152 | } 153 | -------------------------------------------------------------------------------- /provider_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux,!android 2 | 3 | package goui 4 | 5 | /* 6 | #cgo linux openbsd freebsd pkg-config: gtk+-3.0 webkit2gtk-4.0 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "provider.h" 15 | //#include "_cgo_export.h" 16 | 17 | //extern void handleTest(); 18 | extern void handleClientReq(_GoString_ s); 19 | 20 | WebKitWebView* webview; 21 | 22 | static void evalJSDone(GObject *object, GAsyncResult *result, 23 | gpointer arg) { 24 | fprintf(stderr,"eval js finished\n"); 25 | } 26 | 27 | static int evalJS(const char *js) { 28 | fprintf(stderr,"eval js:%s\n",js); 29 | webkit_web_view_run_javascript(webview, js, NULL, 30 | evalJSDone, NULL); 31 | return 0; 32 | } 33 | 34 | //menu 35 | typedef void (*actionPtr)(void); 36 | 37 | typedef struct Action { 38 | actionPtr func; 39 | const char* name; 40 | } Action; 41 | 42 | const int actionCount = 1; 43 | const Action actionMap[] = { 44 | {>k_main_quit,"quit"} 45 | }; 46 | 47 | 48 | void menuAction(GtkMenuItem *menuitem, 49 | gpointer arg) { 50 | fprintf(stderr,"menu clicked:%s\n",(char *)arg); 51 | // 52 | } 53 | 54 | void standardMenuAction(GtkMenuItem *menuitem, 55 | gpointer arg) { 56 | const char* name = (const char *)arg; 57 | fprintf(stderr,"standard menu clicked:%s\n",name); 58 | 59 | for(int i=0;i 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include "provider.h" 17 | 18 | extern void menuClicked(_GoString_ s); 19 | extern void handleClientReq(const char* s); 20 | //extern void goLog(_GoString_ s); 21 | 22 | @interface GoUIMessageHandler : NSObject { 23 | } 24 | @end 25 | 26 | @implementation GoUIMessageHandler 27 | - (void)userContentController:(WKUserContentController *)userContentController 28 | didReceiveScriptMessage:(WKScriptMessage *)message { 29 | goUILog("didReceiveScriptMessage: %s\n",[message.name UTF8String]); 30 | if ([message.name isEqualToString:@"goui"]) { 31 | const char* str = [message.body UTF8String]; 32 | goUILog("Received event %s\n", str); 33 | handleClientReq(str); 34 | //(_GoString_){str, strlen(str)} 35 | } 36 | } 37 | @end 38 | 39 | //menu 40 | 41 | @interface CustomAction : NSObject 42 | @end 43 | 44 | @implementation CustomAction 45 | - (void)action:(id)sender { 46 | //NSLog(@"click menu:%@",[sender representedObject]); 47 | const char* str = [[sender representedObject] UTF8String]; 48 | //menuClicked((_GoString_){str, strlen(str)}); 49 | menuClicked((_GoString_){str, strlen(str)}); 50 | goUILog("click menu: %s\n",str); 51 | } 52 | @end 53 | 54 | @interface GoUIMenu : NSObject 55 | @end 56 | 57 | @implementation GoUIMenu 58 | static CustomAction* customAction; 59 | static NSDictionary* actionMap; 60 | + (void)initialize { 61 | customAction = [[CustomAction alloc] init]; 62 | actionMap = [NSDictionary dictionaryWithObjectsAndKeys: 63 | //app menu 64 | [NSValue valueWithPointer:@selector(orderFrontStandardAboutPanel:)], @"about", 65 | [NSValue valueWithPointer:@selector(hide:)], @"hide", 66 | [NSValue valueWithPointer:@selector(hideOtherApplications:)], @"hideothers", 67 | [NSValue valueWithPointer:@selector(unhideAllApplications:)], @"unhide", 68 | [NSValue valueWithPointer:@selector(terminate:)], @"quit", 69 | nil]; 70 | } 71 | 72 | NSString * utf8(const char* cs) { 73 | NSString *ns = @""; 74 | if(cs) { 75 | ns = [NSString stringWithUTF8String:cs]; 76 | } 77 | return ns; 78 | } 79 | 80 | NSMenuItem* createMenuItem(MenuDef def) { 81 | NSString *title = utf8(def.title); 82 | NSString *key = utf8(def.key); 83 | 84 | SEL act = NULL; 85 | if(def.menuType == standard) { 86 | id pointer = [actionMap objectForKey:[NSString stringWithUTF8String:def.action]]; 87 | if(pointer) { 88 | act = [pointer pointerValue]; 89 | } 90 | } else if(def.menuType == custom) { 91 | act = @selector(action:); 92 | } 93 | NSMenuItem *item = [[[NSMenuItem alloc] initWithTitle:title 94 | action: act 95 | keyEquivalent:key] 96 | autorelease]; 97 | if(def.menuType == custom) { 98 | [item setRepresentedObject:[NSString stringWithUTF8String:def.action]]; 99 | [item setTarget:customAction]; 100 | } 101 | return item; 102 | } 103 | 104 | NSMenuItem* createMenu(MenuDef def) { 105 | NSString *title = utf8(def.title); 106 | 107 | NSMenu *menu = [[[NSMenu alloc] initWithTitle:title] autorelease]; 108 | NSMenuItem *item = createMenuItem(def); 109 | [item setSubmenu:menu]; 110 | 111 | for(int i=0;i { 145 | @private 146 | NSView* _view; 147 | } 148 | @property (nonatomic, assign) NSView* view; 149 | @end 150 | 151 | @implementation WindowDelegate 152 | @synthesize view = _view; 153 | - (void)windowDidResize:(NSNotification *)notification { 154 | goUILog("windowDidResize\n"); 155 | } 156 | 157 | - (void)windowDidMiniaturize:(NSNotification *)notification{ 158 | goUILog("windowDidMiniaturize\n"); 159 | } 160 | - (void)windowDidEnterFullScreen:(NSNotification *)notification { 161 | goUILog("windowDidEnterFullScreen\n"); 162 | } 163 | - (void)windowDidExitFullScreen:(NSNotification *)notification { 164 | goUILog("windowDidExitFullScreen\n"); 165 | } 166 | - (void)windowDidBecomeKey:(NSNotification *)notification { 167 | goUILog("Window: become key\n"); 168 | } 169 | 170 | - (void)windowDidBecomeMain:(NSNotification *)notification { 171 | goUILog("Window: become main\n"); 172 | } 173 | 174 | - (void)windowDidResignKey:(NSNotification *)notification { 175 | goUILog("Window: resign key\n"); 176 | } 177 | 178 | - (void)windowDidResignMain:(NSNotification *)notification { 179 | goUILog("Window: resign main\n"); 180 | } 181 | 182 | - (void)windowWillClose:(NSNotification *)notification { 183 | [NSAutoreleasePool new]; 184 | goUILog("NSWindowDelegate::windowWillClose\n"); 185 | [NSApp terminate:NSApp]; 186 | } 187 | @end 188 | 189 | @interface GoUIWindow : NSObject { 190 | @private 191 | WKWebView* webView; 192 | } 193 | @end 194 | 195 | @implementation GoUIWindow 196 | //@synthesize webView = webView_; 197 | //static WKWebView* webView; 198 | - (void)create:(struct WindowSettings)settings { 199 | @autoreleasepool { 200 | WindowDelegate* delegate = [[WindowDelegate alloc] init]; 201 | NSRect rect = NSMakeRect(0, 0, settings.width, settings.height); 202 | id window = [[NSWindow alloc] 203 | initWithContentRect:rect 204 | styleMask:(NSWindowStyleMaskTitled | 205 | NSWindowStyleMaskClosable | 206 | NSWindowStyleMaskMiniaturizable | 207 | NSWindowStyleMaskResizable | 208 | NSWindowStyleMaskUnifiedTitleAndToolbar ) 209 | backing:NSBackingStoreBuffered 210 | defer:NO]; 211 | 212 | delegate.view = [window contentView]; 213 | [window setDelegate:delegate]; 214 | [window cascadeTopLeftFromPoint:NSMakePoint(settings.left,settings.top)]; 215 | [window setTitle:[NSString stringWithUTF8String:settings.title]]; 216 | 217 | GoUIMessageHandler* handler = [[GoUIMessageHandler alloc] init]; 218 | 219 | WKUserContentController *userContentController = [[WKUserContentController alloc] init]; 220 | [userContentController addScriptMessageHandler:handler name:@"goui"]; 221 | WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; 222 | configuration.userContentController = userContentController; 223 | 224 | webView = [[WKWebView alloc] initWithFrame:rect configuration:configuration]; 225 | [webView.configuration.preferences setValue:@YES forKey:@"developerExtrasEnabled"]; 226 | NSString *index = [NSString stringWithUTF8String:settings.index]; 227 | if([index hasPrefix:@"http"]) { 228 | NSURL *nsURL = [NSURL URLWithString:index]; 229 | NSURLRequest *requestObj = [NSURLRequest requestWithURL:nsURL]; 230 | [webView loadRequest:requestObj]; 231 | } else { 232 | NSString *bundlePath = [[NSBundle mainBundle] resourcePath]; 233 | NSString *dir = [bundlePath stringByAppendingPathComponent:[NSString stringWithUTF8String:settings.webDir]]; 234 | index = [dir stringByAppendingPathComponent:index]; 235 | goUILog("bundlePath:%s",[bundlePath UTF8String]); 236 | goUILog("dir:%s",[dir UTF8String]); 237 | goUILog("index:%s",[index UTF8String]); 238 | 239 | NSURL *nsURL = [NSURL fileURLWithPath:index isDirectory:false]; 240 | NSURL *nsDir = [NSURL fileURLWithPath:dir isDirectory:true]; 241 | [webView loadFileURL:nsURL allowingReadAccessToURL:nsDir]; 242 | } 243 | 244 | [[window contentView] addSubview:webView]; 245 | [window makeKeyAndOrderFront:nil]; 246 | } 247 | } 248 | 249 | -(void) evaluateJS:(NSString*)script { 250 | goUILog("evalue:%s",[script UTF8String]); 251 | [webView evaluateJavaScript:script completionHandler:^(id _Nullable response, NSError * _Nullable error) { 252 | //goUILog("response:%s,error:%s",[response UTF8String],[error UTF8String]); 253 | }]; 254 | } 255 | @end 256 | 257 | //app 258 | @interface ApplicationDelegate : NSObject { 259 | @private 260 | MenuDef* _menuDefs; 261 | int _menuCount; 262 | } 263 | @property (nonatomic, assign) struct MenuDef* menuDefs; 264 | @property (assign) int menuCount; 265 | @end 266 | 267 | @implementation ApplicationDelegate 268 | -(void)applicationWillFinishLaunching:(NSNotification *)aNotification 269 | { 270 | goUILog("applicationWillFinishLaunching\n"); 271 | [NSApplication sharedApplication]; 272 | goUILog("_menuCount: %d\n",_menuCount); 273 | if(_menuCount!=0) { 274 | [GoUIMenu buildMenu:_menuDefs count:_menuCount ]; 275 | } 276 | [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; 277 | } 278 | 279 | -(void)applicationDidFinishLaunching:(NSNotification *)notification 280 | { 281 | goUILog("applicationDidFinishLaunching\n"); 282 | [NSApplication sharedApplication]; 283 | 284 | [NSApp activateIgnoringOtherApps:YES]; 285 | } 286 | @end 287 | 288 | @interface GoUIApp : NSObject 289 | 290 | @end 291 | 292 | @implementation GoUIApp 293 | static GoUIWindow* window; 294 | 295 | +(void)initialize {} 296 | +(void)start:(WindowSettings)settings menuDefs:(struct MenuDef[])menuDefs menuCount: (int)menuCount { 297 | @autoreleasepool { 298 | [NSApplication sharedApplication]; 299 | [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; 300 | 301 | ApplicationDelegate* appDelegate = [[ApplicationDelegate alloc] init]; 302 | appDelegate.menuDefs = menuDefs; 303 | appDelegate.menuCount = menuCount; 304 | goUILog("menuCount: %d\n",menuCount); 305 | [NSApp setDelegate:appDelegate]; 306 | 307 | window = [[GoUIWindow alloc] init]; 308 | [window create:settings]; 309 | goUILog("run loop"); 310 | [NSApp run]; 311 | //run(); 312 | } 313 | } 314 | 315 | +(void)evaluateJS:(NSString*)js { 316 | [window evaluateJS:js]; 317 | } 318 | 319 | +(void)exit { 320 | [NSApp terminate:NSApp]; 321 | } 322 | @end 323 | 324 | void create(WindowSettings settings, MenuDef* menuDefs, int menuCount) { 325 | [GoUIApp start:settings menuDefs:menuDefs menuCount:menuCount]; 326 | } 327 | 328 | void invokeJS(const char *js,int fromMainThread) { 329 | goUILog("invokeJS async:%s",js); 330 | NSString* script = [NSString stringWithUTF8String:js]; 331 | if(fromMainThread) { 332 | [GoUIApp evaluateJS:script]; 333 | } else { 334 | dispatch_async_and_wait(dispatch_get_main_queue(),^{ 335 | [GoUIApp evaluateJS:script]; 336 | }); 337 | } 338 | } 339 | 340 | void exitApp() { 341 | [GoUIApp exit]; 342 | } 343 | */ 344 | import "C" 345 | 346 | func cCreate(cs C.WindowSettings, cMenuDefs *C.MenuDef, count C.int) { 347 | C.create(cs, cMenuDefs, count) 348 | } 349 | 350 | func cActivate() { 351 | 352 | } 353 | 354 | func cInvokeJS(js *C.char, fromMainThread C.int) { 355 | C.invokeJS(js, fromMainThread) 356 | } 357 | 358 | func cExit() { 359 | C.exitApp() 360 | } 361 | -------------------------------------------------------------------------------- /provider_windows.cpp: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | // To build the windows provider, you will need to have the building env for c++/winrt. 4 | // Since CGO uses gcc for windows and does not support the VC++ compiler 'cl.exe'. 5 | // That's why there is a 'ignore' tag above. 6 | // The easiest way to build a GoUI windows app is through [GoUI-cli](https://github.com/FIPress/GoUI-cli) 7 | // You will find instructions to manually build the provider in the readme. 8 | 9 | #define UNICODE 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include "provider_windows.h" 20 | 21 | #pragma comment(lib, "user32") 22 | #pragma comment(lib, "windowsapp") 23 | 24 | using namespace winrt; 25 | using namespace Windows::Foundation; 26 | using namespace Windows::Storage; 27 | using namespace Windows::Storage::Streams; 28 | using namespace Windows::Web; 29 | using namespace Windows::Web::UI; 30 | using namespace Windows::Web::UI::Interop; 31 | 32 | 33 | HWND hWnd = nullptr; 34 | fnHandleClientReq handleClientReq = nullptr; 35 | WebViewControl webView = nullptr; 36 | fnGoUILog fnLog = nullptr; 37 | 38 | static void goLog(const char* log) { 39 | if(fnLog != nullptr) { 40 | fnLog(log); 41 | } 42 | } 43 | 44 | namespace { 45 | class UriToStreamResolver : public winrt::implements 46 | { 47 | private: 48 | static hstring webBase; 49 | public: 50 | UriToStreamResolver() 51 | { 52 | } 53 | 54 | static void InitWebPath(const char* dir) { 55 | wchar_t cwd[MAX_PATH]; 56 | //_getcwd(cwd, MAX_PATH); 57 | GetModuleFileName(NULL, cwd, MAX_PATH); 58 | webBase = Uri(winrt::to_hstring(cwd), winrt::to_hstring("ui")).AbsoluteUri(); 59 | } 60 | 61 | IAsyncOperation UriToStreamAsync(Uri uri) const 62 | { 63 | Uri localUri = Uri(webBase+uri.Path()); 64 | auto fOp = StorageFile::GetFileFromPathAsync(localUri.AbsoluteUri()); 65 | 66 | StorageFile f = fOp.get(); 67 | auto sOp = f.OpenAsync(FileAccessMode::Read); 68 | IRandomAccessStream stream = sOp.get(); 69 | 70 | co_return stream.GetInputStreamAt(0); 71 | } 72 | 73 | }; 74 | 75 | } 76 | hstring UriToStreamResolver::webBase ; 77 | 78 | Rect getRect() { 79 | if (hWnd == nullptr ) { 80 | return Rect(); 81 | } 82 | 83 | RECT r; 84 | GetClientRect(hWnd, &r); 85 | return Rect(r.left, r.top, r.right - r.left, r.bottom - r.top); 86 | } 87 | 88 | void resize() { 89 | if (webView == nullptr) { 90 | return; 91 | } 92 | Rect rect = getRect(); 93 | webView.Bounds(rect); 94 | } 95 | 96 | void invokeScript(const char* js) { 97 | goUILog("invokeScript :%s",js); 98 | webView.InvokeScriptAsync( 99 | L"eval", single_threaded_vector({ winrt::to_hstring(js) })); 100 | } 101 | 102 | void createWebView(const char* dir, const char* index) { 103 | goUILog("create webview:%s",index); 104 | init_apartment(winrt::apartment_type::single_threaded); 105 | UriToStreamResolver::InitWebPath(dir); 106 | WebViewControlProcess wvProcess = WebViewControlProcess(); 107 | 108 | auto op = wvProcess.CreateWebViewControlAsync( 109 | reinterpret_cast(hWnd), getRect()); 110 | op.Completed([index](IAsyncOperation const& sender, AsyncStatus args) { 111 | webView = sender.GetResults(); 112 | webView.Settings().IsScriptNotifyAllowed(true); 113 | webView.IsVisible(true); 114 | 115 | webView.ScriptNotify([](auto const& sender, auto const& args) { 116 | if(handleClientReq != 0) { 117 | goUILog("handle Client Req"); 118 | handleClientReq(to_string(args.Value()).c_str()); 119 | } else { 120 | goUILog("no handler"); 121 | } 122 | 123 | }); 124 | /* 125 | webView.NavigationStarting([](auto const& sender, auto const& args) { 126 | }); 127 | */ 128 | auto uri = webView.BuildLocalStreamUri(L"GoUI", winrt::to_hstring(index)); 129 | auto resolver = winrt::make_self(); 130 | IUriToStreamResolver r = resolver.as(); 131 | webView.NavigateToLocalStreamUri(uri, r); 132 | 133 | //Windows::Foundation::Uri uri{ L"ms-appx-web:///web/index.html" }; 134 | //webView.Navigate(uri); 135 | }); 136 | } 137 | 138 | //The Microsoft Edge WebView2 control enables you to host web content in your application using Microsoft Edge(Chromium) as the rendering engine. 139 | /* 140 | void createWebView2(HWND hWnd) { 141 | ComPtr webviewWindow; 142 | // Known issue - app needs to run on PerMonitorV2 DPI awareness for WebView to look properly 143 | SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); 144 | // Locate the browser and set up the environment for WebView 145 | // Use CreateWebView2EnvironmentWithDetails if you need to specify browser location, user folder, etc. 146 | CreateWebView2Environment( 147 | Callback( 148 | [&webviewWindow, hWnd](HRESULT result, IWebView2Environment * env) -> HRESULT { 149 | // Create a WebView, whose parent is the main window hWnd 150 | env->CreateWebView(hWnd, Callback 151 | ([&webviewWindow, hWnd](HRESULT result, IWebView2WebView * webview) -> HRESULT { 152 | if (webview != nullptr) { 153 | webviewWindow = webview; 154 | } 155 | // Add a few settings for the webview 156 | // this is a redundant demo step as they are the default settings values 157 | IWebView2Settings* Settings; 158 | webviewWindow->get_Settings(&Settings); 159 | Settings->put_IsScriptEnabled(TRUE); 160 | Settings->put_AreDefaultScriptDialogsEnabled(TRUE); 161 | Settings->put_IsWebMessageEnabled(TRUE); 162 | // Resize WebView to fit the bounds of the parent window 163 | RECT bounds; 164 | GetClientRect(hWnd, &bounds); 165 | webviewWindow->put_Bounds(bounds); 166 | // Schedule an async task to navigate to Bing 167 | webviewWindow->Navigate(L"https://www.bing.com/"); 168 | // Step 4 - Navigation events 169 | // Step 5 - Scripting 170 | // Step 6 - Communication between host and web content 171 | return S_OK; 172 | }).Get()); 173 | return S_OK; 174 | }).Get()); 175 | 176 | }*/ 177 | 178 | void closeWindow() { 179 | PostQuitMessage(0); 180 | } 181 | 182 | LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 183 | { 184 | switch (message) 185 | { 186 | case WM_SIZE: 187 | resize(); 188 | break; 189 | case WM_CLOSE: 190 | DestroyWindow(hWnd); 191 | break; 192 | case WM_DESTROY: 193 | closeWindow(); 194 | break; 195 | default: 196 | return DefWindowProc(hWnd, message, wParam, lParam); 197 | break; 198 | } 199 | 200 | return 0; 201 | } 202 | 203 | void createWindow(WindowSettings settings) { 204 | HINSTANCE hInst = winrt::check_pointer(GetModuleHandle(nullptr)); 205 | const auto className = L"GoUIWindow"; 206 | WNDCLASSEX wce; 207 | ZeroMemory(&wce, sizeof(WNDCLASSEX)); 208 | wce.cbSize = sizeof(WNDCLASSEX); 209 | wce.style = CS_HREDRAW | CS_VREDRAW; 210 | wce.lpfnWndProc = WndProc; 211 | wce.cbClsExtra = 0; 212 | wce.cbWndExtra = 0; 213 | wce.hInstance = hInst; 214 | 215 | wce.lpszClassName = className; 216 | check_bool(::RegisterClassExW(&wce)); 217 | 218 | hWnd = check_pointer(CreateWindow( 219 | className, 220 | to_hstring(settings.title).c_str(), 221 | WS_OVERLAPPEDWINDOW, 222 | CW_USEDEFAULT, CW_USEDEFAULT, 223 | settings.width, 224 | settings.height, 225 | NULL, 226 | NULL, 227 | hInst, 228 | NULL 229 | )); 230 | 231 | ShowWindow(hWnd, 232 | SW_SHOW); 233 | UpdateWindow(hWnd); 234 | SetFocus(hWnd); 235 | } 236 | 237 | using dispatch_fn_t = std::function; 238 | 239 | void run() { 240 | MSG msg; 241 | BOOL res; 242 | while ((res = GetMessage(&msg, nullptr, 0, 0)) != -1) { 243 | if (msg.hwnd) { 244 | TranslateMessage(&msg); 245 | DispatchMessage(&msg); 246 | continue; 247 | } 248 | if (msg.message == WM_APP) { 249 | auto f = (dispatch_fn_t*)(msg.lParam); 250 | (*f)(); 251 | delete f; 252 | } 253 | else if (msg.message == WM_QUIT) { 254 | return; 255 | } 256 | } 257 | } 258 | 259 | void __cdecl seLogger(fnGoUILog fn) { 260 | fnLog = fn; 261 | } 262 | 263 | void __cdecl setHandleClientReq(fnHandleClientReq fn) { 264 | handleClientReq = fn; 265 | } 266 | 267 | void __cdecl create(WindowSettings settings, MenuDef* menuDefs, int menuCount) { 268 | createWindow(settings); 269 | createWebView(settings.webDir, settings.index); 270 | run(); 271 | } 272 | 273 | void __cdecl invokeJS(const char* js) { 274 | invokeScript(js); 275 | } 276 | 277 | void __cdecl exitWebview() { 278 | closeWindow(); 279 | } 280 | -------------------------------------------------------------------------------- /provider_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | // +build dev prod 3 | 4 | package goui 5 | 6 | /* 7 | // LDFLAGS will be set by Env CGO_LDFLAGS when build to get the real path of the dll 8 | // #cgo LDFLAGS: -static ${SRCDIR}/provider_windows.dll 9 | 10 | #include "provider_windows.h" 11 | #include "provider.h" 12 | 13 | extern void menuClicked(_GoString_ s); 14 | extern void handleClientReq(const char* s); 15 | 16 | void createApp(WindowSettings settings, MenuDef* menuDefs, int menuCount) { 17 | seLogger(&goLog); 18 | setHandleClientReq(&handleClientReq); 19 | //goUILog("settings.url:%s",settings.url); 20 | create(settings, menuDefs, menuCount); 21 | } 22 | 23 | void invokeScript(const char* js) { 24 | invokeJS(js); 25 | } 26 | 27 | void exitApp() { 28 | exitWebview(); 29 | } 30 | 31 | */ 32 | import "C" 33 | 34 | func cCreate(cs C.WindowSettings, cMenuDefs *C.MenuDef, count C.int) { 35 | C.createApp(cs, cMenuDefs, count) 36 | } 37 | 38 | func cInvokeJS(js *C.char) { 39 | C.invokeScript(js) 40 | } 41 | 42 | func cExit() { 43 | C.exitApp() 44 | } 45 | -------------------------------------------------------------------------------- /provider_windows.h: -------------------------------------------------------------------------------- 1 | #include "provider.h" 2 | 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | typedef void (*fnGoUILog)(const char*); 8 | typedef void (*fnMenuClicked)(const char*); 9 | typedef void (*fnHandleClientReq)(const char*); 10 | 11 | __declspec(dllexport) void __cdecl seLogger(fnGoUILog fn); 12 | __declspec(dllexport) void __cdecl setHandleClientReq(fnHandleClientReq fn); 13 | 14 | __declspec(dllexport) void __cdecl create(WindowSettings settings, MenuDef* menuDefs, int menuCount); 15 | __declspec(dllexport) void __cdecl invokeJS(const char* js); 16 | __declspec(dllexport) void __cdecl exitWebview(); 17 | 18 | #ifdef __cplusplus 19 | } 20 | #endif -------------------------------------------------------------------------------- /provider_windows_mock.go: -------------------------------------------------------------------------------- 1 | // +build windows,!dev,!prod 2 | 3 | package goui 4 | 5 | /* 6 | #include "provider.h" 7 | */ 8 | import "C" 9 | 10 | // this file is just for debug or testing purpose 11 | // when there is no need for a real windows provider, yet need one to get built 12 | 13 | func cCreate(cs C.WindowSettings, cMenuDefs *C.MenuDef, count C.int) { 14 | } 15 | 16 | func cInvokeJS(js *C.char) { 17 | } 18 | 19 | func cExit() { 20 | } 21 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import "C" 4 | 5 | import ( 6 | "encoding/json" 7 | ) 8 | 9 | type Request struct { 10 | Url string `json:"url"` 11 | IsCallback bool `json:"isCallback"` 12 | Context `json:",inline"` 13 | } 14 | 15 | var cbMap = make(map[string]func(string)) 16 | 17 | //export handleClientReq 18 | func handleClientReq(msg *C.char) { 19 | message := C.GoString(msg) 20 | Log("ClientHandler:", message) 21 | 22 | req := new(Request) 23 | err := json.Unmarshal([]byte(message), req) 24 | if err != nil { 25 | Log("unmarshal error:", err) 26 | return 27 | } 28 | 29 | if req.IsCallback { 30 | f := cbMap[req.Url] 31 | if f != nil { 32 | f(req.Data) 33 | delete(cbMap, req.Url) 34 | } 35 | } else { 36 | ctx := &req.Context 37 | handler, params := dispatch(req.Url) 38 | if handler != nil { 39 | ctx.params = params 40 | handler(ctx) 41 | } else { 42 | notFound(ctx) 43 | } 44 | } 45 | 46 | } 47 | 48 | func notFound(ctx *Context) { 49 | ctx.Error("Function not found ") 50 | } 51 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | type route struct { 8 | handler func(*Context) 9 | paras []string //named parameters 10 | regex *regexp.Regexp //if there are parameters 11 | } 12 | 13 | var ( 14 | //optionalPart = regexp.MustCompile(`\((.*?)\)`) 15 | namedPart = regexp.MustCompile(`:([^/]+)`) 16 | //splatPart = regexp.MustCompile(`\*\w+`) 17 | escapeRegExp = regexp.MustCompile(`([\-{}\[\]+?.,\\\^$|#\s])`) 18 | 19 | routes = make(map[string]*route) 20 | ) 21 | 22 | func parseRoute(pattern string, route *route) { 23 | params := namedPart.FindAllStringSubmatch(pattern, -1) 24 | if params != nil { 25 | l := len(params) 26 | route.paras = make([]string, l) 27 | 28 | //fmt.Println(params) 29 | for i, param := range params { 30 | route.paras[i] = param[1] 31 | //fmt.Println(param[1]) 32 | } 33 | pattern = namedPart.ReplaceAllString(pattern, `([^/]+)`) 34 | route.regex = regexp.MustCompile(pattern) 35 | } 36 | routes[pattern] = route 37 | } 38 | 39 | func dispatch(url string) (handler func(*Context), params map[string]string) { 40 | for key, route := range routes { 41 | if route.regex == nil { 42 | if key == url { 43 | handler = route.handler 44 | return 45 | } 46 | 47 | } else { 48 | matches := route.regex.FindAllStringSubmatch(url, -1) 49 | if matches != nil && len(matches) == 1 { 50 | params = make(map[string]string) 51 | vals := matches[0][1:] 52 | l, lkey := len(vals), len(route.paras) 53 | if l > lkey { 54 | l = lkey 55 | } 56 | for i := 0; i < l; i++ { 57 | params[route.paras[i]] = vals[i] 58 | } 59 | handler = route.handler 60 | return 61 | } 62 | } 63 | } 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import "testing" 4 | 5 | func TestParse(t *testing.T) { 6 | url := "book/get/:id/:name" 7 | route := new(route) 8 | parseRoute(url, route) 9 | 10 | _, params := dispatch("book/get/12/my book") 11 | t.Log(params) 12 | } 13 | -------------------------------------------------------------------------------- /screenshots/debug-go.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/screenshots/debug-go.jpeg -------------------------------------------------------------------------------- /screenshots/debug-web-android.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/screenshots/debug-web-android.jpeg -------------------------------------------------------------------------------- /screenshots/debug-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/screenshots/debug-web.png -------------------------------------------------------------------------------- /screenshots/editor-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/screenshots/editor-mac.png -------------------------------------------------------------------------------- /screenshots/editor-ubuntu.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/screenshots/editor-ubuntu.jpeg -------------------------------------------------------------------------------- /screenshots/hello-android.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/screenshots/hello-android.jpeg -------------------------------------------------------------------------------- /screenshots/hello-ios.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/screenshots/hello-ios.jpeg -------------------------------------------------------------------------------- /screenshots/hello-mac.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/screenshots/hello-mac.jpeg -------------------------------------------------------------------------------- /screenshots/hello-ubuntu.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/screenshots/hello-ubuntu.jpeg -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | package goui 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/fipress/fml" 6 | "sync" 7 | ) 8 | 9 | const filename = "store" 10 | 11 | type storage struct { 12 | store *fml.FML 13 | } 14 | 15 | var store *storage 16 | var once sync.Once 17 | 18 | func Storage() *storage { 19 | once.Do(func() { 20 | f, err := fml.Load(filename) 21 | if err != nil { 22 | Log("storage - open file failed:", err) 23 | f = fml.NewFml() 24 | } 25 | store = &storage{f} 26 | }) 27 | return store 28 | } 29 | 30 | func (s *storage) Put(key string, v interface{}) { 31 | s.store.SetValue(key, v) 32 | s.store.WriteToFile(filename) 33 | } 34 | 35 | func (s *storage) GetInt(key string) int { 36 | return s.store.GetInt(key) 37 | } 38 | 39 | func (s *storage) GetString(key string) string { 40 | return s.store.GetString(key) 41 | } 42 | 43 | func (s *storage) PutStruct(key string, v interface{}) (err error) { 44 | b, err := json.Marshal(v) 45 | if err != nil { 46 | Log("Put struct - marshal error:", err) 47 | return 48 | } 49 | s.store.SetValue(key, string(b)) 50 | s.store.WriteToFile(filename) 51 | return 52 | } 53 | 54 | func (s *storage) GetStruct(key string, v interface{}) (err error) { 55 | str := s.store.GetString(key) 56 | if str != "" { 57 | err = json.Unmarshal([]byte(str), v) 58 | if err != nil { 59 | Log("Get struct - unmarshal error:", err) 60 | return 61 | } 62 | } 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /util.c: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | extern void goLog(const char *s); 3 | static const int bufSize = 512; 4 | 5 | inline void goUILog(const char *format, ...) { 6 | char buf[bufSize]; 7 | va_list args; 8 | va_start(args,format); 9 | int len = vsnprintf(buf,bufSize, format,args); 10 | 11 | if(len < bufSize) { 12 | goLog(buf); 13 | } else { 14 | len++; 15 | char *tempBuf = 0; 16 | tempBuf = (char *)malloc(sizeof(char)*len); 17 | if(tempBuf != 0) { 18 | vsnprintf(tempBuf,len, format,args); 19 | goLog(tempBuf); 20 | free(tempBuf); 21 | } 22 | } 23 | va_end(args); 24 | } 25 | -------------------------------------------------------------------------------- /util.h: -------------------------------------------------------------------------------- 1 | #ifndef _GOUI_UTIL_ 2 | #define _GOUI_UTIL_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | void goUILog(const char *format, ...); 10 | 11 | inline int notEmpty(const char* s) { 12 | return s!=0 && s[0]!='\0'; 13 | } 14 | 15 | 16 | #endif -------------------------------------------------------------------------------- /util_cocoa.h: -------------------------------------------------------------------------------- 1 | #ifndef _GOUI_UTIL_OJ_ 2 | #define _GOUI_UTIL_OJ_ 3 | 4 | #include 5 | #include "util.h" 6 | 7 | inline NSString * utf8WithLength(const char* cs, int len) { 8 | NSString *ns = @""; 9 | if(cs!=NULL && cs[0]!='\0') { 10 | if(len==0) { 11 | ns = [NSString stringWithUTF8String:cs]; 12 | } else { 13 | NSString *s = [[NSString alloc] initWithBytes:cs length:len encoding:NSUTF8StringEncoding]; 14 | ns = [s autorelease]; 15 | } 16 | 17 | } 18 | return ns; 19 | } 20 | 21 | inline NSString * utf8(const char* cs) { 22 | return utf8WithLength(cs,0); 23 | } 24 | 25 | #endif -------------------------------------------------------------------------------- /widget/filepicker/filepicker.go: -------------------------------------------------------------------------------- 1 | package filepicker 2 | 3 | /* 4 | 5 | */ 6 | import "C" 7 | import goui "github.com/fipress/GoUI" 8 | 9 | type Settings struct { 10 | IsSave bool `json:"isSave"` //save or open, default to open 11 | Title string `json:"title"` 12 | Message string `json:"message"` 13 | Multiple bool `json:"multiple"` 14 | FileOnly bool `json:"fileOnly"` 15 | DirOnly bool `json:"dirOnly"` 16 | Accept string `json:"accept"` 17 | AllowsOtherFileTypes bool `json:"allowsOtherFileTypes"` 18 | StartLocation string `json:"startLocation"` 19 | 20 | SuggestedFileName string `json:"suggestedFileName"` 21 | CanCreateDir bool `json:"canCreateDir"` 22 | ShowsHiddenFiles bool `json:"showsHiddenFiles"` 23 | 24 | //ViewMode 25 | //FutureAccessList 26 | //MostRecentlyUsedList 27 | } 28 | 29 | func BoolToCInt(b bool) (i C.int) { 30 | if b { 31 | i = 1 32 | } 33 | return 34 | } 35 | 36 | /* 37 | func convertSettings(settings *Settings) C.Settings { 38 | return C.Settings{C.CString(settings.Message), 39 | C.CString(settings.AllowedFileTypes), 40 | C.CString(settings.StartLocation), 41 | C.CString(settings.SuggestedFileName), 42 | BoolToCInt(settings.AllowsMultiple), 43 | BoolToCInt(settings.AllowsFile), 44 | BoolToCInt(settings.AllowsDir), 45 | BoolToCInt(settings.AllowsOtherFileTypes), 46 | BoolToCInt(settings.CanCreateDir), 47 | BoolToCInt(settings.ShowsHiddenFiles), 48 | } 49 | }*/ 50 | 51 | type FilePicker struct { 52 | } 53 | 54 | func (f *FilePicker) Register() { 55 | goui.Service("filepicker", func(context *goui.Context) { 56 | s := new(Settings) 57 | err := context.GetEntity(&s) 58 | if err != nil { 59 | goui.Logf("Get filepicker settings failed:", err.Error()) 60 | context.Success("") 61 | } else { 62 | //action := context.GetParam("action") 63 | filename := openFilePicker(s) 64 | context.Success(filename) 65 | } 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /widget/filepicker/filepicker_macOS.go: -------------------------------------------------------------------------------- 1 | // +build darwin,amd64,!sim 2 | 3 | package filepicker 4 | 5 | /* 6 | 7 | #cgo darwin CFLAGS: -x objective-c 8 | #cgo darwin LDFLAGS: -framework Cocoa -framework WebKit 9 | 10 | #include 11 | #include "../../c/common.h" 12 | 13 | extern void filePicked(const char* s); 14 | 15 | //title, nameLabel, message, canCreateDir, showsHidden, can 16 | 17 | typedef struct FilePickerSettings{ 18 | const char* title; 19 | const char* message; 20 | const char* fileTypes; 21 | const char* startLocation; 22 | const char* suggestedFilename; 23 | int multiple; 24 | int allowsFile; 25 | int allowsDir; 26 | int allowsOtherFileTypes; 27 | int canCreateDir; 28 | int showsHiddenFiles; 29 | } FilePickerSettings; 30 | 31 | void setProperties(NSSavePanel* panel, FilePickerSettings s ) { 32 | [panel setAllowedFileTypes:[[NSString stringWithUTF8String:s.fileTypes] componentsSeparatedByString:@";"]]; 33 | [panel setAllowsOtherFileTypes:s.allowsOtherFileTypes]; 34 | if(notEmpty(s.message)) { 35 | [panel setMessage:[NSString stringWithUTF8String:s.message]]; 36 | } 37 | if(notEmpty(s.title)) { 38 | [panel setTitle:[NSString stringWithUTF8String:s.title]]; 39 | } 40 | 41 | if(notEmpty(s.startLocation)) { 42 | [panel setDirectoryURL:[NSURL URLWithString:[NSString stringWithUTF8String:s.startLocation]]]; 43 | } 44 | 45 | [panel setCanCreateDirectories:s.canCreateDir]; 46 | [panel setShowsHiddenFiles:s.showsHiddenFiles]; 47 | } 48 | 49 | char* openFilePicker(FilePickerSettings s) { 50 | NSOpenPanel *panel = [NSOpenPanel openPanel]; 51 | [panel setAllowsMultipleSelection:s.multiple]; 52 | [panel setCanChooseDirectories:s.allowsDir]; 53 | [panel setCanChooseFiles:s.allowsFile]; 54 | 55 | setProperties(panel,s); 56 | 57 | if ([panel runModal] == NSModalResponseOK) { 58 | int count = panel.URLs.count; 59 | if(count == 1) { 60 | NSString *path = [panel.URLs.firstObject path]; 61 | return (char*)[path UTF8String]; 62 | } else { 63 | NSMutableString *paths = [NSMutableString stringWithString: [panel.URLs.firstObject path]]; 64 | for(int i=1;i 5 | #include "window.h" 6 | 7 | */ 8 | import "C" 9 | import ( 10 | "os" 11 | "path" 12 | "runtime" 13 | "unsafe" 14 | ) 15 | 16 | const defaultDir = "ui" 17 | const defaultIndex = "index.html" 18 | 19 | func BoolToCInt(b bool) (i C.int) { 20 | if b { 21 | i = 1 22 | } 23 | return 24 | } 25 | 26 | func convertSettings(settings Settings) C.WindowSettings { 27 | //dir := path.Dir(settings.Url) 28 | if settings.UIDir == "" { 29 | settings.UIDir = defaultDir 30 | } 31 | 32 | if settings.Index == "" { 33 | settings.Index = defaultIndex 34 | } 35 | 36 | if settings.Url == "" { 37 | settings.Url = path.Join(settings.UIDir, settings.Index) 38 | if runtime.GOOS == "linux" { 39 | wd, _ := os.Getwd() 40 | settings.Url = path.Join("file://", wd, settings.Url) 41 | } else if runtime.GOOS == "android" { 42 | settings.Url = path.Join("file:///android_asset/", settings.Url) 43 | } 44 | } 45 | 46 | // windows needs WebDir and Index 47 | // macOS and iOS need Url 48 | 49 | return C.WindowSettings{C.CString(settings.Title), 50 | C.CString(settings.UIDir), 51 | //C.CString(abs), 52 | C.CString(settings.Index), 53 | C.CString(settings.Url), 54 | C.int(settings.Left), 55 | C.int(settings.Top), 56 | C.int(settings.Width), 57 | C.int(settings.Height), 58 | BoolToCInt(settings.Resizable), 59 | BoolToCInt(settings.Debug), 60 | } 61 | } 62 | 63 | func create(settings Settings, menuDefs []MenuDef) { 64 | //C.Create((*C.WindowSettings)(unsafe.Pointer(settings))) 65 | cs := convertSettings(settings) 66 | cMenuDefs, count := convertMenuDefs(menuDefs) 67 | cCreate(cs, cMenuDefs, count) 68 | } 69 | 70 | func activate() { 71 | 72 | } 73 | 74 | func invokeJS(js string, fromMainThread bool) { 75 | cJs := C.CString(js) 76 | Log("invoke:", js) 77 | defer C.free(unsafe.Pointer(cJs)) 78 | 79 | cInvokeJS(cJs, BoolToCInt(fromMainThread)) 80 | } 81 | 82 | func exit() { 83 | cExit() 84 | } 85 | -------------------------------------------------------------------------------- /window.h: -------------------------------------------------------------------------------- 1 | #ifndef _GOUI_WINDOW_ 2 | #define _GOUI_WINDOW_ 3 | 4 | typedef struct WindowSettings{ 5 | const char* title; 6 | const char* webDir; 7 | const char* index; 8 | const char* url; 9 | int left; 10 | int top; 11 | int width; 12 | int height; 13 | int resizable; 14 | int debug; 15 | } WindowSettings; 16 | 17 | #endif -------------------------------------------------------------------------------- /window_macos.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | // +build amd64,!sim 3 | 4 | package goui 5 | 6 | /* 7 | #cgo darwin CFLAGS: -x objective-c 8 | #cgo darwin LDFLAGS: -framework Cocoa -framework WebKit 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include "util_cocoa.h" 17 | #include "menu_macos.h" 18 | #include "window.h" 19 | 20 | extern void handleClientReq(const char* s); 21 | 22 | @interface GoUIMessageHandler : NSObject { 23 | } 24 | @end 25 | 26 | @implementation GoUIMessageHandler 27 | - (void)userContentController:(WKUserContentController *)userContentController 28 | didReceiveScriptMessage:(WKScriptMessage *)message { 29 | goUILog("didReceiveScriptMessage: %s\n",[message.name UTF8String]); 30 | if ([message.name isEqualToString:@"goui"]) { 31 | const char* str = [message.body UTF8String]; 32 | goUILog("Received event %s\n", str); 33 | handleClientReq(str); 34 | //(_GoString_){str, strlen(str)} 35 | } 36 | } 37 | @end 38 | 39 | 40 | @interface WindowDelegate : NSObject { 41 | @private 42 | NSView* _view; 43 | } 44 | @property (nonatomic, assign) NSView* view; 45 | @end 46 | 47 | @implementation WindowDelegate 48 | @synthesize view = _view; 49 | - (void)windowDidResize:(NSNotification *)notification { 50 | goUILog("windowDidResize\n"); 51 | } 52 | 53 | - (void)windowDidMiniaturize:(NSNotification *)notification{ 54 | goUILog("windowDidMiniaturize\n"); 55 | } 56 | - (void)windowDidEnterFullScreen:(NSNotification *)notification { 57 | goUILog("windowDidEnterFullScreen\n"); 58 | } 59 | - (void)windowDidExitFullScreen:(NSNotification *)notification { 60 | goUILog("windowDidExitFullScreen\n"); 61 | } 62 | - (void)windowDidBecomeKey:(NSNotification *)notification { 63 | goUILog("Window: become key\n"); 64 | } 65 | 66 | - (void)windowDidBecomeMain:(NSNotification *)notification { 67 | goUILog("Window: become main\n"); 68 | } 69 | 70 | - (void)windowDidResignKey:(NSNotification *)notification { 71 | goUILog("Window: resign key\n"); 72 | } 73 | 74 | - (void)windowDidResignMain:(NSNotification *)notification { 75 | goUILog("Window: resign main\n"); 76 | } 77 | 78 | - (void)windowWillClose:(NSNotification *)notification { 79 | [NSAutoreleasePool new]; 80 | goUILog("NSWindowDelegate::windowWillClose\n"); 81 | [NSApp terminate:NSApp]; 82 | } 83 | @end 84 | 85 | @interface GoUIWindow : NSObject { 86 | @private 87 | WKWebView* webView; 88 | } 89 | @end 90 | 91 | @implementation GoUIWindow 92 | //@synthesize webView = webView_; 93 | //static WKWebView* webView; 94 | - (void)create:(struct WindowSettings)settings { 95 | @autoreleasepool { 96 | WindowDelegate* delegate = [[WindowDelegate alloc] init]; 97 | NSRect rect = NSMakeRect(0, 0, settings.width, settings.height); 98 | id window = [[NSWindow alloc] 99 | initWithContentRect:rect 100 | styleMask:(NSWindowStyleMaskTitled | 101 | NSWindowStyleMaskClosable | 102 | NSWindowStyleMaskMiniaturizable | 103 | NSWindowStyleMaskResizable | 104 | NSWindowStyleMaskUnifiedTitleAndToolbar ) 105 | backing:NSBackingStoreBuffered 106 | defer:NO]; 107 | 108 | delegate.view = [window contentView]; 109 | [window setDelegate:delegate]; 110 | [window cascadeTopLeftFromPoint:NSMakePoint(settings.left,settings.top)]; 111 | [window setTitle:[NSString stringWithUTF8String:settings.title]]; 112 | 113 | GoUIMessageHandler* handler = [[GoUIMessageHandler alloc] init]; 114 | 115 | WKUserContentController *userContentController = [[WKUserContentController alloc] init]; 116 | [userContentController addScriptMessageHandler:handler name:@"goui"]; 117 | WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; 118 | configuration.userContentController = userContentController; 119 | 120 | webView = [[WKWebView alloc] initWithFrame:rect configuration:configuration]; 121 | [webView.configuration.preferences setValue:@YES forKey:@"developerExtrasEnabled"]; 122 | NSString *index = [NSString stringWithUTF8String:settings.index]; 123 | if([index hasPrefix:@"http"]) { 124 | NSURL *nsURL = [NSURL URLWithString:index]; 125 | NSURLRequest *requestObj = [NSURLRequest requestWithURL:nsURL]; 126 | [webView loadRequest:requestObj]; 127 | } else { 128 | NSString *bundlePath = [[NSBundle mainBundle] resourcePath]; 129 | NSString *dir = [bundlePath stringByAppendingPathComponent:[NSString stringWithUTF8String:settings.webDir]]; 130 | index = [dir stringByAppendingPathComponent:index]; 131 | goUILog("bundlePath:%s",[bundlePath UTF8String]); 132 | goUILog("dir:%s",[dir UTF8String]); 133 | goUILog("index:%s",[index UTF8String]); 134 | 135 | NSURL *nsURL = [NSURL fileURLWithPath:index isDirectory:false]; 136 | NSURL *nsDir = [NSURL fileURLWithPath:dir isDirectory:true]; 137 | [webView loadFileURL:nsURL allowingReadAccessToURL:nsDir]; 138 | } 139 | 140 | [[window contentView] addSubview:webView]; 141 | [window makeKeyAndOrderFront:nil]; 142 | } 143 | } 144 | 145 | -(void) evaluateJS:(NSString*)script { 146 | goUILog("evalue:%s",[script UTF8String]); 147 | [webView evaluateJavaScript:script completionHandler:^(id _Nullable response, NSError * _Nullable error) { 148 | //goUILog("response:%s,error:%s",[response UTF8String],[error UTF8String]); 149 | }]; 150 | } 151 | @end 152 | 153 | //app 154 | @interface ApplicationDelegate : NSObject { 155 | @private 156 | MenuDef* _menuDefs; 157 | int _menuCount; 158 | } 159 | @property (nonatomic, assign) struct MenuDef* menuDefs; 160 | @property (assign) int menuCount; 161 | @end 162 | 163 | @implementation ApplicationDelegate 164 | -(void)applicationWillFinishLaunching:(NSNotification *)aNotification 165 | { 166 | goUILog("applicationWillFinishLaunching\n"); 167 | [NSApplication sharedApplication]; 168 | goUILog("_menuCount: %d\n",_menuCount); 169 | if(_menuCount!=0) { 170 | [GoUIMenu buildMenu:_menuDefs count:_menuCount ]; 171 | } 172 | [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; 173 | } 174 | 175 | -(void)applicationDidFinishLaunching:(NSNotification *)notification 176 | { 177 | goUILog("applicationDidFinishLaunching\n"); 178 | [NSApplication sharedApplication]; 179 | 180 | [NSApp activateIgnoringOtherApps:YES]; 181 | } 182 | @end 183 | 184 | @interface GoUIApp : NSObject 185 | 186 | @end 187 | 188 | @implementation GoUIApp 189 | static GoUIWindow* window; 190 | 191 | +(void)initialize {} 192 | +(void)start:(WindowSettings)settings menuDefs:(struct MenuDef[])menuDefs menuCount: (int)menuCount { 193 | @autoreleasepool { 194 | [NSApplication sharedApplication]; 195 | [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; 196 | 197 | ApplicationDelegate* appDelegate = [[ApplicationDelegate alloc] init]; 198 | appDelegate.menuDefs = menuDefs; 199 | appDelegate.menuCount = menuCount; 200 | goUILog("menuCount: %d\n",menuCount); 201 | [NSApp setDelegate:appDelegate]; 202 | 203 | window = [[GoUIWindow alloc] init]; 204 | [window create:settings]; 205 | goUILog("run loop"); 206 | [NSApp run]; 207 | //run(); 208 | } 209 | } 210 | 211 | +(void)evaluateJS:(NSString*)js { 212 | [window evaluateJS:js]; 213 | } 214 | 215 | +(void)exit { 216 | [NSApp terminate:NSApp]; 217 | } 218 | @end 219 | 220 | void create(WindowSettings settings, MenuDef* menuDefs, int menuCount) { 221 | [GoUIApp start:settings menuDefs:menuDefs menuCount:menuCount]; 222 | } 223 | 224 | void invokeJS(const char *js,int fromMainThread) { 225 | NSString* script = [NSString stringWithUTF8String:js]; 226 | if(fromMainThread) { 227 | goUILog("invokeJS:%s",js); 228 | [GoUIApp evaluateJS:script]; 229 | } else { 230 | goUILog("invokeJS async:%s",js); 231 | dispatch_async_and_wait(dispatch_get_main_queue(),^{ 232 | [GoUIApp evaluateJS:script]; 233 | }); 234 | } 235 | } 236 | 237 | void exitApp() { 238 | [GoUIApp exit]; 239 | } 240 | 241 | */ 242 | import "C" 243 | 244 | func cCreate(cs C.WindowSettings, cMenuDefs *C.MenuDef, count C.int) { 245 | C.create(cs, cMenuDefs, count) 246 | } 247 | 248 | func cActivate() { 249 | 250 | } 251 | 252 | func cInvokeJS(js *C.char, fromMainThread C.int) { 253 | C.invokeJS(js, fromMainThread) 254 | } 255 | 256 | func cExit() { 257 | C.exitApp() 258 | } 259 | -------------------------------------------------------------------------------- /window_macos.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FIPress/GoUI/57faf36312986f15354c3fa9cd54fed7371bcb29/window_macos.h --------------------------------------------------------------------------------