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 }