├── .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 | [![Awesome Flutter](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat)](https://github.com/Solido/awesome-flutter) 6 | [![Documentation](https://godoc.org/github.com/go-flutter-desktop/go-flutter?status.svg)](http://godoc.org/github.com/go-flutter-desktop/go-flutter) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-flutter-desktop/go-flutter)](https://goreportcard.com/report/github.com/go-flutter-desktop/go-flutter) 8 | [![Join the chat at https://gitter.im/go-flutter-desktop/go-flutter](https://badges.gitter.im/go-flutter-desktop/go-flutter.svg)](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 | Screenshot of the Stocks demo app on macOS 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>('sayHello', 'hello'), throwsA(isInstanceOf())); 59 | // expect(await channel.invokeMapMethod('sayHello', 'hello'), {'hello': 'world'}); 60 | // }); 61 | 62 | // test('can invoke method and get error', () async { 63 | // BinaryMessages.setMockMessageHandler( 64 | // 'ch7', 65 | // (ByteData message) async { 66 | // return jsonMessage.encodeMessage([ 67 | // 'bad', 68 | // 'Something happened', 69 | // {'a': 42, 'b': 3.14}, 70 | // ]); 71 | // }, 72 | // ); 73 | // try { 74 | // await channel.invokeMethod('sayHello', 'hello'); 75 | // fail('Exception expected'); 76 | // } on PlatformException catch (e) { 77 | // expect(e.code, equals('bad')); 78 | // expect(e.message, equals('Something happened')); 79 | // expect(e.details, equals({'a': 42, 'b': 3.14})); 80 | // } catch (e) { 81 | // fail('PlatformException expected'); 82 | // } 83 | // }); 84 | // test('can invoke unimplemented method', () async { 85 | // BinaryMessages.setMockMessageHandler( 86 | // 'ch7', 87 | // (ByteData message) async => null, 88 | // ); 89 | // try { 90 | // await channel.invokeMethod('sayHello', 'hello'); 91 | // fail('Exception expected'); 92 | // } on MissingPluginException catch (e) { 93 | // expect(e.message, contains('sayHello')); 94 | // expect(e.message, contains('ch7')); 95 | // } catch (e) { 96 | // fail('MissingPluginException expected'); 97 | // } 98 | // }); 99 | // test('can handle method call with no registered plugin', () async { 100 | // channel.setMethodCallHandler(null); 101 | // final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); 102 | // ByteData envelope; 103 | // await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) { 104 | // envelope = result; 105 | // }); 106 | // expect(envelope, isNull); 107 | // }); 108 | // test('can handle method call of unimplemented method', () async { 109 | // channel.setMethodCallHandler((MethodCall call) async { 110 | // throw MissingPluginException(); 111 | // }); 112 | // final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); 113 | // ByteData envelope; 114 | // await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) { 115 | // envelope = result; 116 | // }); 117 | // expect(envelope, isNull); 118 | // }); 119 | // test('can handle method call with successful result', () async { 120 | // channel.setMethodCallHandler((MethodCall call) async => '${call.arguments}, world'); 121 | // final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); 122 | // ByteData envelope; 123 | // await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) { 124 | // envelope = result; 125 | // }); 126 | // expect(jsonMethod.decodeEnvelope(envelope), equals('hello, world')); 127 | // }); 128 | // test('can handle method call with expressive error result', () async { 129 | // channel.setMethodCallHandler((MethodCall call) async { 130 | // throw PlatformException(code: 'bad', message: 'sayHello failed', details: null); 131 | // }); 132 | // final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); 133 | // ByteData envelope; 134 | // await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) { 135 | // envelope = result; 136 | // }); 137 | // try { 138 | // jsonMethod.decodeEnvelope(envelope); 139 | // fail('Exception expected'); 140 | // } on PlatformException catch (e) { 141 | // expect(e.code, equals('bad')); 142 | // expect(e.message, equals('sayHello failed')); 143 | // } catch (e) { 144 | // fail('PlatformException expected'); 145 | // } 146 | // }); 147 | // test('can handle method call with other error result', () async { 148 | // channel.setMethodCallHandler((MethodCall call) async { 149 | // throw ArgumentError('bad'); 150 | // }); 151 | // final ByteData call = jsonMethod.encodeMethodCall(const MethodCall('sayHello', 'hello')); 152 | // ByteData envelope; 153 | // await BinaryMessages.handlePlatformMessage('ch7', call, (ByteData result) { 154 | // envelope = result; 155 | // }); 156 | // try { 157 | // jsonMethod.decodeEnvelope(envelope); 158 | // fail('Exception expected'); 159 | // } on PlatformException catch (e) { 160 | // expect(e.code, equals('error')); 161 | // expect(e.message, equals('Invalid argument(s): bad')); 162 | // } catch (e) { 163 | // fail('PlatformException expected'); 164 | // } 165 | // }); 166 | // }); 167 | -------------------------------------------------------------------------------- /plugin/method-handler.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | // MethodHandler defines the interface for a method handler. 4 | type MethodHandler interface { 5 | // HandleMethod is called whenever an incoming 6 | HandleMethod(arguments interface{}) (reply interface{}, err error) 7 | } 8 | 9 | // The MethodHandlerFunc type is an adapter to allow the use of 10 | // ordinary functions as method handlers. If f is a function 11 | // with the appropriate signature, MethodHandlerFunc(f) is a 12 | // MethodHandler that calls f. 13 | type MethodHandlerFunc func(arguments interface{}) (reply interface{}, err error) 14 | 15 | // HandleMethod calls f(arguments). 16 | func (f MethodHandlerFunc) HandleMethod(arguments interface{}) (reply interface{}, err error) { 17 | return f(arguments) 18 | } 19 | -------------------------------------------------------------------------------- /plugin/standard-message-codec_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "math" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/davecgh/go-spew/spew" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestStandardMessageEncodeIntegers(t *testing.T) { 13 | // all these values were taken from flutter tests, lowInt and highInt64 must 14 | // be vars oir the compiler complains about overflowing constant values. 15 | var lowInt64 = int64(-0x7fffffffffffffff) 16 | var highInt64 = int64(0x7fffffffffffffff) 17 | scenarios := []struct { 18 | value interface{} 19 | data []byte 20 | }{ 21 | {value: int32(-0x7fffffff - 1), data: []byte{3, 0x00, 0x00, 0x00, 0x80}}, 22 | {value: int64(-0x7fffffff - 2), data: []byte{4, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff}}, 23 | {value: int32(0x7fffffff), data: []byte{3, 0xff, 0xff, 0xff, 0x7f}}, 24 | {value: int64(0x7fffffff + 1), data: []byte{4, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00}}, 25 | {value: int64(-0x7fffffffffffffff - 1), data: []byte{4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}}, 26 | {value: lowInt64 - 2, data: []byte{4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}}, 27 | {value: int64(0x7fffffffffffffff), data: []byte{4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}}, 28 | {value: highInt64 + 1, data: []byte{4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}}, 29 | } 30 | codec := StandardMessageCodec{} 31 | for _, s := range scenarios { 32 | result, err := codec.EncodeMessage(s.value) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | assert.Equal(t, s.data, result) 37 | } 38 | } 39 | 40 | func TestStandardMessageEncodeSizes(t *testing.T) { 41 | scenarios := []struct { 42 | value interface{} 43 | data []byte 44 | }{ 45 | {value: make([]byte, 253), data: append([]byte{8, 253}, make([]byte, 253)...)}, 46 | {value: make([]byte, 254), data: append([]byte{8, 254, 254, 0}, make([]byte, 254)...)}, 47 | {value: make([]byte, 0xffff), data: append([]byte{8, 254, 0xff, 0xff}, make([]byte, 0xffff)...)}, 48 | {value: make([]byte, 0xffff+1), data: append([]byte{8, 255, 0, 0, 1, 0}, make([]byte, 0xffff+1)...)}, 49 | } 50 | 51 | codec := StandardMessageCodec{} 52 | 53 | for _, s := range scenarios { 54 | result, err := codec.EncodeMessage(s.value) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | assert.Equal(t, s.data, result) 59 | } 60 | } 61 | 62 | func TestStandardMessageEncodeSimple(t *testing.T) { 63 | values := []interface{}{ 64 | nil, 65 | true, 66 | false, 67 | int32(7), 68 | int32(-7), 69 | int64(98742923489), 70 | int64(-98742923489), 71 | int64(9223372036854775807), 72 | int64(-9223372036854775807), 73 | big.NewInt(9223372036854775807), 74 | big.NewInt(-9223372036854775807), 75 | 3.14, 76 | math.Inf(+1), 77 | "", 78 | "hello", 79 | "special chars >☺😂<", 80 | } 81 | 82 | codec := StandardMessageCodec{} 83 | 84 | for _, v := range values { 85 | t.Logf("encoding: %T %v\n", v, v) 86 | 87 | data, err := codec.EncodeMessage(v) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | t.Logf(spew.Sdump(data)) 92 | 93 | v2, err := codec.DecodeMessage(data) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | assert.Equal(t, v, v2) 99 | } 100 | } 101 | 102 | func TestStandardMessageEncodeNaN(t *testing.T) { 103 | // Nan != NaN, which causes Equal(..) to give a false negative. 104 | 105 | v := math.NaN() 106 | codec := StandardMessageCodec{} 107 | 108 | data, err := codec.EncodeMessage(v) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | v2, err := codec.DecodeMessage(data) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | 118 | v2Float64, ok := v2.(float64) 119 | if !ok { 120 | t.Fatal("NaN was not decoded to float64") 121 | } 122 | if !math.IsNaN(v2Float64) { 123 | t.Fatal("NaN was not decoded to NaN") 124 | } 125 | } 126 | 127 | func TestStandardMessageEncodeComposite(t *testing.T) { 128 | values := []interface{}{ 129 | nil, 130 | true, 131 | false, 132 | int32(-707), 133 | int64(-7000000007), 134 | int64(-7000000000000000007), 135 | big.NewInt(-7000000000000000007), 136 | float64(-3.14), 137 | "", 138 | "hello", 139 | []byte{0xBA, 0x5E, 0xBA, 0x11}, 140 | []int32{-0x7fffffff - 1, 0, 0x7fffffff}, 141 | nil, // ensures the offset of the following list is unaligned. 142 | []int64{-0x7fffffffffffffff - 1, 0, 0x7fffffffffffffff}, 143 | nil, // ensures the offset of the following list is unaligned. 144 | []float64{ 145 | math.Inf(1), 146 | math.Inf(-1), 147 | math.MaxFloat32, 148 | -math.MaxFloat64, 149 | 0.0, 150 | -0.0, 151 | }, 152 | []interface{}{"nested", []interface{}{}}, 153 | []interface{}{ 154 | int32(123), 155 | 3.14, 156 | int64(456), 157 | "hi", 158 | []byte{1, 0xff, 0, 0x0f}, 159 | }, 160 | map[interface{}]interface{}{ 161 | "a": "nested", 162 | int32(2): []interface{}{ 163 | int32(123), 164 | 453243.4324234, 165 | int64(456), 166 | "hi", 167 | []byte{1, 0xff, 0, 0x0f}, 168 | }, 169 | nil: map[interface{}]interface{}{ 170 | "foo": "bar", 171 | int32(42): int32(43), 172 | int64(1): big.NewInt(12345), 173 | }, 174 | }, 175 | "world", 176 | } 177 | 178 | // add the whole of values, as last item to values. 179 | values = append(values, values) 180 | 181 | codec := StandardMessageCodec{} 182 | 183 | for _, v := range values { 184 | t.Logf("encoding: %T %v\n", v, v) 185 | 186 | data, err := codec.EncodeMessage(v) 187 | if err != nil { 188 | t.Fatal(err) 189 | } 190 | t.Logf(spew.Sdump(data)) 191 | 192 | v2, err := codec.DecodeMessage(data) 193 | if err != nil { 194 | t.Fatal(err) 195 | } 196 | 197 | assert.Equal(t, v, v2) 198 | } 199 | } 200 | 201 | func TestStandardMessageEncodeAlignment(t *testing.T) { 202 | scenarios := []struct { 203 | value interface{} 204 | data []byte 205 | }{ 206 | {value: 1.0, data: []byte{6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xf0, 0x3f}}, 207 | } 208 | 209 | codec := StandardMessageCodec{} 210 | 211 | for _, s := range scenarios { 212 | result, err := codec.EncodeMessage(s.value) 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | assert.Equal(t, s.data, result) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /plugin/standard-method-codec.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // The first byte in a standard method envelope determines it's type. 10 | const ( 11 | standardMethodEnvelopeSuccess = 0 12 | standardMethodEnvelopeError = 1 13 | ) 14 | 15 | // StandardMethodCodec implements a MethodCodec using the Flutter standard 16 | // binary encoding. 17 | // 18 | // This codec tries to stay compatible with the corresponding 19 | // StandardMethodCodec on the Dart side. 20 | // See https://docs.flutter.io/flutter/services/StandardMethodCodec-class.html 21 | // 22 | // Values supported as method arguments and result payloads are those supported 23 | // by StandardMessageCodec. 24 | type StandardMethodCodec struct { 25 | // Setting a custom/extended StandardMessageCodec is not supported. 26 | codec StandardMessageCodec 27 | } 28 | 29 | var _ MethodCodec = StandardMethodCodec{} 30 | 31 | // EncodeMethodCall fulfils the MethodCodec interface. 32 | func (s StandardMethodCodec) EncodeMethodCall(methodCall MethodCall) (data []byte, err error) { 33 | buf := &bytes.Buffer{} 34 | err = s.codec.writeValue(buf, methodCall.Method) 35 | if err != nil { 36 | return nil, errors.Wrap(err, "failed writing methodcall method name") 37 | } 38 | err = s.codec.writeValue(buf, methodCall.Arguments) 39 | if err != nil { 40 | return nil, errors.Wrap(err, "failed writing methodcall arguments") 41 | } 42 | return buf.Bytes(), nil 43 | } 44 | 45 | // DecodeMethodCall fulfils the MethodCodec interface. 46 | func (s StandardMethodCodec) DecodeMethodCall(data []byte) (methodCall MethodCall, err error) { 47 | buf := bytes.NewBuffer(data) 48 | originalSize := buf.Len() 49 | method, err := s.codec.readValue(buf) 50 | if err != nil { 51 | return methodCall, errors.Wrap(err, "failed to decode method name") 52 | } 53 | var ok bool 54 | methodCall.Method, ok = method.(string) 55 | if !ok { 56 | return methodCall, errors.New("decoded method name is not a string") 57 | } 58 | methodCall.Arguments, err = s.codec.readValueAligned(buf, originalSize) 59 | if err != nil { 60 | return methodCall, errors.Wrap(err, "failed decoding method arguments") 61 | } 62 | return methodCall, nil 63 | } 64 | 65 | // EncodeSuccessEnvelope fulfils the MethodCodec interface. 66 | func (s StandardMethodCodec) EncodeSuccessEnvelope(result interface{}) (data []byte, err error) { 67 | buf := &bytes.Buffer{} 68 | err = buf.WriteByte(standardMethodEnvelopeSuccess) 69 | if err != nil { 70 | return nil, err 71 | } 72 | err = s.codec.writeValue(buf, result) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return buf.Bytes(), nil 77 | } 78 | 79 | // EncodeErrorEnvelope fulfils the MethodCodec interface. 80 | func (s StandardMethodCodec) EncodeErrorEnvelope(code string, message string, details interface{}) (data []byte, err error) { 81 | buf := &bytes.Buffer{} 82 | err = buf.WriteByte(standardMethodEnvelopeError) 83 | if err != nil { 84 | return nil, err 85 | } 86 | err = s.codec.writeValue(buf, code) 87 | if err != nil { 88 | return nil, err 89 | } 90 | err = s.codec.writeValue(buf, message) 91 | if err != nil { 92 | return nil, err 93 | } 94 | err = s.codec.writeValue(buf, details) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return buf.Bytes(), nil 99 | } 100 | 101 | // DecodeEnvelope fulfils the MethodCodec interface. 102 | func (s StandardMethodCodec) DecodeEnvelope(envelope []byte) (result interface{}, err error) { 103 | buf := bytes.NewBuffer(envelope) 104 | flag, err := buf.ReadByte() 105 | if err != nil { 106 | return nil, errors.Wrap(err, "failed reading envelope flag") 107 | } 108 | switch flag { 109 | case standardMethodEnvelopeSuccess: 110 | result, err = s.codec.readValue(buf) 111 | if err != nil { 112 | return nil, errors.Wrap(err, "failed to decode result") 113 | } 114 | return result, nil 115 | 116 | case standardMethodEnvelopeError: 117 | ferr := FlutterError{} 118 | var ok bool 119 | code, err := s.codec.readValue(buf) 120 | if err != nil { 121 | return nil, errors.Wrap(err, "failed to decode error code") 122 | } 123 | ferr.Code, ok = code.(string) 124 | if !ok { 125 | return nil, errors.New("decoded error code is not a string") 126 | } 127 | message, err := s.codec.readValue(buf) 128 | if err != nil { 129 | return nil, errors.Wrap(err, "failed to decode error message") 130 | } 131 | if message != nil { 132 | ferr.Message, ok = message.(string) 133 | if !ok { 134 | return nil, errors.New("decoded error message is not a string") 135 | } 136 | } 137 | ferr.Details, err = s.codec.readValue(buf) 138 | if err != nil { 139 | return nil, errors.Wrap(err, "failed to decode error details") 140 | } 141 | return nil, ferr 142 | default: 143 | return nil, errors.New("unknown envelope flag") 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /plugin/standard-method-codec_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/davecgh/go-spew/spew" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestStandardMethodEncodeMethodCall(t *testing.T) { 11 | scenarios := []struct { 12 | value MethodCall 13 | data []byte 14 | }{ 15 | { 16 | value: MethodCall{ 17 | Method: "12345678", 18 | Arguments: "foobar", 19 | }, 20 | data: []byte{ 21 | 0x07, 0x08, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, // string type, string length, "123456" 22 | 0x37, 0x38, 0x07, 0x06, 0x66, 0x6f, 0x6f, 0x62, // "78", string type, string length, "foob" 23 | 0x61, 0x72, // "ar" 24 | }, 25 | }, 26 | { 27 | value: MethodCall{ 28 | Method: "", 29 | Arguments: float64(3.1415), 30 | }, 31 | data: []byte{ 32 | 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, // string type, string length, "", float type, 5 alignment bytes 33 | 0x6f, 0x12, 0x83, 0xc0, 0xca, 0x21, 0x09, 0x40, // encoded float value (3.1415) 34 | }, 35 | }, 36 | { 37 | value: MethodCall{ 38 | Method: "1", 39 | Arguments: float64(3.1415), 40 | }, 41 | data: []byte{ 42 | 0x07, 0x01, 0x31, 0x06, 0x00, 0x00, 0x00, 0x00, // string type, string length, "1", float type, 4 alignment bytes 43 | 0x6f, 0x12, 0x83, 0xc0, 0xca, 0x21, 0x09, 0x40, // encoded float value (3.1415) 44 | }, 45 | }, 46 | { 47 | value: MethodCall{ 48 | Method: "12", 49 | Arguments: float64(3.1415), 50 | }, 51 | data: []byte{ 52 | 0x07, 0x02, 0x31, 0x32, 0x06, 0x00, 0x00, 0x00, // string type, string length, "12", float type, 3 alignment bytes 53 | 0x6f, 0x12, 0x83, 0xc0, 0xca, 0x21, 0x09, 0x40, // encoded float value (3.1415) 54 | }, 55 | }, 56 | { 57 | value: MethodCall{ 58 | Method: "123", 59 | Arguments: float64(3.1415), 60 | }, 61 | data: []byte{ 62 | 0x07, 0x03, 0x31, 0x32, 0x33, 0x06, 0x00, 0x00, // string type, string length, "123", float type, 2 alignment bytes 63 | 0x6f, 0x12, 0x83, 0xc0, 0xca, 0x21, 0x09, 0x40, // encoded float value (3.1415) 64 | }, 65 | }, 66 | { 67 | value: MethodCall{ 68 | Method: "1234", 69 | Arguments: float64(3.1415), 70 | }, 71 | data: []byte{ 72 | 0x07, 0x04, 0x31, 0x32, 0x33, 0x34, 0x06, 0x00, // string type, string length, "1234", float type, 1 alignment byte 73 | 0x6f, 0x12, 0x83, 0xc0, 0xca, 0x21, 0x09, 0x40, // encoded float value (3.1415) 74 | }, 75 | }, 76 | { 77 | value: MethodCall{ 78 | Method: "12345", 79 | Arguments: float64(3.1415), 80 | }, 81 | data: []byte{ 82 | 0x07, 0x05, 0x31, 0x32, 0x33, 0x34, 0x35, 0x06, // string type, string length, "12345", float type 83 | 0x6f, 0x12, 0x83, 0xc0, 0xca, 0x21, 0x09, 0x40, // encoded float value (3.1415) 84 | }, 85 | }, 86 | { 87 | value: MethodCall{ 88 | Method: "123456", 89 | Arguments: float64(3.1415), 90 | }, 91 | data: []byte{ 92 | 0x07, 0x06, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, // string type, string length, "123456" 93 | 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // float type, alignment bytes 94 | 0x6f, 0x12, 0x83, 0xc0, 0xca, 0x21, 0x09, 0x40, // encoded float value (3.1415) 95 | }, 96 | }, 97 | } 98 | codec := StandardMethodCodec{} 99 | 100 | for _, s := range scenarios { 101 | t.Logf("encoding: %v\n", s.value) 102 | 103 | encodedData, err := codec.EncodeMethodCall(s.value) 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | t.Logf(spew.Sdump(encodedData)) 108 | assert.Equal(t, s.data, encodedData) 109 | 110 | decodedValue, err := codec.DecodeMethodCall(encodedData) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | 115 | assert.Equal(t, s.value, decodedValue) 116 | } 117 | } 118 | 119 | // TestStandardMethodDecodeRealWorldMethodCall tests decoding a method call 120 | // found in the real world, that posed some problems for an early 121 | // implementation. 122 | func TestStandardMethodDecodeRealWorldMethodCall(t *testing.T) { 123 | scenarios := []struct { 124 | value MethodCall 125 | data []byte 126 | }{ 127 | { 128 | value: MethodCall{ 129 | Method: "play", 130 | Arguments: map[interface{}]interface{}{ 131 | "playerId": "fcc45fbf-d44f-4468-8d9a-f9cf64e3443f", 132 | "mode": "PlayerMode.MEDIA_PLAYER", 133 | "url": "https://luan.xyz/files/audio/ambient_c_motion.mp3", 134 | "isLocal": false, 135 | "volume": float64(1), 136 | "position": nil, 137 | "respectSilence": false, 138 | }, 139 | }, 140 | data: []byte{0x07, 0x04, 0x70, 0x6c, 0x61, 0x79, 0x0d, 0x07, 0x07, 0x03, 0x75, 0x72, 0x6c, 0x07, 0x31, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x6c, 0x75, 0x61, 0x6e, 0x2e, 0x78, 0x79, 0x7a, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x2f, 0x61, 0x6d, 0x62, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x5f, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x6d, 0x70, 0x33, 0x07, 0x07, 0x69, 0x73, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x02, 0x07, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0x07, 0x08, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x07, 0x0e, 0x72, 0x65, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x69, 0x6c, 0x65, 0x6e, 0x63, 0x65, 0x02, 0x07, 0x08, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x49, 0x64, 0x07, 0x24, 0x66, 0x63, 0x63, 0x34, 0x35, 0x66, 0x62, 0x66, 0x2d, 0x64, 0x34, 0x34, 0x66, 0x2d, 0x34, 0x34, 0x36, 0x38, 0x2d, 0x38, 0x64, 0x39, 0x61, 0x2d, 0x66, 0x39, 0x63, 0x66, 0x36, 0x34, 0x65, 0x33, 0x34, 0x34, 0x33, 0x66, 0x07, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x07, 0x17, 0x50, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x4d, 0x6f, 0x64, 0x65, 0x2e, 0x4d, 0x45, 0x44, 0x49, 0x41, 0x5f, 0x50, 0x4c, 0x41, 0x59, 0x45, 0x52}, 141 | }, 142 | } 143 | 144 | codec := StandardMethodCodec{} 145 | 146 | for _, s := range scenarios { 147 | result, err := codec.DecodeMethodCall(s.data) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | assert.Equal(t, s.value, result) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /plugin/string-codec.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "errors" 5 | "unicode/utf8" 6 | ) 7 | 8 | // StringCodec implements a MessageCodec using UTF-8 encoded string messages. 9 | type StringCodec struct{} 10 | 11 | // Compiler test to assert that StringCodec implements MessageCodec 12 | var _ MessageCodec = &StringCodec{} 13 | 14 | // EncodeMessage expects message to be a string. 15 | func (StringCodec) EncodeMessage(message interface{}) ([]byte, error) { 16 | if message == nil { 17 | return nil, nil 18 | } 19 | 20 | s, ok := message.(string) 21 | if !ok { 22 | return nil, MessageTypeError{"expected message to be of type string"} 23 | } 24 | if !utf8.ValidString(s) { 25 | return nil, errors.New("error encoding message to bytes, string message is not valid UTF-8 encoded") 26 | } 27 | return []byte(s), nil 28 | } 29 | 30 | // DecodeMessage decodes binary data into a string message. 31 | func (StringCodec) DecodeMessage(data []byte) (message interface{}, err error) { 32 | if data == nil { 33 | return nil, nil 34 | } 35 | 36 | s := string(data) 37 | if !utf8.ValidString(s) { 38 | return nil, errors.New("error decoding bytes to message, bytes are not valid UTF-8 encoded") 39 | } 40 | return s, nil 41 | } 42 | -------------------------------------------------------------------------------- /plugin/string-codec_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestStringEncodeDecode(t *testing.T) { 10 | values := []interface{}{ 11 | nil, 12 | "", 13 | "hello", 14 | "special chars >☺😂<", 15 | } 16 | 17 | codec := StringCodec{} 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 | 32 | func TestStringEncodeFail(t *testing.T) { 33 | codec := StringCodec{} 34 | 35 | // invalid type 36 | _, err := codec.EncodeMessage(int(42)) 37 | assert.NotNil(t, err) 38 | 39 | // invalid 2-octet utf-8 sequence 40 | _, err = codec.EncodeMessage("\xc3\x28") 41 | assert.NotNil(t, err) 42 | } 43 | 44 | func TestStringDecodeFail(t *testing.T) { 45 | codec := StringCodec{} 46 | 47 | // invalid 2-octet utf-8 sequence 48 | _, err := codec.DecodeMessage([]byte("\xc3\x28")) 49 | assert.NotNil(t, err) 50 | } 51 | -------------------------------------------------------------------------------- /pop.go: -------------------------------------------------------------------------------- 1 | package flutter 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | // popBehavior defines how an application should handle the navigation pop 8 | // event from the flutter side. 9 | type popBehavior int 10 | 11 | const ( 12 | // PopBehaviorNone means the system navigation pop event is ignored. 13 | PopBehaviorNone popBehavior = iota 14 | // PopBehaviorHide hides the application window on a system navigation pop 15 | // event. 16 | PopBehaviorHide 17 | // PopBehaviorIconify minimizes/iconifies the application window on a system 18 | // navigation pop event. 19 | PopBehaviorIconify 20 | // PopBehaviorClose closes the application on a system navigation pop event. 21 | PopBehaviorClose 22 | ) 23 | 24 | // PopBehavior sets the PopBehavior on the application 25 | func PopBehavior(p popBehavior) Option { 26 | return func(c *config) { 27 | // TODO: this is a workarround because there is no renderer interface 28 | // yet. We rely on a platform plugin singleton to handle events from the 29 | // flutter side. Should go via Application and renderer abstraction 30 | // layer. 31 | // 32 | // Downside of this workarround is that it will configure the pop 33 | // behavior for all Application's within the same Go process. 34 | defaultPlatformPlugin.popBehavior = p 35 | } 36 | } 37 | 38 | func (p *platformPlugin) handleSystemNavigatorPop(arguments interface{}) (reply interface{}, err error) { 39 | switch p.popBehavior { 40 | case PopBehaviorNone: 41 | return nil, nil 42 | case PopBehaviorHide: 43 | p.window.Hide() 44 | return nil, nil 45 | case PopBehaviorIconify: 46 | p.window.Iconify() 47 | return nil, nil 48 | case PopBehaviorClose: 49 | p.window.SetShouldClose(true) 50 | return nil, nil 51 | default: 52 | return nil, errors.Errorf("unknown pop behavior %T not implemented by platform handler", p.popBehavior) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /restoration.go: -------------------------------------------------------------------------------- 1 | package flutter 2 | 3 | import ( 4 | "github.com/go-flutter-desktop/go-flutter/plugin" 5 | ) 6 | 7 | type restorationPlugin struct{} 8 | 9 | // all hardcoded because theres not pluggable renderer system. 10 | var defaultRestorationPlugin = &restorationPlugin{} 11 | 12 | var _ Plugin = &restorationPlugin{} // compile-time type check 13 | 14 | func (p *restorationPlugin) InitPlugin(messenger plugin.BinaryMessenger) error { 15 | channel := plugin.NewMethodChannel(messenger, "flutter/restoration", plugin.StandardMethodCodec{}) 16 | // Ignored: desktop doesn't need application "restoration" 17 | channel.HandleFunc("get", func(_ interface{}) (interface{}, error) { return nil, nil }) 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /stocks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-flutter-desktop/go-flutter/0fcb3ed6cbc524efd082470654c7e91d59799432/stocks.jpg -------------------------------------------------------------------------------- /text-input.go: -------------------------------------------------------------------------------- 1 | package flutter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "unicode" 8 | "unicode/utf16" 9 | 10 | "github.com/go-flutter-desktop/go-flutter/internal/keyboard" 11 | "github.com/go-flutter-desktop/go-flutter/plugin" 12 | "github.com/go-gl/glfw/v3.3/glfw" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | const textinputChannelName = "flutter/textinput" 17 | 18 | // textinputPlugin implements flutter.Plugin and handles method calls to the 19 | // flutter/textinput channel. 20 | type textinputPlugin struct { 21 | channel *plugin.MethodChannel 22 | 23 | clientID float64 24 | clientConf argSetClientConf 25 | ed argsEditingState 26 | 27 | backOnEscape bool 28 | 29 | virtualKeyboardShow func() 30 | virtualKeyboardHide func() 31 | } 32 | 33 | // argSetClientConf is used to define the config of the TextInput. Options used: 34 | // The type of information for which to optimize the text input control. 35 | // An action the user has requested the text input control to perform. 36 | // Configures how the platform keyboard will select an uppercase or lowercase keyboard. 37 | type argSetClientConf struct { 38 | InputType struct { 39 | Name string `json:"name"` 40 | } `json:"inputType"` 41 | InputAction string `json:"inputAction"` 42 | TextCapitalization string `json:"textCapitalization"` 43 | } 44 | 45 | // argsEditingState is used to hold the current TextInput state. 46 | type argsEditingState struct { 47 | Text string `json:"text"` 48 | utf16Text []uint16 49 | SelectionBase int `json:"selectionBase"` 50 | SelectionExtent int `json:"selectionExtent"` 51 | SelectionAffinity string `json:"selectionAffinity"` 52 | } 53 | 54 | // all hardcoded because theres not pluggable renderer system. 55 | var defaultTextinputPlugin = &textinputPlugin{} 56 | 57 | func (p *textinputPlugin) InitPlugin(messenger plugin.BinaryMessenger) error { 58 | p.channel = plugin.NewMethodChannel(messenger, textinputChannelName, plugin.JSONMethodCodec{}) 59 | p.channel.HandleFuncSync("TextInput.setClient", p.handleSetClient) 60 | p.channel.HandleFuncSync("TextInput.clearClient", p.handleClearClient) 61 | p.channel.HandleFuncSync("TextInput.setEditingState", p.handleSetEditingState) 62 | p.channel.HandleFunc("TextInput.show", func(_ interface{}) (interface{}, error) { 63 | if p.virtualKeyboardShow != nil { 64 | p.virtualKeyboardShow() 65 | } 66 | return nil, nil 67 | }) 68 | p.channel.HandleFunc("TextInput.hide", func(_ interface{}) (interface{}, error) { 69 | if p.virtualKeyboardHide != nil { 70 | p.virtualKeyboardHide() 71 | } 72 | return nil, nil 73 | }) 74 | // Ignored: This information is used by the flutter Web Engine 75 | p.channel.HandleFuncSync("TextInput.setStyle", func(_ interface{}) (interface{}, error) { return nil, nil }) 76 | // Ignored: Used on MacOS to position accent selection menu 77 | p.channel.HandleFuncSync("TextInput.setCaretRect", func(_ interface{}) (interface{}, error) { return nil, nil }) 78 | // Ignored: GLFW dosn't support setting the input method of the current cursor location #426 79 | p.channel.HandleFuncSync("TextInput.setEditableSizeAndTransform", func(_ interface{}) (interface{}, error) { return nil, nil }) 80 | // Ignored: GLFW dosn't support setting the input method of the current cursor location #426 81 | p.channel.HandleFuncSync("TextInput.setMarkedTextRect", func(_ interface{}) (interface{}, error) { return nil, nil }) 82 | // Ignored: This information is used by flutter on Android, iOS and web 83 | p.channel.HandleFuncSync("TextInput.requestAutofill", func(_ interface{}) (interface{}, error) { return nil, nil }) 84 | 85 | return nil 86 | } 87 | 88 | func (p *textinputPlugin) handleSetClient(arguments interface{}) (reply interface{}, err error) { 89 | args := []json.RawMessage{} 90 | err = json.Unmarshal(arguments.(json.RawMessage), &args) 91 | if err != nil { 92 | return nil, errors.Wrap(err, "failed to decode json arguments for handleSetClient") 93 | } 94 | 95 | if len(args) < 2 { 96 | return nil, errors.New("failed to read client args for handleSetClient") 97 | } 98 | 99 | err = json.Unmarshal(args[0], &p.clientID) 100 | if err != nil { 101 | return nil, errors.Wrap(err, "failed to decode clientID for handleSetClient") 102 | } 103 | 104 | err = json.Unmarshal(args[1], &p.clientConf) 105 | if err != nil { 106 | return nil, errors.Wrap(err, "failed to decode clientConf for handleSetClient") 107 | } 108 | 109 | return nil, nil 110 | } 111 | 112 | func (p *textinputPlugin) handleClearClient(arguments interface{}) (reply interface{}, err error) { 113 | p.clientID = 0 114 | return nil, nil 115 | } 116 | 117 | func (p *textinputPlugin) handleSetEditingState(arguments interface{}) (reply interface{}, err error) { 118 | if p.clientID == 0 { 119 | return nil, errors.New("cannot set editing state when no client is selected") 120 | } 121 | 122 | err = json.Unmarshal(arguments.(json.RawMessage), &p.ed) 123 | if err != nil { 124 | return nil, errors.Wrap(err, "failed to decode json arguments for handleSetEditingState") 125 | } 126 | 127 | p.ed.utf16Text = utf16.Encode([]rune(p.ed.Text)) 128 | utf16TextLen := len(p.ed.utf16Text) 129 | 130 | // sometimes flutter sends invalid cursor position 131 | if p.ed.SelectionBase < 0 || 132 | p.ed.SelectionExtent < 0 || 133 | p.ed.SelectionBase > utf16TextLen || 134 | p.ed.SelectionExtent > utf16TextLen { 135 | // set sane default 136 | p.ed.SelectionBase = 0 137 | p.ed.SelectionExtent = 0 138 | // request a new EditingState if text is present in the TextInput 139 | if p.ed.Text != "" { 140 | err := p.channel.InvokeMethod("TextInputClient.requestExistingInputState", nil) 141 | return nil, err 142 | } 143 | return nil, nil 144 | } 145 | 146 | return nil, nil 147 | } 148 | 149 | func (p *textinputPlugin) glfwCharCallback(w *glfw.Window, char rune) { 150 | if p.clientID == 0 { 151 | return 152 | } 153 | // Opinionated: If a flutter dev uses TextCapitalization.characters 154 | // in a TextField, that means she/he wants to receive 155 | // uppercase characters. 156 | // TODO(Drakirus): Handle language-specific case mappings such as Turkish. 157 | if p.clientConf.TextCapitalization == "TextCapitalization.characters" { 158 | char = unicode.ToUpper(char) 159 | } 160 | p.addText(char) 161 | } 162 | 163 | func (p *textinputPlugin) glfwKeyCallback(window *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { 164 | if p.backOnEscape && key == glfw.KeyEscape && action == glfw.Press { 165 | err := defaultNavigationPlugin.channel.InvokeMethod("popRoute", nil) 166 | if err != nil { 167 | fmt.Printf("go-flutter: failed to pop route after escape key press: %v\n", err) 168 | } 169 | return 170 | } 171 | 172 | if (action == glfw.Repeat || action == glfw.Press) && p.clientID != 0 { 173 | 174 | // Enter 175 | if key == glfw.KeyEnter || key == glfw.KeyKPEnter { 176 | if keyboard.DetectTextInputDoneMod(mods) { 177 | // Indicates that they are done typing in the TextInput 178 | p.performAction("TextInputAction.done") 179 | return 180 | } else if p.clientConf.InputType.Name == "TextInputType.multiline" { 181 | p.addText('\n') 182 | } 183 | // this action is described by argSetClientConf. 184 | p.performAction(p.clientConf.InputAction) 185 | } 186 | // Mapping to some text navigation shortcut that are already implemented in 187 | // the flutter framework. 188 | // Home 189 | if key == glfw.KeyHome { 190 | defaultKeyeventsPlugin.sendKeyEvent(window, glfw.KeyLeft, glfw.GetKeyScancode(glfw.KeyLeft), glfw.Press, mods|glfw.ModAlt) 191 | defaultKeyeventsPlugin.sendKeyEvent(window, glfw.KeyLeft, glfw.GetKeyScancode(glfw.KeyLeft), glfw.Release, mods|glfw.ModAlt) 192 | } 193 | // End 194 | if key == glfw.KeyEnd { 195 | defaultKeyeventsPlugin.sendKeyEvent(window, glfw.KeyRight, glfw.GetKeyScancode(glfw.KeyRight), glfw.Press, mods|glfw.ModAlt) 196 | defaultKeyeventsPlugin.sendKeyEvent(window, glfw.KeyRight, glfw.GetKeyScancode(glfw.KeyRight), glfw.Release, mods|glfw.ModAlt) 197 | } 198 | 199 | } 200 | } 201 | 202 | func (p *textinputPlugin) addText(text rune) { 203 | p.removeSelectedText() 204 | utf16text := utf16.Encode([]rune{text}) 205 | utf16TextLen := len(p.ed.utf16Text) + len(utf16text) 206 | newText := make([]uint16, 0, utf16TextLen) 207 | newText = append(newText, p.ed.utf16Text[:p.ed.SelectionBase]...) 208 | newText = append(newText, utf16text...) 209 | newText = append(newText, p.ed.utf16Text[p.ed.SelectionBase:]...) 210 | p.ed.utf16Text = newText 211 | 212 | p.ed.SelectionBase++ 213 | p.ed.SelectionExtent = p.ed.SelectionBase 214 | p.updateEditingState() 215 | } 216 | 217 | // UpupdateEditingState updates the TextInput with the current state by invoking 218 | // TextInputClient.updateEditingState in the flutter framework 219 | func (p *textinputPlugin) updateEditingState() { 220 | p.ed.Text = string(utf16.Decode(p.ed.utf16Text)) 221 | arguments := []interface{}{ 222 | p.clientID, 223 | p.ed, 224 | } 225 | p.channel.InvokeMethod("TextInputClient.updateEditingState", arguments) 226 | } 227 | 228 | // performAction invokes the TextInputClient performAction method in the flutter 229 | // framework 230 | func (p *textinputPlugin) performAction(action string) { 231 | p.channel.InvokeMethod("TextInputClient.performAction", []interface{}{ 232 | p.clientID, 233 | action, 234 | }) 235 | } 236 | 237 | // performClientAction invokes the TextInputClient performAction of the 238 | // TextInputAction. The action is described by argSetClientConf. 239 | func (p *textinputPlugin) performTextInputAction() { 240 | p.performAction(p.clientConf.InputAction) 241 | } 242 | 243 | // removeSelectedText do nothing if no text is selected return true if the 244 | // state needs to updated 245 | func (p *textinputPlugin) removeSelectedText() bool { 246 | selectionIndexStart, selectionIndexEnd := p.getSelectedText() 247 | if selectionIndexStart != selectionIndexEnd { 248 | p.ed.utf16Text = append(p.ed.utf16Text[:selectionIndexStart], p.ed.utf16Text[selectionIndexEnd:]...) 249 | p.ed.SelectionBase = selectionIndexStart 250 | p.ed.SelectionExtent = selectionIndexStart 251 | return true 252 | } 253 | return false 254 | 255 | } 256 | 257 | // getSelectedText return a tuple containing: (left index of the selection, right index of the 258 | // selection, the content of the selection) 259 | func (p *textinputPlugin) getSelectedText() (int, int) { 260 | selectionIndex := []int{p.ed.SelectionBase, p.ed.SelectionExtent} 261 | sort.Ints(selectionIndex) 262 | return selectionIndex[0], 263 | selectionIndex[1] 264 | } 265 | -------------------------------------------------------------------------------- /texture-registry.go: -------------------------------------------------------------------------------- 1 | package flutter 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/go-flutter-desktop/go-flutter/embedder" 8 | "github.com/go-flutter-desktop/go-flutter/internal/opengl" 9 | "github.com/go-flutter-desktop/go-flutter/internal/tasker" 10 | "github.com/go-gl/glfw/v3.3/glfw" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // once is used for the lazy initialization of go-gl/gl. 15 | // The initialization occur on the first requested texture's frame. 16 | var once sync.Once 17 | 18 | // TextureRegistry is a registry entry for a managed Texture. 19 | type TextureRegistry struct { 20 | window *glfw.Window 21 | engine *embedder.FlutterEngine 22 | channels map[int64]*externalTextureHanlder 23 | channelsLock sync.RWMutex 24 | 25 | // engineTasker holds tasks which must be executed in the engine thread 26 | engineTasker *tasker.Tasker 27 | 28 | texture int64 29 | texturesLock sync.Mutex 30 | } 31 | 32 | type externalTextureHanlder struct { 33 | // handle is called when flutter needs the PixelBuffer 34 | handle ExternalTextureHanlderFunc 35 | // gl texture to refer to for this handler 36 | texture uint32 37 | } 38 | 39 | func newTextureRegistry(engine *embedder.FlutterEngine, window *glfw.Window) *TextureRegistry { 40 | return &TextureRegistry{ 41 | window: window, 42 | engine: engine, 43 | channels: make(map[int64]*externalTextureHanlder), 44 | engineTasker: tasker.New(), 45 | } 46 | } 47 | 48 | // init must happen in engine thread 49 | func (t *TextureRegistry) init() error { 50 | t.window.MakeContextCurrent() 51 | // Important! Call open.Init only under the presence of an active OpenGL context, 52 | // i.e., after MakeContextCurrent. 53 | if err := opengl.Init(); err != nil { 54 | return errors.Wrap(err, "TextureRegistry gl init failed") 55 | } 56 | return nil 57 | } 58 | 59 | // NewTexture creates a new Texture 60 | func (t *TextureRegistry) NewTexture() Texture { 61 | t.texturesLock.Lock() 62 | defer t.texturesLock.Unlock() 63 | t.texture++ 64 | return Texture{ID: t.texture, registry: t} 65 | } 66 | 67 | // ExternalTextureHanlderFunc describes the function that handles external 68 | // Texture on a given ID. 69 | type ExternalTextureHanlderFunc func(width int, height int) (bool, *PixelBuffer) 70 | 71 | // PixelBuffer is an in-memory (RGBA) image. 72 | type PixelBuffer struct { 73 | // Pix holds the image's pixels, in R, G, B, A order. 74 | Pix []uint8 75 | // Width and Height of the image's bounds 76 | Width, Height int 77 | } 78 | 79 | // setTextureHandler registers a handler to be invoked when the Flutter 80 | // application want to get a PixelBuffer to draw into the scene. 81 | // 82 | // Registration overwrites any previous registration for the same textureID 83 | // name. Use nil as handler to deregister. 84 | func (t *TextureRegistry) setTextureHandler(textureID int64, handler ExternalTextureHanlderFunc) { 85 | t.channelsLock.Lock() 86 | if handler == nil { 87 | texture := t.channels[textureID] 88 | if texture != nil { 89 | t.engineTasker.Do(func() { 90 | // Must run on the main tread 91 | opengl.DeleteTextures(1, &texture.texture) 92 | }) 93 | } 94 | delete(t.channels, textureID) 95 | } else { 96 | t.channels[textureID] = &externalTextureHanlder{ 97 | handle: handler, 98 | } 99 | } 100 | t.channelsLock.Unlock() 101 | } 102 | 103 | // handleExternalTexture receive low level C calls to create and/or update the 104 | // content of a OpenGL TexImage2D. 105 | // Calls must happen on the engine thread, no need to use engineTasker as this 106 | // function is a callback directly managed by the engine. 107 | func (t *TextureRegistry) handleExternalTexture(textureID int64, 108 | width int, height int) *embedder.FlutterOpenGLTexture { 109 | 110 | once.Do(func() { 111 | t.init() 112 | }) 113 | 114 | t.channelsLock.RLock() 115 | registration, registrationExists := t.channels[textureID] 116 | t.channelsLock.RUnlock() 117 | 118 | if !registrationExists { 119 | fmt.Printf("go-flutter: no texture handler found for Texture ID: %v\n", textureID) 120 | return nil 121 | } 122 | res, pixelBuffer := registration.handle(width, height) 123 | if !res || pixelBuffer == nil { 124 | return nil 125 | } 126 | 127 | if len(pixelBuffer.Pix) == 0 { 128 | return nil 129 | } 130 | 131 | t.window.MakeContextCurrent() 132 | 133 | if registration.texture == 0 { 134 | opengl.CreateTexture(®istration.texture) 135 | } 136 | 137 | opengl.BindTexture(registration.texture) 138 | 139 | opengl.TexImage2D( 140 | int32(pixelBuffer.Width), 141 | int32(pixelBuffer.Height), 142 | opengl.Ptr(pixelBuffer.Pix), 143 | ) 144 | 145 | return &embedder.FlutterOpenGLTexture{ 146 | Target: opengl.TEXTURE2D, 147 | Name: registration.texture, 148 | Format: opengl.RGBA8, 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /texture.go: -------------------------------------------------------------------------------- 1 | package flutter 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | // Texture is an identifier for texture declaration 8 | type Texture struct { 9 | ID int64 10 | registry *TextureRegistry 11 | } 12 | 13 | // Register registers a textureID with his associated handler 14 | func (t *Texture) Register(handler ExternalTextureHanlderFunc) error { 15 | t.registry.setTextureHandler(t.ID, handler) 16 | err := t.registry.engine.RegisterExternalTexture(t.ID) 17 | if err != nil { 18 | t.registry.setTextureHandler(t.ID, nil) 19 | return errors.Errorf("'go-flutter' couldn't register texture with id: '%v': %v", t.ID, err) 20 | } 21 | return nil 22 | } 23 | 24 | // FrameAvailable mark a texture buffer is ready to be draw in the flutter scene 25 | func (t *Texture) FrameAvailable() error { 26 | err := t.registry.engine.MarkExternalTextureFrameAvailable(t.ID) 27 | if err != nil { 28 | return errors.Errorf("'go-flutter' couldn't mark frame available of texture with id: '%v': %v", t.ID, err) 29 | } 30 | return nil 31 | } 32 | 33 | // UnRegister unregisters a textureID with his associated handler 34 | func (t *Texture) UnRegister() error { 35 | err := t.registry.engine.UnregisterExternalTexture(t.ID) 36 | if err != nil { 37 | return errors.Errorf("'go-flutter' couldn't unregisters texture with id: '%v': %v", t.ID, err) 38 | } 39 | t.registry.setTextureHandler(t.ID, nil) 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /window.go: -------------------------------------------------------------------------------- 1 | package flutter 2 | 3 | // windowMode determines the kind of window mode to use for new windows. 4 | type windowMode int 5 | 6 | const ( 7 | // WindowModeDefault is the default window mode. Windows are created with 8 | // borders and close/minimize buttons. 9 | WindowModeDefault windowMode = iota 10 | // WindowModeBorderless removes decorations such as borders and 11 | // close/minimize buttons from the window. 12 | WindowModeBorderless 13 | // WindowModeBorderlessFullscreen starts the application in borderless 14 | // fullscreen mode. Currently, only fullscreen on the primary monitor is 15 | // supported. This option overrides WindowInitialDimensions. Note that on 16 | // some systems a fullscreen window is very hard to close. Make sure your 17 | // Flutter application has a close button and use PopBehaviorIconify to 18 | // minimize or PopBehaviorClose to close the application. 19 | WindowModeBorderlessFullscreen 20 | // WindowModeMaximize starts the application maximized. 21 | WindowModeMaximize 22 | // WindowModeBorderlessMaximize starts the application in borderless 23 | // maximize mode. 24 | WindowModeBorderlessMaximize 25 | ) 26 | 27 | // WindowMode sets the window mode on the application. 28 | func WindowMode(w windowMode) Option { 29 | return func(c *config) { 30 | c.windowMode = w 31 | } 32 | } 33 | --------------------------------------------------------------------------------