Initial commit

This commit is contained in:
Sasha Koshka 2023-01-09 01:03:19 -05:00
commit 00d75d4488
27 changed files with 3036 additions and 0 deletions

2
artist/artist.go Normal file
View File

@ -0,0 +1,2 @@
package artist

127
artist/chisel.go Normal file
View File

@ -0,0 +1,127 @@
package artist
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
// ShadingProfile contains shading information that can be used to draw chiseled
// objects.
type ShadingProfile struct {
Highlight tomo.Image
Shadow tomo.Image
Stroke tomo.Image
Fill tomo.Image
StrokeWeight int
ShadingWeight int
}
// Engraved reverses the shadown and highlight colors of the ShadingProfile to
// produce a new ShadingProfile with an engraved appearance.
func (profile ShadingProfile) Engraved () (reversed ShadingProfile) {
reversed = profile
reversed.Highlight = profile.Shadow
reversed.Shadow = profile.Highlight
return
}
// ChiseledRectangle draws a rectangle with a chiseled/embossed appearance,
// according to the ShadingProfile passed to it.
func ChiseledRectangle (
destination tomo.Canvas,
profile ShadingProfile,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
// FIXME: this breaks when the bounds are smaller than the border or
// shading weight
stroke := profile.Stroke
highlight := profile.Highlight
shadow := profile.Shadow
fill := profile.Fill
strokeWeight := profile.StrokeWeight
shadingWeight := profile.ShadingWeight
bounds = bounds.Canon()
updatedRegion = bounds
strokeWeightVector := image.Point { strokeWeight, strokeWeight }
shadingWeightVector := image.Point { shadingWeight, shadingWeight }
shadingBounds := bounds
shadingBounds.Min = shadingBounds.Min.Add(strokeWeightVector)
shadingBounds.Max = shadingBounds.Max.Sub(strokeWeightVector)
shadingBounds = shadingBounds.Canon()
fillBounds := shadingBounds
fillBounds.Min = fillBounds.Min.Add(shadingWeightVector)
fillBounds.Max = fillBounds.Max.Sub(shadingWeightVector)
fillBounds = fillBounds.Canon()
strokeImageMin := stroke.Bounds().Min
highlightImageMin := highlight.Bounds().Min
shadowImageMin := shadow.Bounds().Min
fillImageMin := fill.Bounds().Min
width := float64(bounds.Dx())
height := float64(bounds.Dy())
yy := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
xx := 0
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
var pixel color.RGBA
point := image.Point { x, y }
switch {
case point.In(fillBounds):
pixel = fill.RGBAAt (
xx - strokeWeight - shadingWeight +
fillImageMin.X,
yy - strokeWeight - shadingWeight +
fillImageMin.Y)
case point.In(shadingBounds):
var highlighted bool
// FIXME: this doesn't work quite right, the
// slope of the line is somewhat off.
bottomCorner :=
float64(xx) < float64(yy) *
(width / height)
if bottomCorner {
highlighted =
float64(xx) <
height - float64(yy)
} else {
highlighted =
width - float64(xx) >
float64(yy)
}
if highlighted {
pixel = highlight.RGBAAt (
xx - strokeWeight +
highlightImageMin.X,
yy - strokeWeight +
highlightImageMin.Y)
} else {
pixel = shadow.RGBAAt (
xx - strokeWeight +
shadowImageMin.X,
yy - strokeWeight +
shadowImageMin.Y)
}
default:
pixel = stroke.RGBAAt (
xx + strokeImageMin.X,
yy + strokeImageMin.Y)
}
destination.SetRGBA(x, y, pixel)
xx ++
}
yy ++
}
return
}

106
artist/line.go Normal file
View File

@ -0,0 +1,106 @@
package artist
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
func Line (
destination tomo.Canvas,
source tomo.Image,
weight int,
min image.Point,
max image.Point,
) (
updatedRegion image.Rectangle,
) {
// TODO: respect weight
updatedRegion = image.Rectangle { Min: min, Max: max }.Canon()
updatedRegion.Max.X ++
updatedRegion.Max.Y ++
if abs(max.Y - min.Y) <
abs(max.X - min.X) {
if max.X < min.X {
temp := min
min = max
max = temp
}
lineLow(destination, source, weight, min, max)
} else {
if max.Y < min.Y {
temp := min
min = max
max = temp
}
lineHigh(destination, source, weight, min, max)
}
return
}
func lineLow (
destination tomo.Canvas,
source tomo.Image,
weight int,
min image.Point,
max image.Point,
) {
deltaX := max.X - min.X
deltaY := max.Y - min.Y
yi := 1
if deltaY < 0 {
yi = -1
deltaY *= -1
}
D := (2 * deltaY) - deltaX
y := min.Y
for x := min.X; x < max.X; x ++ {
destination.SetRGBA(x, y, source.RGBAAt(x, y))
if D > 0 {
y += yi
D += 2 * (deltaY - deltaX)
} else {
D += 2 * deltaY
}
}
}
func lineHigh (
destination tomo.Canvas,
source tomo.Image,
weight int,
min image.Point,
max image.Point,
) {
deltaX := max.X - min.X
deltaY := max.Y - min.Y
xi := 1
if deltaX < 0 {
xi = -1
deltaX *= -1
}
D := (2 * deltaX) - deltaY
x := min.X
for y := min.Y; y < max.Y; y ++ {
destination.SetRGBA(x, y, source.RGBAAt(x, y))
if D > 0 {
x += xi
D += 2 * (deltaX - deltaY)
} else {
D += 2 * deltaX
}
}
}
func abs (in int) (out int) {
if in < 0 { in *= -1}
out = in
return
}

105
artist/rectangle.go Normal file
View File

@ -0,0 +1,105 @@
package artist
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
// Paste transfers one image onto another, offset by the specified point.
func Paste (
destination tomo.Canvas,
source tomo.Image,
offset image.Point,
) (
updatedRegion image.Rectangle,
) {
sourceBounds := source.Bounds().Canon()
updatedRegion = sourceBounds.Add(offset)
for y := sourceBounds.Min.Y; y < sourceBounds.Max.Y; y ++ {
for x := sourceBounds.Min.X; x < sourceBounds.Max.X; x ++ {
destination.SetRGBA (
x + offset.X, y + offset.Y,
source.RGBAAt(x, y))
}}
return
}
// Rectangle draws a rectangle with an inset border. If the border image is nil,
// no border will be drawn. Likewise, if the fill image is nil, the rectangle
// will have no fill.
func Rectangle (
destination tomo.Canvas,
fill tomo.Image,
stroke tomo.Image,
weight int,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
bounds = bounds.Canon()
updatedRegion = bounds
fillBounds := bounds
fillBounds.Min = fillBounds.Min.Add(image.Point { weight, weight })
fillBounds.Max = fillBounds.Max.Sub(image.Point { weight, weight })
fillBounds = fillBounds.Canon()
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
var pixel color.RGBA
if (image.Point { x, y }).In(fillBounds) {
pixel = fill.RGBAAt(x, y)
} else {
pixel = stroke.RGBAAt(x, y)
}
destination.SetRGBA(x, y, pixel)
}}
return
}
// OffsetRectangle is the same as Rectangle, but offsets the border image to the
// top left corner of the border and the fill image to the top left corner of
// the fill.
func OffsetRectangle (
destination tomo.Canvas,
fill tomo.Image,
stroke tomo.Image,
weight int,
bounds image.Rectangle,
) (
updatedRegion image.Rectangle,
) {
bounds = bounds.Canon()
updatedRegion = bounds
fillBounds := bounds
fillBounds.Min = fillBounds.Min.Add(image.Point { weight, weight })
fillBounds.Max = fillBounds.Max.Sub(image.Point { weight, weight })
fillBounds = fillBounds.Canon()
strokeImageMin := stroke.Bounds().Min
fillImageMin := fill.Bounds().Min
yy := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
xx := 0
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
var pixel color.RGBA
if (image.Point { x, y }).In(fillBounds) {
pixel = fill.RGBAAt (
xx - weight + fillImageMin.X,
yy - weight + fillImageMin.Y)
} else {
pixel = stroke.RGBAAt (
xx + strokeImageMin.X,
yy + strokeImageMin.Y)
}
destination.SetRGBA(x, y, pixel)
xx ++
}
yy ++
}
return
}

265
artist/text.go Normal file
View File

