Compare commits

...

31 Commits

Author SHA1 Message Date
f99d9e0d2a Upgrade x/image 2023-08-09 20:55:46 -04:00
c785fb461c Colors now operate more sensibly 2023-08-09 15:13:19 -04:00
487471d7a9 Add colors 2023-08-09 12:08:17 -04:00
2f5421a5c9 Add Role constructor 2023-08-08 03:00:41 -04:00
d0f7047fcf Fix package import error in theme 2023-08-07 21:57:50 -04:00
Sasha Koshka
e63ebdb89e Add MultiCookie to make theming easier 2023-08-07 21:56:28 -04:00
Sasha Koshka
9d40ab654a Clarfied wording in Role.Object 2023-08-07 21:51:05 -04:00
Sasha Koshka
522ff64fd3 Added a theme package 2023-08-07 21:49:11 -04:00
Sasha Koshka
e14bd81c04 Add SetDotColor method 2023-08-07 21:09:58 -04:00
dc377c36a5 Add function keys up to F24 2023-08-02 01:37:46 -04:00
2b99a98a8e Add a ton more doc comments 2023-08-02 01:34:07 -04:00
d1b62f5560 Add dot manipulation to TextBox 2023-07-20 00:14:15 -04:00
85fe5ac65b Added text manipulation 2023-07-19 23:58:27 -04:00
14fc0ba372 Add more doc comments 2023-07-18 21:49:36 -04:00
9f4e8a539a The Do function is now thread safe 2023-07-16 01:06:24 -04:00
1cb3be8de8 Added a global Do function 2023-07-16 00:33:44 -04:00
32e58ce63d Container event propagation can be disabled 2023-07-13 12:53:08 -04:00
4dbd86cec3 ContainerBox can now be aligned as well 2023-07-13 03:00:50 -04:00
573212fe7d Add support for text wrapping 2023-07-12 18:56:04 -04:00
ae5c177484 Minimum sizes make more sense now 2023-07-12 01:45:08 -04:00
facd85ef21 Add SetText method to TextBox 2023-07-08 23:49:51 -04:00
ec967fe4f8 Better way of representing layout hints 2023-07-05 17:44:08 -04:00
9efeaef8a8 Revised layout interface 2023-07-05 04:21:17 -04:00
d6baf82a94 Removed the Gap type 2023-07-05 04:20:56 -04:00
Sasha Koshka
0824aca4ba Added shatter algorithm to canvas 2023-07-05 03:14:34 -04:00
ec7596fa2d FuncBroadcaster actually broadcasts lol 2023-07-05 00:45:32 -04:00
66e1caeebf Reorganized the box interfaces somewhat 2023-07-02 02:46:12 -04:00
8b78d16506 Add window behavior to box 2023-07-02 00:31:05 -04:00
ac1fe7a75a Add methods for working with insets 2023-07-01 19:23:33 -04:00
78aa13f388 Embed Broadcaster in FuncBroadcaster 2023-07-01 19:08:39 -04:00
Sasha Koshka
3e92de7485 Make broadcaster generic 2023-07-01 19:00:26 -04:00
12 changed files with 796 additions and 70 deletions

View File

@@ -4,18 +4,26 @@ import "sort"
import "image"
import "errors"
// Backend is any Tomo implementation. Backends handle window creation, layout,
// rendering, and events so that there can be as many platform-specific
// optimizations as possible.
type Backend interface {
// These methods create new objects. The backend must reject any object
// that was not made by it.
NewWindow (image.Rectangle) (MainWindow, error)
NewBox () Box
NewTextBox () TextBox
NewCanvasBox () CanvasBox
NewContainerBox () ContainerBox
// Run runs the event loop until Stop() is called, or the backend
// experiences a fatal error.
Run () error
// Stop must unblock run.
Run () error
Stop ()
// Do performs a callback function in the main thread as soon as
// Do performs a callback function in the event loop thread as soon as
// possible. This method must be safe to call concurrently.
Do (func ())
}

View File

