aboutsummaryrefslogtreecommitdiffstats
path: root/gui/win/win.go
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2024-01-16 17:18:00 -0500
committerSam Anthony <sam@samanthony.xyz>2024-01-16 17:18:00 -0500
commit6a2e268df7e008579d1b6a0f2ef47597fcbe4862 (patch)
tree22ed525af378da3f176ca89c48c38406d7a50316 /gui/win/win.go
parentdb183cf7570e0f4e448ab5ced0ae41969261a815 (diff)
downloadvolute-6a2e268df7e008579d1b6a0f2ef47597fcbe4862.zip
add window focus event to gui module
Diffstat (limited to 'gui/win/win.go')
-rw-r--r--gui/win/win.go362
1 files changed, 362 insertions, 0 deletions
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()
+}