1 package cview
2
3 import (
4 "math"
5 "sync"
6
7 "github.com/gdamore/tcell/v3"
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 continue
436 } else if row == 0 {
437 row = 1
438 } else {
439 row = -row
440 }
441 rowAbs := row * remainingHeight / proportionalHeight
442 remainingHeight -= rowAbs
443 proportionalHeight -= row
444 if rowAbs < g.minHeight {
445 rowAbs = g.minHeight
446 }
447 rowHeight[index] = rowAbs
448 }
449 for index := 0; index < columns; index++ {
450 column := 0
451 if index < len(g.columns) {
452 column = g.columns[index]
453 }
454 if column > 0 {
455 continue
456 } else if column == 0 {
457 column = 1
458 } else {
459 column = -column
460 }
461 columnAbs := column * remainingWidth / proportionalWidth
462 remainingWidth -= columnAbs
463 proportionalWidth -= column
464 if columnAbs < g.minWidth {
465 columnAbs = g.minWidth
466 }
467 columnWidth[index] = columnAbs
468 }
469
470
471 var columnX, rowY int
472 if g.borders {
473 columnX++
474 rowY++
475 }
476 for index, row := range rowHeight {
477 rowPos[index] = rowY
478 gap := g.gapRows
479 if g.borders {
480 gap = 1
481 }
482 rowY += row + gap
483 }
484 for index, column := range columnWidth {
485 columnPos[index] = columnX
486 gap := g.gapColumns
487 if g.borders {
488 gap = 1
489 }
490 columnX += column + gap
491 }
492
493
494 var focus *gridItem
495 for primitive, item := range items {
496 px := columnPos[item.Column]
497 py := rowPos[item.Row]
498 var pw, ph int
499 for index := 0; index < item.Height; index++ {
500 ph += rowHeight[item.Row+index]
501 }
502 for index := 0; index < item.Width; index++ {
503 pw += columnWidth[item.Column+index]
504 }
505 if g.borders {
506 pw += item.Width - 1
507 ph += item.Height - 1
508 } else {
509 pw += (item.Width - 1) * g.gapColumns
510 ph += (item.Height - 1) * g.gapRows
511 }
512 item.x, item.y, item.w, item.h = px, py, pw, ph
513 item.visible = true
514 if primitive.GetFocusable().HasFocus() {
515 focus = item
516 }
517 }
518
519
520 var offsetX, offsetY int
521 add := 1
522 if !g.borders {
523 add = g.gapRows
524 }
525 for index, height := range rowHeight {
526 if index >= g.rowOffset {
527 break
528 }
529 offsetY += height + add
530 }
531 if !g.borders {
532 add = g.gapColumns
533 }
534 for index, width := range columnWidth {
535 if index >= g.columnOffset {
536 break
537 }
538 offsetX += width + add
539 }
540
541
542 var border int
543 if g.borders {
544 border = 1
545 }
546 last := len(rowPos) - 1
547 if rowPos[last]+rowHeight[last]+border-offsetY < height {
548 offsetY = rowPos[last] - height + rowHeight[last] + border
549 }
550 last = len(columnPos) - 1
551 if columnPos[last]+columnWidth[last]+border-offsetX < width {
552 offsetX = columnPos[last] - width + columnWidth[last] + border
553 }
554
555
556 if focus != nil {
557 if focus.y+focus.h-offsetY >= height {
558 offsetY = focus.y - height + focus.h
559 }
560 if focus.y-offsetY < 0 {
561 offsetY = focus.y
562 }
563 if focus.x+focus.w-offsetX >= width {
564 offsetX = focus.x - width + focus.w
565 }
566 if focus.x-offsetX < 0 {
567 offsetX = focus.x
568 }
569 }
570
571
572 var from, to int
573 for index, pos := range rowPos {
574 if pos-offsetY < 0 {
575 from = index + 1
576 }
577 if pos-offsetY < height {
578 to = index
579 }
580 }
581 if g.rowOffset < from {
582 g.rowOffset = from
583 }
584 if g.rowOffset > to {
585 g.rowOffset = to
586 }
587 from, to = 0, 0
588 for index, pos := range columnPos {
589 if pos-offsetX < 0 {
590 from = index + 1
591 }
592 if pos-offsetX < width {
593 to = index
594 }
595 }
596 if g.columnOffset < from {
597 g.columnOffset = from
598 }
599 if g.columnOffset > to {
600 g.columnOffset = to
601 }
602
603
604 for primitive, item := range items {
605
606 if !item.visible {
607 continue
608 }
609 item.x -= offsetX
610 item.y -= offsetY
611 if item.x >= width || item.x+item.w <= 0 || item.y >= height || item.y+item.h <= 0 {
612 item.visible = false
613 continue
614 }
615 if item.x+item.w > width {
616 item.w = width - item.x
617 }
618 if item.y+item.h > height {
619 item.h = height - item.y
620 }
621 if item.x < 0 {
622 item.w += item.x
623 item.x = 0
624 }
625 if item.y < 0 {
626 item.h += item.y
627 item.y = 0
628 }
629 if item.w <= 0 || item.h <= 0 {
630 item.visible = false
631 continue
632 }
633 item.x += x
634 item.y += y
635 primitive.SetRect(item.x, item.y, item.w, item.h)
636
637
638 if item == focus {
639 defer primitive.Draw(screen)
640 } else {
641 primitive.Draw(screen)
642 }
643
644
645 if g.borders {
646 for bx := item.x; bx < item.x+item.w; bx++ {
647 if bx < 0 || bx >= screenWidth {
648 continue
649 }
650 by := item.y - 1
651 if by >= 0 && by < screenHeight {
652 PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, g.bordersColor)
653 }
654 by = item.y + item.h
655 if by >= 0 && by < screenHeight {
656 PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, g.bordersColor)
657 }
658 }
659 for by := item.y; by < item.y+item.h; by++ {
660 if by < 0 || by >= screenHeight {
661 continue
662 }
663 bx := item.x - 1
664 if bx >= 0 && bx < screenWidth {
665 PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, g.bordersColor)
666 }
667 bx = item.x + item.w
668 if bx >= 0 && bx < screenWidth {
669 PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, g.bordersColor)
670 }
671 }
672 bx, by := item.x-1, item.y-1
673 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
674 PrintJoinedSemigraphics(screen, bx, by, Borders.TopLeft, g.bordersColor)
675 }
676 bx, by = item.x+item.w, item.y-1
677 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
678 PrintJoinedSemigraphics(screen, bx, by, Borders.TopRight, g.bordersColor)
679 }
680 bx, by = item.x-1, item.y+item.h
681 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
682 PrintJoinedSemigraphics(screen, bx, by, Borders.BottomLeft, g.bordersColor)
683 }
684 bx, by = item.x+item.w, item.y+item.h
685 if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
686 PrintJoinedSemigraphics(screen, bx, by, Borders.BottomRight, g.bordersColor)
687 }
688 }
689 }
690 }
691
692
693 func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
694 return g.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
695 if !g.InRect(event.Position()) {
696 return false, nil
697 }
698
699
700 x, y := event.Position()
701 for _, item := range g.items {
702 rectX, rectY, width, height := item.Item.GetRect()
703 inRect := x >= rectX && x < rectX+width && y >= rectY && y < rectY+height
704 if !inRect {
705 continue
706 }
707
708 consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
709 if consumed {
710 return
711 }
712 }
713
714 return
715 })
716 }
717
View as plain text