Atomized the functionality of the base tomo package

This commit is contained in:
Sasha Koshka 2023-02-02 01:47:01 -05:00
parent f71f789b60
commit 04d2ea4767
8 changed files with 486 additions and 6 deletions

View File

@ -1,6 +1,8 @@
package tomo package tomo
import "errors" import "errors"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/elements"
// Backend represents a connection to a display server, or something similar. // Backend represents a connection to a display server, or something similar.
// It is capable of managing an event loop, and creating windows. // It is capable of managing an event loop, and creating windows.
@ -19,13 +21,13 @@ type Backend interface {
// NewWindow creates a new window with the specified width and height, // NewWindow creates a new window with the specified width and height,
// and returns a struct representing it that fulfills the Window // and returns a struct representing it that fulfills the Window
// interface. // interface.
NewWindow (width, height int) (window Window, err error) NewWindow (width, height int) (window elements.Window, err error)
// Copy puts data into the clipboard. // Copy puts data into the clipboard.
Copy (Data) Copy (data.Data)
// Paste returns the data currently in the clipboard. // Paste returns the data currently in the clipboard.
Paste (accept []Mime) (Data) Paste (accept []data.Mime) (data.Data)
} }
// BackendFactory represents a function capable of constructing a backend // BackendFactory represents a function capable of constructing a backend

90
canvas/canvas.go Normal file
View File

@ -0,0 +1,90 @@
package canvas
import "image"
import "image/draw"
import "image/color"
// Canvas is like draw.Image but is also able to return a raw pixel buffer for
// more efficient drawing. This interface can be easily satisfied using a
// BasicCanvas struct.
type Canvas interface {
draw.Image
Buffer () (data []color.RGBA, stride int)
}
// BasicCanvas is a general purpose implementation of tomo.Canvas.
type BasicCanvas struct {
pix []color.RGBA
stride int
rect image.Rectangle
}
// NewBasicCanvas creates a new basic canvas with the specified width and
// height, allocating a buffer for it.
func NewBasicCanvas (width, height int) (canvas BasicCanvas) {
canvas.pix = make([]color.RGBA, height * width)
canvas.stride = width
canvas.rect = image.Rect(0, 0, width, height)
return
}
// you know what it do
func (canvas BasicCanvas) Bounds () (bounds image.Rectangle) {
return canvas.rect
}
// you know what it do
func (canvas BasicCanvas) At (x, y int) (color.Color) {
if !image.Pt(x, y).In(canvas.rect) { return nil }
return canvas.pix[x + y * canvas.stride]
}
// you know what it do
func (canvas BasicCanvas) ColorModel () (model color.Model) {
return color.RGBAModel
}
// you know what it do
func (canvas BasicCanvas) Set (x, y int, c color.Color) {
if !image.Pt(x, y).In(canvas.rect) { return }
r, g, b, a := c.RGBA()
canvas.pix[x + y * canvas.stride] = color.RGBA {
R: uint8(r >> 8),
G: uint8(g >> 8),
B: uint8(b >> 8),
A: uint8(a >> 8),
}
}
// you know what it do
func (canvas BasicCanvas) Buffer () (data []color.RGBA, stride int) {
return canvas.pix, canvas.stride
}
// Reallocate efficiently reallocates the canvas. The data within will be
// garbage. This method will do nothing if this is a cut image.
func (canvas *BasicCanvas) Reallocate (width, height int) {
if canvas.rect.Min != (image.Point { }) { return }
previousLen := len(canvas.pix)
newLen := width * height
bigger := newLen > previousLen
smaller := newLen < previousLen / 2
if bigger || smaller {
canvas.pix = make (
[]color.RGBA,
((height * width) / 4096) * 4096 + 4096)
}
canvas.stride = width
canvas.rect = image.Rect(0, 0, width, height)
}
// Cut returns a sub-canvas of a given canvas.
func Cut (canvas Canvas, bounds image.Rectangle) (reduced BasicCanvas) {
// println(canvas.Bounds().String(), bounds.String())
bounds = bounds.Intersect(canvas.Bounds())
if bounds.Empty() { return }
reduced.rect = bounds
reduced.pix, reduced.stride = canvas.Buffer()
return
}

20
data/data.go Normal file
View File

@ -0,0 +1,20 @@
package data
import "io"
// Data represents arbitrary polymorphic data that can be used for data transfer
// between applications.
type Data map[Mime] io.ReadCloser
// 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
}
var MimePlain = Mime { "text", "plain" }
var MimeFile = Mime { "text", "uri-list" }