@ -0,0 +1,265 @@
package artist
// import "fmt"
import "image"
import "unicode"
import "image/draw"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/sashakoshka/tomo"
type characterLayout struct {
x int
character rune
}
type wordLayout struct {
position image.Point
width int
text []characterLayout
}
// Align specifies a text alignment method.
type Align int
const (
// AlignLeft aligns the start of each line to the beginning point
// of each dot.
AlignLeft Align = iota
AlignRight
AlignCenter
AlignJustify
)
// TextDrawer is a struct that is capable of efficient rendering of wrapped
// text, and calculating text bounds. It avoids doing redundant work
// automatically.
type TextDrawer struct {
text string
runes []rune
face font.Face
width int
height int
align Align
wrap bool
cut bool
layout []wordLayout
layoutClean bool
layoutBounds image.Rectangle
}
// SetText sets the text of the text drawer.
func (drawer *TextDrawer) SetText (text string) {
if drawer.text == text { return }
drawer.text = text
drawer.runes = []rune(text)
drawer.layoutClean = false
}
// SetFace sets the font face of the text drawer.
func (drawer *TextDrawer) SetFace (face font.Face) {
if drawer.face == face { return }
drawer.face = face
drawer.layoutClean = false
}
// SetMaxWidth sets a maximum width for the text drawer, and recalculates the
// layout if needed. If zero is given, there will be no width limit and the text
// will not wrap.
func (drawer *TextDrawer) SetMaxWidth (width int) {
if drawer.width == width { return }
drawer.width = width
drawer.wrap = width != 0
drawer.layoutClean = false
}
// SetMaxHeight sets a maximum height for the text drawer. Lines that are
// entirely below this height will not be drawn, and lines that are on the cusp
// of this maximum height will be clipped at the point that they cross it.
func (drawer *TextDrawer) SetMaxHeight (height int) {
if drawer.height == height { return }
drawer.height = height
drawer.cut = height != 0
drawer.layoutClean = false
}
// SetAlignment specifies how the drawer should align its text. For this to have
// an effect, a maximum width must have been set.
func (drawer *TextDrawer) SetAlignment (align Align) {
if drawer.align == align { return }
drawer.align = align
drawer.layoutClean = false
}
// Draw draws the drawer's text onto the specified canvas at the given offset.
func (drawer *TextDrawer) Draw (
destination tomo.Canvas,
source tomo.Image,
offset image.Point,
) (
updatedRegion image.Rectangle,
) {
if !drawer.layoutClean { drawer.recalculate() }
for _, word := range drawer.layout {
for _, character := range word.text {
destinationRectangle,
mask, maskPoint, _, ok := drawer.face.Glyph (
fixed.P (
offset.X + word.position.X + character.x,
offset.Y + word.position.Y),
character.character)
if !ok { continue }
// FIXME: clip destination rectangle if we are on the cusp of
// the maximum height.
draw.DrawMask (
destination,
destinationRectangle,
source, image.Point { },
mask, maskPoint,
draw.Over)
updatedRegion = updatedRegion.Union(destinationRectangle)
}}
return
}
// LayoutBounds returns a semantic bounding box for text to be used to determine
// an offset for drawing. If a maximum width or height has been set, those will
// be used as the width and height of the bounds respectively. The origin point
// (0, 0) of the returned bounds will be equivalent to the baseline at the start
// of the first line. As such, the minimum of the bounds will be negative.
func (drawer *TextDrawer) LayoutBounds () (bounds image.Rectangle) {
if !drawer.layoutClean { drawer.recalculate() }
bounds = drawer.layoutBounds
return
}
func (drawer *TextDrawer) recalculate () {
drawer.layoutClean = true
drawer.layout = nil
drawer.layoutBounds = image.Rectangle { }
if drawer.runes == nil { return }
if drawer.face == nil { return }
metrics := drawer.face.Metrics()
dot := fixed.Point26_6 { 0, 0 }
index := 0
horizontalExtent := 0
previousCharacter := rune(-1)
for index < len(drawer.runes) {
word := wordLayout { }
word.position.X = dot.X.Round()
word.position.Y = dot.Y.Round()
// process a word
currentCharacterX := fixed.Int26_6(0)
wordWidth := fixed.Int26_6(0)
for index < len(drawer.runes) && !unicode.IsSpace(drawer.runes[index]) {
character := drawer.runes[index]
_, advance, ok := drawer.face.GlyphBounds(character)
index ++
if !ok { continue }
word.text = append(word.text, characterLayout {
x: currentCharacterX.Round(),
character: character,
})
dot.X += advance
wordWidth += advance
currentCharacterX += advance
if dot.X.Round () > horizontalExtent {
horizontalExtent = dot.X.Round()
}
if previousCharacter >= 0 {
dot.X += drawer.face.Kern (
previousCharacter,
character)
}
previousCharacter = character
}
word.width = wordWidth.Round()
// detect if the word that was just processed goes out of
// bounds, and if it does, wrap it
if drawer.wrap &&
word.width + word.position.X > drawer.width &&
word.position.X > 0 {
word.position.Y += metrics.Height.Round()
word.position.X = 0
dot.Y += metrics.Height
dot.X = wordWidth
}
// add the word to the layout
drawer.layout = append(drawer.layout, word)
// skip over whitespace, going onto a new line if there is a
// newline character
for index < len(drawer.runes) && unicode.IsSpace(drawer.runes[index]) {
character := drawer.runes[index]
if character == '\n' {
dot.Y += metrics.Height
dot.X = 0
previousCharacter = character
index ++
} else {
_, advance, ok := drawer.face.GlyphBounds(character)
index ++
if !ok { continue }
dot.X += advance
if previousCharacter >= 0 {
dot.X += drawer.face.Kern (
previousCharacter,
character)
}
previousCharacter = character
}
}
// if there is a set maximum height, and we have crossed it,
// stop processing more words. and remove any words that have
// also crossed the line.
if
drawer.cut &&
(dot.Y - metrics.Ascent - metrics.Descent).Round() >
drawer.height {
for
index := len(drawer.layout) - 1;
index >= 0; index -- {
if drawer.layout[index].position.Y < dot.Y.Round() {
break
}
drawer.layout = drawer.layout[:index]
}
break
}
}
if drawer.wrap {
drawer.layoutBounds.Max.X = drawer.width
} else {
drawer.layoutBounds.Max.X = horizontalExtent
}
if drawer.cut {
drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round()
drawer.layoutBounds.Max.Y = drawer.height - metrics.Ascent.Round()
} else {
drawer.layoutBounds.Min.Y = 0 - metrics.Ascent.Round()
drawer.layoutBounds.Max.Y = dot.Y.Round() + metrics.Descent.Round()
}
// TODO:
// for each line, calculate the bounds as if the words are left aligned,
// and then at the end of the process go through each line and re-align
// everything. this will make the process far simpler.
}

71
artist/uniform.go Normal file
View File

@ -0,0 +1,71 @@
package artist
import "image"
import "image/color"
// Uniform is an infinite-sized Image of uniform color. It implements the
// color.Color, color.Model, and tomo.Image interfaces.
type Uniform struct {
C color.RGBA
}
// NewUniform returns a new Uniform image of the given color.
func NewUniform (c color.Color) (uniform *Uniform) {
uniform = &Uniform { }
r, g, b, a := c.RGBA()
uniform.C.R = uint8(r >> 8)
uniform.C.G = uint8(g >> 8)
uniform.C.B = uint8(b >> 8)
uniform.C.A = uint8(a >> 8)
return
}
func (uniform *Uniform) RGBA () (r, g, b, a uint32) {
r = uint32(uniform.C.R) << 8 | uint32(uniform.C.R)
g = uint32(uniform.C.G) << 8 | uint32(uniform.C.G)
b = uint32(uniform.C.B) << 8 | uint32(uniform.C.B)
a = uint32(uniform.C.A) << 8 | uint32(uniform.C.A)
return
}
func (uniform *Uniform) ColorModel () (model color.Model) {
model = uniform
return
}
func (uniform *Uniform) Convert (in color.Color) (out color.Color) {
out = uniform.C
return
}
func (uniform *Uniform) Bounds () (rectangle image.Rectangle) {
rectangle.Min = image.Point { -1e9, -1e9 }
rectangle.Max = image.Point { 1e9, 1e9 }
return
}
func (uniform *Uniform) At (x, y int) (c color.Color) {
c = uniform.C
return
}
func (uniform *Uniform) RGBAAt (x, y int) (c color.RGBA) {
c = uniform.C
return
}
func (uniform *Uniform) RGBA64At (x, y int) (c color.RGBA64) {
r := uint16(uniform.C.R) << 8 | uint16(uniform.C.R)
g := uint16(uniform.C.G) << 8 | uint16(uniform.C.G)
b := uint16(uniform.C.B) << 8 | uint16(uniform.C.B)
a := uint16(uniform.C.A) << 8 | uint16(uniform.C.A)
c = color.RGBA64 { R: r, G: g, B: b, A: a }
return
}
// Opaque scans the entire image and reports whether it is fully opaque.
func (uniform *Uniform) Opaque () (opaque bool) {
opaque = uniform.C.A == 0xFF
return
}

99
artist/wrap.go Normal file
View File

