github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/box.go (about)

     1  package main
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/xyproto/vt100"
     7  	"github.com/xyproto/wordwrap"
     8  )
     9  
    10  // Box is a position, width and height
    11  type Box struct {
    12  	X int
    13  	Y int
    14  	W int
    15  	H int
    16  }
    17  
    18  // BoxTheme contains the runes used to draw boxes
    19  type BoxTheme struct {
    20  	LowerEdge  *vt100.AttributeColor
    21  	UpperEdge  *vt100.AttributeColor
    22  	Highlight  *vt100.AttributeColor
    23  	Text       *vt100.AttributeColor
    24  	Background *vt100.AttributeColor
    25  	Foreground *vt100.AttributeColor
    26  	HB         rune
    27  	HT         rune
    28  	VR         rune
    29  	VL         rune
    30  	BR         rune
    31  	BL         rune
    32  	TR         rune
    33  	TL         rune
    34  	EdgeLeftT  rune
    35  	EdgeRightT rune
    36  }
    37  
    38  // NewBox creates a new box/container
    39  func NewBox() *Box {
    40  	return &Box{0, 0, 0, 0}
    41  }
    42  
    43  // NewBoxTheme creates a new theme/style for a box/container, based on the editor theme
    44  func (e *Editor) NewBoxTheme() *BoxTheme {
    45  	return &BoxTheme{
    46  		TL:         '╭', // top left
    47  		TR:         '╮', // top right
    48  		BL:         '╰', // bottom left
    49  		BR:         '╯', // bottom right
    50  		VL:         '│', // vertical line, left side
    51  		VR:         '│', // vertical line, right side
    52  		HT:         '─', // horizontal line
    53  		HB:         '─', // horizontal bottom line
    54  		EdgeLeftT:  '├',
    55  		EdgeRightT: '┤',
    56  		Foreground: &e.Foreground,
    57  		Background: &e.BoxBackground,
    58  		Text:       &e.BoxTextColor,
    59  		Highlight:  &e.BoxHighlight,
    60  		UpperEdge:  &e.BoxUpperEdge,
    61  		LowerEdge:  &e.BoxTextColor,
    62  	}
    63  }
    64  
    65  // NewCanvasBox creates a new box/container for the entire canvas/screen
    66  func NewCanvasBox(c *vt100.Canvas) *Box {
    67  	w := int(c.W())
    68  	h := int(c.H())
    69  	return &Box{0, 0, w, h}
    70  }
    71  
    72  // Fill will place a Box so that it fills the entire given container.
    73  func (b *Box) Fill(container *Box) {
    74  	b.X = container.X
    75  	b.Y = container.Y
    76  	b.W = container.W
    77  	b.H = container.H
    78  }
    79  
    80  // FillWithMargins will place a Box inside a given container, with the given margins.
    81  // Margins are given in number of characters.
    82  func (b *Box) FillWithMargins(container *Box, xmargins, ymargins int) {
    83  	b.Fill(container)
    84  	b.X += xmargins
    85  	b.Y += ymargins
    86  	b.W -= xmargins * 2
    87  	b.H -= ymargins * 2
    88  }
    89  
    90  // UpperRightPlacement will place a box in the upper right corner of a container, like a little window
    91  func (b *Box) UpperRightPlacement(container *Box, minWidth int) {
    92  	w := float64(container.W)
    93  	h := float64(container.H)
    94  	b.X = int(w * 0.6)
    95  	b.Y = int(h * 0.1)
    96  	b.W = int(w * 0.3)
    97  	if b.W < minWidth {
    98  		b.W = minWidth
    99  	}
   100  	b.H = int(h * 0.25)
   101  	if (b.X + b.W) >= int(w) {
   102  		b.W = int(w) - b.X
   103  	}
   104  }
   105  
   106  // LowerRightPlacement will place a box in the lower right corner of a container, like a little window
   107  func (b *Box) LowerRightPlacement(container *Box, minWidth int) {
   108  	w := float64(container.W)
   109  	h := float64(container.H)
   110  	b.X = int(w * 0.6)
   111  	b.Y = int(h * 0.37)
   112  	b.W = int(w * 0.3)
   113  	if b.W < minWidth {
   114  		b.W = minWidth
   115  	}
   116  	b.H = int(h * 0.45)
   117  	if (b.X + b.W) >= int(w) {
   118  		b.W = int(w) - b.X
   119  	}
   120  }
   121  
   122  // LowerLeftPlacement will place a box in the lower left corner of a container, like a little window
   123  func (b *Box) LowerLeftPlacement(container *Box, minWidth int) {
   124  	w := float64(container.W)
   125  	h := float64(container.H)
   126  	b.X = int(w * 0.05)
   127  	b.Y = int(h * 0.37)
   128  	b.W = int(w * 0.5)
   129  	if b.W < minWidth {
   130  		b.W = minWidth
   131  	}
   132  	b.H = int(h * 0.45)
   133  	if (b.X + b.W) >= int(w) {
   134  		b.W = int(w) - b.X
   135  	}
   136  }
   137  
   138  // EvenLowerRightPlacement will place the box even lower
   139  func (b *Box) EvenLowerRightPlacement(container *Box, minWidth int) {
   140  	w := float64(container.W)
   141  	h := float64(container.H)
   142  	b.X = int(w * 0.3)
   143  	b.Y = int(h * 0.83)
   144  	b.W = int(w * 0.62)
   145  	if b.W < minWidth {
   146  		b.W = minWidth
   147  	}
   148  	b.H = int(h * 0.18)
   149  	if (b.X + b.W) >= int(w) {
   150  		b.W = int(w) - b.X
   151  	}
   152  }
   153  
   154  // LowerPlacement will place a box in the lower right corner of a container, like a little window
   155  func (b *Box) LowerPlacement(container *Box, minWidth int) {
   156  	w := float64(container.W)
   157  	h := float64(container.H)
   158  	b.X = int(w * 0.1)
   159  	b.Y = int(h * 0.3)
   160  	b.W = int(w * 0.8)
   161  	if b.W < minWidth {
   162  		b.W = minWidth
   163  	}
   164  	b.H = int(h * 0.7)
   165  	if (b.X + b.W) >= int(w) {
   166  		b.W = int(w) - b.X
   167  	}
   168  }
   169  
   170  // Say will output text at the given coordinates, with the configured theme
   171  func (e *Editor) Say(bt *BoxTheme, c *vt100.Canvas, x, y int, text string) {
   172  	c.Write(uint(x), uint(y), *bt.Text, *bt.Background, text)
   173  }
   174  
   175  // DrawBox can draw a box using "text graphics".
   176  // The given Box struct defines the size and placement.
   177  // If extrude is True, the box looks a bit more like it's sticking out.
   178  // bg is expected to be a background color, for instance e.BoxBackground.
   179  func (e *Editor) DrawBox(bt *BoxTheme, c *vt100.Canvas, r *Box) *Box {
   180  	var (
   181  		bg     = bt.Background
   182  		FG1    = bt.UpperEdge
   183  		FG2    = bt.LowerEdge
   184  		x      = uint(r.X)
   185  		y      = uint(r.Y)
   186  		width  = uint(r.W)
   187  		height = uint(r.H)
   188  	)
   189  	c.WriteRune(x, y, *FG1, *bg, bt.TL)
   190  	for i := x + 1; i < x+(width-1); i++ {
   191  		c.WriteRune(i, y, *FG1, *bg, bt.HT)
   192  	}
   193  	c.WriteRune(x+width-1, y, *FG1, *bg, bt.TR)
   194  
   195  	for i := y + 1; i < y+height; i++ {
   196  		c.WriteRune(x, i, *FG1, *bg, bt.VL)
   197  		c.Write(x+1, i, *FG1, *bg, repeatRune(' ', width-2))
   198  		c.WriteRune(x+width-1, i, *FG2, *bg, bt.VR)
   199  	}
   200  	c.WriteRune(x, y+height-1, *FG1, *bg, bt.BL)
   201  	for i := x + 1; i < x+(width-1); i++ {
   202  		c.WriteRune(i, y+height-1, *FG2, *bg, bt.HB)
   203  	}
   204  	c.WriteRune(x+width-1, y+height-1, *FG2, *bg, bt.BR)
   205  	return &Box{int(x), int(y), int(width), int(height)}
   206  }
   207  
   208  // DrawList will draw a list widget. Takes a Box struct for the size and position.
   209  // Takes a list of strings to be listed and an int that represents
   210  // which item is currently selected. Does not scroll or wrap.
   211  // Set selected to -1 to skip highlighting one of the items.
   212  // Uses bt.Highlight, bt.Text and bt.Background.
   213  func (e *Editor) DrawList(bt *BoxTheme, c *vt100.Canvas, r *Box, items []string, selected int) {
   214  	x := uint(r.X)
   215  	for i, s := range items {
   216  		y := uint(r.Y + i)
   217  		if i == selected {
   218  			c.Write(x, y, *bt.Highlight, *bt.Background, s)
   219  		} else {
   220  			c.Write(x, y, *bt.Text, *bt.Background, s)
   221  		}
   222  	}
   223  }
   224  
   225  // DrawTitle draws a title at the top of a box, not exactly centered
   226  func (e *Editor) DrawTitle(bt *BoxTheme, c *vt100.Canvas, r *Box, title string, withSpaces bool) {
   227  	titleWithSpaces := title
   228  	if withSpaces {
   229  		titleWithSpaces = " " + title + " "
   230  	}
   231  
   232  	tmp := bt.Text
   233  	bt.Text = bt.UpperEdge
   234  	e.Say(bt, c, r.X+(r.W-len(titleWithSpaces))/2, r.Y, titleWithSpaces)
   235  	bt.Text = tmp
   236  }
   237  
   238  // DrawFooter draws text at the bottom of a box, not exactly centered
   239  func (e *Editor) DrawFooter(bt *BoxTheme, c *vt100.Canvas, r *Box, text string) {
   240  	textWithSpaces := " " + text + " "
   241  	tmp := bt.Text
   242  	bt.Text = bt.UpperEdge
   243  	e.Say(bt, c, r.X+(r.W-len(textWithSpaces))/2, r.Y+r.H-1, textWithSpaces)
   244  	bt.Text = tmp
   245  }
   246  
   247  // DrawText will draw a text widget. Takes a Box struct for the size and position.
   248  // Takes a list of strings. Does not scroll. Uses bt.Foreground and bt.Background.
   249  // The text is wrapped by using the WordWrap function.
   250  // The number of lines that are added as a concequence of wrapping lines is returned as an int.
   251  func (e *Editor) DrawText(bt *BoxTheme, c *vt100.Canvas, r *Box, text string, dryRun bool) int {
   252  	maxWidth := int(r.W) - 2 // Adjusted width to account for margins
   253  	x := uint(r.X)
   254  	lineIndex := 0
   255  	addedLines := 0 // Counter for added lines
   256  
   257  	// Split the input text into lines
   258  	lines := strings.Split(text, "\n")
   259  
   260  	for _, line := range lines {
   261  		// Attempt to wrap the line
   262  		wrappedLines, err := wordwrap.WordWrap(line, maxWidth)
   263  		if err != nil {
   264  			// If an error occurs, chop the line to fit the width
   265  			if len(line) > maxWidth {
   266  				line = line[:maxWidth]
   267  			}
   268  			wrappedLines = []string{line} // Overwrite wrappedLines with the chopped or original line
   269  		} else {
   270  			// Count the additional lines created by wrapping
   271  			addedLines += len(wrappedLines) - 1
   272  		}
   273  
   274  		// Draw each wrapped or chopped line to the canvas
   275  		for _, wrappedLine := range wrappedLines {
   276  			y := uint(r.Y + lineIndex)
   277  			if !dryRun {
   278  				c.Write(x, y, *bt.Foreground, *bt.Background, wrappedLine)
   279  			}
   280  			lineIndex++
   281  		}
   282  	}
   283  
   284  	return addedLines
   285  }