github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/misc/logos/types.go (about) 1 package logos 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/gdamore/tcell/v2" 8 ) 9 10 // ---------------------------------------- 11 // Page 12 13 // A Page has renderable Elem(ents). 14 type Page struct { 15 Coord // used by parent only. TODO document. 16 Size 17 *Style 18 Attrs 19 Elems []Elem 20 Cursor int // selected cursor element index, or -1. 21 } 22 23 // An elem is something that can draw a portion of itself onto 24 // a view. It has a relative coord and a size. Before it is 25 // drawn, it is rendered. Measure will update its size, while 26 // GetSize() returns the cached size. ProcessEventKey() 27 // returns true if event was consumed. 28 type Elem interface { 29 GetParent() Elem 30 SetParent(Elem) 31 GetCoord() Coord 32 SetCoord(Coord) 33 GetStyle() *Style 34 SetStyle(*Style) 35 GetAttrs() *Attrs 36 GetIsCursor() bool 37 SetIsCursor(bool) 38 GetIsDirty() bool 39 SetIsDirty(bool) 40 GetIsOccluded() bool 41 SetIsOccluded(bool) 42 GetSize() Size 43 Measure() Size 44 Render() bool 45 Draw(offset Coord, dst View) 46 ProcessEventKey(*EventKey) bool 47 String() string 48 StringIndented(indent string) string 49 // NOTE: SetSize(Size) isn't an elem interface, as 50 // containers in general can't force elements to be of a 51 // certain size, but rather prefers drawing out of 52 // bounds; this opinion may distinguishes Logos from 53 // other most gui frameworks. 54 } 55 56 var ( 57 _ Elem = &Page{} 58 _ Elem = &BufferedElemView{} 59 _ Elem = &TextElem{} 60 _ Elem = &Stack{} 61 ) 62 63 // produces a page from a string. 64 // width is the width of the page. 65 // if isCode, width is ignored. 66 func NewPage(s string, width int, isCode bool, style *Style) *Page { 67 page := &Page{ 68 Size: Size{ 69 Width: width, 70 Height: -1, // not set 71 }, 72 Style: style, 73 Elems: nil, // will set 74 Cursor: -1, 75 } 76 elems := []Elem{} 77 if s != "" { 78 pad := style.GetPadding() 79 ypos := 0 + pad.Top 80 xpos := 0 + pad.Left 81 lines := splitLines(s) 82 if isCode { 83 for _, line := range lines { 84 te := NewTextElem(line, style) 85 te.SetParent(page) 86 te.SetCoord(Coord{X: xpos, Y: ypos}) 87 elems = append(elems, te) 88 ypos++ 89 xpos = 0 + pad.Left 90 } 91 } else { 92 for _, line := range lines { 93 words := splitSpaces(line) 94 for _, word := range words { 95 wd := widthOf(word) 96 if width < xpos+wd+pad.Left+pad.Right { 97 if xpos != 0+pad.Left { 98 ypos++ 99 xpos = 0 + pad.Left 100 } 101 } 102 te := NewTextElem(word, style) 103 te.SetParent(page) 104 te.SetCoord(Coord{X: xpos, Y: ypos}) 105 elems = append(elems, te) 106 xpos += te.Width // size of word 107 xpos += 1 // space after each word (not written) 108 } 109 ypos++ 110 xpos = 0 + pad.Left 111 } 112 } 113 } 114 page.Elems = elems 115 page.Measure() 116 page.SetIsDirty(true) 117 return page 118 } 119 120 func (pg *Page) StringIndented(indent string) string { 121 elines := []string{} 122 eindent := indent + " " 123 for _, elem := range pg.Elems { 124 elines = append(elines, eindent+elem.StringIndented(eindent)) 125 } 126 return fmt.Sprintf("Page%v@%p\n%s", 127 pg.Size, 128 pg, 129 strings.Join(elines, "\n")) 130 } 131 132 func (pg *Page) String() string { 133 return fmt.Sprintf("Page%v{%d}@%p", 134 pg.Size, len(pg.Elems), pg) 135 } 136 137 func (pg *Page) NextCoord() Coord { 138 if len(pg.Elems) == 0 { 139 return Coord{X: pg.GetPadding().Left, Y: pg.GetPadding().Top} 140 } else { 141 last := pg.Elems[len(pg.Elems)-1] 142 last.Measure() 143 lcoord := last.GetCoord() 144 lsize := last.GetSize() 145 return Coord{ 146 X: pg.GetPadding().Left, 147 Y: lcoord.Y + lsize.Height, // no spacers by spec. 148 } 149 } 150 } 151 152 func (pg *Page) SetStyle(style *Style) { 153 pg.Style = style 154 } 155 156 // Measures the size of elem and appends to page below the last element, 157 // or if empty, the top-leftmost coordinate exclusive of padding cells. 158 func (pg *Page) AppendElem(elem Elem) { 159 ncoord := pg.NextCoord() 160 elem.SetParent(pg) 161 elem.SetCoord(ncoord) 162 pg.Elems = append(pg.Elems, elem) 163 pg.SetIsDirty(true) 164 } 165 166 // Assumes page starts at 0,0. 167 func (pg *Page) Measure() Size { 168 pad := pg.GetPadding() 169 maxX := pad.Left 170 maxY := pad.Top 171 for _, view := range pg.Elems { 172 coord := view.GetCoord() 173 size := view.GetSize() 174 if maxX < coord.X+size.Width { 175 maxX = coord.X + size.Width 176 } 177 if maxY < coord.Y+size.Height { 178 maxY = coord.Y + size.Height 179 } 180 } 181 size := Size{ 182 Width: maxX + pad.Right, 183 Height: maxY + pad.Bottom, 184 } 185 pg.Size = size 186 return size 187 } 188 189 /* 190 Page draw logic: 191 Let's say we want to draw a Page. We want to draw it onto 192 some buffer, or more specially some "view" of a buffer (as 193 a slice of an array is a view into an array buffer). 194 195 Page virtual bounds: 196 0 - - - - - - - - - - + 197 :\ : 198 : \ (3,3) : 199 : @-----------+ : 200 : |View | : 201 : | | : 202 : +-----------+ : 203 : : 204 + - - - - - - - - - - + 205 206 0 is the origin point for the Page. 207 @ is an offset within the page. @ here is (3,3). 208 It is where the View is conceptually placed, but 209 otherwise the View isn't aware where @ is. 210 This offset is passed in as an argument 'offset'. 211 212 NOTE: Offset is relative in the base page. To offset the 213 drawing position in the view (e.g. to only write on the 214 right half of the buffer view), derive another view from 215 the original. 216 217 The View is associated with an underlying (base) buffer. 218 219 Page virtual bounds: 220 0 - - - - - - - - - - + 221 +=:=====================:===+ <-- underlying Buffer 222 | : : | 223 | : @-------------+ : | 224 | : |View | : | 225 | : |.Offset=(5,2)| : | 226 | : +-------------+ : | 227 | : : | 228 | + - - - - - - - - - - + | 229 +===========================+ 230 231 Each element must be drawn onto the buffer view with the 232 right offset algebra applied. Here is a related diagram 233 showing the buffer in relation to page elements. 234 235 Page virtual bounds: 236 0 - - - - - - - - - - - + 237 :elem 1 |elem 2 : 238 : @------------+ : 239 : |View | : 240 + - - - | - E - - - -|- + 241 :elem 3 | |elem 4 | : 242 : +------------+ : 243 : | : 244 + - - - - - - - - - - - + 245 246 In this example the page is composed of four element tiles. 247 E is elem-4's offset relative to 0, the page's origin. To 248 draw the top-left portion of elem-4 onto the buffer slice 249 as shown, the element is drawn with an offset of @-E, which 250 is negative and indicates that the element should be drawn 251 offset positively (right and bottom) from @. 252 */ 253 254 // Unlike TextElem or BufferedElemView, a Page doesn't keep 255 // its own buffer. Its render function calls the elements' 256 // render functions, and the element buffers are combined 257 // during Draw(). There is a need for distinction because 258 // Draw() can't be too slow, so Render() is about optimizing 259 // Draw() calls. The distinction between *Page and 260 // BufferedElemView gives the user more flexibility. 261 func (pg *Page) Render() (updated bool) { 262 if !pg.GetIsDirty() { 263 return 264 } else { 265 defer pg.SetIsDirty(false) 266 } 267 for _, elem := range pg.Elems { 268 elem.Render() 269 } 270 return true 271 } 272 273 // Draw the rendered page elements onto the view. 274 func (pg *Page) Draw(offset Coord, view View) { 275 style := pg.GetStyle() 276 border := style.GetBorder() 277 minX, maxX, minY, maxY := computeIntersection(pg.Size, offset, view.Bounds) 278 // First, draw page background style. 279 for y := minY; y < maxY; y++ { 280 for x := minX; x < maxX; x++ { 281 xo, yo := x-offset.X, y-offset.Y 282 vcell := view.GetCell(xo, yo) 283 // Draw area and border. 284 if x == 0 { 285 if y == pg.Size.Height-1 { 286 // handle this case first so if height is 1, 287 // this corner is preferred. 288 vcell.SetValue(border.BLCorner(), 1, style, pg) 289 } else if y == 0 { 290 vcell.SetValue(border.TLCorner(), 1, style, pg) 291 } else { 292 vcell.SetValue(border.LeftBorder(y), 1, style, pg) 293 } 294 } else if x == pg.Size.Width-1 { 295 if y == pg.Size.Height-1 { 296 // ditto for future left-right language support. 297 vcell.SetValue(border.BRCorner(), 1, style, pg) 298 } else if y == 0 { 299 vcell.SetValue(border.TRCorner(), 1, style, pg) 300 } else { 301 vcell.SetValue(border.RightBorder(y), 1, style, pg) 302 } 303 } else if y == 0 { 304 vcell.SetValue(border.TopBorder(x), 1, style, pg) 305 } else if y == pg.Size.Height-1 { 306 vcell.SetValue(border.BottomBorder(x), 1, style, pg) 307 } else { // Draw area. 308 vcell.SetValue(" ", 1, style, pg) 309 } 310 } 311 } 312 // Then, draw elems. 313 for _, elem := range pg.Elems { 314 eoffset := offset.Sub(elem.GetCoord()) 315 elem.Draw(eoffset, view) 316 } 317 } 318 319 type EventKey = tcell.EventKey 320 321 func (pg *Page) ProcessEventKey(ev *EventKey) bool { 322 switch ev.Key() { 323 case tcell.KeyEsc: 324 return false 325 case tcell.KeyUp: 326 pg.DecCursor(true) 327 case tcell.KeyDown: 328 pg.IncCursor(true) 329 case tcell.KeyLeft: 330 pg.DecCursor(false) 331 case tcell.KeyRight: 332 pg.IncCursor(false) 333 case tcell.KeyEnter: 334 if pg.Cursor == -1 { 335 // as if pressed down 336 pg.IncCursor(true) 337 return true 338 } 339 // XXX this is a test. 340 st := StackOf(pg) 341 celem := pg.Elems[pg.Cursor] 342 coord := AbsCoord(celem).Sub(AbsCoord(st)) 343 page := NewPage("this is a test", 80, false, pg.Style) 344 coord.Y += 1 345 coord.X += 2 346 page.SetCoord(coord) 347 st.PushLayer(page) 348 default: 349 return false 350 } 351 // Leave as true for convenience in cases above. 352 // If a key event wasn't consumed, return false. 353 return true 354 } 355 356 func (pg *Page) IncCursor(isVertical bool) { 357 if pg.Cursor == -1 { 358 if len(pg.Elems) == 0 { 359 // nothing to select. 360 } else { 361 pg.Cursor = 0 362 pg.Elems[pg.Cursor].SetIsCursor(true) 363 } 364 } else { 365 pg.Elems[pg.Cursor].SetIsCursor(false) 366 pg.Cursor++ 367 if pg.Cursor == len(pg.Elems) { 368 pg.Cursor = 0 // roll back. 369 } 370 pg.Elems[pg.Cursor].SetIsCursor(true) 371 } 372 } 373 374 func (pg *Page) DecCursor(isVertical bool) { 375 if pg.Cursor == -1 { 376 if len(pg.Elems) == 0 { 377 // nothing to select. 378 } else { 379 pg.Cursor = len(pg.Elems) - 1 380 pg.Elems[pg.Cursor].SetIsCursor(true) 381 } 382 } else { 383 pg.Elems[pg.Cursor].SetIsCursor(false) 384 pg.Cursor-- 385 if pg.Cursor == -1 { 386 pg.Cursor = len(pg.Elems) - 1 // roll forward. 387 } 388 pg.Elems[pg.Cursor].SetIsCursor(true) 389 } 390 } 391 392 // ---------------------------------------- 393 // TextElem 394 395 type TextElem struct { 396 Coord 397 Size 398 *Style // ignores padding. 399 Attrs 400 Text string 401 *Buffer 402 } 403 404 func NewTextElem(text string, style *Style) *TextElem { 405 te := &TextElem{ 406 Style: style, 407 Text: text, 408 Buffer: NewBuffer(Size{ 409 Height: 1, 410 Width: widthOf(text), 411 }), 412 } 413 te.Measure() 414 te.SetIsDirty(true) 415 return te 416 } 417 418 func (tel *TextElem) SetStyle(style *Style) { 419 tel.Style = style 420 } 421 422 func (tel *TextElem) StringIndented(indent string) string { 423 return tel.String() 424 } 425 426 func (tel *TextElem) String() string { 427 return fmt.Sprintf("Text{%q}", tel.Text) 428 } 429 430 func (tel *TextElem) Measure() Size { 431 size := Size{ 432 Height: 1, 433 Width: widthOf(tel.Text), 434 } 435 tel.Size = size 436 return size 437 } 438 439 func (tel *TextElem) Render() (updated bool) { 440 if tel.Height != 1 { 441 panic("should not happen") 442 } 443 if !tel.GetIsDirty() { 444 return 445 } else { 446 defer tel.SetIsDirty(false) 447 } 448 tel.Buffer.Reset() 449 style := tel.GetStyle() 450 runes := toRunes(tel.Text) 451 i := 0 452 for 0 < len(runes) { 453 s, w, n := nextCharacter(runes) 454 if n == 0 { 455 panic(fmt.Sprintf( 456 "unexpected error reading next character from runes %v", 457 runes)) 458 } else { 459 runes = runes[n:] 460 } 461 cell := tel.Buffer.GetCell(i, 0) 462 cell.SetValue(s, w, style, tel) 463 for j := 1; j < w; j++ { 464 cell := tel.Buffer.GetCell(i+j, 0) 465 cell.SetValue("", 0, style, tel) // clear next cells 466 } 467 i += w 468 } 469 if i != tel.Buffer.Width { 470 panic(fmt.Sprintf( 471 "wrote %d cells but there are %d in buffer with text %q", 472 i, tel.Buffer.Width, tel.Text)) 473 } 474 return true 475 } 476 477 func (tel *TextElem) Draw(offset Coord, view View) { 478 minX, maxX, minY, maxY := computeIntersection(tel.Size, offset, view.Bounds) 479 for y := minY; y < maxY; y++ { 480 if minY != 0 { 481 panic("should not happen") 482 } 483 for x := minX; x < maxX; x++ { 484 bcell := tel.Buffer.GetCell(x, y) 485 vcell := view.GetCell(x-offset.X, y-offset.Y) 486 vcell.SetValueFromCell(bcell) 487 } 488 } 489 } 490 491 func (tel *TextElem) ProcessEventKey(ev *EventKey) bool { 492 return false // TODO: clipboard. 493 } 494 495 // ---------------------------------------- 496 // misc. 497 498 type Color = tcell.Color 499 500 // Style is purely visual and has no side effects. 501 // It is generally referred to by pointer; you may need to copy before 502 // modifying. 503 type Style struct { 504 Foreground Color 505 Background Color 506 Padding Padding 507 Border Border 508 StyleFlags 509 Other []KVPair 510 CursorStyle *Style 511 } 512 513 func DefaultStyle() *Style { 514 return &Style{ 515 Foreground: gDefaultForeground, 516 Background: gDefaultBackground, 517 CursorStyle: &Style{ 518 Background: tcell.ColorYellow, 519 }, 520 } 521 } 522 523 var ( 524 gDefaultStyle = DefaultStyle() 525 gDefaultForeground = tcell.ColorBlack 526 gDefaultBackground = tcell.ColorLightBlue 527 ) 528 529 func (st *Style) Copy() *Style { 530 st2 := *st 531 return &st2 532 } 533 534 func (st *Style) GetStyle() *Style { 535 return st 536 } 537 538 func (st *Style) GetForeground() Color { 539 if st == nil { 540 return gDefaultStyle.Foreground 541 } else { 542 return st.Foreground 543 } 544 } 545 546 func (st *Style) GetBackground() Color { 547 if st == nil { 548 return gDefaultStyle.Background 549 } else { 550 return st.Background 551 } 552 } 553 554 func (st *Style) GetPadding() Padding { 555 if st == nil { 556 return gDefaultStyle.Padding 557 } else { 558 return st.Padding 559 } 560 } 561 562 func (st *Style) GetBorder() *Border { 563 if st == nil { 564 return &gDefaultStyle.Border 565 } else { 566 return &st.Border 567 } 568 } 569 570 func (st *Style) GetCursorStyle() *Style { 571 if st == nil { 572 return gDefaultStyle.CursorStyle 573 } else if st.CursorStyle == nil { 574 return st 575 } else { 576 return st.CursorStyle 577 } 578 } 579 580 // NOTE: this should only be called during the last step when 581 // writing to screen. The receiver must not be nil and must 582 // not be modified, and the result is a value, not the style 583 // of any particular element. 584 func (st *Style) WithAttrs(attrs *Attrs) (res Style) { 585 if st == nil { 586 panic("unexpected nil style") 587 } 588 if attrs.GetIsCursor() { 589 res = *st.GetCursorStyle() 590 } else { 591 res = *st 592 } 593 if attrs.GetIsOccluded() { 594 res.SetIsShaded(true) 595 } 596 return 597 } 598 599 func (st Style) GetTStyle() (tst tcell.Style) { 600 if st.Foreground.Valid() { 601 tst = tst.Foreground(st.Foreground) 602 } else { 603 tst = tst.Foreground(gDefaultForeground) 604 } 605 if st.Background.Valid() { 606 tst = tst.Background(st.Background) 607 } else { 608 tst = tst.Background(gDefaultBackground) 609 } 610 if st.GetIsShaded() { 611 tst = tst.Dim(true) 612 tst = tst.Background(tcell.ColorGray) 613 } 614 // TODO StyleFlags 615 return tst 616 } 617 618 type StyleFlags uint32 619 620 func (sf StyleFlags) GetIsDim() bool { 621 return (sf & StyleFlagDim) != 0 622 } 623 624 func (sf *StyleFlags) SetIsDim(id bool) { 625 if id { 626 *sf |= StyleFlagDim 627 } else { 628 *sf &= ^StyleFlagDim 629 } 630 } 631 632 func (sf StyleFlags) GetIsShaded() bool { 633 return (sf & StyleFlagShaded) != 0 634 } 635 636 func (sf *StyleFlags) SetIsShaded(id bool) { 637 if id { 638 *sf |= StyleFlagShaded 639 } else { 640 *sf &= ^StyleFlagShaded 641 } 642 } 643 644 const StyleFlagNone StyleFlags = 0 645 646 const ( 647 StyleFlagBold StyleFlags = 1 << iota 648 StyleFlagDim 649 StyleFlagShaded 650 StyleFlagBlink 651 StyleFlagUnderline 652 StyleFlagItalic 653 StyleFlagStrikeThrough 654 ) 655 656 // Attrs have side effects in the Logos system; 657 // for example, the lone cursor element (one with AttrFlagIsCursor set) 658 // is where most key events are sent to. 659 type Attrs struct { 660 Parent Elem 661 AttrFlags 662 Other []KVPair 663 } 664 665 func (tt *Attrs) GetAttrs() *Attrs { 666 return tt 667 } 668 669 func (tt *Attrs) GetParent() Elem { 670 return tt.Parent 671 } 672 673 func (tt *Attrs) SetParent(p Elem) { 674 if tt.Parent != nil && tt.Parent != p { 675 panic("parent already set") 676 } 677 tt.Parent = p 678 } 679 680 func (tt *Attrs) GetIsCursor() bool { 681 return (tt.AttrFlags & AttrFlagIsCursor) != 0 682 } 683 684 func (tt *Attrs) SetIsCursor(ic bool) { 685 if ic { 686 tt.AttrFlags |= AttrFlagIsCursor 687 } else { 688 tt.AttrFlags &= ^AttrFlagIsCursor 689 } 690 tt.SetIsDirty(true) 691 } 692 693 func (tt *Attrs) GetIsDirty() bool { 694 return (tt.AttrFlags & AttrFlagIsDirty) != 0 695 } 696 697 func (tt *Attrs) SetIsDirty(id bool) { 698 if id { 699 tt.AttrFlags |= AttrFlagIsDirty 700 if tt.Parent != nil { 701 tt.Parent.SetIsDirty(true) 702 } 703 } else { 704 tt.AttrFlags &= ^AttrFlagIsDirty 705 } 706 } 707 708 func (tt *Attrs) GetIsOccluded() bool { 709 return (tt.AttrFlags & AttrFlagIsOccluded) != 0 710 } 711 712 func (tt *Attrs) SetIsOccluded(ic bool) { 713 if ic { 714 tt.AttrFlags |= AttrFlagIsOccluded 715 } else { 716 tt.AttrFlags &= ^AttrFlagIsOccluded 717 } 718 tt.SetIsDirty(true) 719 } 720 721 func (tt *Attrs) Merge(ot *Attrs) { 722 if ot.Parent != nil { 723 tt.Parent = ot.Parent 724 } 725 tt.AttrFlags |= ot.AttrFlags 726 tt.Other = ot.Other // TODO merge by key. 727 } 728 729 // ---------------------------------------- 730 // AttrFlags 731 732 // NOTE: AttrFlags are merged with a simple or-assign op. 733 type AttrFlags uint32 734 735 func (af AttrFlags) GetAttrFlags() AttrFlags { 736 return af 737 } 738 739 const AttrFlagNone AttrFlags = 0 740 741 const ( 742 AttrFlagIsCursor AttrFlags = 1 << iota // is current cursor 743 AttrFlagIsSelected // is selected (among possibly others) 744 AttrFlagIsOccluded // is hidden due to stack 745 AttrFlagIsDirty // is dirty (not yet used) 746 ) 747 748 type KVPair struct { 749 Key string 750 Value interface{} 751 } 752 753 // ---------------------------------------- 754 // computeIntersection() 755 756 // els: element size 757 // elo: offset within element 758 // vws: view size 759 // minX,maxX,minY,maxY are relative to el. 760 // maxX and maxY are exclusive. 761 func computeIntersection(els Size, elo Coord, vws Size) (minX, maxX, minY, maxY int) { 762 if elo.X < 0 { 763 /* 764 View 765 +----------+ 766 | [Elem__|____] 767 +----------+ 768 x 0 769 */ 770 minX = 0 771 } else { 772 /* 773 View 774 +----------+ 775 [____|__Elem] | 776 +----------+ 777 0 x 778 */ 779 minX = elo.X 780 } 781 if els.Width <= vws.Width+elo.X { 782 /* 783 View 784 +----------+ 785 [____|__Elem] | 786 +----------+ 787 W w+x 788 */ 789 maxX = els.Width 790 } else { 791 /* 792 View 793 +----------+ 794 | [Elem__|____] 795 +----------+ 796 w+x W 797 */ 798 maxX = vws.Width + elo.X 799 } 800 if elo.Y < 0 { 801 minY = 0 802 } else { 803 minY = elo.Y 804 } 805 if els.Height <= vws.Height+elo.Y { 806 maxY = els.Height 807 } else { 808 maxY = vws.Height + elo.Y 809 } 810 return 811 } 812 813 // ---------------------------------------- 814 // Misc simple types 815 816 type Padding struct { 817 Left int 818 Top int 819 Right int 820 Bottom int 821 } 822 823 func (pd Padding) GetPadding() Padding { 824 return pd 825 } 826 827 // A border can only have width 0 or 1, and is part of the padding. 828 // Each string should represent a character of width 1. 829 type Border struct { 830 Corners [4]string // starts upper-left and clockwise, "" draws no corner. 831 TopLine []string // nil if no top border. 832 BotLine []string // nil if no bottom border. 833 LeftLine []string // nil if no left border. 834 RightLine []string // nil if no right border. 835 } 836 837 func DefaultBorder() Border { 838 return Border{ 839 Corners: [4]string{ 840 string(tcell.RuneULCorner), 841 string(tcell.RuneURCorner), 842 string(tcell.RuneLRCorner), 843 string(tcell.RuneLLCorner), 844 }, 845 TopLine: []string{string(tcell.RuneHLine)}, 846 BotLine: []string{string(tcell.RuneHLine)}, 847 LeftLine: []string{string(tcell.RuneVLine)}, 848 RightLine: []string{string(tcell.RuneVLine)}, 849 } 850 } 851 852 func LeftBorder() Border { 853 return Border{ 854 Corners: [4]string{ 855 string("\u2553"), 856 "", 857 "", 858 string("\u2559"), 859 }, 860 LeftLine: []string{ 861 string("\u2551"), 862 }, 863 } 864 } 865 866 func orSpace(chr string) string { 867 if chr == "" { 868 return " " 869 } else { 870 return chr 871 } 872 } 873 874 func (br *Border) GetCorner(i int) string { 875 if br == nil { 876 return " " 877 } else { 878 return orSpace(br.Corners[i]) 879 } 880 } 881 882 func (br *Border) TLCorner() string { 883 return br.GetCorner(0) 884 } 885 886 func (br *Border) TRCorner() string { 887 return br.GetCorner(1) 888 } 889 890 func (br *Border) BRCorner() string { 891 return br.GetCorner(2) 892 } 893 894 func (br *Border) BLCorner() string { 895 return br.GetCorner(3) 896 } 897 898 func (br *Border) TopBorder(x int) string { 899 if br == nil || br.TopLine == nil { 900 return " " 901 } else { 902 return br.TopLine[x%len(br.TopLine)] 903 } 904 } 905 906 func (br *Border) BottomBorder(x int) string { 907 if br == nil || br.BotLine == nil { 908 return " " 909 } else { 910 return br.BotLine[x%len(br.BotLine)] 911 } 912 } 913 914 func (br *Border) LeftBorder(y int) string { 915 if br == nil || br.LeftLine == nil { 916 return " " 917 } else { 918 return br.LeftLine[y%len(br.LeftLine)] 919 } 920 } 921 922 func (br *Border) RightBorder(y int) string { 923 if br == nil || br.RightLine == nil { 924 return " " 925 } else { 926 return br.RightLine[y%len(br.RightLine)] 927 } 928 } 929 930 type Size struct { 931 Width int 932 Height int // -1 if not set. 933 } 934 935 func (sz Size) String() string { 936 return fmt.Sprintf("{%d,%d}", sz.Width, sz.Height) 937 } 938 939 func (sz Size) IsZero() bool { 940 return sz.Width == 0 && sz.Height == 0 941 } 942 943 func (sz Size) GetSize() Size { 944 return sz 945 } 946 947 // zero widths or heights are valid. 948 func (sz Size) IsValid() bool { 949 return 0 <= sz.Width && 0 <= sz.Height 950 } 951 952 func (sz Size) IsPositive() bool { 953 return 0 < sz.Width && 0 < sz.Height 954 } 955 956 func (sz Size) SubCoord(crd Coord) Size { 957 if !crd.IsNonNegative() { 958 panic("should not happen") 959 } 960 sz2 := Size{ 961 Width: sz.Width - crd.X, 962 Height: sz.Height - crd.Y, 963 } 964 if !sz2.IsValid() { 965 panic("should not happen") 966 } 967 return sz2 968 } 969 970 type Coord struct { 971 X int 972 Y int 973 } 974 975 func (crd Coord) GetCoord() Coord { 976 return crd 977 } 978 979 func (crd *Coord) SetCoord(nc Coord) { 980 *crd = nc 981 } 982 983 func (crd Coord) IsNonNegative() bool { 984 return 0 <= crd.X && 0 <= crd.Y 985 } 986 987 func (crd Coord) Neg() Coord { 988 return Coord{ 989 X: -crd.X, 990 Y: -crd.Y, 991 } 992 } 993 994 func (crd Coord) Add(crd2 Coord) Coord { 995 return Coord{ 996 X: crd.X + crd2.X, 997 Y: crd.Y + crd2.Y, 998 } 999 } 1000 1001 func (crd Coord) Sub(crd2 Coord) Coord { 1002 return Coord{ 1003 X: crd.X - crd2.X, 1004 Y: crd.Y - crd2.Y, 1005 } 1006 }