1 package tview
2
3 import (
4 "math"
5
6 "github.com/gdamore/tcell/v2"
7 )
8
9
10 type gridItem struct {
11 Item Primitive
12 Row, Column int
13 Width, Height int
14 MinGridWidth, MinGridHeight int
15 Focus bool
16
17 visible bool
18 x, y, w, h int
19 }
20
21
22
23
24
25
26
27
28
29
30
31 type Grid struct {
32 *Box
33
34
35 items []*gridItem
36
37
38
39 rows, columns []int
40
41
42 minWidth, minHeight int
43
44
45
46 gapRows, gapColumns int
47
48
49
50 rowOffset, columnOffset int
51
52
53
54
55 borders bool
56
57
58 bordersColor tcell.Color
59 }
60
61
62
63
64
65
66
67
68
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107 func (g *Grid) SetColumns(columns ...int) *Grid {
108 g.columns = columns
109 return g
110 }
111
112
113
114
115
116
117
118 func (g *Grid) SetRows(rows ...int) *Grid {
119 g.rows = rows
120 return g
121 }
122
123
124
125
126 func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) *Grid {
127 g.rows = make([]int, numRows)
128 for index := range g.rows {
129 g.rows[index] = rowSize
130 }
131 g.columns = make([]int, numColumns)
132 for index := range g.columns {
133 g.columns[index] = columnSize
134 }
135 return g
136 }
137
138
139
140 func (g *Grid) SetMinSize(row, column int) *Grid {
141 if row < 0 || column < 0 {
142 panic("Invalid minimum row/column size")
143 }
144 g.minHeight, g.minWidth = row, column
145 return g
146 }
147
148
149
150
151 func (g *Grid) SetGap(row, column int) *Grid {
152 if row < 0 || column < 0 {
153 panic("Invalid gap size")
154 }
155 g.gapRows, g.gapColumns = row, column
156 return g
157 }
158
159
160
161
162 func (g *Grid) SetBorders(borders bool) *Grid {
163 g.borders = borders
164 return g
165 }
166
167
168 func (g *Grid) SetBordersColor(color tcell.Color) *Grid {
169 g.bordersColor = color
170 return g
171 }
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199 func (g *Grid) AddItem(p Primitive, row, column, rowSpan, colSpan, minGridHeight, minGridWidth int, focus bool) *Grid {
200 g.items = append(g.items, &gridItem{
201 Item: p,
202 Row: row,
203 Column: column,
204 Height: rowSpan,
205 Width: colSpan,
206 MinGridHeight: minGridHeight,
207 MinGridWidth: minGridWidth,
208 Focus: focus,
209 })
210 return g
211 }
212
213
214
215 func (g *Grid) RemoveItem(p Primitive) *Grid {
216 for index := len(g.items) - 1; index >= 0; index-- {
217 if g.items[index].Item == p {
218 g.items = append(g.items[:index], g.items[index+1:]...)
219 }
220 }
221 return g
222 }
223
224
225 func (g *Grid) Clear() *Grid {
226 g.items = nil
227 return g
228 }
229
230
231
232
233
234
235 func (g *Grid) SetOffset(rows, columns int) *Grid {
236 g.rowOffset, g.columnOffset = rows, columns
237 return g
238 }
239
240
241
242 func (g *Grid) GetOffset() (rows, columns int) {
243 return g.rowOffset, g.columnOffset
244 }
245
246
247 func (g *Grid) Focus(delegate func(p Primitive)) {
248 for _, item := range g.items {
249 if item.Focus {
250 delegate(item.Item)
251 return
252 }
253 }
254 g.Box.Focus(delegate)
255 }
256
257
258 func (g *Grid) HasFocus() bool {
259 for _, item := range g.items {
260 if item.visible && item.Item.HasFocus() {
261 return true
262 }
263 }
264 return g.Box.HasFocus()
265 }
266
267
268 func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
269 return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
270 if !g.hasFocus {
271
272 for _, item := range g.items {
273 if item != nil && item.Item.HasFocus() {
274 if handler := item.Item.InputHandler(); handler != nil {
275 handler(event, setFocus)
276 return
277 }
278 }
279 }
280 return
281 }
282
283
284 switch event.Key() {
285 case tcell.KeyRune:
286 switch event.Rune() {
287 case 'g':
288 g.rowOffset, g.columnOffset = 0, 0
289 case 'G':
290 g.rowOffset = math.MaxInt32
291 case 'j':
292 g.rowOffset++
293 case 'k':
294 g.rowOffset--
295 case 'h':
296 g.columnOffset--
297 case 'l':
298 g.columnOffset++
299 }
300 case tcell.KeyHome:
301 g.rowOffset, g.columnOffset = 0, 0
302 case tcell.KeyEnd:
303 g.rowOffset = math.MaxInt32
304 case tcell.KeyUp:
305 g.rowOffset--
306 case tcell.KeyDown:
307 g.rowOffset++
308 case tcell.KeyLeft:
309 g.columnOffset--
310 case tcell.KeyRight:
311 g.columnOffset++
312 }
313 })
314 }
315
316
317 func (g *Grid) Draw(screen tcell.Screen) {
318 g.Box.DrawForSubclass(screen, g)
319 x, y, width, height := g.GetInnerRect()
320 screenWidth, screenHeight := screen.Size()
321
322
323 items := make(map[Primitive]*gridItem)
324 for _, item := range g.items {
325 item.visible = false
326 if item.Width <= 0 || item.Height <= 0 || width < item.MinGridWidth || height < item.MinGridHeight {
327 continue
328 }
329 previousItem, ok := items[item.Item]
330 if ok && item.MinGridWidth < previousItem.MinGridWidth && item.MinGridHeight < previousItem.MinGridHeight {
331 continue
332 }
333 items[item.Item] = item
334 }
335
336
337 rows := len(g.rows)
338 columns := len(g.columns)
339 for _, item := range items {
340 rowEnd := item.Row + item.Height
341 if rowEnd > rows {
342 rows = rowEnd
343 }
344 columnEnd := item.Column + item.Width
345 if columnEnd > columns {
346 columns = columnEnd
347 }
348 }
349 if rows == 0 || columns == 0 {
350 return
351 }
352
353
354 rowPos := make([]int, rows)
355 rowHeight := make([]int, rows)
356 columnPos := make([]int, columns)
357 columnWidth := make([]int, columns)
358
359
360 remainingWidth := width
361 remainingHeight := height
362 proportionalWidth := 0
363 proportionalHeight := 0
364 for index, row := range g.rows {
365 if row > 0 {
366 if row < g.minHeight {
367 row = g.minHeight
368 }
369 remainingHeight -= row
370 rowHeight[index] = row
371 } else if row == 0 {
372 proportionalHeight++
373 } else {
374 proportionalHeight += -row
375 }
376 }
377 for index, column := range g.columns {
378 if column > 0 {
379 if column < g.minWidth {
380 column = g.minWidth
381 }
382 remainingWidth -= column
383 columnWidth[index] = column
384 } else if column == 0 {
385 proportionalWidth++
386 } else {
387 proportionalWidth += -column
388 }
389 }
390 if g.borders {
391 remainingHeight -= rows + 1
392 remainingWidth -= columns + 1
393 } else {
394 remainingHeight -= (rows - 1) * g.gapRows
395 remainingWidth -= (columns - 1) * g.gapColumns
396 }
397 if rows > len(g.rows) {
398 proportionalHeight += rows - len(g.rows)
399 }
400 if columns > len(g.columns) {
401 proportionalWidth += columns - len(g.columns)
402 }
403
404
405 for index := 0; index < rows; index++ {
406 row := 0
407 if index < len(g.rows) {
408 row = g.rows[index]
409 }
410 if row > 0 {
411 continue
412 } else if row == 0 {
413 row = 1
414 } else {
415 row = -row
416 }
417 rowAbs := row * remainingHeight / proportionalHeight
418 remainingHeight -= rowAbs
419 proportionalHeight -= row
420 if rowAbs < g.minHeight {
421 rowAbs = g.minHeight
422 }
423 rowHeight[index] = rowAbs
424 }
425 for index := 0; index < columns; index++ {
426 column := 0
427 if index < len(g.columns) {
428 column = g.columns[index]
429 }
430 if column > 0 {
431 continue
432 } else if column == 0 {
433 column = 1
434 } else {
435 column = -column
436 }
437 columnAbs := column * remainingWidth / proportionalWidth
438 remainingWidth -= columnAbs
439 proportionalWidth -= column
440 if columnAbs < g.minWidth {
441 columnAbs = g.minWidth
442 }
443 columnWidth[index] = columnAbs
444 }
445
446
447 var columnX, rowY int
448 if g.borders {
449 columnX++
450 rowY++
451 }
452 for index, row := range rowHeight {
453 rowPos[index] = rowY
454 gap := g.gapRows
455 if g.borders {
456 gap = 1
457 }
458 rowY += row + gap
459 }
460 for index, column := range columnWidth {
461 columnPos[index] = columnX
462 gap := g.gapColumns
463 if g.borders {
464 gap = 1
465 }
466 columnX += column + gap
467 }
468
469
470 var focus *gridItem
471 for primitive, item := range items {
472 px := columnPos[item.Column]
473 py := rowPos[item.Row]
474 var pw, ph int
475 for index := 0; index < item.Height; index++ {
476 ph += rowHeight[item.Row+index]
477 }
478 for index := 0; index < item.Width; index++ {
479 pw += columnWidth[item.Column+index]
480 }
481 if g.borders {
482 pw += item.Width - 1
483 ph += item.Height - 1
484 } else {
485 pw += (item.Width - 1) * g.gapColumns
486 ph += (item.Height - 1) * g.gapRows
487 }
488 item.x, item.y, item.w, item.h = px, py, pw, ph
489 item.visible = true
490 if primitive.HasFocus() {
491 focus = item
492 }
493 }
494
495
496 var offsetX, offsetY int
497 add := 1
498 if !g.borders {
499 add = g.gapRows
500 }
501 for index, height := range rowHeight {
502 if index >= g.rowOffset {
503 break
504 }
505 offsetY += height + add
506 }
507 if !g.borders {
508 add = g.gapColumns
509 }
510 for index, width := range columnWidth {
511 if index >= g.columnOffset {
512 break
513 }
514 offsetX += width + add
515 }
516
517
518 var border int
519 if g.borders {
520 border = 1
521 }
522 last := len(rowPos) - 1
523 if rowPos[last]+rowHeight[last]+border-offsetY < height {
524 offsetY = rowPos[last] - height + rowHeight[last] + border
525 }
526 last = len(columnPos) - 1
527 if columnPos[last]+columnWidth[last]+border-offsetX < width {
528 offsetX = columnPos[last] - width + columnWidth[last] + border
529 }
530
531
532 if focus != nil {
533 if focus.y+focus.h-offsetY >= height {
534 offsetY = focus.y - height + focus.h
535 }
536 if focus.y-offsetY < 0 {
537 offsetY = focus.y
538 }
539 if focus.x+focus.w-offsetX >= width {
540 offsetX = focus.x - width + focus.w
541 }
542 if focus.x-offsetX < 0 {
543 offsetX = focus.x
544 }
545 }
546
547
548 var from, to int
549 for index, pos := range rowPos {
550 if pos-offsetY < 0 {
551 from = index + 1
552 }
553 if pos-offsetY < height {
554 to = index
555 }
556 }
557 if g.rowOffset < from {
558 g.rowOffset = from
559 }
560 if g.rowOffset > to {
561 g.rowOffset = to
562 }
563 from, to = 0, 0
564 for index, pos := range columnPos {
565 if pos-offsetX < 0 {
566 from = index + 1
567 }
568 if pos-offsetX < width {
569 to = index
570 }
571 }
572 if g.columnOffset < from {
573 g.columnOffset = from
574 }
575 if g.columnOffset > to {
576 g.columnOffset = to
577 }
578
579
580 borderStyle := tcell.StyleDefault.Background(g.backgroundColor).Foreground(g.bordersColor)
581 for primitive, item := range items {
582
583 if !item.visible {
584 continue
585 }
586 item.x -= offsetX
587 item.y -= offsetY
588 if item.x >= width || item.x+item.w <= 0 || item.y >= height || item.y+item.h <= 0 {
589 item.visible = false
590 continue
591 }
592 if item.x+item.w > width {
593 item.w = width - item.x
594 }
595 if item.y+item.h > height {
596 item.h = height - item.y
597 }
598 if item.x < 0 {
599 item.w += item.x
600 item.x = 0
601 }
602 if item.y < 0 {
603 item.h += item.y
604 item.y = 0
605 }
606 if item.w <= 0 || item.h <= 0 {
607 item.visible = false
608 continue
609 }
610 item.x += x
611 item.y += y
612 primitive.SetRect(item.x, item.y, item.w, item.h)
613
614
615 if item == focus {
616 defer primitive.Draw(screen)
617 } else {
618 primitive.Draw(screen)
619 }
620
621
622 if g.borders {
623 for bx := item.x; bx < item.x+item.w; bx++ {
624 if bx < 0 || bx >= screenWidth {
625 continue
626 }
627 by := item.y - 1
628 if by >= 0 && by < screenHeight {
629 PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, borderStyle)
630 }
631 by = item.y + item.h
632 if by >= 0 && by < screenHeight {
633 PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, borderStyle)
634 }
635 }
636 for by := item.y; by < item.y+item.h; by++ {
637 if by < 0 || by >= screenHeight {
638 continue
639 }
640 bx := item.x - 1
641 if bx >= 0 && bx < screenWidth {
642 PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, borderStyle)
643 }
644 bx = item.x + item.w
645 if bx >= 0 && bx < screenWidth {
646 PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, borderStyle)
647 }
648 }
649 bx, by := item.x-1, item.y-1
650 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
651 PrintJoinedSemigraphics(screen, bx, by, Borders.TopLeft, borderStyle)
652 }
653 bx, by = item.x+item.w, item.y-1
654 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
655 PrintJoinedSemigraphics(screen, bx, by, Borders.TopRight, borderStyle)
656 }
657 bx, by = item.x-1, item.y+item.h
658 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
659 PrintJoinedSemigraphics(screen, bx, by, Borders.BottomLeft, borderStyle)
660 }
661 bx, by = item.x+item.w, item.y+item.h
662 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
663 PrintJoinedSemigraphics(screen, bx, by, Borders.BottomRight, borderStyle)
664 }
665 }
666 }
667 }
668
669
670 func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
671 return g.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
672 if !g.InRect(event.Position()) {
673 return false, nil
674 }
675
676
677 for _, item := range g.items {
678 if item.Item == nil {
679 continue
680 }
681 consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
682 if consumed {
683 return
684 }
685 }
686
687 return
688 })
689 }
690
View as plain text