...

Source file src/code.rocketnine.space/tslocum/cview/grid.go

Documentation: code.rocketnine.space/tslocum/cview

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

View as plain text