Add files

This commit is contained in:
Sasha Koshka 2023-06-30 16:38:51 -04:00
parent b086f7c411
commit 4a0fdffd07
12 changed files with 651 additions and 0 deletions

View File

@ -0,0 +1,6 @@
# tomo
WIP rewrite of tomo.
This module will serve as a wafer-thin collection of interfaces and glue code so
that plugins will be an actual viable concept.

73
backend.go Normal file
View File

@ -0,0 +1,73 @@
package tomo
import "sort"
import "image"
import "errors"
type Backend interface {
NewWindow (image.Rectangle) MainWindow
NewBox () Box
NewTextBox () TextBox
NewCanvasBox () CanvasBox
NewContainerBox () ContainerBox
// Stop must unblock run.
Run () error
Stop ()
// Do performs a callback function in the main thread as soon as
// possible. This method must be safe to call concurrently.
Do (func ())
}
// Factory is a function that attempts to instatiate a backend. If the
// instantiation process fails at any point, it must clean up all resources and
// return nil and an error explaining what happened.
type Factory func () (Backend, error)
var registered []factoryEntry
type factoryEntry struct {
Factory
priority int
}
// Register registers a backend factory with the given priority number.
func Register (priority int, factory Factory) {
registered = append(registered, factoryEntry {
priority: priority,
Factory: factory,
})
}
// Initialize instantiates a backend. The first backend (sorted by priority)
// that does not throw an error when initialized is used. If no backend could be
// instantiated, this function returns an error. This function should be called
// only once.
func Initialize () (Backend, error) {
backend, err := instantiate()
if err != nil { return nil, err }
return backend, err
}
func instantiate () (Backend, error) {
if len(registered) < 0 {
return nil, errors.New("no available backends")
}
// sort backends by priority
sort.Slice(registered, func (left, right int) bool {
return registered[left].priority < registered[right].priority
})
// attempt to instantiate
errorLog := ""
for _, factory := range registered {
backend, err := factory.Factory()
if err == nil {
return backend, nil
} else {
errorLog += " " + err.Error() + "\n"
}
}
return nil, errors.New("all backends failed:\n" + errorLog)
}

70
canvas/canvas.go Normal file
View File

@ -0,0 +1,70 @@
package canvas
import "image"
import "image/draw"
import "image/color"
// Cap represents a stroke cap type.
type Cap int; const (
CapButt Cap = iota
CapRound
CapSquare
)
// Joint represents a stroke joint type.
type Joint int; const (
JointRount Joint = iota
JointSharp
JointMiter
)
// StrokeAlign determines whether a stroke is drawn inside, outside, or on a
// path.
type StrokeAlign int; const (
StrokeAlignCenter StrokeAlign = iota
StrokeAlignInner
StrokeAlignOuter
)
// Pen represents a drawing context that is linked to a canvas. Each canvas can
// have multiple pens associated with it, each maintaining their own drawing
// context.
type Pen interface {
// Draw draws a path
Draw (points ...image.Point)
Closed (bool) // if the path is closed
Cap (Cap) // line cap stype
Joint (Joint) // line joint style
StrokeWeight (int) // how thick the stroke is
StrokeAlign (StrokeAlign) // where the stroke is drawn
// set the stroke/fill to a solid color
Stroke (color.Color)
Fill (color.Color)
}
// Canvas is an image that supports drawing paths.
type Canvas interface {
draw.Image
// Pen returns a new pen for this canvas.
Pen () Pen
// Clip returns a new canvas that points to a specific area of this one.
Clip (image.Rectangle) Canvas
}
// Drawer is an object that can draw to a canvas.
type Drawer interface {
Draw (Canvas)
}
// PushCanvas is a canvas that can push a region of itself to the screen (or
// some other destination).
type PushCanvas interface {
Canvas
// Push pushes a specified region to the screen.
Push (image.Rectangle)
}

56
data/data.go Normal file
View File

