grid.go (20831B)
1 package tview 2 3 import ( 4 "math" 5 6 "github.com/gdamore/tcell/v2" 7 ) 8 9 // gridItem represents one primitive and its possible position on a grid. 10 type gridItem struct { 11 Item Primitive // The item to be positioned. May be nil for an empty item. 12 Row, Column int // The top-left grid cell where the item is placed. 13 Width, Height int // The number of rows and columns the item occupies. 14 MinGridWidth, MinGridHeight int // The minimum grid width/height for which this item is visible. 15 Focus bool // Whether or not this item attracts the layout's focus. 16 17 visible bool // Whether or not this item was visible the last time the grid was drawn. 18 x, y, w, h int // The last position of the item relative to the top-left corner of the grid. Undefined if visible is false. 19 } 20 21 // Grid is an implementation of a grid-based layout. It works by defining the 22 // size of the rows and columns, then placing primitives into the grid. 23 // 24 // Some settings can lead to the grid exceeding its available space. SetOffset() 25 // can then be used to scroll in steps of rows and columns. These offset values 26 // can also be controlled with the arrow keys (or the "g","G", "j", "k", "h", 27 // and "l" keys) while the grid has focus and none of its contained primitives 28 // do. 29 // 30 // See https://github.com/rivo/tview/wiki/Grid for an example. 31 type Grid struct { 32 *Box 33 34 // The items to be positioned. 35 items []*gridItem 36 37 // The definition of the rows and columns of the grid. See 38 // [Grid.SetRows] / [Grid.SetColumns] for details. 39 rows, columns []int 40 41 // The minimum sizes for rows and columns. 42 minWidth, minHeight int 43 44 // The size of the gaps between neighboring primitives. This is automatically 45 // set to 1 if borders is true. 46 gapRows, gapColumns int 47 48 // The number of rows and columns skipped before drawing the top-left corner 49 // of the grid. 50 rowOffset, columnOffset int 51 52 // Whether or not borders are drawn around grid items. If this is set to true, 53 // a gap size of 1 is automatically assumed (which is filled with the border 54 // graphics). 55 borders bool 56 57 // The color of the borders around grid items. 58 bordersColor tcell.Color 59 } 60 61 // NewGrid returns a new grid-based layout container with no initial primitives. 62 // 63 // Note that Box, the superclass of Grid, will be transparent so that any grid 64 // areas not covered by any primitives will leave their background unchanged. To 65 // clear a Grid's background before any items are drawn, reset its Box to one 66 // with the desired color: 67 // 68 // grid.Box = NewBox() 69 func NewGrid() *Grid { 70 g := &Grid{ 71 bordersColor: Styles.GraphicsColor, 72 } 73 g.Box = NewBox() 74 g.Box.dontClear = true 75 return g 76 } 77 78 // SetColumns defines how the columns of the grid are distributed. Each value 79 // defines the size of one column, starting with the leftmost column. Values 80 // greater than 0 represent absolute column widths (gaps not included). Values 81 // less than or equal to 0 represent proportional column widths or fractions of 82 // the remaining free space, where 0 is treated the same as -1. That is, a 83 // column with a value of -3 will have three times the width of a column with a 84 // value of -1 (or 0). The minimum width set with SetMinSize() is always 85 // observed. 86 // 87 // Primitives may extend beyond the columns defined explicitly with this 88 // function. A value of 0 is assumed for any undefined column. In fact, if you 89 // never call this function, all columns occupied by primitives will have the 90 // same width. On the other hand, unoccupied columns defined with this function 91 // will always take their place. 92 // 93 // Assuming a total width of the grid of 100 cells and a minimum width of 0, the 94 // following call will result in columns with widths of 30, 10, 15, 15, and 30 95 // cells: 96 // 97 // grid.SetColumns(30, 10, -1, -1, -2) 98 // 99 // If a primitive were then placed in the 6th and 7th column, the resulting 100 // widths would be: 30, 10, 10, 10, 20, 10, and 10 cells. 101 // 102 // If you then called SetMinSize() as follows: 103 // 104 // grid.SetMinSize(15, 20) 105 // 106 // The resulting widths would be: 30, 15, 15, 15, 20, 15, and 15 cells, a total 107 // of 125 cells, 25 cells wider than the available grid width. 108 func (g *Grid) SetColumns(columns ...int) *Grid { 109 g.columns = columns 110 return g 111 } 112 113 // SetRows defines how the rows of the grid are distributed. These values behave 114 // the same as the column values provided with [Grid.SetColumns], see there 115 // for a definition and examples. 116 // 117 // The provided values correspond to row heights, the first value defining 118 // the height of the topmost row. 119 func (g *Grid) SetRows(rows ...int) *Grid { 120 g.rows = rows 121 return g 122 } 123 124 // SetSize is a shortcut for [Grid.SetRows] and [Grid.SetColumns] where 125 // all row and column values are set to the given size values. See 126 // [Grid.SetColumns] for details on sizes. 127 func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) *Grid { 128 g.rows = make([]int, numRows) 129 for index := range g.rows { 130 g.rows[index] = rowSize 131 } 132 g.columns = make([]int, numColumns) 133 for index := range g.columns { 134 g.columns[index] = columnSize 135 } 136 return g 137 } 138 139 // SetMinSize sets an absolute minimum width for rows and an absolute minimum 140 // height for columns. Panics if negative values are provided. 141 func (g *Grid) SetMinSize(row, column int) *Grid { 142 if row < 0 || column < 0 { 143 panic("Invalid minimum row/column size") 144 } 145 g.minHeight, g.minWidth = row, column 146 return g 147 } 148 149 // SetGap sets the size of the gaps between neighboring primitives on the grid. 150 // If borders are drawn (see SetBorders()), these values are ignored and a gap 151 // of 1 is assumed. Panics if negative values are provided. 152 func (g *Grid) SetGap(row, column int) *Grid { 153 if row < 0 || column < 0 { 154 panic("Invalid gap size") 155 } 156 g.gapRows, g.gapColumns = row, column 157 return g 158 } 159 160 // SetBorders sets whether or not borders are drawn around grid items. Setting 161 // this value to true will cause the gap values (see SetGap()) to be ignored and 162 // automatically assumed to be 1 where the border graphics are drawn. 163 func (g *Grid) SetBorders(borders bool) *Grid { 164 g.borders = borders 165 return g 166 } 167 168 // SetBordersColor sets the color of the item borders. 169 func (g *Grid) SetBordersColor(color tcell.Color) *Grid { 170 g.bordersColor = color 171 return g 172 } 173 174 // AddItem adds a primitive and its position to the grid. The top-left corner 175 // of the primitive will be located in the top-left corner of the grid cell at 176 // the given row and column and will span "rowSpan" rows and "colSpan" columns. 177 // For example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6: 178 // 179 // grid.AddItem(p, 2, 5, 3, 2, 0, 0, true) 180 // 181 // If rowSpan or colSpan is 0, the primitive will not be drawn. 182 // 183 // You can add the same primitive multiple times with different grid positions. 184 // The minGridWidth and minGridHeight values will then determine which of those 185 // positions will be used. This is similar to CSS media queries. These minimum 186 // values refer to the overall size of the grid. If multiple items for the same 187 // primitive apply, the one with the highest minimum value (width or height, 188 // whatever is higher) will be used, or the primitive added last if those values 189 // are the same. Example: 190 // 191 // grid.AddItem(p, 0, 0, 0, 0, 0, 0, true). // Hide in small grids. 192 // AddItem(p, 0, 0, 1, 2, 100, 0, true). // One-column layout for medium grids. 193 // AddItem(p, 1, 1, 3, 2, 300, 0, true) // Multi-column layout for large grids. 194 // 195 // To use the same grid layout for all sizes, simply set minGridWidth and 196 // minGridHeight to 0. 197 // 198 // If the item's focus is set to true, it will receive focus when the grid 199 // receives focus. If there are multiple items with a true focus flag, the last 200 // visible one that was added will receive focus. 201 func (g *Grid) AddItem(p Primitive, row, column, rowSpan, colSpan, minGridHeight, minGridWidth int, focus bool) *Grid { 202 g.items = append(g.items, &gridItem{ 203 Item: p, 204 Row: row, 205 Column: column, 206 Height: rowSpan, 207 Width: colSpan, 208 MinGridHeight: minGridHeight, 209 MinGridWidth: minGridWidth, 210 Focus: focus, 211 }) 212 return g 213 } 214 215 // RemoveItem removes all items for the given primitive from the grid, keeping 216 // the order of the remaining items intact. 217 func (g *Grid) RemoveItem(p Primitive) *Grid { 218 for index := len(g.items) - 1; index >= 0; index-- { 219 if g.items[index].Item == p { 220 g.items = append(g.items[:index], g.items[index+1:]...) 221 } 222 } 223 return g 224 } 225 226 // Clear removes all items from the grid. 227 func (g *Grid) Clear() *Grid { 228 g.items = nil 229 return g 230 } 231 232 // SetOffset sets the number of rows and columns which are skipped before 233 // drawing the first grid cell in the top-left corner. As the grid will never 234 // completely move off the screen, these values may be adjusted the next time 235 // the grid is drawn. The actual position of the grid may also be adjusted such 236 // that contained primitives that have focus remain visible. 237 func (g *Grid) SetOffset(rows, columns int) *Grid { 238 g.rowOffset, g.columnOffset = rows, columns 239 return g 240 } 241 242 // GetOffset returns the current row and column offset (see SetOffset() for 243 // details). 244 func (g *Grid) GetOffset() (rows, columns int) { 245 return g.rowOffset, g.columnOffset 246 } 247 248 // Focus is called when this primitive receives focus. 249 func (g *Grid) Focus(delegate func(p Primitive)) { 250 for _, item := range g.items { 251 if item.Focus { 252 delegate(item.Item) 253 return 254 } 255 } 256 g.Box.Focus(delegate) 257 } 258 259 // HasFocus returns whether or not this primitive has focus. 260 func (g *Grid) HasFocus() bool { 261 for _, item := range g.items { 262 if item.visible && item.Item.HasFocus() { 263 return true 264 } 265 } 266 return g.Box.HasFocus() 267 } 268 269 // Draw draws this primitive onto the screen. 270 func (g *Grid) Draw(screen tcell.Screen) { 271 g.Box.DrawForSubclass(screen, g) 272 x, y, width, height := g.GetInnerRect() 273 screenWidth, screenHeight := screen.Size() 274 275 // Make a list of items which apply. 276 items := make([]*gridItem, 0, len(g.items)) 277 ItemLoop: 278 for _, item := range g.items { 279 item.visible = false 280 if item.Item == nil || item.Width <= 0 || item.Height <= 0 || width < item.MinGridWidth || height < item.MinGridHeight { 281 continue // Disqualified. 282 } 283 284 // Check for overlaps and multiple layouts of the same item. 285 for index, existing := range items { 286 // Do they overlap or are identical? 287 if item.Item != existing.Item && 288 (item.Row >= existing.Row+existing.Height || item.Row+item.Height <= existing.Row || 289 item.Column >= existing.Column+existing.Width || item.Column+item.Width <= existing.Column) { 290 continue // They don't and aren't. 291 } 292 293 // What's their minimum size? 294 itemMin := item.MinGridWidth 295 if item.MinGridHeight > itemMin { 296 itemMin = item.MinGridHeight 297 } 298 existingMin := existing.MinGridWidth 299 if existing.MinGridHeight > existingMin { 300 existingMin = existing.MinGridHeight 301 } 302 303 // Which one is more important? 304 if itemMin < existingMin { 305 continue ItemLoop // This one isn't. Drop it. 306 } 307 items[index] = item // This one is. Replace the other. 308 continue ItemLoop 309 } 310 311 // This item will be visible. 312 items = append(items, item) 313 } 314 315 // How many rows and columns do we have? 316 rows := len(g.rows) 317 columns := len(g.columns) 318 for _, item := range items { 319 rowEnd := item.Row + item.Height 320 if rowEnd > rows { 321 rows = rowEnd 322 } 323 columnEnd := item.Column + item.Width 324 if columnEnd > columns { 325 columns = columnEnd 326 } 327 } 328 if rows == 0 || columns == 0 { 329 return // No content. 330 } 331 332 // Where are they located? 333 rowPos := make([]int, rows) 334 rowHeight := make([]int, rows) 335 columnPos := make([]int, columns) 336 columnWidth := make([]int, columns) 337 338 // How much space do we distribute? 339 remainingWidth := width 340 remainingHeight := height 341 proportionalWidth := 0 342 proportionalHeight := 0 343 for index, row := range g.rows { 344 if row > 0 { 345 if row < g.minHeight { 346 row = g.minHeight 347 } 348 remainingHeight -= row 349 rowHeight[index] = row 350 } else if row == 0 { 351 proportionalHeight++ 352 } else { 353 proportionalHeight += -row 354 } 355 } 356 for index, column := range g.columns { 357 if column > 0 { 358 if column < g.minWidth { 359 column = g.minWidth 360 } 361 remainingWidth -= column 362 columnWidth[index] = column 363 } else if column == 0 { 364 proportionalWidth++ 365 } else { 366 proportionalWidth += -column 367 } 368 } 369 if g.borders { 370 remainingHeight -= rows + 1 371 remainingWidth -= columns + 1 372 } else { 373 remainingHeight -= (rows - 1) * g.gapRows 374 remainingWidth -= (columns - 1) * g.gapColumns 375 } 376 if rows > len(g.rows) { 377 proportionalHeight += rows - len(g.rows) 378 } 379 if columns > len(g.columns) { 380 proportionalWidth += columns - len(g.columns) 381 } 382 383 // Distribute proportional rows/columns. 384 for index := 0; index < rows; index++ { 385 row := 0 386 if index < len(g.rows) { 387 row = g.rows[index] 388 } 389 if row > 0 { 390 continue // Not proportional. We already know the width. 391 } else if row == 0 { 392 row = 1 393 } else { 394 row = -row 395 } 396 rowAbs := row * remainingHeight / proportionalHeight 397 remainingHeight -= rowAbs 398 proportionalHeight -= row 399 if rowAbs < g.minHeight { 400 rowAbs = g.minHeight 401 } 402 rowHeight[index] = rowAbs 403 } 404 for index := 0; index < columns; index++ { 405 column := 0 406 if index < len(g.columns) { 407 column = g.columns[index] 408 } 409 if column > 0 { 410 continue // Not proportional. We already know the height. 411 } else if column == 0 { 412 column = 1 413 } else { 414 column = -column 415 } 416 columnAbs := column * remainingWidth / proportionalWidth 417 remainingWidth -= columnAbs 418 proportionalWidth -= column 419 if columnAbs < g.minWidth { 420 columnAbs = g.minWidth 421 } 422 columnWidth[index] = columnAbs 423 } 424 425 // Calculate row/column positions. 426 var columnX, rowY int 427 if g.borders { 428 columnX++ 429 rowY++ 430 } 431 for index, row := range rowHeight { 432 rowPos[index] = rowY 433 gap := g.gapRows 434 if g.borders { 435 gap = 1 436 } 437 rowY += row + gap 438 } 439 for index, column := range columnWidth { 440 columnPos[index] = columnX 441 gap := g.gapColumns 442 if g.borders { 443 gap = 1 444 } 445 columnX += column + gap 446 } 447 448 // Calculate primitive positions. 449 var focus *gridItem // The item which has focus. 450 for _, item := range items { 451 px := columnPos[item.Column] 452 py := rowPos[item.Row] 453 var pw, ph int 454 for index := 0; index < item.Height; index++ { 455 ph += rowHeight[item.Row+index] 456 } 457 for index := 0; index < item.Width; index++ { 458 pw += columnWidth[item.Column+index] 459 } 460 if g.borders { 461 pw += item.Width - 1 462 ph += item.Height - 1 463 } else { 464 pw += (item.Width - 1) * g.gapColumns 465 ph += (item.Height - 1) * g.gapRows 466 } 467 item.x, item.y, item.w, item.h = px, py, pw, ph 468 item.visible = true 469 if item.Item.HasFocus() { 470 focus = item 471 } 472 } 473 474 // Calculate screen offsets. 475 var offsetX, offsetY int 476 add := 1 477 if !g.borders { 478 add = g.gapRows 479 } 480 for index, height := range rowHeight { 481 if index >= g.rowOffset { 482 break 483 } 484 offsetY += height + add 485 } 486 if !g.borders { 487 add = g.gapColumns 488 } 489 for index, width := range columnWidth { 490 if index >= g.columnOffset { 491 break 492 } 493 offsetX += width + add 494 } 495 496 // The focused item must be within the visible area. 497 if focus != nil { 498 if focus.y+focus.h-offsetY >= height { 499 offsetY = focus.y - height + focus.h 500 } 501 if focus.y-offsetY < 0 { 502 offsetY = focus.y 503 } 504 if focus.x+focus.w-offsetX >= width { 505 offsetX = focus.x - width + focus.w 506 } 507 if focus.x-offsetX < 0 { 508 offsetX = focus.x 509 } 510 } 511 512 // Adjust row/column offsets based on this value. 513 var from, to int 514 for index, pos := range rowPos { 515 if pos-offsetY < 0 { 516 from = index + 1 517 } 518 if pos-offsetY < height { 519 to = index 520 } 521 } 522 if g.rowOffset < from { 523 g.rowOffset = from 524 } 525 if g.rowOffset > to { 526 g.rowOffset = to 527 } 528 from, to = 0, 0 529 for index, pos := range columnPos { 530 if pos-offsetX < 0 { 531 from = index + 1 532 } 533 if pos-offsetX < width { 534 to = index 535 } 536 } 537 if g.columnOffset < from { 538 g.columnOffset = from 539 } 540 if g.columnOffset > to { 541 g.columnOffset = to 542 } 543 544 // Draw primitives and borders. 545 borderStyle := tcell.StyleDefault.Background(g.backgroundColor).Foreground(g.bordersColor) 546 for _, item := range items { 547 // Final primitive position. 548 if !item.visible { 549 continue 550 } 551 item.x -= offsetX 552 item.y -= offsetY 553 if item.x >= width || item.x+item.w <= 0 || item.y >= height || item.y+item.h <= 0 { 554 item.visible = false 555 continue 556 } 557 if item.x+item.w > width { 558 item.w = width - item.x 559 } 560 if item.y+item.h > height { 561 item.h = height - item.y 562 } 563 if item.x < 0 { 564 item.w += item.x 565 item.x = 0 566 } 567 if item.y < 0 { 568 item.h += item.y 569 item.y = 0 570 } 571 if item.w <= 0 || item.h <= 0 { 572 item.visible = false 573 continue 574 } 575 item.x += x 576 item.y += y 577 item.Item.SetRect(item.x, item.y, item.w, item.h) 578 579 // Draw primitive. 580 if item == focus { 581 defer item.Item.Draw(screen) 582 } else { 583 item.Item.Draw(screen) 584 } 585 586 // Draw border around primitive. 587 if g.borders { 588 for bx := item.x; bx < item.x+item.w; bx++ { // Top/bottom lines. 589 if bx < 0 || bx >= screenWidth { 590 continue 591 } 592 by := item.y - 1 593 if by >= 0 && by < screenHeight { 594 PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, borderStyle) 595 } 596 by = item.y + item.h 597 if by >= 0 && by < screenHeight { 598 PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, borderStyle) 599 } 600 } 601 for by := item.y; by < item.y+item.h; by++ { // Left/right lines. 602 if by < 0 || by >= screenHeight { 603 continue 604 } 605 bx := item.x - 1 606 if bx >= 0 && bx < screenWidth { 607 PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, borderStyle) 608 } 609 bx = item.x + item.w 610 if bx >= 0 && bx < screenWidth { 611 PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, borderStyle) 612 } 613 } 614 bx, by := item.x-1, item.y-1 // Top-left corner. 615 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { 616 PrintJoinedSemigraphics(screen, bx, by, Borders.TopLeft, borderStyle) 617 } 618 bx, by = item.x+item.w, item.y-1 // Top-right corner. 619 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { 620 PrintJoinedSemigraphics(screen, bx, by, Borders.TopRight, borderStyle) 621 } 622 bx, by = item.x-1, item.y+item.h // Bottom-left corner. 623 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { 624 PrintJoinedSemigraphics(screen, bx, by, Borders.BottomLeft, borderStyle) 625 } 626 bx, by = item.x+item.w, item.y+item.h // Bottom-right corner. 627 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight { 628 PrintJoinedSemigraphics(screen, bx, by, Borders.BottomRight, borderStyle) 629 } 630 } 631 } 632 } 633 634 // MouseHandler returns the mouse handler for this primitive. 635 func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { 636 return g.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { 637 if !g.InRect(event.Position()) { 638 return false, nil 639 } 640 641 // Pass mouse events along to the first child item that takes it. 642 for _, item := range g.items { 643 if item.Item == nil { 644 continue 645 } 646 consumed, capture = item.Item.MouseHandler()(action, event, setFocus) 647 if consumed { 648 return 649 } 650 } 651 652 return 653 }) 654 } 655 656 // InputHandler returns the handler for this primitive. 657 func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { 658 return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { 659 if !g.hasFocus { 660 // Pass event on to child primitive. 661 for _, item := range g.items { 662 if item != nil && item.Item.HasFocus() { 663 if handler := item.Item.InputHandler(); handler != nil { 664 handler(event, setFocus) 665 return 666 } 667 } 668 } 669 return 670 } 671 672 // Process our own key events if we have direct focus. 673 switch event.Key() { 674 case tcell.KeyRune: 675 switch event.Rune() { 676 case 'g': 677 g.rowOffset, g.columnOffset = 0, 0 678 case 'G': 679 g.rowOffset = math.MaxInt32 680 case 'j': 681 g.rowOffset++ 682 case 'k': 683 g.rowOffset-- 684 case 'h': 685 g.columnOffset-- 686 case 'l': 687 g.columnOffset++ 688 } 689 case tcell.KeyHome: 690 g.rowOffset, g.columnOffset = 0, 0 691 case tcell.KeyEnd: 692 g.rowOffset = math.MaxInt32 693 case tcell.KeyUp: 694 g.rowOffset-- 695 case tcell.KeyDown: 696 g.rowOffset++ 697 case tcell.KeyLeft: 698 g.columnOffset-- 699 case tcell.KeyRight: 700 g.columnOffset++ 701 } 702 }) 703 } 704 705 // PasteHandler returns the handler for this primitive. 706 func (g *Grid) PasteHandler() func(pastedText string, setFocus func(p Primitive)) { 707 return g.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) { 708 for _, item := range g.items { 709 if item != nil && item.Item.HasFocus() { 710 if handler := item.Item.PasteHandler(); handler != nil { 711 handler(pastedText, setFocus) 712 return 713 } 714 } 715 } 716 }) 717 }