{"id":13543128,"url":"https://github.com/faiface/gui","last_synced_at":"2025-04-05T09:06:53.172Z","repository":{"id":45512284,"uuid":"100732025","full_name":"faiface/gui","owner":"faiface","description":"Super minimal, rock-solid foundation for concurrent GUI in Go.","archived":false,"fork":false,"pushed_at":"2020-11-16T21:53:59.000Z","size":2013,"stargazers_count":487,"open_issues_count":7,"forks_count":35,"subscribers_count":23,"default_branch":"master","last_synced_at":"2025-03-29T08:06:44.120Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/faiface.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-08-18T16:43:25.000Z","updated_at":"2025-03-08T05:31:21.000Z","dependencies_parsed_at":"2022-09-11T04:00:52.505Z","dependency_job_id":null,"html_url":"https://github.com/faiface/gui","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/faiface%2Fgui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/faiface%2Fgui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/faiface%2Fgui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/faiface%2Fgui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/faiface","download_url":"https://codeload.github.com/faiface/gui/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247312077,"owners_count":20918344,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T11:00:23.694Z","updated_at":"2025-04-05T09:06:53.144Z","avatar_url":"https://github.com/faiface.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# faiface/gui [![GoDoc](https://godoc.org/github.com/faiface/gui?status.svg)](https://godoc.org/github.com/faiface/gui) [![Discord](https://img.shields.io/badge/chat-on%20discord-9cf.svg)](https://discord.gg/T5YAAT2)\n\nSuper minimal, rock-solid foundation for concurrent GUI in Go.\n\n## Installation\n\n```\ngo get -u github.com/faiface/gui\n```\n\nCurrently uses [GLFW](https://www.glfw.org/) under the hood, so have [these dependencies](https://github.com/go-gl/glfw#installation).\n\n## Why concurrent GUI?\n\nGUI is concurrent by nature. Elements like buttons, text fields, or canvases are conceptually independent. Conventional GUI frameworks solve this by implementing huge architectures: the event\nloop, call-backs, tickers, you name it.\n\nIn a concurrent GUI, the story is different. Each element is actually handled by its own goroutine,\nor event multiple ones. Elements communicate with each other via channels.\n\nThis has several advantages:\n\n- Make a new element at any time just by spawning a goroutine.\n- Implement animations using simple for-loops.\n- An intenstive computation in one element won't block the whole app.\n- Enables decentralized design - since elements communicate via channels, multiple communications\n  may be going on at once, without any central entity.\n\n## Examples\n\n| [Image Viewer](examples/imageviewer) | [Paint](examples/paint) | [Pexeso](examples/pexeso) |\n| --- | --- | --- |\n| ![Image Viewer Screenshot](examples/imageviewer/screenshot.png) | ![Paint Screenshot](examples/paint/screenshot.png) | ![Pexeso Screenshot](examples/pexeso/screenshot.png) |\n\n## What needs getting done?\n\nThis package is solid, but not complete. Here are some of the things that I'd love to get done with your help:\n\n- Get rid of the C dependencies.\n- Support multiple windows.\n- Mobile support.\n- A widgets/layout package.\n\nContributions are highly welcome!\n\n## Overview\n\nThe idea of concurrent GUI pre-dates Go and is found in another language by Rob Pike called Newsqueak. He explains it quite nicely in [this talk](https://www.youtube.com/watch?v=hB05UFqOtFA\u0026t=2408s). Newsqueak was similar to Go, mostly in that it had channels.\n\nWhy the hell has no one made a concurrent GUI in Go yet? I have no idea. Go is a perfect language for such a thing. Let's change that!\n\n**This package is a minimal foundation for a concurrent GUI in Go.** It doesn't include widgets, layout systems, or anything like that. The main reason is that I am not yet sure how to do them most correctly. So, instead of providing a half-assed, \"fully-featured\" library, I decided to make a small, rock-solid package, where everything is right.\n\n**So, how does this work?**\n\nThe main idea is that different components of the GUI (buttons, text fields, ...) run concurrently and communicate using channels. Furthermore, they receive events from an object called _environment_ and can draw by sending draw commands to it.\n\nHere's [`Env`](https://godoc.org/github.com/faiface/gui#Env0), short for environment:\n\n```go\ntype Env interface {\n\tEvents() \u003c-chan Event\n\tDraw() chan\u003c- func(draw.Image) image.Rectangle\n}\n```\n\nIt's something that produces events (such as mouse clicks and key presses) and accepts draw commands.\n\nClosing the `Draw()` channel destroys the environment. When destroyed (either by closing the `Draw()` channel or by any other reason), the environment will always close the `Events()` channel.\n\nAs you can see, a draw command is a function that draws something onto a [`draw.Image`](https://golang.org/pkg/image/draw/#Image) and returns a rectangle telling which part got changed.\n\nIf you're not familiar with the `\"image\"` and the `\"image/draw\"` packages, go read [this short entry in the Go blog](https://blog.golang.org/go-imagedraw-package).\n\n![Draw](images/draw.png)\n\nYes, `faiface/gui` uses CPU for drawing. You won't make AAA games with it, but the performance is enough for most GUI apps. The benefits are outstanding, though:\n\n1. Drawing is as simple as changing pixels.\n2. No FPS (frames per second), results are immediately on the screen.\n3. No need to organize the API around a GPU library, like OpenGL.\n4. Use all the good packages, like [`\"image\"`](https://golang.org/pkg/image/), [`\"image/draw\"`](https://golang.org/pkg/image/draw/), [`\"golang.org/x/image/font\"`](https://godoc.org/golang.org/x/image/font) for fonts, or [`\"github.com/fogleman/gg\"`](https://godoc.org/github.com/fogleman/gg) for shapes.\n\nWhat is an [`Event`](https://godoc.org/github.com/faiface/gui#Event)? It's an interface:\n\n```go\ntype Event interface {\n\tString() string\n}\n```\n\nThis purpose of this interface is to hold different kinds of events and be able to discriminate among them using a type switch.\n\nExamples of concrete `Event` types are: [`gui.Resize`](https://godoc.org/github.com/faiface/gui#Resize), [`win.WiClose`](https://godoc.org/github.com/faiface/gui/win#WiClose), [`win.MoDown`](https://godoc.org/github.com/faiface/gui/win#MoDown), [`win.KbType`](https://godoc.org/github.com/faiface/gui/win#KbType) (where `Wi`, `Mo`, and `Kb` stand for _window_, _mouse_, and _keyboard_, respectively). When we have an `Event`, we can type switch on it like this:\n\n```go\nswitch event := event.(type) {\ncase gui.Resize:\n    // environment resized to event.Rectangle\ncase win.WiClose:\n    // window closed\ncase win.MoMove:\n    // mouse moved to event.Point\ncase win.MoDown:\n    // mouse button event.Button pressed on event.Point\ncase win.MoUp:\n\t// mouse button event.Button released on event.Point\ncase win.MoScroll:\n\t// mouse scrolled by event.Point\ncase win.KbType:\n    // rune event.Rune typed on the keyboard\ncase win.KbDown:\n    // keyboard key event.Key pressed on the keyboard\ncase win.KbUp:\n    // keyboard key event.Key released on the keyboard\ncase win.KbRepeat:\n    // keyboard key event.Key repeated on the keyboard (happens when held)\n}\n```\n\nThis shows all the possible events that a window can produce.\n\nThe [`gui.Resize`](https://godoc.org/github.com/faiface/gui#Resize) event is not from the package [`win`](https://godoc.org/github.com/faiface/gui/win) because it's not window specific. In fact, every `Env` guarantees to produce `gui.Resize` as its first event.\n\nHow do we create a window? With the [`\"github.com/faiface/gui/win\"`](https://godoc.org/github.com/faiface/gui/win) package:\n\n```go\n// import \"github.com/faiface/gui/win\"\nw, err := win.New(win.Title(\"faiface/win\"), win.Size(800, 600), win.Resizable())\n```\n\nThe [`win.New`](https://godoc.org/github.com/faiface/gui/win#New) constructor uses the [functional options pattern](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) by Dave Cheney. Unsurprisingly, the returned [`*win.Win`](https://godoc.org/github.com/faiface/gui/win#Win) is an `Env`.\n\nDue to stupid limitations imposed by operating systems, the internal code that fetches events from the OS must run on the main thread of the program. To ensure this, we need to call [`mainthread.Run`](https://godoc.org/github.com/faiface/mainthread#Run) in the `main` function:\n\n```go\nimport \"github.com/faiface/mainthread\"\n\nfunc run() {\n    // do everything here, this becomes the new main function\n}\n\nfunc main() {\n    mainthread.Run(run)\n}\n```\n\nHow does it all look together? Here's a simple program that displays a nice, big rectangle in the middle of the window:\n\n```go\npackage main\n\nimport (\n\t\"image\"\n\t\"image/draw\"\n\n\t\"github.com/faiface/gui/win\"\n\t\"github.com/faiface/mainthread\"\n)\n\nfunc run() {\n\tw, err := win.New(win.Title(\"faiface/gui\"), win.Size(800, 600))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tw.Draw() \u003c- func(drw draw.Image) image.Rectangle {\n\t\tr := image.Rect(200, 200, 600, 400)\n\t\tdraw.Draw(drw, r, image.White, image.ZP, draw.Src)\n\t\treturn r\n\t}\n\n\tfor event := range w.Events() {\n\t\tswitch event.(type) {\n\t\tcase win.WiClose:\n\t\t\tclose(w.Draw())\n\t\t}\n\t}\n}\n\nfunc main() {\n\tmainthread.Run(run)\n}\n```\n\n### Muxing\n\nWhen you receive an event from the `Events()` channel, it gets removed from the channel and no one else can receive it. But what if you have a button, a text field, four switches, and a bunch of other things that all want to receive the same events?\n\nThat's where multiplexing, or muxing comes in.\n\n![Mux](images/mux.png)\n\nA [`Mux`](https://godoc.org/github.com/faiface/gui#Mux) basically lets you split a single `Env` into multiple ones.\n\nWhen the original `Env` produces an event, `Mux` sends it to each one of the multiple `Env`s.\n\nWhen any one of the multiple `Env`s receives a draw function, `Mux` sends it to the original `Env`.\n\nTo mux an `Env`, use [`gui.NewMux`](https://godoc.org/github.com/faiface/gui#NewMux):\n\n```go\nmux, env := gui.NewMux(w)\n```\n\nHere we muxed the window `Env` stored in the `w` variable.\n\nWhat's that second return value? That's the _master `Env`_. It's the first environment that the mux creates for us. It has a special role: if you close its `Draw()` channel, you close the `Mux`, all other `Env`s created by the `Mux`, and the original `Env`. But other than that, it's just like any other `Env` created by the `Mux`.\n\nDon't use the original `Env` after muxing it. The `Mux` is using it and you'll steal its events at best.\n\nTo create more `Env`s, we can use [`mux.MakeEnv()`](https://godoc.org/github.com/faiface/gui#Mux.MakeEnv):\n\nFor example, here's a simple program that shows four white rectangles on the screen. Whenever the user clicks on any of them, the rectangle blinks (switches between white and black) 3 times. We use `Mux` to send events to all of the rectangles independently:\n\n```go\npackage main\n\nimport (\n\t\"image\"\n\t\"image/draw\"\n\t\"time\"\n\n\t\"github.com/faiface/gui\"\n\t\"github.com/faiface/gui/win\"\n\t\"github.com/faiface/mainthread\"\n)\n\nfunc Blinker(env gui.Env, r image.Rectangle) {\n\t// redraw takes a bool and produces a draw command\n\tredraw := func(visible bool) func(draw.Image) image.Rectangle {\n\t\treturn func(drw draw.Image) image.Rectangle {\n\t\t\tif visible {\n\t\t\t\tdraw.Draw(drw, r, image.White, image.ZP, draw.Src)\n\t\t\t} else {\n\t\t\t\tdraw.Draw(drw, r, image.Black, image.ZP, draw.Src)\n\t\t\t}\n\t\t\treturn r\n\t\t}\n\t}\n\n\t// first we draw a white rectangle\n\tenv.Draw() \u003c- redraw(true)\n\n\tfor event := range env.Events() {\n\t\tswitch event := event.(type) {\n\t\tcase win.MoDown:\n\t\t\tif event.Point.In(r) {\n\t\t\t\t// user clicked on the rectangle\n\t\t\t\t// we blink 3 times\n\t\t\t\tfor i := 0; i \u003c 3; i++ {\n\t\t\t\t\tenv.Draw() \u003c- redraw(false)\n\t\t\t\t\ttime.Sleep(time.Second / 3)\n\t\t\t\t\tenv.Draw() \u003c- redraw(true)\n\t\t\t\t\ttime.Sleep(time.Second / 3)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tclose(env.Draw())\n}\n\nfunc run() {\n\tw, err := win.New(win.Title(\"faiface/gui\"), win.Size(800, 600))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tmux, env := gui.NewMux(w)\n\n\t// we create four blinkers, each with its own Env from the mux\n\tgo Blinker(mux.MakeEnv(), image.Rect(100, 100, 350, 250))\n\tgo Blinker(mux.MakeEnv(), image.Rect(450, 100, 700, 250))\n\tgo Blinker(mux.MakeEnv(), image.Rect(100, 350, 350, 500))\n\tgo Blinker(mux.MakeEnv(), image.Rect(450, 350, 700, 500))\n\n\t// we use the master env now, w is used by the mux\n\tfor event := range env.Events() {\n\t\tswitch event.(type) {\n\t\tcase win.WiClose:\n\t\t\tclose(env.Draw())\n\t\t}\n\t}\n}\n\nfunc main() {\n\tmainthread.Run(run)\n}\n```\n\nJust for the info, closing the `Draw()` channel on an `Env` created by `mux.MakeEnv()` removes the `Env` from the `Mux`.\n\nWhat if one of the `Env`s hangs and stops consuming events, or if it simply takes longer to consume them? Will all the other `Env`s hang as well?\n\nThey won't, because the channels of events have unlimited capacity and never block. This is implemented using an intermediate goroutine that handles the queueing.\n\n![Events](images/events.png)\n\nAnd that's basically all you need to know about `faiface/gui`! Happy hacking!\n\n## A note on race conditions\n\nThere is no guarantee when a function sent to the `Draw()` channel will be executed, or if at all. Look at this code:\n\n```go\npressed := false\n\nenv.Draw() \u003c- func(drw draw.Image) image.Rectangle {\n\t// use pressed somehow\n}\n\n// change pressed somewhere here\n```\n\nThe code above has a danger of a race condition. The code that changes the `pressed` variable and the code that uses it may run concurrently.\n\n**My advice is to never enclose a shared variable in a drawing function.**\n\nInstead, you can do this:\n\n```go\nredraw := func(pressed bool) func(draw.Image) image.Rectangle {\n\treturn func(drw draw.Image) image.Rectangle {\n\t\t// use the pressed argument\n\t}\n}\n\npressed := false\n\nenv.Draw() \u003c- redraw(pressed)\n\n// changing pressed here doesn't cause race conditions\n```\n\n## Credit\n\nThe cutest ever pictures are all drawn by Tori Bane! You can see more of her drawings and follow her on [Instagram here](https://www.instagram.com/teplomilka/).\n\n## Licence\n\n[MIT](LICENCE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffaiface%2Fgui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffaiface%2Fgui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffaiface%2Fgui/lists"}