@ -0,0 +1,99 @@
package artist
import "git.tebibyte.media/sashakoshka/tomo"
import "image"
import "image/draw"
import "image/color"
// WrappedImage wraps an image.Image and allows it to satisfy tomo.Image.
type WrappedImage struct { Underlying image.Image }
// WrapImage wraps a generic image.Image and allows it to satisfy tomo.Image.
// Do not use this function to wrap images that already satisfy tomo.Image,
// because the resulting wrapped image will be rather slow in comparison.
func WrapImage (underlying image.Image) (wrapped tomo.Image) {
wrapped = WrappedImage { Underlying: underlying }
return
}
func (wrapped WrappedImage) Bounds () (bounds image.Rectangle) {
bounds = wrapped.Underlying.Bounds()
return
}
func (wrapped WrappedImage) ColorModel () (model color.Model) {
model = wrapped.Underlying.ColorModel()
return
}
func (wrapped WrappedImage) At (x, y int) (pixel color.Color) {
pixel = wrapped.Underlying.At(x, y)
return
}
func (wrapped WrappedImage) RGBAAt (x, y int) (pixel color.RGBA) {
r, g, b, a := wrapped.Underlying.At(x, y).RGBA()
pixel.R = uint8(r >> 8)
pixel.G = uint8(g >> 8)
pixel.B = uint8(b >> 8)
pixel.A = uint8(a >> 8)
return
}
// WrappedCanvas wraps a draw.Image and allows it to satisfy tomo.Canvas.
type WrappedCanvas struct { Underlying draw.Image }
// WrapCanvas wraps a generic draw.Image and allows it to satisfy tomo.Canvas.
// Do not use this function to wrap images that already satisfy tomo.Canvas,
// because the resulting wrapped image will be rather slow in comparison.
func WrapCanvas (underlying draw.Image) (wrapped tomo.Canvas) {
wrapped = WrappedCanvas { Underlying: underlying }
return
}
func (wrapped WrappedCanvas) Bounds () (bounds image.Rectangle) {
bounds = wrapped.Underlying.Bounds()
return
}
func (wrapped WrappedCanvas) ColorModel () (model color.Model) {
model = wrapped.Underlying.ColorModel()
return
}
func (wrapped WrappedCanvas) At (x, y int) (pixel color.Color) {
pixel = wrapped.Underlying.At(x, y)
return
}
func (wrapped WrappedCanvas) RGBAAt (x, y int) (pixel color.RGBA) {
r, g, b, a := wrapped.Underlying.At(x, y).RGBA()
pixel.R = uint8(r >> 8)
pixel.G = uint8(g >> 8)
pixel.B = uint8(b >> 8)
pixel.A = uint8(a >> 8)
return
}
func (wrapped WrappedCanvas) Set (x, y int, pixel color.Color) {
wrapped.Underlying.Set(x, y, pixel)
}
func (wrapped WrappedCanvas) SetRGBA (x, y int, pixel color.RGBA) {
wrapped.Underlying.Set(x, y, pixel)
}
// ToRGBA clones an existing image.Image into an image.RGBA struct, which
// directly satisfies tomo.Image. This is useful for things like icons and
// textures.
func ToRGBA (input image.Image) (output *image.RGBA) {
bounds := input.Bounds()
output = image.NewRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
output.Set(x, y, input.At(x, y))
}}
return
}

54
backend.go Normal file
View File

@ -0,0 +1,54 @@
package tomo
import "errors"
// Backend represents a connection to a display server, or something similar.
// It is capable of managing an event loop, and creating windows.
type Backend interface {
// Run runs the backend's event loop. It must block until the backend
// experiences a fatal error, or Stop() is called.
Run () (err error)
// Stop stops the backend's event loop.
Stop ()
// Do executes the specified callback within the main thread as soon as
// possible. This method must be safe to call from other threads.
Do (callback func ())
// NewWindow creates a new window with the specified width and height,
// and returns a struct representing it that fulfills the Window
// interface.
NewWindow (width, height int) (window Window, err error)
}
// BackendFactory represents a function capable of constructing a backend
// struct. Any connections should be initialized within this function. If there
// any errors encountered during this process, the function should immediately
// stop, clean up any resources, and return an error.
type BackendFactory func () (backend Backend, err error)
// RegisterBackend registers a backend factory. When an application calls
// tomo.Run(), the first registered backend that does not throw an error will be
// used.
func RegisterBackend (factory BackendFactory) {
factories = append(factories, factory)
}
var factories []BackendFactory
func instantiateBackend () (backend Backend, err error) {
// find a suitable backend
for _, factory := range factories {
backend, err = factory()
if err == nil && backend != nil { return }
}
// if none were found, but there was no error produced, produce an
// error
if err == nil {
err = errors.New("no available backends")
}
return
}

389
backends/x/encoding.go Normal file
View File

@ -0,0 +1,389 @@
package x
import "unicode"
import "github.com/jezek/xgb/xproto"
import "github.com/jezek/xgbutil/keybind"
import "git.tebibyte.media/sashakoshka/tomo"
// 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] tomo.Key {
0xFFFFFF: tomo.KeyNone,
0xFF63: tomo.KeyInsert,
0xFF67: tomo.KeyMenu,
0xFF61: tomo.KeyPrintScreen,
0xFF6B: tomo.KeyPause,
0xFFE5: tomo.KeyCapsLock,
0xFF14: tomo.KeyScrollLock,
0xFF7F: tomo.KeyNumLock,
0xFF08: tomo.KeyBackspace,
0xFF09: tomo.KeyTab,
0xFF0D: tomo.KeyEnter,
0xFF1B: tomo.KeyEscape,
0xFF52: tomo.KeyUp,
0xFF54: tomo.KeyDown,
0xFF51: tomo.KeyLeft,
0xFF53: tomo.KeyRight,
0xFF55: tomo.KeyPageUp,
0xFF56: tomo.KeyPageDown,
0xFF50: tomo.KeyHome,
0xFF57: tomo.KeyEnd,
0xFFE1: tomo.KeyLeftShift,
0xFFE2: tomo.KeyRightShift,
0xFFE3: tomo.KeyLeftControl,
0xFFE4: tomo.KeyRightControl,
0xFFE7: tomo.KeyLeftMeta,
0xFFE8: tomo.KeyRightMeta,
0xFFE9: tomo.KeyLeftAlt,
0xFFEA: tomo.KeyRightAlt,
0xFFEB: tomo.KeyLeftSuper,
0xFFEC: tomo.KeyRightSuper,
0xFFED: tomo.KeyLeftHyper,
0xFFEE: tomo.KeyRightHyper,
0xFFFF: tomo.KeyDelete,
0xFFBE: tomo.KeyF1,
0xFFBF: tomo.KeyF2,
0xFFC0: tomo.KeyF3,
0xFFC1: tomo.KeyF4,
0xFFC2: tomo.KeyF5,
0xFFC3: tomo.KeyF6,
0xFFC4: tomo.KeyF7,
0xFFC5: tomo.KeyF8,
0xFFC6: tomo.KeyF9,
0xFFC7: tomo.KeyF10,
0xFFC8: tomo.KeyF11,
0xFFC9: tomo.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: tomo.KeyDead,
}
var keypadCodeTable = map[xproto.Keysym] tomo.Key {
0xff80: tomo.Key(' '),
0xff89: tomo.KeyTab,
0xff8d: tomo.KeyEnter,
0xff91: tomo.KeyF1,
0xff92: tomo.KeyF2,
0xff93: tomo.KeyF3,
0xff94: tomo.KeyF4,
0xff95: tomo.KeyHome,
0xff96: tomo.KeyLeft,
0xff97: tomo.KeyUp,
0xff98: tomo.KeyRight,
0xff99: tomo.KeyDown,
0xff9a: tomo.KeyPageUp,
0xff9b: tomo.KeyPageDown,
0xff9c: tomo.KeyEnd,
0xff9d: tomo.KeyHome,
0xff9e: tomo.KeyInsert,
0xff9f: tomo.KeyDelete,
0xffbd: tomo.Key('='),
0xffaa: tomo.Key('*'),
0xffab: tomo.Key('+'),
0xffac: tomo.Key(','),
0xffad: tomo.Key('-'),
0xffae: tomo.Key('.'),
0xffaf: tomo.Key('/'),
0xffb0: tomo.Key('0'),
0xffb1: tomo.Key('1'),
0xffb2: tomo.Key('2'),
0xffb3: tomo.Key('3'),
0xffb4: tomo.Key('4'),
0xffb5: tomo.Key('5'),
0xffb6: tomo.Key('6'),
0xffb7: tomo.Key('7'),
0xffb8: tomo.Key('8'),
0xffb9: tomo.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 tomo.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 = tomo.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
}

235
backends/x/event.go Normal file
View File

