package x import "image" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/data" import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/backend/x/canvas" import "git.tebibyte.media/tomo/backend/internal/system" 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/keybind" import "github.com/jezek/xgbutil/mousebind" import "github.com/jezek/xgbutil/xgraphics" type mainWindow struct { *window } type window struct { backend *Backend hierarchy *system.Hierarchy xWindow *xwindow.Window xImage *xgraphics.Image xCanvas *xcanvas.Canvas title string modalParent *window hasModal bool shy bool visible bool metrics struct { bounds image.Rectangle } onClose event.FuncBroadcaster } type windowLink struct { window *window } func (this *windowLink) GetWindow () tomo.Window { return this.window } func (this *windowLink) PushRegion (region image.Rectangle) { this.window.pushRegion(region) } func (this *windowLink) PushAll () { this.window.pushAll() } func (this *windowLink) NotifyMinimumSizeChange () { this.window.doMinimumSize() } func (this *Backend) NewWindow ( bounds image.Rectangle, ) ( output tomo.MainWindow, err error, ) { this.assert() window, err := this.newWindow(bounds, false) output = mainWindow { window: window } return output, err } func (this *Backend) NewPlainWindow ( bounds image.Rectangle, ) ( output tomo.MainWindow, err error, ) { this.assert() window, err := this.newWindow(bounds, false) window.setType("dock") output = mainWindow { window: window } return output, err } func (this *Backend) newWindow ( bounds image.Rectangle, override bool, ) ( output *window, err error, ) { if bounds.Dx() == 0 { bounds.Max.X = bounds.Min.X + 8 } if bounds.Dy() == 0 { bounds.Max.Y = bounds.Min.Y + 8 } window := &window { backend: this } link := &windowLink { window: window } window.hierarchy = this.system.NewHierarchy(link) window.xWindow, err = xwindow.Generate(this.x) if err != nil { return } if override { err = window.xWindow.CreateChecked ( this.x.RootWin(), bounds.Min.X, bounds.Min.Y, bounds.Dx(), bounds.Dy(), xproto.CwOverrideRedirect, 1) } else { err = window.xWindow.CreateChecked ( this.x.RootWin(), bounds.Min.X, bounds.Min.Y, bounds.Dx(), bounds.Dy(), 0) } if err != nil { return } err = window.xWindow.Listen ( xproto.EventMaskExposure, xproto.EventMaskStructureNotify, xproto.EventMaskPropertyChange, xproto.EventMaskPointerMotion, xproto.EventMaskKeyPress, xproto.EventMaskKeyRelease, xproto.EventMaskButtonPress, xproto.EventMaskButtonRelease) if err != nil { return } window.xWindow.WMGracefulClose (func (xWindow *xwindow.Window) { window.Close() }) xevent.ExposeFun(window.handleExpose). Connect(this.x, window.xWindow.Id) xevent.ConfigureNotifyFun(window.handleConfigureNotify). Connect(this.x, window.xWindow.Id) xevent.KeyPressFun(window.handleKeyPress). Connect(this.x, window.xWindow.Id) xevent.KeyReleaseFun(window.handleKeyRelease). Connect(this.x, window.xWindow.Id) xevent.ButtonPressFun(window.handleButtonPress). Connect(this.x, window.xWindow.Id) xevent.ButtonReleaseFun(window.handleButtonRelease). Connect(this.x, window.xWindow.Id) xevent.MotionNotifyFun(window.handleMotionNotify). Connect(this.x, window.xWindow.Id) // xevent.SelectionNotifyFun(window.handleSelectionNotify). // Connect(this.x, window.xWindow.Id) // xevent.PropertyNotifyFun(window.handlePropertyNotify). // Connect(this.x, window.xWindow.Id) // xevent.SelectionClearFun(window.handleSelectionClear). // Connect(this.x, window.xWindow.Id) // xevent.SelectionRequestFun(window.handleSelectionRequest). // Connect(this.x, window.xWindow.Id) window.metrics.bounds = bounds window.doMinimumSize() this.windows[window.xWindow.Id] = window output = window return } func (this *window) SetRoot (root tomo.Object) { this.hierarchy.SetRoot(root.GetBox()) } func (this *window) SetTitle (title string) { this.title = title ewmh .WmNameSet (this.backend.x, this.xWindow.Id, title) icccm.WmNameSet (this.backend.x, this.xWindow.Id, title) icccm.WmIconNameSet (this.backend.x, this.xWindow.Id, title) } func (this *window) SetIcon (sizes ...image.Image) { wmIcons := []ewmh.WmIcon { } for _, icon := range sizes { 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) } ewmh.WmIconSet ( this.backend.x, this.xWindow.Id, wmIcons) } func (this *window) NewMenu (bounds image.Rectangle) (tomo.Window, error) { menu, err := this.backend.newWindow ( bounds.Add(this.metrics.bounds.Min), true) menu.shy = true icccm.WmTransientForSet ( this.backend.x, menu.xWindow.Id, this.xWindow.Id) menu.setType("POPUP_MENU") // menu.inheritProperties(this) return menu, err } func (this *window) NewModal (bounds image.Rectangle) (tomo.Window, error) { modal, err := this.backend.newWindow ( bounds.Add(this.metrics.bounds.Min), false) icccm.WmTransientForSet ( this.backend.x, modal.xWindow.Id, this.xWindow.Id) ewmh.WmStateSet ( this.backend.x, modal.xWindow.Id, []string { "_NET_WM_STATE_MODAL" }) modal.modalParent = this this.hasModal = true // modal.inheritProperties(window) return modal, err } func (this mainWindow) NewChild (bounds image.Rectangle) (tomo.Window, error) { child, err := this.backend.newWindow ( bounds.Add(this.metrics.bounds.Min), false) if err != nil { return nil, err } child.setClientLeader(this.window) this.setClientLeader(this.window) icccm.WmTransientForSet ( this.backend.x, this.xWindow.Id, this.xWindow.Id) this.setType("UTILITY") // this.inheritProperties(this.window) return this, err } func (this *window) Widget () (tomo.Window, error) { // TODO return nil, nil } func (this *window) Copy (data.Data) { // TODO } func (this *window) Paste (callback func (data.Data, error), accept ...data.Mime) { // TODO } func (this *window) SetVisible (visible bool) { if this.visible == visible { return } this.visible = visible if this.visible { this.xWindow.Map() if this.shy { this.grabInput() } } else { this.xWindow.Unmap() if this.shy { this.ungrabInput() } } } func (this *window) Visible () bool { return this.visible } func (this *window) Close () { xevent .Detach(this.backend.x, this.xWindow.Id) keybind .Detach(this.backend.x, this.xWindow.Id) mousebind.Detach(this.backend.x, this.xWindow.Id) this.onClose.Broadcast() if this.modalParent != nil { // we are a modal dialog, so unlock the parent this.modalParent.hasModal = false } this.SetVisible(false) this.SetRoot(nil) delete(this.backend.windows, this.xWindow.Id) this.xWindow.Destroy() } func (this *window) OnClose (callback func ()) event.Cookie { return this.onClose.Connect(callback) } func (this *window) grabInput () { keybind.GrabKeyboard(this.backend.x, this.xWindow.Id) mousebind.GrabPointer ( this.backend.x, this.xWindow.Id, this.backend.x.RootWin(), 0) } func (this *window) ungrabInput () { keybind.UngrabKeyboard(this.backend.x) mousebind.UngrabPointer(this.backend.x) } func (this *window) setType (ty string) error { return ewmh.WmWindowTypeSet ( this.backend.x, this.xWindow.Id, []string { "_NET_WM_WINDOW_TYPE_" + ty }) } func (this *window) setClientLeader (leader *window) error { hints, _ := icccm.WmHintsGet(this.backend.x, this.xWindow.Id) if hints == nil { hints = &icccm.Hints { } } hints.Flags |= icccm.HintWindowGroup hints.WindowGroup = leader.xWindow.Id return icccm.WmHintsSet ( this.backend.x, this.xWindow.Id, hints) } func (this *window) reallocateCanvas () { var previousWidth, previousHeight int if this.xCanvas != nil { previousWidth = this.xCanvas.Bounds().Dx() previousHeight = this.xCanvas.Bounds().Dy() } newWidth := this.metrics.bounds.Dx() newHeight := this.metrics.bounds.Dy() larger := newWidth > previousWidth || newHeight > previousHeight smaller := newWidth < previousWidth / 2 || newHeight < previousHeight / 2 allocStep := 128 if larger || smaller { if this.xCanvas != nil { this.xCanvas.Destroy() } this.xCanvas = xcanvas.NewCanvasFrom(xgraphics.New ( this.backend.x, image.Rect ( 0, 0, (newWidth / allocStep + 1) * allocStep, (newHeight / allocStep + 1) * allocStep))) this.xCanvas.CreatePixmap() } this.hierarchy.SetCanvas(this.xCanvas.SubCanvas ( this.metrics.bounds.Sub(this.metrics.bounds.Min))) } func (this *window) pushAll () { if this.xCanvas != nil { this.xCanvas.Push(this.xWindow.Id) } } func (this *window) pushRegion (region image.Rectangle) { if this.xCanvas == nil { return } subCanvas := this.xCanvas.SubCanvas(region) if subCanvas == nil { return } subCanvas.(*xcanvas.Canvas).Push(this.xWindow.Id) } func (this *window) drawBackgroundPart (canvas.Canvas) { // no-op for now? maybe eventually windows will be able to have a // background } func (this *window) doMinimumSize () { size := this.hierarchy.MinimumSize() if size.X < 8 { size.X = 8 } if size.Y < 8 { size.Y = 8 } icccm.WmNormalHintsSet ( this.backend.x, this.xWindow.Id, &icccm.NormalHints { Flags: icccm.SizeHintPMinSize, MinWidth: uint(size.X), MinHeight: uint(size.Y), }) newWidth := this.metrics.bounds.Dx() newHeight := this.metrics.bounds.Dy() if newWidth < size.X { newWidth = size.X } if newHeight < size.Y { newHeight = size.Y } if newWidth != this.metrics.bounds.Dx() || newHeight != this.metrics.bounds.Dy() { this.xWindow.Resize(newWidth, newHeight) } }