Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cb3be8de8 | |||
| 32e58ce63d | |||
| 4dbd86cec3 | |||
| 573212fe7d | |||
| ae5c177484 | |||
| facd85ef21 | |||
| ec967fe4f8 | |||
| 9efeaef8a8 | |||
| d6baf82a94 | |||
|
|
0824aca4ba | ||
| ec7596fa2d | |||
| 66e1caeebf | |||
| 8b78d16506 | |||
| ac1fe7a75a | |||
| 78aa13f388 | |||
|
|
3e92de7485 | ||
| b00a7302b4 |
96
canvas/shatter.go
Normal file
96
canvas/shatter.go
Normal 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:]...)
|
||||
}
|
||||
@@ -9,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()
|
||||
}
|
||||
}
|
||||
|
||||
164
object.go
164
object.go
@@ -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,38 +138,68 @@ 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)
|
||||
Add (Object)
|
||||
Delete (Object)
|
||||
Insert (child Object, before Object)
|
||||
Clear ()
|
||||
Length () int
|
||||
At (int) Object
|
||||
SetLayout (Layout)
|
||||
SetOverflow (horizontal, vertical bool)
|
||||
ContentBounds () image.Rectangle
|
||||
ScrollTo (image.Point)
|
||||
OnContentBoundsChange (func ()) event.Cookie
|
||||
SetAlign (x, y Align)
|
||||
}
|
||||
|
||||
// TextBox is a box that contains text content.
|
||||
type TextBox interface {
|
||||
ContentBox
|
||||
|
||||
SetText (string)
|
||||
SetTextColor (color.Color)
|
||||
SetFace (font.Face)
|
||||
SetWrap (bool)
|
||||
}
|
||||
|
||||
// ContentBox is a box that can contain child objects. It arranges them
|
||||
// according to a layout rule.
|
||||
type ContainerBox interface {
|
||||
ContentBox
|
||||
PropagateEvents (bool)
|
||||
SetGap (image.Point)
|
||||
Add (Object)
|
||||
Delete (Object)
|
||||
Insert (child Object, before Object)
|
||||
Clear ()
|
||||
Length () int
|
||||
At (int) Object
|
||||
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
|
||||
AlignX Align
|
||||
AlignY Align
|
||||
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)
|
||||
@@ -106,11 +215,8 @@ type Window interface {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user