@ -0,0 +1,56 @@
// Package data provides operations to deal with arbitrary data and MIME types.
package data
import "io"
import "bytes"
// Data represents arbitrary polymorphic data that can be used for data transfer
// between applications.
type Data map[Mime] io.ReadSeekCloser
// Mime represents a MIME type.
type Mime struct {
// Type is the first half of the MIME type, and Subtype is the second
// half. The separating slash is not included in either. For example,
// text/html becomes:
// Mime { Type: "text", Subtype: "html" }
Type, Subtype string
}
// M is shorthand for creating a MIME type.
func M (ty, subtype string) Mime {
return Mime { ty, subtype }
}
// String returns the string representation of the MIME type.
func (mime Mime) String () string {
return mime.Type + "/" + mime.Subtype
}
func MimePlain () Mime { return Mime { "text", "plain" } }
func MimeFile () Mime { return Mime { "text", "uri-list" } }
type byteReadCloser struct { *bytes.Reader }
func (byteReadCloser) Close () error { return nil }
// Text returns plain text Data given a string.
func Text (text string) Data {
return Bytes(MimePlain(), []byte(text))
}
// Bytes constructs a Data given a buffer and a mime type.
func Bytes (mime Mime, buffer []byte) Data {
return Data {
mime: byteReadCloser { bytes.NewReader(buffer) },
}
}
// Merge combines several Datas together. If multiple Datas provide a reader for
// the same mime type, the ones further on in the list will take precedence.
func Merge (individual ...Data) (combined Data) {
for _, data := range individual {
for mime, reader := range data {
combined[mime] = reader
}}
return
}

52
event/event.go Normal file
View File

@ -0,0 +1,52 @@
package event
// A cookie is returned when you add an event handler so you can remove it
// later if you so choose.
type Cookie interface {
// Close removes the event handler this cookie is for.
Close ()
}
// Broadcaster manages event listeners
type Broadcaster struct {
lastID int
listeners map[int] func ()
}
// Connect adds a new listener to the broadcaster and returns a corresponding
// cookie.
func (broadcaster *Broadcaster) Connect (listener func ()) Cookie {
if listener == nil { return nil }
if broadcaster.listeners == nil {
broadcaster.listeners = make(map[int] func ())
}
cookie := broadcaster.newCookie()
broadcaster.listeners[cookie.id] = listener
return cookie
}
// Broadcast runs all event listeners at once.
func (broadcaster *Broadcaster) Broadcast () {
for _, listener := range broadcaster.listeners {
listener()
}
}
func (broadcaster *Broadcaster) newCookie () cookie {
broadcaster.lastID ++
return cookie {
id: broadcaster.lastID,
broadcaster: broadcaster,
}
}
type cookie struct {
id int
broadcaster *Broadcaster
}
func (cookie cookie) Close () {
delete(cookie.broadcaster.listeners, cookie.id)
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module git.tebibyte.media/tomo/tomo
go 1.20
require golang.org/x/image v0.8.0

33
go.sum Normal file
View File

@ -0,0 +1,33 @@
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.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg=
golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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/sync v0.1.0/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/sys v0.5.0/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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

105
input/input.go Normal file
View File

@ -0,0 +1,105 @@
// Package input defines keyboard and mouse code constants.
package input
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. 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
}

115
object.go Normal file
View File

@ -0,0 +1,115 @@
package tomo
import "image"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/canvas"
type Inset [4]int
type Gap image.Point
type Border struct {
Width Inset
Color [4]color.Color
}
type Align int; const (
AlignStart Align = iota // similar to left-aligned text
AlignMiddle // similar to center-aligned text
AlignEnd // similar to right-aligned text
AlignEven // similar to justified text
)
type Object interface {
Box () Box
}
type Box interface {
Object
Bounds () image.Rectangle
InnerBounds () image.Rectangle
SetBounds (image.Rectangle)
SetColor (color.Color)
SetBorder (...Border)
SetMinimumSize (int, int)
SetPadding (Inset)
SetDNDData (data.Data)
SetDNDAccept (...data.Mime)
SetFocused (bool)
SetFocusable (bool)
Focused () bool
Modifiers (func ()) input.Modifiers
MousePosition (func ()) image.Point
OnFocusEnter (func ()) event.Cookie
OnFocusLeave (func ()) event.Cookie
OnDNDEnter (func ()) event.Cookie
OnDNDLeave (func ()) event.Cookie
OnDNDDrop (func (data.Data)) event.Cookie
OnMouseEnter (func ()) event.Cookie
OnMouseLeave (func ()) event.Cookie
OnMouseMove (func ()) event.Cookie
OnMouseDown (func (input.Button)) event.Cookie
OnMouseUp (func (input.Button)) event.Cookie
OnScroll (func (deltaX, deltaY float64)) event.Cookie
OnKeyDown (func (key input.Key, numberPad bool)) event.Cookie
OnKeyUp (func (key input.Key, numberPad bool)) event.Cookie
ContentBounds () image.Rectangle
ScrollTo (image.Point)
OnContentBoundsChange (func ()) event.Cookie
}
type TextBox interface {
Box
SetTextColor (color.Color)
SetFace (font.Face)
SetHAlign (Align)
SetVAlign (Align)
}
type CanvasBox interface {
Box
SetDrawer (canvas.Drawer)
Invalidate ()
}
type ContainerBox interface {
Box
SetGap (Gap)
Add (Object)
Delete (Object)
Insert (child Object, before Object)
Clear ()
Length () int
At (int) Object
SetLayout (Layout)
}
type Window interface {
SetRoot (Object)
SetIcon (sizes []image.Image)
NewMenu (image.Rectangle) (Window, error)
NewModal (image.Rectangle) (Window, error)
Widget () (Window, error)
Copy (data.Data)
Paste (callback func (data.Data, error), accept ...data.Mime)
Close ()
Show ()
Hide ()
OnClose (func ()) event.Cookie
}
type MainWindow interface {
Window
NewChild (image.Rectangle) (Window, error)
}
type Layout interface {
Arrange (image.Rectangle, Gap, []Box)
}

