Compare commits

...

19 Commits

Author SHA1 Message Date
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
b00a7302b4 Return error from NewWindow 2023-07-01 11:45:48 -04:00
48fe7ef5e1 Added a rectangle method to Canvas 2023-07-01 03:02:08 -04:00
bb07363c89 Window now has SetTitle method 2023-06-30 22:08:17 -04:00
41d3cd8729 Backend.NewWindow now can return error 2023-06-30 22:07:58 -04:00
c7c6fd2894 Add package-level doc comments 2023-06-30 19:40:45 -04:00
a5ba4cd855 Load plugins on start 2023-06-30 19:30:17 -04:00
6 changed files with 270 additions and 47 deletions

View File

@@ -5,7 +5,7 @@ import "image"
import "errors"
type Backend interface {
NewWindow (image.Rectangle) MainWindow
NewWindow (image.Rectangle) (MainWindow, error)
NewBox () Box
NewTextBox () TextBox
NewCanvasBox () CanvasBox

View File

@@ -1,3 +1,5 @@
// Canvas defines a standard interface for images that support drawing
// primitives.
package canvas
import "image"
@@ -30,8 +32,11 @@ type StrokeAlign int; const (
// have multiple pens associated with it, each maintaining their own drawing
// context.
type Pen interface {
// Draw draws a path
Draw (points ...image.Point)
// Rectangle draws a rectangle
Rectangle (image.Rectangle)
// Path draws a path
Path (points ...image.Point)
Closed (bool) // if the path is closed
Cap (Cap) // line cap stype

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

@@ -1,3 +1,5 @@
// Package event provides a system for broadcasting events to multiple event
// handlers.
package event
// A cookie is returned when you add an event handler so you can remove it
@@ -7,46 +9,59 @@ 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 () {
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()
}
}

151
object.go
View File

@@ -8,8 +8,81 @@ import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/canvas"
// Side represents one side of a rectangle.
type Side int; const (
SideTop Side = iota
SideRight
SideBottom
SideLeft
)
type Inset [4]int
type Gap image.Point
// 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]
}
type Border struct {
Width Inset
@@ -23,18 +96,24 @@ 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 () Window
Bounds () image.Rectangle
InnerBounds () image.Rectangle
MinimumSize () image.Point
SetBounds (image.Rectangle)
SetColor (color.Color)
SetBorder (...Border)
SetMinimumSize (int, int)
SetMinimumSize (image.Point)
SetPadding (Inset)
SetDNDData (data.Data)
@@ -43,8 +122,8 @@ type Box interface {
SetFocusable (bool)
Focused () bool
Modifiers (func ()) input.Modifiers
MousePosition (func ()) image.Point
Modifiers () input.Modifiers
MousePosition () image.Point
OnFocusEnter (func ()) event.Cookie
OnFocusLeave (func ()) event.Cookie
@@ -59,29 +138,42 @@ 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 (canvas.Drawer)
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)
SetOverflow (horizontal, vertical bool)
ContentBounds () image.Rectangle
ScrollTo (image.Point)
OnContentBoundsChange (func ()) event.Cookie
}
// TextBox is a box that contains text content.
type TextBox interface {
ContentBox
SetText (string)
SetTextColor (color.Color)
SetFace (font.Face)
SetWrap (bool)
SetHAlign (Align)
SetVAlign (Align)
}
// ContentBox is a box that can contain child objects. It arranges them
// according to a layout rule.
type ContainerBox interface {
ContentBox
SetGap (image.Point)
Add (Object)
Delete (Object)
Insert (child Object, before Object)
@@ -91,25 +183,38 @@ type ContainerBox interface {
SetLayout (Layout)
}
// LayoutHints are passed to a layout to tell it how to arrange child boxes.
type LayoutHints struct {
Bounds image.Rectangle
OverflowX bool
OverflowY bool
Gap image.Point
}
// Layout can be given to a ContainerBox to arrange child objects.
type Layout interface {
MinimumSize (LayoutHints, []Box) image.Point
Arrange (LayoutHints, []Box)
}
// Window is an operating system window. It can contain one object.
type Window interface {
SetRoot (Object)
SetTitle (string)
SetIcon (sizes []image.Image)
NewMenu (image.Rectangle) (Window, error)
NewModal (image.Rectangle) (Window, error)
Widget () (Window, error)
Copy (data.Data)
Paste (callback func (data.Data, error), accept ...data.Mime)
Close ()
Show ()
Hide ()
Close ()
OnClose (func ()) event.Cookie
}
// MainWindow is a top-level operating system window.
type MainWindow interface {
Window
NewChild (image.Rectangle) (Window, error)
}
type Layout interface {
Arrange (image.Rectangle, Gap, []Box)
}

View File

@@ -9,6 +9,8 @@ var backend Backend
// event loop in that order. This function blocks until Stop is called, or the
// backend experiences a fatal error.
func Run (callback func ()) error {
loadPlugins()
if backend != nil {
return errors.New("there is already a backend running")
}
@@ -33,7 +35,7 @@ func Stop () {
backend = nil
}
func NewWindow (bounds image.Rectangle) MainWindow {
func NewWindow (bounds image.Rectangle) (MainWindow, error) {
assertBackend()
return backend.NewWindow(bounds)
}