322 lines
9.2 KiB
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)
|
|
}
|