55
plugin.go Normal file
View File

@ -0,0 +1,55 @@
package tomo
import "os"
import "plugin"
import "path/filepath"
var pluginPaths []string
func loadPlugins () {
for _, dir := range pluginPaths {
if dir != "" {
loadPluginsFrom(dir)
}
}
}
func loadPluginsFrom (dir string) {
entries, err := os.ReadDir(dir)
// its no big deal if one of the dirs doesn't exist
if err != nil { return }
for _, entry := range entries {
if entry.IsDir() { continue }
if filepath.Ext(entry.Name()) != ".so" { continue }
pluginPath := filepath.Join(dir, entry.Name())
loadPlugin(pluginPath)
}
}
func loadPlugin (path string) {
die := func (reason string) {
println("tomo: could not load plugin at", path + ":", reason)
}
plugin, err := plugin.Open(path)
if err != nil {
die(err.Error())
return
}
// check for and obtain basic plugin functions
name, ok := extract[func () string](plugin, "Name")
if !ok { die("does not implement Name() string"); return }
_, ok = extract[func () string](plugin, "Description")
if !ok { die("does not implement Description() string"); return }
println("tomo: loaded plugin", name())
}
func extract[T any] (plugin *plugin.Plugin, name string) (value T, ok bool) {
symbol, err := plugin.Lookup(name)
if err != nil { return }
value, ok = symbol.(T)
return
}

59
tomo.go Normal file
View File

@ -0,0 +1,59 @@
package tomo
import "image"
import "errors"
var backend Backend
// Run initializes a backend, runs the specified callback function, and runs the
// event loop in that order. This function blocks until Stop is called, or the
// backend experiences a fatal error.
func Run (callback func ()) error {
if backend != nil {
return errors.New("there is already a backend running")
}
back, err := Initialize()
if err != nil { return err }
backend = back
callback()
return backend.Run()
}
func assertBackend () {
if backend == nil { panic("nil backend") }
}
// Stop stops the backend, unblocking run. Run may be called again after calling
// Stop.
func Stop () {
assertBackend()
backend.Stop()
backend = nil
}
func NewWindow (bounds image.Rectangle) MainWindow {
assertBackend()
return backend.NewWindow(bounds)
}
func NewBox () Box {
assertBackend()
return backend.NewBox()
}
func NewTextBox () TextBox {
assertBackend()
return backend.NewTextBox()
}
func NewCanvasBox () CanvasBox {
assertBackend()
return backend.NewCanvasBox()
}
func NewContainerBox () ContainerBox {
assertBackend()
return backend.NewContainerBox()
}

22
unix.go Normal file
View File

@ -0,0 +1,22 @@
//go:build linux || darwin || freebsd
package tomo
import "os"
import "strings"
import "path/filepath"
func init () {
pathVariable := os.Getenv("TOMO_PLUGIN_PATH")
pluginPaths = strings.Split(pathVariable, ":")
pluginPaths = append (
pluginPaths,
"/usr/lib/tomo/plugins",
"/usr/local/lib/tomo/plugins")
homeDir, err := os.UserHomeDir()
if err == nil {
pluginPaths = append (
pluginPaths,
filepath.Join(homeDir, ".local/lib/tomo/plugins"))
}
}