stone/backends/pixel/pixel.go

322 lines
9.2 KiB
Go

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