From 4a0fdffd078ff9bc017d5d3bfd778e9b9ece8b71 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 30 Jun 2023 16:38:51 -0400 Subject: [PATCH] Add files --- README.md | 6 +++ backend.go | 73 ++++++++++++++++++++++++++++++ canvas/canvas.go | 70 +++++++++++++++++++++++++++++ data/data.go | 56 +++++++++++++++++++++++ event/event.go | 52 +++++++++++++++++++++ go.mod | 5 +++ go.sum | 33 ++++++++++++++ input/input.go | 105 +++++++++++++++++++++++++++++++++++++++++++ object.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++ plugin.go | 55 +++++++++++++++++++++++ tomo.go | 59 ++++++++++++++++++++++++ unix.go | 22 +++++++++ 12 files changed, 651 insertions(+) create mode 100644 backend.go create mode 100644 canvas/canvas.go create mode 100644 data/data.go create mode 100644 event/event.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 input/input.go create mode 100644 object.go create mode 100644 plugin.go create mode 100644 tomo.go create mode 100644 unix.go diff --git a/README.md b/README.md index e69de29..3928b2f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/backend.go b/backend.go new file mode 100644 index 0000000..5300aae --- /dev/null +++ b/backend.go @@ -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) +} diff --git a/canvas/canvas.go b/canvas/canvas.go new file mode 100644 index 0000000..22ffe86 --- /dev/null +++ b/canvas/canvas.go @@ -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) +} diff --git a/data/data.go b/data/data.go new file mode 100644 index 0000000..ab8e5da --- /dev/null +++ b/data/data.go @@ -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 +} diff --git a/event/event.go b/event/event.go new file mode 100644 index 0000000..8f70e66 --- /dev/null +++ b/event/event.go @@ -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) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f9ffc90 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.tebibyte.media/tomo/tomo + +go 1.20 + +require golang.org/x/image v0.8.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bb724e4 --- /dev/null +++ b/go.sum @@ -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= diff --git a/input/input.go b/input/input.go new file mode 100644 index 0000000..b6de402 --- /dev/null +++ b/input/input.go @@ -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 +} diff --git a/object.go b/object.go new file mode 100644 index 0000000..ab1dfb8 --- /dev/null +++ b/object.go @@ -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) +} diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..c7763c8 --- /dev/null +++ b/plugin.go @@ -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 +} diff --git a/tomo.go b/tomo.go new file mode 100644 index 0000000..ab69830 --- /dev/null +++ b/tomo.go @@ -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() +} diff --git a/unix.go b/unix.go new file mode 100644 index 0000000..363bb6d --- /dev/null +++ b/unix.go @@ -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")) + } +}