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