gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

box.go (16420B)


      1 package tview
      2 
      3 import (
      4 	"github.com/gdamore/tcell/v2"
      5 )
      6 
      7 // Box implements the Primitive interface with an empty background and optional
      8 // elements such as a border and a title. Box itself does not hold any content
      9 // but serves as the superclass of all other primitives. Subclasses add their
     10 // own content, typically (but not necessarily) keeping their content within the
     11 // box's rectangle.
     12 //
     13 // Box provides a number of utility functions available to all primitives.
     14 //
     15 // See https://github.com/rivo/tview/wiki/Box for an example.
     16 type Box struct {
     17 	// The position of the rect.
     18 	x, y, width, height int
     19 
     20 	// The inner rect reserved for the box's content.
     21 	innerX, innerY, innerWidth, innerHeight int
     22 
     23 	// Border padding.
     24 	paddingTop, paddingBottom, paddingLeft, paddingRight int
     25 
     26 	// The box's background color.
     27 	backgroundColor tcell.Color
     28 
     29 	// If set to true, the background of this box is not cleared while drawing.
     30 	dontClear bool
     31 
     32 	// Whether or not a border is drawn, reducing the box's space for content by
     33 	// two in width and height.
     34 	border bool
     35 
     36 	// The border style.
     37 	borderStyle tcell.Style
     38 
     39 	// The title. Only visible if there is a border, too.
     40 	title string
     41 
     42 	// The color of the title.
     43 	titleColor tcell.Color
     44 
     45 	// The alignment of the title.
     46 	titleAlign int
     47 
     48 	// Whether or not this box has focus. This is typically ignored for
     49 	// container primitives (e.g. Flex, Grid, Pages), as they will delegate
     50 	// focus to their children.
     51 	hasFocus bool
     52 
     53 	// Optional callback functions invoked when the primitive receives or loses
     54 	// focus.
     55 	focus, blur func()
     56 
     57 	// An optional capture function which receives a key event and returns the
     58 	// event to be forwarded to the primitive's default input handler (nil if
     59 	// nothing should be forwarded).
     60 	inputCapture func(event *tcell.EventKey) *tcell.EventKey
     61 
     62 	// An optional function which is called before the box is drawn.
     63 	draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)
     64 
     65 	// An optional capture function which receives a mouse event and returns the
     66 	// event to be forwarded to the primitive's default mouse event handler (at
     67 	// least one nil if nothing should be forwarded).
     68 	mouseCapture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)
     69 }
     70 
     71 // NewBox returns a Box without a border.
     72 func NewBox() *Box {
     73 	b := &Box{
     74 		width:           15,
     75 		height:          10,
     76 		innerX:          -1, // Mark as uninitialized.
     77 		backgroundColor: Styles.PrimitiveBackgroundColor,
     78 		borderStyle:     tcell.StyleDefault.Foreground(Styles.BorderColor).Background(Styles.PrimitiveBackgroundColor),
     79 		titleColor:      Styles.TitleColor,
     80 		titleAlign:      AlignCenter,
     81 	}
     82 	return b
     83 }
     84 
     85 // SetBorderPadding sets the size of the borders around the box content.
     86 func (b *Box) SetBorderPadding(top, bottom, left, right int) *Box {
     87 	b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight = top, bottom, left, right
     88 	return b
     89 }
     90 
     91 // GetRect returns the current position of the rectangle, x, y, width, and
     92 // height.
     93 func (b *Box) GetRect() (int, int, int, int) {
     94 	return b.x, b.y, b.width, b.height
     95 }
     96 
     97 // GetInnerRect returns the position of the inner rectangle (x, y, width,
     98 // height), without the border and without any padding. Width and height values
     99 // will clamp to 0 and thus never be negative.
    100 func (b *Box) GetInnerRect() (int, int, int, int) {
    101 	if b.innerX >= 0 {
    102 		return b.innerX, b.innerY, b.innerWidth, b.innerHeight
    103 	}
    104 	x, y, width, height := b.GetRect()
    105 	if b.border {
    106 		x++
    107 		y++
    108 		width -= 2
    109 		height -= 2
    110 	}
    111 	x, y, width, height = x+b.paddingLeft,
    112 		y+b.paddingTop,
    113 		width-b.paddingLeft-b.paddingRight,
    114 		height-b.paddingTop-b.paddingBottom
    115 	if width < 0 {
    116 		width = 0
    117 	}
    118 	if height < 0 {
    119 		height = 0
    120 	}
    121 	return x, y, width, height
    122 }
    123 
    124 // SetRect sets a new position of the primitive. Note that this has no effect
    125 // if this primitive is part of a layout (e.g. Flex, Grid) or if it was added
    126 // like this:
    127 //
    128 //	application.SetRoot(p, true)
    129 func (b *Box) SetRect(x, y, width, height int) {
    130 	b.x = x
    131 	b.y = y
    132 	b.width = width
    133 	b.height = height
    134 	b.innerX = -1 // Mark inner rect as uninitialized.
    135 }
    136 
    137 // SetDrawFunc sets a callback function which is invoked after the box primitive
    138 // has been drawn. This allows you to add a more individual style to the box
    139 // (and all primitives which extend it).
    140 //
    141 // The function is provided with the box's dimensions (set via SetRect()). It
    142 // must return the box's inner dimensions (x, y, width, height) which will be
    143 // returned by GetInnerRect(), used by descendent primitives to draw their own
    144 // content.
    145 func (b *Box) SetDrawFunc(handler func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)) *Box {
    146 	b.draw = handler
    147 	return b
    148 }
    149 
    150 // GetDrawFunc returns the callback function which was installed with
    151 // SetDrawFunc() or nil if no such function has been installed.
    152 func (b *Box) GetDrawFunc() func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
    153 	return b.draw
    154 }
    155 
    156 // WrapInputHandler wraps an input handler (see [Box.InputHandler]) with the
    157 // functionality to capture input (see [Box.SetInputCapture]) before passing it
    158 // on to the provided (default) input handler.
    159 //
    160 // This is only meant to be used by subclassing primitives.
    161 func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primitive))) func(*tcell.EventKey, func(p Primitive)) {
    162 	return func(event *tcell.EventKey, setFocus func(p Primitive)) {
    163 		if b.inputCapture != nil {
    164 			event = b.inputCapture(event)
    165 		}
    166 		if event != nil && inputHandler != nil {
    167 			inputHandler(event, setFocus)
    168 		}
    169 	}
    170 }
    171 
    172 // InputHandler returns nil. Box has no default input handling.
    173 func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
    174 	return b.WrapInputHandler(nil)
    175 }
    176 
    177 // WrapPasteHandler wraps a paste handler (see [Box.PasteHandler]).
    178 func (b *Box) WrapPasteHandler(pasteHandler func(string, func(p Primitive))) func(string, func(p Primitive)) {
    179 	return func(text string, setFocus func(p Primitive)) {
    180 		if pasteHandler != nil {
    181 			pasteHandler(text, setFocus)
    182 		}
    183 	}
    184 }
    185 
    186 // PasteHandler returns nil. Box has no default paste handling.
    187 func (b *Box) PasteHandler() func(pastedText string, setFocus func(p Primitive)) {
    188 	return b.WrapPasteHandler(nil)
    189 }
    190 
    191 // SetInputCapture installs a function which captures key events before they are
    192 // forwarded to the primitive's default key event handler. This function can
    193 // then choose to forward that key event (or a different one) to the default
    194 // handler by returning it. If nil is returned, the default handler will not
    195 // be called.
    196 //
    197 // Providing a nil handler will remove a previously existing handler.
    198 //
    199 // This function can also be used on container primitives (like Flex, Grid, or
    200 // Form) as keyboard events will be handed down until they are handled.
    201 //
    202 // Pasted key events are not forwarded to the input capture function if pasting
    203 // is enabled (see [Application.EnablePaste]).
    204 func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *Box {
    205 	b.inputCapture = capture
    206 	return b
    207 }
    208 
    209 // GetInputCapture returns the function installed with SetInputCapture() or nil
    210 // if no such function has been installed.
    211 func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
    212 	return b.inputCapture
    213 }
    214 
    215 // WrapMouseHandler wraps a mouse event handler (see [Box.MouseHandler]) with the
    216 // functionality to capture mouse events (see [Box.SetMouseCapture]) before passing
    217 // them on to the provided (default) event handler.
    218 //
    219 // This is only meant to be used by subclassing primitives.
    220 func (b *Box) WrapMouseHandler(mouseHandler func(MouseAction, *tcell.EventMouse, func(p Primitive)) (bool, Primitive)) func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
    221 	return func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
    222 		if b.mouseCapture != nil {
    223 			action, event = b.mouseCapture(action, event)
    224 		}
    225 		if event == nil {
    226 			if action == MouseConsumed {
    227 				consumed = true
    228 			}
    229 		} else if mouseHandler != nil {
    230 			consumed, capture = mouseHandler(action, event, setFocus)
    231 		}
    232 		return
    233 	}
    234 }
    235 
    236 // MouseHandler returns nil. Box has no default mouse handling.
    237 func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
    238 	return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
    239 		if action == MouseLeftDown && b.InRect(event.Position()) {
    240 			setFocus(b)
    241 			consumed = true
    242 		}
    243 		return
    244 	})
    245 }
    246 
    247 // SetMouseCapture sets a function which captures mouse events (consisting of
    248 // the original tcell mouse event and the semantic mouse action) before they are
    249 // forwarded to the primitive's default mouse event handler. This function can
    250 // then choose to forward that event (or a different one) by returning it or
    251 // returning a nil mouse event, in which case the default handler will not be
    252 // called.
    253 //
    254 // When a nil event is returned, the returned mouse action value may be set to
    255 // [MouseConsumed] to indicate that the event was consumed and the screen should
    256 // be redrawn. Any other value will not cause a redraw.
    257 //
    258 // Providing a nil handler will remove a previously existing handler.
    259 //
    260 // Note that mouse events are ignored completely if the application has not been
    261 // enabled for mouse events (see [Application.EnableMouse]), which is the
    262 // default.
    263 func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) *Box {
    264 	b.mouseCapture = capture
    265 	return b
    266 }
    267 
    268 // InRect returns true if the given coordinate is within the bounds of the box's
    269 // rectangle.
    270 func (b *Box) InRect(x, y int) bool {
    271 	rectX, rectY, width, height := b.GetRect()
    272 	return x >= rectX && x < rectX+width && y >= rectY && y < rectY+height
    273 }
    274 
    275 // InInnerRect returns true if the given coordinate is within the bounds of the
    276 // box's inner rectangle (within the border and padding).
    277 func (b *Box) InInnerRect(x, y int) bool {
    278 	rectX, rectY, width, height := b.GetInnerRect()
    279 	return x >= rectX && x < rectX+width && y >= rectY && y < rectY+height
    280 }
    281 
    282 // GetMouseCapture returns the function installed with SetMouseCapture() or nil
    283 // if no such function has been installed.
    284 func (b *Box) GetMouseCapture() func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) {
    285 	return b.mouseCapture
    286 }
    287 
    288 // SetBackgroundColor sets the box's background color.
    289 func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
    290 	b.backgroundColor = color
    291 	b.borderStyle = b.borderStyle.Background(color)
    292 	return b
    293 }
    294 
    295 // SetBorder sets the flag indicating whether or not the box should have a
    296 // border.
    297 func (b *Box) SetBorder(show bool) *Box {
    298 	b.border = show
    299 	return b
    300 }
    301 
    302 // SetBorderStyle sets the box's border style.
    303 func (b *Box) SetBorderStyle(style tcell.Style) *Box {
    304 	b.borderStyle = style
    305 	return b
    306 }
    307 
    308 // SetBorderColor sets the box's border color.
    309 func (b *Box) SetBorderColor(color tcell.Color) *Box {
    310 	b.borderStyle = b.borderStyle.Foreground(color)
    311 	return b
    312 }
    313 
    314 // SetBorderAttributes sets the border's style attributes. You can combine
    315 // different attributes using bitmask operations:
    316 //
    317 //	box.SetBorderAttributes(tcell.AttrItalic | tcell.AttrBold)
    318 func (b *Box) SetBorderAttributes(attr tcell.AttrMask) *Box {
    319 	b.borderStyle = b.borderStyle.Attributes(attr)
    320 	return b
    321 }
    322 
    323 // GetBorderAttributes returns the border's style attributes.
    324 func (b *Box) GetBorderAttributes() tcell.AttrMask {
    325 	_, _, attr := b.borderStyle.Decompose()
    326 	return attr
    327 }
    328 
    329 // GetBorderColor returns the box's border color.
    330 func (b *Box) GetBorderColor() tcell.Color {
    331 	color, _, _ := b.borderStyle.Decompose()
    332 	return color
    333 }
    334 
    335 // GetBackgroundColor returns the box's background color.
    336 func (b *Box) GetBackgroundColor() tcell.Color {
    337 	return b.backgroundColor
    338 }
    339 
    340 // SetTitle sets the box's title.
    341 func (b *Box) SetTitle(title string) *Box {
    342 	b.title = title
    343 	return b
    344 }
    345 
    346 // GetTitle returns the box's current title.
    347 func (b *Box) GetTitle() string {
    348 	return b.title
    349 }
    350 
    351 // SetTitleColor sets the box's title color.
    352 func (b *Box) SetTitleColor(color tcell.Color) *Box {
    353 	b.titleColor = color
    354 	return b
    355 }
    356 
    357 // SetTitleAlign sets the alignment of the title, one of AlignLeft, AlignCenter,
    358 // or AlignRight.
    359 func (b *Box) SetTitleAlign(align int) *Box {
    360 	b.titleAlign = align
    361 	return b
    362 }
    363 
    364 // Draw draws this primitive onto the screen.
    365 func (b *Box) Draw(screen tcell.Screen) {
    366 	b.DrawForSubclass(screen, b)
    367 }
    368 
    369 // DrawForSubclass draws this box under the assumption that primitive p is a
    370 // subclass of this box. This is needed e.g. to draw proper box frames which
    371 // depend on the subclass's focus.
    372 //
    373 // Only call this function from your own custom primitives. It is not needed in
    374 // applications that have no custom primitives.
    375 func (b *Box) DrawForSubclass(screen tcell.Screen, p Primitive) {
    376 	// Don't draw anything if there is no space.
    377 	if b.width <= 0 || b.height <= 0 {
    378 		return
    379 	}
    380 
    381 	// Fill background.
    382 	background := tcell.StyleDefault.Background(b.backgroundColor)
    383 	if !b.dontClear {
    384 		for y := b.y; y < b.y+b.height; y++ {
    385 			for x := b.x; x < b.x+b.width; x++ {
    386 				screen.SetContent(x, y, ' ', nil, background)
    387 			}
    388 		}
    389 	}
    390 
    391 	// Draw border.
    392 	if b.border && b.width >= 2 && b.height >= 2 {
    393 		var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
    394 		if p.HasFocus() {
    395 			horizontal = Borders.HorizontalFocus
    396 			vertical = Borders.VerticalFocus
    397 			topLeft = Borders.TopLeftFocus
    398 			topRight = Borders.TopRightFocus
    399 			bottomLeft = Borders.BottomLeftFocus
    400 			bottomRight = Borders.BottomRightFocus
    401 		} else {
    402 			horizontal = Borders.Horizontal
    403 			vertical = Borders.Vertical
    404 			topLeft = Borders.TopLeft
    405 			topRight = Borders.TopRight
    406 			bottomLeft = Borders.BottomLeft
    407 			bottomRight = Borders.BottomRight
    408 		}
    409 		for x := b.x + 1; x < b.x+b.width-1; x++ {
    410 			screen.SetContent(x, b.y, horizontal, nil, b.borderStyle)
    411 			screen.SetContent(x, b.y+b.height-1, horizontal, nil, b.borderStyle)
    412 		}
    413 		for y := b.y + 1; y < b.y+b.height-1; y++ {
    414 			screen.SetContent(b.x, y, vertical, nil, b.borderStyle)
    415 			screen.SetContent(b.x+b.width-1, y, vertical, nil, b.borderStyle)
    416 		}
    417 		screen.SetContent(b.x, b.y, topLeft, nil, b.borderStyle)
    418 		screen.SetContent(b.x+b.width-1, b.y, topRight, nil, b.borderStyle)
    419 		screen.SetContent(b.x, b.y+b.height-1, bottomLeft, nil, b.borderStyle)
    420 		screen.SetContent(b.x+b.width-1, b.y+b.height-1, bottomRight, nil, b.borderStyle)
    421 
    422 		// Draw title.
    423 		if b.title != "" && b.width >= 4 {
    424 			printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
    425 			if len(b.title)-printed > 0 && printed > 0 {
    426 				xEllipsis := b.x + b.width - 2
    427 				if b.titleAlign == AlignRight {
    428 					xEllipsis = b.x + 1
    429 				}
    430 				_, _, style, _ := screen.GetContent(xEllipsis, b.y)
    431 				fg, _, _ := style.Decompose()
    432 				Print(screen, string(SemigraphicsHorizontalEllipsis), xEllipsis, b.y, 1, AlignLeft, fg)
    433 			}
    434 		}
    435 	}
    436 
    437 	// Call custom draw function.
    438 	if b.draw != nil {
    439 		b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.draw(screen, b.x, b.y, b.width, b.height)
    440 	} else {
    441 		// Remember the inner rect.
    442 		b.innerX = -1
    443 		b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.GetInnerRect()
    444 	}
    445 }
    446 
    447 // SetFocusFunc sets a callback function which is invoked when this primitive
    448 // receives focus. Container primitives such as [Flex] or [Grid] may not be
    449 // notified if one of their descendents receive focus directly.
    450 //
    451 // Set to nil to remove the callback function.
    452 func (b *Box) SetFocusFunc(callback func()) *Box {
    453 	b.focus = callback
    454 	return b
    455 }
    456 
    457 // SetBlurFunc sets a callback function which is invoked when this primitive
    458 // loses focus. This does not apply to container primitives such as [Flex] or
    459 // [Grid].
    460 //
    461 // Set to nil to remove the callback function.
    462 func (b *Box) SetBlurFunc(callback func()) *Box {
    463 	b.blur = callback
    464 	return b
    465 }
    466 
    467 // Focus is called when this primitive receives focus.
    468 func (b *Box) Focus(delegate func(p Primitive)) {
    469 	b.hasFocus = true
    470 	if b.focus != nil {
    471 		b.focus()
    472 	}
    473 }
    474 
    475 // Blur is called when this primitive loses focus.
    476 func (b *Box) Blur() {
    477 	if b.blur != nil {
    478 		b.blur()
    479 	}
    480 	b.hasFocus = false
    481 }
    482 
    483 // HasFocus returns whether or not this primitive has focus.
    484 func (b *Box) HasFocus() bool {
    485 	return b.hasFocus
    486 }