@ -0,0 +1,235 @@
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"
type scrollSum struct {
x, y int
}
func (sum *scrollSum) add (button xproto.Button) {
switch button {
case 4:
sum.y --
case 5:
sum.y ++
case 6:
sum.x --
case 7:
sum.x ++
}
}
func (window *Window) handleConfigureNotify (
connection *xgbutil.XUtil,
event xevent.ConfigureNotifyEvent,
) {
configureEvent := *event.ConfigureNotifyEvent
newWidth := int(configureEvent.Width)
newHeight := int(configureEvent.Height)
sizeChanged :=
window.metrics.width != newWidth ||
window.metrics.height != newHeight
window.metrics.width = newWidth
window.metrics.height = newHeight
if sizeChanged {
configureEvent = window.compressConfigureNotify(configureEvent)
window.metrics.width = int(configureEvent.Width)
window.metrics.height = int(configureEvent.Height)
window.reallocateCanvas()
window.resizeChildToFit()
}
}
func (window *Window) handleKeyPress (
connection *xgbutil.XUtil,
event xevent.KeyPressEvent,
) {
if window.child == nil { return}
keyEvent := *event.KeyPressEvent
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
modifiers := tomo.Modifiers {
Shift:
(keyEvent.State & xproto.ModMaskShift) > 0 ||
(keyEvent.State & window.backend.modifierMasks.shiftLock) > 0,
Control: (keyEvent.State & xproto.ModMaskControl) > 0,
Alt: (keyEvent.State & window.backend.modifierMasks.alt) > 0,
Meta: (keyEvent.State & window.backend.modifierMasks.meta) > 0,
Super: (keyEvent.State & window.backend.modifierMasks.super) > 0,
Hyper: (keyEvent.State & window.backend.modifierMasks.hyper) > 0,
NumberPad: numberPad,
}
window.child.Handle (tomo.EventKeyDown {
Key: key,
Modifiers: modifiers,
Repeated: false, // FIXME: return correct value here
})
}
func (window *Window) handleKeyRelease (
connection *xgbutil.XUtil,
event xevent.KeyReleaseEvent,
) {
if window.child == nil { return }
keyEvent := *event.KeyReleaseEvent
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
modifiers := tomo.Modifiers {
Shift:
(keyEvent.State & xproto.ModMaskShift) > 0 ||
(keyEvent.State & window.backend.modifierMasks.shiftLock) > 0,
Control: (keyEvent.State & xproto.ModMaskControl) > 0,
Alt: (keyEvent.State & window.backend.modifierMasks.alt) > 0,
Meta: (keyEvent.State & window.backend.modifierMasks.meta) > 0,
Super: (keyEvent.State & window.backend.modifierMasks.super) > 0,
Hyper: (keyEvent.State & window.backend.modifierMasks.hyper) > 0,
NumberPad: numberPad,
}
window.child.Handle (tomo.EventKeyUp {
Key: key,
Modifiers: modifiers,
})
}
func (window *Window) handleButtonPress (
connection *xgbutil.XUtil,
event xevent.ButtonPressEvent,
) {
if window.child == nil { return }
buttonEvent := *event.ButtonPressEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 {
sum := scrollSum { }
sum.add(buttonEvent.Detail)
window.compressScrollSum(buttonEvent, &sum)
window.child.Handle (tomo.EventScroll {
X: int(buttonEvent.EventX),
Y: int(buttonEvent.EventY),
ScrollX: sum.x,
ScrollY: sum.y,
})
} else {
window.child.Handle (tomo.EventMouseDown {
Button: tomo.Button(buttonEvent.Detail),
X: int(buttonEvent.EventX),
Y: int(buttonEvent.EventY),
})
}
}
func (window *Window) handleButtonRelease (
connection *xgbutil.XUtil,
event xevent.ButtonReleaseEvent,
) {
buttonEvent := *event.ButtonReleaseEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
window.child.Handle (tomo.EventMouseUp {
Button: tomo.Button(buttonEvent.Detail),
X: int(buttonEvent.EventX),
Y: int(buttonEvent.EventY),
})
}
func (window *Window) handleMotionNotify (
connection *xgbutil.XUtil,
event xevent.MotionNotifyEvent,
) {
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
window.child.Handle (tomo.EventMouseMove {
X: int(motionEvent.EventX),
Y: int(motionEvent.EventY),
})
}
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)
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 &&
typedEvent.Detail >= 4 &&
typedEvent.Detail <= 7 {
lastEvent = typedEvent
defer func (index int) {
xevent.DequeueAt(window.backend.connection, index)
} (index)
}
}
return
}

328
backends/x/window.go Normal file
View File

@ -0,0 +1,328 @@
package x
import "image"
import "image/color"
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/xgraphics"
import "git.tebibyte.media/sashakoshka/tomo"
type Window struct {
backend *Backend
xWindow *xwindow.Window
xCanvas *xgraphics.Image
child tomo.Element
onClose func ()
drawCallback func (region tomo.Image)
minimumSizeChangeCallback func (width, height int)
skipChildDrawCallback bool
metrics struct {
width int
height int
}
}
func (backend *Backend) NewWindow (
width, height int,
) (
output tomo.Window,
err error,
) {
if backend == nil { panic("nil backend") }
window := &Window { backend: backend }
window.xWindow, err = xwindow.Generate(backend.connection)
if err != nil { return }
window.xWindow.Create (
backend.connection.RootWin(),
0, 0, width, height, 0)
err = window.xWindow.Listen (
xproto.EventMaskStructureNotify,
xproto.EventMaskPointerMotion,
xproto.EventMaskKeyPress,
xproto.EventMaskKeyRelease,
xproto.EventMaskButtonPress,
xproto.EventMaskButtonRelease)
if err != nil { return }
window.xWindow.WMGracefulClose (func (xWindow *xwindow.Window) {
window.Close()
})
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)
window.metrics.width = width
window.metrics.height = height
window.childMinimumSizeChangeCallback(8, 8)
window.reallocateCanvas()
backend.windows[window.xWindow.Id] = window
output = window
return
}
func (window *Window) ColorModel () (model color.Model) {
return color.RGBAModel
}
func (window *Window) At (x, y int) (pixel color.Color) {
pixel = window.xCanvas.At(x, y)
return
}
func (window *Window) RGBAAt (x, y int) (pixel color.RGBA) {
sourcePixel := window.xCanvas.At(x, y).(xgraphics.BGRA)
pixel = color.RGBA {
R: sourcePixel.R,
G: sourcePixel.G,
B: sourcePixel.B,
A: sourcePixel.A,
}
return
}
func (window *Window) Bounds () (bounds image.Rectangle) {
bounds.Max = image.Point {
X: window.metrics.width,
Y: window.metrics.height,
}
return
}
func (window *Window) Handle (event tomo.Event) () {
switch event.(type) {
case tomo.EventResize:
resizeEvent := event.(tomo.EventResize)
// we will receive a resize event from X later which will be
// handled by our event handler callbacks.
if resizeEvent.Width < window.MinimumWidth() {
resizeEvent.Width = window.MinimumWidth()
}
if resizeEvent.Height < window.MinimumHeight() {
resizeEvent.Height = window.MinimumHeight()
}
window.xWindow.Resize(resizeEvent.Width, resizeEvent.Height)
default:
if window.child != nil { window.child.Handle(event) }
}
return
}
func (window *Window) SetDrawCallback (draw func (region tomo.Image)) {
window.drawCallback = draw
}
func (window *Window) SetMinimumSizeChangeCallback (
notify func (width, height int),
) {
window.minimumSizeChangeCallback = notify
}
func (window *Window) Selectable () (selectable bool) {
if window.child != nil { selectable = window.child.Selectable() }
return
}
func (window *Window) MinimumWidth () (minimum int) {
if window.child != nil { minimum = window.child.MinimumWidth() }
minimum = 8
return
}
func (window *Window) MinimumHeight () (minimum int) {
if window.child != nil { minimum = window.child.MinimumHeight() }
minimum = 8
return
}
func (window *Window) Adopt (child tomo.Element) {
if window.child != nil {
window.child.SetDrawCallback(nil)
window.child.SetMinimumSizeChangeCallback(nil)
}
window.child = child
if child != nil {
child.SetDrawCallback(window.childDrawCallback)
child.SetMinimumSizeChangeCallback (
window.childMinimumSizeChangeCallback)
window.resizeChildToFit()
}
window.childMinimumSizeChangeCallback (
child.MinimumWidth(),
child.MinimumHeight())
}
func (window *Window) Child () (child tomo.Element) {
child = window.child
return
}
func (window *Window) SetTitle (title string) {
ewmh.WmNameSet (
window.backend.connection,
window.xWindow.Id,
title)
}
func (window *Window) SetIcon (sizes []image.Image) {
wmIcons := []ewmh.WmIcon { }
for _, icon := range sizes {
width := icon.Bounds().Max.X
height := icon.Bounds().Max.Y
wmIcon := ewmh.WmIcon {
Width: uint(width),
Height: uint(height),
Data: make ([]uint, width * height),
}
// manually convert image data beacuse of course we have to do
// this
index := 0
for y := 0; y < height; y ++ {
for x := 0; x < width; x ++ {
r, g, b, a := icon.At(x, y).RGBA()
r >>= 8
g >>= 8
b >>= 8
a >>= 8
wmIcon.Data[index] =
(uint(a) << 24) |
(uint(r) << 16) |
(uint(g) << 8) |
(uint(b) << 0)
index ++
}}
wmIcons = append(wmIcons, wmIcon)
}
ewmh.WmIconSet (
window.backend.connection,
window.xWindow.Id,
wmIcons)
}
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()
}
func (window *Window) Hide () {
window.xWindow.Unmap()
}
func (window *Window) Close () {
delete(window.backend.windows, window.xWindow.Id)
if window.onClose != nil { window.onClose() }
xevent.Detach(window.xWindow.X, window.xWindow.Id)
window.xWindow.Destroy()
}
func (window *Window) OnClose (callback func ()) {
window.onClose = callback
}
func (window *Window) reallocateCanvas () {
if window.xCanvas != nil {
window.xCanvas.Destroy()
}
window.xCanvas = xgraphics.New (
window.backend.connection,
image.Rect (
0, 0,
window.metrics.width,
window.metrics.height))
window.xCanvas.XSurfaceSet(window.xWindow.Id)
}
func (window *Window) redrawChildEntirely () {
window.xCanvas.For (func (x, y int) (c xgraphics.BGRA) {
rgba := window.child.RGBAAt(x, y)
c.R, c.G, c.B, c.A = rgba.R, rgba.G, rgba.B, rgba.A
return
})
window.pushRegion(window.xCanvas.Bounds())
}
func (window *Window) resizeChildToFit () {
window.skipChildDrawCallback = true
window.child.Handle(tomo.EventResize {
Width: window.metrics.width,
Height: window.metrics.height,
})
window.skipChildDrawCallback = false
window.redrawChildEntirely()
}
func (window *Window) childDrawCallback (region tomo.Image) {
if window.skipChildDrawCallback { return }
bounds := region.Bounds()
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
rgba := region.RGBAAt(x, y)
window.xCanvas.SetBGRA (x, y, xgraphics.BGRA {
R: rgba.R,
G: rgba.G,
B: rgba.B,
A: rgba.A,
})
}}
window.pushRegion(region.Bounds())
}
func (window *Window) childMinimumSizeChangeCallback (width, height int) {
icccm.WmNormalHintsSet (
window.backend.connection,
window.xWindow.Id,
&icccm.NormalHints {
Flags: icccm.SizeHintPMinSize,
MinWidth: uint(width),
MinHeight: uint(height),
})
newWidth := window.metrics.width
newHeight := window.metrics.height
if newWidth < width { newWidth = width }
if newHeight < height { newHeight = height }
if newWidth != window.metrics.width ||
newHeight != window.metrics.height {
window.xWindow.Resize(newWidth, newHeight)
}
}
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()
window.xCanvas.XPaint(window.xWindow.Id)
}
}

