Yaeh
This commit is contained in:
2
plugins/x/x/doc.go
Normal file
2
plugins/x/x/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package x implements an X11 backend.
|
||||
package x
|
||||
390
plugins/x/x/encoding.go
Normal file
390
plugins/x/x/encoding.go
Normal file
@@ -0,0 +1,390 @@
|
||||
package x
|
||||
|
||||
import "unicode"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/keybind"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
|
||||
// when making changes to this file, look at keysymdef.h and
|
||||
// https://tronche.com/gui/x/xlib/input/keyboard-encoding.html
|
||||
|
||||
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,
|
||||
|
||||
// TODO: send this whenever a compose key, dead key, etc is pressed,
|
||||
// and then send the resulting character while witholding the key
|
||||
// presses that were used to compose it. As far as the program is
|
||||
// concerned, a magical key with the final character was pressed and the
|
||||
// KeyDead key is just so that the program might provide some visual
|
||||
// feedback to the user while input is being waited for.
|
||||
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'),
|
||||
}
|
||||
|
||||
// initializeKeymapInformation grabs keyboard mapping information from the X
|
||||
// server.
|
||||
func (backend *Backend) initializeKeymapInformation () {
|
||||
keybind.Initialize(backend.connection)
|
||||
backend.modifierMasks.capsLock = backend.keysymToMask(0xFFE5)
|
||||
backend.modifierMasks.shiftLock = backend.keysymToMask(0xFFE6)
|
||||
backend.modifierMasks.numLock = backend.keysymToMask(0xFF7F)
|
||||
backend.modifierMasks.modeSwitch = backend.keysymToMask(0xFF7E)
|
||||
|
||||
backend.modifierMasks.hyper = backend.keysymToMask(0xffed)
|
||||
backend.modifierMasks.super = backend.keysymToMask(0xffeb)
|
||||
backend.modifierMasks.meta = backend.keysymToMask(0xffe7)
|
||||
backend.modifierMasks.alt = backend.keysymToMask(0xffe9)
|
||||
}
|
||||
|
||||
// keysymToKeycode converts an X keysym to an X keycode, instead of the other
|
||||
// way around.
|
||||
func (backend *Backend) keysymToKeycode (
|
||||
symbol xproto.Keysym,
|
||||
) (
|
||||
code xproto.Keycode,
|
||||
) {
|
||||
mapping := keybind.KeyMapGet(backend.connection)
|
||||
|
||||
for index, testSymbol := range mapping.Keysyms {
|
||||
if testSymbol == symbol {
|
||||
code = xproto.Keycode (
|
||||
index /
|
||||
int(mapping.KeysymsPerKeycode) +
|
||||
int(backend.connection.Setup().MinKeycode))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// keysymToMask returns the X modmask for a given modifier key.
|
||||
func (backend *Backend) keysymToMask (
|
||||
symbol xproto.Keysym,
|
||||
) (
|
||||
mask uint16,
|
||||
) {
|
||||
mask = keybind.ModGet (
|
||||
backend.connection,
|
||||
backend.keysymToKeycode(symbol))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// keycodeToButton converts an X keycode to a tomo keycode. It implements a more
|
||||
// fleshed out version of some of the logic found in xgbutil/keybind/encoding.go
|
||||
// to get a full keycode to keysym conversion, but eliminates redundant work by
|
||||
// going straight to a tomo keycode.
|
||||
func (backend *Backend) keycodeToKey (
|
||||
keycode xproto.Keycode,
|
||||
state uint16,
|
||||
) (
|
||||
button input.Key,
|
||||
numberPad bool,
|
||||
) {
|
||||
// PARAGRAPH 3
|
||||
//
|
||||
// A list of KeySyms is associated with each KeyCode. The list is
|
||||
// intended to convey the set of symbols on the corresponding key. If
|
||||
// the list (ignoring trailing NoSymbol entries) is a single KeySym
|
||||
// ``K'', then the list is treated as if it were the list ``K NoSymbol
|
||||
// K NoSymbol''. If the list (ignoring trailing NoSymbol entries) is a
|
||||
// pair of KeySyms ``K1 K2'', then the list is treated as if it were the
|
||||
// list ``K1 K2 K1 K2''. If the list (ignoring trailing NoSymbol
|
||||
// entries) is a triple of KeySyms ``K1 K2 K3'', then the list is
|
||||
// treated as if it were the list ``K1 K2 K3 NoSymbol''. When an
|
||||
// explicit ``void'' element is desired in the list, the value
|
||||
// VoidSymbol can be used.
|
||||
symbol1 := keybind.KeysymGet(backend.connection, keycode, 0)
|
||||
symbol2 := keybind.KeysymGet(backend.connection, keycode, 1)
|
||||
symbol3 := keybind.KeysymGet(backend.connection, keycode, 2)
|
||||
symbol4 := keybind.KeysymGet(backend.connection, keycode, 3)
|
||||
switch {
|
||||
case symbol2 == 0 && symbol3 == 0 && symbol4 == 0:
|
||||
symbol3 = symbol1
|
||||
case symbol3 == 0 && symbol4 == 0:
|
||||
symbol3 = symbol1
|
||||
symbol4 = symbol2
|
||||
case symbol4 == 0:
|
||||
symbol4 = 0
|
||||
}
|
||||
symbol1Rune := keysymToRune(symbol1)
|
||||
symbol2Rune := keysymToRune(symbol2)
|
||||
symbol3Rune := keysymToRune(symbol3)
|
||||
symbol4Rune := keysymToRune(symbol4)
|
||||
|
||||
// PARAGRAPH 4
|
||||
//
|
||||
// The first four elements of the list are split into two groups of
|
||||
// KeySyms. Group 1 contains the first and second KeySyms; Group 2
|
||||
// contains the third and fourth KeySyms. Within each group, if the
|
||||
// second element of the group is NoSymbol , then the group should be
|
||||
// treated as if the second element were the same as the first element,
|
||||
// except when the first element is an alphabetic KeySym ``K'' for which
|
||||
// both lowercase and uppercase forms are defined. In that case, the
|
||||
// group should be treated as if the first element were the lowercase
|
||||
// form of ``K'' and the second element were the uppercase form of
|
||||
// ``K.''
|
||||
cased := false
|
||||
if symbol2 == 0 {
|
||||
upper := unicode.IsUpper(symbol1Rune)
|
||||
lower := unicode.IsLower(symbol1Rune)
|
||||
if upper || lower {
|
||||
symbol1Rune = unicode.ToLower(symbol1Rune)
|
||||
symbol2Rune = unicode.ToUpper(symbol1Rune)
|
||||
cased = true
|
||||
} else {
|
||||
symbol2 = symbol1
|
||||
symbol2Rune = symbol1Rune
|
||||
}
|
||||
}
|
||||
if symbol4 == 0 {
|
||||
upper := unicode.IsUpper(symbol3Rune)
|
||||
lower := unicode.IsLower(symbol3Rune)
|
||||
if upper || lower {
|
||||
symbol3Rune = unicode.ToLower(symbol3Rune)
|
||||
symbol4Rune = unicode.ToUpper(symbol3Rune)
|
||||
cased = true
|
||||
} else {
|
||||
symbol4 = symbol3
|
||||
symbol4Rune = symbol3Rune
|
||||
}
|
||||
}
|
||||
|
||||
// PARAGRAPH 5
|
||||
//
|
||||
// The standard rules for obtaining a KeySym from a KeyPress event make
|
||||
// use of only the Group 1 and Group 2 KeySyms; no interpretation of/
|
||||
// other KeySyms in the list is given. Which group to use is determined
|
||||
// by the modifier state. Switching between groups is controlled by the
|
||||
// KeySym named MODE SWITCH, by attaching that KeySym to some KeyCode
|
||||
// and attaching that KeyCode to any one of the modifiers Mod1 through
|
||||
// Mod5. This modifier is called the group modifier. For any KeyCode,
|
||||
// Group 1 is used when the group modifier is off, and Group 2 is used
|
||||
// when the group modifier is on.
|
||||
modeSwitch := state & backend.modifierMasks.modeSwitch > 0
|
||||
if modeSwitch {
|
||||
symbol1 = symbol3
|
||||
symbol1Rune = symbol3Rune
|
||||
symbol2 = symbol4
|
||||
symbol2Rune = symbol4Rune
|
||||
|
||||
}
|
||||
|
||||
// PARAGRAPH 6
|
||||
//
|
||||
// The Lock modifier is interpreted as CapsLock when the KeySym named
|
||||
// XK_Caps_Lock is attached to some KeyCode and that KeyCode is attached
|
||||
// to the Lock modifier. The Lock modifier is interpreted as ShiftLock
|
||||
// when the KeySym named XK_Shift_Lock is attached to some KeyCode and
|
||||
// that KeyCode is attached to the Lock modifier. If the Lock modifier
|
||||
// could be interpreted as both CapsLock and ShiftLock, the CapsLock
|
||||
// interpretation is used.
|
||||
shift :=
|
||||
state & xproto.ModMaskShift > 0 ||
|
||||
state & backend.modifierMasks.shiftLock > 0
|
||||
capsLock := state & backend.modifierMasks.capsLock > 0
|
||||
|
||||
// PARAGRAPH 7
|
||||
//
|
||||
// The operation of keypad keys is controlled by the KeySym named
|
||||
// XK_Num_Lock, by attaching that KeySym to some KeyCode and attaching
|
||||
// that KeyCode to any one of the modifiers Mod1 through Mod5 . This
|
||||
// modifier is called the numlock modifier. The standard KeySyms with
|
||||
// the prefix ``XK_KP_'' in their name are called keypad KeySyms; these
|
||||
// are KeySyms with numeric value in the hexadecimal range 0xFF80 to
|
||||
// 0xFFBD inclusive. In addition, vendor-specific KeySyms in the
|
||||
// hexadecimal range 0x11000000 to 0x1100FFFF are also keypad KeySyms.
|
||||
numLock := state & backend.modifierMasks.numLock > 0
|
||||
|
||||
// PARAGRAPH 8
|
||||
//
|
||||
// Within a group, the choice of KeySym is determined by applying the
|
||||
// first rule that is satisfied from the following list:
|
||||
var selectedKeysym xproto.Keysym
|
||||
var selectedRune rune
|
||||
_, symbol2IsNumPad := keypadCodeTable[symbol2]
|
||||
switch {
|
||||
case numLock && symbol2IsNumPad:
|
||||
// The numlock modifier is on and the second KeySym is a keypad
|
||||
// KeySym. In this case, if the Shift modifier is on, or if the
|
||||
// Lock modifier is on and is interpreted as ShiftLock, then the
|
||||
// first KeySym is used, otherwise the second KeySym is used.
|
||||
if shift {
|
||||
selectedKeysym = symbol1
|
||||
selectedRune = symbol1Rune
|
||||
} else {
|
||||
selectedKeysym = symbol2
|
||||
selectedRune = symbol2Rune
|
||||
}
|
||||
|
||||
case !shift && !capsLock:
|
||||
// The Shift and Lock modifiers are both off. In this case, the
|
||||
// first KeySym is used.
|
||||
selectedKeysym = symbol1
|
||||
selectedRune = symbol1Rune
|
||||
|
||||
case !shift && capsLock:
|
||||
// The Shift modifier is off, and the Lock modifier is on and is
|
||||
// interpreted as CapsLock. In this case, the first KeySym is
|
||||
// used, but if that KeySym is lowercase alphabetic, then the
|
||||
// corresponding uppercase KeySym is used instead.
|
||||
if cased && unicode.IsLower(symbol1Rune) {
|
||||
selectedRune = symbol2Rune
|
||||
} else {
|
||||
selectedKeysym = symbol1
|
||||
selectedRune = symbol1Rune
|
||||
}
|
||||
|
||||
case shift && capsLock:
|
||||
// The Shift modifier is on, and the Lock modifier is on and is
|
||||
// interpreted as CapsLock. In this case, the second KeySym is
|
||||
// used, but if that KeySym is lowercase alphabetic, then the
|
||||
// corresponding uppercase KeySym is used instead.
|
||||
if cased && unicode.IsLower(symbol2Rune) {
|
||||
selectedRune = unicode.ToUpper(symbol2Rune)
|
||||
} else {
|
||||
selectedKeysym = symbol2
|
||||
selectedRune = symbol2Rune
|
||||
}
|
||||
|
||||
case shift:
|
||||
// The Shift modifier is on, or the Lock modifier is on and is
|
||||
// interpreted as ShiftLock, or both. In this case, the second
|
||||
// KeySym is used.
|
||||
selectedKeysym = symbol2
|
||||
selectedRune = symbol2Rune
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// all of the below stuff is specific to tomo's button codes. //
|
||||
////////////////////////////////////////////////////////////////
|
||||
|
||||
// look up in control code table
|
||||
var isControl bool
|
||||
button, isControl = buttonCodeTable[selectedKeysym]
|
||||
if isControl { return }
|
||||
|
||||
// look up in keypad table
|
||||
button, numberPad = keypadCodeTable[selectedKeysym]
|
||||
if numberPad { return }
|
||||
|
||||
// otherwise, use the rune
|
||||
button = input.Key(selectedRune)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// keysymToRune takes in an X keysym and outputs a utf32 code point. This
|
||||
// function does not and should not handle keypad keys, as those are handled
|
||||
// by Backend.keycodeToButton.
|
||||
func keysymToRune (keysym xproto.Keysym) (character rune) {
|
||||
// X keysyms like 0xFF.. or 0xFE.. are non-character keys. these cannot
|
||||
// be converted so we return a zero.
|
||||
if (keysym >> 8) == 0xFF || (keysym >> 8) == 0xFE {
|
||||
character = 0
|
||||
return
|
||||
}
|
||||
|
||||
// some X keysyms have a single bit set to 1 here. i believe this is to
|
||||
// prevent conflicts with existing codes. if we mask it off we will get
|
||||
// a correct utf-32 code point.
|
||||
if keysym & 0xF000000 == 0x1000000 {
|
||||
character = rune(keysym & 0x0111111)
|
||||
return
|
||||
}
|
||||
|
||||
// if none of these things happened, we can safely (i think) assume that
|
||||
// the keysym is an exact utf-32 code point.
|
||||
character = rune(keysym)
|
||||
return
|
||||
}
|
||||
292
plugins/x/x/entity.go
Normal file
292
plugins/x/x/entity.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package x
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
|
||||
type entity struct {
|
||||
window *window
|
||||
parent *entity
|
||||
children []*entity
|
||||
element tomo.Element
|
||||
|
||||
bounds image.Rectangle
|
||||
clippedBounds image.Rectangle
|
||||
minWidth int
|
||||
minHeight int
|
||||
|
||||
selected bool
|
||||
layoutInvalid bool
|
||||
isContainer bool
|
||||
}
|
||||
|
||||
func (backend *Backend) NewEntity (owner tomo.Element) tomo.Entity {
|
||||
entity := &entity { element: owner }
|
||||
if _, ok := owner.(tomo.Container); ok {
|
||||
entity.isContainer = true
|
||||
entity.InvalidateLayout()
|
||||
}
|
||||
return entity
|
||||
}
|
||||
|
||||
func (ent *entity) unlink () {
|
||||
ent.propagate (func (child *entity) bool {
|
||||
if child.window != nil {
|
||||
delete(ent.window.system.drawingInvalid, child)
|
||||
}
|
||||
child.window = nil
|
||||
return true
|
||||
})
|
||||
|
||||
if ent.window != nil {
|
||||
delete(ent.window.system.drawingInvalid, ent)
|
||||
}
|
||||
ent.parent = nil
|
||||
ent.window = nil
|
||||
|
||||
if element, ok := ent.element.(tomo.Selectable); ok {
|
||||
ent.selected = false
|
||||
element.HandleSelectionChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (entity *entity) link (parent *entity) {
|
||||
entity.parent = parent
|
||||
entity.clip(parent.clippedBounds)
|
||||
if parent.window != nil {
|
||||
entity.setWindow(parent.window)
|
||||
}
|
||||
}
|
||||
|
||||
func (ent *entity) setWindow (window *window) {
|
||||
ent.window = window
|
||||
ent.Invalidate()
|
||||
ent.InvalidateLayout()
|
||||
ent.propagate (func (child *entity) bool {
|
||||
child.window = window
|
||||
ent.Invalidate()
|
||||
ent.InvalidateLayout()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (entity *entity) propagate (callback func (*entity) bool) bool {
|
||||
for _, child := range entity.children {
|
||||
if !child.propagate(callback) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return callback(entity)
|
||||
}
|
||||
|
||||
|
||||
func (entity *entity) propagateAlt (callback func (*entity) bool) bool {
|
||||
if !callback(entity) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, child := range entity.children {
|
||||
if !child.propagate(callback) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
func (entity *entity) childAt (point image.Point) *entity {
|
||||
for _, child := range entity.children {
|
||||
if point.In(child.bounds) {
|
||||
return child.childAt(point)
|
||||
}
|
||||
}
|
||||
return entity
|
||||
}
|
||||
|
||||
func (entity *entity) scrollTargetChildAt (point image.Point) *entity {
|
||||
for _, child := range entity.children {
|
||||
if point.In(child.bounds) {
|
||||
result := child.scrollTargetChildAt(point)
|
||||
if result != nil { return result }
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := entity.element.(tomo.ScrollTarget); ok {
|
||||
return entity
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer, tomo.Element)) {
|
||||
if entity.parent == nil { return }
|
||||
if parent, ok := entity.parent.element.(tomo.MouseTargetContainer); ok {
|
||||
callback(parent, entity.element)
|
||||
}
|
||||
entity.parent.forMouseTargetContainers(callback)
|
||||
}
|
||||
|
||||
func (entity *entity) clip (bounds image.Rectangle) {
|
||||
entity.clippedBounds = entity.bounds.Intersect(bounds)
|
||||
for _, child := range entity.children {
|
||||
child.clip(entity.clippedBounds)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------- Entity ----------- //
|
||||
|
||||
func (entity *entity) Invalidate () {
|
||||
if entity.window == nil { return }
|
||||
if entity.window.system.invalidateIgnore { return }
|
||||
entity.window.drawingInvalid.Add(entity)
|
||||
}
|
||||
|
||||
func (entity *entity) Bounds () image.Rectangle {
|
||||
return entity.bounds
|
||||
}
|
||||
|
||||
func (entity *entity) Window () tomo.Window {
|
||||
return entity.window
|
||||
}
|
||||
|
||||
func (entity *entity) SetMinimumSize (width, height int) {
|
||||
entity.minWidth = width
|
||||
entity.minHeight = height
|
||||
if entity.parent == nil {
|
||||
if entity.window != nil {
|
||||
entity.window.setMinimumSize(width, height)
|
||||
}
|
||||
} else {
|
||||
entity.parent.element.(tomo.Container).
|
||||
HandleChildMinimumSizeChange(entity.element)
|
||||
}
|
||||
}
|
||||
|
||||
func (entity *entity) DrawBackground (destination canvas.Canvas) {
|
||||
if entity.parent != nil {
|
||||
entity.parent.element.(tomo.Container).DrawBackground(destination)
|
||||
} else if entity.window != nil {
|
||||
entity.window.system.theme.Pattern (
|
||||
tomo.PatternBackground,
|
||||
tomo.State { }).Draw (
|
||||
destination,
|
||||
entity.window.canvas.Bounds())
|
||||
}
|
||||
}
|
||||
|
||||
// ----------- ContainerEntity ----------- //
|
||||
|
||||
func (entity *entity) InvalidateLayout () {
|
||||
if entity.window == nil { return }
|
||||
if !entity.isContainer { return }
|
||||
entity.layoutInvalid = true
|
||||
entity.window.system.anyLayoutInvalid = true
|
||||
}
|
||||
|
||||
func (ent *entity) Adopt (child tomo.Element) {
|
||||
childEntity, ok := child.Entity().(*entity)
|
||||
if !ok || childEntity == nil { return }
|
||||
childEntity.link(ent)
|
||||
ent.children = append(ent.children, childEntity)
|
||||
}
|
||||
|
||||
func (ent *entity) Insert (index int, child tomo.Element) {
|
||||
childEntity, ok := child.Entity().(*entity)
|
||||
if !ok || childEntity == nil { return }
|
||||
ent.children = append (
|
||||
ent.children[:index + 1],
|
||||
ent.children[index:]...)
|
||||
ent.children[index] = childEntity
|
||||
}
|
||||
|
||||
func (entity *entity) Disown (index int) {
|
||||
entity.children[index].unlink()
|
||||
entity.children = append (
|
||||
entity.children[:index],
|
||||
entity.children[index + 1:]...)
|
||||
}
|
||||
|
||||
func (entity *entity) IndexOf (child tomo.Element) int {
|
||||
for index, childEntity := range entity.children {
|
||||
if childEntity.element == child {
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (entity *entity) Child (index int) tomo.Element {
|
||||
return entity.children[index].element
|
||||
}
|
||||
|
||||
func (entity *entity) CountChildren () int {
|
||||
return len(entity.children)
|
||||
}
|
||||
|
||||
func (entity *entity) PlaceChild (index int, bounds image.Rectangle) {
|
||||
child := entity.children[index]
|
||||
child.bounds = bounds
|
||||
child.clip(entity.clippedBounds)
|
||||
child.Invalidate()
|
||||
child.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (entity *entity) SelectChild (index int, selected bool) {
|
||||
child := entity.children[index]
|
||||
if element, ok := child.element.(tomo.Selectable); ok {
|
||||
if child.selected == selected { return }
|
||||
child.selected = selected
|
||||
element.HandleSelectionChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (entity *entity) ChildMinimumSize (index int) (width, height int) {
|
||||
childEntity := entity.children[index]
|
||||
return childEntity.minWidth, childEntity.minHeight
|
||||
}
|
||||
|
||||
// ----------- FocusableEntity ----------- //
|
||||
|
||||
func (entity *entity) Focused () bool {
|
||||
if entity.window == nil { return false }
|
||||
return entity.window.focused == entity
|
||||
}
|
||||
|
||||
func (entity *entity) Focus () {
|
||||
if entity.window == nil { return }
|
||||
entity.window.system.focus(entity)
|
||||
}
|
||||
|
||||
func (entity *entity) FocusNext () {
|
||||
entity.window.system.focusNext()
|
||||
}
|
||||
|
||||
func (entity *entity) FocusPrevious () {
|
||||
entity.window.system.focusPrevious()
|
||||
}
|
||||
|
||||
// ----------- SelectableEntity ----------- //
|
||||
|
||||
func (entity *entity) Selected () bool {
|
||||
return entity.selected
|
||||
}
|
||||
|
||||
// ----------- FlexibleEntity ----------- //
|
||||
|
||||
func (entity *entity) NotifyFlexibleHeightChange () {
|
||||
if entity.parent == nil { return }
|
||||
if parent, ok := entity.parent.element.(tomo.FlexibleContainer); ok {
|
||||
parent.HandleChildFlexibleHeightChange (
|
||||
entity.element.(tomo.Flexible))
|
||||
}
|
||||
}
|
||||
|
||||
// ----------- ScrollableEntity ----------- //
|
||||
|
||||
func (entity *entity) NotifyScrollBoundsChange () {
|
||||
if entity.parent == nil { return }
|
||||
if parent, ok := entity.parent.element.(tomo.ScrollableContainer); ok {
|
||||
parent.HandleChildScrollBoundsChange (
|
||||
entity.element.(tomo.Scrollable))
|
||||
}
|
||||
}
|
||||
428
plugins/x/x/event.go
Normal file
428
plugins/x/x/event.go
Normal file
@@ -0,0 +1,428 @@
|
||||
package x
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||
|
||||
import "github.com/jezek/xgbutil"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
|
||||
type scrollSum struct {
|
||||
x, y int
|
||||
}
|
||||
|
||||
const scrollDistance = 16
|
||||
|
||||
func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) {
|
||||
shift :=
|
||||
(state & xproto.ModMaskShift) > 0 ||
|
||||
(state & window.backend.modifierMasks.shiftLock) > 0
|
||||
if 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) handleExpose (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ExposeEvent,
|
||||
) {
|
||||
_, region := window.compressExpose(*event.ExposeEvent)
|
||||
window.pushRegion(region)
|
||||
}
|
||||
|
||||
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()
|
||||
window.metrics.bounds = tomo.Bounds (
|
||||
windowGeometry.X() + decorGeometry.X(),
|
||||
windowGeometry.Y() + decorGeometry.Y(),
|
||||
windowGeometry.Width(), windowGeometry.Height())
|
||||
}
|
||||
|
||||
func (window *window) handleConfigureNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ConfigureNotifyEvent,
|
||||
) {
|
||||
if window.child == nil { return }
|
||||
|
||||
configureEvent := *event.ConfigureNotifyEvent
|
||||
|
||||
newWidth := int(configureEvent.Width)
|
||||
newHeight := int(configureEvent.Height)
|
||||
sizeChanged :=
|
||||
window.metrics.bounds.Dx() != newWidth ||
|
||||
window.metrics.bounds.Dy() != newHeight
|
||||
window.updateBounds()
|
||||
|
||||
if sizeChanged {
|
||||
configureEvent = window.compressConfigureNotify(configureEvent)
|
||||
window.reallocateCanvas()
|
||||
window.resizeChildToFit()
|
||||
|
||||
if !window.exposeEventFollows(configureEvent) {
|
||||
window.child.Invalidate()
|
||||
window.child.InvalidateLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (found bool) {
|
||||
nextEvents := xevent.Peek(window.backend.connection)
|
||||
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 (window *window) modifiersFromState (
|
||||
state uint16,
|
||||
) (
|
||||
modifiers input.Modifiers,
|
||||
) {
|
||||
return input.Modifiers {
|
||||
Shift:
|
||||
(state & xproto.ModMaskShift) > 0 ||
|
||||
(state & window.backend.modifierMasks.shiftLock) > 0,
|
||||
Control: (state & xproto.ModMaskControl) > 0,
|
||||
Alt: (state & window.backend.modifierMasks.alt) > 0,
|
||||
Meta: (state & window.backend.modifierMasks.meta) > 0,
|
||||
Super: (state & window.backend.modifierMasks.super) > 0,
|
||||
Hyper: (state & window.backend.modifierMasks.hyper) > 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) handleKeyPress (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.KeyPressEvent,
|
||||
) {
|
||||
if window.hasModal { return }
|
||||
|
||||
keyEvent := *event.KeyPressEvent
|
||||
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
|
||||
modifiers := window.modifiersFromState(keyEvent.State)
|
||||
modifiers.NumberPad = numberPad
|
||||
|
||||
if key == input.KeyTab && modifiers.Alt {
|
||||
if modifiers.Shift {
|
||||
window.system.focusPrevious()
|
||||
} else {
|
||||
window.system.focusNext()
|
||||
}
|
||||
} else if key == input.KeyEscape && window.shy {
|
||||
window.Close()
|
||||
} else if window.focused != nil {
|
||||
focused, ok := window.focused.element.(tomo.KeyboardTarget)
|
||||
if ok { focused.HandleKeyDown(key, modifiers) }
|
||||
}
|
||||
}
|
||||
|
||||
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.connection)
|
||||
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 := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
|
||||
modifiers := window.modifiersFromState(keyEvent.State)
|
||||
modifiers.NumberPad = numberPad
|
||||
|
||||
if window.focused != nil {
|
||||
focused, ok := window.focused.element.(tomo.KeyboardTarget)
|
||||
if ok { focused.HandleKeyUp(key, modifiers) }
|
||||
}
|
||||
}
|
||||
|
||||
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.canvas.Bounds())
|
||||
scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7
|
||||
modifiers := window.modifiersFromState(buttonEvent.State)
|
||||
|
||||
if !insideWindow && window.shy && !scrolling {
|
||||
window.Close()
|
||||
} else if scrolling {
|
||||
underneath := window.system.scrollTargetChildAt(point)
|
||||
if underneath != nil {
|
||||
if child, ok := underneath.element.(tomo.ScrollTarget); ok {
|
||||
sum := scrollSum { }
|
||||
sum.add(buttonEvent.Detail, window, buttonEvent.State)
|
||||
window.compressScrollSum(buttonEvent, &sum)
|
||||
child.HandleScroll (
|
||||
point, float64(sum.x), float64(sum.y),
|
||||
modifiers)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
underneath := window.system.childAt(point)
|
||||
window.system.drags[buttonEvent.Detail] = underneath
|
||||
if child, ok := underneath.element.(tomo.MouseTarget); ok {
|
||||
child.HandleMouseDown (
|
||||
point, input.Button(buttonEvent.Detail),
|
||||
modifiers)
|
||||
}
|
||||
callback := func (container tomo.MouseTargetContainer, child tomo.Element) {
|
||||
container.HandleChildMouseDown (
|
||||
point, input.Button(buttonEvent.Detail),
|
||||
modifiers, child)
|
||||
}
|
||||
underneath.forMouseTargetContainers(callback)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) handleButtonRelease (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.ButtonReleaseEvent,
|
||||
) {
|
||||
if window.hasModal { return }
|
||||
|
||||
buttonEvent := *event.ButtonReleaseEvent
|
||||
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
|
||||
modifiers := window.modifiersFromState(buttonEvent.State)
|
||||
dragging := window.system.drags[buttonEvent.Detail]
|
||||
|
||||
if dragging != nil {
|
||||
if child, ok := dragging.element.(tomo.MouseTarget); ok {
|
||||
child.HandleMouseUp (
|
||||
image.Pt (
|
||||
int(buttonEvent.EventX),
|
||||
int(buttonEvent.EventY)),
|
||||
input.Button(buttonEvent.Detail),
|
||||
modifiers)
|
||||
}
|
||||
callback := func (container tomo.MouseTargetContainer, child tomo.Element) {
|
||||
container.HandleChildMouseUp (
|
||||
image.Pt (
|
||||
int(buttonEvent.EventX),
|
||||
int(buttonEvent.EventY)),
|
||||
input.Button(buttonEvent.Detail),
|
||||
modifiers, child)
|
||||
}
|
||||
dragging.forMouseTargetContainers(callback)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) handleMotionNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.MotionNotifyEvent,
|
||||
) {
|
||||
if window.hasModal { return }
|
||||
|
||||
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
|
||||
x := int(motionEvent.EventX)
|
||||
y :=int(motionEvent.EventY)
|
||||
|
||||
handled := false
|
||||
for _, child := range window.system.drags {
|
||||
if child == nil { continue }
|
||||
if child, ok := child.element.(tomo.MotionTarget); ok {
|
||||
child.HandleMotion(image.Pt(x, y))
|
||||
handled = true
|
||||
}
|
||||
}
|
||||
|
||||
if !handled {
|
||||
child := window.system.childAt(image.Pt(x, y))
|
||||
if child, ok := child.element.(tomo.MotionTarget); ok {
|
||||
child.HandleMotion(image.Pt(x, y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) handleSelectionNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.SelectionNotifyEvent,
|
||||
) {
|
||||
if window.selectionRequest == nil { return }
|
||||
window.selectionRequest.handleSelectionNotify(connection, event)
|
||||
if !window.selectionRequest.open() { window.selectionRequest = nil }
|
||||
}
|
||||
|
||||
func (window *window) handlePropertyNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.PropertyNotifyEvent,
|
||||
) {
|
||||
if window.selectionRequest == nil { return }
|
||||
window.selectionRequest.handlePropertyNotify(connection, event)
|
||||
if !window.selectionRequest.open() { window.selectionRequest = nil }
|
||||
}
|
||||
|
||||
func (window *window) handleSelectionClear (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.SelectionClearEvent,
|
||||
) {
|
||||
window.selectionClaim = nil
|
||||
}
|
||||
|
||||
func (window *window) handleSelectionRequest (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.SelectionRequestEvent,
|
||||
) {
|
||||
if window.selectionClaim == nil { return }
|
||||
window.selectionClaim.handleSelectionRequest(connection, event)
|
||||
}
|
||||
|
||||
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.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
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.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *window) compressConfigureNotify (
|
||||
firstEvent xproto.ConfigureNotifyEvent,
|
||||
) (
|
||||
lastEvent xproto.ConfigureNotifyEvent,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
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.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *window) compressScrollSum (
|
||||
firstEvent xproto.ButtonPressEvent,
|
||||
sum *scrollSum,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
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.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (window *window) compressMotionNotify (
|
||||
firstEvent xproto.MotionNotifyEvent,
|
||||
) (
|
||||
lastEvent xproto.MotionNotifyEvent,
|
||||
) {
|
||||
window.backend.connection.Sync()
|
||||
xevent.Read(window.backend.connection, false)
|
||||
lastEvent = firstEvent
|
||||
|
||||
for index, untypedEvent := range xevent.Peek(window.backend.connection) {
|
||||
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.connection, index)
|
||||
} (index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
365
plugins/x/x/selection.go
Normal file
365
plugins/x/x/selection.go
Normal file
@@ -0,0 +1,365 @@
|
||||
package x
|
||||
|
||||
import "errors"
|
||||
import "strings"
|
||||
import "github.com/jezek/xgb"
|
||||
import "github.com/jezek/xgbutil"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/xprop"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||
|
||||
const clipboardName = "CLIPBOARD"
|
||||
|
||||
type selReqState int; const (
|
||||
selReqStateClosed selReqState = iota
|
||||
selReqStateAwaitTargets
|
||||
selReqStateAwaitValue
|
||||
selReqStateAwaitChunk
|
||||
)
|
||||
|
||||
type selectionRequest struct {
|
||||
state selReqState
|
||||
window *window
|
||||
source xproto.Atom
|
||||
destination xproto.Atom
|
||||
accept []data.Mime
|
||||
incrBuffer []byte
|
||||
incrMime data.Mime
|
||||
callback func (data.Data, error)
|
||||
}
|
||||
|
||||
func (window *window) newSelectionRequest (
|
||||
source, destination xproto.Atom,
|
||||
callback func (data.Data, error),
|
||||
accept ...data.Mime,
|
||||
) (
|
||||
request *selectionRequest,
|
||||
) {
|
||||
request = &selectionRequest {
|
||||
source: source,
|
||||
destination: destination,
|
||||
window: window,
|
||||
accept: accept,
|
||||
callback: callback,
|
||||
}
|
||||
|
||||
targets, err := xprop.Atm(window.backend.connection, "TARGETS")
|
||||
if err != nil { request.die(err); return }
|
||||
request.convertSelection(targets, selReqStateAwaitTargets)
|
||||
return
|
||||
}
|
||||
|
||||
func (request *selectionRequest) convertSelection (
|
||||
target xproto.Atom, switchTo selReqState,
|
||||
) {
|
||||
// The requestor should set the property argument to the name of a
|
||||
// property that the owner can use to report the value of the selection.
|
||||
// Requestors should ensure that the named property does not exist on
|
||||
// the window before issuing the ConvertSelection. The exception to this
|
||||
// rule is when the requestor intends to pass parameters with the
|
||||
// request. Some targets may be defined such that requestors can pass
|
||||
// parameters along with the request. If the requestor wishes to provide
|
||||
// parameters to a request, they should be placed in the specified
|
||||
// property on the requestor window before the requestor issues the
|
||||
// ConvertSelection request, and this property should be named in the
|
||||
// request.
|
||||
err := xproto.DeletePropertyChecked (
|
||||
request.window.backend.connection.Conn(),
|
||||
request.window.xWindow.Id,
|
||||
request.destination).Check()
|
||||
if err != nil { request.die(err); return }
|
||||
|
||||
// The selection argument specifies the particular selection involved,
|
||||
// and the target argument specifies the required form of the
|
||||
// information. For information about the choice of suitable atoms to
|
||||
// use, see section 2.6. The requestor should set the requestor argument
|
||||
// to a window that it created; the owner will place the reply property
|
||||
// there. The requestor should set the time argument to the timestamp on
|
||||
// the event that triggered the request for the selection value. Note
|
||||
// that clients should not specify CurrentTime*.
|
||||
err = xproto.ConvertSelectionChecked (
|
||||
request.window.backend.connection.Conn(),
|
||||
request.window.xWindow.Id,
|
||||
request.source,
|
||||
target,
|
||||
request.destination,
|
||||
// TODO: *possibly replace this zero with an actual timestamp
|
||||
// received from the server. this is non-trivial as we cannot
|
||||
// rely on the timestamp of the last received event, because
|
||||
// there is a possibility that this method is invoked
|
||||
// asynchronously from within tomo.Do().
|
||||
0).Check()
|
||||
if err != nil { request.die(err); return }
|
||||
|
||||
request.state = switchTo
|
||||
}
|
||||
|
||||
func (request *selectionRequest) die (err error) {
|
||||
request.callback(nil, err)
|
||||
request.window.system.afterEvent()
|
||||
request.state = selReqStateClosed
|
||||
}
|
||||
|
||||
func (request *selectionRequest) finalize (data data.Data) {
|
||||
request.callback(data, nil)
|
||||
request.window.system.afterEvent()
|
||||
request.state = selReqStateClosed
|
||||
}
|
||||
|
||||
func (request *selectionRequest) open () bool {
|
||||
return request.state != selReqStateClosed
|
||||
}
|
||||
|
||||
type confidence int; const (
|
||||
confidenceNone confidence = iota
|
||||
confidencePartial
|
||||
confidenceFull
|
||||
)
|
||||
|
||||
func targetToMime (name string) (data.Mime, confidence) {
|
||||
// TODO: add other stuff. reference this table:
|
||||
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
|
||||
// perhaps we should also have parameters for mime types so we can
|
||||
// return an encoding here for things like STRING?
|
||||
switch name {
|
||||
case "ADOBE_PORTABLE_DOCUMENT_FORMAT":
|
||||
return data.M("application", "pdf"), confidenceFull
|
||||
case "APPLE_PICT":
|
||||
return data.M("image", "pict"), confidenceFull
|
||||
case
|
||||
"POSTSCRIPT",
|
||||
"ENCAPSULATED_POSTSCRIPT",
|
||||
"ENCAPSULATED_POSTSCRIPT_INTERCHANGE":
|
||||
return data.M("application", "postscript"), confidenceFull
|
||||
case "FILE_NAME":
|
||||
return data.MimeFile, confidenceFull
|
||||
case "UTF8_STRING":
|
||||
return data.MimePlain, confidenceFull
|
||||
case "TEXT":
|
||||
return data.MimePlain, confidencePartial
|
||||
case "STRING":
|
||||
return data.MimePlain, confidencePartial
|
||||
default:
|
||||
if strings.Count(name, "/") == 1 {
|
||||
ty, subtype, _ := strings.Cut(name, "/")
|
||||
return data.M(ty, subtype), confidenceFull
|
||||
} else {
|
||||
return data.Mime { }, confidenceNone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mimeToTargets (mime data.Mime) (names []string) {
|
||||
names = append(names, mime.String())
|
||||
switch mime {
|
||||
case data.M("application", "pdf"):
|
||||
names = append(names, "ADOBE_PORTABLE_DOCUMENT_FORMAT")
|
||||
case data.M("image", "pict"):
|
||||
names = append(names, "APPLE_PICT")
|
||||
case data.M("application", "postscript"):
|
||||
names = append(names, "POSTSCRIPT")
|
||||
case data.MimeFile:
|
||||
names = append(names, "FILE_NAME")
|
||||
case data.MimePlain:
|
||||
names = append(names, "UTF8_STRING", "TEXT", "STRING")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (request *selectionRequest) handleSelectionNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.SelectionNotifyEvent,
|
||||
) {
|
||||
// the only valid states that we can process a SelectionNotify event in
|
||||
invalidState :=
|
||||
request.state != selReqStateAwaitValue &&
|
||||
request.state != selReqStateAwaitTargets
|
||||
if invalidState { return }
|
||||
|
||||
// Follow:
|
||||
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.4
|
||||
|
||||
// If the property argument is None, the conversion has been refused.
|
||||
// This can mean either that there is no owner for the selection, that
|
||||
// the owner does not support the conversion implied by the target, or
|
||||
// that the server did not have sufficient space to accommodate the
|
||||
// data.
|
||||
if event.Property == 0 { request.die(nil); return }
|
||||
|
||||
// When using GetProperty to retrieve the value of a selection, the
|
||||
// property argument should be set to the corresponding value in the
|
||||
// SelectionNotify event. Because the requestor has no way of knowing
|
||||
// beforehand what type the selection owner will use, the type argument
|
||||
// should be set to AnyPropertyType. Several GetProperty requests may be
|
||||
// needed to retrieve all the data in the selection; each should set the
|
||||
// long-offset argument to the amount of data received so far, and the
|
||||
// size argument to some reasonable buffer size (see section 2.5). If
|
||||
// the returned value of bytes-after is zero, the whole property has
|
||||
// been transferred.
|
||||
reply, err := xproto.GetProperty (
|
||||
connection.Conn(), false, request.window.xWindow.Id,
|
||||
event.Property, xproto.GetPropertyTypeAny,
|
||||
0, (1 << 32) - 1).Reply()
|
||||
if err != nil { request.die(err); return }
|
||||
if reply.Format == 0 {
|
||||
request.die(errors.New("x: missing selection property"))
|
||||
return
|
||||
}
|
||||
|
||||
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.7.2
|
||||
// Requestors may receive a property of type INCR9 in response to any
|
||||
// target that results in selection data. This indicates that the owner
|
||||
// will send the actual data incrementally. The contents of the INCR
|
||||
// property will be an integer, which represents a lower bound on the
|
||||
// number of bytes of data in the selection. The requestor and the
|
||||
// selection owner transfer the data in the selection in the following
|
||||
// manner. The selection requestor starts the transfer process by
|
||||
// deleting the (type==INCR) property forming the reply to the
|
||||
// selection.
|
||||
incr, err := xprop.Atm(request.window.backend.connection, "INCR")
|
||||
if err != nil { request.die(err); return }
|
||||
if reply.Type == incr {
|
||||
// reply to the INCR selection
|
||||
err = xproto.DeletePropertyChecked (
|
||||
request.window.backend.connection.Conn(),
|
||||
request.window.xWindow.Id,
|
||||
request.destination).Check()
|
||||
if err != nil { request.die(err); return }
|
||||
|
||||
// await the first chunk
|
||||
request.state = selReqStateAwaitChunk
|
||||
return
|
||||
}
|
||||
|
||||
// Once all the data in the selection has been retrieved (which may
|
||||
// require getting the values of several properties &emdash; see section
|
||||
// 2.7), the requestor should delete the property in the SelectionNotify
|
||||
// request by using a GetProperty request with the delete argument set
|
||||
// to True. As previously discussed, the owner has no way of knowing
|
||||
// when the data has been transferred to the requestor unless the
|
||||
// property is removed.
|
||||
err = xproto.DeletePropertyChecked (
|
||||
request.window.backend.connection.Conn(),
|
||||
request.window.xWindow.Id,
|
||||
request.destination).Check()
|
||||
if err != nil { request.die(err); return }
|
||||
|
||||
// depending on which state the selection request is in, do something
|
||||
// different with the property's value
|
||||
switch request.state {
|
||||
case selReqStateAwaitValue:
|
||||
// get the type from the property and convert that to the mime
|
||||
// value to pass to the application.
|
||||
targetName, err := xprop.AtomName (
|
||||
request.window.backend.connection, reply.Type)
|
||||
if err != nil { request.die(err); return }
|
||||
mime, _ := targetToMime(targetName)
|
||||
|
||||
// we now have the full selection data in the property, so we
|
||||
// finalize the request and are done.
|
||||
request.finalize(data.Bytes(mime, reply.Value))
|
||||
|
||||
case selReqStateAwaitTargets:
|
||||
// make a list of the atoms we got
|
||||
buffer := reply.Value
|
||||
atoms := make([]xproto.Atom, len(buffer) / 4)
|
||||
for index := range atoms {
|
||||
atoms[index] = xproto.Atom(xgb.Get32(buffer[index * 4:]))
|
||||
}
|
||||
|
||||
// choose the best match out of all targets using a confidence
|
||||
// system
|
||||
confidentMatchFound := false
|
||||
var chosenTarget xproto.Atom
|
||||
for _, atom := range atoms {
|
||||
targetName, err := xprop.AtomName (
|
||||
request.window.backend.connection, atom)
|
||||
if err != nil { request.die(err); return }
|
||||
|
||||
mime, confidence := targetToMime(targetName)
|
||||
if confidence == confidenceNone { continue }
|
||||
|
||||
// if the accepted types list is nil, just choose this
|
||||
// one. however, if we are not 100% confident that this
|
||||
// target can be directly converted into a mime type,
|
||||
// don't mark it as the final match. we still want the
|
||||
// mime type we give to the application to be as
|
||||
// accurate as possible.
|
||||
if request.accept == nil {
|
||||
chosenTarget = atom
|
||||
if confidence == confidenceFull {
|
||||
confidentMatchFound = true
|
||||
}
|
||||
}
|
||||
|
||||
// run through the accepted types list if it exists,
|
||||
// looking for a match. if one is found, then choose
|
||||
// this target. however, if we are not 100% confident
|
||||
// that this target directly corresponds to the mime
|
||||
// type, don't mark it as the final match, because there
|
||||
// may be a better target in the list.
|
||||
for _, accept := range request.accept {
|
||||
if accept == mime {
|
||||
chosenTarget = atom
|
||||
if confidence == confidenceFull {
|
||||
confidentMatchFound = true
|
||||
}
|
||||
break
|
||||
}}
|
||||
|
||||
if confidentMatchFound { break }
|
||||
}
|
||||
|
||||
// if we didn't find a match, finalize the request with an empty
|
||||
// data map to inform the application that, although there were
|
||||
// no errors, there wasn't a suitable target to choose from.
|
||||
if chosenTarget == 0 {
|
||||
request.finalize(data.Data { })
|
||||
return
|
||||
}
|
||||
|
||||
// await the selection value
|
||||
request.convertSelection(chosenTarget, selReqStateAwaitValue)
|
||||
}
|
||||
}
|
||||
|
||||
func (request *selectionRequest) handlePropertyNotify (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.PropertyNotifyEvent,
|
||||
) {
|
||||
// the only valid state that we can process a PropertyNotify event in
|
||||
if request.state != selReqStateAwaitChunk { return }
|
||||
if event.State != xproto.PropertyNewValue { return }
|
||||
|
||||
request.handleINCRProperty(event.Atom)
|
||||
}
|
||||
|
||||
func (request *selectionRequest) handleINCRProperty (property xproto.Atom) {
|
||||
// Retrieving data using GetProperty with the delete argument True.
|
||||
reply, err := xproto.GetProperty (
|
||||
request.window.backend.connection.Conn(), true,
|
||||
request.window.xWindow.Id, property, xproto.GetPropertyTypeAny,
|
||||
0, (1 << 32) - 1).Reply()
|
||||
if err != nil { request.die(err); return }
|
||||
|
||||
if len(reply.Value) == 0 {
|
||||
// a zero length property means the transfer has finished. we
|
||||
// finalize the request with the data we have, and don't wait
|
||||
// for more.
|
||||
request.finalize(data.Bytes(request.incrMime, request.incrBuffer))
|
||||
|
||||
// we want to be extra sure we aren't holding onto this memory
|
||||
request.incrBuffer = nil
|
||||
} else {
|
||||
// a property with content means the transfer is still ongoing.
|
||||
// we append the data we got and wait for more.
|
||||
request.state = selReqStateAwaitChunk
|
||||
request.incrBuffer = append(request.incrBuffer, reply.Value...)
|
||||
|
||||
targetName, err := xprop.AtomName (
|
||||
request.window.backend.connection, reply.Type)
|
||||
if err != nil { request.die(err); return }
|
||||
request.incrMime, _ = targetToMime(targetName)
|
||||
}
|
||||
}
|
||||
164
plugins/x/x/selectionclaim.go
Normal file
164
plugins/x/x/selectionclaim.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package x
|
||||
|
||||
import "io"
|
||||
import "github.com/jezek/xgb"
|
||||
import "github.com/jezek/xgbutil"
|
||||
import "github.com/jezek/xgb/xproto"
|
||||
import "github.com/jezek/xgbutil/xprop"
|
||||
import "github.com/jezek/xgbutil/xevent"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||
|
||||
type selectionClaim struct {
|
||||
window *window
|
||||
data data.Data
|
||||
name xproto.Atom
|
||||
}
|
||||
|
||||
func (window *window) claimSelection (name xproto.Atom, data data.Data) *selectionClaim {
|
||||
// Follow:
|
||||
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.1
|
||||
|
||||
// A client wishing to acquire ownership of a particular selection
|
||||
// should call SetSelectionOwner. The client should set the specified
|
||||
// selection to the atom that represents the selection, set the
|
||||
// specified owner to some window that the client created, and set the
|
||||
// specified time to some time between the current last-change time of
|
||||
// the selection concerned and the current server time. This time value
|
||||
// usually will be obtained from the timestamp of the event that
|
||||
// triggers the acquisition of the selection. Clients should not set the
|
||||
// time value to CurrentTime, because if they do so, they have no way of
|
||||
// finding when they gained ownership of the selection. Clients must use
|
||||
// a window they created so that requestors can route events to the
|
||||
// owner of the selection.
|
||||
err := xproto.SetSelectionOwnerChecked (
|
||||
window.backend.connection.Conn(),
|
||||
window.xWindow.Id, name, 0).Check() // FIXME: should not be zero
|
||||
if err != nil { return nil }
|
||||
|
||||
ownerReply, err := xproto.GetSelectionOwner (
|
||||
window.backend.connection.Conn(), name).Reply()
|
||||
if err != nil { return nil }
|
||||
if ownerReply.Owner != window.xWindow.Id { return nil }
|
||||
|
||||
return &selectionClaim {
|
||||
window: window,
|
||||
data: data,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) refuseSelectionRequest (request xevent.SelectionRequestEvent) {
|
||||
// ... refuse the SelectionRequest by sending the requestor window a
|
||||
// SelectionNotify event with the property set to None (by means of a
|
||||
// SendEvent request with an empty event mask).
|
||||
event := xproto.SelectionNotifyEvent {
|
||||
Requestor: request.Requestor,
|
||||
Selection: request.Selection,
|
||||
Target: request.Target,
|
||||
Property: 0,
|
||||
}.Bytes()
|
||||
xproto.SendEvent (
|
||||
window.backend.connection.Conn(),
|
||||
false, request.Requestor, 0, string(event))
|
||||
}
|
||||
|
||||
func (window *window) fulfillSelectionRequest (
|
||||
data []byte,
|
||||
format byte,
|
||||
ty xproto.Atom,
|
||||
request xevent.SelectionRequestEvent,
|
||||
) {
|
||||
die := func () { window.refuseSelectionRequest(request) }
|
||||
|
||||
// If the specified property is not None, the owner should place the
|
||||
// data resulting from converting the selection into the specified
|
||||
// property on the requestor window and should set the property's type
|
||||
// to some appropriate value, which need not be the same as the
|
||||
// specified target.
|
||||
err := xproto.ChangePropertyChecked (
|
||||
window.backend.connection.Conn(),
|
||||
xproto.PropModeReplace, request.Requestor,
|
||||
request.Property,
|
||||
ty, format,
|
||||
uint32(len(data) / (int(format) / 8)), data).Check()
|
||||
if err != nil { die() }
|
||||
|
||||
// If the property is successfully stored, the owner should acknowledge
|
||||
// the successful conversion by sending the requestor window a
|
||||
// SelectionNotify event (by means of a SendEvent request with an empty
|
||||
// mask).
|
||||
event := xproto.SelectionNotifyEvent {
|
||||
Requestor: request.Requestor,
|
||||
Selection: request.Selection,
|
||||
Target: request.Target,
|
||||
Property: request.Property,
|
||||
}.Bytes()
|
||||
xproto.SendEvent (
|
||||
window.backend.connection.Conn(),
|
||||
false, request.Requestor, 0, string(event))
|
||||
}
|
||||
|
||||
func (claim *selectionClaim) handleSelectionRequest (
|
||||
connection *xgbutil.XUtil,
|
||||
event xevent.SelectionRequestEvent,
|
||||
) {
|
||||
// Follow:
|
||||
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.2
|
||||
|
||||
die := func () { claim.window.refuseSelectionRequest(event) }
|
||||
|
||||
// When a requestor wants the value of a selection, the owner receives a
|
||||
// SelectionRequest event. The specified owner and selection will be the
|
||||
// values that were specified in the SetSelectionOwner request. The
|
||||
// owner should compare the timestamp with the period it has owned the
|
||||
// selection and, if the time is outside, refuse the SelectionRequest.
|
||||
if event.Selection != claim.name { die(); return }
|
||||
|
||||
// If the specified property is None , the requestor is an obsolete
|
||||
// client. Owners are encouraged to support these clients by using the
|
||||
// specified target atom as the property name to be used for the reply.
|
||||
if event.Property == 0 {
|
||||
event.Property = event.Target
|
||||
}
|
||||
|
||||
// Otherwise, the owner should use the target to decide the form into
|
||||
// which the selection should be converted. Some targets may be defined
|
||||
// such that requestors can pass parameters along with the request. The
|
||||
// owner will find these parameters in the property named in the
|
||||
// selection request. The type, format, and contents of this property
|
||||
// are dependent upon the definition of the target. If the target is not
|
||||
// defined to have parameters, the owner should ignore the property if
|
||||
// it is present. If the selection cannot be converted into a form based
|
||||
// on the target (and parameters, if any), the owner should refuse the
|
||||
// SelectionRequest as previously described.
|
||||
targetName, err := xprop.AtomName (
|
||||
claim.window.backend.connection, event.Target)
|
||||
if err != nil { die(); return }
|
||||
|
||||
switch targetName {
|
||||
case "TARGETS":
|
||||
targetNames := []string { "TARGETS", }
|
||||
for mime := range claim.data {
|
||||
targetNames = append(targetNames, mimeToTargets(mime)...)
|
||||
}
|
||||
data := make([]byte, len(targetNames) * 4)
|
||||
for index, name := range targetNames {
|
||||
atom, err := xprop.Atm(claim.window.backend.connection, name)
|
||||
if err != nil { die(); return }
|
||||
xgb.Put32(data[(index) * 4:], uint32(atom))
|
||||
}
|
||||
atomAtom, err := xprop.Atm(claim.window.backend.connection, "ATOM")
|
||||
if err != nil { die(); return }
|
||||
claim.window.fulfillSelectionRequest(data, 32, atomAtom, event)
|
||||
|
||||
default:
|
||||
mime, confidence := targetToMime(targetName)
|
||||
if confidence == confidenceNone { die(); return }
|
||||
reader, ok := claim.data[mime]
|
||||
if !ok { die(); return }
|
||||
reader.Seek(0, io.SeekStart)
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil { die() }
|
||||
claim.window.fulfillSelectionRequest(data, 8, event.Target, event)
|
||||
}
|
||||
}
|
||||
191
plugins/x/x/system.go
Normal file
191
plugins/x/x/system.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package x
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||
|
||||
type entitySet map[*entity] struct { }
|
||||
|
||||
func (set entitySet) Empty () bool {
|
||||
return len(set) == 0
|
||||
}
|
||||
|
||||
func (set entitySet) Has (entity *entity) bool {
|
||||
_, ok := set[entity]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (set entitySet) Add (entity *entity) {
|
||||
set[entity] = struct { } { }
|
||||
}
|
||||
|
||||
type system struct {
|
||||
child *entity
|
||||
focused *entity
|
||||
canvas canvas.BasicCanvas
|
||||
|
||||
theme theme.Wrapped
|
||||
config config.Wrapped
|
||||
|
||||
invalidateIgnore bool
|
||||
drawingInvalid entitySet
|
||||
anyLayoutInvalid bool
|
||||
|
||||
drags [10]*entity
|
||||
|
||||
pushFunc func (image.Rectangle)
|
||||
}
|
||||
|
||||
func (system *system) initialize () {
|
||||
system.drawingInvalid = make(entitySet)
|
||||
}
|
||||
|
||||
func (system *system) SetTheme (theme tomo.Theme) {
|
||||
system.theme.Theme = theme
|
||||
system.propagate (func (entity *entity) bool {
|
||||
if child, ok := system.child.element.(tomo.Themeable); ok {
|
||||
child.SetTheme(theme)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (system *system) SetConfig (config tomo.Config) {
|
||||
system.config.Config = config
|
||||
system.propagate (func (entity *entity) bool {
|
||||
if child, ok := system.child.element.(tomo.Configurable); ok {
|
||||
child.SetConfig(config)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (system *system) focus (entity *entity) {
|
||||
previous := system.focused
|
||||
system.focused = entity
|
||||
if previous != nil {
|
||||
previous.element.(tomo.Focusable).HandleFocusChange()
|
||||
}
|
||||
if entity != nil {
|
||||
entity.element.(tomo.Focusable).HandleFocusChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (system *system) focusNext () {
|
||||
found := system.focused == nil
|
||||
focused := false
|
||||
system.propagateAlt (func (entity *entity) bool {
|
||||
if found {
|
||||
// looking for the next element to select
|
||||
child, ok := entity.element.(tomo.Focusable)
|
||||
if ok && child.Enabled() {
|
||||
// found it
|
||||
entity.Focus()
|
||||
focused = true
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// looking for the current focused element
|
||||
if entity == system.focused {
|
||||
// found it
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if !focused { system.focus(nil) }
|
||||
}
|
||||
|
||||
func (system *system) focusPrevious () {
|
||||
var behind *entity
|
||||
system.propagate (func (entity *entity) bool {
|
||||
if entity == system.focused {
|
||||
return false
|
||||
}
|
||||
|
||||
child, ok := entity.element.(tomo.Focusable)
|
||||
if ok && child.Enabled() { behind = entity }
|
||||
return true
|
||||
})
|
||||
system.focus(behind)
|
||||
}
|
||||
|
||||
func (system *system) propagate (callback func (*entity) bool) {
|
||||
if system.child == nil { return }
|
||||
system.child.propagate(callback)
|
||||
}
|
||||
|
||||
func (system *system) propagateAlt (callback func (*entity) bool) {
|
||||
if system.child == nil { return }
|
||||
system.child.propagateAlt(callback)
|
||||
}
|
||||
|
||||
func (system *system) childAt (point image.Point) *entity {
|
||||
if system.child == nil { return nil }
|
||||
return system.child.childAt(point)
|
||||
}
|
||||
|
||||
func (system *system) scrollTargetChildAt (point image.Point) *entity {
|
||||
if system.child == nil { return nil }
|
||||
return system.child.scrollTargetChildAt(point)
|
||||
}
|
||||
|
||||
func (system *system) resizeChildToFit () {
|
||||
system.child.bounds = system.canvas.Bounds()
|
||||
system.child.clippedBounds = system.child.bounds
|
||||
system.child.Invalidate()
|
||||
if system.child.isContainer {
|
||||
system.child.InvalidateLayout()
|
||||
}
|
||||
}
|
||||
|
||||
func (system *system) afterEvent () {
|
||||
if system.anyLayoutInvalid {
|
||||
system.layout(system.child, false)
|
||||
system.anyLayoutInvalid = false
|
||||
}
|
||||
system.draw()
|
||||
}
|
||||
|
||||
func (system *system) layout (entity *entity, force bool) {
|
||||
if entity == nil { return }
|
||||
if entity.layoutInvalid == true || force {
|
||||
if element, ok := entity.element.(tomo.Layoutable); ok {
|
||||
element.Layout()
|
||||
entity.layoutInvalid = false
|
||||
force = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, child := range entity.children {
|
||||
system.layout(child, force)
|
||||
}
|
||||
}
|
||||
|
||||
func (system *system) draw () {
|
||||
finalBounds := image.Rectangle { }
|
||||
|
||||
// ignore invalidations that result from drawing elements, because if an
|
||||
// element decides to do that it really needs to rethink its life
|
||||
// choices.
|
||||
system.invalidateIgnore = true
|
||||
defer func () { system.invalidateIgnore = false } ()
|
||||
|
||||
for entity := range system.drawingInvalid {
|
||||
if entity.clippedBounds.Empty() { continue }
|
||||
entity.element.Draw (canvas.Cut (
|
||||
system.canvas,
|
||||
entity.clippedBounds))
|
||||
finalBounds = finalBounds.Union(entity.clippedBounds)
|
||||
}
|
||||
system.drawingInvalid = make(entitySet)
|
||||
|
||||
// TODO: don't just union all the bounds together, we can definetly
|
||||
// consolidateupdated regions more efficiently than this.
|
||||
if !finalBounds.Empty() {
|
||||
system.pushFunc(finalBounds)
|
||||
}
|
||||
}
|
||||
473
plugins/x/x/window.go
Normal file
473
plugins/x/x/window.go
Normal file
@@ -0,0 +1,473 @@
|
||||
package x
|
||||
|
||||
import "image"
|
||||
import "errors"
|
||||
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"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
|
||||
type mainWindow struct { *window }
|
||||
type menuWindow struct { *window }
|
||||
type window struct {
|
||||
system
|
||||
|
||||
backend *Backend
|
||||
xWindow *xwindow.Window
|
||||
xCanvas *xgraphics.Image
|
||||
|
||||
title, application string
|
||||
|
||||
modalParent *window
|
||||
hasModal bool
|
||||
shy bool
|
||||
|
||||
selectionRequest *selectionRequest
|
||||
selectionClaim *selectionClaim
|
||||
|
||||
metrics struct {
|
||||
bounds image.Rectangle
|
||||
}
|
||||
|
||||
onClose func ()
|
||||
}
|
||||
|
||||
func (backend *Backend) NewWindow (
|
||||
bounds image.Rectangle,
|
||||
) (
|
||||
output tomo.MainWindow,
|
||||
err error,
|
||||
) {
|
||||
if backend == nil { panic("nil backend") }
|
||||
window, err := backend.newWindow(bounds, false)
|
||||
|
||||
output = mainWindow { 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.system.initialize()
|
||||
window.system.pushFunc = window.pasteAndPush
|
||||
window.theme.Case = tomo.C("tomo", "window")
|
||||
|
||||
window.xWindow, err = xwindow.Generate(backend.connection)
|
||||
if err != nil { return }
|
||||
|
||||
if override {
|
||||
err = window.xWindow.CreateChecked (
|
||||
backend.connection.RootWin(),
|
||||
bounds.Min.X, bounds.Min.Y, bounds.Dx(), bounds.Dy(),
|
||||
xproto.CwOverrideRedirect, 1)
|
||||
} else {
|
||||
err = window.xWindow.CreateChecked (
|
||||
backend.connection.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.connection, window.xWindow.Id)
|
||||
xevent.ConfigureNotifyFun(window.handleConfigureNotify).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.KeyPressFun(window.handleKeyPress).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.KeyReleaseFun(window.handleKeyRelease).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.ButtonPressFun(window.handleButtonPress).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.ButtonReleaseFun(window.handleButtonRelease).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.MotionNotifyFun(window.handleMotionNotify).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.SelectionNotifyFun(window.handleSelectionNotify).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.PropertyNotifyFun(window.handlePropertyNotify).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.SelectionClearFun(window.handleSelectionClear).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
xevent.SelectionRequestFun(window.handleSelectionRequest).
|
||||
Connect(backend.connection, window.xWindow.Id)
|
||||
|
||||
window.SetTheme(backend.theme)
|
||||
window.SetConfig(backend.config)
|
||||
|
||||
window.metrics.bounds = bounds
|
||||
window.setMinimumSize(8, 8)
|
||||
|
||||
window.reallocateCanvas()
|
||||
|
||||
backend.windows[window.xWindow.Id] = window
|
||||
|
||||
output = window
|
||||
return
|
||||
}
|
||||
|
||||
func (window *window) Window () tomo.Window {
|
||||
return window
|
||||
}
|
||||
|
||||
func (window *window) Adopt (child tomo.Element) {
|
||||
// disown previous child
|
||||
if window.child != nil {
|
||||
window.child.unlink()
|
||||
window.child = nil
|
||||
}
|
||||
|
||||
// adopt new child
|
||||
if child != nil {
|
||||
childEntity, ok := child.Entity().(*entity)
|
||||
if ok && childEntity != nil {
|
||||
window.child = childEntity
|
||||
childEntity.setWindow(window)
|
||||
window.setMinimumSize (
|
||||
childEntity.minWidth,
|
||||
childEntity.minHeight)
|
||||
window.resizeChildToFit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) SetTitle (title string) {
|
||||
window.title = title
|
||||
ewmh.WmNameSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
title)
|
||||
icccm.WmNameSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
title)
|
||||
icccm.WmIconNameSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
title)
|
||||
}
|
||||
|
||||
func (window *window) SetApplicationName (name string) {
|
||||
window.application = name
|
||||
icccm.WmClassSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
&icccm.WmClass {
|
||||
Instance: name,
|
||||
Class: name,
|
||||
})
|
||||
}
|
||||
|
||||
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.connection,
|
||||
window.xWindow.Id,
|
||||
wmIcons)
|
||||
}
|
||||
|
||||
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.connection,
|
||||
modal.xWindow.Id,
|
||||
window.xWindow.Id)
|
||||
ewmh.WmStateSet (
|
||||
window.backend.connection,
|
||||
modal.xWindow.Id,
|
||||
[]string { "_NET_WM_STATE_MODAL" })
|
||||
modal.modalParent = window
|
||||
window.hasModal = true
|
||||
modal.inheritProperties(window)
|
||||
return modal, err
|
||||
}
|
||||
|
||||
func (window *window) NewMenu (bounds image.Rectangle) (tomo.MenuWindow, error) {
|
||||
menu, err := window.backend.newWindow (
|
||||
bounds.Add(window.metrics.bounds.Min), true)
|
||||
menu.shy = true
|
||||
icccm.WmTransientForSet (
|
||||
window.backend.connection,
|
||||
menu.xWindow.Id,
|
||||
window.xWindow.Id)
|
||||
menu.setType("POPUP_MENU")
|
||||
menu.inheritProperties(window)
|
||||
return menuWindow { window: menu }, err
|
||||
}
|
||||
|
||||
func (window mainWindow) NewPanel (bounds image.Rectangle) (tomo.Window, error) {
|
||||
panel, err := window.backend.newWindow (
|
||||
bounds.Add(window.metrics.bounds.Min), false)
|
||||
if err != nil { return nil, err }
|
||||
panel.setClientLeader(window.window)
|
||||
window.setClientLeader(window.window)
|
||||
icccm.WmTransientForSet (
|
||||
window.backend.connection,
|
||||
panel.xWindow.Id,
|
||||
window.xWindow.Id)
|
||||
panel.setType("UTILITY")
|
||||
panel.inheritProperties(window.window)
|
||||
return panel, err
|
||||
}
|
||||
|
||||
func (window menuWindow) Pin () {
|
||||
// TODO take off override redirect
|
||||
// TODO turn off shy
|
||||
// TODO set window type to MENU
|
||||
// TODO iungrab keyboard and mouse
|
||||
}
|
||||
|
||||
func (window *window) Show () {
|
||||
if window.child == nil {
|
||||
window.xCanvas.For (func (x, y int) xgraphics.BGRA {
|
||||
return xgraphics.BGRA { }
|
||||
})
|
||||
|
||||
window.pushRegion(window.xCanvas.Bounds())
|
||||
}
|
||||
|
||||
window.xWindow.Map()
|
||||
if window.shy { window.grabInput() }
|
||||
}
|
||||
|
||||
func (window *window) Hide () {
|
||||
window.xWindow.Unmap()
|
||||
if window.shy { window.ungrabInput() }
|
||||
}
|
||||
|
||||
func (window *window) Copy (data data.Data) {
|
||||
selectionAtom, err := xprop.Atm(window.backend.connection, clipboardName)
|
||||
if err != nil { return }
|
||||
window.selectionClaim = window.claimSelection(selectionAtom, data)
|
||||
}
|
||||
|
||||
func (window *window) Paste (callback func (data.Data, error), accept ...data.Mime) {
|
||||
// Follow:
|
||||
// https://tronche.com/gui/x/icccm/sec-2.html#s-2.4
|
||||
die := func (err error) { callback(nil, err) }
|
||||
if window.selectionRequest != nil {
|
||||
// TODO: add the request to a queue and take care of it when the
|
||||
// current selection has completed
|
||||
die(errors.New("there is already a selection request"))
|
||||
return
|
||||
}
|
||||
|
||||
propertyName := "TOMO_SELECTION"
|
||||
selectionAtom, err := xprop.Atm(window.backend.connection, clipboardName)
|
||||
if err != nil { die(err); return }
|
||||
propertyAtom, err := xprop.Atm(window.backend.connection, propertyName)
|
||||
if err != nil { die(err); return }
|
||||
|
||||
window.selectionRequest = window.newSelectionRequest (
|
||||
selectionAtom, propertyAtom, callback, accept...)
|
||||
if !window.selectionRequest.open() { window.selectionRequest = nil }
|
||||
return
|
||||
}
|
||||
|
||||
func (window *window) Close () {
|
||||
if window.onClose != nil { window.onClose() }
|
||||
if window.modalParent != nil {
|
||||
// we are a modal dialog, so unlock the parent
|
||||
window.modalParent.hasModal = false
|
||||
}
|
||||
window.Hide()
|
||||
window.Adopt(nil)
|
||||
delete(window.backend.windows, window.xWindow.Id)
|
||||
window.xWindow.Destroy()
|
||||
}
|
||||
|
||||
func (window *window) OnClose (callback func ()) {
|
||||
window.onClose = callback
|
||||
}
|
||||
|
||||
func (window *window) grabInput () {
|
||||
keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id)
|
||||
mousebind.GrabPointer (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
window.backend.connection.RootWin(), 0)
|
||||
}
|
||||
|
||||
func (window *window) ungrabInput () {
|
||||
keybind.UngrabKeyboard(window.backend.connection)
|
||||
mousebind.UngrabPointer(window.backend.connection)
|
||||
}
|
||||
|
||||
func (window *window) inheritProperties (parent *window) {
|
||||
window.SetApplicationName(parent.application)
|
||||
}
|
||||
|
||||
func (window *window) setType (ty string) error {
|
||||
return ewmh.WmWindowTypeSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
[]string { "_NET_WM_WINDOW_TYPE_" + ty })
|
||||
}
|
||||
|
||||
func (window *window) setClientLeader (leader *window) error {
|
||||
hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id)
|
||||
if hints == nil {
|
||||
hints = &icccm.Hints { }
|
||||
}
|
||||
hints.Flags |= icccm.HintWindowGroup
|
||||
hints.WindowGroup = leader.xWindow.Id
|
||||
return icccm.WmHintsSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
hints)
|
||||
}
|
||||
|
||||
func (window *window) reallocateCanvas () {
|
||||
window.canvas.Reallocate (
|
||||
window.metrics.bounds.Dx(),
|
||||
window.metrics.bounds.Dy())
|
||||
|
||||
previousWidth, previousHeight := 0, 0
|
||||
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 = xgraphics.New (
|
||||
window.backend.connection,
|
||||
image.Rect (
|
||||
0, 0,
|
||||
(newWidth / allocStep + 1) * allocStep,
|
||||
(newHeight / allocStep + 1) * allocStep))
|
||||
window.xCanvas.CreatePixmap()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (window *window) pasteAndPush (region image.Rectangle) {
|
||||
window.paste(region)
|
||||
window.pushRegion(region)
|
||||
}
|
||||
|
||||
func (window *window) paste (region image.Rectangle) {
|
||||
canvas := canvas.Cut(window.canvas, region)
|
||||
data, stride := canvas.Buffer()
|
||||
bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds())
|
||||
|
||||
dstStride := window.xCanvas.Stride
|
||||
dstData := window.xCanvas.Pix
|
||||
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||
srcYComponent := y * stride
|
||||
dstYComponent := y * dstStride
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
|
||||
rgba := data[srcYComponent + x]
|
||||
index := dstYComponent + x * 4
|
||||
dstData[index + 0] = rgba.B
|
||||
dstData[index + 1] = rgba.G
|
||||
dstData[index + 2] = rgba.R
|
||||
dstData[index + 3] = rgba.A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) pushRegion (region image.Rectangle) {
|
||||
if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") }
|
||||
image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image)
|
||||
if ok {
|
||||
image.XDraw()
|
||||
image.XExpPaint (
|
||||
window.xWindow.Id,
|
||||
image.Bounds().Min.X,
|
||||
image.Bounds().Min.Y)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *window) setMinimumSize (width, height int) {
|
||||
if width < 8 { width = 8 }
|
||||
if height < 8 { height = 8 }
|
||||
icccm.WmNormalHintsSet (
|
||||
window.backend.connection,
|
||||
window.xWindow.Id,
|
||||
&icccm.NormalHints {
|
||||
Flags: icccm.SizeHintPMinSize,
|
||||
MinWidth: uint(width),
|
||||
MinHeight: uint(height),
|
||||
})
|
||||
newWidth := window.metrics.bounds.Dx()
|
||||
newHeight := window.metrics.bounds.Dy()
|
||||
if newWidth < width { newWidth = width }
|
||||
if newHeight < height { newHeight = height }
|
||||
if newWidth != window.metrics.bounds.Dx() ||
|
||||
newHeight != window.metrics.bounds.Dy() {
|
||||
window.xWindow.Resize(newWidth, newHeight)
|
||||
}
|
||||
}
|
||||
123
plugins/x/x/x.go
Normal file
123
plugins/x/x/x.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package x
|
||||
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
|
||||
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"
|
||||
|
||||
// Backend is an instance of an X backend.
|
||||
type Backend struct {
|
||||
connection *xgbutil.XUtil
|
||||
|
||||
doChannel chan(func ())
|
||||
|
||||
modifierMasks struct {
|
||||
capsLock uint16
|
||||
shiftLock uint16
|
||||
numLock uint16
|
||||
modeSwitch uint16
|
||||
|
||||
alt uint16
|
||||
meta uint16
|
||||
super uint16
|
||||
hyper uint16
|
||||
}
|
||||
|
||||
theme tomo.Theme
|
||||
config tomo.Config
|
||||
|
||||
windows map[xproto.Window] *window
|
||||
|
||||
open bool
|
||||
}
|
||||
|
||||
// NewBackend instantiates an X backend.
|
||||
func NewBackend () (output tomo.Backend, err error) {
|
||||
backend := &Backend {
|
||||
windows: map[xproto.Window] *window { },
|
||||
doChannel: make(chan func (), 32),
|
||||
open: true,
|
||||
}
|
||||
|
||||
// connect to X
|
||||
backend.connection, err = xgbutil.NewConn()
|
||||
if err != nil { return }
|
||||
backend.initializeKeymapInformation()
|
||||
|
||||
keybind.Initialize(backend.connection)
|
||||
mousebind.Initialize(backend.connection)
|
||||
|
||||
output = backend
|
||||
return
|
||||
}
|
||||
|
||||
// Run runs the backend's event loop. This method will not exit until Stop() is
|
||||
// called, or the backend experiences a fatal error.
|
||||
func (backend *Backend) Run () (err error) {
|
||||
backend.assert()
|
||||
pingBefore,
|
||||
pingAfter,
|
||||
pingQuit := xevent.MainPing(backend.connection)
|
||||
for {
|
||||
select {
|
||||
case <- pingBefore:
|
||||
<- pingAfter
|
||||
case callback := <- backend.doChannel:
|
||||
callback()
|
||||
case <- pingQuit:
|
||||
return
|
||||
}
|
||||
for _, window := range backend.windows {
|
||||
window.system.afterEvent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully closes the connection and stops the event loop.
|
||||
func (backend *Backend) Stop () {
|
||||
backend.assert()
|
||||
if !backend.open { return }
|
||||
backend.open = false
|
||||
|
||||
toClose := []*window { }
|
||||
for _, window := range backend.windows {
|
||||
toClose = append(toClose, window)
|
||||
}
|
||||
for _, window := range toClose {
|
||||
window.Close()
|
||||
}
|
||||
xevent.Quit(backend.connection)
|
||||
backend.connection.Conn().Close()
|
||||
}
|
||||
|
||||
// Do executes the specified callback within the main thread as soon as
|
||||
// possible. This function can be safely called from other threads.
|
||||
func (backend *Backend) Do (callback func ()) {
|
||||
backend.assert()
|
||||
backend.doChannel <- callback
|
||||
}
|
||||
|
||||
// SetTheme sets the theme of all open windows.
|
||||
func (backend *Backend) SetTheme (theme tomo.Theme) {
|
||||
backend.assert()
|
||||
backend.theme = theme
|
||||
for _, window := range backend.windows {
|
||||
window.SetTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfig sets the configuration of all open windows.
|
||||
func (backend *Backend) SetConfig (config tomo.Config) {
|
||||
backend.assert()
|
||||
backend.config = config
|
||||
for _, window := range backend.windows {
|
||||
window.SetConfig(config)
|
||||
}
|
||||
}
|
||||
|
||||
func (backend *Backend) assert () {
|
||||
if backend == nil { panic("nil backend") }
|
||||
}
|
||||
Reference in New Issue
Block a user