package x import "os" // import "fmt" import "sync" import "image" import "image/draw" import "golang.org/x/image/font" import "golang.org/x/image/math/fixed" import "golang.org/x/image/font/opentype" 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" import "github.com/flopp/go-findfont" type Backend struct { application *stone.Application config *stone.Config connection *xgbutil.XUtil window *xwindow.Window canvas *xgraphics.Image channel chan(stone.Event) drawLock sync.Mutex 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 } memory struct { windowWidth int windowHeight int } } func (backend *Backend) Run (channel chan(stone.Event)) { backend.channel = channel xevent.Main(backend.connection) backend.shutDown() } 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) 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) 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) backend.application.SetSize(backend.calculateBufferSize()) backend.channel <- stone.EventResize { } } } func (backend *Backend) handleButtonPress ( connection *xgbutil.XUtil, event xevent.ButtonPressEvent, ) { buttonEvent := *event.ButtonPressEvent backend.channel <- stone.EventPress(buttonEvent.Detail) } func (backend *Backend) handleButtonRelease ( connection *xgbutil.XUtil, event xevent.ButtonReleaseEvent, ) { buttonEvent := *event.ButtonReleaseEvent backend.channel <- stone.EventRelease(buttonEvent.Detail) } 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 { } } // 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) 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 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.canvas.XSurfaceSet(backend.window.Id) } 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) }} return } 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) // 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) 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 } // 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 } // 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()) // 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) 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 } 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)) } } // init registers this backend when the program starts. func init () { stone.RegisterBackend(factory) }