159
elements/element.go Normal file
View File

@ -0,0 +1,159 @@
package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// Element represents a basic on-screen object.
type Element interface {
// Element must implement the Canvas interface. Elements should start
// out with a completely blank buffer, and only allocate memory and draw
// on it for the first time when sent an EventResize event.
canvas.Canvas
// MinimumSize specifies the minimum amount of pixels this element's
// width and height may be set to. If the element is given a resize
// event with dimensions smaller than this, it will use its minimum
// instead of the offending dimension(s).
MinimumSize () (width, height int)
// DrawTo sets this element's canvas. This should only be called by the
// parent element. This is typically a region of the parent element's
// canvas.
DrawTo (canvas canvas.Canvas)
// OnDamage sets a function to be called when an area of the element is
// drawn on and should be pushed to the screen.
OnDamage (callback func (region canvas.Canvas))
// OnMinimumSizeChange sets a function to be called when the element's
// minimum size is changed.
OnMinimumSizeChange (callback func ())
}
// Focusable represents an element that has keyboard navigation support. This
// includes inputs, buttons, sliders, etc. as well as any elements that have
// children (so keyboard navigation events can be propagated downward).
type Focusable interface {
Element
// Focused returns whether or not this element is currently focused.
Focused () (selected bool)
// Focus focuses this element, if its parent element grants the
// request.
Focus ()
// HandleFocus causes this element to mark itself as focused. If the
// element does not have children, it is disabled, or there are no more
// selectable children in the given direction, it should return false
// and do nothing. Otherwise, it should select itself and any children
// (if applicable) and return true.
HandleFocus (direction input.KeynavDirection) (accepted bool)
// HandleDeselection causes this element to mark itself and all of its
// children as unfocused.
HandleUnfocus ()
// OnFocusRequest sets a function to be called when this element wants
// its parent element to focus it. Parent elements should return true if
// the request was granted, and false if it was not.
OnFocusRequest (func () (granted bool))
// OnFocusMotionRequest sets a function to be called when this
// element wants its parent element to focus the element behind or in
// front of it, depending on the specified direction. Parent elements
// should return true if the request was granted, and false if it was
// not.
OnFocusMotionRequest (func (direction input.KeynavDirection) (granted bool))
}
// KeyboardTarget represents an element that can receive keyboard input.
type KeyboardTarget interface {
Element
// HandleKeyDown is called when a key is pressed down or repeated while
// this element has keyboard focus. It is important to note that not
// every key down event is guaranteed to be paired with exactly one key
// up event. This is the reason a list of modifier keys held down at the
// time of the key press is given.
HandleKeyDown (key input.Key, modifiers input.Modifiers)
// HandleKeyUp is called when a key is released while this element has
// keyboard focus.
HandleKeyUp (key input.Key, modifiers input.Modifiers)
}
// MouseTarget represents an element that can receive mouse events.
type MouseTarget interface {
Element
// Each of these handler methods is passed the position of the mouse
// cursor at the time of the event as x, y.
// HandleMouseDown is called when a mouse button is pressed down on this
// element.
HandleMouseDown (x, y int, button input.Button)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on this element.
HandleMouseUp (x, y int, button input.Button)
// HandleMouseMove is called when the mouse is moved over this element,
// or the mouse is moving while being held down and originally pressed
// down on this element.
HandleMouseMove (x, y int)
// HandleScroll is called when the mouse is scrolled. The X and Y
// direction of the scroll event are passed as deltaX and deltaY.
HandleMouseScroll (x, y int, deltaX, deltaY float64)
}
// Flexible represents an element who's preferred minimum height can change in
// response to its width.
type Flexible interface {
Element
// FlexibleHeightFor returns what the element's minimum height would be
// if resized to a specified width. This does not actually alter the
// state of the element in any way, but it may perform significant work,
// so it should be called sparingly.
//
// It is reccomended that parent containers check for this interface and
// take this method's value into account in order to support things like
// flow layouts and text wrapping, but it is not absolutely necessary.
// The element's MinimumSize method will still return the absolute
// minimum size that the element may be resized to.
//
// It is important to note that if a parent container checks for
// flexible chilren, it itself will likely need to be flexible.
FlexibleHeightFor (width int) (height int)
// OnFlexibleHeightChange sets a function to be called when the
// parameters affecting this element's flexible height are changed.
OnFlexibleHeightChange (callback func ())
}
// Scrollable represents an element that can be scrolled. It acts as a viewport
// through which its contents can be observed.
type Scrollable interface {
Element
// ScrollContentBounds returns the full content size of the element.
ScrollContentBounds () (bounds image.Rectangle)
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
ScrollViewportBounds () (bounds image.Rectangle)
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
ScrollTo (position image.Point)
// ScrollAxes returns the supported axes for scrolling.
ScrollAxes () (horizontal, vertical bool)
// OnScrollBoundsChange sets a function to be called when the element's
// ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed.
OnScrollBoundsChange (callback func ())
}

