x-backend #2

Merged
sashakoshka merged 34 commits from x-backend into main 2022-11-14 21:40:25 -07:00
18 changed files with 1064 additions and 607 deletions

View File

@ -2,11 +2,10 @@
Stone is a backend-agnostic application framework designed to: Stone is a backend-agnostic application framework designed to:
- Combine the simplicity of developing TUI programs with the input capabilities - Combine the simplicity and ease of development inherent to TUI programs with the extended capabilities of GUI programs
of GUI programs
- Be adaptable to run virtually anywhere - Be adaptable to run virtually anywhere
Currently, the only supported backend is 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 your own. Stone will automatically run through the list of registered backends
and instantiate the first one that doesn't throw an error. and instantiate the first one that doesn't throw an error.

View File

@ -1,5 +1,6 @@
package stone package stone
import "image"
import "image/color" import "image/color"
// Application represents an application. // Application represents an application.
@ -7,17 +8,11 @@ type Application struct {
DamageBuffer DamageBuffer
title string title string
icons []image.Image
backend Backend backend Backend
config Config 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 // 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. // broadcasts events. If no suitable backend can be found, an error is returned.
func (application *Application) Run () ( 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: 0x2E, G: 0x34, B: 0x40, A: 0xFF },
color.RGBA { R: 0xA8, G: 0x55, B: 0x5D, 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) application.backend, err = instantiateBackend(application)
if err != nil { return } if err != nil { return }
@ -49,6 +46,41 @@ func (application *Application) Run () (
return 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. // Config returns a pointer to the application's configuration.
func (application *Application) Config () (config *Config) { func (application *Application) Config () (config *Config) {
config = &application.config config = &application.config

BIN
assets/scaffold16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

BIN
assets/scaffold32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -5,8 +5,9 @@ import "errors"
type Backend interface { type Backend interface {
Run (channel chan(Event)) Run (channel chan(Event))
SetTitle (title string) SetTitle (title string) (err error)
SetIcon (icons []image.Image) SetIcon (icons []image.Image) (err error)
Draw ()
} }
type BackendFactory func (application *Application) (backend Backend, err error) type BackendFactory func (application *Application) (backend Backend, err error)

View File

@ -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)
}

142
backends/x/draw.go Normal file
View File

@ -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))
}
}

131
backends/x/event.go Normal file
View File

@ -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 { }
}

159
backends/x/factory.go Normal file
View File

@ -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)
}

187
backends/x/unicode.go Normal file
View File

@ -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
}

View File

@ -1,10 +1,13 @@
package x package x
// import "fmt"
import "sync"
import "image" import "image"
import "golang.org/x/image/font"
// import "github.com/jezek/xgb"
import "github.com/jezek/xgbutil" import "github.com/jezek/xgbutil"
// import "github.com/jezek/xgbutil/ewmh" import "github.com/jezek/xgbutil/ewmh"
import "github.com/jezek/xgbutil/xevent"
import "github.com/jezek/xgbutil/xwindow" import "github.com/jezek/xgbutil/xwindow"
import "github.com/jezek/xgbutil/xgraphics" import "github.com/jezek/xgbutil/xgraphics"
@ -18,47 +21,74 @@ type Backend struct {
canvas *xgraphics.Image canvas *xgraphics.Image
channel chan(stone.Event) channel chan(stone.Event)
ping struct { drawCellBounds bool
before chan(struct { }) drawBufferBounds bool
after chan(struct { })
quit chan(struct { }) drawLock sync.Mutex
font struct {
face font.Face
} }
colors [4]xgraphics.BGRA
metrics struct { metrics struct {
cellWidth int windowWidth int
cellHeight int windowHeight int
padding int cellWidth int
paddingX int cellHeight int
paddingY int padding int
descent int paddingX int
paddingY int
descent int
}
memory struct {
windowWidth int
windowHeight int
} }
} }
func (backend *Backend) Run (channel chan(stone.Event)) { func (backend *Backend) SetTitle (title string) (err error) {
backend.channel = channel err = ewmh.WmNameSet(backend.connection, backend.window.Id, title)
return
for { }
select {
case <- backend.ping.before:
<- backend.ping.after
case <- backend.ping.quit: func (backend *Backend) SetIcon (icons []image.Image) (err error) {
backend.shutDown() wmIcons := []ewmh.WmIcon { }
return
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) { err = ewmh.WmIconSet(backend.connection, backend.window.Id, wmIcons)
return
}
func (backend *Backend) SetIcon (icons []image.Image) {
}
func (backend *Backend) shutDown () {
backend.channel <- stone.EventQuit { }
} }
// calculateWindowSize calculates window bounds based on the internal buffer // calculateWindowSize calculates window bounds based on the internal buffer
@ -74,54 +104,56 @@ func (backend *Backend) calculateWindowSize () (x, y int) {
return return
} }
func (backend *Backend) calculateBufferSize () (width, height int) {
// factory instantiates an X backend. width =
func factory (application *stone.Application) (output stone.Backend, err error) { (backend.metrics.windowWidth - backend.metrics.padding * 2) /
backend := &Backend { backend.metrics.cellWidth
application: application, height =
config: application.Config(), (backend.metrics.windowHeight - backend.metrics.padding * 2) /
}
// calculate metrics
// TODO: base these off of font metrics
backend.metrics.cellWidth = 8
backend.metrics.cellHeight = 16
backend.metrics.padding =
backend.config.Padding() *
backend.metrics.cellHeight 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 return
} }
// init registers this backend when the program starts. func (backend *Backend) reallocateCanvas () {
func init () { if backend.canvas != nil {
stone.RegisterBackend(factory) 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
} }

View File

@ -3,8 +3,8 @@ package stone
type Event interface { } type Event interface { }
type EventQuit struct { } type EventQuit struct { }
type EventPress Button type EventPress struct { Button }
type EventRelease Button type EventRelease struct { Button }
type EventResize struct { } type EventResize struct { }
type EventMouseMove struct { type EventMouseMove struct {
X int X int

67
examples/draw/main.go Normal file
View File

@ -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()
}
}
}

