├── .gitattributes
├── .github
└── ISSUE_TEMPLATE
│ ├── BUG.md
│ ├── FEATURE_REQUEST.md
│ └── config.yml
├── .gitignore
├── LICENSE
├── README.md
├── accessibility.go
├── application.go
├── build-constant.go
├── doc.go
├── embedder
├── README.md
├── build.go
├── doc.go
├── embedder.go
├── embedder.h
├── embedder_helper.c
└── embedder_proxy.go
├── event-loop.go
├── glfw.go
├── go.mod
├── go.sum
├── internal
├── currentthread
│ └── thread-id.go
├── debounce
│ └── debounce.go
├── execpath
│ └── path.go
├── keyboard
│ ├── keyboard.go
│ ├── keyboard_darwin.go
│ └── keyboard_pc.go
├── opengl
│ ├── doc.go
│ ├── opengl.go
│ └── opengl_none.go
├── priorityqueue
│ └── priorityqueue.go
└── tasker
│ └── tasker.go
├── isolate.go
├── key-events.go
├── lifecycle.go
├── mascot.png
├── messenger.go
├── mousecursor.go
├── navigation.go
├── option.go
├── platform.go
├── plugin.go
├── plugin
├── basic-message-channel.go
├── basic-message-channel_test.go
├── binary-codec.go
├── binary-codec_test.go
├── binary-messenger.go
├── codec.go
├── doc.go
├── endian.go
├── error.go
├── error_test.go
├── event-channel.go
├── event-sink.go
├── helper_test.go
├── json-method-codec.go
├── json-method-codec_test.go
├── method-channel.go
├── method-channel_test.go
├── method-handler.go
├── standard-message-codec.go
├── standard-message-codec_test.go
├── standard-method-codec.go
├── standard-method-codec_test.go
├── string-codec.go
└── string-codec_test.go
├── pop.go
├── renovate.json
├── restoration.go
├── stocks.jpg
├── text-input.go
├── texture-registry.go
├── texture.go
└── window.go
/.gitattributes:
--------------------------------------------------------------------------------
1 | example/* linguist-vendored=true
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F41B Bug Report"
3 | about: "If something isn't working as expected."
4 | ---
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ## Hover doctor
13 |
14 |
15 |
16 | ```
17 | $ hover doctor
18 | hover: Running on linux
19 | hover: Docker installed: true
20 | hover: Sharing flutter version
21 | [...]
22 | ```
23 |
24 | ## Error output
25 |
26 |
27 |
28 | Using `hover build [...] --XXX` to build my application, I get the following
29 | error:
30 |
31 | ```log
32 | hover: Using engine from cache
33 | hover: Cleaning the build directory
34 | hover: Bundling flutter app
35 | hover: Compiling 'go-flutter' and plugins
36 | [...]
37 | ```
38 |
39 |
48 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F680 Feature Request"
3 | about: "I have a suggestion (and may want to implement it)!"
4 | labels: enhancement
5 | ---
6 |
7 | ## Is your feature request related to a problem?
8 |
9 | A clear and concise description of what the problem is.
10 | `I have an issue when [...]`
11 |
12 | ## Describe the feature you'd like
13 |
14 | A clear and concise description of what you want to happen.
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | .DS_Store
15 | .dart_tool/
16 |
17 | .packages
18 | .pub/
19 |
20 | build/
21 |
22 | .flutter-plugins
23 |
24 | # Examples generated files
25 | example/simpleDemo/simpleDemo
26 | example/stocks/stocks
27 |
28 | # sometimes when flutter crashes it creates log files
29 | *.log
30 |
31 | # other crashes report:
32 | core.*
33 |
34 | .vscode
35 | .idea
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2019, Pierre Champion
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # go-flutter - A package that brings Flutter to the desktop
4 |
5 | [](https://github.com/Solido/awesome-flutter)
6 | [](http://godoc.org/github.com/go-flutter-desktop/go-flutter)
7 | [](https://goreportcard.com/report/github.com/go-flutter-desktop/go-flutter)
8 | [](https://gitter.im/go-flutter-desktop/go-flutter?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
9 |
10 | ## Purpose
11 |
12 | [Flutter](http://flutter.io/) allows you to build beautiful native apps on iOS and Android from a single codebase.
13 |
14 | This [unofficial](https://github.com/go-flutter-desktop/go-flutter/issues/191#issuecomment-511384007) project brings Flutter to the desktop through the power of [Go](http://golang.org/) and [GLFW](https://github.com/go-gl/glfw).
15 |
16 | The flutter engine itself doesn't know how to deal with desktop platforms _(eg handling mouse/keyboard input)_. Instead, it exposes an abstraction layer for whatever platform to implement. This project implements the [Flutter's Embedding API](https://github.com/flutter/flutter/wiki/Custom-Flutter-Engine-Embedders) using a single code base that runs on Windows, macOS, and Linux. For rendering, [**GLFW**](https://github.com/go-gl/glfw) fits the job because it provides the right abstractions over the OpenGL's Buffer/Mouse/Keyboard for each platform.
17 |
18 | The choice of [Golang](https://github.com/golang/go) comes from the fact that it has the same tooling on every platform. Plus Golang is a great language because it keeps everything simple and readable, which makes it easy to build cross-platform plugins.
19 |
20 |
21 |
22 |
23 |
24 | ## Getting started
25 |
26 | The best way to get started is to install [hover](https://github.com/go-flutter-desktop/hover), the official go-flutter tool to set up, build and run Flutter apps on the desktop, including hot-reload.
27 |
28 | Read the [hover tutorial](https://github.com/go-flutter-desktop/hover) to run your app on the desktop, or start with [one of our example apps](https://github.com/go-flutter-desktop/examples).
29 |
30 | If you want more in-depth information about go-flutter, read the [wiki](https://github.com/go-flutter-desktop/go-flutter/wiki).
31 |
32 | ## Supported features
33 |
34 | - Linux :penguin:
35 | - MacOS :apple:
36 | - Windows :checkered_flag:
37 | - [**Hot Reload**](https://github.com/go-flutter-desktop/go-flutter/issues/129#issuecomment-513590141)
38 | - Plugin system
39 | - BinaryMessageCodec, BinaryMessageChannel
40 | - StandardMessageCodec, JSONMessageCodec
41 | - StandardMethodCodec, **MethodChannel**
42 | - Plugin detection for [supported plugins](https://github.com/go-flutter-desktop/go-flutter/wiki/Create-a-hover-compatible-plugin)
43 | - Importable as Go library into custom projects
44 | - UTF-8 Text input
45 | - Clipboard copy & paste
46 | - Window title and icon
47 | - Standard keyboard shortcuts
48 | - ctrl-c ctrl-v ctrl-x ctrl-a
49 | - Home End shift-Home shift-End
50 | - Left ctrl-Left ctrl-shift-Left
51 | - Right ctrl-Right ctrl-shift-Right
52 | - Backspace ctrl-Backspace Delete
53 | - Mouse-over/hovering
54 | - Mouse-buttons
55 | - RawKeyboard events
56 | - Distribution format (windows-msi, mac-dmg, linux-appimage, and more)
57 | - Cross-compiling using docker :whale:
58 |
59 | Are you missing a feature? [Open an issue!](https://github.com/go-flutter-desktop/go-flutter/issues/new)
60 |
61 | ## Examples
62 |
63 | A separate repository contains example Flutter apps that also run on the desktop. Go to [github.com/go-flutter-desktop/examples](https://github.com/go-flutter-desktop/examples) to give them a try.
64 |
65 | ## Plugins
66 |
67 | Some popular plugins are already implemented over at [github.com/go-flutter-desktop/plugins](https://github.com/go-flutter-desktop/plugins).
68 | If you have implemented a plugin that you would like to share, feel free to open a PR on the plugins repository!
69 |
70 | For a detailed tutorial on how to create a plugin, read the [wiki](https://github.com/go-flutter-desktop/go-flutter/wiki/Implement-a-plugin).
71 |
72 | ## Version compatibility
73 |
74 | ### Flutter version
75 |
76 | Flutter itself is a relatively young project. Its framework and engine are updated often. The go-flutter project tries to stay compatible with the [beta channel](https://github.com/flutter/flutter/wiki/Flutter-build-release-channels) of Flutter.
77 |
78 | ### Go version
79 |
80 | Updating Go is simple and Go [seldomly has backwards-incompatible changes](https://golang.org/doc/go1compat). This project remains compatible with the [latest Go stable release](https://golang.org/dl/).
81 |
82 | ### GLFW version
83 |
84 | This project uses go-gl/glfw for GLFW v3.3.
85 |
86 | ## License
87 |
88 | [BSD 3-Clause License](LICENSE)
89 |
--------------------------------------------------------------------------------
/accessibility.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import "github.com/go-flutter-desktop/go-flutter/plugin"
4 |
5 | type accessibilityPlugin struct{}
6 |
7 | // hardcoded because there is no swappable renderer interface.
8 | var defaultAccessibilityPlugin = &accessibilityPlugin{}
9 |
10 | func (p *accessibilityPlugin) InitPlugin(messenger plugin.BinaryMessenger) error {
11 | channel := plugin.NewBasicMessageChannel(messenger, "flutter/accessibility", plugin.StandardMessageCodec{})
12 | // Ignored: go-flutter doesn't support accessibility events
13 | channel.HandleFunc(func(_ interface{}) (interface{}, error) { return nil, nil })
14 | return nil
15 | }
16 |
--------------------------------------------------------------------------------
/application.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "runtime"
7 | "runtime/debug"
8 | "time"
9 | "unsafe"
10 |
11 | "github.com/Xuanwo/go-locale"
12 | "github.com/go-gl/glfw/v3.3/glfw"
13 | "github.com/pkg/errors"
14 | "golang.org/x/text/language"
15 |
16 | "github.com/go-flutter-desktop/go-flutter/embedder"
17 | "github.com/go-flutter-desktop/go-flutter/internal/debounce"
18 | "github.com/go-flutter-desktop/go-flutter/internal/opengl"
19 | "github.com/go-flutter-desktop/go-flutter/internal/tasker"
20 | )
21 |
22 | // Run executes a flutter application with the provided options.
23 | // given limitations this method must be called by the main function directly.
24 | //
25 | // Run(opt) is short for NewApplication(opt).Run()
26 | func Run(opt ...Option) (err error) {
27 | return NewApplication(opt...).Run()
28 | }
29 |
30 | // Application provides the flutter engine in a user friendly matter.
31 | type Application struct {
32 | config config
33 | engine *embedder.FlutterEngine
34 | window *glfw.Window
35 | resourceWindow *glfw.Window
36 | }
37 |
38 | // NewApplication creates a new application with provided options.
39 | func NewApplication(opt ...Option) *Application {
40 | app := &Application{
41 | config: newApplicationConfig(),
42 | }
43 |
44 | // The platformPlugin, textinputPlugin, etc. are currently hardcoded as we
45 | // have a hard link with GLFW. The plugins must be singleton and are
46 | // accessed directly from the flutter package to wire up with glfw. If
47 | // there's going to be a renderer interface, it's init would replace this
48 | // configuration.
49 | opt = append(opt, AddPlugin(defaultNavigationPlugin))
50 | opt = append(opt, AddPlugin(defaultPlatformPlugin))
51 | opt = append(opt, AddPlugin(defaultTextinputPlugin))
52 | opt = append(opt, AddPlugin(defaultLifecyclePlugin))
53 | opt = append(opt, AddPlugin(defaultKeyeventsPlugin))
54 | opt = append(opt, AddPlugin(defaultAccessibilityPlugin))
55 | opt = append(opt, AddPlugin(defaultIsolatePlugin))
56 | opt = append(opt, AddPlugin(defaultMousecursorPlugin))
57 | opt = append(opt, AddPlugin(defaultRestorationPlugin))
58 |
59 | // apply all configs
60 | for _, o := range opt {
61 | o(&app.config)
62 | }
63 |
64 | return app
65 | }
66 |
67 | func postEmptyEvent() {
68 | defer func() {
69 | p := recover()
70 | if p != nil {
71 | fmt.Printf("go-flutter: recovered from panic 'glfw.PostEmptyEvent()': %v\n", p)
72 | debug.PrintStack()
73 | }
74 | }()
75 | glfw.PostEmptyEvent()
76 | }
77 |
78 | // createResourceWindow creates an invisible GLFW window that shares the 'view'
79 | // window's resource context. This window is used to upload resources in the
80 | // background. Must be call after the 'view' window is created.
81 | //
82 | // Though optional, it is recommended that all embedders set this callback as
83 | // it will lead to better performance in texture handling.
84 | func createResourceWindow(window *glfw.Window) (*glfw.Window, error) {
85 | opengl.GLFWWindowHint()
86 | glfw.WindowHint(glfw.Decorated, glfw.False)
87 | glfw.WindowHint(glfw.Visible, glfw.False)
88 | if runtime.GOOS == "linux" {
89 | // Skia expects an EGL context on linux (libglvnd)
90 | glfw.WindowHint(glfw.ContextCreationAPI, glfw.EGLContextAPI)
91 | }
92 | resourceWindow, err := glfw.CreateWindow(1, 1, "", nil, window)
93 | if err != nil {
94 | return nil, errors.Wrap(err, "error creating the resource window")
95 | }
96 | glfw.DefaultWindowHints()
97 | return resourceWindow, nil
98 | }
99 |
100 | // Run starts the application and waits for it to finish.
101 | func (a *Application) Run() error {
102 | runtime.LockOSThread()
103 |
104 | err := glfw.Init()
105 | if err != nil {
106 | return errors.Wrap(err, "glfw init")
107 | }
108 | defer glfw.Terminate()
109 |
110 | var monitor *glfw.Monitor
111 | switch a.config.windowMode {
112 | case WindowModeDefault:
113 | // nothing
114 | case WindowModeMaximize:
115 | glfw.WindowHint(glfw.Maximized, glfw.True)
116 | case WindowModeBorderlessMaximize:
117 | glfw.WindowHint(glfw.Maximized, glfw.True)
118 | glfw.WindowHint(glfw.Decorated, glfw.False)
119 | case WindowModeBorderless:
120 | glfw.WindowHint(glfw.Decorated, glfw.False)
121 | case WindowModeBorderlessFullscreen:
122 | monitor = glfw.GetPrimaryMonitor()
123 | mode := monitor.GetVideoMode()
124 | a.config.windowInitialDimensions.width = mode.Width
125 | a.config.windowInitialDimensions.height = mode.Height
126 | glfw.WindowHint(glfw.RedBits, mode.RedBits)
127 | glfw.WindowHint(glfw.GreenBits, mode.GreenBits)
128 | glfw.WindowHint(glfw.BlueBits, mode.BlueBits)
129 | glfw.WindowHint(glfw.RefreshRate, mode.RefreshRate)
130 | default:
131 | return errors.Errorf("invalid window mode %T", a.config.windowMode)
132 | }
133 |
134 | opengl.GLFWWindowHint()
135 |
136 | {
137 | // TODO(drakirus): Delete this when https://github.com/go-gl/glfw/issues/272 is resolved.
138 | // Post an empty event from the main thread before it can happen in a non-main thread,
139 | // to work around https://github.com/glfw/glfw/issues/1649.
140 | postEmptyEvent()
141 | }
142 |
143 | if a.config.windowInitialLocation.xpos != 0 {
144 | // To create the window at a specific position, make it initially invisible
145 | // using the Visible window hint, set its position and then show it.
146 | glfw.WindowHint(glfw.Visible, glfw.False)
147 | }
148 |
149 | glfw.WindowHint(glfw.ScaleToMonitor, glfw.True)
150 | if a.config.windowAlwaysOnTop {
151 | glfw.WindowHint(glfw.Floating, glfw.True)
152 | }
153 | if a.config.windowTransparent {
154 | glfw.WindowHint(glfw.TransparentFramebuffer, glfw.True)
155 | }
156 |
157 | if runtime.GOOS == "linux" {
158 | // Skia expects an EGL context on linux (libglvnd)
159 | glfw.WindowHint(glfw.ContextCreationAPI, glfw.EGLContextAPI)
160 | }
161 |
162 | a.window, err = glfw.CreateWindow(a.config.windowInitialDimensions.width, a.config.windowInitialDimensions.height, "Loading..", monitor, nil)
163 | if err != nil {
164 | return errors.Wrap(err, "creating glfw window")
165 | }
166 | defer a.window.Destroy()
167 | glfw.DefaultWindowHints()
168 |
169 | if a.config.windowInitialLocation.xpos != 0 {
170 | a.window.SetPos(a.config.windowInitialLocation.xpos, a.config.windowInitialLocation.ypos)
171 | a.window.Show()
172 | }
173 |
174 | a.resourceWindow, err = createResourceWindow(a.window)
175 | if err != nil {
176 | fmt.Printf("go-flutter: WARNING %v\n", err)
177 | } else {
178 | defer a.resourceWindow.Destroy()
179 | }
180 |
181 | if a.config.windowIconProvider != nil {
182 | images, err := a.config.windowIconProvider()
183 | if err != nil {
184 | return errors.Wrap(err, "getting images from icon provider")
185 | }
186 | a.window.SetIcon(images)
187 | }
188 |
189 | a.window.SetTitle(ProjectName)
190 |
191 | if a.config.windowDimensionLimits.minWidth != 0 {
192 | a.window.SetSizeLimits(
193 | a.config.windowDimensionLimits.minWidth,
194 | a.config.windowDimensionLimits.minHeight,
195 | a.config.windowDimensionLimits.maxWidth,
196 | a.config.windowDimensionLimits.maxHeight,
197 | )
198 | }
199 |
200 | // Create a empty FlutterEngine.
201 | a.engine = embedder.NewFlutterEngine()
202 |
203 | // Set configuration values to engine.
204 | a.engine.AssetsPath = a.config.flutterAssetsPath
205 | a.engine.IcuDataPath = a.config.icuDataPath
206 | a.engine.ElfSnapshotPath = a.config.elfSnapshotpath
207 |
208 | // Create a messenger and init plugins
209 | messenger := newMessenger(a.engine)
210 | // Attach PlatformMessage callback function onto the engine
211 | a.engine.PlatfromMessage = messenger.handlePlatformMessage
212 |
213 | // Create a TextureRegistry
214 | texturer := newTextureRegistry(a.engine, a.window)
215 | // Attach TextureRegistry callback function onto the engine
216 | a.engine.GLExternalTextureFrameCallback = texturer.handleExternalTexture
217 |
218 | // Create a new eventloop
219 | eventLoop := newEventLoop(
220 | postEmptyEvent, // Wakeup GLFW
221 | a.engine.RunTask, // Flush tasks
222 | )
223 | // Attach TaskRunner callback functions onto the engine
224 | a.engine.TaskRunnerRunOnCurrentThread = eventLoop.RunOnCurrentThread
225 | a.engine.TaskRunnerPostTask = eventLoop.PostTask
226 |
227 | // Attach GL callback functions onto the engine
228 | a.engine.GLMakeCurrent = func() bool {
229 | a.window.MakeContextCurrent()
230 | return true
231 | }
232 | a.engine.GLClearCurrent = func() bool {
233 | glfw.DetachCurrentContext()
234 | return true
235 | }
236 | a.engine.GLPresent = func() bool {
237 | a.window.SwapBuffers()
238 | return true
239 | }
240 | a.engine.GLFboCallback = func() int32 {
241 | return 0
242 | }
243 | a.engine.GLMakeResourceCurrent = func() bool {
244 | if a.resourceWindow == nil {
245 | return false
246 | }
247 | a.resourceWindow.MakeContextCurrent()
248 | return true
249 | }
250 | a.engine.GLProcResolver = func(procName string) unsafe.Pointer {
251 | return glfw.GetProcAddress(procName)
252 | }
253 |
254 | // Set the glfw window user pointer to point to the FlutterEngine so that
255 | // callback functions may obtain the FlutterEngine from the glfw window
256 | // user pointer.
257 | flutterEnginePointer := uintptr(unsafe.Pointer(a.engine))
258 | defer func() {
259 | runtime.KeepAlive(flutterEnginePointer)
260 | }()
261 | a.window.SetUserPointer(unsafe.Pointer(&flutterEnginePointer))
262 |
263 | // Start the engine
264 | err = a.engine.Run(unsafe.Pointer(&flutterEnginePointer), a.config.vmArguments)
265 | if err != nil {
266 | fmt.Printf("go-flutter: %v\n", err)
267 | os.Exit(1)
268 | }
269 |
270 | languageTag, err := locale.Detect()
271 | if err != nil {
272 | fmt.Printf("go-flutter: failed to detect locale code: %v\n", err)
273 | languageTag = language.English
274 | }
275 | base, _ := languageTag.Base()
276 | region, _ := languageTag.Region()
277 | scriptCode, _ := languageTag.Script()
278 | err = a.engine.UpdateSystemLocale(base.String(), region.String(), scriptCode.String())
279 | if err != nil {
280 | fmt.Printf("go-flutter: %v\n", err)
281 | }
282 |
283 | // Register plugins
284 | for _, p := range a.config.plugins {
285 | err = p.InitPlugin(messenger)
286 | if err != nil {
287 | return errors.Wrap(err, "failed to initialize plugin "+fmt.Sprintf("%T", p))
288 | }
289 |
290 | // Extra init call for plugins that satisfy the PluginGLFW interface.
291 | if glfwPlugin, ok := p.(PluginGLFW); ok {
292 | err = glfwPlugin.InitPluginGLFW(a.window)
293 | if err != nil {
294 | return errors.Wrap(err, "failed to initialize glfw plugin"+fmt.Sprintf("%T", p))
295 | }
296 | }
297 |
298 | // Extra init call for plugins that satisfy the PluginTexture interface.
299 | if texturePlugin, ok := p.(PluginTexture); ok {
300 | err = texturePlugin.InitPluginTexture(texturer)
301 | if err != nil {
302 | return errors.Wrap(err, "failed to initialize texture plugin"+fmt.Sprintf("%T", p))
303 | }
304 | }
305 | }
306 |
307 | // Change the flutter initial route
308 | initialRoute := os.Getenv("GOFLUTTER_ROUTE")
309 | if initialRoute != "" {
310 | defaultPlatformPlugin.addFrameworkReadyCallback(func() {
311 | defaultNavigationPlugin.
312 | channel.InvokeMethod("pushRoute", initialRoute)
313 | })
314 | }
315 |
316 | // Setup a new windowManager to handle windows pixel ratio's and pointer
317 | // devices.
318 | windowManager := newWindowManager(a.config.forcePixelRatio)
319 | // force first refresh
320 | windowManager.glfwRefreshCallback(a.window)
321 | // Attach glfw window callbacks for refresh and position changes
322 | a.window.SetRefreshCallback(windowManager.glfwRefreshCallback)
323 | // Debounce the position callback.
324 | // This avoid making too much flutter redraw and potentially redundant
325 | // network calls.
326 | glfwDebouceTasker := tasker.New()
327 | debounced := debounce.New(50 * time.Millisecond)
328 | // SetPosCallback is called when the window is moved, this directly calls
329 | // glfwRefreshCallback in order to redraw and avoid transparent scene.
330 | a.window.SetPosCallback(func(window *glfw.Window, xpos int, ypos int) {
331 | debounced(func() {
332 | glfwDebouceTasker.Do(func() {
333 | windowManager.glfwRefreshCallback(window)
334 | })
335 | })
336 | })
337 | a.window.SetContentScaleCallback(func(window *glfw.Window, x float32, y float32) {
338 | windowManager.glfwRefreshCallback(window)
339 | })
340 |
341 | // Attach glfw window callbacks for text input
342 | defaultTextinputPlugin.backOnEscape = a.config.backOnEscape
343 | a.window.SetKeyCallback(
344 | func(window *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
345 | defaultTextinputPlugin.glfwKeyCallback(window, key, scancode, action, mods)
346 | defaultKeyeventsPlugin.sendKeyEvent(window, key, scancode, action, mods)
347 | })
348 | a.window.SetCharCallback(defaultTextinputPlugin.glfwCharCallback)
349 |
350 | // Attach glfw window callback for iconification
351 | a.window.SetIconifyCallback(defaultLifecyclePlugin.glfwIconifyCallback)
352 |
353 | // Attach glfw window callbacks for mouse input
354 | a.window.SetCursorEnterCallback(windowManager.glfwCursorEnterCallback)
355 | a.window.SetCursorPosCallback(windowManager.glfwCursorPosCallback)
356 | a.window.SetMouseButtonCallback(windowManager.glfwMouseButtonCallback)
357 | a.window.SetScrollCallback(
358 | func(window *glfw.Window, xoff float64, yoff float64) {
359 | windowManager.glfwScrollCallback(window, xoff, yoff, a.config.scrollAmount)
360 | })
361 |
362 | // Shutdown the engine if we return from this function (on purpose or panic)
363 | defer a.engine.Shutdown()
364 |
365 | // Handle events until the window indicates we should stop. An event may tell the window to stop, in which case
366 | // we'll exit on next iteration.
367 | for !a.window.ShouldClose() {
368 | eventLoop.WaitForEvents(func(duration float64) {
369 | glfw.WaitEventsTimeout(duration)
370 | })
371 |
372 | // Execute tasks that MUST be run in the engine thread (!blocks rendering!)
373 | glfwDebouceTasker.ExecuteTasks()
374 | messenger.engineTasker.ExecuteTasks()
375 | texturer.engineTasker.ExecuteTasks()
376 | }
377 |
378 | fmt.Println("go-flutter: closing application")
379 |
380 | return nil
381 | }
382 |
383 | // TODO: app.Start(), app.Wait()?
384 |
--------------------------------------------------------------------------------
/build-constant.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | // Compile configuration constants persistent across all flutter.Application.
4 | // The values of config(option.go) can change between flutter.Run calls, those
5 | // values contains informations that needs to be access globally, without
6 | // requiring an flutter.Application.
7 | //
8 | // Values overwritten by hover during the 'Compiling 'go-flutter' and
9 | // plugins' phase.
10 | var (
11 | // ProjectVersion contains the version of the build
12 | ProjectVersion = "unknown"
13 | // ProjectVersion contains the version of the go-flutter been used
14 | PlatformVersion = "unknown"
15 | // ProjectName contains the application name
16 | ProjectName = "unknown"
17 | // ProjectOrganizationName contains the package org name, (Can by set upon flutter create (--org flag))
18 | ProjectOrganizationName = "unknown"
19 | )
20 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Package flutter combines the embedder API with GLFW and plugins. Flutter and
2 | // Go on the desktop.
3 | //
4 | // go-flutter is in active development. API's must be considered beta and may
5 | // be changed.
6 | package flutter
7 |
--------------------------------------------------------------------------------
/embedder/README.md:
--------------------------------------------------------------------------------
1 | # embedder
2 |
3 | This package wraps the [Flutter embedder API](https://raw.githubusercontent.com/flutter/engine/master/shell/platform/embedder/embedder.h) in Go and adds some helper functions to work with it. This package does NOT contain any platform specific code (glfw, etc.) and may eventualy be used for platforms other than the ones targeted by go-flutter right now. Note that this package does not promise a stable API: types, functions, methods may all change in a breaking way.
4 |
5 | ## Build
6 |
7 | To build this package set the `CGO_LDFLAGS` and run `go build`. For example:
8 |
9 | On Linux:
10 | ```bash
11 | export CGO_LDFLAGS="-L${HOME}/.cache/hover/engine/linux/"
12 | go build
13 | ```
14 |
15 | To build this package on Mac OS
16 | ```bash
17 | export CGO_LDFLAGS="-F ${HOME}/Library/Caches/hover/engine/darwin"
18 | go build
19 | ```
20 |
21 | To build this package on Windows
22 | ```cmd
23 | set CGO_LDFLAGS="-L%HOMEPATH%/.cache/hover/engine/windows/"
24 | go build
25 | ```
26 |
27 | This works if [hover](https://github.com/go-flutter-desktop/hover) has cached the flutter engine for the local user.
28 |
--------------------------------------------------------------------------------
/embedder/build.go:
--------------------------------------------------------------------------------
1 | // +build !no_engine_tags
2 |
3 | package embedder
4 |
5 | // #cgo linux LDFLAGS: -lflutter_engine -Wl,-rpath,$ORIGIN
6 | // #cgo windows LDFLAGS: -lflutter_engine
7 | // #cgo darwin LDFLAGS: -framework FlutterEmbedder
8 | import "C"
9 |
--------------------------------------------------------------------------------
/embedder/doc.go:
--------------------------------------------------------------------------------
1 | // Package embedder wraps the Flutter Embedder C API to Go.
2 | //
3 | // The package contains some GLFW specific implementation helpers. These may be
4 | // removed in the future.
5 | //
6 | // If you wish to publish a Flutter application to desktop, you shouldn't use
7 | // this package directly. Instead, use the go-flutter package. There is NO
8 | // compatibility promise on this package (embedder). Breaking changes can and will
9 | // occur.
10 | package embedder
11 |
--------------------------------------------------------------------------------
/embedder/embedder.go:
--------------------------------------------------------------------------------
1 | package embedder
2 |
3 | import "C"
4 |
5 | // #include "embedder.h"
6 | // #include
7 | // FlutterEngineResult runFlutter(void *user_data, FlutterEngine *engine, FlutterProjectArgs * Args);
8 | // FlutterEngineResult
9 | // createMessageResponseHandle(FlutterEngine engine, void *user_data,
10 | // FlutterPlatformMessageResponseHandle **reply);
11 | // const int32_t kFlutterSemanticsNodeIdBatchEnd = -1;
12 | // const int32_t kFlutterSemanticsCustomActionIdBatchEnd = -1;
13 | // FlutterEngineAOTDataSource* createAOTDataSource(FlutterEngineAOTDataSource *data_in, const char * elfSnapshotPath);
14 | import "C"
15 | import (
16 | "fmt"
17 | "runtime"
18 | "sync"
19 | "unsafe"
20 |
21 | "github.com/pkg/errors"
22 | )
23 |
24 | // Result corresponds to the C.enum retuned by the shared flutter library
25 | // whenever we call it.
26 | type Result int32
27 |
28 | // Values representing the status of an Result.
29 | const (
30 | ResultSuccess Result = C.kSuccess
31 | ResultInvalidLibraryVersion Result = C.kInvalidLibraryVersion
32 | ResultInvalidArguments Result = C.kInvalidArguments
33 | ResultInternalInconsistency Result = C.kInternalInconsistency
34 | ResultEngineNotRunning Result = -1
35 | )
36 |
37 | // FlutterOpenGLTexture corresponds to the C.FlutterOpenGLTexture struct.
38 | type FlutterOpenGLTexture struct {
39 | // Target texture of the active texture unit (example GL_TEXTURE_2D)
40 | Target uint32
41 | // The name of the texture
42 | Name uint32
43 | // The texture format (example GL_RGBA8)
44 | Format uint32
45 | }
46 |
47 | // FlutterTask is a type alias to C.FlutterTask
48 | type FlutterTask = C.FlutterTask
49 |
50 | // FlutterEngine corresponds to the C.FlutterEngine with his associated callback's method.
51 | type FlutterEngine struct {
52 | // Flutter Engine.
53 | Engine C.FlutterEngine
54 |
55 | // closed indicates if the engine has Shutdown
56 | closed bool
57 | sync sync.Mutex
58 |
59 | // GL callback functions
60 | GLMakeCurrent func() bool
61 | GLClearCurrent func() bool
62 | GLPresent func() bool
63 | GLFboCallback func() int32
64 | GLMakeResourceCurrent func() bool
65 | GLProcResolver func(procName string) unsafe.Pointer
66 | GLExternalTextureFrameCallback func(textureID int64, width int, height int) *FlutterOpenGLTexture
67 |
68 | // task runner interop
69 | TaskRunnerRunOnCurrentThread func() bool
70 | TaskRunnerPostTask func(trask FlutterTask, targetTimeNanos uint64)
71 |
72 | // platform message callback function
73 | PlatfromMessage func(message *PlatformMessage)
74 |
75 | // Engine arguments
76 | AssetsPath string
77 | IcuDataPath string
78 |
79 | // AOT ELF snopshot path
80 | // only required for AOT app.
81 | ElfSnapshotPath string
82 | aotDataSource C.FlutterEngineAOTData
83 | }
84 |
85 | // GoError convert a FlutterEngineResult to a golang readable error
86 | func (res Result) GoError(caller string) error {
87 | switch res {
88 | case ResultSuccess:
89 | return nil
90 | case ResultInvalidLibraryVersion:
91 | return errors.Errorf("%s returned result code %d (invalid library version)", caller, res)
92 | case ResultInvalidArguments:
93 | return errors.Errorf("%s returned result code %d (invalid arguments)", caller, res)
94 | case ResultInternalInconsistency:
95 | return errors.Errorf("%s returned result code %d (internal inconsistency)", caller, res)
96 | case ResultEngineNotRunning:
97 | return errors.Errorf("%s returned result code %d (engine not running)", caller, res)
98 | default:
99 | return errors.Errorf("%s returned result code %d (unknown result code)", caller, res)
100 | }
101 | }
102 |
103 | // NewFlutterEngine creates an empty FlutterEngine.
104 | func NewFlutterEngine() *FlutterEngine {
105 | return &FlutterEngine{}
106 | }
107 |
108 | // Run launches the Flutter Engine in a background thread.
109 | func (flu *FlutterEngine) Run(userData unsafe.Pointer, vmArgs []string) error {
110 |
111 | cVMArgs := C.malloc(C.size_t(len(vmArgs)) * C.size_t(unsafe.Sizeof(uintptr(0))))
112 | defer C.free(cVMArgs)
113 |
114 | a := (*[1<<30 - 1]*C.char)(cVMArgs)
115 |
116 | for idx, substring := range vmArgs {
117 | a[idx] = C.CString(substring)
118 | defer C.free(unsafe.Pointer(a[idx]))
119 | }
120 |
121 | assetsPath := C.CString(flu.AssetsPath)
122 | icuDataPath := C.CString(flu.IcuDataPath)
123 | defer C.free(unsafe.Pointer(assetsPath))
124 | defer C.free(unsafe.Pointer(icuDataPath))
125 |
126 | args := C.FlutterProjectArgs{
127 | assets_path: assetsPath,
128 | icu_data_path: icuDataPath,
129 | command_line_argv: (**C.char)(cVMArgs),
130 | command_line_argc: C.int(len(vmArgs)),
131 | shutdown_dart_vm_when_done: true,
132 | }
133 |
134 | if C.FlutterEngineRunsAOTCompiledDartCode() {
135 | elfSnapshotPath := C.CString(flu.ElfSnapshotPath)
136 | defer C.free(unsafe.Pointer(elfSnapshotPath))
137 |
138 | dataIn := C.FlutterEngineAOTDataSource{}
139 |
140 | C.createAOTDataSource(&dataIn, elfSnapshotPath)
141 | res := (Result)(C.FlutterEngineCreateAOTData(&dataIn, &flu.aotDataSource))
142 | if res != ResultSuccess {
143 | return res.GoError("C.FlutterEngineCreateAOTData()")
144 | }
145 | args.aot_data = flu.aotDataSource
146 | }
147 |
148 | args.struct_size = C.size_t(unsafe.Sizeof(args))
149 |
150 | res := (Result)(C.runFlutter(userData, &flu.Engine, &args))
151 | if flu.Engine == nil {
152 | return ResultInvalidArguments.GoError("engine.Run()")
153 | }
154 |
155 | return res.GoError("engine.Run()")
156 | }
157 |
158 | // Shutdown stops the Flutter engine.
159 | func (flu *FlutterEngine) Shutdown() error {
160 | flu.sync.Lock()
161 | defer flu.sync.Unlock()
162 | flu.closed = true
163 |
164 | res := (Result)(C.FlutterEngineShutdown(flu.Engine))
165 | if res != ResultSuccess {
166 | return res.GoError("engine.Shutdown()")
167 | }
168 |
169 | if C.FlutterEngineRunsAOTCompiledDartCode() {
170 | res := (Result)(C.FlutterEngineCollectAOTData(flu.aotDataSource))
171 | if res != ResultSuccess {
172 | return res.GoError("engine.Shutdown()")
173 | }
174 | }
175 | return nil
176 | }
177 |
178 | // PointerPhase corresponds to the C.enum describing phase of the mouse pointer.
179 | type PointerPhase int32
180 |
181 | // Values representing the mouse phase.
182 | const (
183 | PointerPhaseCancel PointerPhase = C.kCancel
184 | PointerPhaseUp PointerPhase = C.kUp
185 | PointerPhaseDown PointerPhase = C.kDown
186 | PointerPhaseMove PointerPhase = C.kMove
187 | PointerPhaseAdd PointerPhase = C.kAdd
188 | PointerPhaseRemove PointerPhase = C.kRemove
189 | PointerPhaseHover PointerPhase = C.kHover
190 | )
191 |
192 | // PointerButtonMouse corresponds to the C.enum describing the mouse buttons.
193 | type PointerButtonMouse int64
194 |
195 | // Values representing the mouse buttons.
196 | const (
197 | PointerButtonMousePrimary PointerButtonMouse = C.kFlutterPointerButtonMousePrimary
198 | PointerButtonMouseSecondary PointerButtonMouse = C.kFlutterPointerButtonMouseSecondary
199 | PointerButtonMouseMiddle PointerButtonMouse = C.kFlutterPointerButtonMouseMiddle
200 | )
201 |
202 | // PointerSignalKind corresponds to the C.enum describing signal kind of the mouse pointer.
203 | type PointerSignalKind int32
204 |
205 | // Values representing the pointer signal kind.
206 | const (
207 | PointerSignalKindNone PointerSignalKind = C.kFlutterPointerSignalKindNone
208 | PointerSignalKindScroll PointerSignalKind = C.kFlutterPointerSignalKindScroll
209 | )
210 |
211 | // PointerDeviceKind corresponds to the C.enum describing device kind of the mouse pointer.
212 | type PointerDeviceKind int32
213 |
214 | // Values representing the pointer signal kind.
215 | const (
216 | PointerDeviceKindMouse PointerDeviceKind = C.kFlutterPointerDeviceKindMouse
217 | PointerDeviceKindTouch PointerDeviceKind = C.kFlutterPointerDeviceKindTouch
218 | )
219 |
220 | // PointerEvent represents the position and phase of the mouse at a given time.
221 | type PointerEvent struct {
222 | Phase PointerPhase
223 | X float64
224 | Y float64
225 | SignalKind PointerSignalKind
226 | ScrollDeltaX float64
227 | ScrollDeltaY float64
228 | Buttons PointerButtonMouse
229 | }
230 |
231 | // SendPointerEvent is used to send an PointerEvent to the Flutter engine.
232 | func (flu *FlutterEngine) SendPointerEvent(event PointerEvent) error {
233 | cPointerEvent := C.FlutterPointerEvent{
234 | phase: (C.FlutterPointerPhase)(event.Phase),
235 | x: C.double(event.X),
236 | y: C.double(event.Y),
237 | timestamp: C.size_t(FlutterEngineGetCurrentTime()),
238 | signal_kind: (C.FlutterPointerSignalKind)(event.SignalKind),
239 | device_kind: (C.FlutterPointerDeviceKind)(PointerDeviceKindMouse),
240 | scroll_delta_x: C.double(event.ScrollDeltaX),
241 | scroll_delta_y: C.double(event.ScrollDeltaY),
242 | buttons: C.int64_t(event.Buttons),
243 | }
244 | cPointerEvent.struct_size = C.size_t(unsafe.Sizeof(cPointerEvent))
245 |
246 | res := C.FlutterEngineSendPointerEvent(flu.Engine, &cPointerEvent, 1)
247 |
248 | return (Result)(res).GoError("engine.SendPointerEvent")
249 | }
250 |
251 | // WindowMetricsEvent represents a window's resolution.
252 | type WindowMetricsEvent struct {
253 | Width int
254 | Height int
255 | PixelRatio float64
256 | }
257 |
258 | // SendWindowMetricsEvent is used to send a WindowMetricsEvent to the Flutter
259 | // Engine.
260 | func (flu *FlutterEngine) SendWindowMetricsEvent(event WindowMetricsEvent) error {
261 | cMetricEvent := C.FlutterWindowMetricsEvent{
262 | width: C.size_t(event.Width),
263 | height: C.size_t(event.Height),
264 | pixel_ratio: C.double(event.PixelRatio),
265 | }
266 | cMetricEvent.struct_size = C.size_t(unsafe.Sizeof(cMetricEvent))
267 |
268 | res := C.FlutterEngineSendWindowMetricsEvent(flu.Engine, &cMetricEvent)
269 |
270 | return (Result)(res).GoError("engine.SendWindowMetricsEvent()")
271 | }
272 |
273 | // PlatformMessage represents a binary message sent from or to the flutter
274 | // application.
275 | type PlatformMessage struct {
276 | Channel string
277 | Message []byte
278 |
279 | // ResponseHandle is set on some received platform message. All
280 | // PlatformMessage received with this attribute must send a response with
281 | // `SendPlatformMessageResponse`.
282 | // ResponseHandle can also be created from the embedder side when a
283 | // platform(golang) message needs native callback.
284 | ResponseHandle PlatformMessageResponseHandle
285 | }
286 |
287 | // PlatformMessageResponseHandle is a pointer that is used to wire a platform
288 | // message response to the original platform message.
289 | type PlatformMessageResponseHandle uintptr
290 |
291 | // ExpectsResponse indicates whether the platform message should receive a
292 | // response.
293 | func (p PlatformMessage) ExpectsResponse() bool {
294 | return p.ResponseHandle != 0
295 | }
296 |
297 | // UpdateSystemLocale is used to update the flutter locale to the system locale
298 | func (flu *FlutterEngine) UpdateSystemLocale(lang, country, script string) error {
299 | languageCode := C.CString(lang)
300 | countryCode := C.CString(country)
301 | scriptCode := C.CString(script)
302 | defer C.free(unsafe.Pointer(languageCode))
303 | defer C.free(unsafe.Pointer(countryCode))
304 | defer C.free(unsafe.Pointer(scriptCode))
305 |
306 | locale := C.FlutterLocale{
307 | language_code: languageCode,
308 | country_code: countryCode,
309 | script_code: scriptCode,
310 | }
311 |
312 | locale.struct_size = C.size_t(unsafe.Sizeof(locale))
313 | arr := (**C.FlutterLocale)(C.malloc(locale.struct_size))
314 | defer C.free(unsafe.Pointer(arr))
315 | (*[1]*C.FlutterLocale)(unsafe.Pointer(arr))[0] = &locale
316 |
317 | res := C.FlutterEngineUpdateLocales(flu.Engine, arr, (C.size_t)(1))
318 |
319 | return (Result)(res).GoError("engine.UpdateSystemLocale()")
320 | }
321 |
322 | // SendPlatformMessage is used to send a PlatformMessage to the Flutter engine.
323 | func (flu *FlutterEngine) SendPlatformMessage(msg *PlatformMessage) error {
324 | flu.sync.Lock()
325 | defer flu.sync.Unlock()
326 | if flu.closed {
327 | return ResultEngineNotRunning.GoError("engine.SendPlatformMessage()")
328 | }
329 |
330 | message := C.CBytes(msg.Message)
331 | channel := C.CString(msg.Channel)
332 | defer C.free(message)
333 | defer C.free(unsafe.Pointer(channel))
334 |
335 | cPlatformMessage := C.FlutterPlatformMessage{
336 | channel: channel,
337 | message: (*C.uint8_t)(message),
338 | message_size: C.size_t(len(msg.Message)),
339 |
340 | response_handle: (*C.FlutterPlatformMessageResponseHandle)(unsafe.Pointer(msg.ResponseHandle)),
341 | }
342 |
343 | cPlatformMessage.struct_size = C.size_t(unsafe.Sizeof(cPlatformMessage))
344 |
345 | res := C.FlutterEngineSendPlatformMessage(
346 | flu.Engine,
347 | &cPlatformMessage,
348 | )
349 |
350 | return (Result)(res).GoError("engine.SendPlatformMessage()")
351 | }
352 |
353 | // SendPlatformMessageResponse is used to send a message to the Flutter side
354 | // using the correct ResponseHandle.
355 | func (flu *FlutterEngine) SendPlatformMessageResponse(
356 | responseTo PlatformMessageResponseHandle,
357 | encodedMessage []byte,
358 | ) error {
359 |
360 | message := C.CBytes(encodedMessage)
361 | defer C.free(message)
362 |
363 | res := C.FlutterEngineSendPlatformMessageResponse(
364 | flu.Engine,
365 | (*C.FlutterPlatformMessageResponseHandle)(unsafe.Pointer(responseTo)),
366 | (*C.uint8_t)(message),
367 | (C.size_t)(len(encodedMessage)),
368 | )
369 |
370 | return (Result)(res).GoError("engine.SendPlatformMessageResponse()")
371 | }
372 |
373 | // RunTask inform the engine to run the specified task.
374 | func (flu *FlutterEngine) RunTask(task *FlutterTask) error {
375 | res := C.FlutterEngineRunTask(flu.Engine, task)
376 | return (Result)(res).GoError("engine.RunTask()")
377 | }
378 |
379 | // RegisterExternalTexture registers an external texture with a unique identifier.
380 | func (flu *FlutterEngine) RegisterExternalTexture(textureID int64) error {
381 | flu.sync.Lock()
382 | defer flu.sync.Unlock()
383 | if flu.closed {
384 | return ResultEngineNotRunning.GoError("engine.RegisterExternalTexture()")
385 | }
386 | res := C.FlutterEngineRegisterExternalTexture(flu.Engine, C.int64_t(textureID))
387 | return (Result)(res).GoError("engine.RegisterExternalTexture()")
388 | }
389 |
390 | // UnregisterExternalTexture unregisters a previous texture registration.
391 | func (flu *FlutterEngine) UnregisterExternalTexture(textureID int64) error {
392 | flu.sync.Lock()
393 | defer flu.sync.Unlock()
394 | if flu.closed {
395 | return ResultEngineNotRunning.GoError("engine.UnregisterExternalTexture()")
396 | }
397 | res := C.FlutterEngineUnregisterExternalTexture(flu.Engine, C.int64_t(textureID))
398 | return (Result)(res).GoError("engine.UnregisterExternalTexture()")
399 | }
400 |
401 | // MarkExternalTextureFrameAvailable marks that a new texture frame is
402 | // available for a given texture identifier.
403 | func (flu *FlutterEngine) MarkExternalTextureFrameAvailable(textureID int64) error {
404 | flu.sync.Lock()
405 | defer flu.sync.Unlock()
406 | if flu.closed {
407 | return ResultEngineNotRunning.GoError("engine.MarkExternalTextureFrameAvailable()")
408 | }
409 | res := C.FlutterEngineMarkExternalTextureFrameAvailable(flu.Engine, C.int64_t(textureID))
410 | return (Result)(res).GoError("engine.MarkExternalTextureFrameAvailable()")
411 | }
412 |
413 | // DataCallback is a function called when a PlatformMessage response send back
414 | // to the embedder.
415 | type DataCallback struct {
416 | // Handle func
417 | Handle func(binaryReply []byte)
418 | }
419 |
420 | // CreatePlatformMessageResponseHandle creates a platform message response
421 | // handle that allows the embedder to set a native callback for a response to a
422 | // message.
423 | // Must be collected via `ReleasePlatformMessageResponseHandle` after the call
424 | // to `SendPlatformMessage`.
425 | func (flu *FlutterEngine) CreatePlatformMessageResponseHandle(callback *DataCallback) (PlatformMessageResponseHandle, error) {
426 | var responseHandle *C.FlutterPlatformMessageResponseHandle
427 |
428 | callbackPointer := uintptr(unsafe.Pointer(callback))
429 | defer func() {
430 | runtime.KeepAlive(callbackPointer)
431 | }()
432 |
433 | res := C.createMessageResponseHandle(flu.Engine, unsafe.Pointer(&callbackPointer), &responseHandle)
434 | return PlatformMessageResponseHandle(unsafe.Pointer(responseHandle)), (Result)(res).GoError("engine.CreatePlatformMessageResponseHandle()")
435 | }
436 |
437 | // ReleasePlatformMessageResponseHandle collects a platform message response
438 | // handle.
439 | func (flu *FlutterEngine) ReleasePlatformMessageResponseHandle(responseHandle PlatformMessageResponseHandle) {
440 | cResponseHandle := (*C.FlutterPlatformMessageResponseHandle)(unsafe.Pointer(responseHandle))
441 | res := C.FlutterPlatformMessageReleaseResponseHandle(flu.Engine, cResponseHandle)
442 | if (Result)(res) != ResultSuccess {
443 | fmt.Printf("go-flutter: failed to collect platform response message handle\n")
444 | }
445 | }
446 |
447 | // FlutterEngineGetCurrentTime gets the current time in nanoseconds from the clock used by the flutter
448 | // engine.
449 | func FlutterEngineGetCurrentTime() uint64 {
450 | return uint64(C.FlutterEngineGetCurrentTime())
451 | }
452 |
--------------------------------------------------------------------------------
/embedder/embedder_helper.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include "embedder.h"
5 |
6 | // C proxy definitions. These are implemented in Go.
7 | bool proxy_make_current(void *user_data);
8 | bool proxy_clear_current(void *user_data);
9 | bool proxy_present(void *user_data);
10 | uint32_t proxy_fbo_callback(void *user_data);
11 | bool proxy_make_resource_current(void *user_data);
12 | void *proxy_gl_proc_resolver(void *user_data, const char *procname);
13 | void proxy_platform_message_callback(const FlutterPlatformMessage *message,
14 | void *user_data);
15 | bool proxy_gl_external_texture_frame_callback(void *user_data,
16 | int64_t texture_id, size_t width,
17 | size_t height,
18 | FlutterOpenGLTexture *texture);
19 |
20 | bool proxy_runs_task_on_current_thread_callback(void *user_data);
21 | void proxy_post_task_callback(FlutterTask task, uint64_t target_time_nanos,
22 | void *user_data);
23 |
24 | void proxy_desktop_binary_reply(const uint8_t *data, size_t data_size,
25 | void *user_data);
26 |
27 | // C helper
28 | FlutterEngineResult runFlutter(void *user_data, FlutterEngine *engine, FlutterProjectArgs *Args) {
29 | FlutterRendererConfig config = {};
30 | config.type = kOpenGL;
31 |
32 | config.open_gl.struct_size = sizeof(FlutterOpenGLRendererConfig);
33 | config.open_gl.make_current = proxy_make_current;
34 | config.open_gl.clear_current = proxy_clear_current;
35 | config.open_gl.present = proxy_present;
36 | config.open_gl.fbo_callback = proxy_fbo_callback;
37 | config.open_gl.make_resource_current = proxy_make_resource_current;
38 | config.open_gl.gl_proc_resolver = proxy_gl_proc_resolver;
39 | config.open_gl.gl_external_texture_frame_callback =
40 | proxy_gl_external_texture_frame_callback;
41 |
42 | Args->platform_message_callback = proxy_platform_message_callback;
43 |
44 | // Configure task runner interop
45 | FlutterTaskRunnerDescription platform_task_runner = {};
46 | platform_task_runner.struct_size = sizeof(FlutterTaskRunnerDescription);
47 | platform_task_runner.user_data = user_data;
48 | platform_task_runner.runs_task_on_current_thread_callback =
49 | proxy_runs_task_on_current_thread_callback;
50 | platform_task_runner.post_task_callback = proxy_post_task_callback;
51 |
52 | FlutterCustomTaskRunners custom_task_runners = {};
53 | custom_task_runners.struct_size = sizeof(FlutterCustomTaskRunners);
54 | // Render task and platform task are handled by the same TaskRunner
55 | custom_task_runners.platform_task_runner = &platform_task_runner;
56 | custom_task_runners.render_task_runner = &platform_task_runner;
57 | Args->custom_task_runners = &custom_task_runners;
58 |
59 | return FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, Args, user_data,
60 | engine);
61 | }
62 |
63 | FlutterEngineAOTDataSource* createAOTDataSource(FlutterEngineAOTDataSource *data_in, const char * elfSnapshotPath) {
64 | data_in->type = kFlutterEngineAOTDataSourceTypeElfPath;
65 | data_in->elf_path = elfSnapshotPath;
66 | return data_in;
67 | }
68 |
69 | FlutterEngineResult
70 | createMessageResponseHandle(FlutterEngine engine, void *user_data,
71 | FlutterPlatformMessageResponseHandle **reply) {
72 | return FlutterPlatformMessageCreateResponseHandle(
73 | engine, proxy_desktop_binary_reply, user_data, reply);
74 | }
75 |
--------------------------------------------------------------------------------
/embedder/embedder_proxy.go:
--------------------------------------------------------------------------------
1 | package embedder
2 |
3 | // #include "embedder.h"
4 | import "C"
5 | import (
6 | "unsafe"
7 | )
8 |
9 | //export proxy_platform_message_callback
10 | func proxy_platform_message_callback(message *C.FlutterPlatformMessage, userData unsafe.Pointer) {
11 | msg := &PlatformMessage{
12 | Channel: C.GoString(message.channel),
13 | Message: C.GoBytes(unsafe.Pointer(message.message), C.int(message.message_size)),
14 | ResponseHandle: PlatformMessageResponseHandle(unsafe.Pointer(message.response_handle)),
15 | }
16 | flutterEnginePointer := *(*uintptr)(userData)
17 | flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
18 | flutterEngine.PlatfromMessage(msg)
19 | }
20 |
21 | //export proxy_make_current
22 | func proxy_make_current(userData unsafe.Pointer) C.bool {
23 | flutterEnginePointer := *(*uintptr)(userData)
24 | flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
25 | return C.bool(flutterEngine.GLMakeCurrent())
26 | }
27 |
28 | //export proxy_clear_current
29 | func proxy_clear_current(userData unsafe.Pointer) C.bool {
30 | flutterEnginePointer := *(*uintptr)(userData)
31 | flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
32 | return C.bool(flutterEngine.GLClearCurrent())
33 | }
34 |
35 | //export proxy_present
36 | func proxy_present(userData unsafe.Pointer) C.bool {
37 | flutterEnginePointer := *(*uintptr)(userData)
38 | flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
39 | return C.bool(flutterEngine.GLPresent())
40 | }
41 |
42 | //export proxy_fbo_callback
43 | func proxy_fbo_callback(userData unsafe.Pointer) C.uint32_t {
44 | flutterEnginePointer := *(*uintptr)(userData)
45 | flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
46 | return C.uint32_t(flutterEngine.GLFboCallback())
47 | }
48 |
49 | //export proxy_make_resource_current
50 | func proxy_make_resource_current(userData unsafe.Pointer) C.bool {
51 | flutterEnginePointer := *(*uintptr)(userData)
52 | flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
53 | return C.bool(flutterEngine.GLMakeResourceCurrent())
54 | }
55 |
56 | //export proxy_gl_proc_resolver
57 | func proxy_gl_proc_resolver(userData unsafe.Pointer, procname *C.char) unsafe.Pointer {
58 | flutterEnginePointer := *(*uintptr)(userData)
59 | flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
60 | return flutterEngine.GLProcResolver(C.GoString(procname))
61 | }
62 |
63 | //export proxy_gl_external_texture_frame_callback
64 | func proxy_gl_external_texture_frame_callback(
65 | userData unsafe.Pointer,
66 | textureID int64,
67 | width C.size_t,
68 | height C.size_t,
69 | texture *C.FlutterOpenGLTexture,
70 | ) C.bool {
71 | flutterEnginePointer := *(*uintptr)(userData)
72 | flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
73 | embedderGLTexture := flutterEngine.GLExternalTextureFrameCallback(textureID, int(width), int(height))
74 | if embedderGLTexture == nil {
75 | return C.bool(false)
76 | }
77 | texture.target = C.uint32_t(embedderGLTexture.Target)
78 | texture.name = C.uint32_t(embedderGLTexture.Name)
79 | texture.format = C.uint32_t(embedderGLTexture.Format)
80 | return C.bool(true)
81 | }
82 |
83 | //export proxy_runs_task_on_current_thread_callback
84 | func proxy_runs_task_on_current_thread_callback(userData unsafe.Pointer) C.bool {
85 | flutterEnginePointer := *(*uintptr)(userData)
86 | flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
87 | return C.bool(flutterEngine.TaskRunnerRunOnCurrentThread())
88 | }
89 |
90 | //export proxy_post_task_callback
91 | func proxy_post_task_callback(task C.FlutterTask, targetTimeNanos C.uint64_t, userData unsafe.Pointer) {
92 | flutterEnginePointer := *(*uintptr)(userData)
93 | flutterEngine := (*FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
94 | flutterEngine.TaskRunnerPostTask(task, uint64(targetTimeNanos))
95 | }
96 |
97 | //export proxy_desktop_binary_reply
98 | func proxy_desktop_binary_reply(data *C.uint8_t, dataSize C.size_t, userData unsafe.Pointer) {
99 | callbackPointer := *(*uintptr)(userData)
100 | callback := (*DataCallback)(unsafe.Pointer(callbackPointer))
101 | callback.Handle(C.GoBytes(unsafe.Pointer(data), C.int(dataSize)))
102 | }
103 |
--------------------------------------------------------------------------------
/event-loop.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "container/heap"
5 | "fmt"
6 | "math"
7 | "time"
8 |
9 | "github.com/go-flutter-desktop/go-flutter/embedder"
10 | "github.com/go-flutter-desktop/go-flutter/internal/currentthread"
11 | "github.com/go-flutter-desktop/go-flutter/internal/priorityqueue"
12 | )
13 |
14 | // EventLoop is a event loop for the main thread that allows for delayed task
15 | // execution.
16 | type EventLoop struct {
17 | // store the task (event) by their priorities
18 | priorityqueue *priorityqueue.PriorityQueue
19 | // called when a task has been received, used to Wakeup the rendering event loop
20 | postEmptyEvent func()
21 |
22 | onExpiredTask func(*embedder.FlutterTask) error
23 |
24 | // timeout for non-Rendering events that needs to be processed in a polling manner
25 | platformMessageRefreshRate time.Duration
26 |
27 | // identifier for the current thread
28 | mainThreadID currentthread.ThreadID
29 | }
30 |
31 | func newEventLoop(postEmptyEvent func(), onExpiredTask func(*embedder.FlutterTask) error) *EventLoop {
32 | pq := priorityqueue.NewPriorityQueue()
33 | heap.Init(pq)
34 | return &EventLoop{
35 | priorityqueue: pq,
36 | postEmptyEvent: postEmptyEvent,
37 | onExpiredTask: onExpiredTask,
38 | mainThreadID: currentthread.ID(),
39 |
40 | // 25 Millisecond is arbitrary value, not too high (adds too much delay to
41 | // platform messages) and not too low (heavy CPU consumption).
42 | // This value isn't related to FPS, as rendering events are process in a
43 | // waiting manner.
44 | // Platform message are fetched from the engine every time the rendering
45 | // event loop process rendering event (e.g.: moving the cursor on the
46 | // window), when no rendering event occur (e.g., window minimized) platform
47 | // message are fetch every 25ms.
48 | platformMessageRefreshRate: time.Duration(25) * time.Millisecond,
49 | }
50 | }
51 |
52 | // RunOnCurrentThread return true if tasks posted on the
53 | // calling thread will be run on that same thread.
54 | func (t *EventLoop) RunOnCurrentThread() bool {
55 | return currentthread.Equal(currentthread.ID(), t.mainThreadID)
56 | }
57 |
58 | // PostTask posts a Flutter engine tasks to the event loop for delayed execution.
59 | // PostTask must ALWAYS be called on the same goroutine/thread as `newEventLoop`
60 | func (t *EventLoop) PostTask(task embedder.FlutterTask, targetTimeNanos uint64) {
61 |
62 | taskDuration := time.Duration(targetTimeNanos) * time.Nanosecond
63 | engineDuration := time.Duration(embedder.FlutterEngineGetCurrentTime())
64 |
65 | t.priorityqueue.Lock()
66 | item := &priorityqueue.Item{
67 | Value: task,
68 | FireTime: time.Now().Add(taskDuration - engineDuration),
69 | }
70 | heap.Push(t.priorityqueue, item)
71 | t.priorityqueue.Unlock()
72 |
73 | t.postEmptyEvent()
74 | }
75 |
76 | // WaitForEvents waits for an any Rendering or pending Flutter Engine events
77 | // and returns when either is encountered.
78 | // Expired engine events are processed
79 | func (t *EventLoop) WaitForEvents(rendererWaitEvents func(float64)) {
80 | now := time.Now()
81 |
82 | expiredTasks := make([]*priorityqueue.Item, 0)
83 | var top *priorityqueue.Item
84 |
85 | t.priorityqueue.Lock()
86 | for t.priorityqueue.Len() > 0 {
87 |
88 | // Remove the item from the delayed tasks queue.
89 | top = heap.Pop(t.priorityqueue).(*priorityqueue.Item)
90 |
91 | // If this task (and all tasks after this) has not yet expired, there is
92 | // nothing more to do. Quit iterating.
93 | if top.FireTime.After(now) {
94 | heap.Push(t.priorityqueue, top) // push the item back into the queue
95 | break
96 | }
97 |
98 | // Make a record of the expired task. Do NOT service the task here
99 | // because we are still holding onto the task queue mutex. We don't want
100 | // other threads to block on posting tasks onto this thread till we are
101 | // done processing expired tasks.
102 | expiredTasks = append(expiredTasks, top)
103 |
104 | }
105 | hasTask := t.priorityqueue.Len() != 0
106 | t.priorityqueue.Unlock()
107 |
108 | // Fire expired tasks.
109 | for _, item := range expiredTasks {
110 | task := item.Value
111 | if err := t.onExpiredTask(&task); err != nil {
112 | fmt.Printf("go-flutter: couldn't process task %v: %v\n", task, err)
113 | }
114 | }
115 |
116 | // Sleep till the next task needs to be processed. If a new task comes
117 | // along, the rendererWaitEvents will be resolved early because PostTask
118 | // posts an empty event.
119 | if !hasTask {
120 | rendererWaitEvents(t.platformMessageRefreshRate.Seconds())
121 | } else {
122 | if top.FireTime.After(now) {
123 | durationWait := math.Min(top.FireTime.Sub(now).Seconds(), t.platformMessageRefreshRate.Seconds())
124 | rendererWaitEvents(durationWait)
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/glfw.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | "unsafe"
7 |
8 | "github.com/go-flutter-desktop/go-flutter/embedder"
9 | "github.com/go-gl/glfw/v3.3/glfw"
10 | )
11 |
12 | // dpPerInch defines the amount of display pixels per inch as defined for Flutter.
13 | const dpPerInch = 160.0
14 |
15 | // TODO (GeertJohan): better name for this, confusing with 'actual' window
16 | // managers. Renderer interface? implemented by this type for glfw? type
17 | // glfwRenderer or glfwManager? All the attaching to glfw.Window must be done
18 | // during manager init in that case. Cannot be done by Application.
19 | type windowManager struct {
20 | // forcedPixelRatio forces the pixelRatio to given value, when value is not zero.
21 | forcedPixelRatio float64
22 |
23 | // current pointer state
24 | pointerPhase embedder.PointerPhase
25 | pointerButton embedder.PointerButtonMouse
26 | pointerCurrentlyAdded bool
27 |
28 | // caching of ppsc to avoid re-calculating every event
29 | pixelsPerScreenCoordinate float64
30 | }
31 |
32 | func newWindowManager(forcedPixelRatio float64) *windowManager {
33 | return &windowManager{
34 | forcedPixelRatio: forcedPixelRatio,
35 | pixelsPerScreenCoordinate: 1.0,
36 | pointerPhase: embedder.PointerPhaseHover,
37 | }
38 | }
39 |
40 | func (m *windowManager) sendPointerEvent(window *glfw.Window, phase embedder.PointerPhase, x, y float64) {
41 | // synthesize an PointerPhaseAdd if the pointer isn't already added
42 | if !m.pointerCurrentlyAdded && phase != embedder.PointerPhaseAdd {
43 | m.sendPointerEvent(window, embedder.PointerPhaseAdd, x, y)
44 | }
45 |
46 | // Don't double-add the pointer
47 | if m.pointerCurrentlyAdded && phase == embedder.PointerPhaseAdd {
48 | return
49 | }
50 |
51 | event := embedder.PointerEvent{
52 | Phase: phase,
53 | X: x * m.pixelsPerScreenCoordinate,
54 | Y: y * m.pixelsPerScreenCoordinate,
55 | Buttons: m.pointerButton,
56 | }
57 |
58 | flutterEnginePointer := *(*uintptr)(window.GetUserPointer())
59 | flutterEngine := (*embedder.FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
60 |
61 | // Always send a pointer event with PhaseMove before an eventual PhaseRemove.
62 | // If x/y on the last move doesn't equal x/y on the PhaseRemove, the remove
63 | // is canceled in Flutter.
64 | if phase == embedder.PointerPhaseRemove {
65 | event.Phase = embedder.PointerPhaseHover
66 | flutterEngine.SendPointerEvent(event)
67 | event.Phase = embedder.PointerPhaseRemove
68 | }
69 |
70 | flutterEngine.SendPointerEvent(event)
71 |
72 | if phase == embedder.PointerPhaseAdd {
73 | m.pointerCurrentlyAdded = true
74 | } else if phase == embedder.PointerPhaseRemove {
75 | m.pointerCurrentlyAdded = false
76 | }
77 | }
78 |
79 | func (m *windowManager) sendPointerEventButton(window *glfw.Window, phase embedder.PointerPhase) {
80 | x, y := window.GetCursorPos()
81 | event := embedder.PointerEvent{
82 | Phase: phase,
83 | X: x * m.pixelsPerScreenCoordinate,
84 | Y: y * m.pixelsPerScreenCoordinate,
85 | SignalKind: embedder.PointerSignalKindNone,
86 | Buttons: m.pointerButton,
87 | }
88 | flutterEnginePointer := *(*uintptr)(window.GetUserPointer())
89 | flutterEngine := (*embedder.FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
90 | flutterEngine.SendPointerEvent(event)
91 | }
92 |
93 | func (m *windowManager) sendPointerEventScroll(window *glfw.Window, xDelta, yDelta float64) {
94 | x, y := window.GetCursorPos()
95 | event := embedder.PointerEvent{
96 | Phase: m.pointerPhase,
97 | X: x * m.pixelsPerScreenCoordinate,
98 | Y: y * m.pixelsPerScreenCoordinate,
99 | SignalKind: embedder.PointerSignalKindScroll,
100 | ScrollDeltaX: xDelta,
101 | ScrollDeltaY: yDelta,
102 | Buttons: m.pointerButton,
103 | }
104 |
105 | flutterEnginePointer := *(*uintptr)(window.GetUserPointer())
106 | flutterEngine := (*embedder.FlutterEngine)(unsafe.Pointer(flutterEnginePointer))
107 |
108 | flutterEngine.SendPointerEvent(event)
109 | }
110 |
111 | func (m *windowManager) glfwCursorEnterCallback(window *glfw.Window, entered bool) {
112 | x, y := window.GetCursorPos()
113 | if entered {
114 | m.sendPointerEvent(window, embedder.PointerPhaseAdd, x, y)
115 | // the mouse can enter the windows while having button pressed.
116 | // if so, don't overwrite the phase.
117 | if m.pointerButton == 0 {
118 | m.pointerPhase = embedder.PointerPhaseHover
119 | }
120 | } else {
121 | // if the mouse is still in 'phaseMove' outside the window (click-drag
122 | // outside). Don't remove the cursor.
123 | if m.pointerButton == 0 {
124 | m.sendPointerEvent(window, embedder.PointerPhaseRemove, x, y)
125 | }
126 | }
127 | }
128 |
129 | func (m *windowManager) glfwCursorPosCallback(window *glfw.Window, x, y float64) {
130 | m.sendPointerEvent(window, m.pointerPhase, x, y)
131 | }
132 |
133 | func (m *windowManager) handleButtonPhase(window *glfw.Window, action glfw.Action, buttons embedder.PointerButtonMouse) {
134 | if action == glfw.Press {
135 | m.pointerButton |= buttons
136 | // If only one button is pressed then each bits of buttons will be equals
137 | // to m.pointerButton.
138 | if m.pointerButton == buttons {
139 | m.sendPointerEventButton(window, embedder.PointerPhaseDown)
140 | } else {
141 | // if any other buttons are already pressed when a new button is pressed,
142 | // the engine is expecting a Move phase instead of a Down phase.
143 | m.sendPointerEventButton(window, embedder.PointerPhaseMove)
144 | }
145 | m.pointerPhase = embedder.PointerPhaseMove
146 | }
147 |
148 | if action == glfw.Release {
149 | // Always send a pointer event with PhaseMove before an eventual
150 | // PhaseUp. Even if the last button was released. If x/y on the last
151 | // move doesn't equal x/y on the PhaseUp, the click is canceled in
152 | // Flutter. On MacOS, the Release event always has y-1 of the last move
153 | // event. By sending a PhaseMove here (after the release) we avoid a
154 | // difference in x/y.
155 | m.sendPointerEventButton(window, embedder.PointerPhaseMove)
156 |
157 | m.pointerButton ^= buttons
158 | // If all button are released then m.pointerButton is cleared
159 | if m.pointerButton == 0 {
160 | m.sendPointerEventButton(window, embedder.PointerPhaseUp)
161 | m.pointerPhase = embedder.PointerPhaseHover
162 | } else {
163 | // if any other buttons are still pressed when one button is released
164 | // the engine is expecting a Move phase instead of a Up phase.
165 | m.sendPointerEventButton(window, embedder.PointerPhaseMove)
166 | }
167 | }
168 | }
169 |
170 | func (m *windowManager) glfwMouseButtonCallback(window *glfw.Window, key glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
171 | switch key {
172 | case glfw.MouseButtonLeft:
173 | m.handleButtonPhase(window, action, embedder.PointerButtonMousePrimary)
174 | case glfw.MouseButtonRight:
175 | m.handleButtonPhase(window, action, embedder.PointerButtonMouseSecondary)
176 | case glfw.MouseButtonMiddle:
177 | m.handleButtonPhase(window, action, embedder.PointerButtonMouseMiddle)
178 | default:
179 | m.handleButtonPhase(window, action, 1<
11 | // typedef DWORD thrd_t;
12 | // #else
13 | // #include
14 | // typedef pthread_t thrd_t;
15 | // #endif
16 | //
17 | // int thrd_equal(thrd_t thr0, thrd_t thr1) {
18 | // #if defined(_WIN32) || defined(__WIN32__) || defined(__WINDOWS__)
19 | // return thr0 == thr1;
20 | // #else
21 | // return pthread_equal(thr0, thr1);
22 | // #endif
23 | // }
24 | //
25 | // thrd_t thrd_current(void) {
26 | // #if defined(_WIN32) || defined(__WIN32__) || defined(__WINDOWS__)
27 | // return GetCurrentThreadId();
28 | // #else
29 | // return pthread_self();
30 | // #endif
31 | // }
32 | import "C"
33 |
34 | // ThreadID correspond to an opaque thread identifier
35 | type ThreadID C.thrd_t
36 |
37 | // ID returns the id of the current thread
38 | func ID() ThreadID {
39 | return (ThreadID)(C.thrd_current())
40 | }
41 |
42 | // Equal compares two thread identifiers.
43 | func Equal(t1, t2 ThreadID) bool {
44 | return C.thrd_equal((C.thrd_t)(t1), (C.thrd_t)(t2)) != 0
45 | }
46 |
--------------------------------------------------------------------------------
/internal/debounce/debounce.go:
--------------------------------------------------------------------------------
1 | // Copyright © 2019 Bjørn Erik Pedersen .
2 | //
3 | // Use of this source code is governed by an MIT-style
4 | // license that can be found in the LICENSE file.
5 |
6 | // Package debounce provides a debouncer func. The most typical use case would be
7 | // the user typing a text into a form; the UI needs an update, but let's wait for
8 | // a break.
9 | //
10 | // Copied from https://github.com/bep/debounce/blob/master/debounce.go
11 | package debounce
12 |
13 | import (
14 | "sync"
15 | "time"
16 | )
17 |
18 | // New returns a debounced function that takes another functions as its argument.
19 | // This function will be called when the debounced function stops being called
20 | // for the given duration.
21 | // The debounced function can be invoked with different functions, if needed,
22 | // the last one will win.
23 | func New(after time.Duration) func(f func()) {
24 | d := &debouncer{after: after}
25 |
26 | return func(f func()) {
27 | d.add(f)
28 | }
29 | }
30 |
31 | type debouncer struct {
32 | mu sync.Mutex
33 | after time.Duration
34 | timer *time.Timer
35 | }
36 |
37 | func (d *debouncer) add(f func()) {
38 | d.mu.Lock()
39 | defer d.mu.Unlock()
40 |
41 | if d.timer != nil {
42 | d.timer.Stop()
43 | }
44 | d.timer = time.AfterFunc(d.after, f)
45 | }
46 |
--------------------------------------------------------------------------------
/internal/execpath/path.go:
--------------------------------------------------------------------------------
1 | package execpath
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | )
7 |
8 | var execPath string
9 |
10 | // ExecPath returns the absolute path for the currently running executable. The
11 | // path is cached after first call.
12 | func ExecPath() (string, error) {
13 | if execPath != "" {
14 | return execPath, nil
15 | }
16 | var err error
17 | execPath, err = os.Executable()
18 | if err != nil {
19 | return "", err
20 | }
21 | execPath, err = filepath.EvalSymlinks(execPath)
22 | if err != nil {
23 | return "", err
24 | }
25 | return execPath, nil
26 | }
27 |
--------------------------------------------------------------------------------
/internal/keyboard/keyboard.go:
--------------------------------------------------------------------------------
1 | package keyboard
2 |
3 | import (
4 | "fmt"
5 | "runtime/debug"
6 |
7 | "github.com/go-gl/glfw/v3.3/glfw"
8 | "github.com/pkg/errors"
9 | )
10 |
11 | // Event corresponds to a Flutter (dart) compatible RawKeyEventData keyevent data.
12 | // Multi-platform keycode translation is handled within this package.
13 | //
14 | // As input, go-flutter gets GLFW-keyevent who are only compatible with
15 | // RawKeyEventDataLinux. To fully support keyboard shortcut (like Command+C to
16 | // copy on darwin), the flutter framework expect the sent keyevent data to be
17 | // in the form of a RawKeyEventDataMacOs keyevent data.
18 | // This package maps the GLFW key-codes to the MacOS ones.
19 | //
20 | // On the flutter framework side:
21 | // RawKeyEventDataMacOs data is received for darwin
22 | // RawKeyEventDataLinux data is received for linux and windows
23 | type Event struct {
24 | // Common
25 | Keymap string `json:"keymap"` // Linux/MacOS switch
26 | Character string `json:"character"`
27 | KeyCode int `json:"keyCode"`
28 | Modifiers int `json:"modifiers"`
29 | Type string `json:"type"`
30 |
31 | // Linux
32 | Toolkit string `json:"toolkit,omitempty"`
33 | ScanCode int `json:"scanCode,omitempty"`
34 | UnicodeScalarValues uint32 `json:"unicodeScalarValues,omitempty"`
35 |
36 | // MacOS
37 | CharactersIgnoringModifiers string `json:"charactersIgnoringModifiers,omitempty"`
38 | Characters string `json:"characters,omitempty"`
39 | }
40 |
41 | // Normalize takes a GLFW-based key and normalizes it by converting
42 | // the input to a keyboard.Event struct compatible with Flutter for the current
43 | // OS.
44 | //
45 | // RawKeyEventDataMacOs data for darwin
46 | // RawKeyEventDataLinux data for linux and windows
47 | func Normalize(key glfw.Key, scancode int, mods glfw.ModifierKey, action glfw.Action) (event Event, err error) {
48 | var typeKey string
49 | if action == glfw.Release {
50 | typeKey = "keyup"
51 | } else if action == glfw.Press {
52 | typeKey = "keydown"
53 | } else if action == glfw.Repeat {
54 | typeKey = "keydown"
55 | } else {
56 | return event, errors.Errorf("unknown key event type: %v\n", action)
57 | }
58 |
59 | defer func() {
60 | p := recover()
61 | if p != nil {
62 | fmt.Printf("go-flutter: recovered from panic while handling %s event: %v\n", typeKey, p)
63 | debug.PrintStack()
64 | }
65 | }()
66 |
67 | // This function call can fail with panic()
68 | utf8 := glfw.GetKeyName(key, scancode)
69 |
70 | event = Event{
71 | Type: typeKey,
72 | Character: utf8,
73 | UnicodeScalarValues: codepointFromGLFWKey([]rune(utf8)),
74 | }
75 |
76 | event.platfromNormalize(key, scancode, mods)
77 |
78 | return event, nil
79 | }
80 |
81 | // codepointFromGLFWKey Queries for the printable key name given a key.
82 | // The Flutter framework accepts only one code point, therefore, only the first
83 | // code point will be used. There is unlikely to be more than one, but there
84 | // is no guarantee that it won't happen.
85 | func codepointFromGLFWKey(utf8 []rune) uint32 {
86 | if len(utf8) == 0 {
87 | return 0
88 | }
89 | return uint32(utf8[0])
90 | }
91 |
--------------------------------------------------------------------------------
/internal/keyboard/keyboard_pc.go:
--------------------------------------------------------------------------------
1 | // +build !darwin
2 |
3 | package keyboard
4 |
5 | import "github.com/go-gl/glfw/v3.3/glfw"
6 |
7 | // DetectTextInputDoneMod returns true if the modifiers pressed
8 | // indicate the typed text can be committed
9 | func DetectTextInputDoneMod(mods glfw.ModifierKey) bool {
10 | return mods&glfw.ModControl != 0
11 | }
12 |
13 | // platfromNormalize normalizes for linux and windows
14 | func (e *Event) platfromNormalize(key glfw.Key, scancode int, mods glfw.ModifierKey) {
15 | e.Keymap = "linux"
16 | e.Toolkit = "glfw"
17 | e.Modifiers = int(mods)
18 | e.KeyCode = int(key)
19 | e.ScanCode = scancode
20 | }
21 |
--------------------------------------------------------------------------------
/internal/opengl/doc.go:
--------------------------------------------------------------------------------
1 | // Package opengl wraps the go-gl/gl OpenGL bindings with a compile-time OpenGL
2 | // version selector.
3 | //
4 | // This package allows clients to target multiple version of OpenGL when
5 | // building go-flutter.
6 | // default is v3.3
7 | //
8 | // Golang build constraints are used for version selection.
9 | package opengl
10 |
--------------------------------------------------------------------------------
/internal/opengl/opengl.go:
--------------------------------------------------------------------------------
1 | // +build !openglnone
2 |
3 | package opengl
4 |
5 | // The default version (3.3) of OpenGL used by go-flutter.
6 | // If you want to support other version, copy/pase this file, change the import
7 | // statement, add builds constraints and open a PR.
8 |
9 | import (
10 | "unsafe"
11 |
12 | "github.com/go-gl/gl/v3.3-core/gl"
13 | "github.com/go-gl/glfw/v3.3/glfw"
14 | )
15 |
16 | // const exposed to go-flutter
17 | const (
18 | TEXTURE2D = gl.TEXTURE_2D
19 | RGBA8 = gl.RGBA8
20 | )
21 |
22 | // Init opengl
23 | func Init() error {
24 | return gl.Init()
25 | }
26 |
27 | // DeleteTextures deletes named textures
28 | func DeleteTextures(n int32, textures *uint32) {
29 | gl.DeleteTextures(n, textures)
30 | }
31 |
32 | // CreateTexture creates a texture for go-flutter uses
33 | func CreateTexture(texture *uint32) {
34 | gl.GenTextures(1, texture)
35 | gl.BindTexture(gl.TEXTURE_2D, *texture)
36 | // set the texture wrapping parameters
37 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_BORDER)
38 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_BORDER)
39 | // set texture filtering parameters
40 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
41 | gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
42 | }
43 |
44 | // BindTexture binds a named texture to a texturing target
45 | func BindTexture(texture uint32) {
46 | gl.BindTexture(gl.TEXTURE_2D, texture)
47 | }
48 |
49 | // Ptr takes a slice or pointer (to a singular scalar value or the first
50 | // element of an array or slice) and returns its GL-compatible address.
51 | func Ptr(data interface{}) unsafe.Pointer {
52 | return gl.Ptr(data)
53 | }
54 |
55 | // TexImage2D specifies a two-dimensional texture image
56 | func TexImage2D(width, height int32, pixels unsafe.Pointer) {
57 | // It the current flutter/engine can only support RGBA texture.
58 | gl.TexImage2D(
59 | gl.TEXTURE_2D,
60 | 0,
61 | gl.RGBA,
62 | width,
63 | height,
64 | 0,
65 | gl.RGBA,
66 | gl.UNSIGNED_BYTE,
67 | pixels,
68 | )
69 | }
70 |
71 | // GLFWWindowHint sets hints for the next call to CreateWindow.
72 | func GLFWWindowHint() {
73 | glfw.WindowHint(glfw.ContextVersionMajor, 3)
74 | glfw.WindowHint(glfw.ContextVersionMinor, 3)
75 | glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
76 | glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
77 | }
78 |
--------------------------------------------------------------------------------
/internal/opengl/opengl_none.go:
--------------------------------------------------------------------------------
1 | // +build openglnone
2 |
3 | package opengl
4 |
5 | // Don't link go-flutter against OpenGL, less-dependency at the cost of not
6 | // supporting external texture (eg: video_plugin)
7 | //
8 | // compile go-flutter with: hover build|run --opengl=none
9 |
10 | import (
11 | "unsafe"
12 | )
13 |
14 | // const exposed to go-flutter
15 | const (
16 | TEXTURE2D = 0
17 | RGBA8 = 0
18 | )
19 |
20 | // Init opengl
21 | func Init() error { return nil }
22 |
23 | // DeleteTextures deletes named textures
24 | func DeleteTextures(n int32, textures *uint32) {}
25 |
26 | // CreateTexture creates a texture for go-flutter uses
27 | func CreateTexture(texture *uint32) {}
28 |
29 | // BindTexture binds a named texture to a texturing target
30 | func BindTexture(texture uint32) {}
31 |
32 | // Ptr takes a slice or pointer (to a singular scalar value or the first
33 | // element of an array or slice) and returns its GL-compatible address.
34 | func Ptr(data interface{}) unsafe.Pointer { return nil }
35 |
36 | // TexImage2D specifies a two-dimensional texture image
37 | func TexImage2D(width, height int32, pixels unsafe.Pointer) {
38 | panic("go-flutter: go-flutter wasn't compiled with support for external texture plugin.")
39 | }
40 |
41 | // GLFWWindowHint sets hints for the next call to CreateWindow.
42 | func GLFWWindowHint() {}
43 |
--------------------------------------------------------------------------------
/internal/priorityqueue/priorityqueue.go:
--------------------------------------------------------------------------------
1 | package priorityqueue
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "github.com/go-flutter-desktop/go-flutter/embedder"
8 | )
9 |
10 | // An Item is something we manage in a priority queue.
11 | type Item struct {
12 | Value embedder.FlutterTask // The value of the item
13 | FireTime time.Time // The priority of the item in the queue.
14 |
15 | // The index is needed by update and is maintained by the heap.Interface methods.
16 | index int // The index of the item in the heap.
17 | }
18 |
19 | // A PriorityQueue implements heap.Interface and holds Items.
20 | type PriorityQueue struct {
21 | queue []*Item
22 | sync.Mutex
23 | }
24 |
25 | // NewPriorityQueue create a new PriorityQueue
26 | func NewPriorityQueue() *PriorityQueue {
27 | pq := &PriorityQueue{}
28 | pq.queue = make([]*Item, 0)
29 | return pq
30 | }
31 |
32 | func (pq *PriorityQueue) Len() int { return len(pq.queue) }
33 |
34 | func (pq *PriorityQueue) Less(i, j int) bool {
35 | // We want Pop to give us the lowest, not highest, priority so we use lower
36 | // than here.
37 | return pq.queue[i].FireTime.Before(pq.queue[j].FireTime)
38 | }
39 |
40 | func (pq *PriorityQueue) Swap(i, j int) {
41 | pq.queue[i], pq.queue[j] = pq.queue[j], pq.queue[i]
42 | pq.queue[i].index = i
43 | pq.queue[j].index = j
44 | }
45 |
46 | // Push add a new priority/value pair in the queue. 0 priority = max.
47 | func (pq *PriorityQueue) Push(x interface{}) {
48 | n := len(pq.queue)
49 | item := x.(*Item)
50 | item.index = n
51 | pq.queue = append(pq.queue, item)
52 | }
53 |
54 | // Pop Remove and return the highest item (lowest priority)
55 | func (pq *PriorityQueue) Pop() interface{} {
56 | old := pq.queue
57 | n := len(old)
58 | item := old[n-1]
59 | item.index = -1 // for safety
60 | pq.queue = old[0 : n-1]
61 | return item
62 | }
63 |
--------------------------------------------------------------------------------
/internal/tasker/tasker.go:
--------------------------------------------------------------------------------
1 | package tasker
2 |
3 | // Tasker is a small helper utility that makes it easy to move tasks to a
4 | // different goroutine. This is useful when some work must be executed from a
5 | // specific goroutine / OS thread.
6 | type Tasker struct {
7 | taskCh chan task
8 | }
9 |
10 | type task struct {
11 | f func()
12 | doneCh chan struct{}
13 | }
14 |
15 | // New prepares a new Tasker.
16 | func New() *Tasker {
17 | t := &Tasker{
18 | taskCh: make(chan task),
19 | }
20 | return t
21 | }
22 |
23 | // Do runs the given function in the goroutine where ExecuteTasks is called. Do
24 | // blocks until the given function has completed.
25 | func (t *Tasker) Do(f func()) {
26 | doneCh := make(chan struct{})
27 | t.taskCh <- task{
28 | f: f,
29 | doneCh: doneCh,
30 | }
31 | <-doneCh
32 | }
33 |
34 | // ExecuteTasks executes any pending tasks, then returns.
35 | func (t *Tasker) ExecuteTasks() {
36 | for {
37 | select {
38 | case task := <-t.taskCh:
39 | task.f()
40 | task.doneCh <- struct{}{}
41 | default:
42 | return
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/isolate.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import "github.com/go-flutter-desktop/go-flutter/plugin"
4 |
5 | type isolatePlugin struct{}
6 |
7 | // hard-coded because there is no swappable renderer interface.
8 | var defaultIsolatePlugin = &isolatePlugin{}
9 |
10 | func (p *isolatePlugin) InitPlugin(messenger plugin.BinaryMessenger) error {
11 | channel := plugin.NewBasicMessageChannel(messenger, "flutter/isolate", plugin.StringCodec{})
12 | // Ignored: go-flutter doesn't support isolate events
13 | channel.HandleFunc(func(_ interface{}) (interface{}, error) { return nil, nil })
14 | return nil
15 | }
16 |
--------------------------------------------------------------------------------
/key-events.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/go-flutter-desktop/go-flutter/internal/keyboard"
8 | "github.com/go-flutter-desktop/go-flutter/plugin"
9 | "github.com/go-gl/glfw/v3.3/glfw"
10 | )
11 |
12 | const keyEventChannelName = "flutter/keyevent"
13 |
14 | // keyeventPlugin implements flutter.Plugin and handles method calls to
15 | // flutter/keyevent channel.
16 | // The sent keyevents are RawKeyEventDataLinux on linux and windows
17 | // The sent keyevents are RawKeyEventDataMacOs on darwin (needs a conversion layer)
18 | type keyeventPlugin struct {
19 | channel *plugin.BasicMessageChannel
20 | }
21 |
22 | var defaultKeyeventsPlugin = &keyeventPlugin{}
23 |
24 | func (p *keyeventPlugin) InitPlugin(messenger plugin.BinaryMessenger) error {
25 | p.channel = plugin.NewBasicMessageChannel(messenger, keyEventChannelName, keyEventJSONMessageCodec{})
26 | return nil
27 | }
28 |
29 | type keyEventJSONMessageCodec struct{}
30 |
31 | // EncodeMessage encodes a keyEventMessage to a slice of bytes.
32 | func (j keyEventJSONMessageCodec) EncodeMessage(message interface{}) (binaryMessage []byte, err error) {
33 | return json.Marshal(message)
34 | }
35 |
36 | // send-only channel
37 | func (j keyEventJSONMessageCodec) DecodeMessage(binaryMessage []byte) (message interface{}, err error) {
38 | return message, err
39 | }
40 |
41 | func (p *keyeventPlugin) sendKeyEvent(window *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
42 | event, err := keyboard.Normalize(key, scancode, mods, action)
43 | if err != nil {
44 | fmt.Printf("go-flutter: failed to Normalize key event: %v", err)
45 | return
46 | }
47 |
48 | err = p.channel.Send(event)
49 | if err != nil {
50 | fmt.Printf("go-flutter: Failed to send raw_keyboard event %v: %v\n", event, err)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lifecycle.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/go-flutter-desktop/go-flutter/plugin"
7 | "github.com/go-gl/glfw/v3.3/glfw"
8 | )
9 |
10 | const lifecycleChannelName = "flutter/lifecycle"
11 |
12 | // lifecyclePlugin implements flutter.Plugin and handles method calls to the
13 | // flutter/lifecycle channel.
14 | type lifecyclePlugin struct {
15 | channel *plugin.BasicMessageChannel
16 | }
17 |
18 | // all hardcoded because theres not pluggable renderer system.
19 | var defaultLifecyclePlugin = &lifecyclePlugin{}
20 |
21 | func (p *lifecyclePlugin) InitPlugin(messenger plugin.BinaryMessenger) error {
22 | p.channel = plugin.NewBasicMessageChannel(messenger, lifecycleChannelName, plugin.StringCodec{})
23 | return nil
24 | }
25 |
26 | func (p *lifecyclePlugin) glfwIconifyCallback(w *glfw.Window, iconified bool) {
27 | var state string
28 | switch iconified {
29 | case true:
30 | state = "AppLifecycleState.paused"
31 | case false:
32 | state = "AppLifecycleState.resumed"
33 | }
34 | err := p.channel.Send(state)
35 | if err != nil {
36 | fmt.Printf("go-flutter: Failed to send lifecycle event %s: %v\n", state, err)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/mascot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-flutter-desktop/go-flutter/0fcb3ed6cbc524efd082470654c7e91d59799432/mascot.png
--------------------------------------------------------------------------------
/messenger.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | "sync"
7 |
8 | "github.com/go-flutter-desktop/go-flutter/embedder"
9 | "github.com/go-flutter-desktop/go-flutter/internal/tasker"
10 | "github.com/go-flutter-desktop/go-flutter/plugin"
11 | )
12 |
13 | type messenger struct {
14 | engine *embedder.FlutterEngine
15 |
16 | channels map[string]plugin.ChannelHandlerFunc
17 | channelsLock sync.RWMutex
18 |
19 | // engineTasker holds tasks which must be executed in the engine thread
20 | engineTasker *tasker.Tasker
21 | }
22 |
23 | var _ plugin.BinaryMessenger = &messenger{}
24 |
25 | func newMessenger(engine *embedder.FlutterEngine) *messenger {
26 | return &messenger{
27 | engine: engine,
28 | channels: make(map[string]plugin.ChannelHandlerFunc),
29 | engineTasker: tasker.New(),
30 | }
31 | }
32 |
33 | // SendWithReply pushes a binary message on a channel to the Flutter side and
34 | // wait for a reply.
35 | // NOTE: If no value are returned by the flutter handler, the function will
36 | // wait forever. In case you don't want to wait for reply, use Send.
37 | func (m *messenger) SendWithReply(channel string, binaryMessage []byte) (binaryReply []byte, err error) {
38 | reply := make(chan []byte)
39 | defer close(reply)
40 | callbackHandle := &embedder.DataCallback{
41 | Handle: func(binaryMessage []byte) {
42 | reply <- binaryMessage
43 | },
44 | }
45 | defer runtime.KeepAlive(callbackHandle)
46 | responseHandle, err := m.engine.CreatePlatformMessageResponseHandle(callbackHandle)
47 | if err != nil {
48 | return nil, err
49 | }
50 | defer m.engine.ReleasePlatformMessageResponseHandle(responseHandle)
51 |
52 | msg := &embedder.PlatformMessage{
53 | Channel: channel,
54 | Message: binaryMessage,
55 | ResponseHandle: responseHandle,
56 | }
57 |
58 | if m.engine.TaskRunnerRunOnCurrentThread() {
59 | err = m.engine.SendPlatformMessage(msg)
60 | } else {
61 | replyErr := make(chan error)
62 | defer close(replyErr)
63 |
64 | postEmptyEvent()
65 | go m.engineTasker.Do(func() {
66 | replyErr <- m.engine.SendPlatformMessage(msg)
67 | })
68 | err = <-replyErr
69 | }
70 |
71 | // wait for a reply and return
72 | return <-reply, nil
73 | }
74 |
75 | // Send pushes a binary message on a channel to the Flutter side without
76 | // expecting replies.
77 | func (m *messenger) Send(channel string, binaryMessage []byte) (err error) {
78 | msg := &embedder.PlatformMessage{
79 | Channel: channel,
80 | Message: binaryMessage,
81 | }
82 |
83 | if m.engine.TaskRunnerRunOnCurrentThread() {
84 | err = m.engine.SendPlatformMessage(msg)
85 | } else {
86 | replyErr := make(chan error)
87 | defer close(replyErr)
88 |
89 | postEmptyEvent()
90 | go m.engineTasker.Do(func() {
91 | replyErr <- m.engine.SendPlatformMessage(msg)
92 | })
93 | err = <-replyErr
94 | }
95 | if err != nil {
96 | return err
97 | }
98 |
99 | return nil
100 | }
101 |
102 | // SetChannelHandler satisfies plugin.BinaryMessenger
103 | func (m *messenger) SetChannelHandler(channel string, channelHandler plugin.ChannelHandlerFunc) {
104 | m.channelsLock.Lock()
105 | if channelHandler == nil {
106 | delete(m.channels, channel)
107 | } else {
108 | m.channels[channel] = channelHandler
109 | }
110 | m.channelsLock.Unlock()
111 | }
112 |
113 | func (m *messenger) handlePlatformMessage(message *embedder.PlatformMessage) {
114 | m.channelsLock.RLock()
115 | channelHander := m.channels[message.Channel]
116 | m.channelsLock.RUnlock()
117 |
118 | if channelHander == nil {
119 | // print a log, but continue on to send a nil reply when required
120 | fmt.Println("go-flutter: no handler found for channel " + message.Channel)
121 | return
122 | }
123 |
124 | var err error
125 | err = channelHander(message.Message, responseSender{
126 | engine: m.engine,
127 | message: message,
128 | engineTasker: m.engineTasker,
129 | })
130 | if err != nil {
131 | fmt.Printf("go-flutter: handling message on channel "+message.Channel+" failed: %v\n", err)
132 | }
133 | }
134 |
135 | type responseSender struct {
136 | engine *embedder.FlutterEngine
137 | message *embedder.PlatformMessage
138 | engineTasker *tasker.Tasker
139 | }
140 |
141 | func (r responseSender) Send(binaryReply []byte) {
142 | if !r.message.ExpectsResponse() {
143 | return // quick path when no response should be sent
144 | }
145 |
146 | // TODO: detect multiple responses on the same message and spam the log
147 | // about it.
148 |
149 | postEmptyEvent()
150 | go r.engineTasker.Do(func() {
151 | err := r.engine.SendPlatformMessageResponse(r.message.ResponseHandle, binaryReply)
152 | if err != nil {
153 | fmt.Printf("go-flutter: failed sending response for message on channel '%s': %v", r.message.Channel, err)
154 | }
155 | })
156 | }
157 |
--------------------------------------------------------------------------------
/mousecursor.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "fmt"
5 | "github.com/go-flutter-desktop/go-flutter/plugin"
6 | "github.com/go-gl/glfw/v3.3/glfw"
7 | )
8 |
9 | const mousecursorChannelName = "flutter/mousecursor"
10 |
11 | // mousecursorPlugin implements flutter.Plugin and handles method calls to the
12 | // flutter/mousecursor channel.
13 | type mousecursorPlugin struct {
14 | window *glfw.Window
15 | lastCursor *glfw.Cursor
16 | }
17 |
18 | var defaultMousecursorPlugin = &mousecursorPlugin{}
19 |
20 | func (p *mousecursorPlugin) InitPlugin(messenger plugin.BinaryMessenger) error {
21 | channel := plugin.NewMethodChannel(messenger, mousecursorChannelName, plugin.StandardMethodCodec{})
22 | channel.HandleFuncSync("activateSystemCursor", p.handleActivateSystemCursor)
23 | return nil
24 | }
25 | func (p *mousecursorPlugin) InitPluginGLFW(window *glfw.Window) error {
26 | p.window = window
27 | return nil
28 | }
29 |
30 | func (p *mousecursorPlugin) handleActivateSystemCursor(arguments interface{}) (reply interface{}, err error) {
31 | args := arguments.(map[interface{}]interface{})
32 | var cursor *glfw.Cursor
33 | if args["kind"] == "none" {
34 | p.window.SetInputMode(glfw.CursorMode, glfw.CursorHidden)
35 | } else {
36 | p.window.SetInputMode(glfw.CursorMode, glfw.CursorNormal)
37 | }
38 | switch kind := args["kind"]; {
39 | case kind == "none" || kind == "basic":
40 | // nil cursor resets to standard arrow cursor
41 | case kind == "forbidden" || kind == "grab" || kind == "grabbing":
42 | // nil cursor resets to standard arrow cursor
43 | // go-gl GLFW currently (latest tagged v3.3 version) has no cursors for "forbidden", "grab" and "grabbing"
44 | // TODO: Wait for https://github.com/glfw/glfw/commit/7dbdd2e6a5f01d2a4b377a197618948617517b0e to appear in go-gl GLFW and implement the "forbidden" cursor
45 | case kind == "click":
46 | cursor = glfw.CreateStandardCursor(glfw.HandCursor)
47 | case kind == "text":
48 | cursor = glfw.CreateStandardCursor(glfw.IBeamCursor)
49 | default:
50 | return nil, fmt.Errorf("cursor kind %s not implemented", args["kind"])
51 | }
52 | p.window.SetCursor(cursor)
53 | if p.lastCursor != nil {
54 | p.lastCursor.Destroy()
55 | }
56 | p.lastCursor = cursor
57 | return nil, nil
58 | }
59 |
--------------------------------------------------------------------------------
/navigation.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "github.com/go-flutter-desktop/go-flutter/plugin"
5 | )
6 |
7 | const navigationChannelName = "flutter/navigation"
8 |
9 | // navigationPlugin implements flutter.Plugin and handles method calls to the
10 | // flutter/navigation channel.
11 | type navigationPlugin struct {
12 | channel *plugin.MethodChannel
13 | }
14 |
15 | // all hardcoded because theres not pluggable renderer system.
16 | var defaultNavigationPlugin = &navigationPlugin{}
17 |
18 | func (p *navigationPlugin) InitPlugin(messenger plugin.BinaryMessenger) error {
19 | p.channel = plugin.NewMethodChannel(messenger, navigationChannelName, plugin.JSONMethodCodec{})
20 |
21 | // Ignored: This information isn't properly formated to set the window.SetTitle
22 | p.channel.HandleFuncSync("routeUpdated", func(_ interface{}) (interface{}, error) { return nil, nil })
23 |
24 | // Currently ignored on platforms other than web
25 | p.channel.HandleFuncSync("selectSingleEntryHistory", func(_ interface{}) (interface{}, error) { return nil, nil })
26 | p.channel.HandleFuncSync("routeInformationUpdated", func(_ interface{}) (interface{}, error) { return nil, nil })
27 |
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/option.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/go-flutter-desktop/go-flutter/internal/execpath"
10 | )
11 |
12 | type config struct {
13 | flutterAssetsPath string
14 | icuDataPath string
15 | elfSnapshotpath string
16 | vmArguments []string
17 |
18 | windowIconProvider func() ([]image.Image, error)
19 | windowInitialDimensions windowDimensions
20 | windowInitialLocation windowLocation
21 | windowDimensionLimits windowDimensionLimits
22 | windowMode windowMode
23 | windowAlwaysOnTop bool
24 | windowTransparent bool
25 |
26 | backOnEscape bool
27 |
28 | forcePixelRatio float64
29 | scrollAmount float64
30 |
31 | plugins []Plugin
32 | }
33 |
34 | type windowDimensions struct {
35 | width int
36 | height int
37 | }
38 |
39 | type windowLocation struct {
40 | xpos int
41 | ypos int
42 | }
43 |
44 | type windowDimensionLimits struct {
45 | minWidth int
46 | minHeight int
47 | maxWidth int
48 | maxHeight int
49 | }
50 |
51 | // newApplicationConfig define the default configuration values for a new
52 | // Application. These values may be changed at any time.
53 | func newApplicationConfig() config {
54 | execPath, err := execpath.ExecPath()
55 | if err != nil {
56 | fmt.Printf("go-flutter: failed to resolve path for executable: %v", err)
57 | os.Exit(1)
58 | }
59 | return config{
60 | windowInitialDimensions: windowDimensions{
61 | width: 800,
62 | height: 600,
63 | },
64 | windowMode: WindowModeDefault,
65 | windowAlwaysOnTop: false,
66 | windowTransparent: false,
67 | scrollAmount: 100.0,
68 |
69 | backOnEscape: true,
70 |
71 | // Sane configuration values for the engine.
72 | flutterAssetsPath: filepath.Join(filepath.Dir(execPath), "flutter_assets"),
73 | icuDataPath: filepath.Join(filepath.Dir(execPath), "icudtl.dat"),
74 | // only required for AOT app.
75 | elfSnapshotpath: filepath.Join(filepath.Dir(execPath), "libapp.so"),
76 | }
77 | }
78 |
79 | // Option for Application
80 | type Option func(*config)
81 |
82 | // ProjectAssetsPath specify the flutter assets directory.
83 | func ProjectAssetsPath(p string) Option {
84 | _, err := os.Stat(p)
85 | if err != nil {
86 | fmt.Printf("go-flutter: failed to stat flutter assets path: %v\n", err)
87 | os.Exit(1)
88 | }
89 | return func(c *config) {
90 | c.flutterAssetsPath = p
91 | }
92 | }
93 |
94 | // ApplicationELFSnapshotPath specify the path to the ELF AOT snapshot.
95 | // only required by AOT.
96 | func ApplicationELFSnapshotPath(p string) Option {
97 | _, err := os.Stat(p)
98 | if err != nil {
99 | fmt.Printf("go-flutter: failed to stat ELF snapshot path: %v\n", err)
100 | os.Exit(1)
101 | }
102 | return func(c *config) {
103 | c.elfSnapshotpath = p
104 | }
105 | }
106 |
107 | // ApplicationICUDataPath specify the path to the ICUData.
108 | func ApplicationICUDataPath(p string) Option {
109 | _, err := os.Stat(p)
110 | if err != nil {
111 | fmt.Printf("go-flutter: failed to stat icu data path: %v\n", err)
112 | os.Exit(1)
113 | }
114 | return func(c *config) {
115 | c.icuDataPath = p
116 | }
117 | }
118 |
119 | // OptionVMArguments specify the arguments to the Dart VM.
120 | func OptionVMArguments(a []string) Option {
121 | return func(c *config) {
122 | // First should be argument is argv[0]
123 | c.vmArguments = append([]string{""}, a...)
124 | }
125 | }
126 |
127 | // WindowInitialDimensions specify the startup's dimension of the window.
128 | func WindowInitialDimensions(width, height int) Option {
129 | if width < 1 {
130 | fmt.Println("go-flutter: invalid initial value for width, must be 1 or greater.")
131 | os.Exit(1)
132 | }
133 | if height < 1 {
134 | fmt.Println("go-flutter: invalid initial value for height, must be 1 or greater.")
135 | os.Exit(1)
136 | }
137 |
138 | return func(c *config) {
139 | c.windowInitialDimensions.width = width
140 | c.windowInitialDimensions.height = height
141 | }
142 | }
143 |
144 | // WindowInitialLocation specify the startup's position of the window.
145 | // Location, in screen coordinates, of the upper-left corner of the client area
146 | // of the window.
147 | func WindowInitialLocation(xpos, ypos int) Option {
148 | if xpos < 1 {
149 | fmt.Println("go-flutter: invalid initial value for xpos location, must be 1 or greater.")
150 | os.Exit(1)
151 | }
152 | if ypos < 1 {
153 | fmt.Println("go-flutter: invalid initial value for ypos location, must be 1 or greater.")
154 | os.Exit(1)
155 | }
156 |
157 | return func(c *config) {
158 | c.windowInitialLocation.xpos = xpos
159 | c.windowInitialLocation.ypos = ypos
160 | }
161 | }
162 |
163 | // WindowDimensionLimits specify the dimension limits of the window.
164 | // Does not work when the window is fullscreen or not resizable.
165 | func WindowDimensionLimits(minWidth, minHeight, maxWidth, maxHeight int) Option {
166 | if minWidth < 1 {
167 | fmt.Println("go-flutter: invalid initial value for minWidth, must be 1 or greater.")
168 | os.Exit(1)
169 | }
170 | if minHeight < 1 {
171 | fmt.Println("go-flutter: invalid initial value for minHeight, must be 1 or greater.")
172 | os.Exit(1)
173 | }
174 | if maxWidth < minWidth {
175 | fmt.Println("go-flutter: invalid initial value for maxWidth, must be greater or equal to minWidth.")
176 | os.Exit(1)
177 | }
178 | if maxHeight < minHeight {
179 | fmt.Println("go-flutter: invalid initial value for maxHeight, must be greater or equal to minHeight.")
180 | os.Exit(1)
181 | }
182 |
183 | return func(c *config) {
184 | c.windowDimensionLimits.minWidth = minWidth
185 | c.windowDimensionLimits.minHeight = minHeight
186 | c.windowDimensionLimits.maxWidth = maxWidth
187 | c.windowDimensionLimits.maxHeight = maxHeight
188 | }
189 | }
190 |
191 | // BackOnEscape controls the mapping of the escape key.
192 | //
193 | // If true, pops the current route when escape is pressed.
194 | // If false, escape is delivered to the application.
195 | func BackOnEscape(backOnEscape bool) Option {
196 | return func(c *config) {
197 | c.backOnEscape = backOnEscape
198 | }
199 | }
200 |
201 | // WindowIcon sets an icon provider func, which is called during window
202 | // initialization. For tips on the kind of images to provide, see
203 | // https://godoc.org/github.com/go-gl/glfw/v3.3/glfw#Window.SetIcon
204 | func WindowIcon(iconProivder func() ([]image.Image, error)) Option {
205 | return func(c *config) {
206 | c.windowIconProvider = iconProivder
207 | }
208 | }
209 |
210 | // ForcePixelRatio forces the the scale factor for the screen. By default,
211 | // go-flutter will calculate the correct pixel ratio for the user, based on
212 | // their monitor DPI. Setting this option is not advised.
213 | func ForcePixelRatio(ratio float64) Option {
214 | return func(c *config) {
215 | c.forcePixelRatio = ratio
216 | }
217 | }
218 |
219 | // WindowTransparentBackground sets the init window background to be transparent
220 | func WindowTransparentBackground(enabled bool) Option {
221 | return func(c *config) {
222 | c.windowTransparent = enabled
223 | }
224 | }
225 |
226 | // WindowAlwaysOnTop sets the application window to be always on top of other windows
227 | func WindowAlwaysOnTop(enabled bool) Option {
228 | return func(c *config) {
229 | c.windowAlwaysOnTop = enabled
230 | }
231 | }
232 |
233 | // AddPlugin adds a plugin to the flutter application.
234 | func AddPlugin(p Plugin) Option {
235 | return func(c *config) {
236 | c.plugins = append(c.plugins, p)
237 | }
238 | }
239 |
240 | // VirtualKeyboardShow sets an func called when the flutter framework want to
241 | // show the keyboard.
242 | // This Option is interesting for people wanting to display the on-screen
243 | // keyboard on TextField focus.
244 | // It's up to the flutter developer to implement (or not) this function with
245 | // the OS related call.
246 | func VirtualKeyboardShow(showCallback func()) Option {
247 | return func(c *config) {
248 | // Reference the callback to the platform plugin (singleton) responsible
249 | // for textinput.
250 | defaultTextinputPlugin.virtualKeyboardShow = showCallback
251 | }
252 | }
253 |
254 | // VirtualKeyboardHide sets an func called when the flutter framework want to
255 | // hide the keyboard.
256 | func VirtualKeyboardHide(hideCallback func()) Option {
257 | return func(c *config) {
258 | // Reference the callback to the platform plugin (singleton) responsible
259 | // for textinput.
260 | defaultTextinputPlugin.virtualKeyboardHide = hideCallback
261 | }
262 | }
263 |
264 | // ScrollAmount sets the number of pixels to scroll with the mouse wheel
265 | func ScrollAmount(amount float64) Option {
266 | return func(c *config) {
267 | c.scrollAmount = amount
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/platform.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/go-gl/glfw/v3.3/glfw"
7 | "github.com/pkg/errors"
8 |
9 | "github.com/go-flutter-desktop/go-flutter/plugin"
10 | )
11 |
12 | // platformPlugin implements flutter.Plugin and handles method calls to the
13 | // flutter/platform channel.
14 | type platformPlugin struct {
15 | popBehavior popBehavior
16 |
17 | messenger plugin.BinaryMessenger
18 | window *glfw.Window
19 |
20 | // flutterInitialized is used as callbacks to know when the flutter framework
21 | // is running and ready to process upstream plugin calls.
22 | // (It usually takes ~10 rendering frame).
23 | // flutterInitialized is trigger when the plugin "flutter/platform" received
24 | // a message from "SystemChrome.setApplicationSwitcherDescription".
25 | flutterInitialized []func()
26 | }
27 |
28 | // hardcoded because there is no swappable renderer interface.
29 | var defaultPlatformPlugin = &platformPlugin{
30 | popBehavior: PopBehaviorNone,
31 | }
32 |
33 | var _ PluginGLFW = &platformPlugin{} // compile-time type check
34 |
35 | func (p *platformPlugin) InitPlugin(messenger plugin.BinaryMessenger) error {
36 | p.messenger = messenger
37 | channel := plugin.NewMethodChannel(p.messenger, "flutter/platform", plugin.JSONMethodCodec{})
38 |
39 | channel.HandleFuncSync("Clipboard.setData", p.handleClipboardSetData)
40 | channel.HandleFuncSync("Clipboard.getData", p.handleClipboardGetData)
41 | channel.HandleFuncSync("Clipboard.hasStrings", p.handleClipboardHasString)
42 |
43 | channel.HandleFuncSync("SystemNavigator.pop", p.handleSystemNavigatorPop)
44 | channel.HandleFunc("SystemChrome.setApplicationSwitcherDescription", p.handleWindowSetTitle)
45 |
46 | // Ignored: Desktop's don't have system overlays
47 | channel.HandleFuncSync("SystemChrome.setSystemUIOverlayStyle", func(_ interface{}) (interface{}, error) { return nil, nil })
48 | // Ignored: Desktop's don't have haptic feedback
49 | channel.HandleFuncSync("HapticFeedback.vibrate", func(_ interface{}) (interface{}, error) { return nil, nil })
50 | // Ignored: Desktop's don't play sound on every clicks
51 | channel.HandleFuncSync("SystemSound.play", func(_ interface{}) (interface{}, error) { return nil, nil })
52 |
53 | return nil
54 | }
55 |
56 | func (p *platformPlugin) InitPluginGLFW(window *glfw.Window) (err error) {
57 | p.window = window
58 | return nil
59 | }
60 |
61 | func (p *platformPlugin) handleClipboardSetData(arguments interface{}) (reply interface{}, err error) {
62 | newClipboard := struct {
63 | Text string `json:"text"`
64 | }{}
65 | err = json.Unmarshal(arguments.(json.RawMessage), &newClipboard)
66 | if err != nil {
67 | return nil, errors.Wrap(err, "failed to decode json arguments for handleClipboardSetData")
68 | }
69 | p.window.SetClipboardString(newClipboard.Text)
70 | return nil, nil
71 | }
72 |
73 | func (p *platformPlugin) handleClipboardGetData(arguments interface{}) (reply interface{}, err error) {
74 | requestedMime := ""
75 | err = json.Unmarshal(arguments.(json.RawMessage), &requestedMime)
76 | if err != nil {
77 | return nil, errors.Wrap(err, "failed to decode json arguments for handleClipboardGetData")
78 | }
79 | if requestedMime != "text/plain" {
80 | return nil, errors.New("obtaining mime type " + requestedMime + " from clipboard is not yet supported in go-flutter")
81 | }
82 |
83 | var clipText string
84 | clipText = p.window.GetClipboardString()
85 |
86 | reply = struct {
87 | Text string `json:"text"`
88 | }{
89 | Text: clipText,
90 | }
91 | return reply, nil
92 | }
93 |
94 | func (p *platformPlugin) handleClipboardHasString(arguments interface{}) (reply interface{}, err error) {
95 | var clipText string
96 | clipText = p.window.GetClipboardString()
97 |
98 | reply = struct {
99 | Value bool `json:"value"`
100 | }{
101 | Value: len(clipText) > 0,
102 | }
103 | return reply, nil
104 | }
105 |
106 | func (p *platformPlugin) handleWindowSetTitle(arguments interface{}) (reply interface{}, err error) {
107 | // triggers flutter framework initialized callbacks
108 | for _, f := range p.flutterInitialized {
109 | f()
110 | }
111 |
112 | return nil, nil
113 | }
114 |
115 | // addFrameworkReadyCallback adds a callback which if trigger when the flutter
116 | // framework is ready.
117 | func (p *platformPlugin) addFrameworkReadyCallback(f func()) {
118 | p.flutterInitialized = append(p.flutterInitialized, f)
119 | }
120 |
--------------------------------------------------------------------------------
/plugin.go:
--------------------------------------------------------------------------------
1 | package flutter
2 |
3 | import (
4 | "github.com/go-gl/glfw/v3.3/glfw"
5 |
6 | "github.com/go-flutter-desktop/go-flutter/plugin"
7 | )
8 |
9 | // TODO: move type Plugin into package plugin?
10 |
11 | // Plugin defines the interface that each plugin must implement.
12 | // When InitPlugin is called, the plugin may execute setup operations.
13 | // The BinaryMessenger is passed to allow the plugin to register channels.
14 | // A plugin may optionally implement PluginGLFW.
15 | type Plugin interface {
16 | // InitPlugin is called during the startup of the flutter application. The
17 | // plugin is responsible for setting up channels using the BinaryMessenger.
18 | // If an error is returned it is printend the application is stopped.
19 | InitPlugin(messenger plugin.BinaryMessenger) error
20 | }
21 |
22 | // PluginGLFW defines the interface for plugins that are GLFW-aware. Plugins may
23 | // implement this interface to receive access to the *glfw.Window. Note that
24 | // plugins must still implement the Plugin interface. The call to InitPluginGLFW
25 | // is made after the call to InitPlugin.
26 | //
27 | // PluginGLFW is separated because not all plugins need to know about glfw,
28 | // Adding glfw.Window to the InitPlugin call would add glfw as dependency to
29 | // every plugin implementation. Also, this helps in a scenarion where glfw is
30 | // moved into a separate renderer/glfw package.
31 | //
32 | // The PluginGLFW interface is not stable and may change at any time.
33 | type PluginGLFW interface {
34 | // Any type inmplementing PluginGLFW must also implement Plugin.
35 | Plugin
36 | // InitPluginGLFW is called after the call to InitPlugin. When an error is
37 | // returned it is printend the application is stopped.
38 | InitPluginGLFW(window *glfw.Window) error
39 | }
40 |
41 | // PluginTexture defines the interface for plugins that needs to create and
42 | // manage backend textures. Plugins may implement this interface to receive
43 | // access to the TextureRegistry. Note that plugins must still implement the
44 | // Plugin interface. The call to PluginTexture is made after the call to
45 | // PluginGLFW.
46 | //
47 | // PluginTexture is separated because not all plugins need to send raw pixel to
48 | // the Flutter scene.
49 | type PluginTexture interface {
50 | // Any type inmplementing PluginTexture must also implement Plugin.
51 | Plugin
52 | // InitPluginTexture is called after the call to InitPlugin. When an error is
53 | // returned it is printend the application is stopped.
54 | InitPluginTexture(registry *TextureRegistry) error
55 | }
56 |
--------------------------------------------------------------------------------
/plugin/basic-message-channel.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import "github.com/pkg/errors"
4 |
5 | // BasicMessageHandler defines the interfece for a basic message handler.
6 | type BasicMessageHandler interface {
7 | HandleMessage(message interface{}) (reply interface{}, err error)
8 | }
9 |
10 | // The BasicMessageHandlerFunc type is an adapter to allow the use of
11 | // ordinary functions as basic message handlers. If f is a function
12 | // with the appropriate signature, BasicMessageHandlerFunc(f) is a
13 | // BasicMessageHandler that calls f.
14 | type BasicMessageHandlerFunc func(message interface{}) (reply interface{}, err error)
15 |
16 | // HandleMessage calls f(message).
17 | func (f BasicMessageHandlerFunc) HandleMessage(message interface{}) (reply interface{}, err error) {
18 | return f(message)
19 | }
20 |
21 | // BasicMessageChannel presents named channel for communicating with the Flutter
22 | // application using basic, asynchronous message passing.
23 | //
24 | // Messages are encoded into binary before being sent, and binary messages
25 | /// received are decoded into. The MessageCodec used must be compatible with the
26 | // one used by the Flutter application. This can be achieved by creating a
27 | // BasicMessageChannel counterpart of this channel on the Dart side.
28 | // See: https://docs.flutter.io/flutter/services/BasicMessageChannel-class.html
29 | //
30 | // The static Go type of messages sent and received is interface{}, but only
31 | // values supported by the specified MessageCodec can be used.
32 | //
33 | // The logical identity of the channel is given by its name. Identically named
34 | // channels will interfere with each other's communication.
35 | type BasicMessageChannel struct {
36 | messenger BinaryMessenger
37 | channelName string
38 | codec MessageCodec
39 | handler BasicMessageHandler
40 | }
41 |
42 | // NewBasicMessageChannel creates a BasicMessageChannel.
43 | //
44 | // Call Handle or HandleFunc on the returned BasicMessageChannel to provide the
45 | // channel with a handler for incoming messages.
46 | func NewBasicMessageChannel(messenger BinaryMessenger, channelName string, codec MessageCodec) *BasicMessageChannel {
47 | b := &BasicMessageChannel{
48 | messenger: messenger,
49 | channelName: channelName,
50 | codec: codec,
51 | }
52 | messenger.SetChannelHandler(b.channelName, b.handleChannelMessage)
53 | return b
54 | }
55 |
56 | // Send encodes and sends the specified message to the Flutter application
57 | // without waiting for a reply.
58 | func (b *BasicMessageChannel) Send(message interface{}) error {
59 | encodedMessage, err := b.codec.EncodeMessage(message)
60 | if err != nil {
61 | return errors.Wrap(err, "failed to encode outgoing message")
62 | }
63 | err = b.messenger.Send(b.channelName, encodedMessage)
64 | if err != nil {
65 | return errors.Wrap(err, "failed to send outgoing message")
66 | }
67 | return nil
68 | }
69 |
70 | // SendWithReply encodes and sends the specified message to the Flutter
71 | // application and returns the reply, or an error.
72 | //
73 | // NOTE: If no value are returned by the handler setted in the
74 | // setMessageHandler flutter method, the function will wait forever. In case
75 | // you don't want to wait for reply, use Send or launch the
76 | // function in a goroutine.
77 | func (b *BasicMessageChannel) SendWithReply(message interface{}) (reply interface{}, err error) {
78 | encodedMessage, err := b.codec.EncodeMessage(message)
79 | if err != nil {
80 | return nil, errors.Wrap(err, "failed to encode outgoing message")
81 | }
82 | encodedReply, err := b.messenger.SendWithReply(b.channelName, encodedMessage)
83 | if err != nil {
84 | return nil, errors.Wrap(err, "failed to send outgoing message")
85 | }
86 | reply, err = b.codec.DecodeMessage(encodedReply)
87 | if err != nil {
88 | return nil, errors.Wrap(err, "failed to decode incoming reply")
89 | }
90 | return reply, nil
91 | }
92 |
93 | // Handle registers a message handler on this channel for receiving messages
94 | // sent from the Flutter application.
95 | //
96 | // Consecutive calls override any existing handler registration for (the name
97 | // of) this channel.
98 | //
99 | // When given nil as handler, any incoming message on this channel will be
100 | // handled silently by sending a nil reply which triggers the dart
101 | // MissingPluginException exception.
102 | func (b *BasicMessageChannel) Handle(handler BasicMessageHandler) {
103 | b.handler = handler
104 | }
105 |
106 | // HandleFunc is a shorthand for b.Handle(BasicMessageHandlerFunc(f))
107 | func (b *BasicMessageChannel) HandleFunc(f func(message interface{}) (reply interface{}, err error)) {
108 | if f == nil {
109 | b.Handle(nil)
110 | return
111 | }
112 |
113 | b.Handle(BasicMessageHandlerFunc(f))
114 | }
115 |
116 | // handleChannelMessage decodes an incoming binary envelopes, calls the bassic
117 | // message handler, and encodes the outgoing reply into an envelope.
118 | func (b *BasicMessageChannel) handleChannelMessage(binaryMessage []byte, r ResponseSender) (err error) {
119 | if b.handler == nil {
120 | return nil
121 | }
122 | message, err := b.codec.DecodeMessage(binaryMessage)
123 | if err != nil {
124 | return errors.Wrap(err, "failed to decode incoming message")
125 | }
126 | reply, err := b.handler.HandleMessage(message)
127 | if err != nil {
128 | return errors.Wrap(err, "handler for incoming basic message failed")
129 | }
130 | binaryReply, err := b.codec.EncodeMessage(reply)
131 | if err != nil {
132 | return errors.Wrap(err, "failed to encode outgoing reply")
133 | }
134 | r.Send(binaryReply)
135 | return nil
136 | }
137 |
--------------------------------------------------------------------------------
/plugin/basic-message-channel_test.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/davecgh/go-spew/spew"
7 | "github.com/pkg/errors"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | // TestBasicMethodChannelStringCodecSend tests the sending a messagen and
12 | // receiving a reply, using a basic message channel with the string codec.
13 | func TestBasicMethodChannelStringCodecSend(t *testing.T) {
14 | codec := StringCodec{}
15 | messenger := NewTestingBinaryMessenger()
16 | messenger.MockSetChannelHandler("ch", func(encodedMessage []byte, r ResponseSender) error {
17 | message, err := codec.DecodeMessage(encodedMessage)
18 | if err != nil {
19 | return errors.Wrap(err, "failed to decode message")
20 | }
21 | messageString, ok := message.(string)
22 | if !ok {
23 | return errors.New("message is invalid type, expected string")
24 | }
25 | reply := messageString + " world"
26 | encodedReply, err := codec.EncodeMessage(reply)
27 | if err != nil {
28 | return errors.Wrap(err, "failed to encode message")
29 | }
30 | r.Send(encodedReply)
31 | return nil
32 | })
33 | channel := NewBasicMessageChannel(messenger, "ch", codec)
34 | reply, err := channel.SendWithReply("hello")
35 | if err != nil {
36 | t.Fatal(err)
37 | }
38 | t.Log(spew.Sdump(reply))
39 | replyString, ok := reply.(string)
40 | if !ok {
41 | t.Fatal("reply is invalid type, expected string")
42 | }
43 | assert.Equal(t, "hello world", replyString)
44 | }
45 |
46 | // TestBasicMethodChannelStringCodecHandle tests the handling a messagen and
47 | // sending a reply, using a basic message channel with the string codec.
48 | func TestBasicMethodChannelStringCodecHandle(t *testing.T) {
49 | codec := StringCodec{}
50 | messenger := NewTestingBinaryMessenger()
51 | channel := NewBasicMessageChannel(messenger, "ch", codec)
52 | channel.HandleFunc(func(message interface{}) (reply interface{}, err error) {
53 | messageString, ok := message.(string)
54 | if !ok {
55 | return nil, errors.New("message is invalid type, expected string")
56 | }
57 | reply = messageString + " world"
58 | return reply, nil
59 | })
60 | encodedMessage, err := codec.EncodeMessage("hello")
61 | if err != nil {
62 | t.Fatalf("failed to encode message: %v", err)
63 | }
64 | encodedReply, err := messenger.MockSend("ch", encodedMessage)
65 | if err != nil {
66 | t.Fatal(err)
67 | }
68 | reply, err := codec.DecodeMessage(encodedReply)
69 | if err != nil {
70 | t.Fatalf("failed to decode reply: %v", err)
71 | }
72 | t.Log(spew.Sdump(reply))
73 | replyString, ok := reply.(string)
74 | if !ok {
75 | t.Fatal("reply is invalid type, expected string")
76 | }
77 | assert.Equal(t, "hello world", replyString)
78 | }
79 |
80 | // TestBasicMethodChannelBinaryCodecSend tests the sending a messagen and
81 | // receiving a reply, using a basic message channel with the binary codec.
82 | func TestBasicMethodChannelBinaryCodecSend(t *testing.T) {
83 | codec := BinaryCodec{}
84 | messenger := NewTestingBinaryMessenger()
85 | messenger.MockSetChannelHandler("ch", func(encodedMessage []byte, r ResponseSender) error {
86 | message, err := codec.DecodeMessage(encodedMessage)
87 | if err != nil {
88 | return errors.Wrap(err, "failed to decode message")
89 | }
90 | messageBytes, ok := message.([]byte)
91 | if !ok {
92 | return errors.New("message is invalid type, expected []byte")
93 | }
94 | reply := append(messageBytes, 0x02)
95 | encodedReply, err := codec.EncodeMessage(reply)
96 | if err != nil {
97 | return errors.Wrap(err, "failed to encode message")
98 | }
99 | r.Send(encodedReply)
100 | return nil
101 | })
102 | channel := NewBasicMessageChannel(messenger, "ch", codec)
103 | reply, err := channel.SendWithReply([]byte{0x01})
104 | if err != nil {
105 | t.Fatal(err)
106 | }
107 | t.Log(spew.Sdump(reply))
108 | replyString, ok := reply.([]byte)
109 | if !ok {
110 | t.Fatal("reply is invalid type, expected []byte")
111 | }
112 | assert.Equal(t, []byte{0x01, 0x02}, replyString)
113 | }
114 |
115 | // TestBasicMethodChannelBinaryCodecHandle tests the handling a messagen and
116 | // sending a reply, using a basic message channel with the binary codec.
117 | func TestBasicMethodChannelBinaryCodecHandle(t *testing.T) {
118 | codec := BinaryCodec{}
119 | messenger := NewTestingBinaryMessenger()
120 | channel := NewBasicMessageChannel(messenger, "ch", codec)
121 | channel.HandleFunc(func(message interface{}) (reply interface{}, err error) {
122 | messageBytes, ok := message.([]byte)
123 | if !ok {
124 | return nil, errors.New("message is invalid type, expected []byte")
125 | }
126 | reply = append(messageBytes, 0x02)
127 | return reply, nil
128 | })
129 | encodedMessage, err := codec.EncodeMessage([]byte{0x01})
130 | if err != nil {
131 | t.Fatalf("failed to encode message: %v", err)
132 | }
133 | encodedReply, err := messenger.MockSend("ch", encodedMessage)
134 | if err != nil {
135 | t.Fatal(err)
136 | }
137 | reply, err := codec.DecodeMessage(encodedReply)
138 | if err != nil {
139 | t.Fatalf("failed to decode reply: %v", err)
140 | }
141 | t.Log(spew.Sdump(reply))
142 | replyString, ok := reply.([]byte)
143 | if !ok {
144 | t.Fatal("reply is invalid type, expected []byte")
145 | }
146 | assert.Equal(t, []byte{0x01, 0x02}, replyString)
147 | }
148 |
149 | func TestBasicMethodChannelNilHandler(t *testing.T) {
150 | codec := StringCodec{}
151 | messenger := NewTestingBinaryMessenger()
152 | channel := NewBasicMessageChannel(messenger, "ch", codec)
153 | channel.HandleFunc(nil)
154 | reply, err := messenger.MockSend("ch", []byte("abcd"))
155 | assert.Nil(t, reply)
156 | assert.Nil(t, err)
157 | }
158 | func TestBasicMethodChannelNilMockHandler(t *testing.T) {
159 | codec := StringCodec{}
160 | messenger := NewTestingBinaryMessenger()
161 | messenger.MockSetChannelHandler("ch", nil)
162 | channel := NewBasicMessageChannel(messenger, "ch", codec)
163 | reply, err := channel.SendWithReply("hello")
164 | assert.Nil(t, reply)
165 | assert.NotNil(t, err)
166 | assert.Equal(t, "failed to send outgoing message: no handler set", err.Error())
167 | }
168 |
169 | func TestBasicMethodChannelEncodeFail(t *testing.T) {
170 | codec := StringCodec{}
171 | messenger := NewTestingBinaryMessenger()
172 | channel := NewBasicMessageChannel(messenger, "ch", codec)
173 | reply, err := channel.SendWithReply(int(42)) // invalid value
174 | assert.Nil(t, reply)
175 | assert.NotNil(t, err)
176 | assert.Equal(t, "failed to encode outgoing message: invalid type provided to message codec: expected message to be of type string", err.Error())
177 | }
178 |
--------------------------------------------------------------------------------
/plugin/binary-codec.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | // BinaryCodec implements a MessageCodec using unencoded binary messages, represented as byte slices.
4 | type BinaryCodec struct{}
5 |
6 | // Compiler test to assert that BinaryCodec implements MessageCodec
7 | var _ MessageCodec = &BinaryCodec{}
8 |
9 | // EncodeMessage expects message to be a slice of bytes.
10 | func (BinaryCodec) EncodeMessage(message interface{}) ([]byte, error) {
11 | if message == nil {
12 | return nil, nil
13 | }
14 |
15 | b, ok := message.([]byte)
16 | if !ok {
17 | return nil, MessageTypeError{"expected message to be of type []byte"}
18 | }
19 | return b, nil
20 | }
21 |
22 | // DecodeMessage decodes binary data into binary data.
23 | func (BinaryCodec) DecodeMessage(data []byte) (message interface{}, err error) {
24 | if data == nil {
25 | return nil, nil
26 | }
27 |
28 | return data, nil
29 | }
30 |
--------------------------------------------------------------------------------
/plugin/binary-codec_test.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestBinaryEncodeDecode(t *testing.T) {
10 | values := []interface{}{
11 | nil,
12 | []byte{},
13 | []byte{0, 0, 0, 0},
14 | []byte{1, 2, 3, 4},
15 | }
16 |
17 | codec := BinaryCodec{}
18 |
19 | for _, v := range values {
20 | data, err := codec.EncodeMessage(v)
21 | if err != nil {
22 | t.Fatal(err)
23 | }
24 | v2, err := codec.DecodeMessage(data)
25 | if err != nil {
26 | t.Fatal(err)
27 | }
28 | assert.Equal(t, v, v2)
29 | }
30 | }
31 | func TestBinaryEncodeFail(t *testing.T) {
32 | codec := BinaryCodec{}
33 |
34 | _, err := codec.EncodeMessage("invalid value")
35 | assert.NotNil(t, err)
36 | assert.Equal(t, "invalid type provided to message codec: expected message to be of type []byte", err.Error())
37 | }
38 |
--------------------------------------------------------------------------------
/plugin/binary-messenger.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | // BinaryMessenger defines a bidirectional binary messenger.
4 | type BinaryMessenger interface {
5 | // SendWithReply sends a binary message to the Flutter application.
6 | SendWithReply(channel string, binaryMessage []byte) (binaryReply []byte, err error)
7 |
8 | // Send sends a binary message to the Flutter application without
9 | // expecting a reply.
10 | Send(channel string, binaryMessage []byte) (err error)
11 |
12 | // SetChannelHandler registers a handler to be invoked when the Flutter
13 | // application sends a message to its host platform on given channel.
14 | //
15 | // Registration overwrites any previous registration for the same channel
16 | // name. Use nil as handler to deregister.
17 | SetChannelHandler(channel string, handler ChannelHandlerFunc)
18 | }
19 |
20 | // ResponseSender defines the interface that must be implemented by a messenger
21 | // to handle replies on on message. It is an error to call Send multiple times
22 | // on the same ResponseSender.
23 | type ResponseSender interface {
24 | // Send may return before the message was passed to the flutter side.
25 | Send(binaryReply []byte)
26 | }
27 |
28 | // ChannelHandlerFunc describes the function that handles binary messages sent
29 | // on a channel. For each message, ResponseSender.Send must be called once.
30 | type ChannelHandlerFunc func(binaryMessage []byte, r ResponseSender) (err error)
31 |
--------------------------------------------------------------------------------
/plugin/codec.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | // MessageCodec defines a message encoding/decoding mechanism.
4 | type MessageCodec interface {
5 | // EncodeMessage encodes a message to a slice of bytes.
6 | EncodeMessage(message interface{}) (binaryMessage []byte, err error)
7 | // DecodeMessage decodes a slice of bytes to a message.
8 | DecodeMessage(binaryMessage []byte) (message interface{}, err error)
9 | }
10 |
11 | // MethodCall describes a method invocation.
12 | type MethodCall struct {
13 | Method string
14 | Arguments interface{}
15 | }
16 |
17 | // MethodCodec describes a codec for method calls and enveloped results.
18 | type MethodCodec interface {
19 | // EncodeMethodCall encodes the MethodCall into binary
20 | // Returns an error on invalid MethodCall arguments.
21 | EncodeMethodCall(methodCall MethodCall) (data []byte, err error)
22 |
23 | // DecodeMethodCal decodes the MethodCall from binary.
24 | // Returns an error on invalid data.
25 | DecodeMethodCall(data []byte) (methodCall MethodCall, err error)
26 |
27 | /// Encodes a successful [result] into a binary envelope.
28 | EncodeSuccessEnvelope(result interface{}) (data []byte, err error)
29 |
30 | // EncodeErrorEnvelope encodes an error result into a binary envelope.
31 | // The specified error code, human-readable error message, and error
32 | // details correspond to the fields of Flutter's PlatformException.
33 | EncodeErrorEnvelope(code string, message string, details interface{}) (data []byte, err error)
34 |
35 | // DecodeEnvelope decodes the specified result [envelope] from binary.
36 | // Returns a FlutterError as error if provided envelope represents an error,
37 | // otherwise returns the enveloped result.
38 | DecodeEnvelope(envelope []byte) (result interface{}, err error)
39 | }
40 |
--------------------------------------------------------------------------------
/plugin/doc.go:
--------------------------------------------------------------------------------
1 | // Package plugin contains message codecs, method codecs and channel
2 | // implementations which allow plugins to communicate between the flutter
3 | // framework and the host (Go).
4 | //
5 | // go-flutter/plugin is frozen, the API should not much change.
6 | package plugin
7 |
--------------------------------------------------------------------------------
/plugin/endian.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "encoding/binary"
5 | "unsafe"
6 | )
7 |
8 | var endian binary.ByteOrder
9 |
10 | func init() {
11 | // find out endiannes of the host
12 | const intSize int = int(unsafe.Sizeof(0))
13 | var i = 0x1
14 | bs := (*[intSize]byte)(unsafe.Pointer(&i))
15 | if bs[0] == 0 {
16 | endian = binary.BigEndian
17 | } else {
18 | endian = binary.LittleEndian
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/plugin/error.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import "fmt"
4 |
5 | // FlutterError is returned to indicate that a Flutter method invocation
6 | // failed on the Flutter side.
7 | type FlutterError struct {
8 | Code string
9 | Message string
10 | Details interface{}
11 | }
12 |
13 | // Error returns a string describing the FlutterError
14 | func (e FlutterError) Error() string {
15 | return fmt.Sprintf("Error %s in Flutter: %s (%v)", e.Code, e.Message, e.Details)
16 | }
17 |
18 | // MessageTypeError is returned when a MessageCodec implementation is asked to
19 | // encode a message of the wrong type.
20 | type MessageTypeError struct {
21 | hint string
22 | }
23 |
24 | // Error returns a string describing the MessageTypeError
25 | func (e MessageTypeError) Error() string {
26 | return fmt.Sprintf("invalid type provided to message codec: %s", e.hint)
27 | }
28 |
--------------------------------------------------------------------------------
/plugin/error_test.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestMessageTypeError(t *testing.T) {
10 | m := MessageTypeError{
11 | hint: "unexpected type uintptr",
12 | }
13 | assert.Equal(t, "invalid type provided to message codec: unexpected type uintptr", m.Error())
14 | }
15 |
16 | func TestFlutterError(t *testing.T) {
17 | f := FlutterError{
18 | Code: "error",
19 | Message: "This is totally wrong",
20 | Details: []interface{}{
21 | "foo",
22 | 42,
23 | "bar",
24 | },
25 | }
26 | assert.Equal(t, "Error error in Flutter: This is totally wrong ([foo 42 bar])", f.Error())
27 | }
28 |
--------------------------------------------------------------------------------
/plugin/event-channel.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "fmt"
5 | "runtime/debug"
6 |
7 | "github.com/pkg/errors"
8 | )
9 |
10 | // EventChannel provides way for flutter applications and hosts to communicate
11 | // using event streams.
12 | // It must be used with a codec, for example the StandardMethodCodec.
13 | type EventChannel struct {
14 | messenger BinaryMessenger
15 | channelName string
16 | methodCodec MethodCodec
17 |
18 | handler StreamHandler
19 | activeSink *EventSink
20 | }
21 |
22 | // NewEventChannel creates a new event channel
23 | func NewEventChannel(messenger BinaryMessenger, channelName string, methodCodec MethodCodec) (channel *EventChannel) {
24 | ec := &EventChannel{
25 | messenger: messenger,
26 | channelName: channelName,
27 | methodCodec: methodCodec,
28 | }
29 | messenger.SetChannelHandler(channelName, ec.handleChannelMessage)
30 | return ec
31 | }
32 |
33 | // Handle registers a StreamHandler for a event channel.
34 | //
35 | // Consecutive calls override any existing handler registration.
36 | // When given nil as handler, the previously registered
37 | // handler for a method is unregistrered.
38 | //
39 | // When no handler is registered for a method, it will be handled silently by
40 | // sending a nil reply which triggers the dart MissingPluginException exception.
41 | func (e *EventChannel) Handle(handler StreamHandler) {
42 | e.handler = handler
43 | }
44 |
45 | // handleChannelMessage decodes incoming binary message to a method call, calls the
46 | // handler, and encodes the outgoing reply.
47 | func (e *EventChannel) handleChannelMessage(binaryMessage []byte, responseSender ResponseSender) (err error) {
48 | methodCall, err := e.methodCodec.DecodeMethodCall(binaryMessage)
49 | if err != nil {
50 | return errors.Wrap(err, "failed to decode incoming message")
51 | }
52 |
53 | if e.handler == nil {
54 | fmt.Printf("go-flutter: no method handler registered for event channel '%s'\n", e.channelName)
55 | responseSender.Send(nil)
56 | return nil
57 | }
58 |
59 | defer func() {
60 | p := recover()
61 | if p != nil {
62 | fmt.Printf("go-flutter: recovered from panic while handling message for event channel '%s': %v\n", e.channelName, p)
63 | debug.PrintStack()
64 | }
65 | }()
66 |
67 | switch methodCall.Method {
68 | case "listen":
69 |
70 | binaryReply, err := e.methodCodec.EncodeSuccessEnvelope(nil)
71 | if err != nil {
72 | fmt.Printf("go-flutter: failed to encode listen envelope for event channel '%s', error: %v\n", e.channelName, err)
73 | }
74 | responseSender.Send(binaryReply)
75 |
76 | if e.activeSink != nil {
77 | // Repeated calls to onListen may happen during hot restart.
78 | // We separate them with a call to onCancel.
79 | e.handler.OnCancel(nil)
80 | }
81 |
82 | e.activeSink = &EventSink{eventChannel: e}
83 | go e.handler.OnListen(methodCall.Arguments, e.activeSink)
84 |
85 | case "cancel":
86 | if e.activeSink != nil {
87 | e.activeSink = nil
88 | go e.handler.OnCancel(methodCall.Arguments)
89 |
90 | binaryReply, _ := e.methodCodec.EncodeSuccessEnvelope(nil)
91 | responseSender.Send(binaryReply)
92 | } else {
93 | fmt.Printf("go-flutter: No active stream to cancel onEventChannel '%s'\n", e.channelName)
94 | binaryReply, _ := e.methodCodec.EncodeErrorEnvelope("error", "No active stream to cancel", nil)
95 | responseSender.Send(binaryReply)
96 | }
97 |
98 | default:
99 | fmt.Printf("go-flutter: no StreamHandler handler registered for method '%s' on EventChannel '%s'\n", methodCall.Method, e.channelName)
100 | responseSender.Send(nil) // MissingPluginException
101 | }
102 |
103 | return nil
104 | }
105 |
--------------------------------------------------------------------------------
/plugin/event-sink.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | )
7 |
8 | // StreamHandler defines the interface for a stream handler setup and tear-down
9 | // requests.
10 | type StreamHandler interface {
11 | // OnListen handles a request to set up an event stream.
12 | OnListen(arguments interface{}, sink *EventSink)
13 | // OnCancel handles a request to tear down the most recently created event
14 | // stream.
15 | OnCancel(arguments interface{})
16 | }
17 |
18 | // EventSink defines the interface for producers of events to send message to
19 | // Flutter. StreamHandler act as a clients of EventSink for sending events.
20 | type EventSink struct {
21 | eventChannel *EventChannel
22 |
23 | hasEnded bool
24 | sync.Mutex
25 | }
26 |
27 | // Success consumes a successful event.
28 | func (es *EventSink) Success(event interface{}) {
29 | es.Lock()
30 | defer es.Unlock()
31 | if es.hasEnded || es != es.eventChannel.activeSink {
32 | return
33 | }
34 |
35 | binaryMsg, err := es.eventChannel.methodCodec.EncodeSuccessEnvelope(event)
36 | if err != nil {
37 | fmt.Printf("go-flutter: failed to encode success envelope for event channel '%s', error: %v\n", es.eventChannel.channelName, err)
38 | }
39 | err = es.eventChannel.messenger.Send(es.eventChannel.channelName, binaryMsg)
40 | if err != nil {
41 | fmt.Printf("go-flutter: failed to send Success message on event channel '%s', error: %v\n", es.eventChannel.channelName, err)
42 | }
43 | }
44 |
45 | // Error consumes an error event.
46 | func (es *EventSink) Error(errorCode string, errorMessage string, errorDetails interface{}) {
47 | es.Lock()
48 | defer es.Unlock()
49 | if es.hasEnded || es != es.eventChannel.activeSink {
50 | return
51 | }
52 |
53 | binaryMsg, err := es.eventChannel.methodCodec.EncodeErrorEnvelope(errorCode, errorMessage, errorDetails)
54 | if err != nil {
55 | fmt.Printf("go-flutter: failed to encode success envelope for event channel '%s', error: %v\n", es.eventChannel.channelName, err)
56 | }
57 | err = es.eventChannel.messenger.Send(es.eventChannel.channelName, binaryMsg)
58 | if err != nil {
59 | fmt.Printf("go-flutter: failed to send Error message on event channel '%s', error: %v\n", es.eventChannel.channelName, err)
60 | }
61 | }
62 |
63 | // EndOfStream consumes end of stream.
64 | func (es *EventSink) EndOfStream() {
65 | es.Lock()
66 | defer es.Unlock()
67 | if es.hasEnded || es != es.eventChannel.activeSink {
68 | return
69 | }
70 | es.hasEnded = true
71 |
72 | err := es.eventChannel.messenger.Send(es.eventChannel.channelName, nil)
73 | if err != nil {
74 | fmt.Printf("go-flutter: failed to send EndOfStream message on event channel '%s', error: %v\n", es.eventChannel.channelName, err)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/plugin/helper_test.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "errors"
5 | "sync"
6 | )
7 |
8 | // TestingBinaryMessenger implements the BinaryMessenger interface for testing
9 | // purposes. It can be used as a backend in tests for BasicMessageChannel and
10 | // StandardMethodChannel.
11 | // TODO: perhaps this can be exported (non-test) into a subpackage `plugintest`.
12 | // The TestingBinaryMessenger may then be used to test other plugins.
13 | // s/TestingBinaryMessenger/MockBinaryMessenger/ ?
14 | type TestingBinaryMessenger struct {
15 | channelHandlersLock sync.Mutex
16 | channelHandlers map[string]ChannelHandlerFunc
17 |
18 | // handlers mocking the other side of the BinaryMessenger
19 | mockChannelHandlersLock sync.Mutex
20 | mockChannelHandlers map[string]ChannelHandlerFunc
21 | }
22 |
23 | func NewTestingBinaryMessenger() *TestingBinaryMessenger {
24 | return &TestingBinaryMessenger{
25 | channelHandlers: make(map[string]ChannelHandlerFunc),
26 | mockChannelHandlers: make(map[string]ChannelHandlerFunc),
27 | }
28 | }
29 |
30 | func (t *TestingBinaryMessenger) Send(channel string, message []byte) (err error) {
31 | err = t.Send(channel, message)
32 | return err
33 | }
34 |
35 | // Send sends the bytes onto the given channel.
36 | // In this testing implementation of a BinaryMessenger, the handler for the
37 | // channel may be set using MockSetMessageHandler
38 | func (t *TestingBinaryMessenger) SendWithReply(channel string, message []byte) (reply []byte, err error) {
39 | t.mockChannelHandlersLock.Lock()
40 | handler := t.mockChannelHandlers[channel]
41 | t.mockChannelHandlersLock.Unlock()
42 | if handler == nil {
43 | return nil, errors.New("no handler set")
44 | }
45 |
46 | r := mockResponseSender{}
47 | handler(message, &r)
48 | return r.binaryReply, nil
49 | }
50 |
51 | // SetMessageHandler registers a binary message handler on given channel.
52 | // In this testing implementation of a BinaryMessenger, the handler may be
53 | // executed by calling MockSend(..).
54 | func (t *TestingBinaryMessenger) SetChannelHandler(channel string, handler ChannelHandlerFunc) {
55 | t.channelHandlersLock.Lock()
56 | t.channelHandlers[channel] = handler
57 | t.channelHandlersLock.Unlock()
58 | }
59 |
60 | // MockSend imitates a send method call from the other end of the binary
61 | // messenger. It calls a method that was registered through SetMessageHandler.
62 | func (t *TestingBinaryMessenger) MockSend(channel string, message []byte) (reply []byte, err error) {
63 | t.channelHandlersLock.Lock()
64 | handler := t.channelHandlers[channel]
65 | t.channelHandlersLock.Unlock()
66 | if handler == nil {
67 | return nil, errors.New("no handler set")
68 | }
69 |
70 | r := mockResponseSender{}
71 | handler(message, &r)
72 | return r.binaryReply, nil
73 | }
74 |
75 | // MockSetChannelHandler imitates a handler set at the other end of the
76 | // binary messenger.
77 | func (t *TestingBinaryMessenger) MockSetChannelHandler(channel string, handler ChannelHandlerFunc) {
78 | t.mockChannelHandlersLock.Lock()
79 | t.mockChannelHandlers[channel] = handler
80 | t.mockChannelHandlersLock.Unlock()
81 | }
82 |
83 | type mockResponseSender struct {
84 | binaryReply []byte
85 | }
86 |
87 | func (m *mockResponseSender) Send(binaryReply []byte) {
88 | m.binaryReply = binaryReply
89 | }
90 |
--------------------------------------------------------------------------------
/plugin/json-method-codec.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/pkg/errors"
7 | )
8 |
9 | // JSONMethodCodec implements a MethodCodec using JSON for message encoding.
10 | type JSONMethodCodec struct{}
11 |
12 | var _ MethodCodec = JSONMethodCodec{}
13 |
14 | // EncodeMethodCall encodes the MethodCall into binary Returns an error on
15 | // invalid MethodCall arguments.
16 | func (j JSONMethodCodec) EncodeMethodCall(methodCall MethodCall) (data []byte, err error) {
17 | jmc := struct {
18 | Method string `json:"method"`
19 | Args interface{} `json:"args"`
20 | }{
21 | Method: methodCall.Method,
22 | Args: methodCall.Arguments,
23 | }
24 | return json.Marshal(&jmc)
25 | }
26 |
27 | // DecodeMethodCall decodes the MethodCall from binary. Nore that the MethodCall
28 | // arguments are not fully parsed, they are always a json.RawMessage and must be
29 | // decoded by the MethodHandler. Returns an error on invalid data.
30 | func (j JSONMethodCodec) DecodeMethodCall(data []byte) (methodCall MethodCall, err error) {
31 | jmc := struct {
32 | Method string `json:"method"`
33 | Args json.RawMessage `json:"args"`
34 | }{}
35 | err = json.Unmarshal(data, &jmc)
36 | if err != nil {
37 | return MethodCall{}, errors.Wrap(err, "failed to decode json method call")
38 | }
39 | mc := MethodCall{
40 | Method: jmc.Method,
41 | Arguments: jmc.Args,
42 | }
43 | return mc, nil
44 | }
45 |
46 | // EncodeSuccessEnvelope encodes a successful result into a binary envelope. The
47 | // result value must be encodable in JSON.
48 | func (j JSONMethodCodec) EncodeSuccessEnvelope(result interface{}) (data []byte, err error) {
49 | return json.Marshal([]interface{}{result})
50 | }
51 |
52 | // EncodeErrorEnvelope encodes an error result into a binary envelope. The
53 | // specified error code, human-readable error message, and error details
54 | // correspond to the fields of Flutter's PlatformException.
55 | func (j JSONMethodCodec) EncodeErrorEnvelope(code string, message string, details interface{}) (data []byte, err error) {
56 | return json.Marshal([]interface{}{code, message, details})
57 | }
58 |
59 | // DecodeEnvelope decodes the specified result envelope from binary. Returns a
60 | // FlutterError as error if provided envelope represents an error, otherwise
61 | // returns the result as a json.RawMessage
62 | func (j JSONMethodCodec) DecodeEnvelope(envelope []byte) (result interface{}, err error) {
63 | fields := []json.RawMessage{}
64 | err = json.Unmarshal(envelope, &fields)
65 | if err != nil {
66 | return nil, errors.Wrap(err, "failed to decode envelope")
67 | }
68 |
69 | if len(fields) == 1 {
70 | return fields[0], nil
71 | }
72 | if len(fields) == 3 {
73 | ferr := FlutterError{
74 | Details: fields[2],
75 | }
76 | err = json.Unmarshal(fields[0], &ferr.Code)
77 | if err != nil {
78 | return nil, errors.Wrap(err, "failed to decode field 'code' from json error envelope")
79 | }
80 | err = json.Unmarshal(fields[1], &ferr.Message)
81 | if err != nil {
82 | return nil, errors.Wrap(err, "failed to decode field 'message' from json error envelope")
83 | }
84 | return nil, ferr
85 | }
86 | return nil, errors.New("invalid JSON envelope")
87 | }
88 |
--------------------------------------------------------------------------------
/plugin/json-method-codec_test.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestJSONMethodCodecEncodeDecodeSuccessEnvelope(t *testing.T) {
12 | scenarios := []struct {
13 | value interface{}
14 | decodedMessage json.RawMessage
15 | }{
16 | {
17 | value: int(42),
18 | decodedMessage: json.RawMessage("42"),
19 | },
20 | {
21 | value: float64(3.1415),
22 | decodedMessage: json.RawMessage("3.1415"),
23 | },
24 | {
25 | value: "string",
26 | decodedMessage: json.RawMessage(`"string"`),
27 | },
28 | {
29 | value: []byte("bytes"),
30 | decodedMessage: json.RawMessage([]byte(`"Ynl0ZXM="`)),
31 | },
32 | {
33 | value: []interface{}{"list", 0x0f, "thinks"},
34 | decodedMessage: json.RawMessage(`["list",15,"thinks"]`),
35 | },
36 | {
37 | value: map[string]interface{}{
38 | "foo": "bar",
39 | "number": 42,
40 | },
41 | decodedMessage: json.RawMessage(`{"foo":"bar","number":42}`),
42 | },
43 | }
44 |
45 | codec := JSONMethodCodec{}
46 |
47 | for _, scenario := range scenarios {
48 | message, err := codec.EncodeSuccessEnvelope(scenario.value)
49 | assert.Nil(t, err)
50 | decodedMessage, err := codec.DecodeEnvelope(message)
51 | assert.Nil(t, err)
52 | assert.Equal(t, scenario.decodedMessage, decodedMessage)
53 | }
54 |
55 | for i, argument := range scenarios {
56 | methodName := fmt.Sprintf("metohd.%d", i)
57 | binaryMessage, err := codec.EncodeMethodCall(MethodCall{
58 | Method: methodName,
59 | Arguments: argument.value,
60 | })
61 | assert.Nil(t, err)
62 | methodCall, err := codec.DecodeMethodCall(binaryMessage)
63 | assert.Nil(t, err)
64 | assert.Equal(t, methodName, methodCall.Method)
65 | assert.Equal(t, argument.decodedMessage, methodCall.Arguments)
66 | }
67 | }
68 |
69 | func TestJSONMethodCodecEncodeDecodeErrorEnvelope(t *testing.T) {
70 | errorCode := "myErrorCode"
71 | errorMessage := "myErrorMessage"
72 | errorDetails := map[string]interface{}{
73 | "foo": "bar",
74 | "number": 42,
75 | }
76 | expectedFerr := FlutterError{
77 | Code: errorCode,
78 | Message: errorMessage,
79 | Details: json.RawMessage(`{"foo":"bar","number":42}`),
80 | }
81 |
82 | codec := JSONMethodCodec{}
83 |
84 | env, err := codec.EncodeErrorEnvelope(errorCode, errorMessage, errorDetails)
85 | assert.Nil(t, err)
86 | assert.NotNil(t, env)
87 | result, err := codec.DecodeEnvelope(env)
88 | assert.Nil(t, result)
89 | assert.Equal(t, expectedFerr, err)
90 | }
91 |
--------------------------------------------------------------------------------
/plugin/method-channel.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "fmt"
5 | "runtime/debug"
6 | "sync"
7 |
8 | "github.com/pkg/errors"
9 | )
10 |
11 | // MethodChannel provides way for flutter applications and hosts to communicate.
12 | // It must be used with a codec, for example the StandardMethodCodec. For more
13 | // information please read
14 | // https://flutter.dev/docs/development/platform-integration/platform-channels
15 | type MethodChannel struct {
16 | messenger BinaryMessenger
17 | channelName string
18 | methodCodec MethodCodec
19 |
20 | methods map[string]methodHandlerRegistration
21 | catchAllhandler MethodHandler
22 | methodsLock sync.RWMutex
23 | }
24 |
25 | type methodHandlerRegistration struct {
26 | handler MethodHandler
27 | sync bool
28 | }
29 |
30 | // NewMethodChannel creates a new method channel
31 | func NewMethodChannel(messenger BinaryMessenger, channelName string, methodCodec MethodCodec) (channel *MethodChannel) {
32 | mc := &MethodChannel{
33 | messenger: messenger,
34 | channelName: channelName,
35 | methodCodec: methodCodec,
36 |
37 | methods: make(map[string]methodHandlerRegistration),
38 | }
39 | messenger.SetChannelHandler(channelName, mc.handleChannelMessage)
40 | return mc
41 | }
42 |
43 | // InvokeMethod sends a methodcall to the binary messenger without waiting for
44 | // a reply.
45 | func (m *MethodChannel) InvokeMethod(name string, arguments interface{}) error {
46 | encodedMessage, err := m.methodCodec.EncodeMethodCall(MethodCall{
47 | Method: name,
48 | Arguments: arguments,
49 | })
50 | if err != nil {
51 | return errors.Wrap(err, "failed to encode methodcall")
52 | }
53 | err = m.messenger.Send(m.channelName, encodedMessage)
54 | if err != nil {
55 | return errors.Wrap(err, "failed to send methodcall")
56 | }
57 | return nil
58 | }
59 |
60 | // InvokeMethodWithReply sends a methodcall to the binary messenger and wait
61 | // for a reply.
62 | //
63 | // NOTE: If no value are returned by the handler setted in the
64 | // setMethodCallHandler flutter method, the function will wait forever. In case
65 | // you don't want to wait for reply, use InvokeMethod or launch the
66 | // function in a goroutine.
67 | func (m *MethodChannel) InvokeMethodWithReply(name string, arguments interface{}) (result interface{}, err error) {
68 | encodedMessage, err := m.methodCodec.EncodeMethodCall(MethodCall{
69 | Method: name,
70 | Arguments: arguments,
71 | })
72 | if err != nil {
73 | return nil, errors.Wrap(err, "failed to encode methodcall")
74 | }
75 | encodedReply, err := m.messenger.SendWithReply(m.channelName, encodedMessage)
76 | if err != nil {
77 | return nil, errors.Wrap(err, "failed to send methodcall")
78 | }
79 | result, err = m.methodCodec.DecodeEnvelope(encodedReply)
80 | if err != nil {
81 | return nil, err
82 | }
83 | return result, nil
84 | }
85 |
86 | // Handle registers a method handler for method calls with given name.
87 | //
88 | // Consecutive calls override any existing handler registration for (the name
89 | // of) this method. When given nil as handler, the previously registered
90 | // handler for a method is unregistered.
91 | //
92 | // When no handler is registered for a method, it will be handled silently by
93 | // sending a nil reply which triggers the dart MissingPluginException exception.
94 | func (m *MethodChannel) Handle(methodName string, handler MethodHandler) {
95 | m.methodsLock.Lock()
96 | if handler == nil {
97 | delete(m.methods, methodName)
98 | } else {
99 | m.methods[methodName] = methodHandlerRegistration{
100 | handler: handler,
101 | }
102 | }
103 | m.methodsLock.Unlock()
104 | }
105 |
106 | // HandleFunc is a shorthand for m.Handle("name", MethodHandlerFunc(f))
107 | //
108 | // The argument of the function f is an interface corresponding to the
109 | // MethodCall.Arguments values
110 | func (m *MethodChannel) HandleFunc(methodName string, f func(arguments interface{}) (reply interface{}, err error)) {
111 | if f == nil {
112 | m.Handle(methodName, nil)
113 | return
114 | }
115 |
116 | m.Handle(methodName, MethodHandlerFunc(f))
117 | }
118 |
119 | // HandleSync is like Handle, but messages for given method are handled
120 | // synchronously.
121 | func (m *MethodChannel) HandleSync(methodName string, handler MethodHandler) {
122 | m.methodsLock.Lock()
123 | if handler == nil {
124 | delete(m.methods, methodName)
125 | } else {
126 | m.methods[methodName] = methodHandlerRegistration{
127 | handler: handler,
128 | sync: true,
129 | }
130 | }
131 | m.methodsLock.Unlock()
132 | }
133 |
134 | // HandleFuncSync is a shorthand for m.HandleSync("name", MethodHandlerFunc(f))
135 | //
136 | // The argument of the function f is an interface corresponding to the
137 | // MethodCall.Arguments values
138 | func (m *MethodChannel) HandleFuncSync(methodName string, f func(arguments interface{}) (reply interface{}, err error)) {
139 | if f == nil {
140 | m.HandleSync(methodName, nil)
141 | return
142 | }
143 |
144 | m.HandleSync(methodName, MethodHandlerFunc(f))
145 | }
146 |
147 | // ClearAllHandle clear all the handlers registered by
148 | // Handle\HandleFunc and HandleSync\HandleFuncSync.
149 | // ClearAllHandle doesn't not clear the handler registered by CatchAllHandle\CatchAllHandleFunc
150 | func (m *MethodChannel) ClearAllHandle() {
151 | m.methodsLock.Lock()
152 | m.methods = make(map[string]methodHandlerRegistration)
153 | m.methodsLock.Unlock()
154 | }
155 |
156 | // CatchAllHandle registers a default method handler.
157 | // When no Handle are found, the handler provided in CatchAllHandle will be
158 | // used. If no CatchAllHandle is provided, a MissingPluginException exception
159 | // is sent when no handler is registered for a method name.
160 | //
161 | // Consecutive calls override any existing handler registration for (the name
162 | // of) this method. When given nil as handler, the previously registered
163 | // handler for a method is unregistered.
164 | //
165 | // The argument of the HandleMethod function of the MethodHandler interface is
166 | // a MethodCall struct instead of MethodCall.Arguments
167 | func (m *MethodChannel) CatchAllHandle(handler MethodHandler) {
168 | m.methodsLock.Lock()
169 | m.catchAllhandler = handler
170 | m.methodsLock.Unlock()
171 | }
172 |
173 | // CatchAllHandleFunc is a shorthand for m.CatchAllHandle(MethodHandlerFunc(f))
174 | //
175 | // The argument of the function f is a MethodCall struct instead of
176 | // MethodCall.Arguments
177 | func (m *MethodChannel) CatchAllHandleFunc(f func(arguments interface{}) (reply interface{}, err error)) {
178 | m.CatchAllHandle(MethodHandlerFunc(f))
179 | }
180 |
181 | // handleChannelMessage decodes incoming binary message to a method call, calls the
182 | // handler, and encodes the outgoing reply.
183 | func (m *MethodChannel) handleChannelMessage(binaryMessage []byte, responseSender ResponseSender) (err error) {
184 | methodCall, err := m.methodCodec.DecodeMethodCall(binaryMessage)
185 | if err != nil {
186 | return errors.Wrap(err, "failed to decode incoming message")
187 | }
188 |
189 | m.methodsLock.RLock()
190 | registration, registrationExists := m.methods[methodCall.Method]
191 | m.methodsLock.RUnlock()
192 | if !registrationExists {
193 |
194 | if m.catchAllhandler != nil {
195 | go m.handleMethodCall(m.catchAllhandler, methodCall.Method, methodCall, responseSender)
196 | return nil
197 | }
198 |
199 | fmt.Printf("go-flutter: no method handler registered for method '%s' on channel '%s'\n", methodCall.Method, m.channelName)
200 | responseSender.Send(nil)
201 | return nil
202 | }
203 |
204 | if registration.sync {
205 | m.handleMethodCall(registration.handler, methodCall.Method, methodCall.Arguments, responseSender)
206 | } else {
207 | go m.handleMethodCall(registration.handler, methodCall.Method, methodCall.Arguments, responseSender)
208 | }
209 |
210 | return nil
211 | }
212 |
213 | // handleMethodCall handles the methodcall and sends a response.
214 | func (m *MethodChannel) handleMethodCall(handler MethodHandler, methodName string, methodArgs interface{}, responseSender ResponseSender) {
215 | defer func() {
216 | p := recover()
217 | if p != nil {
218 | fmt.Printf("go-flutter: recovered from panic while handling call for method '%s' on channel '%s': %v\n", methodName, m.channelName, p)
219 | debug.PrintStack()
220 | }
221 | }()
222 |
223 | reply, err := handler.HandleMethod(methodArgs)
224 | if err != nil {
225 | fmt.Printf("go-flutter: handler for method '%s' on channel '%s' returned an error: %v\n", methodName, m.channelName, err)
226 |
227 | var errorCode string
228 | switch t := err.(type) {
229 | case *Error:
230 | errorCode = t.code
231 | default:
232 | errorCode = "error"
233 | }
234 |
235 | binaryReply, err := m.methodCodec.EncodeErrorEnvelope(errorCode, err.Error(), nil)
236 | if err != nil {
237 | fmt.Printf("go-flutter: failed to encode error envelope for method '%s' on channel '%s', error: %v\n", methodName, m.channelName, err)
238 | }
239 | responseSender.Send(binaryReply)
240 | return
241 | }
242 | binaryReply, err := m.methodCodec.EncodeSuccessEnvelope(reply)
243 | if err != nil {
244 | fmt.Printf("go-flutter: failed to encode success envelope for method '%s' on channel '%s', error: %v\n", methodName, m.channelName, err)
245 | }
246 | responseSender.Send(binaryReply)
247 | }
248 |
249 | // Error implement the Go error interface, can be thrown from a go-flutter
250 | // method channel plugin to return custom error codes.
251 | // Normal Go error can also be used, the error code will default to "error".
252 | type Error struct {
253 | err string
254 | code string
255 | }
256 |
257 | // Error is needed to comply with the Golang error interface.
258 | func (e *Error) Error() string {
259 | return e.err
260 | }
261 |
262 | // NewError create an error with an specific error code.
263 | func NewError(code string, err error) *Error {
264 | pe := &Error{
265 | code: code,
266 | err: err.Error(),
267 | }
268 | return pe
269 | }
270 |
--------------------------------------------------------------------------------
/plugin/method-channel_test.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestMethodChannelJSONInvoke(t *testing.T) {
11 | messenger := NewTestingBinaryMessenger()
12 | codec := JSONMethodCodec{}
13 | channel := NewMethodChannel(messenger, "ch", codec)
14 | messenger.MockSetChannelHandler("ch", func(msg []byte, r ResponseSender) error {
15 | methodCall, err := codec.DecodeMethodCall(msg)
16 | assert.Nil(t, err)
17 | assert.NotNil(t, methodCall)
18 | if methodCall.Method == "sayHello" {
19 | var greeting string
20 | err = json.Unmarshal(methodCall.Arguments.(json.RawMessage), &greeting)
21 | assert.Nil(t, err)
22 | binaryReply, err := codec.EncodeSuccessEnvelope(greeting + " world")
23 | assert.Nil(t, err)
24 | r.Send(binaryReply)
25 | return nil
26 | }
27 | binaryReply, err := codec.EncodeErrorEnvelope("unknown", "", nil)
28 | assert.Nil(t, err)
29 | r.Send(binaryReply)
30 | return nil
31 | })
32 | result, err := channel.InvokeMethodWithReply("sayHello", "hello")
33 | assert.Nil(t, err)
34 | assert.Equal(t, json.RawMessage(`"hello world"`), result)
35 |
36 | result, err = channel.InvokeMethodWithReply("invalidMethod", "")
37 | assert.Nil(t, result)
38 | expectedError := FlutterError{
39 | Code: "unknown",
40 | Message: "",
41 | Details: json.RawMessage(`null`),
42 | }
43 | assert.Equal(t, expectedError, err)
44 | }
45 |
46 | // test('can invoke map method and get result', () async {
47 | // BinaryMessages.setMockMessageHandler(
48 | // 'ch7',
49 | // (ByteData message) async {
50 | // final Map methodCall = jsonMessage.decodeMessage(message);
51 | // if (methodCall['method'] == 'sayHello') {
52 | // return jsonMessage.encodeMessage([{'${methodCall['args']}': 'world'}]);
53 | // } else {
54 | // return jsonMessage.encodeMessage(['unknown', null, null]);
55 | // }
56 | // },
57 | // );
58 | // expect(channel.invokeMethod