From 82caf1efd8d889ecc3e328dfa7758607f3ba1e95 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sun, 13 Nov 2022 22:44:19 -0500 Subject: [PATCH] Split X backend into multiple files --- backends/x/draw.go | 128 +++++++++++++++ backends/x/event.go | 107 +++++++++++++ backends/x/factory.go | 151 ++++++++++++++++++ backends/x/x.go | 351 ------------------------------------------ 4 files changed, 386 insertions(+), 351 deletions(-) create mode 100644 backends/x/draw.go create mode 100644 backends/x/event.go create mode 100644 backends/x/factory.go diff --git a/backends/x/draw.go b/backends/x/draw.go new file mode 100644 index 0000000..82ff82b --- /dev/null +++ b/backends/x/draw.go @@ -0,0 +1,128 @@ +package x + +import "image" +import "image/draw" +import "golang.org/x/image/math/fixed" + +import "git.tebibyte.media/sashakoshka/stone" + +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) 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) 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 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)) + } +} diff --git a/backends/x/event.go b/backends/x/event.go new file mode 100644 index 0000000..76a5e61 --- /dev/null +++ b/backends/x/event.go @@ -0,0 +1,107 @@ +package x + +import "image" + +import "github.com/jezek/xgbutil" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/xevent" +import "github.com/jezek/xgbutil/keybind" + +import "git.tebibyte.media/sashakoshka/stone" + +func (backend *Backend) Run (channel chan(stone.Event)) { + backend.channel = channel + xevent.Main(backend.connection) + backend.shutDown() +} + + +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) handleKeyPress ( + connection *xgbutil.XUtil, + event xevent.KeyPressEvent, +) { + keyEvent := *event.KeyPressEvent + keySym := keybind.KeysymGet(backend.connection, keyEvent.Detail, 0) + // TODO: convert to keysym and then to a button value +} + +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 { } +} diff --git a/backends/x/factory.go b/backends/x/factory.go new file mode 100644 index 0000000..2ed9b3c --- /dev/null +++ b/backends/x/factory.go @@ -0,0 +1,151 @@ +package x + +import "os" +import "golang.org/x/image/font" +import "golang.org/x/image/font/opentype" +import "golang.org/x/image/font/basicfont" + +import "github.com/jezek/xgbutil" +import "github.com/jezek/xgb/xproto" +import "github.com/jezek/xgbutil/icccm" +import "github.com/jezek/xgbutil/xevent" +import "github.com/jezek/xgbutil/xwindow" +import "github.com/jezek/xgbutil/keybind" +import "github.com/jezek/xgbutil/xgraphics" + +import "git.tebibyte.media/sashakoshka/stone" + +import "github.com/flopp/go-findfont" + +// 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 } + keybind.Initialize(backend.connection) + + // 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()) + if err != nil { return } + + // set minimum dimensions + minWidth := + backend.metrics.cellWidth + backend.metrics.padding * 2 + minHeight := + backend.metrics.cellHeight + backend.metrics.padding * 2 + err = icccm.WmNormalHintsSet ( + backend.connection, + backend.window.Id, + &icccm.NormalHints { + Flags: icccm.SizeHintPMinSize, + MinWidth: uint(minWidth), + MinHeight: uint(minHeight), + }) + if err != nil { return } + + // 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 +} + +// init registers this backend when the program starts. +func init () { + stone.RegisterBackend(factory) +} diff --git a/backends/x/x.go b/backends/x/x.go index 3f70a4b..b7c7cf2 100644 --- a/backends/x/x.go +++ b/backends/x/x.go @@ -1,28 +1,18 @@ 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/icccm" -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 @@ -56,32 +46,6 @@ type Backend struct { } } -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 @@ -124,87 +88,6 @@ func (backend *Backend) SetIcon (icons []image.Image) (err error) { 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) { @@ -245,69 +128,6 @@ func (backend *Backend) reallocateCanvas () { 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 @@ -334,174 +154,3 @@ func (backend *Backend) boundsOfCell (x, y int) (bounds image.Rectangle) { } 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()) - if err != nil { return } - - // set minimum dimensions - minWidth := - backend.metrics.cellWidth + backend.metrics.padding * 2 - minHeight := - backend.metrics.cellHeight + backend.metrics.padding * 2 - err = icccm.WmNormalHintsSet ( - backend.connection, - backend.window.Id, - &icccm.NormalHints { - Flags: icccm.SizeHintPMinSize, - MinWidth: uint(minWidth), - MinHeight: uint(minHeight), - }) - if err != nil { return } - - // 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) -}