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