1 package cview
2
3 import (
4 "math"
5 "sync"
6
7 "github.com/gdamore/tcell/v2"
8 )
9
10
11 type gridItem struct {
12 Item Primitive
13 Row, Column int
14 Width, Height int
15 MinGridWidth, MinGridHeight int
16 Focus bool
17
18 visible bool
19 x, y, w, h int
20 }
21
22
23
24
25
26
27
28
29
30 type Grid struct {
31 *Box
32
33
34 items []*gridItem
35
36
37
38 rows, columns []int
39
40
41 minWidth, minHeight int
42
43
44
45 gapRows, gapColumns int
46
47
48
49 rowOffset, columnOffset int
50
51
52
53
54 borders bool
55
56
57 bordersColor tcell.Color
58
59 sync.RWMutex
60 }
61
62
63
64
65
66
67
68
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
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
108 func (g *Grid) SetColumns(columns ...int) {
109 g.Lock()
110 defer g.Unlock()
111
112 g.columns = columns
113 }
114
115
116
117
118
119
120
121 func (g *Grid) SetRows(rows ...int) {
122 g.Lock()
123 defer g.Unlock()
124
125 g.rows = rows
126 }
127
128
129
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
145
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
157
158
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
170
171
172 func (g *Grid) SetBorders(borders bool) {
173 g.Lock()
174 defer g.Unlock()
175
176 g.borders = borders
177 }
178
179
180 func (g *Grid) SetBordersColor(color tcell.Color) {
181 g.Lock()
182 defer g.Unlock()
183
184 g.bordersColor = color
185 }
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
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
230
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
243 func (g *Grid) Clear() {
244 g.Lock()
245 defer g.Unlock()
246
247 g.items = nil
248 }
249
250
251
252
253
254
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
263
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
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
290 func (g *Grid) Blur() {
291 g.Lock()
292 defer g.Unlock()
293
294 g.hasFocus = false
295 }
296
297
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
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
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
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
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
375 }
376
377
378 rowPos := make([]int, rows)
379 rowHeight := make([]int, rows)
380 columnPos := make([]int, columns)
381 columnWidth := make([]int, columns)
382
383
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
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
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
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
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
500 var focus *gridItem
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
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
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
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
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
610 for primitive, item := range items {
611
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
644 if item == focus {
645 defer primitive.Draw(screen)
646 } else {
647 primitive.Draw(screen)
648 }
649
650
651 if g.borders {
652 for bx := item.x; bx < item.x+item.w; bx++ {
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++ {
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
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
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
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
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
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
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