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 "image"
import "errors" 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 { 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) NewWindow (image.Rectangle) (MainWindow, error)
NewBox () Box NewBox () Box
NewTextBox () TextBox NewTextBox () TextBox
NewCanvasBox () CanvasBox NewCanvasBox () CanvasBox
NewContainerBox () ContainerBox NewContainerBox () ContainerBox
// Run runs the event loop until Stop() is called, or the backend
// experiences a fatal error.
Run () error
// Stop must unblock run. // Stop must unblock run.
Run () error
Stop () 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. // possible. This method must be safe to call concurrently.
Do (func ()) Do (func ())
} }

View File

@@ -8,24 +8,24 @@ import "image/color"
// Cap represents a stroke cap type. // Cap represents a stroke cap type.
type Cap int; const ( type Cap int; const (
CapButt Cap = iota CapButt Cap = iota // Square cap that ends at the point
CapRound CapRound // Round cap that surrounds the point
CapSquare CapSquare // square cap that surrounds the point
) )
// Joint represents a stroke joint type. // Joint represents a stroke joint type.
type Joint int; const ( type Joint int; const (
JointRount Joint = iota JointRount Joint = iota // Rounded joint
JointSharp JointSharp // Sharp joint
JointMiter JointMiter // Clipped/beveled joint
) )
// StrokeAlign determines whether a stroke is drawn inside, outside, or on a // StrokeAlign determines whether a stroke is drawn inside, outside, or on a
// path. // path.
type StrokeAlign int; const ( type StrokeAlign int; const (
StrokeAlignCenter StrokeAlign = iota StrokeAlignCenter StrokeAlign = iota // Centered on the path
StrokeAlignInner StrokeAlignInner // Inset into the path
StrokeAlignOuter StrokeAlignOuter // Outset around the path
) )
// Pen represents a drawing context that is linked to a canvas. Each canvas can // 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 StrokeWeight (int) // how thick the stroke is
StrokeAlign (StrokeAlign) // where the stroke is drawn StrokeAlign (StrokeAlign) // where the stroke is drawn
// set the stroke/fill to a solid color Stroke (color.Color) // Sets the stroke to a solid color
Stroke (color.Color) Fill (color.Color) // Sets the fill to a solid color
Fill (color.Color)
} }
// Canvas is an image that supports drawing paths. // 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. // Drawer is an object that can draw to a canvas.
type Drawer interface { type Drawer interface {
// Draw draws to the given canvas.
Draw (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 return mime.Type + "/" + mime.Subtype
} }
// MimePlain returns the MIME type of plain text.
func MimePlain () Mime { return Mime { "text", "plain" } } 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" } } func MimeFile () Mime { return Mime { "text", "uri-list" } }
type byteReadCloser struct { *bytes.Reader } type byteReadCloser struct { *bytes.Reader }

View File

@@ -9,46 +9,77 @@ type Cookie interface {
Close () Close ()
} }
// Broadcaster manages event listeners // Broadcaster manages event listeners.
type Broadcaster struct { type Broadcaster[L any] struct {
lastID int lastID int
listeners map[int] func () listeners map[int] L
} }
// Connect adds a new listener to the broadcaster and returns a corresponding // Connect adds a new listener to the broadcaster and returns a corresponding
// cookie. // cookie.
func (broadcaster *Broadcaster) Connect (listener func ()) Cookie { func (broadcaster *Broadcaster[L]) Connect (listener L) Cookie {
if listener == nil { return nil } broadcaster.ensure()
if broadcaster.listeners == nil {
broadcaster.listeners = make(map[int] func ())
}
cookie := broadcaster.newCookie() cookie := broadcaster.newCookie()
broadcaster.listeners[cookie.id] = listener broadcaster.listeners[cookie.id] = listener
return cookie return cookie
} }
// Broadcast runs all event listeners at once. // Listeners returns a map of all connected listeners.
func (broadcaster *Broadcaster) Broadcast () { func (broadcaster *Broadcaster[L]) Listeners () map[int] L {
for _, listener := range broadcaster.listeners { broadcaster.ensure()
listener() return broadcaster.listeners
}
} }
func (broadcaster *Broadcaster) newCookie () cookie { func (broadcaster *Broadcaster[L]) newCookie () cookie[L] {
broadcaster.lastID ++ broadcaster.lastID ++
return cookie { return cookie[L] {
id: broadcaster.lastID, id: broadcaster.lastID,
broadcaster: broadcaster, broadcaster: broadcaster,
} }
} }
type cookie struct { func (broadcaster *Broadcaster[L]) ensure () {
id int if broadcaster.listeners == nil {
broadcaster *Broadcaster 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) 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 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/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 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg=
golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM= 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.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/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-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.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.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.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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@@ -59,6 +59,18 @@ const (
KeyF10 Key = 138 KeyF10 Key = 138
KeyF11 Key = 139 KeyF11 Key = 139
KeyF12 Key = 140 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. // Button represents a mouse button.

309
object.go
View File

@@ -3,19 +3,97 @@ package tomo
import "image" import "image"
import "image/color" import "image/color"
import "golang.org/x/image/font" 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/data"
import "git.tebibyte.media/tomo/tomo/event" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
type Inset [4]int // Side represents one side of a rectangle.
type Gap image.Point 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 { type Border struct {
Width Inset Width Inset
Color [4]color.Color Color [4]color.Color
} }
// Align lists basic alignment types.
type Align int; const ( type Align int; const (
AlignStart Align = iota // similar to left-aligned text AlignStart Align = iota // similar to left-aligned text
AlignMiddle // similar to center-aligned text AlignMiddle // similar to center-aligned text
@@ -23,29 +101,76 @@ type Align int; const (
AlignEven // similar to justified text 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 { type Object interface {
Box () Box Box () Box
} }
// Box is a basic styled box.
type Box interface { type Box interface {
Object 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 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 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) SetBounds (image.Rectangle)
// SetColor sets the background color of this Box.
SetColor (color.Color) 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) 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) 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) 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) 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) 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) 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 OnFocusEnter (func ()) event.Cookie
OnFocusLeave (func ()) event.Cookie OnFocusLeave (func ()) event.Cookie
OnDNDEnter (func ()) event.Cookie OnDNDEnter (func ()) event.Cookie
@@ -59,58 +184,172 @@ type Box interface {
OnScroll (func (deltaX, deltaY float64)) event.Cookie OnScroll (func (deltaX, deltaY float64)) event.Cookie
OnKeyDown (func (key input.Key, numberPad bool)) event.Cookie OnKeyDown (func (key input.Key, numberPad bool)) event.Cookie
OnKeyUp (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 { type CanvasBox interface {
Box Box
// SetDrawer sets the Drawer that will be called upon to draw the Box's
// content when it is invalidated.
SetDrawer (canvas.Drawer) 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 () 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 Box
SetGap (Gap)
Add (Object) // SetOverflow sets whether or not the Box's content overflows
Delete (Object) // horizontally and vertically. Overflowing content is clipped to the
Insert (child Object, before Object) // bounds of the Box inset by all Borders (but not padding).
Clear () SetOverflow (horizontal, vertical bool)
Length () int // SetAlign sets how the Box's content is distributed horizontally and
At (int) Object // vertically.
SetLayout (Layout) 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 { 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) SetRoot (Object)
// SetTitle sets the title of the window.
SetTitle (string) SetTitle (string)
SetIcon (sizes []image.Image) // SetIcon sets the icon of the window. When multiple icon sizes are
NewMenu (image.Rectangle) (Window, error) // provided, the best fitting one is chosen for display.
NewModal (image.Rectangle) (Window, error) 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) 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) 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) Paste (callback func (data.Data, error), accept ...data.Mime)
// Show shows the window.
Show () Show ()
// Hide hides the window.
Hide () Hide ()
// Close closes the window.
Close () Close ()
// OnClose specifies a function to be called when the window is closed.
OnClose (func ()) event.Cookie OnClose (func ()) event.Cookie
} }
// MainWindow is a top-level operating system window.
type MainWindow interface { type MainWindow interface {
Window 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) 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 package tomo
import "sync"
import "image" import "image"
import "errors" import "errors"
var backendLock sync.Mutex
var backend Backend var backend Backend
// Run initializes a backend, runs the specified callback function, and runs the // Run initializes a backend, runs the specified callback function, and runs the
@@ -17,7 +19,10 @@ func Run (callback func ()) error {
back, err := Initialize() back, err := Initialize()
if err != nil { return err } if err != nil { return err }
backendLock.Lock()
backend = back backend = back
backendLock.Unlock()
callback() callback()
return backend.Run() return backend.Run()
@@ -32,29 +37,44 @@ func assertBackend () {
func Stop () { func Stop () {
assertBackend() assertBackend()
backend.Stop() backend.Stop()
backendLock.Lock()
backend = nil 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) { func NewWindow (bounds image.Rectangle) (MainWindow, error) {
assertBackend() assertBackend()
return backend.NewWindow(bounds) return backend.NewWindow(bounds)
} }
// NewBox creates and returns a basic Box.
func NewBox () Box { func NewBox () Box {
assertBackend() assertBackend()
return backend.NewBox() return backend.NewBox()
} }
// NewTextBox creates and returns a Box that can display text.
func NewTextBox () TextBox { func NewTextBox () TextBox {
assertBackend() assertBackend()
return backend.NewTextBox() return backend.NewTextBox()
} }
// NewCanvasBox creates and returns a Box that can display custom graphics.
func NewCanvasBox () CanvasBox { func NewCanvasBox () CanvasBox {
assertBackend() assertBackend()
return backend.NewCanvasBox() return backend.NewCanvasBox()
} }
// NewContainerBox creates and returns a Box that can contain other boxes.
func NewContainerBox () ContainerBox { func NewContainerBox () ContainerBox {
assertBackend() assertBackend()
return backend.NewContainerBox() return backend.NewContainerBox()