Merge pull request 'x-backend' (#2) from x-backend into main

Reviewed-on: #2
This commit is contained in:
Sasha Koshka 2022-11-15 04:40:24 +00:00
commit 2986c8fd03
18 changed files with 1064 additions and 607 deletions

View File

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

View File

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

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 {
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)

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

View File

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

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

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

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/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=

201
input.go
View File

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