canvas.go

  1package lipgloss
  2
  3import (
  4	"fmt"
  5	"image"
  6	"sort"
  7
  8	uv "github.com/charmbracelet/ultraviolet"
  9	"github.com/charmbracelet/x/ansi"
 10)
 11
 12// Canvas is a collection of layers that can be composed together to form a
 13// single frame of text.
 14type Canvas struct {
 15	layers []*Layer
 16}
 17
 18// NewCanvas creates a new Canvas with the given layers. This is a convenient
 19// way to create a Canvas with one or more layers.
 20func NewCanvas(layers ...*Layer) (c *Canvas) {
 21	c = new(Canvas)
 22	c.AddLayers(layers...)
 23	return
 24}
 25
 26// InBounds returns true if the point is within the bounds of the Canvas.
 27func (c *Canvas) InBounds(x, y int) bool {
 28	return image.Pt(x, y).In(c.Bounds())
 29}
 30
 31// Bounds returns the bounds of the Canvas.
 32func (c *Canvas) Bounds() image.Rectangle {
 33	// Figure out the size of the canvas
 34	x0, y0, x1, y1 := 0, 0, 0, 0
 35	for _, l := range c.layers {
 36		x0 = min(x0, l.rect.Min.X)
 37		y0 = min(y0, l.rect.Min.Y)
 38		x1 = max(x1, l.rect.Max.X)
 39		y1 = max(y1, l.rect.Max.Y)
 40	}
 41
 42	// Adjust the size of the canvas if it's negative
 43	x0, y0 = max(x0, 0), max(y0, 0)
 44
 45	return image.Rect(x0, y0, x1, y1)
 46}
 47
 48// Hit returns the [Layer.ID] at the given point. If no Layer is found,
 49// nil is returned.
 50func (c *Canvas) Hit(x, y int) string {
 51	for i := len(c.layers) - 1; i >= 0; i-- {
 52		if c.layers[i].InBounds(x, y) {
 53			return c.layers[i].Hit(x, y).GetID()
 54		}
 55	}
 56	return ""
 57}
 58
 59// AddLayers adds the given layers to the Canvas.
 60func (c *Canvas) AddLayers(layers ...*Layer) {
 61	c.layers = append(c.layers, layers...)
 62	sortLayers(c.layers, false)
 63}
 64
 65// Get returns the Layer with the given ID. If the ID is not found, nil is
 66// returned.
 67func (c *Canvas) Get(id string) *Layer {
 68	for _, l := range c.layers {
 69		if la := l.Get(id); la != nil {
 70			return la
 71		}
 72	}
 73	return nil
 74}
 75
 76// Draw draws the [Canvas] into the given screen and area.
 77func (c *Canvas) Draw(scr uv.Screen, area image.Rectangle) {
 78	for _, l := range c.layers {
 79		l.Draw(scr, area)
 80	}
 81}
 82
 83// Render renders the Canvas to a string.
 84func (c *Canvas) Render() string {
 85	area := c.Bounds()
 86	buf := uv.NewScreenBuffer(area.Dx(), area.Dy())
 87	buf.Method = ansi.GraphemeWidth
 88	c.Draw(buf, area)
 89	return buf.Render()
 90}
 91
 92// Layer represents a window layer that can be composed with other layers.
 93type Layer struct {
 94	rect     image.Rectangle
 95	zIndex   int
 96	children []*Layer
 97	id       string
 98	content  uv.Drawable
 99}
