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] }