├── 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 | [](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 | 
119 |
120 | **Note**
121 | You may use remote debugging of Safari or Chrome to debug iOS or Android web page.
122 |
123 | 
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 | 
181 |
182 | Ubuntu
183 | 
184 |
185 | iOS
186 | 
187 |
188 | Android
189 | 
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 | 
199 |
200 | Ubuntu
201 | 
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 |
Fake Chatbot
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('')
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
')
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
--------------------------------------------------------------------------------