package x import "image" // import "errors" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/x/canvas" 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 "github.com/jezek/xgb/xproto" import "github.com/jezek/xgbutil/ewmh" import "github.com/jezek/xgbutil/icccm" // import "github.com/jezek/xgbutil/xprop" 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 xWindow *xwindow.Window xImage *xgraphics.Image xCanvas *xcanvas.Canvas title string modalParent *window hasModal bool shy bool metrics struct { bounds image.Rectangle } modifiers input.Modifiers mousePosition image.Point drags [10]anyBox onClose event.FuncBroadcaster root anyBox focused anyBox hovered anyBox // TODO: needMinimum and needLayout should be priority queues. for the // minimums, we need to start at the deeper parts of the layout tree and // go upward towards the top. for the layouts, we need to start at the // top of the layout tree and progressively go deeper. this will // eliminate redundant layout calculations. needMinimum boxSet needLayout boxSet needDraw boxSet needRedo bool minimumClean bool } func (backend *Backend) NewWindow ( bounds image.Rectangle, ) ( output tomo.MainWindow, err error, ) { backend.assert() window, err := backend.newWindow(bounds, false) output = mainWindow { window: window } return output, err } func (backend *Backend) NewPlainWindow ( bounds image.Rectangle, ) ( output tomo.MainWindow, err error, ) { backend.assert() window, err := backend.newWindow(bounds, false) window.setType("dock") output = mainWindow { window: window } return output, err } func (backend *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: backend } window.xWindow, err = xwindow.Generate(backend.x) if err != nil { return } if override { err = window.xWindow.CreateChecked ( backend.x.RootWin(), bounds.Min.X, bounds.Min.Y, bounds.Dx(), bounds.Dy(), xproto.CwOverrideRedirect, 1) } else { err = window.xWindow.CreateChecked ( backend.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(backend.x, window.xWindow.Id) xevent.ConfigureNotifyFun(window.handleConfigureNotify). Connect(backend.x, window.xWindow.Id) xevent.KeyPressFun(window.handleKeyPress). Connect(backend.x, window.xWindow.Id) xevent.KeyReleaseFun(window.handleKeyRelease). Connect(backend.x, window.xWindow.Id) xevent.ButtonPressFun(window.handleButtonPress). Connect(backend.x, window.xWindow.Id) xevent.ButtonReleaseFun(window.handleButtonRelease). Connect(backend.x, window.xWindow.Id) xevent.MotionNotifyFun(window.handleMotionNotify). Connect(backend.x, window.xWindow.Id) // xevent.SelectionNotifyFun(window.handleSelectionNotify). // Connect(backend.x, window.xWindow.Id) // xevent.PropertyNotifyFun(window.handlePropertyNotify). // Connect(backend.x, window.xWindow.Id) // xevent.SelectionClearFun(window.handleSelectionClear). // Connect(backend.x, window.xWindow.Id) // xevent.SelectionRequestFun(window.handleSelectionRequest). // Connect(backend.x, window.xWindow.Id) window.metrics.bounds = bounds window.doMinimumSize() backend.windows[window.xWindow.Id] = window output = window return } func (window *window) SetTitle (title string) { window.title = title ewmh .WmNameSet (window.backend.x, window.xWindow.Id, title) icccm.WmNameSet (window.backend.x, window.xWindow.Id, title) icccm.WmIconNameSet (window.backend.x, window.xWindow.Id, title) } func (window *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 ( window.backend.x, window.xWindow.Id, wmIcons) } func (window *window) NewMenu (bounds image.Rectangle) (tomo.Window, error) { menu, err := window.backend.newWindow ( bounds.Add(window.metrics.bounds.Min), true) menu.shy = true icccm.WmTransientForSet ( window.backend.x, menu.xWindow.Id, window.xWindow.Id) menu.setType("POPUP_MENU") // menu.inheritProperties(window) return menu, err } func (window *window) NewModal (bounds image.Rectangle) (tomo.Window, error) { modal, err := window.backend.newWindow ( bounds.Add(window.metrics.bounds.Min), false) icccm.WmTransientForSet ( window.backend.x, modal.xWindow.Id, window.xWindow.Id) ewmh.WmStateSet ( window.backend.x, modal.xWindow.Id, []string { "_NET_WM_STATE_MODAL" }) modal.modalParent = window window.hasModal = true // modal.inheritProperties(window) return modal, err } func (window mainWindow) NewChild (bounds image.Rectangle) (tomo.Window, error) { child, err := window.backend.newWindow ( bounds.Add(window.metrics.bounds.Min), false) if err != nil { return nil, err } child.setClientLeader(window.window) window.setClientLeader(window.window) icccm.WmTransientForSet ( window.backend.x, window.xWindow.Id, window.xWindow.Id) window.setType("UTILITY") // window.inheritProperties(window.window) return window, err } func (window *window) Widget () (tomo.Window, error) { // TODO return nil, nil } func (window *window) Copy (data.Data) { // TODO } func (window *window) Paste (callback func (data.Data, error), accept ...data.Mime) { // TODO } func (window *window) Show () { window.xWindow.Map() if window.shy { window.grabInput() } } func (window *window) Hide () { window.xWindow.Unmap() if window.shy { window.ungrabInput() } } func (window *window) Close () { xevent .Detach(window.backend.x, window.xWindow.Id) keybind .Detach(window.backend.x, window.xWindow.Id) mousebind.Detach(window.backend.x, window.xWindow.Id) window.onClose.Broadcast() if window.modalParent != nil { // we are a modal dialog, so unlock the parent window.modalParent.hasModal = false } window.Hide() window.SetRoot(nil) delete(window.backend.windows, window.xWindow.Id) window.xWindow.Destroy() } func (window *window) OnClose (callback func ()) event.Cookie { return window.onClose.Connect(callback) } func (window *window) grabInput () { keybind.GrabKeyboard(window.backend.x, window.xWindow.Id) mousebind.GrabPointer ( window.backend.x, window.xWindow.Id, window.backend.x.RootWin(), 0) } func (window *window) ungrabInput () { keybind.UngrabKeyboard(window.backend.x) mousebind.UngrabPointer(window.backend.x) } func (window *window) setType (ty string) error { return ewmh.WmWindowTypeSet ( window.backend.x, window.xWindow.Id, []string { "_NET_WM_WINDOW_TYPE_" + ty }) } func (window *window) setClientLeader (leader *window) error { hints, _ := icccm.WmHintsGet(window.backend.x, window.xWindow.Id) if hints == nil { hints = &icccm.Hints { } } hints.Flags |= icccm.HintWindowGroup hints.WindowGroup = leader.xWindow.Id return icccm.WmHintsSet ( window.backend.x, window.xWindow.Id, hints) } func (window *window) reallocateCanvas () { var previousWidth, previousHeight int if window.xCanvas != nil { previousWidth = window.xCanvas.Bounds().Dx() previousHeight = window.xCanvas.Bounds().Dy() } newWidth := window.metrics.bounds.Dx() newHeight := window.metrics.bounds.Dy() larger := newWidth > previousWidth || newHeight > previousHeight smaller := newWidth < previousWidth / 2 || newHeight < previousHeight / 2 allocStep := 128 if larger || smaller { if window.xCanvas != nil { window.xCanvas.Destroy() } window.xCanvas = xcanvas.NewCanvasFrom(xgraphics.New ( window.backend.x, image.Rect ( 0, 0, (newWidth / allocStep + 1) * allocStep, (newHeight / allocStep + 1) * allocStep))) window.xCanvas.CreatePixmap() } window.needRedo = true } func (window *window) pushAll () { if window.xCanvas != nil { window.xCanvas.Push(window.xWindow.Id) } } func (window *window) pushRegion (region image.Rectangle) { if window.xCanvas == nil { return } subCanvas := window.xCanvas.Clip(region) if subCanvas == nil { return } subCanvas.(*xcanvas.Canvas).Push(window.xWindow.Id) } func (window *window) drawBackgroundPart (canvas.Canvas) { // no-op for now? maybe eventually windows will be able to have a // background } func (window *window) doMinimumSize () { window.minimumClean = true size := image.Point { } if window.root != nil { size = window.root.MinimumSize() } if size.X < 8 { size.X = 8 } if size.Y < 8 { size.Y = 8 } icccm.WmNormalHintsSet ( window.backend.x, window.xWindow.Id, &icccm.NormalHints { Flags: icccm.SizeHintPMinSize, MinWidth: uint(size.X), MinHeight: uint(size.Y), }) newWidth := window.metrics.bounds.Dx() newHeight := window.metrics.bounds.Dy() if newWidth < size.X { newWidth = size.X } if newHeight < size.Y { newHeight = size.Y } if newWidth != window.metrics.bounds.Dx() || newHeight != window.metrics.bounds.Dy() { window.xWindow.Resize(newWidth, newHeight) } }