85
backends/x/x.go Normal file
View File

@ -0,0 +1,85 @@
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"
// 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
}
windows map[xproto.Window] *Window
}
// NewBackend instantiates an X backend.
func NewBackend () (output tomo.Backend, err error) {
backend := &Backend {
windows: map[xproto.Window] *Window { },
}
// connect to X
backend.connection, err = xgbutil.NewConn()
if err != nil { return }
backend.initializeKeymapInformation()
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
}
}
}
// Stop gracefully closes the connection and stops the event loop.
func (backend *Backend) Stop () {
backend.assert()
for _, window := range backend.windows {
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
}
func (backend *Backend) assert () {
if backend == nil { panic("nil backend") }
}
func init () {
tomo.RegisterBackend(NewBackend)
}

View File

@ -0,0 +1,47 @@
package defaultfont
import "golang.org/x/image/font/basicfont"
var FaceRegular = basicfont.Face7x13
// FIXME: make bold, italic, and bold italic masks by processing the Face7x13
// mask.
var FaceBold = &basicfont.Face {
Advance: 7,
Width: 6,
Height: 13,
Ascent: 11,
Descent: 2,
Mask: FaceRegular.Mask, // TODO
Ranges: []basicfont.Range {
{ '\u0020', '\u007f', 0 },
{ '\ufffd', '\ufffe', 95 },
},
}
var FaceItalic = &basicfont.Face {
Advance: 7,
Width: 6,
Height: 13,
Ascent: 11,
Descent: 2,
Mask: FaceRegular.Mask, // TODO
Ranges: []basicfont.Range {
{ '\u0020', '\u007f', 0 },
{ '\ufffd', '\ufffe', 95 },
},
}
var FaceBoldItalic = &basicfont.Face {
Advance: 7,
Width: 6,
Height: 13,
Ascent: 11,
Descent: 2,
Mask: FaceRegular.Mask, // TODO
Ranges: []basicfont.Range {
{ '\u0020', '\u007f', 0 },
{ '\ufffd', '\ufffe', 95 },
},
}

176
elements/basic/button.go Normal file
View File

@ -0,0 +1,176 @@
package basic
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
type Button struct {
core Core
pressed bool
enabled bool
onClick func ()
text string
drawer artist.TextDrawer
}
func NewButton (text string) (element *Button) {
element = &Button { enabled: true }
element.core = NewCore(element)
element.drawer.SetFace(theme.FontFaceRegular())
element.SetText(text)
return
}
func (element *Button) Handle (event tomo.Event) {
switch event.(type) {
case tomo.EventResize:
resizeEvent := event.(tomo.EventResize)
element.core.AllocateCanvas (
resizeEvent.Width,
resizeEvent.Height)
element.draw()
case tomo.EventMouseDown:
if !element.enabled { break }
mouseDownEvent := event.(tomo.EventMouseDown)
if mouseDownEvent.Button != tomo.ButtonLeft { break }
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
case tomo.EventMouseUp:
if !element.enabled { break }
mouseUpEvent := event.(tomo.EventMouseUp)
if mouseUpEvent.Button != tomo.ButtonLeft { break }
element.pressed = false
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
within := image.Point { mouseUpEvent.X, mouseUpEvent.Y }.
In(element.Bounds())
if within && element.onClick != nil {
element.onClick()
}
// TODO: handle selection events, and the enter key
}
return
}
func (element *Button) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
if element.core.HasImage () {
element.draw()
element.core.PushAll()
}
}
func (element *Button) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText(text)
textBounds := element.drawer.LayoutBounds()
element.core.SetMinimumSize (
theme.Padding() * 2 + textBounds.Dx(),
theme.Padding() * 2 + textBounds.Dy())
if element.core.HasImage () {
element.draw()
element.core.PushAll()
}
}
func (element *Button) OnClick (callback func ()) {
element.onClick = callback
}
func (element *Button) ColorModel () (model color.Model) {
return color.RGBAModel
}
func (element *Button) At (x, y int) (pixel color.Color) {
pixel = element.core.At(x, y)
return
}
func (element *Button) RGBAAt (x, y int) (pixel color.RGBA) {
pixel = element.core.RGBAAt(x, y)
return
}
func (element *Button) Bounds () (bounds image.Rectangle) {
bounds = element.core.Bounds()
return
}
func (element *Button) SetDrawCallback (draw func (region tomo.Image)) {
element.core.SetDrawCallback(draw)
}
func (element *Button) SetMinimumSizeChangeCallback (
notify func (width, height int),
) {
element.core.SetMinimumSizeChangeCallback(notify)
}
func (element *Button) Selectable () (selectable bool) {
selectable = true
return
}
func (element *Button) MinimumWidth () (minimum int) {
minimum = element.core.MinimumWidth()
return
}
func (element *Button) MinimumHeight () (minimum int) {
minimum = element.core.MinimumHeight()
return
}
func (element *Button) draw () {
bounds := element.core.Bounds()
artist.ChiseledRectangle (
element.core,
theme.RaisedProfile(element.pressed, element.enabled),
bounds)
innerBounds := bounds
innerBounds.Min.X += theme.Padding()
innerBounds.Min.Y += theme.Padding()
innerBounds.Max.X -= theme.Padding()
innerBounds.Max.Y -= theme.Padding()
textBounds := element.drawer.LayoutBounds()
offset := image.Point {
X: theme.Padding() + (innerBounds.Dx() - textBounds.Dx()) / 2,
Y: theme.Padding() + (innerBounds.Dy() - textBounds.Dy()) / 2,
}
// account for the fact that the bounding rectangle will be shifted over
// due to the bounds origin being at the baseline of the first line
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
if element.pressed {
offset = offset.Add(theme.SinkOffsetVector())
}
foreground := theme.ForegroundImage()
if !element.enabled {
foreground = theme.DisabledForegroundImage()
}
element.drawer.Draw(element.core, foreground, offset)
}

