Bring over X backend code
This commit is contained in:
parent
687b40b35a
commit
284b91f0e5
141
x/backend.go
Normal file
141
x/backend.go
Normal file
@ -0,0 +1,141 @@
|
||||
package x
|
||||
|
||||
import "image"
|
||||
import "errors"
|
||||
import "git.tebibyte.media/tomo/tomo"
|
||||
import "git.tebibyte.media/tomo/xgbkb"
|
||||
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/xgbutil"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
import "github.com/jezek/xgbutil/keybind"
|
||||
import "github.com/jezek/xgbutil/mousebind"
|
||||
|
||||
type Backend struct {
|
||||
x *xgbutil.XUtil
|
||||
system *system.System
|
||||
|
||||
doChannel chan func()
|
||||
windows map[xproto.Window] *window
|
||||
open bool
|
||||
}
|
||||
|
||||
type backendLink struct {
|
||||
backend *Backend
|
||||
}
|
||||
|
||||
func (this *backendLink) NewTexture (source image.Image) canvas.TextureCloser {
|
||||
return this.backend.NewTexture(source)
|
||||
}
|
||||
|
||||
func (this *backendLink) NewCanvas (bounds image.Rectangle) canvas.Canvas {
|
||||
return this.backend.NewCanvas(bounds)
|
||||
}
|
||||
|
||||
func (this *backendLink) NewSurface (bounds image.Rectangle) (system.SurfaceLink, error) {
|
||||
// TODO
|
||||
return nil, errors.New("x: not implemented")
|
||||
}
|
||||
|
||||
func New () (tomo.Backend, error) {
|
||||
backend := &Backend {
|
||||
doChannel: make(chan func (), 32),
|
||||
windows: make(map[xproto.Window] *window),
|
||||
open: true,
|
||||
}
|
||||
link := &backendLink {
|
||||
backend: backend,
|
||||
}
|
||||
|
||||
backend.system = system.New(link)
|
||||
|
||||
var err error
|
||||
backend.x, err = xgbutil.NewConn()
|
||||
if err != nil { return nil, err }
|
||||
|
||||
keybind .Initialize(backend.x)
|
||||
xgbkb .Initialize(backend.x)
|
||||
mousebind.Initialize(backend.x)
|
||||
|
||||
return backend, nil
|
||||
}
|
||||
|
||||
func (this *Backend) Run () error {
|
||||
this.assert()
|
||||
pingBefore,
|
||||
pingAfter,
|
||||
pingQuit := xevent.MainPing(this.x)
|
||||
for {
|
||||
select {
|
||||
case <- pingBefore:
|
||||
<- pingAfter
|
||||
case callback := <- this.doChannel:
|
||||
callback()
|
||||
case <- pingQuit:
|
||||
return nil // FIXME: if we exited due to an error say so
|
||||
}
|
||||
this.afterEvent()
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Backend) Stop () {
|
||||
this.assert()
|
||||
if !this.open { return }
|
||||
this.open = false
|
||||
|
||||
toClose := []*window { }
|
||||
for _, panel := range this.windows {
|
||||
toClose = append(toClose, panel)
|
||||
}
|
||||
for _, panel := range toClose {
|
||||
panel.Close()
|
||||
}
|
||||
xevent.Quit(this.x)
|
||||
this.x.Conn().Close()
|
||||
}
|
||||
|
||||
func (this *Backend) Do (callback func ()) {
|
||||
this.assert()
|
||||
this.doChannel <- callback
|
||||
}
|
||||
|
||||
func (this *Backend) NewBox () tomo.Box {
|
||||
return this.system.NewBox()
|
||||
}
|
||||
|
||||
func (this *Backend) NewCanvasBox () tomo.CanvasBox {
|
||||
return this.system.NewCanvasBox()
|
||||
}
|
||||
|
||||
func (this *Backend) NewContainerBox () tomo.ContainerBox {
|
||||
return this.system.NewContainerBox()
|
||||
}
|
||||
|
||||
func (this *Backend) NewSurfaceBox () (tomo.SurfaceBox, error) {
|
||||
return this.system.NewSurfaceBox()
|
||||
}
|
||||
|
||||
func (this *Backend) NewTextBox () tomo.TextBox {
|
||||
return this.system.NewTextBox()
|
||||
}
|
||||
|
||||
func (this *Backend) NewTexture (source image.Image) canvas.TextureCloser {
|
||||
return xcanvas.NewTextureFrom(source)
|
||||
}
|
||||
|
||||
func (this *Backend) NewCanvas (bounds image.Rectangle) canvas.Canvas {
|
||||
return xcanvas.NewCanvas(this.x, bounds)
|
||||
}
|
||||
|
||||
func (this *Backend) assert () {
|
||||
if this == nil { panic("x: nil backend") }
|
||||
}
|
||||
|
||||
func (this *Backend) afterEvent () {
|
||||
for _, window := range this.windows {
|
||||
window.hierarchy.AfterEvent()
|
||||
}
|
||||
}
|
445
x/event.go
Normal file
445
x/event.go
Normal file
@ -0,0 +1,445 @@
|
||||
package x
|
||||
|
||||
import "image"
|
||||
|
||||
import "github.com/jezek/xgbutil"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "git.tebibyte.media/tomo/xgbkb"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
import "git.tebibyte.media/tomo/tomo/input"
|
||||
|
||||
type scrollSum struct {
|
||||
x, y int
|
||||
}
|
||||
|
||||
// TODO: this needs to be configurable, we need a config api
|
||||
const scrollDistance = 16
|
||||
|
||||
func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) {
|
||||
if xgbkb.StateToModifiers(state).Shift {
|
||||
switch button {
|
||||
case 4: sum.x -= scrollDistance
|
||||
case 5: sum.x += scrollDistance
|
||||
case 6: sum.y -= scrollDistance
|
||||
case 7: sum.y += scrollDistance
|
||||
}
|
||||
} else {
|
||||
switch button {
|
||||
case 4: sum.y -= scrollDistance
|
||||
case 5: sum.y += scrollDistance
|
||||
case 6: sum.x -= scrollDistance
|
||||
case 7: sum.x += scrollDistance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buttonCodeTable = map[xproto.Keysym] input.Key {
|
||||
0xFFFFFF: input.KeyNone,
|
||||
|
||||
0xFF63: input.KeyInsert,
|
||||
0xFF67: input.KeyMenu,
|
||||
0xFF61: input.KeyPrintScreen,
|
||||
0xFF6B: input.KeyPause,
|
||||
0xFFE5: input.KeyCapsLock,
|
||||
0xFF14: input.KeyScrollLock,
|
||||
0xFF7F: input.KeyNumLock,
|
||||
0xFF08: input.KeyBackspace,
|
||||
0xFF09: input.KeyTab,
|
||||
0xFE20: input.KeyTab,
|
||||
0xFF0D: input.KeyEnter,
|
||||
0xFF1B: input.KeyEscape,
|
||||
|
||||
0xFF52: input.KeyUp,
|
||||
0xFF54: input.KeyDown,
|
||||
0xFF51: input.KeyLeft,
|
||||
0xFF53: input.KeyRight,
|
||||
0xFF55: input.KeyPageUp,
|
||||
0xFF56: input.KeyPageDown,
|
||||
0xFF50: input.KeyHome,
|
||||
0xFF57: input.KeyEnd,
|
||||
|
||||
0xFFE1: input.KeyLeftShift,
|
||||
0xFFE2: input.KeyRightShift,
|
||||
0xFFE3: input.KeyLeftControl,
|
||||
0xFFE4: input.KeyRightControl,
|
||||
|
||||
0xFFE7: input.KeyLeftMeta,
|
||||
0xFFE8: input.KeyRightMeta,
|
||||
0xFFE9: input.KeyLeftAlt,
|
||||
0xFFEA: input.KeyRightAlt,
|
||||
0xFFEB: input.KeyLeftSuper,
|
||||
0xFFEC: input.KeyRightSuper,
|
||||
0xFFED: input.KeyLeftHyper,
|
||||
0xFFEE: input.KeyRightHyper,
|
||||
|
||||
0xFFFF: input.KeyDelete,
|
||||
|
||||
0xFFBE: input.KeyF1,
|
||||
0xFFBF: input.KeyF2,
|
||||
0xFFC0: input.KeyF3,
|
||||
0xFFC1: input.KeyF4,
|
||||
0xFFC2: input.KeyF5,
|
||||
0xFFC3: input.KeyF6,
|
||||
0xFFC4: input.KeyF7,
|
||||
0xFFC5: input.KeyF8,
|
||||
0xFFC6: input.KeyF9,
|
||||
0xFFC7: input.KeyF10,
|
||||
0xFFC8: input.KeyF11,
|
||||
0xFFC9: input.KeyF12,
|
||||
|
||||
0xFF20: input.KeyDead,
|
||||
}
|
||||
|
||||
var keypadCodeTable = map[xproto.Keysym] input.Key {
|
||||
0xff80: input.Key(' '),
|
||||
0xff89: input.KeyTab,
|
||||
0xff8d: input.KeyEnter,
|
||||
0xff91: input.KeyF1,
|
||||
0xff92: input.KeyF2,
|
||||
0xff93: input.KeyF3,
|
||||
0xff94: input.KeyF4,
|
||||
0xff95: input.KeyHome,
|
||||
0xff96: input.KeyLeft,
|
||||
0xff97: input.KeyUp,
|
||||
0xff98: input.KeyRight,
|
||||
0xff99: input.KeyDown,
|
||||
0xff9a: input.KeyPageUp,
|
||||
0xff9b: input.KeyPageDown,
|
||||
0xff9c: input.KeyEnd,
|
||||
0xff9d: input.KeyHome,
|
||||
0xff9e: input.KeyInsert,
|
||||
0xff9f: input.KeyDelete,
|
||||
0xffbd: input.Key('='),
|
||||
0xffaa: input.Key('*'),
|
||||
0xffab: input.Key('+'),
|
||||
0xffac: input.Key(','),
|
||||
0xffad: input.Key('-'),
|
||||
0xffae: input.Key('.'),
|
||||
0xffaf: input.Key('/'),
|
||||
|
||||
0xffb0: input.Key('0'),
|
||||
0xffb1: input.Key('1'),
|
||||
0xffb2: input.Key('2'),
|
||||
0xffb3: input.Key('3'),
|
||||
0xffb4: input.Key('4'),
|
||||
0xffb5: input.Key('5'),
|
||||
0xffb6: input.Key('6'),
|
||||
0xffb7: input.Key('7'),
|
||||
0xffb8: input.Key('8'),
|
||||
0xffb9: input.Key('9'),
|
||||
}
|
||||
|
||||
func (window *window) handleExpose (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ExposeEvent,
|
||||
) {
|
||||
if window.xCanvas == nil {
|
||||
window.reallocateCanvas()
|
||||
}
|
||||
window.compressExpose(*event.ExposeEvent)
|
||||
window.pushAll()
|
||||
}
|
||||
|
||||
func (window *window) updateBounds () {
|
||||
// FIXME: some window managers parent windows more than once, we might
|
||||
// need to sum up all their positions.
|
||||
decorGeometry, _ := window.xWindow.DecorGeometry()
|
||||
windowGeometry, _ := window.xWindow.Geometry()
|
||||
origin := image.Pt(
|
||||
windowGeometry.X() + decorGeometry.X(),
|
||||
windowGeometry.Y() + decorGeometry.Y())
|
||||
window.metrics.bounds = image.Rectangle {
|
||||
Min: origin,
|
||||
Max: origin.Add(image.Pt(windowGeometry.Width(), windowGeometry.Height())),
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) handleConfigureNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ConfigureNotifyEvent,
|
||||
) {
|
||||
configureEvent := *event.ConfigureNotifyEvent
|
||||
configureEvent = window.compressConfigureNotify(configureEvent)
|
||||
|
||||
oldBounds := window.metrics.bounds
|
||||
window.updateBounds()
|
||||
newBounds := window.metrics.bounds
|
||||
|
||||
sizeChanged :=
|
||||
oldBounds.Dx() != newBounds.Dx() ||
|
||||
oldBounds.Dy() != newBounds.Dy()
|
||||
|
||||
if sizeChanged {
|
||||
window.reallocateCanvas()
|
||||
|
||||
// TODO figure out what to do with this
|
||||
// if !window.exposeEventFollows(configureEvent) {
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (found bool) {
|
||||
nextEvents := xevent.Peek(window.backend.x)
|
||||
if len(nextEvents) > 0 {
|
||||
untypedEvent := nextEvents[0]
|
||||
if untypedEvent.Err == nil {
|
||||
typedEvent, ok :=
|
||||
untypedEvent.Event.(xproto.ConfigureNotifyEvent)
|
||||
|
||||
if ok && typedEvent.Window == event.Window {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func keycodeToKey (keycode xproto.Keycode, state uint16) (input.Key, bool) {
|
||||
keysym, char := xgbkb.KeycodeToKeysym(keycode, state)
|
||||
|
||||
if xgbkb.IsOnNumpad(keysym) {
|
||||
// look up in keypad table
|
||||
key := keypadCodeTable[keysym]
|
||||
return key, true
|
||||
} else {
|
||||
// look up in control code table
|
||||
key, isControl := buttonCodeTable[keysym]
|
||||
if isControl {
|
||||
return key, false
|
||||
} else {
|
||||
// return as rune
|
||||
return input.Key(char), false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) handleKeyPress (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.KeyPressEvent,
|
||||
) {
|
||||
if window.hasModal { return }
|
||||
|
||||
keyEvent := *event.KeyPressEvent
|
||||
key, numberPad := keycodeToKey(keyEvent.Detail, keyEvent.State)
|
||||
window.updateModifiers(keyEvent.State)
|
||||
|
||||
if key == input.KeyEscape && window.shy {
|
||||
window.Close()
|
||||
} else {
|
||||
window.hierarchy.HandleKeyDown(key, numberPad)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) handleKeyRelease (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.KeyReleaseEvent,
|
||||
) {
|
||||
if window.hasModal { return }
|
||||
|
||||
keyEvent := *event.KeyReleaseEvent
|
||||
|
||||
// do not process this event if it was generated from a key repeat
|
||||
nextEvents := xevent.Peek(window.backend.x)
|
||||
if len(nextEvents) > 0 {
|
||||
untypedEvent := nextEvents[0]
|
||||
if untypedEvent.Err == nil {
|
||||
typedEvent, ok :=
|
||||
untypedEvent.Event.(xproto.KeyPressEvent)
|
||||
|
||||
if ok && typedEvent.Detail == keyEvent.Detail &&
|
||||
typedEvent.Event == keyEvent.Event &&
|
||||
typedEvent.State == keyEvent.State {
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key, numberPad := keycodeToKey(keyEvent.Detail, keyEvent.State)
|
||||
window.updateModifiers(keyEvent.State)
|
||||
window.hierarchy.HandleKeyUp(key, numberPad)
|
||||
}
|
||||
|
||||
func (window *window) handleButtonPress (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ButtonPressEvent,
|
||||
) {
|
||||
if window.hasModal { return }
|
||||
|
||||
buttonEvent := *event.ButtonPressEvent
|
||||
point := image.Pt(int(buttonEvent.EventX), int(buttonEvent.EventY))
|
||||
insideWindow := point.In(window.xCanvas.Bounds())
|
||||
scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7
|
||||
window.updateModifiers(buttonEvent.State)
|
||||
window.updateMousePosition(buttonEvent.EventX, buttonEvent.EventY)
|
||||
|
||||
if !insideWindow && window.shy && !scrolling {
|
||||
window.Close()
|
||||
} else if scrolling {
|
||||
sum := scrollSum { }
|
||||
sum.add(buttonEvent.Detail, window, buttonEvent.State)
|
||||
window.compressScrollSum(buttonEvent, &sum)
|
||||
window.hierarchy.HandleScroll(float64(sum.x), float64(sum.y))
|
||||
} else {
|
||||
window.hierarchy.HandleMouseDown(input.Button(buttonEvent.Detail))
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) handleButtonRelease (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ButtonReleaseEvent,
|
||||
) {
|
||||
if window.hasModal { return }
|
||||
|
||||
buttonEvent := *event.ButtonReleaseEvent
|
||||
scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7
|
||||
window.updateModifiers(buttonEvent.State)
|
||||
window.updateMousePosition(buttonEvent.EventX, buttonEvent.EventY)
|
||||
if !scrolling {
|
||||
window.hierarchy.HandleMouseUp(input.Button(buttonEvent.Detail))
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) handleMotionNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.MotionNotifyEvent,
|
||||
) {
|
||||
if window.hasModal { return }
|
||||
|
||||
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
|
||||
window.updateMousePosition(motionEvent.EventX, motionEvent.EventY)
|
||||
}
|
||||
|
||||
func (window *window) compressExpose (
|
||||
firstEvent xproto.ExposeEvent,
|
||||
) (
|
||||
lastEvent xproto.ExposeEvent,
|
||||
region image.Rectangle,
|
||||
) {
|
||||
region = image.Rect (
|
||||
int(firstEvent.X), int(firstEvent.Y),
|
||||
int(firstEvent.X + firstEvent.Width),
|
||||
int(firstEvent.Y + firstEvent.Height))
|
||||
|
||||
window.backend.x.Sync()
|
||||
xevent.Read(window.backend.x, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.x) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.ExposeEvent)
|
||||
if !ok { continue }
|
||||
|
||||
if firstEvent.Window == typedEvent.Window {
|
||||
region = region.Union (image.Rect (
|
||||
int(typedEvent.X), int(typedEvent.Y),
|
||||
int(typedEvent.X + typedEvent.Width),
|
||||
int(typedEvent.Y + typedEvent.Height)))
|
||||
|
||||
lastEvent = typedEvent
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.x, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *window) compressConfigureNotify (
|
||||
firstEvent xproto.ConfigureNotifyEvent,
|
||||
) (
|
||||
lastEvent xproto.ConfigureNotifyEvent,
|
||||
) {
|
||||
window.backend.x.Sync()
|
||||
xevent.Read(window.backend.x, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.x) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.ConfigureNotifyEvent)
|
||||
if !ok { continue }
|
||||
|
||||
if firstEvent.Event == typedEvent.Event &&
|
||||
firstEvent.Window == typedEvent.Window {
|
||||
|
||||
lastEvent = typedEvent
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.x, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *window) compressScrollSum (
|
||||
firstEvent xproto.ButtonPressEvent,
|
||||
sum *scrollSum,
|
||||
) {
|
||||
window.backend.x.Sync()
|
||||
xevent.Read(window.backend.x, false)
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.x) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.ButtonPressEvent)
|
||||
if !ok { continue }
|
||||
|
||||
if firstEvent.Event == typedEvent.Event &&
|
||||
typedEvent.Detail >= 4 &&
|
||||
typedEvent.Detail <= 7 {
|
||||
|
||||
sum.add(typedEvent.Detail, window, typedEvent.State)
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.x, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *window) compressMotionNotify (
|
||||
firstEvent xproto.MotionNotifyEvent,
|
||||
) (
|
||||
lastEvent xproto.MotionNotifyEvent,
|
||||
) {
|
||||
window.backend.x.Sync()
|
||||
xevent.Read(window.backend.x, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.x) {
|
||||
if untypedEvent.Err != nil { continue }
|
||||
|
||||
typedEvent, ok := untypedEvent.Event.(xproto.MotionNotifyEvent)
|
||||
if !ok { continue }
|
||||
|
||||
if firstEvent.Event == typedEvent.Event {
|
||||
lastEvent = typedEvent
|
||||
defer func (index int) {
|
||||
xevent.DequeueAt(window.backend.x, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *window) updateModifiers (state uint16) {
|
||||
xModifiers := xgbkb.StateToModifiers(state)
|
||||
window.hierarchy.HandleModifiers(input.Modifiers {
|
||||
Shift: xModifiers.Shift || xModifiers.ShiftLock,
|
||||
Control: xModifiers.Control,
|
||||
Alt: xModifiers.Alt,
|
||||
Meta: xModifiers.Meta,
|
||||
Super: xModifiers.Super,
|
||||
Hyper: xModifiers.Hyper,
|
||||
})
|
||||
}
|
||||
|
||||
func (window *window) updateMousePosition (x, y int16) {
|
||||
window.hierarchy.HandleMouseMove(image.Pt(int(x), int(y)))
|
||||
}
|
419
x/window.go
Normal file
419
x/window.go
Normal file
@ -0,0 +1,419 @@
|
||||
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 mainWindow struct { *window }
|
||||
type window struct {
|
||||
backend *Backend
|
||||
hierarchy *system.Hierarchy
|
||||
xWindow *xwindow.Window
|
||||
xImage *xgraphics.Image
|
||||
xCanvas *xcanvas.Canvas
|
||||
|
||||
title string
|
||||
|
||||
modalParent *window
|
||||
hasModal bool
|
||||
shy bool
|
||||
visible bool
|
||||
|
||||
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,
|
||||
) (
|
||||
output tomo.MainWindow,
|
||||
err error,
|
||||
) {
|
||||
this.assert()
|
||||
window, err := this.newWindow(bounds, false)
|
||||
|
||||
output = mainWindow { window: window }
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (this *Backend) NewPlainWindow (
|
||||
bounds image.Rectangle,
|
||||
) (
|
||||
output tomo.MainWindow,
|
||||
err error,
|
||||
) {
|
||||
this.assert()
|
||||
window, err := this.newWindow(bounds, false)
|
||||
window.setType("dock")
|
||||
|
||||
output = mainWindow { window: window }
|
||||
return output, 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.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) {
|
||||
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 (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 (
|
||||
this.backend.x,
|
||||
this.xWindow.Id,
|
||||
wmIcons)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (this mainWindow) NewChild (bounds image.Rectangle) (tomo.Window, error) {
|
||||
child, err := this.backend.newWindow (
|
||||
bounds.Add(this.metrics.bounds.Min), false)
|
||||
if err != nil { return nil, err }
|
||||
child.setClientLeader(this.window)
|
||||
this.setClientLeader(this.window)
|
||||
icccm.WmTransientForSet (
|
||||
this.backend.x,
|
||||
this.xWindow.Id,
|
||||
this.xWindow.Id)
|
||||
this.setType("UTILITY")
|
||||
// this.inheritProperties(this.window)
|
||||
return this, err
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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 {
|
||||
this.xCanvas.Destroy()
|
||||
}
|
||||
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 }
|
||||
icccm.WmNormalHintsSet (
|
||||
this.backend.x,
|
||||
this.xWindow.Id,
|
||||
&icccm.NormalHints {
|
||||
Flags: icccm.SizeHintPMinSize,
|
||||
MinWidth: uint(size.X),
|
||||
MinHeight: uint(size.Y),
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user