package ui import ( "github.com/gdamore/tcell/v2" ) // Painter draws into a region of the screen, clipping everything to it. // // Coordinates passed to a painter are local: (0, 0) is the top-left cell of // its own region, wherever that sits on the screen. Sub returns a painter for // a part of the region, which is how a window hands its interior to the widget // inside it without that widget ever knowing where it is. type Painter struct { screen tcell.Screen originX int originY int clip Rect // in absolute screen coordinates size Rect // the painter's own region, in local coordinates } // NewPainter returns a painter covering the whole screen. func NewPainter(screen tcell.Screen) *Painter { width, height := screen.Size() area := Rect{W: width, H: height} return &Painter{screen: screen, clip: area, size: area} } // Sub returns a painter for the screen rectangle r. // // Unlike every other method here, r is in absolute screen coordinates: widget // bounds are absolute throughout this package, so laying a child out and // hit-testing a mouse click both work without translating anything. The result // is clipped to r *and* to whatever this painter was already clipped to, so a // child can never draw outside the box its parent was given. func (p *Painter) Sub(r Rect) *Painter { return &Painter{ screen: p.screen, originX: r.X, originY: r.Y, clip: p.clip.Intersect(r), size: Rect{W: r.W, H: r.H}, } } // Size returns the painter's region in local coordinates, so its width and // height are what a widget lays itself out in. func (p *Painter) Size() Rect { return p.size } // SetCell draws one character, doing nothing when it falls outside the clip. func (p *Painter) SetCell(x, y int, r rune, style tcell.Style) { absX, absY := p.originX+x, p.originY+y if !p.clip.Contains(absX, absY) { return } p.screen.SetContent(absX, absY, r, nil, style) } // CellAt returns the character and style currently drawn at a local position, // which is what shading a shadow over existing content needs. func (p *Painter) CellAt(x, y int) (rune, tcell.Style) { absX, absY := p.originX+x, p.originY+y if !p.clip.Contains(absX, absY) { return ' ', tcell.StyleDefault } r, _, style, _ := p.screen.GetContent(absX, absY) return r, style } // Text draws a string from left to right, one cell per rune, and returns the // column just after the last one drawn. // // Runes past the right edge are clipped away rather than wrapped: a widget // that overflows its box is a layout bug, and wrapping would hide it. func (p *Painter) Text(x, y int, text string, style tcell.Style) int { for _, r := range text { p.SetCell(x, y, r, style) x++ } return x } // TextLimited draws a string, cutting it short with an ellipsis when it does // not fit in width cells. It is what draws a file name in a title bar. func (p *Painter) TextLimited(x, y, width int, text string, style tcell.Style) int { runes := []rune(text) if width <= 0 { return x } if len(runes) > width { runes = runes[:width-1] return p.Text(x, y, string(runes)+"…", style) } return p.Text(x, y, string(runes), style) } // Fill covers a rectangle with one character. func (p *Painter) Fill(r Rect, ch rune, style tcell.Style) { for y := r.Y; y < r.Bottom(); y++ { for x := r.X; x < r.Right(); x++ { p.SetCell(x, y, ch, style) } } } // Clear covers the painter's whole region with spaces in the given style. func (p *Painter) Clear(style tcell.Style) { p.Fill(p.size, ' ', style) } // HLine draws a horizontal run of one character. func (p *Painter) HLine(x, y, width int, ch rune, style tcell.Style) { for i := range width { p.SetCell(x+i, y, ch, style) } } // VLine draws a vertical run of one character. func (p *Painter) VLine(x, y, height int, ch rune, style tcell.Style) { for i := range height { p.SetCell(x, y+i, ch, style) } } // Shade restyles the characters already drawn in a rectangle without changing // them, which is how a window casts a shadow over the desktop behind it. func (p *Painter) Shade(r Rect, style tcell.Style) { for y := r.Y; y < r.Bottom(); y++ { for x := r.X; x < r.Right(); x++ { ch, _ := p.CellAt(x, y) p.SetCell(x, y, ch, style) } } } // ShowCursor puts the terminal cursor at a local position, or hides it when // the position falls outside the clip — which is what happens when the focused // widget is scrolled out of sight. func (p *Painter) ShowCursor(x, y int) { absX, absY := p.originX+x, p.originY+y if !p.clip.Contains(absX, absY) { p.screen.HideCursor() return } p.screen.ShowCursor(absX, absY) }