138
elements/basic/core.go Normal file
View File

@ -0,0 +1,138 @@
package basic
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
// Core is a struct that implements some core functionality common to most
// widgets. It is possible to embed this directly into a struct, but this is not
// reccomended as it exposes internal functionality.
type Core struct {
*image.RGBA
parent tomo.Element
drawCallback func (region tomo.Image)
minimumSizeChangeCallback func (width, height int)
metrics struct {
minimumWidth int
minimumHeight int
}
}
// Core creates a new element core.
func NewCore (parent tomo.Element) (core Core) {
core = Core { parent: parent }
return
}
func (core Core) ColorModel () (model color.Model) {
return color.RGBAModel
}
func (core Core) At (x, y int) (pixel color.Color) {
if core.RGBA == nil { return color.RGBA { } }
pixel = core.RGBA.At(x, y)
return
}
func (core Core) RGBAAt (x, y int) (pixel color.RGBA) {
if core.RGBA == nil { return color.RGBA { } }
pixel = core.RGBA.RGBAAt(x, y)
return
}
func (core Core) Bounds () (bounds image.Rectangle) {
if core.RGBA != nil { bounds = core.RGBA.Bounds() }
return
}
func (core *Core) SetDrawCallback (draw func (region tomo.Image)) {
core.drawCallback = draw
}
func (core *Core) SetMinimumSizeChangeCallback (
notify func (width, height int),
) {
core.minimumSizeChangeCallback = notify
}
func (core Core) HasImage () (has bool) {
has = core.RGBA != nil
return
}
func (core Core) PushRegion (bounds image.Rectangle) {
if core.drawCallback != nil {
core.drawCallback(core.SubImage(bounds).
(*image.RGBA))
}
}
func (core Core) PushAll () {
core.PushRegion(core.Bounds())
}
func (core *Core) AllocateCanvas (width, height int) {
width, height, _ = core.ConstrainSize(width, height)
core.RGBA = image.NewRGBA(image.Rect (0, 0, width, height))
}
func (core Core) MinimumWidth () (minimum int) {
minimum = core.metrics.minimumWidth
return
}
func (core Core) MinimumHeight () (minimum int) {
minimum = core.metrics.minimumHeight
return
}
func (core *Core) SetMinimumSize (width, height int) {
if width != core.metrics.minimumWidth ||
height != core.metrics.minimumHeight {
core.metrics.minimumWidth = width
core.metrics.minimumHeight = height
if core.minimumSizeChangeCallback != nil {
core.minimumSizeChangeCallback(width, height)
}
// if there is an image buffer, and the current size is less
// than this new minimum size, send core.parent a resize event.
if core.HasImage() {
bounds := core.Bounds()
imageWidth,
imageHeight,
constrained := core.ConstrainSize (
bounds.Dx(),
bounds.Dy())
if constrained {
core.parent.Handle (tomo.EventResize {
Width: imageWidth,
Height: imageHeight,
})
}
}
}
}
func (core Core) ConstrainSize (
inWidth, inHeight int,
) (
outWidth, outHeight int,
constrained bool,
) {
outWidth = inWidth
outHeight = inHeight
if outWidth < core.metrics.minimumWidth {
outWidth = core.metrics.minimumWidth
constrained = true
}
if outHeight < core.metrics.minimumHeight {
outHeight = core.metrics.minimumHeight
constrained = true
}
return
}

114
elements/basic/label.go Normal file
View File

@ -0,0 +1,114 @@
package basic
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
type Label struct {
core Core
text string
drawer artist.TextDrawer
}
func NewLabel (text string) (element *Label) {
element = &Label { }
element.core = NewCore(element)
face := theme.FontFaceRegular()
element.drawer.SetFace(face)
element.SetText(text)
// FIXME: set the minimum size to one char
metrics := face.Metrics()
emspace, _ := face.GlyphAdvance('M')
intEmspace := emspace.Round()
if intEmspace < 1 { intEmspace = theme.Padding()}
element.core.SetMinimumSize(intEmspace, metrics.Height.Round())
return
}
func (element *Label) Handle (event tomo.Event) {
switch event.(type) {
case tomo.EventResize:
resizeEvent := event.(tomo.EventResize)
element.core.AllocateCanvas (
resizeEvent.Width,
resizeEvent.Height)
element.drawer.SetMaxWidth (resizeEvent.Width)
element.drawer.SetMaxHeight(resizeEvent.Height)
element.draw()
}
return
}
func (element *Label) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText(text)
if element.core.HasImage () {
element.draw()
element.core.PushAll()
}
}
func (element *Label) ColorModel () (model color.Model) {
return color.RGBAModel
}
func (element *Label) At (x, y int) (pixel color.Color) {
pixel = element.core.At(x, y)
return
}
func (element *Label) RGBAAt (x, y int) (pixel color.RGBA) {
pixel = element.core.RGBAAt(x, y)
return
}
func (element *Label) Bounds () (bounds image.Rectangle) {
bounds = element.core.Bounds()
return
}
func (element *Label) SetDrawCallback (draw func (region tomo.Image)) {
element.core.SetDrawCallback(draw)
}
func (element *Label) SetMinimumSizeChangeCallback (
notify func (width, height int),
) {
element.core.SetMinimumSizeChangeCallback(notify)
}
func (element *Label) Selectable () (selectable bool) {
return
}
func (element *Label) MinimumWidth () (minimum int) {
minimum = element.core.MinimumWidth()
return
}
func (element *Label) MinimumHeight () (minimum int) {
minimum = element.core.MinimumHeight()
return
}
func (element *Label) draw () {
bounds := element.core.Bounds()
artist.Rectangle (
element.core,
theme.BackgroundImage(),
nil, 0,
bounds)
textBounds := element.drawer.LayoutBounds()
foreground := theme.ForegroundImage()
element.drawer.Draw (element.core, foreground, image.Point {
X: 0 - textBounds.Min.X,
Y: 0 - textBounds.Min.Y,
})
}

90
elements/basic/test.go Normal file
View File

@ -0,0 +1,90 @@
package basic
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// Test is a simple element that can be used as a placeholder.
type Test struct {
core Core
}
// NewTest creates a new test element.
func NewTest () (element *Test) {
element = &Test { }
element.core = NewCore(element)
element.core.SetMinimumSize(32, 32)
return
}
func (element *Test) Handle (event tomo.Event) {
switch event.(type) {
case tomo.EventResize:
resizeEvent := event.(tomo.EventResize)
element.core.AllocateCanvas (
resizeEvent.Width,
resizeEvent.Height)
for y := 0; y < resizeEvent.Height; y ++ {
for x := 0; x < resizeEvent.Width; x ++ {
pixel := color.RGBA {
R: 0x40, G: 0x80, B: 0x90, A: 0xFF,
}
element.core.SetRGBA (x, y, pixel)
}}
artist.Line (
element.core, artist.NewUniform(color.White), 1,
image.Pt(0, 0),
image.Pt(resizeEvent.Width, resizeEvent.Height))
artist.Line (
element.core, artist.NewUniform(color.White), 1,
image.Pt(0, resizeEvent.Height),
image.Pt(resizeEvent.Width, 0))
default:
}
return
}
func (element *Test) ColorModel () (model color.Model) {
return color.RGBAModel
}
func (element *Test) At (x, y int) (pixel color.Color) {
pixel = element.core.At(x, y)
return
}
func (element *Test) RGBAAt (x, y int) (pixel color.RGBA) {
pixel = element.core.RGBAAt(x, y)
return
}
func (element *Test) Bounds () (bounds image.Rectangle) {
bounds = element.core.Bounds()
return
}
func (element *Test) SetDrawCallback (draw func (region tomo.Image)) {
element.core.SetDrawCallback(draw)
}
func (element *Test) SetMinimumSizeChangeCallback (
notify func (width, height int),
) {
element.core.SetMinimumSizeChangeCallback(notify)
}
func (element *Test) Selectable () (selectable bool) {
return
}
func (element *Test) MinimumWidth () (minimum int) {
minimum = element.core.MinimumWidth()
return
}
func (element *Test) MinimumHeight () (minimum int) {
minimum = element.core.MinimumHeight()
return
}

88
event.go Normal file
View File

