Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

canvas.gno

5.71 Kb · 233 lines
  1package art
  2
  3import "strings"
  4
  5// Cell is one character cell: a rune and its two colours. FG and BG are packed
  6// 0xRRGGBB values, or [Default].
  7type Cell struct {
  8	R      rune
  9	FG, BG int
 10}
 11
 12// Canvas is a fixed grid of cells, addressed (x, y) from the top left.
 13//
 14// FG and BG are the pen: the colours the drawing helpers ([Write], [HLine],
 15// [Box] and the rest) apply to every cell they touch. They start at [Default].
 16// Set them with [Canvas.Pen]; a caller that only wants shapes can ignore them
 17// entirely.
 18type Canvas struct {
 19	W, H   int
 20	Cells  []Cell
 21	FG, BG int
 22}
 23
 24// Blank is the cell a new canvas is filled with.
 25var Blank = Cell{R: ' ', FG: Default, BG: Default}
 26
 27// NewCanvas returns a w by h canvas of blank cells. Negative dimensions are
 28// treated as zero, so a canvas built from arithmetic that underflowed is empty
 29// rather than a panic halfway through a Render.
 30func NewCanvas(w, h int) *Canvas {
 31	if w < 0 {
 32		w = 0
 33	}
 34	if h < 0 {
 35		h = 0
 36	}
 37	cells := make([]Cell, w*h)
 38	for i := 0; i < len(cells); i++ {
 39		cells[i] = Blank
 40	}
 41	return &Canvas{W: w, H: h, Cells: cells, FG: Default, BG: Default}
 42}
 43
 44// Pen sets the colours the drawing helpers will apply, and returns the canvas
 45// so calls can be chained.
 46func (c *Canvas) Pen(fg, bg int) *Canvas {
 47	c.FG, c.BG = fg, bg
 48	return c
 49}
 50
 51// In reports whether (x, y) is on the canvas. Every helper in this package
 52// clips silently rather than panicking: art is drawn by arithmetic and a
 53// one-off-the-edge line is not worth aborting a Render over.
 54func (c *Canvas) In(x, y int) bool {
 55	return x >= 0 && y >= 0 && x < c.W && y < c.H
 56}
 57
 58// At returns the cell at (x, y), or [Blank] if that is off the canvas.
 59func (c *Canvas) At(x, y int) Cell {
 60	if !c.In(x, y) {
 61		return Blank
 62	}
 63	return c.Cells[y*c.W+x]
 64}
 65
 66// Set writes a whole cell, colours included, ignoring the pen.
 67func (c *Canvas) Set(x, y int, cell Cell) {
 68	if !c.In(x, y) {
 69		return
 70	}
 71	c.Cells[y*c.W+x] = cell
 72}
 73
 74// Put writes one rune in the pen's colours.
 75func (c *Canvas) Put(x, y int, r rune) {
 76	c.Set(x, y, Cell{R: r, FG: c.FG, BG: c.BG})
 77}
 78
 79// Fill sets every cell on the canvas to cell.
 80func (c *Canvas) Fill(cell Cell) {
 81	for i := 0; i < len(c.Cells); i++ {
 82		c.Cells[i] = cell
 83	}
 84}
 85
 86// Write draws s left to right from (x, y) in the pen's colours. It does not
 87// wrap: anything past the right edge is clipped. A '\n' moves to the next row,
 88// back at the starting column, which is what makes a multi-line literal blit
 89// the way it reads in source.
 90func (c *Canvas) Write(x, y int, s string) {
 91	cx, cy := x, y
 92	for _, r := range s {
 93		if r == '\n' {
 94			cx, cy = x, cy+1
 95			continue
 96		}
 97		c.Put(cx, cy, r)
 98		cx++
 99	}
100}
101
102// HLine draws n runes rightwards from (x, y).
103func (c *Canvas) HLine(x, y, n int, r rune) {
104	for i := 0; i < n; i++ {
105		c.Put(x+i, y, r)
106	}
107}
108
109// VLine draws n runes downwards from (x, y).
110func (c *Canvas) VLine(x, y, n int, r rune) {
111	for i := 0; i < n; i++ {
112		c.Put(x, y+i, r)
113	}
114}
115
116// Rect fills a w by h area with r.
117func (c *Canvas) Rect(x, y, w, h int, r rune) {
118	for j := 0; j < h; j++ {
119		c.HLine(x, y+j, w, r)
120	}
121}
122
123// Blit copies src onto c with src's top left at (x, y), colours and all.
124// Cells landing off the edge are dropped.
125func (c *Canvas) Blit(x, y int, src *Canvas) {
126	if src == nil {
127		return
128	}
129	for j := 0; j < src.H; j++ {
130		for i := 0; i < src.W; i++ {
131			c.Set(x+i, y+j, src.Cells[j*src.W+i])
132		}
133	}
134}
135
136// Text emits the canvas as plain text: runes only, one line per row, trailing
137// spaces trimmed. This is the form that belongs inside a triple-backtick fence,
138// and the form a caller can pipe.
139//
140// Colours are dropped, which is the whole point: a consumer asking for text has
141// said it cannot show them.
142func (c Canvas) Text() string {
143	var b strings.Builder
144	for y := 0; y < c.H; y++ {
145		var line strings.Builder
146		for x := 0; x < c.W; x++ {
147			line.WriteRune(c.Cells[y*c.W+x].R)
148		}
149		b.WriteString(strings.TrimRight(line.String(), " "))
150		if y < c.H-1 {
151			b.WriteString("\n")
152		}
153	}
154	return b.String()
155}
156
157// String is [Canvas.Text], so a canvas can be printed directly.
158func (c Canvas) String() string { return c.Text() }
159
160// ANSI emits the canvas with SGR truecolour escapes, one reset at the end of
161// every coloured row. Trailing spaces are NOT trimmed here: a trailing cell may
162// carry a background colour, and trimming it would put a hole in the picture.
163//
164// This output cannot reach gnoweb. See the package doc.
165func (c Canvas) ANSI() string {
166	var b strings.Builder
167	for y := 0; y < c.H; y++ {
168		fg, bg := Default, Default
169		for x := 0; x < c.W; x++ {
170			cl := c.Cells[y*c.W+x]
171			if cl.FG != fg || cl.BG != bg {
172				b.WriteString(sgr(cl.FG, cl.BG))
173				fg, bg = cl.FG, cl.BG
174			}
175			b.WriteRune(cl.R)
176		}
177		if fg != Default || bg != Default {
178			b.WriteString(reset)
179		}
180		if y < c.H-1 {
181			b.WriteString("\n")
182		}
183	}
184	return b.String()
185}
186
187// Escape is the byte that starts every SGR sequence. Exported so a consumer can
188// strip ANSI without hard-coding "\x1b".
189const Escape = '\x1b'
190
191const reset = "\x1b[0m"
192
193func sgr(fg, bg int) string {
194	var b strings.Builder
195	b.WriteString("\x1b[")
196	if fg == Default {
197		b.WriteString("39")
198	} else {
199		r, g, bl := Split(fg)
200		b.WriteString("38;2;")
201		writeInt(&b, r)
202		b.WriteByte(';')
203		writeInt(&b, g)
204		b.WriteByte(';')
205		writeInt(&b, bl)
206	}
207	b.WriteByte(';')
208	if bg == Default {
209		b.WriteString("49")
210	} else {
211		r, g, bl := Split(bg)
212		b.WriteString("48;2;")
213		writeInt(&b, r)
214		b.WriteByte(';')
215		writeInt(&b, g)
216		b.WriteByte(';')
217		writeInt(&b, bl)
218	}
219	b.WriteByte('m')
220	return b.String()
221}
222
223// writeInt writes a 0-255 value without strconv, which this package would
224// otherwise import for three call sites.
225func writeInt(b *strings.Builder, v int) {
226	if v >= 100 {
227		b.WriteByte(byte('0' + v/100))
228	}
229	if v >= 10 {
230		b.WriteByte(byte('0' + (v/10)%10))
231	}
232	b.WriteByte(byte('0' + v%10))
233}