From 04d2ea4767cecaaad49ee12248eae9c48074ca9a Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 2 Feb 2023 01:47:01 -0500 Subject: [PATCH] Atomized the functionality of the base tomo package --- backend.go | 8 ++- canvas/canvas.go | 90 +++++++++++++++++++++++++ data/data.go | 20 ++++++ elements/element.go | 159 ++++++++++++++++++++++++++++++++++++++++++++ elements/window.go | 39 +++++++++++ input/input.go | 131 ++++++++++++++++++++++++++++++++++++ layouts/layout.go | 37 +++++++++++ tomo.go | 8 ++- 8 files changed, 486 insertions(+), 6 deletions(-) create mode 100644 canvas/canvas.go create mode 100644 data/data.go create mode 100644 elements/element.go create mode 100644 elements/window.go create mode 100644 input/input.go create mode 100644 layouts/layout.go diff --git a/backend.go b/backend.go index 6ad3f70..3b1ff31 100644 --- a/backend.go +++ b/backend.go @@ -1,6 +1,8 @@ package tomo 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. // 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, // and returns a struct representing it that fulfills the Window // 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 (Data) + Copy (data.Data) // 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 diff --git a/canvas/canvas.go b/canvas/canvas.go new file mode 100644 index 0000000..cb2da2a --- /dev/null +++ b/canvas/canvas.go @@ -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 +} diff --git a/data/data.go b/data/data.go new file mode 100644 index 0000000..8055711 --- /dev/null +++ b/data/data.go @@ -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" } diff --git a/elements/element.go b/elements/element.go new file mode 100644 index 0000000..f97a48c --- /dev/null +++ b/elements/element.go @@ -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 ()) +} diff --git a/elements/window.go b/elements/window.go new file mode 100644 index 0000000..618d795 --- /dev/null +++ b/elements/window.go @@ -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 ()) +} diff --git a/input/input.go b/input/input.go new file mode 100644 index 0000000..b523829 --- /dev/null +++ b/input/input.go @@ -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 + } +} diff --git a/layouts/layout.go b/layouts/layout.go new file mode 100644 index 0000000..1e44f9b --- /dev/null +++ b/layouts/layout.go @@ -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, + ) +} diff --git a/tomo.go b/tomo.go index 5dc4dde..b1416b0 100644 --- a/tomo.go +++ b/tomo.go @@ -1,6 +1,8 @@ package tomo import "errors" +import "git.tebibyte.media/sashakoshka/tomo/data" +import "git.tebibyte.media/sashakoshka/tomo/elements" var backend Backend @@ -32,7 +34,7 @@ func Do (callback func ()) { // 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) { +func NewWindow (width, height int) (window elements.Window, err error) { if backend == nil { err = errors.New("no backend is running.") return @@ -41,14 +43,14 @@ func NewWindow (width, height int) (window Window, err error) { } // Copy puts data into the clipboard. -func Copy (data Data) { +func Copy (data data.Data) { if backend == nil { panic("no backend is running") } backend.Copy(data) } // Paste returns the data currently in the clipboard. This method may // return nil. -func Paste (accept []Mime) (Data) { +func Paste (accept []data.Mime) (data.Data) { if backend == nil { panic("no backend is running") } return backend.Paste(accept) }