package x import "image" import "image/draw" import "image/color" import "golang.org/x/image/font" import "golang.org/x/image/math/fixed" import "golang.org/x/image/font/basicfont" import "github.com/jezek/xgb" import "github.com/jezek/xgbutil" import "github.com/jezek/xgb/xproto" import "github.com/jezek/xgbutil/ewmh" import "github.com/jezek/xgbutil/xevent" import "github.com/jezek/xgbutil/xwindow" import "github.com/jezek/xgbutil/xgraphics" import "git.tebibyte.media/sashakoshka/stone" type Backend struct { application *stone.Application config *stone.Config connection *xgbutil.XUtil window *xwindow.Window canvas *xgraphics.Image channel chan(stone.Event) ping struct { before chan(struct { }) after chan(struct { }) quit chan(struct { }) } font struct { face font.Face } colors [4]xgraphics.BGRA metrics struct { windowWidth int windowHeight int cellWidth int cellHeight int padding int paddingX int paddingY int descent int } } type fakeImage struct { color color.Color } func (fake fakeImage) ColorModel () (model color.Model) { model = color.RGBAModel return } func (fake fakeImage) Bounds () (bounds image.Rectangle) { bounds.Max = image.Point { X: 1024, Y: 1024, } return } func (fake fakeImage) At (x, y int) (pixel color.Color) { pixel = fake.color return } func (backend *Backend) Run (channel chan(stone.Event)) { backend.channel = channel for { select { case <- backend.ping.before: // if the queue is empty, don't dequeue anything because // it would cause a fucking segfault lmao (???) if !xevent.Empty(backend.connection) { event, err := xevent.Dequeue(backend.connection) if err != nil { // TODO: do something with err } if event != nil { backend.handleXEvent(event) } } <- backend.ping.after case <- backend.ping.quit: backend.shutDown() return } } } func (backend *Backend) SetTitle (title string) (err error) { err = ewmh.WmNameSet(backend.connection, backend.window.Id, title) 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) } err = ewmh.WmIconSet(backend.connection, backend.window.Id, wmIcons) return } func (backend *Backend) handleXEvent (event xgb.Event) { switch event.(type) { case xproto.ConfigureNotifyEvent: configureEvent := event.(xproto.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 { // compress events configureEvent = backend.compressConfigureNotify(configureEvent) // resize and rebind canvas backend.reallocateCanvas() } } } 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 { } } // calculateWindowSize calculates window bounds based on the internal buffer // size. func (backend *Backend) calculateWindowSize () (x, y int) { width, height := backend.application.Size() x = width * backend.metrics.cellWidth + backend.metrics.padding * 2 y = height * backend.metrics.cellHeight + backend.metrics.padding * 2 return } 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.drawRune(0, 0, 'T') backend.drawRune(1, 0, 'h') backend.drawRune(2, 0, 'e') backend.drawRune(4, 0, 'q') backend.drawRune(5, 0, 'u') backend.drawRune(6, 0, 'i') backend.drawRune(7, 0, 'c') backend.drawRune(8, 0, 'k') backend.canvas.XSurfaceSet(backend.window.Id) backend.canvas.XDraw() backend.canvas.XPaint(backend.window.Id) } func (backend *Backend) drawRune (x, y int, character rune) { _, mask, maskPoint, _, _ := backend.font.face.Glyph ( fixed.Point26_6 { }, character) draw.DrawMask ( backend.canvas, backend.boundsOfCell(x, y), fakeImage { color: backend.config.Color(stone.ColorForeground), }, image.Point { }, mask, maskPoint, draw.Over) } 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 } // factory instantiates an X backend. func factory (application *stone.Application) (output stone.Backend, err error) { backend := &Backend { application: application, config: application.Config(), } // load font // TODO: load this from a file 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 } // create the window backend.window.Create ( backend.connection.RootWin(), 0, 0, backend.metrics.windowWidth, backend.metrics.windowHeight, 0) backend.window.Map() backend.window.Listen(xproto.EventMaskStructureNotify) backend.SetTitle(application.Title()) backend.SetIcon(application.Icon()) // create a canvas backend.reallocateCanvas() // 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) }