100
101// NewLayer creates a new Layer with the given content. It calculates the size
102// based on the widest line and the number of lines in the content.
103func NewLayer(content any) (l *Layer) {
104	l = new(Layer)
105	l.SetContent(content)
106	return l
107}
108
109// InBounds returns true if the point is within the bounds of the Layer.
110func (l *Layer) InBounds(x, y int) bool {
111	return image.Pt(x, y).In(l.Bounds())
112}
113
114// Bounds returns the bounds of the Layer.
115func (l *Layer) Bounds() image.Rectangle {
116	return l.rect
117}
118
119// Hit returns the [Layer.ID] at the given point. If no Layer is found,
120// returns nil is returned.
121func (l *Layer) Hit(x, y int) *Layer {
122	// Reverse the order of the layers so that the top-most layer is checked
123	// first.
124	for i := len(l.children) - 1; i >= 0; i-- {
125		if l.children[i].InBounds(x, y) {
126			return l.children[i].Hit(x, y)
127		}
128	}
129
130	if image.Pt(x, y).In(l.Bounds()) {
131		return l
132	}
133
134	return nil
135}
136
137// ID sets the ID of the Layer. The ID can be used to identify the Layer when
138// performing hit tests.
139func (l *Layer) ID(id string) *Layer {
140	l.id = id
141	return l
142}
143
144// GetID returns the ID of the Layer.
145func (l *Layer) GetID() string {
146	return l.id
147}
148
149// X sets the x-coordinate of the Layer.
150func (l *Layer) X(x int) *Layer {
151	l.rect = l.rect.Add(image.Pt(x, 0))
152	return l
153}
154
155// Y sets the y-coordinate of the Layer.
156func (l *Layer) Y(y int) *Layer {
157	l.rect = l.rect.Add(image.Pt(0, y))
158	return l
159}
160
161// Z sets the z-index of the Layer.
162func (l *Layer) Z(z int) *Layer {
163	l.zIndex = z
164	return l
165}
166
167// GetX returns the x-coordinate of the Layer.
168func (l *Layer) GetX() int {
169	return l.rect.Min.X
170}
171
172// GetY returns the y-coordinate of the Layer.
173func (l *Layer) GetY() int {
174	return l.rect.Min.Y
175}
176
177// GetZ returns the z-index of the Layer.
178func (l *Layer) GetZ() int {
179	return l.zIndex
180}
181
182// Width sets the width of the Layer.
183func (l *Layer) Width(width int) *Layer {
184	l.rect.Max.X = l.rect.Min.X + width
185	return l
186}
187
188// Height sets the height of the Layer.
189func (l *Layer) Height(height int) *Layer {
190	l.rect.Max.Y = l.rect.Min.Y + height
191	return l
192}
193
194// GetWidth returns the width of the Layer.
195func (l *Layer) GetWidth() int {
196	return l.rect.Dx()
197}
198
199// GetHeight returns the height of the Layer.
200func (l *Layer) GetHeight() int {
201	return l.rect.Dy()
202}
203
204// AddLayers adds child layers to the Layer.
205func (l *Layer) AddLayers(layers ...*Layer) *Layer {
206	// Make children relative to the parent
207	for _, child := range layers {
208		child.rect = child.rect.Add(l.rect.Min)
209		child.zIndex += l.zIndex
210	}
211	l.children = append(l.children, layers...)
212	sortLayers(l.children, false)
213	return l
214}
215
216// SetContent sets the content of the Layer.
217func (l *Layer) SetContent(content any) *Layer {
218	var drawable uv.Drawable
219	var rect image.Rectangle
220	switch c := content.(type) {
221	case uv.Drawable:
222		drawable = c
223		switch c := c.(type) {
224		case interface{ Bounds() image.Rectangle }:
225			rect.Max.X = c.Bounds().Dx()
226			rect.Max.Y = c.Bounds().Dy()
227		case interface {
228			Width() int
229			Height() int
230		}:
231			rect.Max.X = c.Width()
232			rect.Max.Y = c.Height()
233		}
234	case fmt.Stringer:
235		s := c.String()
236		drawable = uv.NewStyledString(s)
237		rect = image.Rect(0, 0, Width(s), Height(s))
238	case string:
239		drawable = uv.NewStyledString(c)
240		rect = image.Rect(0, 0, Width(c), Height(c))
241	default:
242		s := fmt.Sprint(content)
243		drawable = uv.NewStyledString(s)
244		rect = image.Rect(0, 0, Width(s), Height(s))
245	}
246	l.content = drawable
247	l.rect = rect
248	return l
249}
250
251// Content returns the content of the Layer.
252func (l *Layer) Content() any {
253	return l.content
254}
255
256// Draw draws the Layer onto the given screen buffer.
257func (l *Layer) Draw(scr uv.Screen, area image.Rectangle) {
258	if l.content == nil {
259		return
260	}
261	l.content.Draw(scr, area.Intersect(l.Bounds()))
262	for _, child := range l.children {
263		if child.content == nil {
264			continue
265		}
266		child.content.Draw(scr, area.Intersect(child.Bounds()))
267	}
268}
269
270// Get returns the Layer with the given ID. If the ID is not found, it returns
271// nil.
272func (l *Layer) Get(id string) *Layer {
273	if l.id == id {
274		return l
275	}
276	for _, child := range l.children {
277		if child.id == id {
278			return child
279		}
280	}
281	return nil
282}
283
284// sortLayers sorts the layers by z-index, from lowest to highest.
285func sortLayers(ls []*Layer, reverse bool) {
286	if reverse {
287		sort.Stable(sort.Reverse(layers(ls)))
288	} else {
289		sort.Stable(layers(ls))
290	}
291}
292
293// layers implements sort.Interface for []*Layer.
294type layers []*Layer
295
296func (l layers) Len() int           { return len(l) }
297func (l layers) Less(i, j int) bool { return l[i].zIndex < l[j].zIndex }
298func (l layers) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }