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