backend/x/window.go

482 lines
12 KiB
Go

package x
import "image"
import "strings"
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
}
on struct {
close event.FuncBroadcaster
tryClose event.Broadcaster[func () bool]
}
}
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 (kind tomo.WindowKind, bounds image.Rectangle) (tomo.Window, error) {
this.assert()
window, err := this.newWindow(bounds, kind == tomo.WindowKindMenu)
if err != nil { return nil, err }
window.setKind(kind)
return window, nil
}
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) {
holdOff := false
for _, callback := range window.on.tryClose.Listeners() {
if !callback() { holdOff = true }
}
if !holdOff { 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 (kind tomo.WindowKind, bounds image.Rectangle) (tomo.Window, error) {
leader := this.leader
child, err := this.backend.newWindow (
bounds.Add(this.metrics.innerBounds.Min), kind == tomo.WindowKindMenu)
if err != nil { return nil, err }
child.leader = leader
err = child.setKind(kind)
if err != nil { return nil, err }
if kind == tomo.WindowKindModal {
this.hasModal = true
child.modalParent = this
}
icccm.WmTransientForSet (
this.backend.x,
child.xWindow.Id,
leader.xWindow.Id)
// child.inheritProperties(leader.window)
return child, 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 () error {
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.on.close.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()
return nil // TODO maybe return an error? maybe join them?
}
func (this *window) OnClose (callback func ()) event.Cookie {
return this.on.close.Connect(callback)
}
func (this *window) OnTryClose (callback func () bool) event.Cookie {
return this.on.tryClose.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) setKind (kind tomo.WindowKind) error {
err := this.setType(windowKindToType(kind))
if err != nil { return err }
if kind == tomo.WindowKindModal {
err = this.setState("MODAL")
if err != nil { return err }
}
if kind == tomo.WindowKindMenu {
this.shy = true
}
return nil
}
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) setState (state string) error {
return ewmh.WmStateSet (
this.backend.x,
this.xWindow.Id,
[]string { "_NET_WM_STATE_" + state })
}
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)
}
}
func windowKindToType (kind tomo.WindowKind) string {
switch kind {
case tomo.WindowKindNormal: return "NORMAL"
case tomo.WindowKindPlain: return "DOCK"
case tomo.WindowKindUtility: return "UTILITY"
case tomo.WindowKindToolbar: return "MENU"
case tomo.WindowKindMenu: return "POPUP_MENU"
case tomo.WindowKindModal: return "NORMAL"
default: return strings.ReplaceAll(strings.ToUpper(string(kind)), " ", "_")
}
}