aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--focus.go58
-rw-r--r--go.mod20
-rw-r--r--go.sum43
-rw-r--r--gui/LICENSE21
-rw-r--r--gui/README.md342
-rw-r--r--gui/env.go33
-rw-r--r--gui/event.go70
-rw-r--r--gui/layout/doc.go10
-rw-r--r--gui/layout/grid.go115
-rw-r--r--gui/layout/intercepter.go43
-rw-r--r--gui/layout/layout.go20
-rw-r--r--gui/layout/mux.go172
-rw-r--r--gui/layout/scroller.go132
-rw-r--r--gui/layout/split.go26
-rw-r--r--gui/mux.go134
-rw-r--r--gui/widget/concurrent_face.go51
-rw-r--r--gui/widget/text.go78
-rw-r--r--gui/widget/widget.go130
-rw-r--r--gui/win/events.go93
-rw-r--r--gui/win/win.go362
-rw-r--r--main.go252
-rw-r--r--ui.go371
22 files changed, 2020 insertions, 556 deletions
diff --git a/focus.go b/focus.go
new file mode 100644
index 0000000..331d232
--- /dev/null
+++ b/focus.go
@@ -0,0 +1,58 @@
+package main
+
+type Focus struct {
+ widgets [][]chan bool
+ p Point // currently focused widget
+}
+
+func NewFocus(rows []int) Focus {
+ f := Focus{
+ make([][]chan bool, len(rows)),
+ Point{},
+ }
+ for i := range f.widgets {
+ f.widgets[i] = make([]chan bool, rows[i])
+ for j := range f.widgets[i] {
+ f.widgets[i][j] = make(chan bool)
+ }
+ }
+ return f
+}
+
+func (f *Focus) Left() {
+ f.widgets[f.p.Y][f.p.X] <- false
+ if f.p.X <= 0 {
+ f.p.X = len(f.widgets[f.p.Y]) - 1
+ } else {
+ f.p.X--
+ }
+ f.widgets[f.p.Y][f.p.X] <- true
+}
+
+func (f *Focus) Right() {
+ f.widgets[f.p.Y][f.p.X] <- false
+ f.p.X = (f.p.X + 1) % len(f.widgets[f.p.Y])
+ f.widgets[f.p.Y][f.p.X] <- true
+}
+
+func (f *Focus) Up() {
+ f.widgets[f.p.Y][f.p.X] <- false
+ if f.p.Y <= 0 {
+ f.p.Y = len(f.widgets) - 1
+ } else {
+ f.p.Y--
+ }
+ f.p.X = min(f.p.X, len(f.widgets[f.p.Y])-1)
+ f.widgets[f.p.Y][f.p.X] <- true
+}
+
+func (f *Focus) Down() {
+ f.widgets[f.p.Y][f.p.X] <- false
+ f.p.Y = (f.p.Y + 1) % len(f.widgets)
+ f.p.X = min(f.p.X, len(f.widgets[f.p.Y])-1)
+ f.widgets[f.p.Y][f.p.X] <- true
+}
+
+type Point struct {
+ X, Y int
+}
diff --git a/go.mod b/go.mod
index 3eb3590..25fa890 100644
--- a/go.mod
+++ b/go.mod
@@ -1,21 +1,13 @@
module volute
-go 1.18
+go 1.21
require (
- github.com/AllenDang/giu v0.6.2
github.com/BurntSushi/toml v1.1.0
+ github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3
+ github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71
+ github.com/go-gl/glfw v0.0.0-20240108052320-294b0144ba39
+ golang.org/x/image v0.15.0
)
-require (
- github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8 // indirect
- github.com/AllenDang/imgui-go v1.12.1-0.20220322114136-499bbf6a42ad // indirect
- github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 // indirect
- github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
- github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 // indirect
- github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
- github.com/sahilm/fuzzy v0.1.0 // indirect
- golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
- golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 // indirect
- gopkg.in/eapache/queue.v1 v1.1.0 // indirect
-)
+require golang.org/x/text v0.14.0 // indirect
diff --git a/go.sum b/go.sum
index 074e02e..523443f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,39 +1,12 @@
-github.com/AllenDang/giu v0.6.2 h1:CFIHSQxDqEFNsNnTO9LXBVZ8zlInV71H3M6V3BNagmI=
-github.com/AllenDang/giu v0.6.2/go.mod h1:9hCQh0l0wbBzOqe9cr02EB9EsNOy9AwFIjG4xVsR6TI=
-github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8 h1:dKZMqib/yUDoCFigmz2agG8geZ/e3iRq304/KJXqKyw=
-github.com/AllenDang/go-findfont v0.0.0-20200702051237-9f180485aeb8/go.mod h1:b4uuDd0s6KRIPa84cEEchdQ9ICh7K0OryZHbSzMca9k=
-github.com/AllenDang/imgui-go v1.12.1-0.20220322114136-499bbf6a42ad h1:Kr961C2uEEAklK+jBRiZVnQH0AgS7o6pXrIgUTUUGiM=
-github.com/AllenDang/imgui-go v1.12.1-0.20220322114136-499bbf6a42ad/go.mod h1:kuPs9RWleaUuK7D49bE6HPxyRA36Lp4ICKGp+5OnnbY=
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q=
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M=
-github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
-github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 h1:TL70PMkdPCt9cRhKTqsm+giRpgrd0IGEj763nNr2VFY=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
-github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
-github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
-github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
-golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4=
-golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
-golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc=
-golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/eapache/queue.v1 v1.1.0 h1:EldqoJEGtXYiVCMRo2C9mePO2UUGnYn2+qLmlQSqPdc=
-gopkg.in/eapache/queue.v1 v1.1.0/go.mod h1:wNtmx1/O7kZSR9zNT1TTOJ7GLpm3Vn7srzlfylFbQwU=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
+github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
+github.com/go-gl/glfw v0.0.0-20240108052320-294b0144ba39 h1:NQ/PsAvvBcCUiFODaU3tmnKesYruVJL+Dx5hTRrr+DA=
+github.com/go-gl/glfw v0.0.0-20240108052320-294b0144ba39/go.mod h1:wyvWpaEu9B/VQiV1jsPs7Mha9I7yto/HqIBw197ZAzk=
+golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
+golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
diff --git a/gui/LICENSE b/gui/LICENSE
new file mode 100644
index 0000000..b497e83
--- /dev/null
+++ b/gui/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Michal Štrba
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/gui/README.md b/gui/README.md
new file mode 100644
index 0000000..7e83229
--- /dev/null
+++ b/gui/README.md
@@ -0,0 +1,342 @@
+# 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)
+
+Super minimal, rock-solid foundation for concurrent GUI in Go.
+
+## Installation
+
+```
+go get -u github.com/faiface/gui
+```
+
+Currently uses [GLFW](https://www.glfw.org/) under the hood, so have [these dependencies](https://github.com/go-gl/glfw#installation).
+
+## Why concurrent GUI?
+
+GUI 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
+loop, call-backs, tickers, you name it.
+
+In a concurrent GUI, the story is different. Each element is actually handled by its own goroutine,
+or event multiple ones. Elements communicate with each other via channels.
+
+This has several advantages:
+
+- Make a new element at any time just by spawning a goroutine.
+- Implement animations using simple for-loops.
+- An intenstive computation in one element won't block the whole app.
+- Enables decentralized design - since elements communicate via channels, multiple communications
+ may be going on at once, without any central entity.
+
+## Examples
+
+| [Image Viewer](examples/imageviewer) | [Paint](examples/paint) | [Pexeso](examples/pexeso) |
+| --- | --- | --- |
+| ![Image Viewer Screenshot](examples/imageviewer/screenshot.png) | ![Paint Screenshot](examples/paint/screenshot.png) | ![Pexeso Screenshot](examples/pexeso/screenshot.png) |
+
+## What needs getting done?
+
+This package is solid, but not complete. Here are some of the things that I'd love to get done with your help:
+
+- Get rid of the C dependencies.
+- Support multiple windows.
+- Mobile support.
+- A widgets/layout package.
+
+Contributions are highly welcome!
+
+## Overview
+
+The 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&t=2408s). Newsqueak was similar to Go, mostly in that it had channels.
+
+Why 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!
+
+**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.
+
+**So, how does this work?**
+
+The 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.
+
+Here's [`Env`](https://godoc.org/github.com/faiface/gui#Env0), short for environment:
+
+```go
+type Env interface {
+ Events() <-chan Event
+ Draw() chan<- func(draw.Image) image.Rectangle
+}
+```
+
+It's something that produces events (such as mouse clicks and key presses) and accepts draw commands.
+
+Closing 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.
+
+As 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.
+
+If 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).
+
+![Draw](images/draw.png)
+
+Yes, `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:
+
+1. Drawing is as simple as changing pixels.
+2. No FPS (frames per second), results are immediately on the screen.
+3. No need to organize the API around a GPU library, like OpenGL.
+4. 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.
+
+What is an [`Event`](https://godoc.org/github.com/faiface/gui#Event)? It's an interface:
+
+```go
+type Event interface {
+ String() string
+}
+```
+
+This purpose of this interface is to hold different kinds of events and be able to discriminate among them using a type switch.
+
+Examples 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:
+
+```go
+switch event := event.(type) {
+case gui.Resize:
+ // environment resized to event.Rectangle
+case win.WiClose:
+ // window closed
+case win.MoMove:
+ // mouse moved to event.Point
+case win.MoDown:
+ // mouse button event.Button pressed on event.Point
+case win.MoUp:
+ // mouse button event.Button released on event.Point
+case win.MoScroll:
+ // mouse scrolled by event.Point
+case win.KbType:
+ // rune event.Rune typed on the keyboard
+case win.KbDown:
+ // keyboard key event.Key pressed on the keyboard
+case win.KbUp:
+ // keyboard key event.Key released on the keyboard
+case win.KbRepeat:
+ // keyboard key event.Key repeated on the keyboard (happens when held)
+}
+```
+
+This shows all the possible events that a window can produce.
+
+The [`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.
+
+How do we create a window? With the [`"github.com/faiface/gui/win"`](https://godoc.org/github.com/faiface/gui/win) package:
+
+```go
+// import "github.com/faiface/gui/win"
+w, err := win.New(win.Title("faiface/win"), win.Size(800, 600), win.Resizable())
+```
+
+The [`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`.
+
+Due 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:
+
+```go
+import "github.com/faiface/mainthread"
+
+func run() {
+ // do everything here, this becomes the new main function
+}
+
+func main() {
+ mainthread.Run(run)
+}
+```
+
+How does it all look together? Here's a simple program that displays a nice, big rectangle in the middle of the window:
+
+```go
+package main
+
+import (
+ "image"
+ "image/draw"
+
+ "github.com/faiface/gui/win"
+ "github.com/faiface/mainthread"
+)
+
+func run() {
+ w, err := win.New(win.Title("faiface/gui"), win.Size(800, 600))
+ if err != nil {
+ panic(err)
+ }
+
+ w.Draw() <- func(drw draw.Image) image.Rectangle {
+ r := image.Rect(200, 200, 600, 400)
+ draw.Draw(drw, r, image.White, image.ZP, draw.Src)
+ return r
+ }
+
+ for event := range w.Events() {
+ switch event.(type) {
+ case win.WiClose:
+ close(w.Draw())
+ }
+ }
+}
+
+func main() {
+ mainthread.Run(run)
+}
+```
+
+### Muxing
+
+When 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?
+
+That's where multiplexing, or muxing comes in.
+
+![Mux](images/mux.png)
+
+A [`Mux`](https://godoc.org/github.com/faiface/gui#Mux) basically lets you split a single `Env` into multiple ones.
+
+When the original `Env` produces an event, `Mux` sends it to each one of the multiple `Env`s.
+
+When any one of the multiple `Env`s receives a draw function, `Mux` sends it to the original `Env`.
+
+To mux an `Env`, use [`gui.NewMux`](https://godoc.org/github.com/faiface/gui#NewMux):
+
+```go
+mux, env := gui.NewMux(w)
+```
+
+Here we muxed the window `Env` stored in the `w` variable.
+
+What'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`.
+
+Don't use the original `Env` after muxing it. The `Mux` is using it and you'll steal its events at best.
+
+To create more `Env`s, we can use [`mux.MakeEnv()`](https://godoc.org/github.com/faiface/gui#Mux.MakeEnv):
+
+For 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:
+
+```go
+package main
+
+import (
+ "image"
+ "image/draw"
+ "time"
+
+ "github.com/faiface/gui"
+ "github.com/faiface/gui/win"
+ "github.com/faiface/mainthread"
+)
+
+func Blinker(env gui.Env, r image.Rectangle) {
+ // redraw takes a bool and produces a draw command
+ redraw := func(visible bool) func(draw.Image) image.Rectangle {
+ return func(drw draw.Image) image.Rectangle {
+ if visible {
+ draw.Draw(drw, r, image.White, image.ZP, draw.Src)
+ } else {
+ draw.Draw(drw, r, image.Black, image.ZP, draw.Src)
+ }
+ return r
+ }
+ }
+
+ // first we draw a white rectangle
+ env.Draw() <- redraw(true)
+
+ for event := range env.Events() {
+ switch event := event.(type) {
+ case win.MoDown:
+ if event.Point.In(r) {
+ // user clicked on the rectangle
+ // we blink 3 times
+ for i := 0; i < 3; i++ {
+ env.Draw() <- redraw(false)
+ time.Sleep(time.Second / 3)
+ env.Draw() <- redraw(true)
+ time.Sleep(time.Second / 3)
+ }
+ }
+ }
+ }
+
+ close(env.Draw())
+}
+
+func run() {
+ w, err := win.New(win.Title("faiface/gui"), win.Size(800, 600))
+ if err != nil {
+ panic(err)
+ }
+
+ mux, env := gui.NewMux(w)
+
+ // we create four blinkers, each with its own Env from the mux
+ go Blinker(mux.MakeEnv(), image.Rect(100, 100, 350, 250))
+ go Blinker(mux.MakeEnv(), image.Rect(450, 100, 700, 250))
+ go Blinker(mux.MakeEnv(), image.Rect(100, 350, 350, 500))
+ go Blinker(mux.MakeEnv(), image.Rect(450, 350, 700, 500))
+
+ // we use the master env now, w is used by the mux
+ for event := range env.Events() {
+ switch event.(type) {
+ case win.WiClose:
+ close(env.Draw())
+ }
+ }
+}
+
+func main() {
+ mainthread.Run(run)
+}
+```
+
+Just for the info, closing the `Draw()` channel on an `Env` created by `mux.MakeEnv()` removes the `Env` from the `Mux`.
+
+What 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?
+
+They won't, because the channels of events have unlimited capacity and never block. This is implemented using an intermediate goroutine that handles the queueing.
+
+![Events](images/events.png)
+
+And that's basically all you need to know about `faiface/gui`! Happy hacking!
+
+## A note on race conditions
+
+There is no guarantee when a function sent to the `Draw()` channel will be executed, or if at all. Look at this code:
+
+```go
+pressed := false
+
+env.Draw() <- func(drw draw.Image) image.Rectangle {
+ // use pressed somehow
+}
+
+// change pressed somewhere here
+```
+
+The 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.
+
+**My advice is to never enclose a shared variable in a drawing function.**
+
+Instead, you can do this:
+
+```go
+redraw := func(pressed bool) func(draw.Image) image.Rectangle {
+ return func(drw draw.Image) image.Rectangle {
+ // use the pressed argument
+ }
+}
+
+pressed := false
+
+env.Draw() <- redraw(pressed)
+
+// changing pressed here doesn't cause race conditions
+```
+
+## Credit
+
+The 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/).
+
+## Licence
+
+[MIT](LICENCE)
diff --git a/gui/env.go b/gui/env.go
new file mode 100644
index 0000000..2515417
--- /dev/null
+++ b/gui/env.go
@@ -0,0 +1,33 @@
+package gui
+
+import (
+ "image"
+ "image/draw"
+)
+
+// Env is the most important thing in this package. It is an interactive graphical
+// environment, such as a window.
+//
+// It has two channels: Events() and Draw().
+//
+// The Events() channel produces events, like mouse and keyboard presses, while the
+// Draw() channel receives drawing functions. A drawing function draws onto the
+// supplied draw.Image, which is the drawing area of the Env and returns a rectangle
+// covering the whole part of the image that got changed.
+//
+// An Env guarantees to produce a "resize/<x0>/<y0>/<x1>/<y1>" event as its first event.
+//
+// The Events() channel must be unlimited in capacity. Use MakeEventsChan() to create
+// a channel of events with an unlimited capacity.
+//
+// The Draw() channel may be synchronous.
+//
+// Drawing functions sent to the Draw() channel are not guaranteed to be executed.
+//
+// Closing the Draw() channel results in closing the Env. The Env will subsequently
+// close the Events() channel. On the other hand, when the Events() channel gets closed
+// the user of the Env should subsequently close the Draw() channel.
+type Env interface {
+ Events() <-chan Event
+ Draw() chan<- func(draw.Image) image.Rectangle
+}
diff --git a/gui/event.go b/gui/event.go
new file mode 100644
index 0000000..533317e
--- /dev/null
+++ b/gui/event.go
@@ -0,0 +1,70 @@
+package gui
+
+import (
+ "fmt"
+ "image"
+)
+
+// Event is something that can happen in an environment.
+//
+// This package defines only one kind of event: Resize. Other packages implementing environments
+// may implement more kinds of events. For example, the win package implements all kinds of
+// events for mouse and keyboard.
+type Event interface {
+ String() string
+}
+
+// Resize is an event that happens when the environment changes the size of its drawing area.
+type Resize struct {
+ image.Rectangle
+}
+
+func (r Resize) String() string {
+ return fmt.Sprintf("resize/%d/%d/%d/%d", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
+}
+
+// MakeEventsChan implements a channel of events with an unlimited capacity. It does so
+// by creating a goroutine that queues incoming events. Sending to this channel never blocks
+// and no events get lost.
+//
+// The unlimited capacity channel is very suitable for delivering events because the consumer
+// may be unavailable for some time (doing a heavy computation), but will get to the events
+// later.
+//
+// An unlimited capacity channel has its dangers in general, but is completely fine for
+// the purpose of delivering events. This is because the production of events is fairly
+// infrequent and should never out-run their consumption in the long term.
+func MakeEventsChan() (<-chan Event, chan<- Event) {
+ out, in := make(chan Event), make(chan Event)
+
+ go func() {
+ var queue []Event
+
+ for {
+ x, ok := <-in
+ if !ok {
+ close(out)
+ return
+ }
+ queue = append(queue, x)
+
+ for len(queue) > 0 {
+ select {
+ case out <- queue[0]:
+ queue = queue[1:]
+ case x, ok := <-in:
+ if !ok {
+ for _, x := range queue {
+ out <- x
+ }
+ close(out)
+ return
+ }
+ queue = append(queue, x)
+ }
+ }
+ }
+ }()
+
+ return out, in
+}
diff --git a/gui/layout/doc.go b/gui/layout/doc.go
new file mode 100644
index 0000000..8305d43
--- /dev/null
+++ b/gui/layout/doc.go
@@ -0,0 +1,10 @@
+/*
+Package layout provides a Layout system for faiface/gui.
+
+The core of the package is the Layout interface, everything else is just
+implementation.
+
+The Layouts represent a Layout, and the Mux makes them usable.
+The Mux basically acts as a sort of driver.
+*/
+package layout
diff --git a/gui/layout/grid.go b/gui/layout/grid.go
new file mode 100644
index 0000000..324353c
--- /dev/null
+++ b/gui/layout/grid.go
@@ -0,0 +1,115 @@
+package layout
+
+import (
+ "image"
+ "image/color"
+ "image/draw"
+ "log"
+
+ "volute/gui"
+)
+
+var _ Layout = Grid{}
+
+// Grid represents a grid with rows and columns in each row.
+// Each row can be a different length.
+type Grid struct {
+ // Rows represents the number of childs of each row.
+ Rows []int
+ // Background represents the background of the grid as a uniform color.
+ Background color.Color
+ // Gap represents the grid gap, equal on all sides.
+ Gap int
+ // Split represents the way the space is divided among the columns in each row.
+ Split SplitFunc
+ // SplitRows represents the way the space is divided among the rows.
+ SplitRows SplitFunc
+
+ Margin int
+ Border int
+ BorderColor color.Color
+
+ // Flip represents the orientation of the grid.
+ // When false, rows are spread in the Y axis and columns in the X axis.
+ // When true, rows are spread in the X axis and columns in the Y axis.
+ Flip bool
+}
+
+func (g Grid) redraw(drw draw.Image, bounds image.Rectangle) {
+ col := g.Background
+ if col == nil {
+ col = color.Black
+ }
+ if g.Border > 0 {
+ bcol := g.BorderColor
+ if bcol == nil {
+ bcol = color.Black
+ }
+ draw.Draw(drw, bounds, image.NewUniform(bcol), image.ZP, draw.Src)
+ }
+ draw.Draw(drw, bounds.Inset(g.Border), image.NewUniform(col), image.ZP, draw.Src)
+}
+
+func (g Grid) Intercept(env gui.Env) gui.Env {
+ return RedrawIntercepter{g.redraw}.Intercept(env)
+}
+
+func (g Grid) Lay(bounds image.Rectangle) []image.Rectangle {
+ gap := g.Gap
+ rows := g.Rows
+ splitMain := g.Split
+ if splitMain == nil {
+ splitMain = EvenSplit
+ }
+ splitSec := g.SplitRows
+ if splitSec == nil {
+ splitSec = EvenSplit
+ }
+ margin := g.Margin
+ flip := g.Flip
+ if margin+gap < 0 {
+ log.Println("Grid goes out of bounds")
+ }
+ if margin+gap < g.Border {
+ log.Println("Grid border will not be shown properly")
+ }
+
+ ret := make([]image.Rectangle, 0)
+
+ // Sorry it's not very understandable
+ var H, W int
+ var mX, mY int
+ if flip {
+ H = bounds.Dx()
+ W = bounds.Dy()
+ mX = bounds.Min.Y
+ mY = bounds.Min.X
+ } else {
+ H = bounds.Dy()
+ W = bounds.Dx()
+ mX = bounds.Min.X
+ mY = bounds.Min.Y
+ }
+ rowsH := splitSec(len(rows), H-(gap*(len(rows)+1))-margin*2)
+ var X int
+ var Y int
+ Y = gap + mY + margin
+ for y, cols := range rows {
+ h := rowsH[y]
+ colsW := splitMain(cols, W-(gap*(cols+1))-margin*2)
+ X = gap + mX + margin
+ for _, w := range colsW {
+ var r image.Rectangle
+ if flip {
+ r = image.Rect(Y, X, Y+h, X+w)
+ } else {
+ r = image.Rect(X, Y, X+w, Y+h)
+ }
+ ret = append(ret, r)
+ X += gap + w
+ }
+ Y += gap + h
+ }
+
+ return ret
+}
diff --git a/gui/layout/intercepter.go b/gui/layout/intercepter.go
new file mode 100644
index 0000000..d25d578
--- /dev/null
+++ b/gui/layout/intercepter.go
@@ -0,0 +1,43 @@
+package layout
+
+import (
+ "image"
+ "image/draw"
+
+ "volute/gui"
+)
+
+// Intercepter represents an element that can interact with Envs.
+// An Intercepter can modify Events, stop them or emit arbitrary ones.
+// It can also put itself in the draw pipeline, for throttling very
+// expensive draw calls for example.
+type Intercepter interface {
+ Intercept(gui.Env) gui.Env
+}
+
+var _ Intercepter = RedrawIntercepter{}
+
+// RedrawIntercepter is a basic Intercepter, it is meant for use in simple Layouts
+// that only need to redraw themselves.
+type RedrawIntercepter struct {
+ Redraw func(draw.Image, image.Rectangle)
+}
+
+// Intercept implements Intercepter
+func (ri RedrawIntercepter) Intercept(env gui.Env) gui.Env {
+ out, in := gui.MakeEventsChan()
+ go func() {
+ for e := range env.Events() {
+ in <- e
+ if resize, ok := e.(gui.Resize); ok {
+ env.Draw() <- func(drw draw.Image) image.Rectangle {
+ bounds := resize.Rectangle
+ ri.Redraw(drw, bounds)
+ return bounds
+ }
+ }
+ }
+ }()
+ ret := &muxEnv{out, env.Draw()}
+ return ret
+}
diff --git a/gui/layout/layout.go b/gui/layout/layout.go
new file mode 100644
index 0000000..4d8e616
--- /dev/null
+++ b/gui/layout/layout.go
@@ -0,0 +1,20 @@
+package layout
+
+import (
+ "image"
+)
+
+// Layout represents any graphical layout
+//
+// Lay represents the way to divide space among your childs.
+// It takes a parameter of how much space is available,
+// and returns where exactly to put its childs.
+//
+// Intercept transforms an Env channel to another.
+// This way the Layout can emit its own Events, re-emit previous ones,
+// or even stop an event from propagating, think win.MoScroll.
+// It can be a no-op.
+type Layout interface {
+ Lay(image.Rectangle) []image.Rectangle
+ Intercepter
+}
diff --git a/gui/layout/mux.go b/gui/layout/mux.go
new file mode 100644
index 0000000..7b193e7
--- /dev/null
+++ b/gui/layout/mux.go
@@ -0,0 +1,172 @@
+package layout
+
+import (
+ "image"
+ "image/draw"
+ "log"
+ "sync"
+
+ "volute/gui"
+)
+
+// Mux can be used to multiplex an Env, let's call it a root Env. Mux implements a way to
+// create multiple virtual Envs that all interact with the root Env. They receive the same
+// events apart from gui.Resize, and their draw functions get redirected to the root Env.
+//
+// All gui.Resize events are instead modified according to the underlying Layout.
+// The master Env gets the original gui.Resize events.
+type Mux struct {
+ mu sync.Mutex
+ lastResize gui.Event
+ eventsIns []chan<- gui.Event
+ draw chan<- func(draw.Image) image.Rectangle
+
+ layout Layout
+}
+
+// Layout returns the underlying Layout of the Mux.
+func (mux *Mux) Layout() Layout {
+ return mux.layout
+}
+
+// NewMux should only be used internally by Layouts.
+// It has mostly the same behaviour as gui.Mux, except for its use of an underlying Layout
+// for modifying the gui.Resize events sent to the childs.
+func NewMux(ev gui.Env, envs []*gui.Env, l Layout) (mux *Mux, master gui.Env) {
+ env := l.Intercept(ev)
+ drawChan := make(chan func(draw.Image) image.Rectangle)
+ mux = &Mux{
+ layout: l,
+ draw: drawChan,
+ }
+ master, masterIn := mux.makeEnv(true)
+ events := make(chan gui.Event)
+ go func() {
+ for d := range drawChan {
+ env.Draw() <- d
+ }
+ close(env.Draw())
+ }()
+
+ go func() {
+ for e := range env.Events() {
+ events <- e
+ }
+ }()
+
+ go func() {
+ for e := range events {
+ // master gets a copy of all events to the Mux
+ masterIn <- e
+ mux.mu.Lock()
+ if resize, ok := e.(gui.Resize); ok {
+ mux.lastResize = resize
+ rect := resize.Rectangle
+ lay := mux.layout.Lay(rect)
+ if len(lay) < len(envs) {
+ log.Printf("Lay of %T is not large enough (%d) for %d childs, skipping\n", l, len(lay), len(envs))
+ mux.mu.Unlock()
+ continue
+ }
+
+ // Send appropriate resize Events to childs
+ for i, eventsIn := range mux.eventsIns {
+ resize.Rectangle = lay[i]
+ eventsIn <- resize
+ }
+
+ } else {
+ for _, eventsIn := range mux.eventsIns {
+ eventsIn <- e
+ }
+ }
+ mux.mu.Unlock()
+ }
+ mux.mu.Lock()
+ for _, eventsIn := range mux.eventsIns {
+ close(eventsIn)
+ }
+ mux.mu.Unlock()
+ }()
+
+ for _, en := range envs {
+ *en, _ = mux.makeEnv(false)
+ }
+ return
+}
+
+type muxEnv struct {
+ events <-chan gui.Event
+ draw chan<- func(draw.Image) image.Rectangle
+}
+
+func (m *muxEnv) Events() <-chan gui.Event { return m.events }
+func (m *muxEnv) Draw() chan<- func(draw.Image) image.Rectangle { return m.draw }
+
+// We do not store master env
+func (mux *Mux) makeEnv(master bool) (env gui.Env, eventsIn chan<- gui.Event) {
+ eventsOut, eventsIn := gui.MakeEventsChan()
+ drawChan := make(chan func(draw.Image) image.Rectangle)
+ env = &muxEnv{eventsOut, drawChan}
+
+ if !master {
+ mux.mu.Lock()
+ mux.eventsIns = append(mux.eventsIns, eventsIn)
+ // make sure to always send a resize event to a new Env if we got the size already
+ // that means it missed the resize event by the root Env
+ if mux.lastResize != nil {
+ eventsIn <- mux.lastResize
+ }
+ mux.mu.Unlock()
+ }
+
+ go func() {
+ func() {
+ // When the master Env gets its Draw() channel closed, it closes all the Events()
+ // channels of all the children Envs, and it also closes the internal draw channel
+ // of the Mux. Otherwise, closing the Draw() channel of the master Env wouldn't
+ // close the Env the Mux is muxing. However, some child Envs of the Mux may still
+ // send some drawing commmands before they realize that their Events() channel got
+ // closed.
+ //
+ // That is perfectly fine if their drawing commands simply get ignored. This down here
+ // is a little hacky, but (I hope) perfectly fine solution to the problem.
+ //
+ // When the internal draw channel of the Mux gets closed, the line marked with ! will
+ // cause panic. We recover this panic, then we receive, but ignore all furhter draw
+ // commands, correctly draining the Env until it closes itself.
+ defer func() {
+ if recover() != nil {
+ for range drawChan {
+ }
+ }
+ }()
+ for d := range drawChan {
+ mux.draw <- d // !
+ }
+ }()
+ if master {
+ mux.mu.Lock()
+ for _, eventsIn := range mux.eventsIns {
+ close(eventsIn)
+ }
+ mux.eventsIns = nil
+ close(mux.draw)
+ mux.mu.Unlock()
+ } else {
+ mux.mu.Lock()
+ i := -1
+ for i = range mux.eventsIns {
+ if mux.eventsIns[i] == eventsIn {
+ break
+ }
+ }
+ if i != -1 {
+ mux.eventsIns = append(mux.eventsIns[:i], mux.eventsIns[i+1:]...)
+ }
+ mux.mu.Unlock()
+ }
+ }()
+
+ return env, eventsIn
+}
diff --git a/gui/layout/scroller.go b/gui/layout/scroller.go
new file mode 100644
index 0000000..b7fc3f5
--- /dev/null
+++ b/gui/layout/scroller.go
@@ -0,0 +1,132 @@
+package layout
+
+import (
+ "image"
+ "image/color"
+ "image/draw"
+ "sync"
+
+ "volute/gui"
+ "volute/gui/win"
+)
+
+var _ Layout = &Scroller{}
+
+type Scroller struct {
+ Background color.Color
+ Length int
+ ChildHeight int
+ Offset int
+ Gap int
+ Vertical bool
+}
+
+func (s Scroller) redraw(drw draw.Image, bounds image.Rectangle) {
+ col := s.Background
+ if col == nil {
+ col = image.Black
+ }
+ draw.Draw(drw, bounds, image.NewUniform(col), image.ZP, draw.Src)
+}
+
+func clamp(val, a, b int) int {
+ if a > b {
+ if val < b {
+ return b
+ }
+ if val > a {
+ return a
+ }
+ } else {
+ if val > b {
+ return b
+ }
+ if val < a {
+ return a
+ }
+ }
+ return val
+}
+
+func (s *Scroller) Intercept(env gui.Env) gui.Env {
+ evs := env.Events()
+ out, in := gui.MakeEventsChan()
+ drawChan := make(chan func(draw.Image) image.Rectangle)
+ ret := &muxEnv{out, drawChan}
+ var lastResize gui.Resize
+ var img draw.Image
+ img = image.NewRGBA(image.ZR)
+ var mu sync.Mutex
+ var over bool
+
+ go func() {
+ for dc := range drawChan {
+ mu.Lock()
+ // draw.Draw will not draw out of bounds, call should be inexpensive if element not visible
+ res := dc(img)
+ // Only send a draw call up if visibly changed
+ if res.Intersect(img.Bounds()) != image.ZR {
+ env.Draw() <- func(drw draw.Image) image.Rectangle {
+ draw.Draw(drw, lastResize.Rectangle, img, lastResize.Rectangle.Min, draw.Over)
+ return img.Bounds()
+ }
+ }
+ mu.Unlock()
+ }
+ }()
+
+ go func() {
+ for ev := range evs {
+ switch ev := ev.(type) {
+ case win.MoMove:
+ mu.Lock()
+ over = ev.Point.In(lastResize.Rectangle)
+ mu.Unlock()
+ case win.MoScroll:
+ if !over {
+ continue
+ }
+ mu.Lock()
+ oldoff := s.Offset
+ v := s.Length*s.ChildHeight + ((s.Length + 1) * s.Gap)
+ if s.Vertical {
+ h := lastResize.Dx()
+ s.Offset = clamp(s.Offset+ev.Point.X*16, h-v, 0)
+ } else {
+ h := lastResize.Dy()
+ s.Offset = clamp(s.Offset+ev.Point.Y*16, h-v, 0)
+ }
+ if oldoff != s.Offset {
+ s.redraw(img, img.Bounds())
+ in <- lastResize
+ }
+ mu.Unlock()
+ case gui.Resize:
+ mu.Lock()
+ lastResize = ev
+ img = image.NewRGBA(lastResize.Rectangle)
+ s.redraw(img, img.Bounds())
+ mu.Unlock()
+ in <- ev
+ default:
+ in <- ev
+ }
+ }
+ }()
+ return ret
+}
+
+func (s Scroller) Lay(bounds image.Rectangle) []image.Rectangle {
+ items := s.Length
+ ch := s.ChildHeight
+ gap := s.Gap
+
+ ret := make([]image.Rectangle, items)
+ Y := bounds.Min.Y + s.Offset + gap
+ for i := 0; i < items; i++ {
+ r := image.Rect(bounds.Min.X+gap, Y, bounds.Max.X-gap, Y+ch)
+ ret[i] = r
+ Y += ch + gap
+ }
+ return ret
+}
diff --git a/gui/layout/split.go b/gui/layout/split.go
new file mode 100644
index 0000000..db04225
--- /dev/null
+++ b/gui/layout/split.go
@@ -0,0 +1,26 @@
+package layout
+
+import "fmt"
+
+// SplitFunc represents a way to split a space among a number of elements.
+// The length of the returned slice must be equal to the number of elements.
+// The sum of all elements of the returned slice must be eqal to the space.
+type SplitFunc func(elements int, space int) []int
+
+var _ SplitFunc = EvenSplit
+
+// EvenSplit is a SplitFunc used to split a space (almost) evenly among the elements.
+// It is almost evenly because width may not be divisible by elements.
+func EvenSplit(elements int, width int) []int {
+ if elements <= 0 {
+ panic(fmt.Errorf("EvenSplit: elements must be greater than 0"))
+ }
+ ret := make([]int, elements)
+ for elements > 0 {
+ v := width / elements
+ width -= v
+ elements -= 1
+ ret[elements] = v
+ }
+ return ret
+}
diff --git a/gui/mux.go b/gui/mux.go
new file mode 100644
index 0000000..4198d61
--- /dev/null
+++ b/gui/mux.go
@@ -0,0 +1,134 @@
+package gui
+
+import (
+ "image"
+ "image/draw"
+ "sync"
+)
+
+// Mux can be used to multiplex an Env, let's call it a root Env. Mux implements a way to
+// create multiple virtual Envs that all interact with the root Env. They receive the same
+// events and their draw functions get redirected to the root Env.
+type Mux struct {
+ mu sync.Mutex
+ lastResize Event
+ eventsIns []chan<- Event
+ draw chan<- func(draw.Image) image.Rectangle
+}
+
+// NewMux creates a new Mux that multiplexes the given Env. It returns the Mux along with
+// a master Env. The master Env is just like any other Env created by the Mux, except that
+// closing the Draw() channel on the master Env closes the whole Mux and all other Envs
+// created by the Mux.
+func NewMux(env Env) (mux *Mux, master Env) {
+ drawChan := make(chan func(draw.Image) image.Rectangle)
+ mux = &Mux{draw: drawChan}
+ master = mux.makeEnv(true)
+
+ go func() {
+ for d := range drawChan {
+ env.Draw() <- d
+ }
+ close(env.Draw())
+ }()
+
+ go func() {
+ for e := range env.Events() {
+ mux.mu.Lock()
+ if resize, ok := e.(Resize); ok {
+ mux.lastResize = resize
+ }
+ for _, eventsIn := range mux.eventsIns {
+ eventsIn <- e
+ }
+ mux.mu.Unlock()
+ }
+ mux.mu.Lock()
+ for _, eventsIn := range mux.eventsIns {
+ close(eventsIn)
+ }
+ mux.mu.Unlock()
+ }()
+
+ return mux, master
+}
+
+// MakeEnv creates a new virtual Env that interacts with the root Env of the Mux. Closing
+// the Draw() channel of the Env will not close the Mux, or any other Env created by the Mux
+// but will delete the Env from the Mux.
+func (mux *Mux) MakeEnv() Env {
+ return mux.makeEnv(false)
+}
+
+type muxEnv struct {
+ events <-chan Event
+ draw chan<- func(draw.Image) image.Rectangle
+}
+
+func (m *muxEnv) Events() <-chan Event { return m.events }
+func (m *muxEnv) Draw() chan<- func(draw.Image) image.Rectangle { return m.draw }
+
+func (mux *Mux) makeEnv(master bool) Env {
+ eventsOut, eventsIn := MakeEventsChan()
+ drawChan := make(chan func(draw.Image) image.Rectangle)
+ env := &muxEnv{eventsOut, drawChan}
+
+ mux.mu.Lock()
+ mux.eventsIns = append(mux.eventsIns, eventsIn)
+ // make sure to always send a resize event to a new Env if we got the size already
+ // that means it missed the resize event by the root Env
+ if mux.lastResize != nil {
+ eventsIn <- mux.lastResize
+ }
+ mux.mu.Unlock()
+
+ go func() {
+ func() {
+ // When the master Env gets its Draw() channel closed, it closes all the Events()
+ // channels of all the children Envs, and it also closes the internal draw channel
+ // of the Mux. Otherwise, closing the Draw() channel of the master Env wouldn't
+ // close the Env the Mux is muxing. However, some child Envs of the Mux may still
+ // send some drawing commmands before they realize that their Events() channel got
+ // closed.
+ //
+ // That is perfectly fine if their drawing commands simply get ignored. This down here
+ // is a little hacky, but (I hope) perfectly fine solution to the problem.
+ //
+ // When the internal draw channel of the Mux gets closed, the line marked with ! will
+ // cause panic. We recover this panic, then we receive, but ignore all furhter draw
+ // commands, correctly draining the Env until it closes itself.
+ defer func() {
+ if recover() != nil {
+ for range drawChan {
+ }
+ }
+ }()
+ for d := range drawChan {
+ mux.draw <- d // !
+ }
+ }()
+ if master {
+ mux.mu.Lock()
+ for _, eventsIn := range mux.eventsIns {
+ close(eventsIn)
+ }
+ mux.eventsIns = nil
+ close(mux.draw)
+ mux.mu.Unlock()
+ } else {
+ mux.mu.Lock()
+ i := -1
+ for i = range mux.eventsIns {
+ if mux.eventsIns[i] == eventsIn {
+ break
+ }
+ }
+ if i != -1 {
+ mux.eventsIns = append(mux.eventsIns[:i], mux.eventsIns[i+1:]...)
+ }
+ mux.mu.Unlock()
+ }
+ }()
+
+ return env
+}
diff --git a/gui/widget/concurrent_face.go b/gui/widget/concurrent_face.go
new file mode 100644
index 0000000..98db572
--- /dev/null
+++ b/gui/widget/concurrent_face.go
@@ -0,0 +1,51 @@
+package widget
+
+import (
+ "image"
+ "sync"
+
+ "golang.org/x/image/font"
+ "golang.org/x/image/math/fixed"
+)
+
+type concurrentFace struct {
+ mu sync.Mutex
+ face font.Face
+}
+
+func (cf *concurrentFace) Close() error {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.Close()
+}
+
+func (cf *concurrentFace) Glyph(dot fixed.Point26_6, r rune) (
+ dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.Glyph(dot, r)
+}
+
+func (cf *concurrentFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.GlyphBounds(r)
+}
+
+func (cf *concurrentFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.GlyphAdvance(r)
+}
+
+func (cf *concurrentFace) Kern(r0, r1 rune) fixed.Int26_6 {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.Kern(r0, r1)
+}
+
+func (cf *concurrentFace) Metrics() font.Metrics {
+ cf.mu.Lock()
+ defer cf.mu.Unlock()
+ return cf.face.Metrics()
+}
diff --git a/gui/widget/text.go b/gui/widget/text.go
new file mode 100644
index 0000000..1b40096
--- /dev/null
+++ b/gui/widget/text.go
@@ -0,0 +1,78 @@
+package widget
+
+import (
+ "log"
+ "sync"
+
+ "image"
+ "image/color"
+ "image/draw"
+
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/font/opentype"
+ "golang.org/x/image/math/fixed"
+)
+
+var (
+ FONT = goregular.TTF
+ FONT_SIZE = 15
+ DPI = 72
+ PAD = 3
+)
+
+var face *concurrentFace
+
+func init() {
+ fnt, err := opentype.Parse(FONT)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fce, err := opentype.NewFace(fnt, &opentype.FaceOptions{
+ Size: float64(FONT_SIZE),
+ DPI: float64(DPI),
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ face = &concurrentFace{sync.Mutex{}, fce}
+}
+
+func TextSize(text string) image.Point {
+ bounds := textBounds([]byte(text), font.Drawer{Face: face})
+ return image.Point{bounds.Max.X - bounds.Min.X + 2*PAD, bounds.Max.Y - bounds.Min.Y + 2*PAD}
+}
+
+func drawText(text []byte, dst draw.Image, r image.Rectangle, fg, bg color.Color) {
+ drawer := font.Drawer{
+ Src: &image.Uniform{fg},
+ Face: face,
+ Dot: fixed.P(0, 0),
+ }
+
+ // background
+ draw.Draw(dst, r, &image.Uniform{bg}, image.ZP, draw.Src)
+
+ // text image
+ bounds := textBounds(text, drawer)
+ textImg := image.NewRGBA(bounds)
+ draw.Draw(textImg, bounds, &image.Uniform{bg}, image.ZP, draw.Src)
+ drawer.Dst = textImg
+ drawer.DrawBytes(text)
+
+ // draw text image over background
+ leftCentre := image.Pt(bounds.Min.X, (bounds.Min.Y+bounds.Max.Y)/2)
+ target := image.Pt(r.Max.X-bounds.Max.X-PAD, (r.Min.Y+r.Max.Y)/2)
+ delta := target.Sub(leftCentre)
+ draw.Draw(dst, bounds.Add(delta).Intersect(r), drawer.Dst, bounds.Min, draw.Src)
+}
+
+func textBounds(text []byte, drawer font.Drawer) image.Rectangle {
+ b, _ := drawer.BoundBytes(text)
+ return image.Rect(
+ b.Min.X.Floor(),
+ b.Min.Y.Floor(),
+ b.Max.X.Ceil(),
+ b.Max.Y.Ceil(),
+ )
+}
diff --git a/gui/widget/widget.go b/gui/widget/widget.go
new file mode 100644
index 0000000..cbc837e
--- /dev/null
+++ b/gui/widget/widget.go
@@ -0,0 +1,130 @@
+package widget
+
+import (
+ "cmp"
+ "fmt"
+
+ "image"
+ "image/color"
+ "image/draw"
+
+ "volute/gui"
+ "volute/gui/win"
+)
+
+var (
+ FOCUS_COLOR = color.RGBA{179, 217, 255, 255}
+ GREEN = color.RGBA{51, 102, 0, 255}
+ BLACK = color.Gray{0}
+ WHITE = color.Gray{255}
+)
+
+func Label(text string, r image.Rectangle, env gui.Env) {
+ redraw := func(drw draw.Image) image.Rectangle {
+ drawText([]byte(text), drw, r, BLACK, WHITE)
+ return r
+ }
+ env.Draw() <- redraw
+ for event := range env.Events() {
+ switch event := event.(type) {
+ case win.WiFocus:
+ if event.Focused {
+ env.Draw() <- redraw
+ }
+ }
+ }
+ close(env.Draw())
+}
+
+func Input(val chan<- uint, r image.Rectangle, focusChan <-chan bool, env gui.Env) {
+ redraw := func(text []byte, focus bool) func(draw.Image) image.Rectangle {
+ return func(drw draw.Image) image.Rectangle {
+ if focus {
+ drawText(text, drw, r, GREEN, FOCUS_COLOR)
+ } else {
+ drawText(text, drw, r, GREEN, WHITE)
+ }
+ return r
+ }
+ }
+ text := []byte{'0'}
+ focus := false
+
+ env.Draw() <- redraw(text, focus)
+
+ for {
+ select {
+ case focus = <-focusChan:
+ env.Draw() <- redraw(text, focus)
+ case event := <-env.Events():
+ switch event := event.(type) {
+ case win.WiFocus:
+ if event.Focused {
+ env.Draw() <- redraw(text, focus)
+ }
+ case win.KbType:
+ if focus && isDigit(event.Rune) {
+ text = fmt.Appendf(text, "%c", event.Rune)
+ env.Draw() <- redraw(text, focus)
+ val <- atoi(text)
+ }
+ case win.KbDown:
+ if focus && event.Key == win.KeyBackspace && len(text) > 0 {
+ text = text[:len(text)-1]
+ env.Draw() <- redraw(text, focus)
+ val <- atoi(text)
+ }
+ }
+ }
+ }
+ close(env.Draw())
+}
+
+func Output(val <-chan uint, r image.Rectangle, env gui.Env) {
+ redraw := func(n uint) func(draw.Image) image.Rectangle {
+ return func(drw draw.Image) image.Rectangle {
+ drawText([]byte(fmt.Sprint(n)), drw, r, BLACK, WHITE)
+ return r
+ }
+ }
+
+ var n uint = 0
+ env.Draw() <- redraw(n)
+
+Loop:
+ for {
+ select {
+ case n = <-val:
+ env.Draw() <- redraw(n)
+ case event, ok := <-env.Events():
+ if !ok { // channel closed
+ break Loop
+ }
+ if event, ok := event.(win.WiFocus); ok && event.Focused {
+ env.Draw() <- redraw(n)
+ }
+ }
+ }
+ close(env.Draw())
+}
+
+func isDigit(r rune) bool {
+ return '0' <= r && r <= '9'
+}
+
+func contains[T cmp.Ordered](slc []T, v T) bool {
+ for i := range slc {
+ if slc[i] == v {
+ return true
+ }
+ }
+ return false
+}
+
+func atoi(s []byte) uint {
+ var n uint = 0
+ for _, d := range s {
+ n = n*10 + uint(d-'0')
+ }
+ return n
+}
diff --git a/gui/win/events.go b/gui/win/events.go
new file mode 100644
index 0000000..bae8017
--- /dev/null
+++ b/gui/win/events.go
@@ -0,0 +1,93 @@
+package win
+
+import (
+ "fmt"
+ "image"
+)
+
+// Button indicates a mouse button in an event.
+type Button string
+
+// List of all mouse buttons.
+const (
+ ButtonLeft Button = "left"
+ ButtonRight Button = "right"
+ ButtonMiddle Button = "middle"
+)
+
+// Key indicates a keyboard key in an event.
+type Key string
+
+// List of all keyboard keys.
+const (
+ KeyLeft Key = "left"
+ KeyRight Key = "right"
+ KeyUp Key = "up"
+ KeyDown Key = "down"
+ KeyEscape Key = "escape"
+ KeySpace Key = "space"
+ KeyBackspace Key = "backspace"
+ KeyDelete Key = "delete"
+ KeyEnter Key = "enter"
+ KeyTab Key = "tab"
+ KeyHome Key = "home"
+ KeyEnd Key = "end"
+ KeyPageUp Key = "pageup"
+ KeyPageDown Key = "pagedown"
+ KeyShift Key = "shift"
+ KeyCtrl Key = "ctrl"
+ KeyAlt Key = "alt"
+)
+
+type (
+ // WiClose is an event that happens when the user presses the close button on the window.
+ WiClose struct{}
+
+ // WiFocus is an event that happens when the window gains or loses focus.
+ WiFocus struct{ Focused bool }
+
+ // MoMove is an event that happens when the mouse gets moved across the window.
+ MoMove struct{ image.Point }
+
+ // MoDown is an event that happens when a mouse button gets pressed.
+ MoDown struct {
+ image.Point
+ Button Button
+ }
+
+ // MoUp is an event that happens when a mouse button gets released.
+ MoUp struct {
+ image.Point
+ Button Button
+ }
+
+ // MoScroll is an event that happens on scrolling the mouse.
+ //
+ // The Point field tells the amount scrolled in each direction.
+ MoScroll struct{ image.Point }
+
+ // KbType is an event that happens when a Unicode character gets typed on the keyboard.
+ KbType struct{ Rune rune }
+
+ // KbDown is an event that happens when a key on the keyboard gets pressed.
+ KbDown struct{ Key Key }
+
+ // KbUp is an event that happens when a key on the keyboard gets released.
+ KbUp struct{ Key Key }
+
+ // KbRepeat is an event that happens when a key on the keyboard gets repeated.
+ //
+ // This happens when its held down for some time.
+ KbRepeat struct{ Key Key }
+)
+
+func (wc WiClose) String() string { return "wi/close" }
+func (wf WiFocus) String() string { return "wi/focus" }
+func (mm MoMove) String() string { return fmt.Sprintf("mo/move/%d/%d", mm.X, mm.Y) }
+func (md MoDown) String() string { return fmt.Sprintf("mo/down/%d/%d/%s", md.X, md.Y, md.Button) }
+func (mu MoUp) String() string { return fmt.Sprintf("mo/up/%d/%d/%s", mu.X, mu.Y, mu.Button) }
+func (ms MoScroll) String() string { return fmt.Sprintf("mo/scroll/%d/%d", ms.X, ms.Y) }
+func (kt KbType) String() string { return fmt.Sprintf("kb/type/%d", kt.Rune) }
+func (kd KbDown) String() string { return fmt.Sprintf("kb/down/%s", kd.Key) }
+func (ku KbUp) String() string { return fmt.Sprintf("kb/up/%s", ku.Key) }
+func (kr KbRepeat) String() string { return fmt.Sprintf("kb/repeat/%s", kr.Key) }
diff --git a/gui/win/win.go b/gui/win/win.go
new file mode 100644
index 0000000..2b45868
--- /dev/null
+++ b/gui/win/win.go
@@ -0,0 +1,362 @@
+package win
+
+import (
+ "image"
+ "image/draw"
+ "runtime"
+ "time"
+ "unsafe"
+
+ "github.com/faiface/mainthread"
+ "github.com/go-gl/gl/v2.1/gl"
+ "github.com/go-gl/glfw/v3.2/glfw"
+ "volute/gui"
+)
+
+// Option is a functional option to the window constructor New.
+type Option func(*options)
+
+type options struct {
+ title string
+ width, height int
+ resizable bool
+ borderless bool
+ maximized bool
+}
+
+// Title option sets the title (caption) of the window.
+func Title(title string) Option {
+ return func(o *options) {
+ o.title = title
+ }
+}
+
+// Size option sets the width and height of the window.
+func Size(width, height int) Option {
+ return func(o *options) {
+ o.width = width
+ o.height = height
+ }
+}
+
+// Resizable option makes the window resizable by the user.
+func Resizable() Option {
+ return func(o *options) {
+ o.resizable = true
+ }
+}
+
+// Borderless option makes the window borderless.
+func Borderless() Option {
+ return func(o *options) {
+ o.borderless = true
+ }
+}
+
+// Maximized option makes the window start maximized.
+func Maximized() Option {
+ return func(o *options) {
+ o.maximized = true
+ }
+}
+
+// New creates a new window with all the supplied options.
+//
+// The default title is empty and the default size is 640x480.
+func New(opts ...Option) (*Win, error) {
+ o := options{
+ title: "",
+ width: 640,
+ height: 480,
+ resizable: false,
+ borderless: false,
+ maximized: false,
+ }
+ for _, opt := range opts {
+ opt(&o)
+ }
+
+ eventsOut, eventsIn := gui.MakeEventsChan()
+
+ w := &Win{
+ eventsOut: eventsOut,
+ eventsIn: eventsIn,
+ draw: make(chan func(draw.Image) image.Rectangle),
+ newSize: make(chan image.Rectangle),
+ finish: make(chan struct{}),
+ }
+
+ var err error
+ mainthread.Call(func() {
+ w.w, err = makeGLFWWin(&o)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ mainthread.Call(func() {
+ // hiDPI hack
+ width, _ := w.w.GetFramebufferSize()
+ w.ratio = width / o.width
+ if w.ratio < 1 {
+ w.ratio = 1
+ }
+ if w.ratio != 1 {
+ o.width /= w.ratio
+ o.height /= w.ratio
+ }
+ w.w.Destroy()
+ w.w, err = makeGLFWWin(&o)
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ bounds := image.Rect(0, 0, o.width*w.ratio, o.height*w.ratio)
+ w.img = image.NewRGBA(bounds)
+
+ go func() {
+ runtime.LockOSThread()
+ w.openGLThread()
+ }()
+
+ mainthread.CallNonBlock(w.eventThread)
+
+ return w, nil
+}
+
+func makeGLFWWin(o *options) (*glfw.Window, error) {
+ err := glfw.Init()
+ if err != nil {
+ return nil, err
+ }
+ glfw.WindowHint(glfw.DoubleBuffer, glfw.False)
+ if o.resizable {
+ glfw.WindowHint(glfw.Resizable, glfw.True)
+ } else {
+ glfw.WindowHint(glfw.Resizable, glfw.False)
+ }
+ if o.borderless {
+ glfw.WindowHint(glfw.Decorated, glfw.False)
+ }
+ if o.maximized {
+ glfw.WindowHint(glfw.Maximized, glfw.True)
+ }
+ w, err := glfw.CreateWindow(o.width, o.height, o.title, nil, nil)
+ if err != nil {
+ return nil, err
+ }
+ if o.maximized {
+ o.width, o.height = w.GetFramebufferSize() // set o.width and o.height to the window size due to the window being maximized
+ }
+ return w, nil
+}
+
+// Win is an Env that handles an actual graphical window.
+//
+// It receives its events from the OS and it draws to the surface of the window.
+//
+// Warning: only one window can be open at a time. This will be fixed.
+type Win struct {
+ eventsOut <-chan gui.Event
+ eventsIn chan<- gui.Event
+ draw chan func(draw.Image) image.Rectangle
+
+ newSize chan image.Rectangle
+ finish chan struct{}
+
+ w *glfw.Window
+ img *image.RGBA
+ ratio int
+}
+
+// Events returns the events channel of the window.
+func (w *Win) Events() <-chan gui.Event { return w.eventsOut }
+
+// Draw returns the draw channel of the window.
+func (w *Win) Draw() chan<- func(draw.Image) image.Rectangle { return w.draw }
+
+var buttons = map[glfw.MouseButton]Button{
+ glfw.MouseButtonLeft: ButtonLeft,
+ glfw.MouseButtonRight: ButtonRight,
+ glfw.MouseButtonMiddle: ButtonMiddle,
+}
+
+var keys = map[glfw.Key]Key{
+ glfw.KeyLeft: KeyLeft,
+ glfw.KeyRight: KeyRight,
+ glfw.KeyUp: KeyUp,
+ glfw.KeyDown: KeyDown,
+ glfw.KeyEscape: KeyEscape,
+ glfw.KeySpace: KeySpace,
+ glfw.KeyBackspace: KeyBackspace,
+ glfw.KeyDelete: KeyDelete,
+ glfw.KeyEnter: KeyEnter,
+ glfw.KeyTab: KeyTab,
+ glfw.KeyHome: KeyHome,
+ glfw.KeyEnd: KeyEnd,
+ glfw.KeyPageUp: KeyPageUp,
+ glfw.KeyPageDown: KeyPageDown,
+ glfw.KeyLeftShift: KeyShift,
+ glfw.KeyRightShift: KeyShift,
+ glfw.KeyLeftControl: KeyCtrl,
+ glfw.KeyRightControl: KeyCtrl,
+ glfw.KeyLeftAlt: KeyAlt,
+ glfw.KeyRightAlt: KeyAlt,
+}
+
+func (w *Win) eventThread() {
+ var moX, moY int
+
+ w.w.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
+ moX, moY = int(x), int(y)
+ w.eventsIn <- MoMove{image.Pt(moX*w.ratio, moY*w.ratio)}
+ })
+
+ w.w.SetMouseButtonCallback(func(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
+ b, ok := buttons[button]
+ if !ok {
+ return
+ }
+ switch action {
+ case glfw.Press:
+ w.eventsIn <- MoDown{image.Pt(moX*w.ratio, moY*w.ratio), b}
+ case glfw.Release:
+ w.eventsIn <- MoUp{image.Pt(moX*w.ratio, moY*w.ratio), b}
+ }
+ })
+
+ w.w.SetScrollCallback(func(_ *glfw.Window, xoff, yoff float64) {
+ w.eventsIn <- MoScroll{image.Pt(int(xoff), int(yoff))}
+ })
+
+ w.w.SetCharCallback(func(_ *glfw.Window, r rune) {
+ w.eventsIn <- KbType{r}
+ })
+
+ w.w.SetKeyCallback(func(_ *glfw.Window, key glfw.Key, _ int, action glfw.Action, _ glfw.ModifierKey) {
+ k, ok := keys[key]
+ if !ok {
+ return
+ }
+ switch action {
+ case glfw.Press:
+ w.eventsIn <- KbDown{k}
+ case glfw.Release:
+ w.eventsIn <- KbUp{k}
+ case glfw.Repeat:
+ w.eventsIn <- KbRepeat{k}
+ }
+ })
+
+ w.w.SetFramebufferSizeCallback(func(_ *glfw.Window, width, height int) {
+ r := image.Rect(0, 0, width, height)
+ w.newSize <- r
+ w.eventsIn <- gui.Resize{Rectangle: r}
+ })
+
+ w.w.SetCloseCallback(func(_ *glfw.Window) {
+ w.eventsIn <- WiClose{}
+ })
+
+ w.w.SetFocusCallback(func(_ *glfw.Window, focused bool) {
+ w.eventsIn <- WiFocus{focused}
+ })
+
+ r := w.img.Bounds()
+ w.eventsIn <- gui.Resize{Rectangle: r}
+
+ for {
+ select {
+ case <-w.finish:
+ close(w.eventsIn)
+ w.w.Destroy()
+ return
+ default:
+ glfw.WaitEventsTimeout(1.0 / 30)
+ }
+ }
+}
+
+func (w *Win) openGLThread() {
+ w.w.MakeContextCurrent()
+ gl.Init()
+
+ w.openGLFlush(w.img.Bounds())
+
+loop:
+ for {
+ var totalR image.Rectangle
+
+ select {
+ case r := <-w.newSize:
+ img := image.NewRGBA(r)
+ draw.Draw(img, w.img.Bounds(), w.img, w.img.Bounds().Min, draw.Src)
+ w.img = img
+ totalR = totalR.Union(r)
+
+ case d, ok := <-w.draw:
+ if !ok {
+ close(w.finish)
+ return
+ }
+ r := d(w.img)
+ totalR = totalR.Union(r)
+ }
+
+ for {
+ select {
+ case <-time.After(time.Second / 960):
+ w.openGLFlush(totalR)
+ totalR = image.ZR
+ continue loop
+
+ case r := <-w.newSize:
+ img := image.NewRGBA(r)
+ draw.Draw(img, w.img.Bounds(), w.img, w.img.Bounds().Min, draw.Src)
+ w.img = img
+ totalR = totalR.Union(r)
+
+ case d, ok := <-w.draw:
+ if !ok {
+ close(w.finish)
+ return
+ }
+ r := d(w.img)
+ totalR = totalR.Union(r)
+ }
+ }
+ }
+}
+
+func (w *Win) openGLFlush(r image.Rectangle) {
+ bounds := w.img.Bounds()
+ r = r.Intersect(bounds)
+ if r.Empty() {
+ return
+ }
+
+ tmp := image.NewRGBA(r)
+ draw.Draw(tmp, r, w.img, r.Min, draw.Src)
+
+ gl.DrawBuffer(gl.FRONT)
+ gl.Viewport(
+ int32(bounds.Min.X),
+ int32(bounds.Min.Y),
+ int32(bounds.Dx()),
+ int32(bounds.Dy()),
+ )
+ gl.RasterPos2d(
+ -1+2*float64(r.Min.X)/float64(bounds.Dx()),
+ +1-2*float64(r.Min.Y)/float64(bounds.Dy()),
+ )
+ gl.PixelZoom(1, -1)
+ gl.DrawPixels(
+ int32(r.Dx()),
+ int32(r.Dy()),
+ gl.RGBA,
+ gl.UNSIGNED_BYTE,
+ unsafe.Pointer(&tmp.Pix[0]),
+ )
+ gl.Flush()
+}
diff --git a/main.go b/main.go
index aaf4264..1bf2788 100644
--- a/main.go
+++ b/main.go
@@ -1,158 +1,138 @@
package main
import (
- "fmt"
- g "github.com/AllenDang/giu"
"image"
- "image/draw"
- _ "image/jpeg"
- "os"
-)
-
-const (
- gasConstant = 8.314472
- airMolarMass = 0.0289647 // kg/mol
-)
-
-var (
- defaultDisplacement = 2 * Litre
- defaultSpeed int32 = 2000
- defaultVE int32 = 80
- defaultTemperature = Temperature{25, Celcius}
-)
+ "image/color"
-var (
- defaultManufacturer = "borgwarner"
- defaultSeries = "efr"
- defaultModel = "6258"
+ "github.com/faiface/mainthread"
+ "volute/gui"
+ "volute/gui/layout"
+ "volute/gui/widget"
+ "volute/gui/win"
)
-// Number of data points on the compressor map.
-var numPoints = 1
-
-var (
- displacement = defaultDisplacement
- volumeUnitIndex int32
-
- // Angular crankshaft speed in RPM.
- speed = []int32{defaultSpeed}
-
- volumetricEfficiency = []int32{defaultVE}
-
- intakeAirTemperature = []Temperature{defaultTemperature}
- temperatureUnitIndex int32
-
- manifoldPressure = []Pressure{AtmosphericPressure()}
- pressureUnitIndex int32
-)
-
-var pressureRatio []float32
-
-func pressureRatioAt(point int) float32 {
- u := Pascal
- m := manifoldPressure[point] / u
- a := AtmosphericPressure() / u
- return float32(m / a)
-}
-func init() {
- pressureRatio = append(pressureRatio, pressureRatioAt(0))
-}
+const (
+ WIDTH = 800
+ HEIGHT = 600
-var (
- massFlowRateAir []MassFlowRate
- massFlowRateUnitIndex int32
+ POINTS = 6
)
-func massFlowRateAt(point int) MassFlowRate {
- rpm := float32(speed[point])
- disp := float32(displacement / CubicMetre)
- ve := float32(volumetricEfficiency[point]) / 100.0
- cubicMetresPerMin := (rpm / 2.0) * disp * ve
-
- iat, err := intakeAirTemperature[point].AsUnit(Kelvin)
- Check(err)
- pres := manifoldPressure[point] / Pascal
- molsPerMin := (float32(pres) * cubicMetresPerMin) / (gasConstant * iat)
-
- kgPerMin := molsPerMin * airMolarMass
-
- mfr := MassFlowRate(kgPerMin/60.0) * KilogramsPerSecond
- return mfr
-}
-func init() {
- massFlowRateAir = append(massFlowRateAir, massFlowRateAt(0))
-}
+func run() {
+ w, err := win.New(win.Title("volute"), win.Size(WIDTH, HEIGHT))
+ if err != nil {
+ panic(err)
+ }
+ mux, env := gui.NewMux(w)
-var (
- compressorImage *image.RGBA
- compressorTexture *g.Texture
- selectedCompressor Compressor
-)
+ var (
+ displacementChan = make(chan uint)
+ rpmChan = make([]chan uint, POINTS)
+ veChan = make([]chan uint, POINTS)
+ focus = NewFocus([]int{1, POINTS, POINTS})
+ )
+ for i := 0; i < POINTS; i++ {
+ rpmChan[i] = make(chan uint)
+ veChan[i] = make(chan uint)
+ }
-func init() {
- manufacturer := defaultManufacturer
- series := defaultSeries
- model := defaultModel
- c, ok := Compressors[manufacturer][series][model]
- if !ok {
- fmt.Printf("compressor.Compressors()[\"%s\"][\"%s\"][\"%s\"] does not exist.\n",
- manufacturer, series, model,
+ bounds := layout.Grid{
+ Rows: []int{2, 7, 7},
+ Background: color.Gray{255},
+ Gap: 1,
+ Split: split,
+ SplitRows: splitRows,
+ Margin: 0,
+ Border: 0,
+ BorderColor: color.Gray{16},
+ Flip: false,
+ }.Lay(image.Rect(0, 0, WIDTH, HEIGHT))
+
+ go widget.Label("displacement (cc)", bounds[0], mux.MakeEnv())
+ go widget.Input(
+ displacementChan,
+ bounds[1],
+ focus.widgets[0][0],
+ mux.MakeEnv(),
+ )
+ go widget.Label("speed (rpm)", bounds[2], mux.MakeEnv())
+ go widget.Label("VE (%)", bounds[3+POINTS], mux.MakeEnv())
+ for i := 0; i < POINTS; i++ {
+ go widget.Input(
+ rpmChan[i],
+ bounds[3+i],
+ focus.widgets[1][i],
+ mux.MakeEnv(),
+ )
+ go widget.Input(
+ veChan[i],
+ bounds[3+POINTS+1+i],
+ focus.widgets[2][i],
+ mux.MakeEnv(),
)
- os.Exit(1)
}
- setCompressor(c)
+ focus.widgets[focus.p.Y][focus.p.X] <- true
+
+Loop:
+ for {
+ select {
+ case _ = <-displacementChan:
+ case _ = <-rpmChan[0]:
+ case _ = <-veChan[0]:
+ case event, ok := <-env.Events():
+ if !ok { // channel closed
+ break Loop
+ }
+ switch event := event.(type) {
+ case win.WiClose:
+ break Loop
+ case win.KbType:
+ switch event.Rune {
+ case 'q':
+ break Loop
+ case 'h':
+ focus.Left()
+ case 'j':
+ focus.Down()
+ case 'k':
+ focus.Up()
+ case 'l':
+ focus.Right()
+ }
+ }
+ }
+ }
+ close(env.Draw())
+ close(displacementChan)
+ for i := range rpmChan {
+ close(rpmChan[i])
+ }
}
-func main() {
- wnd := g.NewMasterWindow("volute", 400, 200, 0)
-
- go updateCompImg()
- m := <-updatedCompImg
- g.EnqueueNewTextureFromRgba(m, func(tex *g.Texture) {
- compressorTexture = tex
- })
-
- wnd.Run(loop)
+func split(elements int, space int) []int {
+ bounds := make([]int, elements)
+ widths := []int{
+ widget.TextSize("displacement (cc)").X,
+ widget.TextSize("123456").X,
+ }
+ for i := 0; i < elements && space > 0; i++ {
+ bounds[i] = min(widths[min(i, len(widths)-1)], space)
+ space -= bounds[i]
+ }
+ return bounds
}
-func setCompressor(c Compressor) {
- f, err := os.Open(c.FileName)
- Check(err)
- defer f.Close()
-
- j, _, err := image.Decode(f)
- Check(err)
-
- b := j.Bounds()
- m := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
- draw.Draw(m, m.Bounds(), j, b.Min, draw.Src)
-
- selectedCompressor = c
- compressorImage = m
-
- go updateCompImg()
+func splitRows(elements int, space int) []int {
+ bounds := make([]int, elements)
+ height := widget.TextSize("1").Y
+ for i := 0; i < elements && space > 0; i++ {
+ bounds[i] = min(height, space)
+ space -= bounds[i]
+ }
+ return bounds
}
-func loop() {
- g.SingleWindow().Layout(
- displacementRow(),
- g.Table().
- Size(g.Auto, 190).
- Rows(
- speedRow(),
- volumetricEfficiencyRow(),
- intakeAirTemperatureRow(),
- manifoldPressureRow(),
- pressureRatioRow(),
- massFlowRateRow(),
- duplicateDeleteRow(),
- ).
- Columns(
- columns()...,
- ).
- Flags(g.TableFlagsSizingFixedFit),
- selectCompressor(),
- g.Custom(compressorWidget),
- )
+func main() {
+ mainthread.Run(run)
}
diff --git a/ui.go b/ui.go
deleted file mode 100644
index f132dd1..0000000
--- a/ui.go
+++ /dev/null
@@ -1,371 +0,0 @@
-package main
-
-import (
- "fmt"
- g "github.com/AllenDang/giu"
- "image"
- "image/color"
- "image/draw"
- "strconv"
-)
-
-var red = color.RGBA{255, 0, 0, 255}
-
-func displacementRow() *g.RowWidget {
- s := VolumeUnits[volumeUnitIndex]
- unit, err := ParseVolumeUnit(s)
- Check(err)
- engDisp := float32(displacement / unit)
- valWid, _ := g.CalcTextSize("12345.67")
- unitWid, _ := g.CalcTextSize(VolumeUnits[volumeUnitIndex])
- return g.Row(
- g.Label("Engine Displacement"),
- g.InputFloat(&engDisp).
- Format("%.2f").
- OnChange(func() {
- displacement = Volume(engDisp) * unit
- for i := 0; i < numPoints; i++ {
- massFlowRateAir[i] = massFlowRateAt(i)
- go updateCompImg()
- }
- }).
- Size(valWid),
- g.Combo(
- "",
- VolumeUnits[volumeUnitIndex],
- VolumeUnits,
- &volumeUnitIndex,
- ).Size(unitWid*2),
- )
-}
-
-func speedRow() *g.TableRowWidget {
- widgets := []g.Widget{
- g.Label("Engine Speed"),
- g.Label("rpm"),
- }
- for i := 0; i < numPoints; i++ {
- i := i
- widgets = append(
- widgets,
- g.InputInt(&speed[i]).OnChange(func() {
- massFlowRateAir[i] = massFlowRateAt(i)
- go updateCompImg()
- }),
- )
- }
- return g.TableRow(widgets...)
-}
-
-func volumetricEfficiencyRow() *g.TableRowWidget {
- widgets := []g.Widget{
- g.Label("Volumetric Efficiency"),
- g.Label("%"),
- }
- for i := 0; i < numPoints; i++ {
- i := i
- widgets = append(
- widgets,
- g.InputInt(&volumetricEfficiency[i]).OnChange(func() {
- massFlowRateAir[i] = massFlowRateAt(i)
- go updateCompImg()
- }),
- )
- }
- return g.TableRow(widgets...)
-}
-
-func intakeAirTemperatureRow() *g.TableRowWidget {
- wid, _ := g.CalcTextSize(TemperatureUnits[temperatureUnitIndex])
- widgets := []g.Widget{
- g.Label("Intake Air Temperature"),
- g.Combo(
- "",
- TemperatureUnits[temperatureUnitIndex],
- TemperatureUnits,
- &temperatureUnitIndex,
- ).OnChange(func() {
- s := TemperatureUnits[temperatureUnitIndex]
- u, err := ParseTemperatureUnit(s)
- Check(err)
-
- for i := range intakeAirTemperature {
- t, err := intakeAirTemperature[i].AsUnit(u)
- Check(err)
- intakeAirTemperature[i] = Temperature{t, u}
- }
- }).Size(wid * 2),
- }
- for i := 0; i < numPoints; i++ {
- i := i
- widgets = append(
- widgets,
- g.InputFloat(&intakeAirTemperature[i].Val).
- Format("%.2f").
- OnChange(func() {
- massFlowRateAir[i] = massFlowRateAt(i)
- go updateCompImg()
- }),
- )
- }
- return g.TableRow(widgets...)
-}
-
-func manifoldPressureRow() *g.TableRowWidget {
- s := PressureUnits[pressureUnitIndex]
- unit, err := ParsePressureUnit(s)
- Check(err)
- wid, _ := g.CalcTextSize(PressureUnits[pressureUnitIndex])
- widgets := []g.Widget{
- g.Label("Manifold Absolute Pressure"),
- g.Combo(
- "",
- PressureUnits[pressureUnitIndex],
- PressureUnits,
- &pressureUnitIndex,
- ).Size(wid * 2),
- }
- for i := 0; i < numPoints; i++ {
- i := i
- manPres := float32(manifoldPressure[i] / unit)
- widgets = append(
- widgets,
- g.InputFloat(&manPres).Format("%.2f").
- OnChange(func() {
- manifoldPressure[i] = Pressure(manPres * float32(unit))
- pressureRatio[i] = pressureRatioAt(i)
- massFlowRateAir[i] = massFlowRateAt(i)
- go updateCompImg()
- }),
- )
- }
- return g.TableRow(widgets...)
-}
-
-func pressureRatioRow() *g.TableRowWidget {
- widgets := []g.Widget{
- g.Label("Pressure Ratio"),
- g.Label(""),
- }
- for i := 0; i < numPoints; i++ {
- pr := strconv.FormatFloat(float64(pressureRatio[i]), 'f', 1, 32)
- widgets = append(
- widgets,
- g.Label(pr),
- )
- }
- return g.TableRow(widgets...)
-}
-
-func massFlowRateRow() *g.TableRowWidget {
- s := MassFlowRateUnits[massFlowRateUnitIndex]
- mfrUnit, err := ParseMassFlowRateUnit(s)
- Check(err)
-
- wid, _ := g.CalcTextSize(MassFlowRateUnits[massFlowRateUnitIndex])
- widgets := []g.Widget{
- g.Label("Mass Flow Rate"),
- g.Combo(
- "",
- MassFlowRateUnits[massFlowRateUnitIndex],
- MassFlowRateUnits,
- &massFlowRateUnitIndex,
- ).Size(wid * 2),
- }
- for i := 0; i < numPoints; i++ {
- mfr := strconv.FormatFloat(
- float64(massFlowRateAir[i]/mfrUnit),
- 'f',
- 3,
- 32,
- )
- widgets = append(
- widgets,
- g.Label(mfr),
- )
- }
- return g.TableRow(widgets...)
-}
-
-func duplicateDeleteRow() *g.TableRowWidget {
- widgets := []g.Widget{g.Label(""), g.Label("")}
- for i := 0; i < numPoints; i++ {
- i := i
- widgets = append(widgets, g.Row(
- g.Button("Duplicate").OnClick(func() {
- numPoints++
- speed = Insert(
- speed,
- speed[i],
- i,
- )
- volumetricEfficiency = Insert(
- volumetricEfficiency,
- volumetricEfficiency[i],
- i,
- )
- intakeAirTemperature = Insert(
- intakeAirTemperature,
- intakeAirTemperature[i],
- i,
- )
- manifoldPressure = Insert(
- manifoldPressure,
- manifoldPressure[i],
- i,
- )
- pressureRatio = Insert(
- pressureRatio,
- pressureRatio[i],
- i,
- )
- massFlowRateAir = Insert(
- massFlowRateAir,
- massFlowRateAir[i],
- i,
- )
- go updateCompImg()
- }),
- g.Button("Delete").OnClick(func() {
- if numPoints < 2 {
- return
- }
- numPoints--
- speed = Remove(speed, i)
- volumetricEfficiency = Remove(volumetricEfficiency, i)
- intakeAirTemperature = Remove(intakeAirTemperature, i)
- manifoldPressure = Remove(manifoldPressure, i)
- pressureRatio = Remove(pressureRatio, i)
- massFlowRateAir = Remove(massFlowRateAir, i)
- go updateCompImg()
- }),
- ))
- }
- return g.TableRow(widgets...)
-}
-
-func columns() []*g.TableColumnWidget {
- widgets := []*g.TableColumnWidget{
- g.TableColumn("Parameter"),
- g.TableColumn("Unit"),
- }
- for i := 0; i < numPoints; i++ {
- widgets = append(
- widgets,
- g.TableColumn(fmt.Sprintf("Point %d", i+1)),
- )
- }
- return widgets
-}
-
-var compressorTree []g.Widget
-
-func init() {
- for man := range Compressors {
- man := man // Manufacturer
- var serNodes []g.Widget
- for ser := range Compressors[man] {
- ser := ser // Series
- var modNodes []g.Widget
- for mod, c := range Compressors[man][ser] {
- mod := mod // Model
- c := c // Compressor
- modNodes = append(
- modNodes,
- g.Selectable(mod).OnClick(func() {
- go setCompressor(c)
- }),
- )
- }
- serNodes = append(
- serNodes,
- g.TreeNode(ser).Layout(modNodes...),
- )
- }
- manNode := g.TreeNode(man).Layout(serNodes...)
- compressorTree = append(compressorTree, manNode)
- }
-}
-
-func selectCompressor() g.Widget {
- return g.ComboCustom("Compressor", selectedCompressor.Name).
- Layout(compressorTree...)
-}
-
-var updatedCompImg = make(chan image.Image)
-
-func updateCompImg() {
- img := copyImage(compressorImage)
- for i := 0; i < numPoints; i++ {
- pos := pointPos(i)
- ps := img.Bounds().Dx() / 100 // Point size
- draw.Draw(img,
- image.Rect(pos.X-ps/2, pos.Y-ps/2, pos.X+ps/2, pos.Y+ps/2),
- &image.Uniform{red},
- image.ZP,
- draw.Src,
- )
- }
- updatedCompImg <- img
-}
-
-func compressorWidget() {
- select {
- case m := <-updatedCompImg:
- g.EnqueueNewTextureFromRgba(m, func(tex *g.Texture) {
- compressorTexture = tex
- })
- default:
- }
-
- canvas := g.GetCanvas()
- if compressorTexture != nil {
- winWidth, winHeight := g.GetAvailableRegion()
-
- bounds := compressorImage.Bounds()
- imWidth := float32(bounds.Dx())
- imHeight := float32(bounds.Dy())
-
- var ratio, xratio, yratio float32
- xratio = winWidth / imWidth
- yratio = winHeight / imHeight
- if xratio < yratio {
- ratio = xratio
- } else {
- ratio = yratio
- }
-
- x := int(imWidth * ratio)
- y := int(imHeight * ratio)
-
- canvas.AddImage(
- compressorTexture,
- image.Pt(0, 250),
- image.Pt(x, y),
- )
- }
-}
-
-func copyImage(old *image.RGBA) *image.RGBA {
- b := old.Bounds()
- img := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
- draw.Draw(img, img.Bounds(), old, b.Min, draw.Src)
- return img
-}
-
-// The position on the compressor map of an operating point.
-func pointPos(i int) (pos image.Point) {
- const unit = KilogramsPerSecond
- mfr := massFlowRateAir[i] / unit
- maxMfr := selectedCompressor.MaxFlow / unit
- min := selectedCompressor.MinX
- max := selectedCompressor.MaxX
- pos.X = min + int(float32(max-min)*float32(mfr/maxMfr))
-
- min = selectedCompressor.MinY
- max = selectedCompressor.MaxY
- pr := pressureRatio[i]
- maxPr := selectedCompressor.MaxPR
- pos.Y = min - int(float32((min-max))*((pr-1.0)/(maxPr-1.0)))
- return pos
-}