@ -0,0 +1,88 @@
package tomo
// Event represents any event. Use a type switch to figure out what sort of
// event it is.
type Event interface { }
// EventResize is sent to an element when its parent decides to resize it.
// Elements should not do anything if the width and height do not change.
type EventResize struct {
// The width and height the element should not be less than the
// element's reported minimum width and height. If by some chance they
// are anyways, the element should use its minimum width and height
// instead.
Width, Height int
}
// EventKeyDown is sent to the currently selected element when a key on the
// keyboard is pressed. Containers must propagate this event downwards.
type EventKeyDown struct {
Key
Modifiers
Repeated bool
}
// EventKeyDown is sent to the currently selected element when a key on the
// keyboard is released. Containers must propagate this event downwards.
type EventKeyUp struct {
Key
Modifiers
}
// EventMouseDown is sent to the element the mouse is positioned over when it is
// clicked. Containers must propagate this event downwards, with X and Y values
// relative to the top left corner of the child element.
type EventMouseDown struct {
// The button that was released
Button
// The X and Y position of the mouse cursor at the time of the event,
// relative to the top left corner of the element
X, Y int
}
// EventMouseUp is sent to the element that was positioned under the mouse the
// last time this particular mouse button was pressed down when it is released.
// Containers must propagate this event downwards, with X and Y values relative
// to the top left corner of the child element.
type EventMouseUp struct {
// The button that was released
Button
// The X and Y position of the mouse cursor at the time of the event,
// relative to the top left corner of the element
X, Y int
}
// EventMouseMove is sent to the element positioned under the mouse cursor when
// the mouse moves, or if a mouse button is currently being pressed, the element
// that the mouse was positioned under when it was pressed down. Containers must
// propogate this event downwards, with X and Y values relative to the top left
// corner of the child element.
type EventMouseMove struct {
// The X and Y position of the mouse cursor at the time of the event,
// relative to the top left corner of the element
X, Y int
}
// EventScroll is sent to the element positioned under the mouse cursor when the
// scroll wheel (or equivalent) is spun. Containers must propogate this event
// downwards.
type EventScroll struct {
// The X and Y position of the mouse cursor at the time of the event,
// relative to the top left corner of the element
X, Y int
// The X and Y amount the scroll wheel moved
ScrollX, ScrollY int
}
// EventSelect is sent to selectable elements when they become selected, whether
// by a mouse click or by keyboard navigation. Containers must propagate this
// event downwards.
type EventSelect struct { }
// EventDeselect is sent to selectable elements when they stop being selected,
// whether by a mouse click or by keyboard navigation. Containers must propagate
// this event downwards.
type EventDeselect struct { }

29
examples/button/main.go Normal file
View File

@ -0,0 +1,29 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(2, 2)
window.SetTitle("example button")
button := basic.NewButton("hello tomo!")
button.OnClick (func () {
// when we set the button's text to something longer, the window
// will automatically resize to accomodate it.
button.SetText("you clicked me.\nwow, there are two lines!")
button.OnClick (func () {
button.SetText (
"stop clicking me you idiot!\n" +
"you've already seen it all!")
button.OnClick(tomo.Stop)
})
})
window.Adopt(button)
window.OnClose(tomo.Stop)
window.Show()
}

19
examples/label/main.go Normal file
View File

@ -0,0 +1,19 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(480, 360)
window.SetTitle("example label")
window.Adopt(basic.NewLabel(text))
window.OnClose(tomo.Stop)
window.Show()
}
const text = "Resize the window to see the text wrap:\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Fermentum et sollicitudin ac orci phasellus egestas tellus rutrum. Aliquam vestibulum morbi blandit cursus risus at ultrices mi. Gravida dictum fusce ut placerat. Cursus metus aliquam eleifend mi in nulla posuere. Sit amet nulla facilisi morbi tempus iaculis urna id. Amet volutpat consequat mauris nunc congue nisi vitae. Varius duis at consectetur lorem donec massa sapien faucibus et. Vitae elementum curabitur vitae nunc sed velit dignissim. In hac habitasse platea dictumst quisque sagittis purus. Enim nulla aliquet porttitor lacus luctus accumsan tortor. Lectus magna fringilla urna porttitor rhoncus dolor purus non.\n\nNon pulvinar neque laoreet suspendisse. Viverra adipiscing at in tellus integer. Vulputate dignissim suspendisse in est ante. Purus in mollis nunc sed id semper. In est ante in nibh mauris cursus. Risus pretium quam vulputate dignissim suspendisse in est. Blandit aliquam etiam erat velit scelerisque in dictum. Lectus quam id leo in. Odio tempor orci dapibus ultrices in iaculis. Pharetra sit amet aliquam id. Elit ut aliquam purus sit. Egestas dui id ornare arcu odio ut sem nulla pharetra. Massa tempor nec feugiat nisl pretium fusce id. Dui accumsan sit amet nulla facilisi morbi. A lacus vestibulum sed arcu non odio euismod. Nam libero justo laoreet sit amet cursus. Mattis rhoncus urna neque viverra justo nec. Mauris augue neque gravida in fermentum et sollicitudin ac. Vulputate mi sit amet mauris. Ut sem nulla pharetra diam sit amet."

17
examples/test/main.go Normal file
View File

@ -0,0 +1,17 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(128, 128)
window.SetTitle("hellorld!")
window.Adopt(basic.NewTest())
window.OnClose(tomo.Stop)
window.Show()
}

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module git.tebibyte.media/sashakoshka/tomo
go 1.19
require (
github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66
golang.org/x/image v0.3.0
)
require (
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect
github.com/jezek/xgb v1.1.0
)

34
go.sum Normal file
View File

@ -0,0 +1,34 @@
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA=
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66 h1:+wPhoJD8EH0/bXipIq8Lc2z477jfox9zkXPCJdhvHj8=
github.com/jezek/xgbutil v0.0.0-20210302171758-530099784e66/go.mod h1:KACeV+k6b+aoLTVrrurywEbu3UpqoQcQywj4qX8aQKM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg=
golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

112
input.go Normal file
View File

@ -0,0 +1,112 @@
package tomo
import "unicode"
// Key represents a keyboard key.
type Key int
const (
KeyNone Key = 0
KeyInsert Key = 1
KeyMenu Key = 2
KeyPrintScreen Key = 3
KeyPause Key = 4
KeyCapsLock Key = 5
KeyScrollLock Key = 6
KeyNumLock Key = 7
KeyBackspace Key = 8
KeyTab Key = 9
KeyEnter Key = 10
KeyEscape Key = 11
KeyUp Key = 12
KeyDown Key = 13
KeyLeft Key = 14
KeyRight Key = 15
KeyPageUp Key = 16
KeyPageDown Key = 17
KeyHome Key = 18
KeyEnd Key = 19
KeyLeftShift Key = 20
KeyRightShift Key = 21
KeyLeftControl Key = 22
KeyRightControl Key = 23
KeyLeftAlt Key = 24
KeyRightAlt Key = 25
KeyLeftMeta Key = 26
KeyRightMeta Key = 27
KeyLeftSuper Key = 28
KeyRightSuper Key = 29
KeyLeftHyper Key = 30
KeyRightHyper Key = 31
KeyDelete Key = 127
KeyDead Key = 128
KeyF1 Key = 129
KeyF2 Key = 130
KeyF3 Key = 131
KeyF4 Key = 132
KeyF5 Key = 133
KeyF6 Key = 134
KeyF7 Key = 135
KeyF8 Key = 136
KeyF9 Key = 137
KeyF10 Key = 138
KeyF11 Key = 139
KeyF12 Key = 140
)
// Button represents a mouse button.
type Button int
const (
ButtonNone Button = iota
Button1
Button2
Button3
Button4
Button5
Button6
Button7
Button8
Button9
ButtonLeft Button = Button1
ButtonMiddle Button = Button2
ButtonRight Button = Button3
ButtonBack Button = Button8
ButtonForward Button = Button9
)
// Printable returns whether or not the key's character could show up on screen.
// If this function returns true, the key can be cast to a rune and used as
// such.
func (key Key) Printable () (printable bool) {
printable = unicode.IsPrint(rune(key))
return
}
// Modifiers lists what modifier keys are being pressed. This is used in
// conjunction with a Key code in a Key press event. These should be used
// instead of attempting to track the state of the modifier keys, because there
// is no guarantee that one press event will be coupled with one release event.
type Modifiers struct {
Shift bool
Control bool
Alt bool
Meta bool
Super bool
Hyper bool
// NumberPad does not represent a key, but it behaves like one. If it is
// set to true, the Key was pressed on the number pad. It is treated
// as a modifier key because if you don't care whether a key was pressed
// on the number pad or not, you can just ignore this value.
NumberPad bool
}

6
iterator/iterator.go Normal file
View File

@ -0,0 +1,6 @@
package iterator
type Iterator[ELEMENT_TYPE any] interface {
Length() int
Next() ELEMENT_TYPE
}

172
theme/theme.go Normal file
View File