View File

@ -3,40 +3,80 @@ package main
import "os" import "os"
import "fmt" import "fmt"
import "time" import "time"
import "image"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/stone" import "git.tebibyte.media/sashakoshka/stone"
import _ "git.tebibyte.media/sashakoshka/stone/backends/x" import _ "git.tebibyte.media/sashakoshka/stone/backends/x"
var application = &stone.Application { }
var currentTime = time.Time { }
var tickPing = make(chan(struct { }))
func main () { 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() channel, err := application.Run()
if err != nil { panic(err) } if err != nil { panic(err) }
currentTime := time.Time { } redraw()
go tick()
for { for {
event := <- channel select {
switch event.(type) { case <- tickPing:
case stone.EventQuit: redraw()
os.Exit(0)
case event := <- channel:
switch event.(type) {
case stone.EventQuit:
os.Exit(0)
case stone.EventResize: case stone.EventResize:
currentTime = time.Now() redraw()
}
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))
} }
} }
} }
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)
}
}

56
examples/type/main.go Normal file
View File

@ -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()
}
}
}

13
go.mod
View File

@ -3,19 +3,14 @@ module git.tebibyte.media/sashakoshka/stone
go 1.18 go 1.18
require ( 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 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 ( require (
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect
github.com/faiface/glhf v0.0.0-20181018222622-82a6317ac380 // indirect golang.org/x/text v0.4.0 // 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
) )

54
go.sum
View File

@ -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/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 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= 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/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw=
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/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk= 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/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 h1:+wPhoJD8EH0/bXipIq8Lc2z477jfox9zkXPCJdhvHj8=
github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66/go.mod h1:KACeV+k6b+aoLTVrrurywEbu3UpqoQcQywj4qX8aQKM= 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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff h1:+2zgJKVDVAz/BWSsuniCmU1kLCjL88Z8/kv39xCI9NQ= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 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.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=

201
input.go
View File

@ -1,143 +1,78 @@
package stone package stone
// These should be identical to the glfw keys import "unicode"
type Button int type Button int
const ( const (
KeyUnknown Button = -1 ButtonUnknown Button = 0
KeySpace Button = 162 KeyInsert Button = 1
KeyApostrophe Button = 161 KeyMenu Button = 2
KeyComma Button = 96 KeyPrintScreen Button = 3
KeyMinus Button = 93 KeyPause Button = 4
KeyPeriod Button = 92 KeyCapsLock Button = 5
KeySlash Button = 91 KeyScrollLock Button = 6
Key0 Button = 90 KeyNumLock Button = 7
Key1 Button = 89 KeyBackspace Button = 8
Key2 Button = 88 KeyTab Button = 9
Key3 Button = 87 KeyEnter Button = 10
Key4 Button = 86 KeyEscape Button = 11
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
KeyEscape Button = 348 KeyUp Button = 12
KeyEnter Button = 347 KeyDown Button = 13
KeyTab Button = 346 KeyLeft Button = 14
KeyBackspace Button = 345 KeyRight Button = 15
KeyInsert Button = 344 KeyPageUp Button = 16
KeyDelete Button = 343 KeyPageDown Button = 17
KeyRight Button = 342 KeyHome Button = 18
KeyLeft Button = 341 KeyEnd Button = 19
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
MouseButton1 Button = 0 KeyLeftShift Button = 20
MouseButton2 Button = 1 KeyRightShift Button = 21
MouseButton3 Button = 2 KeyLeftControl Button = 22
MouseButton4 Button = 3 KeyRightControl Button = 23
MouseButton5 Button = 4 KeyLeftAlt Button = 24
MouseButton6 Button = 5 KeyRightAlt Button = 25
MouseButton7 Button = 6 KeyLeftSuper Button = 26
MouseButton8 Button = 7 KeyRightSuper Button = 27
MouseButtonLeft Button = MouseButton1
MouseButtonRight Button = MouseButton2 KeyDelete Button = 127
MouseButtonMiddle Button = MouseButton3
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
}