github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/misc/logos/buffer.go (about)

     1  package logos
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/gdamore/tcell/v2"
     8  )
     9  
    10  // ----------------------------------------
    11  // Buffer
    12  
    13  // A Buffer is a buffer area in which to draw.
    14  type Buffer struct {
    15  	Size
    16  	Cells []Cell
    17  }
    18  
    19  func NewBuffer(sz Size) *Buffer {
    20  	return &Buffer{
    21  		Size:  sz,
    22  		Cells: make([]Cell, sz.Width*sz.Height),
    23  	}
    24  }
    25  
    26  func (bb *Buffer) Reset() {
    27  	sz := bb.Size
    28  	bb.Cells = make([]Cell, sz.Width*sz.Height)
    29  }
    30  
    31  func (bb *Buffer) GetCell(x, y int) *Cell {
    32  	if bb.Width <= x {
    33  		panic(fmt.Sprintf(
    34  			"index x=%d out of bounds, width=%d",
    35  			x, bb.Width))
    36  	}
    37  	if bb.Height <= y {
    38  		panic(fmt.Sprintf(
    39  			"index y=%d out of bounds, height=%d",
    40  			y, bb.Height))
    41  	}
    42  	return &bb.Cells[y*bb.Width+x]
    43  }
    44  
    45  func (bb *Buffer) Sprint() string {
    46  	lines := []string{}
    47  	for y := 0; y < bb.Height; y++ {
    48  		parts := []string{}
    49  		for x := 0; x < bb.Width; x++ {
    50  			cell := bb.GetCell(x, y)
    51  			parts = append(parts, cell.Character)
    52  			// NOTE: hacky way to debug.
    53  			if true {
    54  				attrs := cell.GetAttrs()
    55  				flags := attrs.GetAttrFlags()
    56  				parts = append(parts,
    57  					fmt.Sprintf("%X", flags))
    58  			}
    59  		}
    60  		line := strings.Join(parts, "")
    61  		lines = append(lines, line)
    62  	}
    63  	return strings.Join(lines, "\n")
    64  }
    65  
    66  func (bb *Buffer) DrawToScreen(s tcell.Screen) {
    67  	sw, sh := s.Size()
    68  	if bb.Size.Width != sw || bb.Size.Height != sh {
    69  		panic("buffer doesn't match screen size")
    70  	}
    71  	for y := 0; y < sh; y++ {
    72  		for x := 0; x < sw; x++ {
    73  			cell := bb.GetCell(x, y)
    74  			if x == 0 && y == 0 {
    75  				// NOTE: to thwart some inexplicable bugs.
    76  				s.SetContent(0, 0, tcell.RunePlus, nil, gDefaultSpaceTStyle)
    77  			} else {
    78  				mainc, combc, tstyle := cell.GetTCellContent()
    79  				s.SetContent(x, y, mainc, combc, tstyle)
    80  			}
    81  		}
    82  	}
    83  }
    84  
    85  // ----------------------------------------
    86  // Cell
    87  
    88  // A terminal character cell.
    89  type Cell struct {
    90  	Character string // 1 unicode character, or " ".
    91  	Width     int
    92  	*Style
    93  	Attrs
    94  	Ref Elem // reference to element
    95  }
    96  
    97  // NOTE: there is a difference in behavior
    98  // from SetValue(...,c2) when c2 has
    99  // attributes not present in c2.Ref, so the
   100  // two are not equivalent.
   101  func (cc *Cell) SetValueFromCell(c2 *Cell) {
   102  	cc.SetValue(c2.Character, c2.Width, c2.Style, c2.Ref)
   103  	cc.Character = c2.Character
   104  	cc.Width = c2.Width
   105  	if c2.Style != nil {
   106  		cc.Style = c2.Style
   107  	}
   108  	cc.Attrs.Merge(c2.GetAttrs())
   109  	if c2.Ref != nil {
   110  		cc.Ref = c2.Ref
   111  	}
   112  }
   113  
   114  func (cc *Cell) SetValue(chs string, w int, st *Style, el Elem) {
   115  	cc.Character = chs
   116  	cc.Width = w
   117  	if st != nil {
   118  		cc.Style = st
   119  	}
   120  	if el != nil {
   121  		cc.Attrs.Merge(el.GetAttrs())
   122  		cc.Ref = el
   123  	}
   124  }
   125  
   126  func (cc *Cell) Reset() {
   127  	*cc = Cell{}
   128  }
   129  
   130  var gDefaultSpaceTStyle = tcell.StyleDefault.
   131  	Dim(true).
   132  	Background(tcell.ColorGray)
   133  
   134  // This is where a bit of dynamic logic is performed,
   135  // namely where the attr is used to derive the final style.
   136  func (cc *Cell) GetTCellContent() (mainc rune, combc []rune, tstyle tcell.Style) {
   137  	style := cc.Style
   138  	attrs := &cc.Attrs
   139  	if cc.Character == "" {
   140  		mainc = '?' // for debugging
   141  		if style == nil {
   142  			// special case
   143  			tstyle = gDefaultSpaceTStyle
   144  		} else {
   145  			tstyle = style.WithAttrs(attrs).GetTStyle()
   146  		}
   147  	} else {
   148  		rz := toRunes(cc.Character)
   149  		mainc = rz[0]
   150  		combc = rz[1:]
   151  		if style == nil {
   152  			tstyle = gDefaultStyle.WithAttrs(attrs).GetTStyle()
   153  		} else {
   154  			tstyle = style.WithAttrs(attrs).GetTStyle()
   155  		}
   156  	}
   157  	return
   158  }
   159  
   160  // ----------------------------------------
   161  // View
   162  // analogy: "Buffer:View :: array:slice".
   163  
   164  // Offset and Size must be within bounds of *Buffer.
   165  type View struct {
   166  	Base   *Buffer
   167  	Offset Coord // offset within Buffer
   168  	Bounds Size  // total size of slice
   169  }
   170  
   171  // offset elements must be 0 or positive.
   172  func (bb *Buffer) NewView(offset Coord) View {
   173  	if !offset.IsNonNegative() {
   174  		panic("should not happen")
   175  	}
   176  	return View{
   177  		Base:   bb,
   178  		Offset: offset,
   179  		Bounds: bb.Size,
   180  	}
   181  }
   182  
   183  func (bs View) NewView(offset Coord) View {
   184  	return View{
   185  		Base:   bs.Base,
   186  		Offset: bs.Offset.Add(offset),
   187  		Bounds: bs.Bounds.SubCoord(offset),
   188  	}
   189  }
   190  
   191  func (bs View) GetCell(x, y int) *Cell {
   192  	if bs.Bounds.Width <= x {
   193  		panic("should not happen")
   194  	}
   195  	if bs.Bounds.Height <= y {
   196  		panic("should not happen")
   197  	}
   198  	return bs.Base.GetCell(
   199  		bs.Offset.X+x,
   200  		bs.Offset.Y+y,
   201  	)
   202  }
   203  
   204  // ----------------------------------------
   205  // BufferedView
   206  
   207  // A view onto an element.
   208  // Somewhat like a slice onto an array
   209  // (as a view is onto an elem),
   210  // except cells are allocated here.
   211  type BufferedElemView struct {
   212  	Coord
   213  	Size
   214  	*Style
   215  	Attrs         // e.g. to focus on a scrollbar
   216  	Base    Elem  // the underlying elem
   217  	Offset  Coord // within elem for pagination
   218  	*Buffer       // view's internal draw screen
   219  }
   220  
   221  // Returns a new *BufferedElemView that spans the whole elem.
   222  // If size is zero, the elem is measured first to get the full
   223  // buffer size. The result must still be rendered before drawing.  The
   224  // *BufferedElemView inherits the style and coordinates of the elem, and
   225  // the elem's coord is set to zero.
   226  func NewBufferedElemView(elem Elem, size Size) *BufferedElemView {
   227  	if size.IsZero() {
   228  		size = elem.Measure()
   229  	}
   230  	bpv := &BufferedElemView{
   231  		Size:   size,
   232  		Style:  elem.GetStyle(),
   233  		Base:   elem,
   234  		Offset: Coord{0, 0},
   235  		// NOTE: be lazy, size may change.
   236  		// Buffer: NewBuffer(size),
   237  	}
   238  	bpv.SetCoord(elem.GetCoord())
   239  	bpv.SetIsDirty(true)
   240  	elem.SetParent(bpv)
   241  	elem.SetCoord(Coord{}) // required for abs calc.
   242  	return bpv
   243  }
   244  
   245  func (bpv *BufferedElemView) StringIndented(indent string) string {
   246  	return fmt.Sprintf("Buffered%v@%p\n%s    %s",
   247  		bpv.Size,
   248  		bpv,
   249  		indent,
   250  		bpv.Base.StringIndented(indent+"    "))
   251  }
   252  
   253  func (bpv *BufferedElemView) String() string {
   254  	return fmt.Sprintf("Buffered%v{%v}@%p",
   255  		bpv.Size,
   256  		bpv.Base,
   257  		bpv)
   258  }
   259  
   260  // Pass on style to the base elem.
   261  func (bpv *BufferedElemView) SetStyle(style *Style) {
   262  	bpv.Style = style
   263  	bpv.Base.SetStyle(style)
   264  	bpv.SetIsDirty(true)
   265  }
   266  
   267  func (bpv *BufferedElemView) SetSize(size Size) {
   268  	bpv.Size = size
   269  	bpv.Buffer = nil
   270  	bpv.SetIsDirty(true)
   271  }
   272  
   273  // Cursor status pierces the buffered elem view;
   274  // but a few key events are still consumed before the base.
   275  func (bpv *BufferedElemView) SetIsCursor(ic bool) {
   276  	bpv.Attrs.SetIsCursor(ic)
   277  	bpv.Base.SetIsCursor(ic)
   278  }
   279  
   280  // BufferedElemView's size is simply defined by .Size.
   281  func (bpv *BufferedElemView) Measure() Size {
   282  	return bpv.Size
   283  }
   284  
   285  // Renders the elem onto the internal buffer.
   286  // Assumes buffered elem view's elem was already rendered.
   287  // TODO: this function could be optimized to reduce
   288  // redundant background cell modifications.
   289  func (bpv *BufferedElemView) Render() (updated bool) {
   290  	if !bpv.GetIsDirty() {
   291  		return
   292  	} else {
   293  		defer bpv.SetIsDirty(false)
   294  	}
   295  	// Get or initialize buffer.
   296  	buffer := bpv.Buffer
   297  	if buffer == nil {
   298  		buffer = NewBuffer(bpv.Size)
   299  		bpv.Buffer = buffer
   300  	}
   301  	// Reset the buffer's cells.  We could
   302  	// use Buffer.Reset(), but this helps
   303  	// distinguish between undefined cells
   304  	// and those of an empty buffer.
   305  	for x := 0; x < buffer.Size.Width; x++ {
   306  		for y := 0; y < buffer.Size.Height; y++ {
   307  			cell := buffer.GetCell(x, y)
   308  			cell.Reset()
   309  			// Draw Logos star background.
   310  			cell.SetValue("\u2606", 1, bpv.Style, bpv)
   311  		}
   312  	}
   313  	// Then, render and draw elem.
   314  	bpv.Base.Render()
   315  	bpv.Base.Draw(bpv.Offset, buffer.NewView(Coord{}))
   316  	return true
   317  }
   318  
   319  func (bpv *BufferedElemView) Draw(offset Coord, view View) {
   320  	minX, maxX, minY, maxY := computeIntersection(bpv.Size, offset, view.Bounds)
   321  	for y := minY; y < maxY; y++ {
   322  		for x := minX; x < maxX; x++ {
   323  			bcell := bpv.Buffer.GetCell(x, y)
   324  			vcell := view.GetCell(x-offset.X, y-offset.Y)
   325  			vcell.SetValueFromCell(bcell)
   326  		}
   327  	}
   328  }
   329  
   330  func (bpv *BufferedElemView) ProcessEventKey(ev *EventKey) bool {
   331  	// Pagination is outer-greedy, and so Logos
   332  	// generally just likes infinite areas.
   333  	switch evr := ev.Rune(); evr {
   334  	case 'a': // left
   335  		bpv.Scroll(Coord{16, 0})
   336  	case 's': // down
   337  		bpv.Scroll(Coord{0, -8})
   338  	case 'd': // right
   339  		bpv.Scroll(Coord{-16, 0})
   340  	case 'w': // up
   341  		bpv.Scroll(Coord{0, 8})
   342  	default:
   343  		// Try to get the base to handle it.
   344  		if bpv.Base.ProcessEventKey(ev) {
   345  			return true
   346  		}
   347  		return false
   348  	}
   349  	return true // convenience for cases.
   350  }
   351  
   352  func (bpv *BufferedElemView) Scroll(dir Coord) {
   353  	bpv.Offset = bpv.Offset.Add(dir)
   354  	bpv.SetIsDirty(true)
   355  }