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 }