@ -0,0 +1,172 @@
package theme
import "image"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
// none of these colors are final! TODO: generate these values from a theme
// file at startup.
var foregroundImage = artist.NewUniform(color.Gray16 { 0x0000})
var disabledForegroundImage = artist.NewUniform(color.Gray16 { 0x5555})
var accentImage = artist.NewUniform(color.RGBA { 0xFF, 0x22, 0x00, 0xFF})
var highlightImage = artist.NewUniform(color.Gray16 { 0xEEEE })
var shadowImage = artist.NewUniform(color.Gray16 { 0x3333 })
var backgroundImage = artist.NewUniform(color.Gray16 { 0xAAAA})
var backgroundProfile = artist.ShadingProfile {
Highlight: highlightImage,
Shadow: shadowImage,
Stroke: artist.NewUniform(color.Gray16 { 0x0000 }),
Fill: backgroundImage,
StrokeWeight: 1,
ShadingWeight: 1,
}
var engravedBackgroundProfile = backgroundProfile.Engraved()
var raisedImage = artist.NewUniform(color.RGBA { 0x8D, 0x98, 0x94, 0xFF})
var raisedProfile = artist.ShadingProfile {
Highlight: highlightImage,
Shadow: shadowImage,
Stroke: artist.NewUniform(color.Gray16 { 0x0000 }),
Fill: raisedImage,
StrokeWeight: 1,
ShadingWeight: 1,
}
var engravedRaisedProfile = artist.ShadingProfile {
Highlight: artist.NewUniform(color.Gray16 { 0x7777 }),
Shadow: raisedImage,
Stroke: artist.NewUniform(color.Gray16 { 0x0000 }),
Fill: raisedImage,
StrokeWeight: 1,
ShadingWeight: 1,
}
var disabledRaisedProfile = artist.ShadingProfile {
Highlight: artist.NewUniform(color.Gray16 { 0x7777 }),
Shadow: artist.NewUniform(color.Gray16 { 0x7777 }),
Stroke: artist.NewUniform(color.Gray16 { 0x3333 }),
Fill: raisedImage,
StrokeWeight: 1,
ShadingWeight: 0,
}
var inputImage = artist.NewUniform(color.Gray16 { 0xFFFF })
var inputProfile = artist.ShadingProfile {
Highlight: shadowImage,
Shadow: highlightImage,
Stroke: artist.NewUniform(color.Gray16 { 0x0000 }),
Fill: inputImage,
StrokeWeight: 1,
ShadingWeight: 1,
}
var disabledInputProfile = artist.ShadingProfile {
Highlight: artist.NewUniform(color.Gray16 { 0x7777 }),
Shadow: artist.NewUniform(color.Gray16 { 0x7777 }),
Stroke: artist.NewUniform(color.Gray16 { 0x3333 }),
Fill: inputImage,
StrokeWeight: 1,
ShadingWeight: 0,
}
// BackgroundProfile returns the shading profile to be used for backgrounds.
func BackgroundProfile (engraved bool) artist.ShadingProfile {
if engraved {
return engravedBackgroundProfile
} else {
return backgroundProfile
}
}
// RaisedProfile returns the shading profile to be used for raised objects such
// as buttons.
func RaisedProfile (engraved bool, enabled bool) artist.ShadingProfile {
if enabled {
if engraved {
return engravedRaisedProfile
} else {
return raisedProfile
}
} else {
return disabledRaisedProfile
}
}
// InputProfile returns the shading profile to be used for input fields.
func InputProfile (enabled bool) artist.ShadingProfile {
if enabled {
return inputProfile
} else {
return disabledInputProfile
}
}
// BackgroundImage returns the texture/color used for the fill of
// BackgroundProfile.
func BackgroundImage () tomo.Image {
return backgroundImage
}
// RaisedImage returns the texture/color used for the fill of RaisedProfile.
func RaisedImage () tomo.Image {
return raisedImage
}
// InputImage returns the texture/color used for the fill of InputProfile.
func InputImage () tomo.Image {
return inputImage
}
// ForegroundImage returns the texture/color text and monochromatic icons should
// be drawn with.
func ForegroundImage () tomo.Image {
return foregroundImage
}
// DisabledForegroundImage returns the texture/color text and monochromatic
// icons should be drawn with if they are disabled.
func DisabledForegroundImage () tomo.Image {
return disabledForegroundImage
}
// AccentImage returns the accent texture/color.
func AccentImage () tomo.Image {
return accentImage
}
// TODO: load fonts from an actual source instead of using basicfont
// FontFaceRegular returns the font face to be used for normal text.
func FontFaceRegular () font.Face {
return defaultfont.FaceRegular
}
// FontFaceBold returns the font face to be used for bolded text.
func FontFaceBold () font.Face {
return defaultfont.FaceBold
}
// FontFaceItalic returns the font face to be used for italicized text.
func FontFaceItalic () font.Face {
return defaultfont.FaceItalic
}
// FontFaceBoldItalic returns the font face to be used for text that is both
// bolded and italicized.
func FontFaceBoldItalic () font.Face {
return defaultfont.FaceBoldItalic
}
// Padding returns how spaced out things should be on the screen. Generally,
// text should be offset from its container on all sides by this amount.
func Padding () int {
return 8
}
// SinkOffsetVector specifies a vector for things such as text to move by when a
// "sinking in" effect is desired, such as a button label during a button press.
func SinkOffsetVector () image.Point {
return image.Point { 1, 1 }
}

114
tomo.go Normal file
View File

@ -0,0 +1,114 @@
package tomo
import "image"
import "errors"
import "image/draw"
import "image/color"
// import "git.tebibyte.media/sashakoshka/tomo/iterator"
// Image represents a simple image buffer that fulfills the image.Image
// interface while also having methods that do away with the use of the
// color.Color interface to facilitate more efficient drawing. This interface
// can be easily satisfied using an image.RGBA struct.
type Image interface {
image.Image
RGBAAt (x, y int) (c color.RGBA)
}
// Canvas is like Image but also requires a SetRGBA method. This interface can
// be easily satisfied using an image.RGBA struct.
type Canvas interface {
draw.Image
RGBAAt (x, y int) (c color.RGBA)
SetRGBA (x, y int, c color.RGBA)
}
// Element represents a basic on-screen object.
type Element interface {
// Element must implement the Image interface. Elements should start out
// with a completely blank image buffer, and only set its size and draw
// on it for the first time when sent an EventResize event.
Image
// Handle handles an event, propagating it to children if necessary.
Handle (event Event)
// Selectable returns whether this element can be selected. If this
// element contains other selectable elements, it must return true.
Selectable () (bool)
// SetDrawCallback sets a function to be called when a part of the
// element's surface is updated. The updated region will be passed to
// the callback as a sub-image.
SetDrawCallback (draw func (region Image))
// SetMinimumSizeChangeCallback sets a function to be called when the
// element's minimum width and/or height changes. When this function is
// called, the element will have already been resized and there is no
// need to send it a resize event.
SetMinimumSizeChangeCallback (notify func (width, height int))
// MinimumWidth specifies the minimum amount of pixels this element's
// width may be set to. If the element is resized to an amount smaller
// that MinimumWidth, it will instead set its width to MinimumWidth.
MinimumWidth () (minimum int)
// MinimumHeight specifies the minimum amount of pixels this element's
// height may be set to. If the element is resized to an amount smaller
// that MinimumHeight, it will instead set its height to MinimumHeight.
MinimumHeight () (minimum int)
}
// Window represents a top-level container generated by the currently running
// backend. It can contain a single element. It is hidden by default, and must
// be explicitly shown with the Show() method. If it contains no element, it
// displays a black (or transprent) background.
type Window interface {
Element
Adopt (child Element)
Child () (child Element)
SetTitle (title string)
SetIcon (sizes []image.Image)
Show ()
Hide ()
Close ()
OnClose (func ())
}
var backend Backend
// Run initializes a backend, calls the callback function, and begins the event
// loop in that order. This function does not return until Stop() is called, or
// the backend experiences a fatal error.
func Run (callback func ()) (err error) {
backend, err = instantiateBackend()
if callback != nil { callback() }
err = backend.Run()
backend = nil
return
}
// Stop gracefully stops the event loop and shuts the backend down. Call this
// before closing your application.
func Stop () {
if backend != nil { backend.Stop() }
}
// Do executes the specified callback within the main thread as soon as
// possible. This function can be safely called from other threads.
func Do (callback func ()) {
}
// NewWindow creates a new window using the current backend, and returns it as a
// Window. If the window could not be created, an error is returned explaining
// why. If this function is called without a running backend, an error is
// returned as well.
func NewWindow (width, height int) (window Window, err error) {
if backend == nil {
err = errors.New("no backend is running.")
return
}
window, err = backend.NewWindow(width, height)
return
}