39
elements/window.go Normal file
View File

@ -0,0 +1,39 @@
package elements
import "image"
// 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 {
// Adopt sets the root element of the window. There can only be one of
// these at one time.
Adopt (child Element)
// Child returns the root element of the window.
Child () (child Element)
// SetTitle sets the title that appears on the window's title bar. This
// method might have no effect with some backends.
SetTitle (title string)
// SetIcon taks in a list different sizes of the same icon and selects
// the best one to display on the window title bar, dock, or whatever is
// applicable for the given backend. This method might have no effect
// for some backends.
SetIcon (sizes []image.Image)
// Show shows the window. The window starts off hidden, so this must be
// called after initial setup to make sure it is visible.
Show ()
// Hide hides the window.
Hide ()
// Close closes the window.
Close ()
// OnClose specifies a function to be called when the window is closed.
OnClose (func ())
}

131
input/input.go Normal file
View File

@ -0,0 +1,131 @@
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. 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
}
// KeynavDirection represents a keyboard navigation direction.
type KeynavDirection int
const (
KeynavDirectionNeutral KeynavDirection = 0
KeynavDirectionBackward KeynavDirection = -1
KeynavDirectionForward KeynavDirection = 1
)
// Canon returns a well-formed direction.
func (direction KeynavDirection) Canon () (canon KeynavDirection) {
if direction > 0 {
return KeynavDirectionForward
} else if direction == 0 {
return KeynavDirectionNeutral
} else {
return KeynavDirectionBackward
}
}

37
layouts/layout.go Normal file
View File

@ -0,0 +1,37 @@
package layouts
import "image"
import "git.tebibyte.media/sashakoshka/tomo/elements"
// LayoutEntry associates an element with layout and positioning information so
// it can be arranged by a Layout.
type LayoutEntry struct {
elements.Element
Bounds image.Rectangle
Expand bool
}
// Layout is capable of arranging elements within a container. It is also able
// to determine the minimum amount of room it needs to do so.
type Layout interface {
// Arrange takes in a slice of entries and a bounding width and height,
// and changes the position of the entiries in the slice so that they
// are properly laid out. The given width and height should not be less
// than what is returned by MinimumSize.
Arrange (entries []LayoutEntry, margin int, bounds image.Rectangle)
// MinimumSize returns the minimum width and height that the layout
// needs to properly arrange the given slice of layout entries.
MinimumSize (entries []LayoutEntry, margin int) (width, height int)
// FlexibleHeightFor Returns the minimum height the layout needs to lay
// out the specified elements at the given width, taking into account
// flexible elements.
FlexibleHeightFor (
entries []LayoutEntry,
margin int,
squeeze int,
) (
height int,
)
}

View File

@ -1,6 +1,8 @@
package tomo package tomo
import "errors" import "errors"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/elements"
var backend Backend var backend Backend
@ -32,7 +34,7 @@ func Do (callback func ()) {
// Window. If the window could not be created, an error is returned explaining // 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 // why. If this function is called without a running backend, an error is
// returned as well. // returned as well.
func NewWindow (width, height int) (window Window, err error) { func NewWindow (width, height int) (window elements.Window, err error) {
if backend == nil { if backend == nil {
err = errors.New("no backend is running.") err = errors.New("no backend is running.")
return return
@ -41,14 +43,14 @@ func NewWindow (width, height int) (window Window, err error) {
} }
// Copy puts data into the clipboard. // Copy puts data into the clipboard.
func Copy (data Data) { func Copy (data data.Data) {
if backend == nil { panic("no backend is running") } if backend == nil { panic("no backend is running") }
backend.Copy(data) backend.Copy(data)
} }
// Paste returns the data currently in the clipboard. This method may // Paste returns the data currently in the clipboard. This method may
// return nil. // return nil.
func Paste (accept []Mime) (Data) { func Paste (accept []data.Mime) (data.Data) {
if backend == nil { panic("no backend is running") } if backend == nil { panic("no backend is running") }
return backend.Paste(accept) return backend.Paste(accept)
} }