diff --git a/README.md b/README.md index cb858b7..5b5c3ef 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,10 @@ Stone is a backend-agnostic application framework designed to: -- Combine the simplicity of developing TUI programs with the input capabilities - of GUI programs +- Combine the simplicity and ease of development inherent to TUI programs with the extended capabilities of GUI programs - Be adaptable to run virtually anywhere Currently, the only supported backend is -[pixel](https://github.com/faiface/pixel), but it is very easy to write and link +[X](https://github.com/jezek/xgbutil), but it is very easy to write and link your own. Stone will automatically run through the list of registered backends and instantiate the first one that doesn't throw an error. diff --git a/application.go b/application.go index 7fe44c4..206af06 100644 --- a/application.go +++ b/application.go @@ -1,5 +1,6 @@ package stone +import "image" import "image/color" // Application represents an application. @@ -7,17 +8,11 @@ type Application struct { DamageBuffer title string + icons []image.Image backend Backend config Config } -// SetTitle sets the application's title. If in a window, it will appear as the -// window's name. -func (application *Application) SetTitle (title string) { - application.title = title - application.backend.SetTitle(title) -} - // Run initializes the application, starts it, and then returns a channel that // broadcasts events. If no suitable backend can be found, an error is returned. func (application *Application) Run () ( @@ -37,8 +32,10 @@ func (application *Application) Run () ( color.RGBA { R: 0x2E, G: 0x34, B: 0x40, A: 0xFF }, color.RGBA { R: 0xA8, G: 0x55, B: 0x5D, A: 0xFF }, } + application.config.fontName = "" + application.config.fontSize = 11 - application.config.padding = 4 + application.config.padding = 2 application.backend, err = instantiateBackend(application) if err != nil { return } @@ -49,6 +46,41 @@ func (application *Application) Run () ( return } +// Draw "commits" changes made in the buffer to the display. +func (application *Application) Draw () { + application.backend.Draw() +} + +// SetTitle sets the application's title. If in a window, it will appear as the +// window's name. +func (application *Application) SetTitle (title string) (err error) { + application.title = title + if application.backend != nil { + err = application.backend.SetTitle(title) + } + + return +} + +func (application *Application) Title () (title string) { + title = application.title + return +} + +func (application *Application) SetIcon (sizes []image.Image) (err error) { + application.icons = sizes + if application.backend != nil { + err = application.backend.SetIcon(sizes) + } + + return +} + +func (application *Application) Icon () (sizes []image.Image) { + sizes = application.icons + return +} + // Config returns a pointer to the application's configuration. func (application *Application) Config () (config *Config) { config = &application.config diff --git a/assets/scaffold16.png b/assets/scaffold16.png new file mode 100644 index 0000000..6fb6890 Binary files /dev/null and b/assets/scaffold16.png differ diff --git a/assets/scaffold32.png b/assets/scaffold32.png new file mode 100644 index 0000000..abb193a Binary files /dev/null and b/assets/scaffold32.png differ diff --git a/backend.go b/backend.go index a6b5b98..96678fc 100644 --- a/backend.go +++ b/backend.go @@ -5,8 +5,9 @@ import "errors" type Backend interface { Run (channel chan(Event)) - SetTitle (title string) - SetIcon (icons []image.Image) + SetTitle (title string) (err error) + SetIcon (icons []image.Image) (err error) + Draw () } type BackendFactory func (application *Application) (backend Backend, err error) diff --git a/backends/pixel/pixel.go b/backends/pixel/pixel.go deleted file mode 100644 index 544173e..0000000 --- a/backends/pixel/pixel.go +++ /dev/null @@ -1,321 +0,0 @@ -package pixel - -import "time" -import "golang.org/x/image/font" -import "github.com/faiface/pixel" -import "github.com/faiface/pixel/text" -import "github.com/faiface/pixel/imdraw" -import "github.com/faiface/pixel/pixelgl" -import "golang.org/x/image/font/basicfont" -import "git.tebibyte.media/sashakoshka/stone" - -// Backend represents an instance of the pixel backend -type Backend struct { - window *pixelgl.Window - boundsDirty bool - windowBounds pixel.Vec - fontFace font.Face - application *stone.Application - config *stone.Config - backgroundStamper *imdraw.IMDraw - fontAtlas *text.Atlas - textDrawer *text.Text - showBounds bool - showCellBounds bool - - metrics struct { - cellWidth int - cellHeight int - padding int - paddingX int - paddingY int - descent int - } -} - -// Run satisfies the Run method of the Backend interface. Due to the nature of -// pixel, this will forcibly bind to the main thread. -func (backend *Backend) Run (callback func (application *stone.Application)) { - // backend.showBounds = true - // backend.showCellBounds = true - - if backend.fontFace == nil { - backend.fontFace = basicfont.Face7x13 - } - - backend.backgroundStamper = imdraw.New(nil) - backend.fontAtlas = text.NewAtlas(backend.fontFace, text.ASCII) - backend.textDrawer = text.New(pixel.V(0, 0), backend.fontAtlas) - - backend.metrics.descent = int(backend.fontAtlas.Descent()) - backend.metrics.cellHeight = int(backend.fontAtlas.LineHeight()) - // FIXME?: this might not be the best way to get the cell width - faceAdvance, ok := backend.fontFace.GlyphAdvance('M') - if ok { - backend.metrics.cellWidth = faceAdvance.Round() - } else { - backend.metrics.cellWidth = backend.metrics.cellHeight / 2 - } - - pixelgl.Run (func () { - // construct the window, and all that - var err error - backend.window, err = pixelgl.NewWindow (pixelgl.WindowConfig { - Resizable: true, - Undecorated: true, - VSync: true, - NoIconify: true, - Title: backend.application.Title(), - Bounds: backend.calculateWindowSize(), - }) - backend.Poll() - - // TODO: this should return the error and not panic - if err != nil { panic(err.Error()) } - callback(backend.application) - }) -} - -// Await fulfills the Await method of the Backend interface. -func (backend *Backend) Await (timeout time.Duration) (keepRunning bool) { - if backend.window == nil { - panic("call to Backend.Await before window exists") - } - - backend.draw() - backend.window.UpdateInputWait(timeout) - backend.processEvents() - keepRunning = !backend.window.Closed() - return -} - -// Poll fulfills the Poll method of the Backend interface. -func (backend *Backend) Poll () (keepRunning bool) { - if backend.window == nil { - panic("call to Backend.Poll before window exists") - } - - backend.draw() - backend.window.UpdateInput() - backend.processEvents() - keepRunning = !backend.window.Closed() - return -} - -// SetTitle fulfills the SetTitle method of the Backend interface. -func (backend *Backend) SetTitle (title string) { - if backend.window != nil { - backend.window.SetTitle(title) - } -} - -// JustPressed fulfills the JustPressed method of the Backend interface. -func (backend *Backend) JustPressed (button stone.Button) (pressed bool) { - pressed = backend.window.JustPressed(pixelgl.Button(button)) - return -} - -// JustReleased fulfills the JustReleased method of the Backend interface. -func (backend *Backend) JustReleased (button stone.Button) (released bool) { - released = backend.window.JustReleased(pixelgl.Button(button)) - return -} - -// Pressed fulfills the Pressed method of the Backend interface. -func (backend *Backend) Pressed (button stone.Button) (pressed bool) { - pressed = backend.window.Pressed(pixelgl.Button(button)) - return -} - -// Repeated fulfills the Repeated method of the Backend interface. -func (backend *Backend) Repeated (button stone.Button) (repeated bool) { - repeated = backend.window.Repeated(pixelgl.Button(button)) - return -} - -// Typed fulfills the Typed method of the Backend interface. -func (backend *Backend) Typed () (text string) { - text = backend.window.Typed() - return -} - -// Resized fulfills the Resized method of the Backend interface. -func (backend *Backend) Resized () (resized bool) { - resized = backend.boundsDirty - return -} - -// MousePosition fulfills the MousePosition method of the Backend interface. -func (backend *Backend) MousePosition () (x, y int) { - vector := backend.window.MousePosition() - x = int ( - (vector.X - float64(backend.metrics.paddingX)) / - float64(backend.metrics.cellWidth)) - y = int ( - (backend.windowBounds.Y - - vector.Y - - float64(backend.metrics.paddingY)) / - float64(backend.metrics.cellHeight)) - return -} - -// draw renders updates to the screen. -func (backend *Backend) draw () { - // didDrawing := false - width, height := backend.application.Size() - - if backend.boundsDirty { - backend.window.Clear ( - backend.config.Color(stone.ColorApplication)) - backend.boundsDirty = false - // didDrawing = true - } else { - // clear out dirty cells before drawing them. this is in an else - // block because if the bounds were dirty, we have already - // cleared the entire screen. - - backend.backgroundStamper.Clear() - backend.backgroundStamper.Color = - backend.config.Color(stone.ColorApplication) - for x := 0; x < width; x ++ { - for y := 0; y < height; y ++ { - clean := backend.application.Clean(x, y) - if clean { continue } - - backend.backgroundStamper.Push ( - backend.vectorAtPosition(x, y), - backend.vectorAtPosition(x + 1, y + 1)) - backend.backgroundStamper.Rectangle(0) - // didDrawing = true - - if backend.showCellBounds { - backend.backgroundStamper.Color = - backend.config.Color(stone.ColorForeground) - backend.backgroundStamper.Push ( - backend.vectorAtPosition(x, y), - backend.vectorAtPosition(x + 1, y + 1)) - backend.backgroundStamper.Rectangle(1) - backend.backgroundStamper.Color = - backend.config.Color(stone.ColorApplication) - } - } - } - backend.backgroundStamper.Draw(backend.window) - } - - backend.textDrawer.Clear() - backend.textDrawer.Color = - backend.config.Color(stone.ColorForeground) - for x := 0; x < width; x ++ { - for y := 0; y < height; y ++ { - clean := backend.application.Clean(x, y) - if clean { continue } - backend.application.MarkClean(x, y) - - cell := backend.application.Cell(x, y) - content := cell.Rune() - if content < 32 { continue } - - // draw cell - backend.textDrawer.Dot = pixel.V ( - float64 ( - x * backend.metrics.cellWidth + - backend.metrics.paddingX), - backend.windowBounds.Y - float64 ( - (y + 1) * backend.metrics.cellHeight + - backend.metrics.paddingY - - backend.metrics.descent)) - - backend.textDrawer.WriteRune(content) - backend.textDrawer.Draw(backend.window, pixel.IM) - } - } - - // draw a rectangle around the buffer if we are showing bounds - if backend.showBounds { - backend.backgroundStamper.Clear() - backend.backgroundStamper.Color = - backend.config.Color(stone.ColorBackground) - backend.backgroundStamper.Push ( - backend.vectorAtPosition(0, 0), - backend.vectorAtPosition(width, height)) - backend.backgroundStamper.Rectangle(1) - backend.backgroundStamper.Draw(backend.window) - } - - backend.window.SwapBuffers() -} - -// processEvents reacts to events recieved from pixel, resizing and -// recalculating things as need be. -func (backend *Backend) processEvents () { - newBounds := backend.window.Bounds().Max - backend.boundsDirty = backend.windowBounds != newBounds - backend.windowBounds = newBounds - - if backend.boundsDirty { - // calculate padding - backend.metrics.padding = - backend.config.Padding() * - backend.metrics.cellWidth - deadArea := float64(backend.metrics.padding * 2) - - // calculate new width and height for buffer - width := int ( - (backend.windowBounds.X - deadArea) / - float64(backend.metrics.cellWidth)) - height := int ( - (backend.windowBounds.Y - deadArea) / - float64(backend.metrics.cellHeight)) - backend.application.SetSize(width, height) - - // position buffer in center of screen - frameWidth := width * backend.metrics.cellWidth - frameHeight := height * backend.metrics.cellHeight - backend.metrics.paddingX = (int(backend.windowBounds.X) - frameWidth) / 2 - backend.metrics.paddingY = (int(backend.windowBounds.Y) - frameHeight) / 2 - } -} - -// vectorAtPosition generates a pixel vector at the top left of the specified -// cell. -func (backend *Backend) vectorAtPosition (x, y int) (vector pixel.Vec) { - vector = pixel.V ( - float64 ( - x * backend.metrics.cellWidth + - backend.metrics.paddingX), - backend.windowBounds.Y - float64 ( - y * backend.metrics.cellHeight + - backend.metrics.paddingY)) - return -} - -// calculateWindowSize calculates window bounds based on the internal buffer -// size. -func (backend *Backend) calculateWindowSize () (bounds pixel.Rect) { - width, height := backend.application.Size() - bounds = pixel.R ( - 0, 0, - float64 ( - width * backend.metrics.cellWidth + - backend.metrics.padding * 2), - float64 ( - height * backend.metrics.cellHeight + - backend.metrics.padding * 2)) - return -} - -// factory instantiates a pixel backend. -func factory (application *stone.Application) (output stone.Backend, err error) { - backend := &Backend { - application: application, - config: application.Config(), - } - output = backend - return -} - -// init registers this backend when the program starts. -func init () { - stone.RegisterBackend(factory) -} diff --git a/backends/x/draw.go b/backends/x/draw.go new file mode 100644 index 0000000..0a94d08 --- /dev/null +++ b/backends/x/draw.go @@ -0,0 +1,142 @@ +package x + +import "image" +import "image/draw" +import "golang.org/x/image/math/fixed" + +import "git.tebibyte.media/sashakoshka/stone" + +func (backend *Backend) Draw () { + backend.drawLock.Lock() + defer backend.drawLock.Unlock() + + boundsChanged := + backend.memory.windowWidth != backend.metrics.windowWidth || + backend.memory.windowHeight != backend.metrics.windowHeight + backend.memory.windowWidth = backend.metrics.windowWidth + backend.memory.windowHeight = backend.metrics.windowHeight + + if boundsChanged { + backend.reallocateCanvas() + backend.drawCells(true) + backend.canvas.XDraw() + backend.canvas.XPaint(backend.window.Id) + } else { + backend.updateWindowAreas(backend.drawCells(false)...) + } +} + +func (backend *Backend) updateWindowAreas (areas ...image.Rectangle) { + backend.canvas.XPaintRects(backend.window.Id, areas...) +} + +func (backend *Backend) drawRune (x, y int, character rune) { + // TODO: cache these draws as non-transparent buffers with the + // application background color as the background. that way, we won't + // need to redraw the characters *or* composite them. + + fillRectangle ( + &image.Uniform { + C: backend.config.Color(stone.ColorApplication), + }, + backend.canvas, + backend.boundsOfCell(x, y)) + + if character < 32 { return } + + origin := backend.originOfCell(x, y + 1) + destinationRectangle, mask, maskPoint, _, _ := backend.font.face.Glyph ( + fixed.Point26_6 { + X: fixed.I(origin.X), + Y: fixed.I(origin.Y - backend.metrics.descent), + }, + character) + + if backend.drawCellBounds { + strokeRectangle ( + &image.Uniform { + C: backend.config.Color(stone.ColorForeground), + }, + backend.canvas, + backend.boundsOfCell(x, y)) + } + + draw.DrawMask ( + backend.canvas, + destinationRectangle, + &image.Uniform { + C: backend.config.Color(stone.ColorForeground), + }, + image.Point { }, + mask, + maskPoint, + draw.Over) +} + +func (backend *Backend) drawCells (forceRedraw bool) (areas []image.Rectangle) { + width, height := backend.application.Size() + for y := 0; y < height; y ++ { + for x := 0; x < width; x ++ { + if !forceRedraw && backend.application.Clean(x, y) { continue } + backend.application.MarkClean(x, y) + + cell := backend.application.Cell(x, y) + content := cell.Rune() + + if forceRedraw && content < 32 { continue } + + areas = append(areas, backend.boundsOfCell(x, y)) + backend.drawRune(x, y, content) + }} + + if backend.drawBufferBounds && forceRedraw { + strokeRectangle ( + &image.Uniform { + C: backend.config.Color(stone.ColorForeground), + }, + backend.canvas, + image.Rectangle { + Min: backend.originOfCell(0, 0), + Max: backend.originOfCell(width, height), + }) + } + return +} + +func fillRectangle ( + source image.Image, + destination draw.Image, + bounds image.Rectangle, +) { + for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { + for x := bounds.Min.X; x < bounds.Max.X; x ++ { + destination.Set(x, y, source.At(x, y)) + }} +} + +func strokeRectangle ( + source image.Image, + destination draw.Image, + bounds image.Rectangle, +) { + x := 0 + y := bounds.Min.Y + for x = bounds.Min.X; x < bounds.Max.X; x ++ { + destination.Set(x, y, source.At(x, y)) + } + + y = bounds.Max.Y - 1 + for x = bounds.Min.X; x < bounds.Max.X; x ++ { + destination.Set(x, y, source.At(x, y)) + } + + x = bounds.Min.X + for y = bounds.Min.Y; y < bounds.Max.Y; y ++ { + destination.Set(x, y, source.At(x, y)) + } + + x = bounds.Max.X - 1 + for y = bounds.Min.Y; y < bounds.Max.Y; y ++ { + destination.Set(x, y, source.At(x, y)) + } +} diff --git a/backends/x/event.go b/backends/x/event.go new file mode 100644 index 0000000..e55c61e --- /dev/null +++ b/backends/x/event.go @@ -0,0 +1,131 @@ +package x + +import "image" + +import "github.com/jezek/xgbutil" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/xevent" + +import "git.tebibyte.media/sashakoshka/stone" + +func (backend *Backend) Run (channel chan(stone.Event)) { + backend.channel = channel + xevent.Main(backend.connection) + backend.shutDown() +} + + +func (backend *Backend) handleConfigureNotify ( + connection *xgbutil.XUtil, + event xevent.ConfigureNotifyEvent, +) { + configureEvent := *event.ConfigureNotifyEvent + + newWidth := int(configureEvent.Width) + newHeight := int(configureEvent.Height) + sizeChanged := + backend.metrics.windowWidth != newWidth || + backend.metrics.windowHeight != newHeight + backend.metrics.windowWidth = newWidth + backend.metrics.windowHeight = newHeight + + if sizeChanged { + configureEvent = + backend.compressConfigureNotify(configureEvent) + + // resize buffer + width, height := backend.calculateBufferSize() + backend.application.SetSize(width, height) + + // position buffer in the center of the screen + frameWidth := width * backend.metrics.cellWidth + frameHeight := height * backend.metrics.cellHeight + backend.metrics.paddingX = + (backend.metrics.windowWidth - frameWidth) / 2 + backend.metrics.paddingY = + (backend.metrics.windowHeight - frameHeight) / 2 + + backend.channel <- stone.EventResize { } + } +} + +func (backend *Backend) handleButtonPress ( + connection *xgbutil.XUtil, + event xevent.ButtonPressEvent, +) { + buttonEvent := *event.ButtonPressEvent + backend.channel <- stone.EventPress { + Button: stone.Button(buttonEvent.Detail + 127), + } +} + +func (backend *Backend) handleButtonRelease ( + connection *xgbutil.XUtil, + event xevent.ButtonReleaseEvent, +) { + buttonEvent := *event.ButtonReleaseEvent + backend.channel <- stone.EventRelease { + Button: stone.Button(buttonEvent.Detail + 127), + } +} + +func (backend *Backend) handleKeyPress ( + connection *xgbutil.XUtil, + event xevent.KeyPressEvent, +) { + keyEvent := *event.KeyPressEvent + button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State) + backend.channel <- stone.EventPress { Button: button } +} + +func (backend *Backend) handleKeyRelease ( + connection *xgbutil.XUtil, + event xevent.KeyReleaseEvent, +) { + keyEvent := *event.KeyReleaseEvent + button := backend.keycodeToButton(keyEvent.Detail, keyEvent.State) + backend.channel <- stone.EventRelease { Button: button } +} + +func (backend *Backend) handleMotionNotify ( + connection *xgbutil.XUtil, + event xevent.MotionNotifyEvent, +) { + motionEvent := *event.MotionNotifyEvent + x, y := backend.cellAt (image.Point { + X: int(motionEvent.EventX), + Y: int(motionEvent.EventY), + }) + backend.channel <- stone.EventMouseMove { + X: x, + Y: y, + } +} + +func (backend *Backend) compressConfigureNotify ( + firstEvent xproto.ConfigureNotifyEvent, +) ( + lastEvent xproto.ConfigureNotifyEvent, +) { + backend.connection.Sync() + xevent.Read(backend.connection, false) + lastEvent = firstEvent + + for index, untypedEvent := range xevent.Peek(backend.connection) { + if untypedEvent.Err != nil { continue } + + typedEvent, ok := untypedEvent.Event.(xproto.ConfigureNotifyEvent) + if !ok { continue } + + lastEvent = typedEvent + defer func (index int) { + xevent.DequeueAt(backend.connection, index) + } (index) + } + + return +} + +func (backend *Backend) shutDown () { + backend.channel <- stone.EventQuit { } +} diff --git a/backends/x/factory.go b/backends/x/factory.go new file mode 100644 index 0000000..c19fd51 --- /dev/null +++ b/backends/x/factory.go @@ -0,0 +1,159 @@ +package x + +import "os" +import "golang.org/x/image/font" +import "golang.org/x/image/font/opentype" +import "golang.org/x/image/font/basicfont" + +import "github.com/jezek/xgbutil" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/icccm" +import "github.com/jezek/xgbutil/xevent" +import "github.com/jezek/xgbutil/xwindow" +import "github.com/jezek/xgbutil/keybind" +import "github.com/jezek/xgbutil/xgraphics" + +import "git.tebibyte.media/sashakoshka/stone" + +import "github.com/flopp/go-findfont" + +// factory instantiates an X backend. +func factory (application *stone.Application) (output stone.Backend, err error) { + backend := &Backend { + application: application, + config: application.Config(), + } + + // load font + backend.font.face = findAndLoadFont ( + backend.config.FontName(), + float64(backend.config.FontSize())) + if backend.font.face == nil { + backend.font.face = basicfont.Face7x13 + } + + // pre-calculate colors + for index := 0; index < len(backend.colors); index ++ { + color := backend.config.Color(stone.Color(index)) + r, g, b, a := color.RGBA() + r >>= 8 + g >>= 8 + b >>= 8 + a >>= 8 + backend.colors[index] = xgraphics.BGRA { + R: uint8(r), + G: uint8(g), + B: uint8(b), + A: uint8(a), + } + } + + // calculate metrics + metrics := backend.font.face.Metrics() + glyphAdvance, _ := backend.font.face.GlyphAdvance('M') + backend.metrics.cellWidth = glyphAdvance.Round() + backend.metrics.cellHeight = metrics.Height.Round() + backend.metrics.descent = metrics.Descent.Round() + backend.metrics.padding = + backend.config.Padding() * + backend.metrics.cellHeight + backend.metrics.paddingX = backend.metrics.padding + backend.metrics.paddingY = backend.metrics.padding + backend.metrics.windowWidth, + backend.metrics.windowHeight = backend.calculateWindowSize() + + // connect to X + backend.connection, err = xgbutil.NewConn() + if err != nil { return } + backend.window, err = xwindow.Generate(backend.connection) + if err != nil { return } + keybind.Initialize(backend.connection) + + // create the window + backend.window.Create ( + backend.connection.RootWin(), + 0, 0, + backend.metrics.windowWidth, backend.metrics.windowHeight, + 0) + backend.window.Map() + // TODO: also listen to mouse movement (compressed) and mouse and + // keyboard buttons (uncompressed) + err = backend.window.Listen ( + xproto.EventMaskStructureNotify, + xproto.EventMaskPointerMotion, + xproto.EventMaskKeyPress, + xproto.EventMaskKeyRelease, + xproto.EventMaskButtonPress, + xproto.EventMaskButtonRelease) + backend.SetTitle(application.Title()) + backend.SetIcon(application.Icon()) + if err != nil { return } + + // set minimum dimensions + minWidth := + backend.metrics.cellWidth + backend.metrics.padding * 2 + minHeight := + backend.metrics.cellHeight + backend.metrics.padding * 2 + err = icccm.WmNormalHintsSet ( + backend.connection, + backend.window.Id, + &icccm.NormalHints { + Flags: icccm.SizeHintPMinSize, + MinWidth: uint(minWidth), + MinHeight: uint(minHeight), + }) + if err != nil { return } + + // create a canvas + backend.reallocateCanvas() + + // attatch graceful close handler + backend.window.WMGracefulClose (func (window *xwindow.Window) { + backend.window.Destroy() + backend.shutDown() + }) + + // attatch event handlers + xevent.ConfigureNotifyFun(backend.handleConfigureNotify). + Connect(backend.connection, backend.window.Id) + xevent.ButtonPressFun(backend.handleButtonPress). + Connect(backend.connection, backend.window.Id) + xevent.ButtonReleaseFun(backend.handleButtonRelease). + Connect(backend.connection, backend.window.Id) + xevent.MotionNotifyFun(backend.handleMotionNotify). + Connect(backend.connection, backend.window.Id) + xevent.KeyPressFun(backend.handleKeyPress). + Connect(backend.connection, backend.window.Id) + xevent.KeyReleaseFun(backend.handleKeyRelease). + Connect(backend.connection, backend.window.Id) + + // uncomment these to draw debug bounds + // backend.drawCellBounds = true + // backend.drawBufferBounds = true + + output = backend + return +} + +func findAndLoadFont (name string, size float64) (face font.Face) { + if name == "" { return } + fontPath, err := findfont.Find(name) + if err != nil { return } + fontFile, err := os.Open(fontPath) + if err != nil { return } + fontObject, err := opentype.ParseReaderAt(fontFile) + if err != nil { return } + face, err = opentype.NewFace (fontObject, &opentype.FaceOptions { + Size: size, + DPI: 96, + Hinting: font.HintingFull, + }) + if err != nil { face = nil } + + return +} + +// init registers this backend when the program starts. +func init () { + stone.RegisterBackend(factory) +} diff --git a/backends/x/unicode.go b/backends/x/unicode.go new file mode 100644 index 0000000..ecfa329 --- /dev/null +++ b/backends/x/unicode.go @@ -0,0 +1,187 @@ +package x + +import "unicode" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/keybind" +import "git.tebibyte.media/sashakoshka/stone" + +// when making changes to this file, look at keysymdef.h and +// https://tronche.com/gui/x/xlib/input/keyboard-encoding.html + +var buttonCodeTable = map[xproto.Keysym] stone.Button { + 0xFFFFFF: stone.ButtonUnknown, + + 0xFF63: stone.KeyInsert, + 0xFF67: stone.KeyMenu, + 0xFF61: stone.KeyPrintScreen, + 0xFF6B: stone.KeyPause, + 0xFFE5: stone.KeyCapsLock, + 0xFF14: stone.KeyScrollLock, + 0xFF7F: stone.KeyNumLock, + 0xFF08: stone.KeyBackspace, + 0xFF09: stone.KeyTab, + 0xFF0D: stone.KeyEnter, + 0xFF1B: stone.KeyEscape, + + 0xFF52: stone.KeyUp, + 0xFF54: stone.KeyDown, + 0xFF51: stone.KeyLeft, + 0xFF53: stone.KeyRight, + 0xFF55: stone.KeyPageUp, + 0xFF56: stone.KeyPageDown, + 0xFF50: stone.KeyHome, + 0xFF57: stone.KeyEnd, + + 0xFFE1: stone.KeyLeftShift, + 0xFFE2: stone.KeyRightShift, + 0xFFE3: stone.KeyLeftControl, + 0xFFE4: stone.KeyRightControl, + 0xFFE9: stone.KeyLeftAlt, + 0xFFEA: stone.KeyRightAlt, + 0xFFEB: stone.KeyLeftSuper, + 0xFFEC: stone.KeyRightSuper, + + 0xFFFF: stone.KeyDelete, + + 0xFFBE: stone.KeyF1, + 0xFFBF: stone.KeyF2, + 0xFFC0: stone.KeyF3, + 0xFFC1: stone.KeyF4, + 0xFFC2: stone.KeyF5, + 0xFFC3: stone.KeyF6, + 0xFFC4: stone.KeyF7, + 0xFFC5: stone.KeyF8, + 0xFFC6: stone.KeyF9, + 0xFFC7: stone.KeyF10, + 0xFFC8: stone.KeyF11, + 0xFFC9: stone.KeyF12, +} + +func (backend *Backend) keycodeToButton ( + keycode xproto.Keycode, + state uint16, +) ( + button stone.Button, +) { + // FIXME: also set shift to true if the lock modifier is on and the lock + // modifier is interpreted as shiftLock + shift := state & xproto.ModMaskShift > 0 + + // FIXME: only set this to true if the lock modifier is on and the lock + // modifier is interpreted as capsLock + capsLock := state & xproto.ModMaskLock > 0 + + symbol1 := keybind.KeysymGet(backend.connection, keycode, 0) + symbol2 := keybind.KeysymGet(backend.connection, keycode, 1) + symbol3 := keybind.KeysymGet(backend.connection, keycode, 2) + symbol4 := keybind.KeysymGet(backend.connection, keycode, 3) + + cased := false + + // third paragraph + switch { + case symbol2 == 0 && symbol3 == 0 && symbol4 == 0: + symbol3 = symbol1 + case symbol3 == 0 && symbol4 == 0: + symbol3 = symbol1 + symbol2 = symbol2 + case symbol4 == 0: + symbol4 = 0 + } + + symbol1Rune := keysymToRune(symbol1) + symbol2Rune := keysymToRune(symbol2) + symbol3Rune := keysymToRune(symbol3) + symbol4Rune := keysymToRune(symbol4) + + // FIXME: we ignore mode switch stuff + _ = symbol4Rune + + // fourth paragraph + if symbol2 == 0 { + upper := unicode.IsUpper(symbol1Rune) + lower := unicode.IsLower(symbol1Rune) + if upper || lower { + symbol1Rune = unicode.ToLower(symbol1Rune) + symbol2Rune = unicode.ToUpper(symbol1Rune) + cased = true + } else { + symbol2 = symbol1 + } + } + if symbol4 == 0 { + upper := unicode.IsUpper(symbol3Rune) + lower := unicode.IsLower(symbol3Rune) + if upper || lower { + symbol3Rune = unicode.ToLower(symbol3Rune) + symbol4Rune = unicode.ToUpper(symbol3Rune) + cased = true + } else { + symbol4 = symbol3 + } + } + + var selectedKeysym xproto.Keysym + var selectedRune rune + + // big ol list in the middle + switch { + // FIXME: take into account numlock + case !shift && !capsLock: + selectedKeysym = symbol1 + selectedRune = symbol1Rune + + case !shift && capsLock: + if cased && unicode.IsLower(symbol1Rune) { + selectedRune = symbol2Rune + } else { + selectedKeysym = symbol1 + selectedRune = symbol1Rune + } + + case shift && capsLock: + if cased && unicode.IsLower(symbol2Rune) { + selectedRune = unicode.ToUpper(symbol2Rune) + } else { + selectedKeysym = symbol2 + selectedRune = symbol2Rune + } + + case shift: + selectedKeysym = symbol2 + selectedRune = symbol2Rune + } + + // look up in table + var isControl bool + button, isControl = buttonCodeTable[selectedKeysym] + + // if it wasn't found, + if !isControl { + button = stone.Button(selectedRune) + } + + return +} + +func keysymToRune (keysym xproto.Keysym) (character rune) { + // X keysyms like 0xFF.. or 0xFE.. are non-character keys. these cannot + // be converted so we return a zero. + if (keysym >> 8) == 0xFF || (keysym >> 8) == 0xFE { + character = 0 + return + } + + // some X keysyms have a single bit set to 1 here. i believe this is to + // prevent conflicts with existing codes. if we mask it off we will get + // a correct utf-32 code point. + if keysym & 0xF000000 == 0x1000000 { + character = rune(keysym & 0x0111111) + return + } + + // if none of these things happened, we can safely (i think) assume that + // the keysym is an exact utf-32 code point. + character = rune(keysym) + return +} diff --git a/backends/x/x.go b/backends/x/x.go index 517c8b3..c930c84 100644 --- a/backends/x/x.go +++ b/backends/x/x.go @@ -1,10 +1,13 @@ package x +// import "fmt" +import "sync" import "image" +import "golang.org/x/image/font" +// import "github.com/jezek/xgb" import "github.com/jezek/xgbutil" -// import "github.com/jezek/xgbutil/ewmh" -import "github.com/jezek/xgbutil/xevent" +import "github.com/jezek/xgbutil/ewmh" import "github.com/jezek/xgbutil/xwindow" import "github.com/jezek/xgbutil/xgraphics" @@ -18,47 +21,74 @@ type Backend struct { canvas *xgraphics.Image channel chan(stone.Event) - ping struct { - before chan(struct { }) - after chan(struct { }) - quit chan(struct { }) + drawCellBounds bool + drawBufferBounds bool + + drawLock sync.Mutex + + font struct { + face font.Face } + + colors [4]xgraphics.BGRA metrics struct { - cellWidth int - cellHeight int - padding int - paddingX int - paddingY int - descent int + windowWidth int + windowHeight int + cellWidth int + cellHeight int + padding int + paddingX int + paddingY int + descent int + } + + memory struct { + windowWidth int + windowHeight int } } -func (backend *Backend) Run (channel chan(stone.Event)) { - backend.channel = channel - - for { - select { - case <- backend.ping.before: - <- backend.ping.after +func (backend *Backend) SetTitle (title string) (err error) { + err = ewmh.WmNameSet(backend.connection, backend.window.Id, title) + return +} - case <- backend.ping.quit: - backend.shutDown() - return +func (backend *Backend) SetIcon (icons []image.Image) (err error) { + wmIcons := []ewmh.WmIcon { } + + for _, icon := range icons { + width := icon.Bounds().Max.X + height := icon.Bounds().Max.Y + wmIcon := ewmh.WmIcon { + Width: uint(width), + Height: uint(height), + Data: make ([]uint, width * height), } + + // manually convert image data beacuse of course we have to do + // this + index := 0 + for y := 0; y < height; y ++ { + for x := 0; x < width; x ++ { + r, g, b, a := icon.At(x, y).RGBA() + r >>= 8 + g >>= 8 + b >>= 8 + a >>= 8 + wmIcon.Data[index] = + (uint(a) << 24) | + (uint(r) << 16) | + (uint(g) << 8) | + (uint(b) << 0) + index ++ + }} + + wmIcons = append(wmIcons, wmIcon) } -} -func (backend *Backend) SetTitle (title string) { - -} - -func (backend *Backend) SetIcon (icons []image.Image) { - -} - -func (backend *Backend) shutDown () { - backend.channel <- stone.EventQuit { } + err = ewmh.WmIconSet(backend.connection, backend.window.Id, wmIcons) + return } // calculateWindowSize calculates window bounds based on the internal buffer @@ -74,54 +104,56 @@ func (backend *Backend) calculateWindowSize () (x, y int) { return } - -// factory instantiates an X backend. -func factory (application *stone.Application) (output stone.Backend, err error) { - backend := &Backend { - application: application, - config: application.Config(), - } - - // calculate metrics - // TODO: base these off of font metrics - backend.metrics.cellWidth = 8 - backend.metrics.cellHeight = 16 - backend.metrics.padding = - backend.config.Padding() * +func (backend *Backend) calculateBufferSize () (width, height int) { + width = + (backend.metrics.windowWidth - backend.metrics.padding * 2) / + backend.metrics.cellWidth + height = + (backend.metrics.windowHeight - backend.metrics.padding * 2) / backend.metrics.cellHeight - backend.metrics.paddingX = backend.metrics.padding - backend.metrics.paddingY = backend.metrics.padding - - // connect to X - backend.connection, err = xgbutil.NewConn() - if err != nil { return } - backend.window, err = xwindow.Generate(backend.connection) - if err != nil { return } - - // create the window - windowWidth, windowHeight := backend.calculateWindowSize() - backend.window.Create ( - backend.connection.RootWin(), - 0, 0, windowWidth, windowHeight, - 0) - backend.window.Map() - - // attatch graceful close handler - backend.window.WMGracefulClose (func (window *xwindow.Window) { - backend.window.Destroy() - backend.shutDown() - }) - - // start event loop - backend.ping.before, - backend.ping.after, - backend.ping.quit = xevent.MainPing(backend.connection) - - output = backend return } -// init registers this backend when the program starts. -func init () { - stone.RegisterBackend(factory) +func (backend *Backend) reallocateCanvas () { + if backend.canvas != nil { + backend.canvas.Destroy() + } + backend.canvas = xgraphics.New ( + backend.connection, + image.Rect ( + 0, 0, + backend.metrics.windowWidth, + backend.metrics.windowHeight)) + backend.canvas.For (func (x, y int) xgraphics.BGRA { + return backend.colors[stone.ColorApplication] + }) + + backend.canvas.XSurfaceSet(backend.window.Id) +} + +func (backend *Backend) cellAt (onScreen image.Point) (x, y int) { + x = (onScreen.X - backend.metrics.paddingX) / backend.metrics.cellWidth + y = (onScreen.Y - backend.metrics.paddingY) / backend.metrics.cellHeight + return +} + +func (backend *Backend) cellSubImage (x, y int) (cell *xgraphics.Image) { + cell = backend.canvas.SubImage(backend.boundsOfCell(x, y)).(*xgraphics.Image) + return +} + +func (backend *Backend) originOfCell (x, y int) (origin image.Point) { + origin = image.Point { + X: x * backend.metrics.cellWidth + backend.metrics.paddingX, + Y: y * backend.metrics.cellHeight + backend.metrics.paddingY, + } + return +} + +func (backend *Backend) boundsOfCell (x, y int) (bounds image.Rectangle) { + bounds = image.Rectangle { + Min: backend.originOfCell(x, y), + Max: backend.originOfCell(x + 1, y + 1), + } + return } diff --git a/event.go b/event.go index f60cff2..8fbefb7 100644 --- a/event.go +++ b/event.go @@ -3,8 +3,8 @@ package stone type Event interface { } type EventQuit struct { } -type EventPress Button -type EventRelease Button +type EventPress struct { Button } +type EventRelease struct { Button } type EventResize struct { } type EventMouseMove struct { X int diff --git a/examples/draw/main.go b/examples/draw/main.go new file mode 100644 index 0000000..7d78da8 --- /dev/null +++ b/examples/draw/main.go @@ -0,0 +1,67 @@ +package main + +import "os" +import "image" +import _ "image/png" +import "git.tebibyte.media/sashakoshka/stone" +import _ "git.tebibyte.media/sashakoshka/stone/backends/x" + +var application = &stone.Application { } +var mousePressed bool + +func main () { + application.SetTitle("hellorld") + application.SetSize(32, 16) + + iconFile16, err := os.Open("assets/scaffold16.png") + if err != nil { panic(err) } + icon16, _, err := image.Decode(iconFile16) + if err != nil { panic(err) } + iconFile16.Close() + iconFile32, err := os.Open("assets/scaffold32.png") + if err != nil { panic(err) } + icon32, _, err := image.Decode(iconFile32) + if err != nil { panic(err) } + iconFile16.Close() + + application.SetIcon([]image.Image { icon16, icon32 }) + + channel, err := application.Run() + if err != nil { panic(err) } + + application.Draw() + + for { + event := <- channel + switch event.(type) { + case stone.EventQuit: + os.Exit(0) + + case stone.EventPress: + button := event.(stone.EventPress).Button + if button == stone.MouseButtonLeft { + mousePressed = true + application.SetRune(0, 0, '+') + application.Draw() + } + + case stone.EventRelease: + button := event.(stone.EventRelease).Button + if button == stone.MouseButtonLeft { + mousePressed = false + application.SetRune(0, 0, 0) + application.Draw() + } + + case stone.EventMouseMove: + event := event.(stone.EventMouseMove) + if mousePressed { + application.SetRune(event.X, event.Y, '#') + application.Draw() + } + + case stone.EventResize: + application.Draw() + } + } +} diff --git a/examples/hello/main.go b/examples/hello/main.go index 1dd1f89..86444f0 100644 --- a/examples/hello/main.go +++ b/examples/hello/main.go @@ -3,40 +3,80 @@ package main import "os" import "fmt" import "time" +import "image" +import _ "image/png" import "git.tebibyte.media/sashakoshka/stone" import _ "git.tebibyte.media/sashakoshka/stone/backends/x" +var application = &stone.Application { } +var currentTime = time.Time { } +var tickPing = make(chan(struct { })) + func main () { - application := &stone.Application { } + application.SetTitle("hellorld") + application.SetSize(12, 2) + + iconFile16, err := os.Open("assets/scaffold16.png") + if err != nil { panic(err) } + icon16, _, err := image.Decode(iconFile16) + if err != nil { panic(err) } + iconFile16.Close() + iconFile32, err := os.Open("assets/scaffold32.png") + if err != nil { panic(err) } + icon32, _, err := image.Decode(iconFile32) + if err != nil { panic(err) } + iconFile16.Close() + + application.SetIcon([]image.Image { icon16, icon32 }) + channel, err := application.Run() if err != nil { panic(err) } - currentTime := time.Time { } + redraw() + go tick() for { - event := <- channel - switch event.(type) { - case stone.EventQuit: - os.Exit(0) + select { + case <- tickPing: + redraw() + + case event := <- channel: + switch event.(type) { + case stone.EventQuit: + os.Exit(0) - case stone.EventResize: - currentTime = time.Now() - - application.ResetDot() - fmt.Fprintln(application, "hellorld!") - - hour := currentTime.Hour() - minute := currentTime.Minute() - second := currentTime.Second() - - application.SetRune(0, 1, rune(hour / 10 + 48)) - application.SetRune(1, 1, rune(hour % 10 + 48)) - application.SetRune(2, 1, ':') - application.SetRune(3, 1, rune(minute / 10 + 48)) - application.SetRune(4, 1, rune(minute % 10 + 48)) - application.SetRune(5, 1, ':') - application.SetRune(6, 1, rune(second / 10 + 48)) - application.SetRune(7, 1, rune(second % 10 + 48)) + case stone.EventResize: + redraw() + } } } } + +func redraw () { + currentTime = time.Now() + + application.ResetDot() + fmt.Fprintln(application, "hellorld!") + + hour := currentTime.Hour() + minute := currentTime.Minute() + second := currentTime.Second() + + application.SetRune(0, 1, rune(hour / 10 + 48)) + application.SetRune(1, 1, rune(hour % 10 + 48)) + application.SetRune(2, 1, ':') + application.SetRune(3, 1, rune(minute / 10 + 48)) + application.SetRune(4, 1, rune(minute % 10 + 48)) + application.SetRune(5, 1, ':') + application.SetRune(6, 1, rune(second / 10 + 48)) + application.SetRune(7, 1, rune(second % 10 + 48)) + + application.Draw() +} + +func tick () { + for { + tickPing <- struct { } { } + time.Sleep(time.Second) + } +} diff --git a/examples/type/main.go b/examples/type/main.go new file mode 100644 index 0000000..1cda63e --- /dev/null +++ b/examples/type/main.go @@ -0,0 +1,56 @@ +package main + +import "os" +import "image" +import _ "image/png" +import "git.tebibyte.media/sashakoshka/stone" +import _ "git.tebibyte.media/sashakoshka/stone/backends/x" + +var application = &stone.Application { } +var caret = 0 + +func main () { + application.SetTitle("hellorld") + application.SetSize(32, 16) + + iconFile16, err := os.Open("assets/scaffold16.png") + if err != nil { panic(err) } + icon16, _, err := image.Decode(iconFile16) + if err != nil { panic(err) } + iconFile16.Close() + iconFile32, err := os.Open("assets/scaffold32.png") + if err != nil { panic(err) } + icon32, _, err := image.Decode(iconFile32) + if err != nil { panic(err) } + iconFile16.Close() + + application.SetIcon([]image.Image { icon16, icon32 }) + + channel, err := application.Run() + if err != nil { panic(err) } + + application.Draw() + + for { + event := <- channel + switch event.(type) { + case stone.EventQuit: + os.Exit(0) + + case stone.EventPress: + button := event.(stone.EventPress).Button + if button.Printable() { + application.SetRune(caret, 0, rune(button)) + caret ++ + width, _ := application.Size() + if caret >= width { + caret = 0 + } + application.Draw() + } + + case stone.EventResize: + application.Draw() + } + } +} diff --git a/go.mod b/go.mod index 7d11633..292faa2 100644 --- a/go.mod +++ b/go.mod @@ -3,19 +3,14 @@ module git.tebibyte.media/sashakoshka/stone go 1.18 require ( - github.com/faiface/pixel v0.10.0 + github.com/flopp/go-findfont v0.1.0 + github.com/jezek/xgb v1.1.0 github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 - golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff + golang.org/x/image v0.1.0 ) require ( github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect - github.com/faiface/glhf v0.0.0-20181018222622-82a6317ac380 // indirect - github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 // indirect - github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 // indirect - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 // indirect - github.com/go-gl/mathgl v0.0.0-20190416160123-c4601bc793c7 // indirect - github.com/jezek/xgb v1.1.0 // indirect - github.com/pkg/errors v0.8.1 // indirect + golang.org/x/text v0.4.0 // indirect ) diff --git a/go.sum b/go.sum index 6390802..5b46941 100644 --- a/go.sum +++ b/go.sum @@ -2,34 +2,36 @@ github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJ github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= -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/glhf v0.0.0-20181018222622-82a6317ac380 h1:FvZ0mIGh6b3kOITxUnxS3tLZMh7yEoHo75v3/AgUqg0= -github.com/faiface/glhf v0.0.0-20181018222622-82a6317ac380/go.mod h1:zqnPFFIuYFFxl7uH2gYByJwIVKG7fRqlqQCbzAnHs9g= -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/faiface/pixel v0.10.0 h1:EHm3ZdQw2Ck4y51cZqFfqQpwLqNHOoXwbNEc9Dijql0= -github.com/faiface/pixel v0.10.0/go.mod h1:lU0YYcW77vL0F1CG8oX51GXurymL45MXd57otHNLK7A= -github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw= -github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 h1:b+9H1GAsx5RsjvDFLoS5zkNBzIQMuVKUYQDmxU3N5XE= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/mathgl v0.0.0-20190416160123-c4601bc793c7 h1:THttjeRn1iiz69E875U6gAik8KTWk/JYAHoSVpUxBBI= -github.com/go-gl/mathgl v0.0.0-20190416160123-c4601bc793c7/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU= +github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 h1:+wPhoJD8EH0/bXipIq8Lc2z477jfox9zkXPCJdhvHj8= github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66/go.mod h1:KACeV+k6b+aoLTVrrurywEbu3UpqoQcQywj4qX8aQKM= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff h1:+2zgJKVDVAz/BWSsuniCmU1kLCjL88Z8/kv39xCI9NQ= -golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk= +golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/input.go b/input.go index 694a234..5476ab1 100644 --- a/input.go +++ b/input.go @@ -1,143 +1,78 @@ package stone -// These should be identical to the glfw keys +import "unicode" type Button int const ( - KeyUnknown Button = -1 + ButtonUnknown Button = 0 - KeySpace Button = 162 - KeyApostrophe Button = 161 - KeyComma Button = 96 - KeyMinus Button = 93 - KeyPeriod Button = 92 - KeySlash Button = 91 - Key0 Button = 90 - Key1 Button = 89 - Key2 Button = 88 - Key3 Button = 87 - Key4 Button = 86 - Key5 Button = 85 - Key6 Button = 84 - Key7 Button = 83 - Key8 Button = 82 - Key9 Button = 81 - KeySemicolon Button = 80 - KeyEqual Button = 79 - KeyA Button = 78 - KeyB Button = 77 - KeyC Button = 76 - KeyD Button = 75 - KeyE Button = 74 - KeyF Button = 73 - KeyG Button = 72 - KeyH Button = 71 - KeyI Button = 70 - KeyJ Button = 69 - KeyK Button = 68 - KeyL Button = 67 - KeyM Button = 66 - KeyN Button = 65 - KeyO Button = 61 - KeyP Button = 59 - KeyQ Button = 57 - KeyR Button = 56 - KeyS Button = 55 - KeyT Button = 54 - KeyU Button = 53 - KeyV Button = 52 - KeyW Button = 51 - KeyX Button = 50 - KeyY Button = 49 - KeyZ Button = 48 - KeyLeftBracket Button = 47 - KeyBackslash Button = 46 - KeyRightBracket Button = 45 - KeyGraveAccent Button = 44 - KeyWorld1 Button = 39 - KeyWorld2 Button = 32 + KeyInsert Button = 1 + KeyMenu Button = 2 + KeyPrintScreen Button = 3 + KeyPause Button = 4 + KeyCapsLock Button = 5 + KeyScrollLock Button = 6 + KeyNumLock Button = 7 + KeyBackspace Button = 8 + KeyTab Button = 9 + KeyEnter Button = 10 + KeyEscape Button = 11 - KeyEscape Button = 348 - KeyEnter Button = 347 - KeyTab Button = 346 - KeyBackspace Button = 345 - KeyInsert Button = 344 - KeyDelete Button = 343 - KeyRight Button = 342 - KeyLeft Button = 341 - KeyDown Button = 340 - KeyUp Button = 336 - KeyPageUp Button = 335 - KeyPageDown Button = 334 - KeyHome Button = 333 - KeyEnd Button = 332 - KeyCapsLock Button = 331 - KeyScrollLock Button = 330 - KeyNumLock Button = 329 - KeyPrintScreen Button = 328 - KeyPause Button = 327 - KeyF1 Button = 326 - KeyF2 Button = 325 - KeyF3 Button = 324 - KeyF4 Button = 323 - KeyF5 Button = 322 - KeyF6 Button = 321 - KeyF7 Button = 320 - KeyF8 Button = 314 - KeyF9 Button = 313 - KeyF10 Button = 312 - KeyF11 Button = 311 - KeyF12 Button = 310 - KeyF13 Button = 309 - KeyF14 Button = 308 - KeyF15 Button = 307 - KeyF16 Button = 306 - KeyF17 Button = 305 - KeyF18 Button = 304 - KeyF19 Button = 303 - KeyF20 Button = 302 - KeyF21 Button = 301 - KeyF22 Button = 300 - KeyF23 Button = 299 - KeyF24 Button = 298 - KeyF25 Button = 297 - KeyKP0 Button = 296 - KeyKP1 Button = 295 - KeyKP2 Button = 294 - KeyKP3 Button = 293 - KeyKP4 Button = 292 - KeyKP5 Button = 291 - KeyKP6 Button = 290 - KeyKP7 Button = 284 - KeyKP8 Button = 283 - KeyKP9 Button = 282 - KeyKPDecimal Button = 281 - KeyKPDivide Button = 280 - KeyKPMultiply Button = 269 - KeyKPSubtract Button = 268 - KeyKPAdd Button = 267 - KeyKPEnter Button = 266 - KeyKPEqual Button = 265 - KeyLeftShift Button = 264 - KeyLeftControl Button = 263 - KeyLeftAlt Button = 262 - KeyLeftSuper Button = 261 - KeyRightShift Button = 260 - KeyRightControl Button = 259 - KeyRightAlt Button = 258 - KeyRightSuper Button = 257 - KeyMenu Button = 256 + KeyUp Button = 12 + KeyDown Button = 13 + KeyLeft Button = 14 + KeyRight Button = 15 + KeyPageUp Button = 16 + KeyPageDown Button = 17 + KeyHome Button = 18 + KeyEnd Button = 19 - MouseButton1 Button = 0 - MouseButton2 Button = 1 - MouseButton3 Button = 2 - MouseButton4 Button = 3 - MouseButton5 Button = 4 - MouseButton6 Button = 5 - MouseButton7 Button = 6 - MouseButton8 Button = 7 - MouseButtonLeft Button = MouseButton1 - MouseButtonRight Button = MouseButton2 - MouseButtonMiddle Button = MouseButton3 + KeyLeftShift Button = 20 + KeyRightShift Button = 21 + KeyLeftControl Button = 22 + KeyRightControl Button = 23 + KeyLeftAlt Button = 24 + KeyRightAlt Button = 25 + KeyLeftSuper Button = 26 + KeyRightSuper Button = 27 + + KeyDelete Button = 127 + + MouseButton1 Button = 128 + MouseButton2 Button = 129 + MouseButton3 Button = 130 + MouseButton4 Button = 131 + MouseButton5 Button = 132 + MouseButton6 Button = 133 + MouseButton7 Button = 134 + MouseButton8 Button = 135 + MouseButton9 Button = 136 + MouseButtonLeft Button = MouseButton1 + MouseButtonMiddle Button = MouseButton2 + MouseButtonRight Button = MouseButton3 + MouseButtonScrollUp Button = MouseButton4 + MouseButtonScrollDown Button = MouseButton5 + MouseButtonScrollLeft Button = MouseButton6 + MouseButtonScrollRight Button = MouseButton7 + MouseButtonBack Button = MouseButton8 + MouseButtonForward Button = MouseButton9 + + KeyF1 Button = 144 + KeyF2 Button = 145 + KeyF3 Button = 146 + KeyF4 Button = 147 + KeyF5 Button = 148 + KeyF6 Button = 149 + KeyF7 Button = 150 + KeyF8 Button = 151 + KeyF9 Button = 152 + KeyF10 Button = 153 + KeyF11 Button = 154 + KeyF12 Button = 155 ) + +func (button Button) Printable () (printable bool) { + printable = unicode.IsPrint(rune(button)) + return +}