2024-06-02 21:09:20 -06:00
|
|
|
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 window struct {
|
|
|
|
backend *Backend
|
|
|
|
hierarchy *system.Hierarchy
|
|
|
|
xWindow *xwindow.Window
|
|
|
|
xImage *xgraphics.Image
|
|
|
|
xCanvas *xcanvas.Canvas
|
|
|
|
|
|
|
|
title string
|
|
|
|
|
2024-06-06 23:07:28 -06:00
|
|
|
leader *window
|
2024-06-02 21:09:20 -06:00
|
|
|
modalParent *window
|
|
|
|
hasModal bool
|
|
|
|
shy bool
|
|
|
|
visible bool
|
2024-06-11 21:58:19 -06:00
|
|
|
resizeX bool
|
|
|
|
resizeY bool
|
2024-06-02 21:09:20 -06:00
|
|
|
|
|
|
|
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,
|
|
|
|
) (
|
2024-06-06 23:07:28 -06:00
|
|
|
output tomo.Window,
|
2024-06-02 21:09:20 -06:00
|
|
|
err error,
|
|
|
|
) {
|
|
|
|
this.assert()
|
2024-06-06 23:07:28 -06:00
|
|
|
return this.newWindow(bounds, false)
|
2024-06-02 21:09:20 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (this *Backend) NewPlainWindow (
|
|
|
|
bounds image.Rectangle,
|
|
|
|
) (
|
2024-06-06 23:07:28 -06:00
|
|
|
output tomo.Window,
|
2024-06-02 21:09:20 -06:00
|
|
|
err error,
|
|
|
|
) {
|
|
|
|
this.assert()
|
|
|
|
window, err := this.newWindow(bounds, false)
|
|
|
|
window.setType("dock")
|
|
|
|
|
2024-06-06 23:07:28 -06:00
|
|
|
return window, err
|
2024-06-02 21:09:20 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2024-06-06 23:07:28 -06:00
|
|
|
window.leader = window
|
2024-06-11 22:29:07 -06:00
|
|
|
window.resizeX = true
|
|
|
|
window.resizeY = true
|
2024-06-02 21:09:20 -06:00
|
|
|
|
|
|
|
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) {
|
2024-06-02 23:51:43 -06:00
|
|
|
if root == nil {
|
|
|
|
this.hierarchy.SetRoot(nil)
|
|
|
|
} else {
|
|
|
|
this.hierarchy.SetRoot(root.GetBox())
|
|
|
|
}
|
2024-06-02 21:09:20 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-06-06 23:12:04 -06:00
|
|
|
func (this *window) SetIcon (sizes ...canvas.Texture) {
|
2024-06-02 21:09:20 -06:00
|
|
|
wmIcons := []ewmh.WmIcon { }
|
2024-06-06 23:12:04 -06:00
|
|
|
|
2024-06-02 21:09:20 -06:00
|
|
|
for _, icon := range sizes {
|
2024-06-15 21:33:59 -06:00
|
|
|
icon, ok := icon.(*xcanvas.Texture)
|
|
|
|
if !ok { continue }
|
|
|
|
|
|
|
|
bounds := icon.Bounds()
|
|
|
|
width := bounds.Dx()
|
|
|
|
height := bounds.Dy()
|
2024-06-02 21:09:20 -06:00
|
|
|
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
|
2024-06-15 21:33:59 -06:00
|
|
|
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)
|
2024-06-02 21:09:20 -06:00
|
|
|
wmIcon.Data[index] =
|
2024-06-15 21:33:59 -06:00
|
|
|
(uint(pixel.A) << 24) |
|
|
|
|
(uint(pixel.R) << 16) |
|
|
|
|
(uint(pixel.G) << 8) |
|
|
|
|
(uint(pixel.B) << 0)
|
2024-06-02 21:09:20 -06:00
|
|
|
index ++
|
|
|
|
}}
|
|
|
|
|
|
|
|
wmIcons = append(wmIcons, wmIcon)
|
|
|
|
}
|
|
|
|
|
|
|
|
ewmh.WmIconSet (
|
|
|
|
this.backend.x,
|
|
|
|
this.xWindow.Id,
|
|
|
|
wmIcons)
|
|
|
|
}
|
|
|
|
|
2024-06-11 21:45:40 -06:00
|
|
|
func (this *window) SetResizable (x, y bool) {
|
2024-06-11 21:58:19 -06:00
|
|
|
if this.resizeX == x && this.resizeY == y { return }
|
|
|
|
this.resizeX = x
|
|
|
|
this.resizeY = y
|
|
|
|
this.doMinimumSize()
|
2024-06-11 21:45:40 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2024-06-02 21:09:20 -06:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-06-06 23:07:28 -06:00
|
|
|
func (this *window) NewChild (bounds image.Rectangle) (tomo.Window, error) {
|
|
|
|
leader := this.leader
|
|
|
|
|
2024-06-02 21:09:20 -06:00
|
|
|
child, err := this.backend.newWindow (
|
|
|
|
bounds.Add(this.metrics.bounds.Min), false)
|
2024-06-06 23:07:28 -06:00
|
|
|
child.leader = leader
|
2024-06-02 21:09:20 -06:00
|
|
|
if err != nil { return nil, err }
|
2024-06-06 23:07:28 -06:00
|
|
|
|
|
|
|
child.setClientLeader(leader)
|
|
|
|
leader.setClientLeader(leader)
|
|
|
|
|
2024-06-02 21:09:20 -06:00
|
|
|
icccm.WmTransientForSet (
|
|
|
|
this.backend.x,
|
2024-06-06 23:07:28 -06:00
|
|
|
child.xWindow.Id,
|
|
|
|
leader.xWindow.Id)
|
|
|
|
child.setType("UTILITY")
|
|
|
|
// child.inheritProperties(leader.window)
|
2024-06-06 23:47:24 -06:00
|
|
|
return child, err
|
2024-06-02 21:09:20 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
2024-06-11 21:45:40 -06:00
|
|
|
this.hierarchy.Close()
|
2024-06-02 21:09:20 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2024-06-03 18:54:45 -06:00
|
|
|
this.xCanvas.Close()
|
2024-06-02 21:09:20 -06:00
|
|
|
}
|
|
|
|
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 }
|
2024-06-11 22:29:07 -06:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-06-02 21:09:20 -06:00
|
|
|
icccm.WmNormalHintsSet (
|
|
|
|
this.backend.x,
|
|
|
|
this.xWindow.Id,
|
2024-06-11 22:29:07 -06:00
|
|
|
&hints)
|
2024-06-02 21:09:20 -06:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|