@@ -8,24 +8,24 @@ import "image/color"
// Cap represents a stroke cap type.
type Cap int; const (
CapButt Cap = iota
CapRound
CapSquare
CapButt Cap = iota // Square cap that ends at the point
CapRound // Round cap that surrounds the point
CapSquare // square cap that surrounds the point
)
// Joint represents a stroke joint type.
type Joint int; const (
JointRount Joint = iota
JointSharp
JointMiter
JointRount Joint = iota // Rounded joint
JointSharp // Sharp joint
JointMiter // Clipped/beveled joint
)
// StrokeAlign determines whether a stroke is drawn inside, outside, or on a
// path.
type StrokeAlign int; const (
StrokeAlignCenter StrokeAlign = iota
StrokeAlignInner
StrokeAlignOuter
StrokeAlignCenter StrokeAlign = iota // Centered on the path
StrokeAlignInner // Inset into the path
StrokeAlignOuter // Outset around the path
)
// Pen represents a drawing context that is linked to a canvas. Each canvas can
@@ -44,9 +44,8 @@ type Pen interface {
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)
Stroke (color.Color) // Sets the stroke to a solid color
Fill (color.Color) // Sets the fill to a solid color
}
// Canvas is an image that supports drawing paths.
@@ -62,6 +61,7 @@ type Canvas interface {
// Drawer is an object that can draw to a canvas.
type Drawer interface {
// Draw draws to the given canvas.
Draw (Canvas)
}

96
canvas/shatter.go Normal file
View File

@@ -0,0 +1,96 @@
package canvas
import "image"
// Shatter takes in a bounding rectangle, and several rectangles to be
// subtracted from it. It returns a slice of rectangles that tile together to
// make up the difference between them. This is intended to be used for figuring
// out which areas of a container box's background are covered by other boxes so
// it doesn't waste CPU cycles drawing to those areas.
func Shatter (
glass image.Rectangle,
rocks ...image.Rectangle,
) (
tiles []image.Rectangle,
) {
// in this function, the metaphor of throwing several rocks at a sheet
// of glass is used to illustrate the concept.
tiles = []image.Rectangle { glass }
for _, rock := range rocks {
// check each tile to see if the rock has collided with it
tileLen := len(tiles)
for tileIndex := 0; tileIndex < tileLen; tileIndex ++ {
tile := tiles[tileIndex]
if !rock.Overlaps(tile) { continue }
newTiles, n := shatterOnce(tile, rock)
if n > 0 {
// the tile was shattered into one or more sub
// tiles
tiles[tileIndex] = newTiles[0]
tiles = append(tiles, newTiles[1:n]...)
} else {
// the tile was entirely obscured by the rock
// and must be wholly removed
tiles = remove(tiles, tileIndex)
tileIndex --
tileLen --
}
}
}
return
}
func shatterOnce (glass, rock image.Rectangle) (tiles [4]image.Rectangle, n int) {
rock = rock.Intersect(glass)
// |'''''''''''|
// | |
// |###|'''| |
// |###|___| |
// | |
// |___________|
if rock.Min.X > glass.Min.X { tiles[n] = image.Rect (
glass.Min.X, rock.Min.Y,
rock.Min.X, rock.Max.Y,
); n ++ }
// |'''''''''''|
// | |
// | |'''|###|
// | |___|###|
// | |
// |___________|
if rock.Max.X < glass.Max.X { tiles[n] = image.Rect (
rock.Max.X, rock.Min.Y,
glass.Max.X, rock.Max.Y,
); n ++ }
// |###########|
// |###########|
// | |'''| |
// | |___| |
// | |
// |___________|
if rock.Min.Y > glass.Min.Y { tiles[n] = image.Rect (
glass.Min.X, glass.Min.Y,
glass.Max.X, rock.Min.Y,
); n ++ }
// |'''''''''''|
// | |
// | |'''| |
// | |___| |
// |###########|
// |###########|
if rock.Max.Y < glass.Max.Y { tiles[n] = image.Rect (
glass.Min.X, rock.Max.Y,
glass.Max.X, glass.Max.Y,
); n ++ }
return
}
func remove[ELEMENT any] (slice []ELEMENT, s int) []ELEMENT {
return append(slice[:s], slice[s + 1:]...)
}

View File

@@ -27,7 +27,10 @@ func (mime Mime) String () string {
return mime.Type + "/" + mime.Subtype
}
// MimePlain returns the MIME type of plain text.
func MimePlain () Mime { return Mime { "text", "plain" } }
// MimeFile returns the MIME type of a file path/URI.
func MimeFile () Mime { return Mime { "text", "uri-list" } }
type byteReadCloser struct { *bytes.Reader }

View File

@@ -9,46 +9,77 @@ type Cookie interface {
Close ()
}
// Broadcaster manages event listeners
type Broadcaster struct {
// Broadcaster manages event listeners.
type Broadcaster[L any] struct {
lastID int
listeners map[int] func ()
listeners map[int] L
}
// 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 ())
}
func (broadcaster *Broadcaster[L]) Connect (listener L) Cookie {
broadcaster.ensure()
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()
}
// Listeners returns a map of all connected listeners.
func (broadcaster *Broadcaster[L]) Listeners () map[int] L {
broadcaster.ensure()
return broadcaster.listeners
}
func (broadcaster *Broadcaster) newCookie () cookie {
func (broadcaster *Broadcaster[L]) newCookie () cookie[L] {
broadcaster.lastID ++
return cookie {
return cookie[L] {
id: broadcaster.lastID,
broadcaster: broadcaster,
}
}
type cookie struct {
id int
broadcaster *Broadcaster
func (broadcaster *Broadcaster[L]) ensure () {
if broadcaster.listeners == nil {
broadcaster.listeners = make(map[int] L)
}
}
func (cookie cookie) Close () {
// NoCookie is a cookie that does nothing when closed.
type NoCookie struct { }
func (NoCookie) Close () { }
type cookie[L any] struct {
id int
broadcaster *Broadcaster[L]
}
func (cookie cookie[L]) Close () {
delete(cookie.broadcaster.listeners, cookie.id)
}
// FuncBroadcaster is a broadcaster that manages functions with no arguments.
type FuncBroadcaster struct {
Broadcaster[func ()]
}
// Broadcast calls all connected listener funcs.
func (broadcaster *FuncBroadcaster) Broadcast () {
for _, listener := range broadcaster.Listeners() {
listener()
}
}
type multiCookie []Cookie
// MultiCookie creates a single cookie that, when closed, closes a list of other
// cookies.
func MultiCookie (cookies ...Cookie) Cookie {
return multiCookie(cookies)
}
func (cookies multiCookie) Close () {
for _, cookie := range cookies {
cookie.Close()
}
}

2
go.mod
View File

@@ -2,4 +2,4 @@ module git.tebibyte.media/tomo/tomo
go 1.20
require golang.org/x/image v0.8.0
require golang.org/x/image v0.11.0

3
go.sum
View File

@@ -3,6 +3,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
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/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
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=
@@ -26,6 +28,7 @@ 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/text v0.12.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=

View File

@@ -59,6 +59,18 @@ const (
KeyF10 Key = 138
KeyF11 Key = 139
KeyF12 Key = 140
KeyF13 Key = 141
KeyF14 Key = 142
KeyF15 Key = 143
KeyF16 Key = 144
KeyF17 Key = 145
KeyF18 Key = 146
KeyF19 Key = 147
KeyF20 Key = 148
KeyF21 Key = 149
KeyF22 Key = 150
KeyF23 Key = 151
KeyF24 Key = 152
)
// Button represents a mouse button.

309
object.go
View File

@@ -3,19 +3,97 @@ package tomo
import "image"
import "image/color"
import "golang.org/x/image/font"
import "git.tebibyte.media/tomo/tomo/text"
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
// Side represents one side of a rectangle.
type Side int; const (
SideTop Side = iota
SideRight
SideBottom
SideLeft
)
// Inset represents a rectangle inset that can have a different value for each
// side.
type Inset [4]int
// I allows you to create an inset in a CSS-ish way:
//
// - One argument: all sides are set to this value
// - Two arguments: the top and bottom sides are set to the first value, and
// the left and right sides are set to the second value.
// - Three arguments: the top side is set by the first value, the left and
// right sides are set by the second vaue, and the bottom side is set by the
// third value.
// - Four arguments: each value corresponds to a side.
//
// This function will panic if an argument count that isn't one of these is
// given.
func I (sides ...int) Inset {
switch len(sides) {
case 1: return Inset { sides[0], sides[0], sides[0], sides[0] }
case 2: return Inset { sides[0], sides[1], sides[0], sides[1] }
case 3: return Inset { sides[0], sides[1], sides[2], sides[1] }
case 4: return Inset { sides[0], sides[1], sides[2], sides[3] }
default: panic("I: illegal argument count.")
}
}
// Apply returns the given rectangle, shrunk on all four sides by the given
// inset. If a measurment of the inset is negative, that side will instead be
// expanded outward. If the rectangle's dimensions cannot be reduced any
// further, an empty rectangle near its center will be returned.
func (inset Inset) Apply (bigger image.Rectangle) (smaller image.Rectangle) {
smaller = bigger
if smaller.Dx() < inset[3] + inset[1] {
smaller.Min.X = (smaller.Min.X + smaller.Max.X) / 2
smaller.Max.X = smaller.Min.X
} else {
smaller.Min.X += inset[3]
smaller.Max.X -= inset[1]
}
if smaller.Dy() < inset[0] + inset[2] {
smaller.Min.Y = (smaller.Min.Y + smaller.Max.Y) / 2
smaller.Max.Y = smaller.Min.Y
} else {
smaller.Min.Y += inset[0]
smaller.Max.Y -= inset[2]
}
return
}
// Inverse returns a negated version of the inset.
func (inset Inset) Inverse () (prime Inset) {
return Inset {
inset[0] * -1,
inset[1] * -1,
inset[2] * -1,
inset[3] * -1,
}
}
// Horizontal returns the sum of SideRight and SideLeft.
func (inset Inset) Horizontal () int {
return inset[SideRight] + inset[SideLeft]
}
// Vertical returns the sum of SideTop and SideBottom.
func (inset Inset) Vertical () int {
return inset[SideTop] + inset[SideBottom]
}
// Border represents a single border of a box.
type Border struct {
Width Inset
Color [4]color.Color
}
// Align lists basic alignment types.
type Align int; const (
AlignStart Align = iota // similar to left-aligned text
AlignMiddle // similar to center-aligned text
@@ -23,29 +101,76 @@ type Align int; const (
AlignEven // similar to justified text
)
// Object is any obscreen object. Each object must be linked to a box, even if
// it is that box.
type Object interface {
Box () Box
}
// Box is a basic styled box.
type Box interface {
Object
// Window returns the Window this Box is a part of.
Window () Window
// Bounds returns the outer bounding rectangle of the Box relative to
// the Window.
Bounds () image.Rectangle
// InnerBounds returns the inner bounding rectangle of the box. It is
// the value of Bounds inset by the Box's border and padding.
InnerBounds () image.Rectangle
// MinimumSize returns the minimum width and height this Box's bounds
// can be set to. This will return the value of whichever of these is
// greater:
// - The size as set by SetMinimumSize
// - The size taken up by the Box's border and padding. If there is
// internal content that does not overflow, the size of that is also
// taken into account here.
MinimumSize () image.Point
// SetBounds sets the bounding rectangle of this Box relative to the
// Window.
SetBounds (image.Rectangle)
// SetColor sets the background color of this Box.
SetColor (color.Color)
// SetBorder sets the Border(s) of the box. The first Border will be the
// most outset, and the last Border will be the most inset.
SetBorder (...Border)
SetMinimumSize (int, int)
// SetMinimumSize sets the minimum width and height of the box, as
// described in MinimumSize.
SetMinimumSize (image.Point)
// SetPadding sets the padding between the Box's innermost Border and
// its content.
SetPadding (Inset)
// SetDNDData sets the data that will be picked up if this Box is
// dragged. If this is nil (which is the default), this Box will not be
// picked up.
SetDNDData (data.Data)
// SetDNDAccept sets the type of data that can be dropped onto this Box.
// If this is nil (which is the default), this Box will reject all
// drops.
SetDNDAccept (...data.Mime)
// SetFocused sets whether or not this Box has keyboard focus. If set to
// true, this method will steal focus away from whichever Object
// currently has focus.
SetFocused (bool)
// SetFocusable sets whether or not this Box can receive keyboard focus.
// If set to false and the Box is already focused. the focus is removed.
SetFocusable (bool)
Focused () bool
Modifiers (func ()) input.Modifiers
MousePosition (func ()) image.Point
// Focused returns whether or not this Box has keyboard focus.
Focused () bool
// Modifiers returns which modifier keys on the keyboard are currently
// being held down.
Modifiers () input.Modifiers
// MousePosition returns the position of the mouse pointer relative to
// the Window.
MousePosition () image.Point
// These are event subscription functions that allow callbacks to be
// connected to particular events. Multiple callbacks may be connected
// to the same event at once. Callbacks can be removed by closing the
// returned cookie.
OnFocusEnter (func ()) event.Cookie
OnFocusLeave (func ()) event.Cookie
OnDNDEnter (func ()) event.Cookie
@@ -59,58 +184,172 @@ type Box interface {
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)
}
// CanvasBox is a box that can be drawn to.
type CanvasBox interface {
Box
// SetDrawer sets the Drawer that will be called upon to draw the Box's
// content when it is invalidated.
SetDrawer (canvas.Drawer)
// Invalidate causes the Box's area to be redrawn at the end of the
// event cycle, even if it wouldn't be otherwise.
Invalidate ()
}
type ContainerBox interface {
// ContentBox is an abstract box that has some kind of content. Its only purpose
// is to be embedded into TextBox and ContainerBox.
type ContentBox interface {
Box
SetGap (Gap)
Add (Object)
Delete (Object)
Insert (child Object, before Object)
Clear ()
Length () int
At (int) Object
SetLayout (Layout)
// SetOverflow sets whether or not the Box's content overflows
// horizontally and vertically. Overflowing content is clipped to the
// bounds of the Box inset by all Borders (but not padding).
SetOverflow (horizontal, vertical bool)
// SetAlign sets how the Box's content is distributed horizontally and
// vertically.
SetAlign (x, y Align)
// ContentBounds returns the bounds of the inner content of the Box
// relative to the window.
ContentBounds () image.Rectangle
// ScrollTo shifts the origin of the Box's content to the origin of the
// Box's InnerBounds, offset by the given point.
ScrollTo (image.Point)
// OnContentBoundsChange specifies a function to be called when the
// Box's ContentBounds or InnerBounds changes.
OnContentBoundsChange (func ()) event.Cookie
}
// TextBox is a box that contains text content.
type TextBox interface {
ContentBox
// SetText sets the text content of the Box.
SetText (string)
// SetTextColor sets the text color.
SetTextColor (color.Color)
// SetFace sets the font face text is rendered in.
SetFace (font.Face)
// SetWrap sets whether or not the text wraps.
SetWrap (bool)
// SetSelectable sets whether or not the text content can be
// highlighted/selected.
SetSelectable (bool)
// SetDotColor sets the highlight color of selected text.
SetDotColor (color.Color)
// Select sets the text cursor or selection.
Select (text.Dot)
// Dot returns the text cursor or selection.
Dot () text.Dot
// OnDotChange specifies a function to be called when the text cursor or
// selection changes.
OnDotChange (func ()) event.Cookie
}
// ContentBox is a box that can contain child Objects. It arranges them
// according to a layout rule.
type ContainerBox interface {
ContentBox
// SetPropagateEvents specifies whether or not child Objects will
// receive user input events. It is true by default. If it is false, all
// user input that would otherwise be directed to a child Box is
// directed to this Box.
SetPropagateEvents (bool)
// SetGap sets the gap between child Objects.
SetGap (image.Point)
// Add appends a child Object.
Add (Object)
// Delete removes a child Object, if it is a child of this Box.
Delete (Object)
// Insert inserts a child Object before a specified Object. If the
// before Object is nil or is not contained within this Box, the
// inserted Object is appended.
Insert (child Object, before Object)
// Clear removes all child Objects.
Clear ()
// Length returns the amount of child objects.
Length () int
// At returns the child Object at the specified index.
At (int) Object
// SetLayout sets the layout of this Box. Child Objects will be
// positioned according to it.
SetLayout (Layout)
}
// LayoutHints are passed to a layout to tell it how to arrange child boxes.
type LayoutHints struct {
// Bounds is the bounding rectangle that children should be placed
// within. Any padding values are already applied.
Bounds image.Rectangle
// OverflowX and OverflowY control wether child Boxes may be positioned
// outside of Bounds.
OverflowX bool
OverflowY bool
// AlignX and AlignY control how child Boxes are aligned horizontally
// and vertically. The effect of this may vary depending on the Layout.
AlignX Align
AlignY Align
// Gap controls the amount of horizontal and vertical spacing in-between
// child Boxes.
Gap image.Point
}
// A Layout can be given to a ContainerBox to arrange child objects.
type Layout interface {
// MinimumSize returns the minimum width and height of
// LayoutHints.Bounds needed to properly lay out all child Boxes.
MinimumSize (LayoutHints, []Box) image.Point
// Arrange arranges child boxes according to the given LayoutHints.
Arrange (LayoutHints, []Box)
}
// Window is an operating system window. It can contain one object.
type Window interface {
// SetRoot sets the root child of the window. There can only be one at
// a time, and setting it will remove the current child if there is one.
SetRoot (Object)
// SetTitle sets the title of the window.
SetTitle (string)
SetIcon (sizes []image.Image)
NewMenu (image.Rectangle) (Window, error)
NewModal (image.Rectangle) (Window, error)
// SetIcon sets the icon of the window. When multiple icon sizes are
// provided, the best fitting one is chosen for display.
SetIcon (... image.Image)
// Widget returns a window representing a smaller iconified form of this
// window. How exactly this window is used depends on the platform.
// Subsequent calls to this method on the same window will return the
// same window object.
Widget () (Window, error)
// NewMenu creates a new menu window. This window is undecorated and
// will close once the user clicks outside of it.
NewMenu (image.Rectangle) (Window, error)
// NewModal creates a new modal window that blocks all input to this
// window until it is closed.
NewModal (image.Rectangle) (Window, error)
// Copy copies data to the clipboard.
Copy (data.Data)
// Paste reads data from the clipboard. When the data is available or an
// error has occurred, the provided function will be called.
Paste (callback func (data.Data, error), accept ...data.Mime)
// Show shows the window.
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 ()) event.Cookie
}
// MainWindow is a top-level operating system window.
type MainWindow interface {
Window
// NewChild creates a new window that is semantically a child of this
// window. It does not actually reside within this window, but it may be
// linked to it via some other means. This is intended for things like
// toolboxes and tear-off menus.
NewChild (image.Rectangle) (Window, error)
}
type Layout interface {
Arrange (image.Rectangle, Gap, []Box)
}

248
text/text.go Normal file
View File

@@ -0,0 +1,248 @@
package text
import "unicode"
// Dot represents a cursor or text selection. It has a start and end position,
// referring to where the user began and ended the selection respectively.
type Dot struct { Start, End int }
// EmptyDot returns a zero-width dot at the specified position.
func EmptyDot (position int) Dot {
return Dot { position, position }
}
// Canon places the lesser value at the start, and the greater value at the end.
// Note that a canonized dot does not in all cases correspond directly to the
// original, because there is a semantic value to the start and end positions.
func (dot Dot) Canon () Dot {
if dot.Start > dot.End {
return Dot { dot.End, dot.Start }
} else {
return dot
}
}
// Empty returns whether or not the
func (dot Dot) Empty () bool {
return dot.Start == dot.End
}
// Add shifts the dot to the right by the specified amount.
func (dot Dot) Add (delta int) Dot {
return Dot {
dot.Start + delta,
dot.End + delta,
}
}
// Sub shifts the dot to the left by the specified amount.
func (dot Dot) Sub (delta int) Dot {
return Dot {
dot.Start - delta,
dot.End - delta,
}
}
// Constrain constrains the dot's start and end from zero to length (inclusive).
func (dot Dot) Constrain (length int) Dot {
if dot.Start < 0 { dot.Start = 0 }
if dot.Start > length { dot.Start = length }
if dot.End < 0 { dot.End = 0 }
if dot.End > length { dot.End = length }
return dot
}
// Width returns how many runes the dot spans.
func (dot Dot) Width () int {
dot = dot.Canon()
return dot.End - dot.Start
}
// Slice returns the subset of text that the dot covers.
func (dot Dot) Slice (text []rune) []rune {
dot = dot.Canon().Constrain(len(text))
return text[dot.Start:dot.End]
}
// WordToLeft returns how far away to the left the next word boundary is from a
// given position.
func WordToLeft (text []rune, position int) (length int) {
if position < 1 { return }
if position > len(text) { position = len(text) }
index := position - 1
for index >= 0 && unicode.IsSpace(text[index]) {
length ++
index --
}
for index >= 0 && !unicode.IsSpace(text[index]) {
length ++
index --
}
return
}
// WordToRight returns how far away to the right the next word boundary is from
// a given position.
func WordToRight (text []rune, position int) (length int) {
if position < 0 { return }
if position > len(text) { position = len(text) }
index := position
for index < len(text) && unicode.IsSpace(text[index]) {
length ++
index ++
}
for index < len(text) && !unicode.IsSpace(text[index]) {
length ++
index ++
}
return
}
// WordAround returns a dot that surrounds the word at the specified position.
func WordAround (text []rune, position int) (around Dot) {
return Dot {
position - WordToLeft(text, position),
position + WordToRight(text, position),
}
}
// Backspace deletes the rune to the left of the dot. If word is true, it
// deletes up until the next word boundary on the left. If the dot is non-empty,
// it deletes the text inside of the dot.
func Backspace (text []rune, dot Dot, word bool) (result []rune, moved Dot) {
dot = dot.Constrain(len(text))
if dot.Empty() {
distance := 1
if word {
distance = WordToLeft(text, dot.End)
}
result = append (
result,
text[:dot.Sub(distance).Constrain(len(text)).End]...)
result = append(result, text[dot.End:]...)
moved = EmptyDot(dot.Sub(distance).Start)
return
} else {
return Delete(text, dot, word)
}
}
// Delete deletes the rune to the right of the dot. If word is true, it deletes
// up until the next word boundary on the right. If the dot is non-empty, it
// deletes the text inside of the dot.
func Delete (text []rune, dot Dot, word bool) (result []rune, moved Dot) {
dot = dot.Constrain(len(text))
if dot.Empty() {
distance := 1
if word {
distance = WordToRight(text, dot.End)
}
result = append(result, text[:dot.End]...)
result = append (
result,
text[dot.Add(distance).Constrain(len(text)).End:]...)
moved = dot
return
} else {
dot = dot.Canon()
result = append(result, text[:dot.Start]...)
result = append(result, text[dot.End:]...)
moved = EmptyDot(dot.Start)
return
}
}
// Lift removes the section of text inside of the dot, and returns a copy of it.
func Lift (text []rune, dot Dot) (result []rune, moved Dot, lifted []rune) {
dot = dot.Constrain(len(text))
if dot.Empty() {
moved = dot
return
}
dot = dot.Canon()
lifted = make([]rune, dot.Width())
copy(lifted, dot.Slice(text))
result = append(result, text[:dot.Start]...)
result = append(result, text[dot.End:]...)
moved = EmptyDot(dot.Start)
return
}
// Type inserts one of more runes into the text at the dot position. If the dot
// is non-empty, it replaces the text inside of the dot with the new runes.
func Type (text []rune, dot Dot, characters ...rune) (result []rune, moved Dot) {
dot = dot.Constrain(len(text))
if dot.Empty() {
result = append(result, text[:dot.End]...)
result = append(result, characters...)
if dot.End < len(text) {
result = append(result, text[dot.End:]...)
}
moved = EmptyDot(dot.Add(len(characters)).End)
return
} else {
dot = dot.Canon()
result = append(result, text[:dot.Start]...)
result = append(result, characters...)
result = append(result, text[dot.End:]...)
moved = EmptyDot(dot.Add(len(characters)).Start)
return
}
}
// MoveLeft moves the dot left one rune. If word is true, it moves the dot to
// the next word boundary on the left.
func MoveLeft (text []rune, dot Dot, word bool) (moved Dot) {
dot = dot.Canon().Constrain(len(text))
distance := 0
if dot.Empty() {
distance = 1
}
if word {
distance = WordToLeft(text, dot.Start)
}
moved = EmptyDot(dot.Sub(distance).Start)
return
}
// MoveRight moves the dot right one rune. If word is true, it moves the dot to
// the next word boundary on the right.
func MoveRight (text []rune, dot Dot, word bool) (moved Dot) {
dot = dot.Canon().Constrain(len(text))
distance := 0
if dot.Empty() {
distance = 1
}
if word {
distance = WordToRight(text, dot.End)
}
moved = EmptyDot(dot.Add(distance).End)
return
}
// SelectLeft moves the end of the dot left one rune. If word is true, it moves
// the end of the dot to the next word boundary on the left.
func SelectLeft (text []rune, dot Dot, word bool) (moved Dot) {
dot = dot.Constrain(len(text))
distance := 1
if word {
distance = WordToLeft(text, dot.End)
}
dot.End -= distance
return dot
}
// SelectRight moves the end of the dot right one rune. If word is true, it
// moves the end of the dot to the next word boundary on the right.
func SelectRight (text []rune, dot Dot, word bool) (moved Dot) {
dot = dot.Constrain(len(text))
distance := 1
if word {
distance = WordToRight(text, dot.End)
}
dot.End += distance
return dot
}

66
theme/theme.go Normal file
View File

@@ -0,0 +1,66 @@
package theme
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
// Role describes the role of an object.
type Role struct {
// Package is an optional namespace field. If specified, it should be
// the package name or module name the object is from.
Package string
// Object specifies what type of object it is. For example:
// - TextInput
// - Table
// - Label
// - Dial
// This should correspond directly to the type name of the object.
Object string
}
// R is shorthand for creating a Role structure.
func R (pack, object string) Role {
return Role { Package: pack, Object: object }
}
// Color represents a color ID.
type Color int; const (
ColorBackground Color = iota
ColorForeground
ColorRaised
ColorSunken
ColorAccent
)
// RGBA satisfies the color.Color interface.
func (id Color) RGBA () (r, g, b, a uint32) {
if current == nil { return }
return current.RGBA(id)
}
// Theme is an object that can apply a visual style to different objects.
type Theme interface {
// Apply applies the theme to the given object, according to the given
// role. This may register event listeners with the given object;
// closing the returned cookie will remove them.
Apply (tomo.Object, Role) event.Cookie
// RGBA returns the RGBA values of the corresponding color ID.
RGBA (Color) (r, g, b, a uint32)
}
var current Theme
// SetTheme sets the theme.
func SetTheme (theme Theme) {
current = theme
}
// Apply applies the current theme to the given object, according to the given
// role. This may register event listeners with the given object; closing the
// returned cookie will remove them.
func Apply (object tomo.Object, role Role) event.Cookie {
if current == nil { return event.NoCookie { } }
return current.Apply(object, role)
}

20
tomo.go
View File

@@ -1,8 +1,10 @@
package tomo
import "sync"
import "image"
import "errors"
var backendLock sync.Mutex
var backend Backend
// Run initializes a backend, runs the specified callback function, and runs the
@@ -17,7 +19,10 @@ func Run (callback func ()) error {
back, err := Initialize()
if err != nil { return err }
backendLock.Lock()
backend = back
backendLock.Unlock()
callback()
return backend.Run()
@@ -32,29 +37,44 @@ func assertBackend () {
func Stop () {
assertBackend()
backend.Stop()
backendLock.Lock()
backend = nil
backendLock.Unlock()
}
// Do performs a callback function in the event loop thread as soon as possible.
func Do (callback func ()) {
backendLock.Lock()
if backend != nil { backend.Do(callback) }
backendLock.Unlock()
}
// NewWindow creates and returns a window within the specified bounds on screen.
func NewWindow (bounds image.Rectangle) (MainWindow, error) {
assertBackend()
return backend.NewWindow(bounds)
}
// NewBox creates and returns a basic Box.
func NewBox () Box {
assertBackend()
return backend.NewBox()
}
// NewTextBox creates and returns a Box that can display text.
func NewTextBox () TextBox {
assertBackend()
return backend.NewTextBox()
}
// NewCanvasBox creates and returns a Box that can display custom graphics.
func NewCanvasBox () CanvasBox {
assertBackend()
return backend.NewCanvasBox()
}
// NewContainerBox creates and returns a Box that can contain other boxes.
func NewContainerBox () ContainerBox {
assertBackend()
return backend.NewContainerBox()