Sasha Koshka
ff8875535d
Boxes that need their minimum size to be updated now use a map like for layout and drawing. Size set with MinimumSize is now treated as separate from the content size and the larger size is used.
391 lines
9.8 KiB
Go
391 lines
9.8 KiB
Go
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 "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) 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.NewFrom(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) 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)
|
|
}
|
|
}
|