package x import "image" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/data" import "git.tebibyte.media/tomo/tomo/input" 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 window struct { backend *Backend hierarchy *system.Hierarchy xWindow *xwindow.Window xImage *xgraphics.Image xCanvas *xcanvas.Canvas title string leader *window modalParent *window hasModal bool shy bool visible bool resizeX bool resizeY bool metrics struct { bounds image.Rectangle // bounds, including frame innerBounds image.Rectangle // bounds of the drawable area } 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.Window, err error, ) { this.assert() return this.newWindow(bounds, false) } func (this *Backend) NewPlainWindow ( bounds image.Rectangle, ) ( output tomo.Window, err error, ) { this.assert() window, err := this.newWindow(bounds, false) window.setType("dock") return window, 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.leader = window window.resizeX = true window.resizeY = true 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.innerBounds = bounds window.doMinimumSize() this.windows[window.xWindow.Id] = window output = window return } func (this *window) Bounds () image.Rectangle { return this.metrics.bounds.Sub(this.metrics.innerBounds.Min) } func (this *window) InnerBounds () image.Rectangle { return this.metrics.innerBounds.Sub(this.metrics.innerBounds.Min) } func (this *window) SetRoot (root tomo.Object) { if root == nil { this.hierarchy.SetRoot(nil) } else { 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 (icon tomo.Icon) { textures := []canvas.Texture { icon.Texture(tomo.IconSizeSmall), icon.Texture(tomo.IconSizeMedium), icon.Texture(tomo.IconSizeLarge), } wmIcons := []ewmh.WmIcon { } for _, icon := range textures { icon := xcanvas.AssertTexture(icon) bounds := icon.Bounds() width := bounds.Dx() height := bounds.Dy() 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 := bounds.Min.Y; y < bounds.Max.Y; y ++ { for x := bounds.Min.X; x < bounds.Max.X; x ++ { pixel := icon.BGRAAt(x, y) wmIcon.Data[index] = (uint(pixel.A) << 24) | (uint(pixel.R) << 16) | (uint(pixel.G) << 8) | (uint(pixel.B) << 0) index ++ }} wmIcons = append(wmIcons, wmIcon) } ewmh.WmIconSet ( this.backend.x, this.xWindow.Id, wmIcons) } func (this *window) SetResizable (x, y bool) { if this.resizeX == x && this.resizeY == y { return } this.resizeX = x this.resizeY = y this.doMinimumSize() } func (this *window) SetBounds (bounds image.Rectangle) { this.xWindow.WMMoveResize ( bounds.Min.X, bounds.Min.Y, bounds.Min.X + bounds.Dx(), bounds.Min.Y + bounds.Dy()) } func (this *window) NewChild (bounds image.Rectangle) (tomo.Window, error) { leader := this.leader child, err := this.backend.newWindow ( bounds.Add(this.metrics.innerBounds.Min), false) child.leader = leader if err != nil { return nil, err } child.setClientLeader(leader) leader.setClientLeader(leader) icccm.WmTransientForSet ( this.backend.x, child.xWindow.Id, leader.xWindow.Id) child.setType("UTILITY") // child.inheritProperties(leader.window) return child, err } func (this *window) NewMenu (bounds image.Rectangle) (tomo.Window, error) { menu, err := this.backend.newWindow ( bounds.Add(this.metrics.innerBounds.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.innerBounds.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 *window) Modifiers () input.Modifiers { return this.hierarchy.Modifiers() } func (this *window) MousePosition () image.Point { return this.hierarchy.MousePosition() } 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() this.hierarchy.Close() } 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.innerBounds.Dx() newHeight := this.metrics.innerBounds.Dy() larger := newWidth > previousWidth || newHeight > previousHeight smaller := newWidth < previousWidth / 2 || newHeight < previousHeight / 2 allocStep := 128 if larger || smaller { if this.xCanvas != nil { this.xCanvas.Close() } 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.innerBounds.Sub(this.metrics.innerBounds.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 } hints := icccm.NormalHints { Flags: icccm.SizeHintPMinSize, MinWidth: uint(size.X), MinHeight: uint(size.Y), // now you can tell your friends that the max size of a Tomo // window under X when one of the dimensions is constrained is // 99999999999 MaxWidth: uint(99999999999), MaxHeight: uint(99999999999), } if !this.resizeX { hints.Flags |= icccm.SizeHintPMaxSize hints.MaxWidth = uint(size.X) } if !this.resizeY { hints.Flags |= icccm.SizeHintPMaxSize hints.MaxHeight = uint(size.Y) } icccm.WmNormalHintsSet ( this.backend.x, this.xWindow.Id, &hints) newWidth := this.metrics.innerBounds.Dx() newHeight := this.metrics.innerBounds.Dy() if newWidth < size.X { newWidth = size.X } if newHeight < size.Y { newHeight = size.Y } if newWidth != this.metrics.innerBounds.Dx() || newHeight != this.metrics.innerBounds.Dy() { this.xWindow.Resize(newWidth, newHeight) } }