You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
572 lines
16 KiB
572 lines
16 KiB
package cview |
|
|
|
import ( |
|
"sync" |
|
|
|
"github.com/gdamore/tcell/v2" |
|
) |
|
|
|
// Box is the base Primitive for all widgets. It has a background color and |
|
// optional surrounding elements such as a border and a title. It does not have |
|
// inner text. Widgets embed Box and draw their text over it. |
|
type Box struct { |
|
// The position of the rect. |
|
x, y, width, height int |
|
|
|
// Padding. |
|
paddingTop, paddingBottom, paddingLeft, paddingRight int |
|
|
|
// The inner rect reserved for the box's content. |
|
innerX, innerY, innerWidth, innerHeight int |
|
|
|
// Whether or not the box is visible. |
|
visible bool |
|
|
|
// The border color when the box has focus. |
|
borderColorFocused tcell.Color |
|
|
|
// The box's background color. |
|
backgroundColor tcell.Color |
|
|
|
// Whether or not the box's background is transparent. |
|
backgroundTransparent bool |
|
|
|
// Whether or not a border is drawn, reducing the box's space for content by |
|
// two in width and height. |
|
border bool |
|
|
|
// The color of the border. |
|
borderColor tcell.Color |
|
|
|
// The style attributes of the border. |
|
borderAttributes tcell.AttrMask |
|
|
|
// The title. Only visible if there is a border, too. |
|
title []byte |
|
|
|
// The color of the title. |
|
titleColor tcell.Color |
|
|
|
// The alignment of the title. |
|
titleAlign int |
|
|
|
// Provides a way to find out if this box has focus. We always go through |
|
// this interface because it may be overridden by implementing classes. |
|
focus Focusable |
|
|
|
// Whether or not this box has focus. |
|
hasFocus bool |
|
|
|
// Whether or not this box shows its focus. |
|
showFocus bool |
|
|
|
// An optional capture function which receives a key event and returns the |
|
// event to be forwarded to the primitive's default input handler (nil if |
|
// nothing should be forwarded). |
|
inputCapture func(event *tcell.EventKey) *tcell.EventKey |
|
|
|
// An optional function which is called before the box is drawn. |
|
draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) |
|
|
|
// An optional capture function which receives a mouse event and returns the |
|
// event to be forwarded to the primitive's default mouse event handler (at |
|
// least one nil if nothing should be forwarded). |
|
mouseCapture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) |
|
|
|
l sync.RWMutex |
|
} |
|
|
|
// NewBox returns a Box without a border. |
|
func NewBox() *Box { |
|
b := &Box{ |
|
width: 15, |
|
height: 10, |
|
visible: true, |
|
backgroundColor: Styles.PrimitiveBackgroundColor, |
|
borderColor: Styles.BorderColor, |
|
titleColor: Styles.TitleColor, |
|
borderColorFocused: ColorUnset, |
|
titleAlign: AlignCenter, |
|
showFocus: true, |
|
} |
|
b.focus = b |
|
b.updateInnerRect() |
|
return b |
|
} |
|
|
|
func (b *Box) updateInnerRect() { |
|
x, y, width, height := b.x, b.y, b.width, b.height |
|
|
|
// Subtract border space |
|
if b.border { |
|
x++ |
|
y++ |
|
width -= 2 |
|
height -= 2 |
|
} |
|
|
|
// Subtract padding |
|
x, y, width, height = |
|
x+b.paddingLeft, |
|
y+b.paddingTop, |
|
width-b.paddingLeft-b.paddingRight, |
|
height-b.paddingTop-b.paddingBottom |
|
|
|
if width < 0 { |
|
width = 0 |
|
} |
|
if height < 0 { |
|
height = 0 |
|
} |
|
|
|
b.innerX, b.innerY, b.innerWidth, b.innerHeight = x, y, width, height |
|
} |
|
|
|
// GetPadding returns the size of the padding around the box content. |
|
func (b *Box) GetPadding() (top, bottom, left, right int) { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
|
|
return b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight |
|
} |
|
|
|
// SetPadding sets the size of the padding around the box content. |
|
func (b *Box) SetPadding(top, bottom, left, right int) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight = top, bottom, left, right |
|
|
|
b.updateInnerRect() |
|
} |
|
|
|
// GetRect returns the current position of the rectangle, x, y, width, and |
|
// height. |
|
func (b *Box) GetRect() (int, int, int, int) { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
|
|
return b.x, b.y, b.width, b.height |
|
} |
|
|
|
// GetInnerRect returns the position of the inner rectangle (x, y, width, |
|
// height), without the border and without any padding. Width and height values |
|
// will clamp to 0 and thus never be negative. |
|
func (b *Box) GetInnerRect() (int, int, int, int) { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
|
|
return b.innerX, b.innerY, b.innerWidth, b.innerHeight |
|
} |
|
|
|
// SetRect sets a new position of the primitive. Note that this has no effect |
|
// if this primitive is part of a layout (e.g. Flex, Grid) or if it was added |
|
// like this: |
|
// |
|
// application.SetRoot(b, true) |
|
func (b *Box) SetRect(x, y, width, height int) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.x, b.y, b.width, b.height = x, y, width, height |
|
|
|
b.updateInnerRect() |
|
} |
|
|
|
// SetVisible sets the flag indicating whether or not the box is visible. |
|
func (b *Box) SetVisible(v bool) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.visible = v |
|
} |
|
|
|
// GetVisible returns a value indicating whether or not the box is visible. |
|
func (b *Box) GetVisible() bool { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
|
|
return b.visible |
|
} |
|
|
|
// SetDrawFunc sets a callback function which is invoked after the box primitive |
|
// has been drawn. This allows you to add a more individual style to the box |
|
// (and all primitives which extend it). |
|
// |
|
// The function is provided with the box's dimensions (set via SetRect()). It |
|
// must return the box's inner dimensions (x, y, width, height) which will be |
|
// returned by GetInnerRect(), used by descendent primitives to draw their own |
|
// content. |
|
func (b *Box) SetDrawFunc(handler func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.draw = handler |
|
} |
|
|
|
// GetDrawFunc returns the callback function which was installed with |
|
// SetDrawFunc() or nil if no such function has been installed. |
|
func (b *Box) GetDrawFunc() func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
|
|
return b.draw |
|
} |
|
|
|
// WrapInputHandler wraps an input handler (see InputHandler()) with the |
|
// functionality to capture input (see SetInputCapture()) before passing it |
|
// on to the provided (default) input handler. |
|
// |
|
// This is only meant to be used by subclassing primitives. |
|
func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primitive))) func(*tcell.EventKey, func(p Primitive)) { |
|
return func(event *tcell.EventKey, setFocus func(p Primitive)) { |
|
if b.inputCapture != nil { |
|
event = b.inputCapture(event) |
|
} |
|
if event != nil && inputHandler != nil { |
|
inputHandler(event, setFocus) |
|
} |
|
} |
|
} |
|
|
|
// InputHandler returns nil. |
|
func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
|
|
return b.WrapInputHandler(nil) |
|
} |
|
|
|
// SetInputCapture installs a function which captures key events before they are |
|
// forwarded to the primitive's default key event handler. This function can |
|
// then choose to forward that key event (or a different one) to the default |
|
// handler by returning it. If nil is returned, the default handler will not |
|
// be called. |
|
// |
|
// Providing a nil handler will remove a previously existing handler. |
|
// |
|
// Note that this function will not have an effect on primitives composed of |
|
// other primitives, such as Form, Flex, or Grid. Key events are only captured |
|
// by the primitives that have focus (e.g. InputField) and only one primitive |
|
// can have focus at a time. Composing primitives such as Form pass the focus on |
|
// to their contained primitives and thus never receive any key events |
|
// themselves. Therefore, they cannot intercept key events. |
|
func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.inputCapture = capture |
|
} |
|
|
|
// GetInputCapture returns the function installed with SetInputCapture() or nil |
|
// if no such function has been installed. |
|
func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
|
|
return b.inputCapture |
|
} |
|
|
|
// WrapMouseHandler wraps a mouse event handler (see MouseHandler()) with the |
|
// functionality to capture mouse events (see SetMouseCapture()) before passing |
|
// them on to the provided (default) event handler. |
|
// |
|
// This is only meant to be used by subclassing primitives. |
|
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) { |
|
return func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { |
|
if b.mouseCapture != nil { |
|
action, event = b.mouseCapture(action, event) |
|
} |
|
if event != nil && mouseHandler != nil { |
|
consumed, capture = mouseHandler(action, event, setFocus) |
|
} |
|
return |
|
} |
|
} |
|
|
|
// MouseHandler returns nil. |
|
func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { |
|
return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { |
|
if action == MouseLeftClick && b.InRect(event.Position()) { |
|
setFocus(b) |
|
consumed = true |
|
} |
|
return |
|
}) |
|
} |
|
|
|
// SetMouseCapture sets a function which captures mouse events (consisting of |
|
// the original tcell mouse event and the semantic mouse action) before they are |
|
// forwarded to the primitive's default mouse event handler. This function can |
|
// then choose to forward that event (or a different one) by returning it or |
|
// returning a nil mouse event, in which case the default handler will not be |
|
// called. |
|
// |
|
// Providing a nil handler will remove a previously existing handler. |
|
func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) { |
|
b.mouseCapture = capture |
|
} |
|
|
|
// InRect returns true if the given coordinate is within the bounds of the box's |
|
// rectangle. |
|
func (b *Box) InRect(x, y int) bool { |
|
rectX, rectY, width, height := b.GetRect() |
|
return x >= rectX && x < rectX+width && y >= rectY && y < rectY+height |
|
} |
|
|
|
// GetMouseCapture returns the function installed with SetMouseCapture() or nil |
|
// if no such function has been installed. |
|
func (b *Box) GetMouseCapture() func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) { |
|
return b.mouseCapture |
|
} |
|
|
|
// SetBackgroundColor sets the box's background color. |
|
func (b *Box) SetBackgroundColor(color tcell.Color) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.backgroundColor = color |
|
} |
|
|
|
// GetBackgroundColor returns the box's background color. |
|
func (b *Box) GetBackgroundColor() tcell.Color { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
return b.backgroundColor |
|
} |
|
|
|
// SetBackgroundTransparent sets the flag indicating whether or not the box's |
|
// background is transparent. The screen is not cleared before drawing the |
|
// application. Overlaying transparent widgets directly onto the screen may |
|
// result in artifacts. To resolve this, add a blank, non-transparent Box to |
|
// the bottom layer of the interface via Panels, or set a handler via |
|
// SetBeforeDrawFunc which clears the screen. |
|
func (b *Box) SetBackgroundTransparent(transparent bool) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.backgroundTransparent = transparent |
|
} |
|
|
|
// GetBorder returns a value indicating whether the box have a border |
|
// or not. |
|
func (b *Box) GetBorder() bool { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
return b.border |
|
} |
|
|
|
// SetBorder sets the flag indicating whether or not the box should have a |
|
// border. |
|
func (b *Box) SetBorder(show bool) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.border = show |
|
|
|
b.updateInnerRect() |
|
} |
|
|
|
// SetBorderColor sets the box's border color. |
|
func (b *Box) SetBorderColor(color tcell.Color) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.borderColor = color |
|
} |
|
|
|
// SetBorderColorFocused sets the box's border color when the box is focused. |
|
func (b *Box) SetBorderColorFocused(color tcell.Color) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
b.borderColorFocused = color |
|
} |
|
|
|
// SetBorderAttributes sets the border's style attributes. You can combine |
|
// different attributes using bitmask operations: |
|
// |
|
// box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold) |
|
func (b *Box) SetBorderAttributes(attr tcell.AttrMask) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.borderAttributes = attr |
|
} |
|
|
|
// SetTitle sets the box's title. |
|
func (b *Box) SetTitle(title string) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.title = []byte(title) |
|
} |
|
|
|
// GetTitle returns the box's current title. |
|
func (b *Box) GetTitle() string { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
|
|
return string(b.title) |
|
} |
|
|
|
// SetTitleColor sets the box's title color. |
|
func (b *Box) SetTitleColor(color tcell.Color) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.titleColor = color |
|
} |
|
|
|
// SetTitleAlign sets the alignment of the title, one of AlignLeft, AlignCenter, |
|
// or AlignRight. |
|
func (b *Box) SetTitleAlign(align int) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.titleAlign = align |
|
} |
|
|
|
// Draw draws this primitive onto the screen. |
|
func (b *Box) Draw(screen tcell.Screen) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
// Don't draw anything if the box is hidden |
|
if !b.visible { |
|
return |
|
} |
|
|
|
// Don't draw anything if there is no space. |
|
if b.width <= 0 || b.height <= 0 { |
|
return |
|
} |
|
|
|
def := tcell.StyleDefault |
|
|
|
// Fill background. |
|
background := def.Background(b.backgroundColor) |
|
if !b.backgroundTransparent { |
|
for y := b.y; y < b.y+b.height; y++ { |
|
for x := b.x; x < b.x+b.width; x++ { |
|
screen.SetContent(x, y, ' ', nil, background) |
|
} |
|
} |
|
} |
|
|
|
// Draw border. |
|
if b.border && b.width >= 2 && b.height >= 2 { |
|
border := SetAttributes(background.Foreground(b.borderColor), b.borderAttributes) |
|
var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune |
|
|
|
var hasFocus bool |
|
if b.focus == b { |
|
hasFocus = b.hasFocus |
|
} else { |
|
hasFocus = b.focus.HasFocus() |
|
} |
|
|
|
if hasFocus && b.borderColorFocused != ColorUnset { |
|
border = SetAttributes(background.Foreground(b.borderColorFocused), b.borderAttributes) |
|
} |
|
|
|
if hasFocus && b.showFocus { |
|
horizontal = Borders.HorizontalFocus |
|
vertical = Borders.VerticalFocus |
|
topLeft = Borders.TopLeftFocus |
|
topRight = Borders.TopRightFocus |
|
bottomLeft = Borders.BottomLeftFocus |
|
bottomRight = Borders.BottomRightFocus |
|
} else { |
|
horizontal = Borders.Horizontal |
|
vertical = Borders.Vertical |
|
topLeft = Borders.TopLeft |
|
topRight = Borders.TopRight |
|
bottomLeft = Borders.BottomLeft |
|
bottomRight = Borders.BottomRight |
|
} |
|
for x := b.x + 1; x < b.x+b.width-1; x++ { |
|
screen.SetContent(x, b.y, horizontal, nil, border) |
|
screen.SetContent(x, b.y+b.height-1, horizontal, nil, border) |
|
} |
|
for y := b.y + 1; y < b.y+b.height-1; y++ { |
|
screen.SetContent(b.x, y, vertical, nil, border) |
|
screen.SetContent(b.x+b.width-1, y, vertical, nil, border) |
|
} |
|
screen.SetContent(b.x, b.y, topLeft, nil, border) |
|
screen.SetContent(b.x+b.width-1, b.y, topRight, nil, border) |
|
screen.SetContent(b.x, b.y+b.height-1, bottomLeft, nil, border) |
|
screen.SetContent(b.x+b.width-1, b.y+b.height-1, bottomRight, nil, border) |
|
|
|
// Draw title. |
|
if len(b.title) > 0 && b.width >= 4 { |
|
printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor) |
|
if len(b.title)-printed > 0 && printed > 0 { |
|
_, _, style, _ := screen.GetContent(b.x+b.width-2, b.y) |
|
fg, _, _ := style.Decompose() |
|
Print(screen, []byte(string(SemigraphicsHorizontalEllipsis)), b.x+b.width-2, b.y, 1, AlignLeft, fg) |
|
} |
|
} |
|
} |
|
|
|
// Call custom draw function. |
|
if b.draw != nil { |
|
b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.draw(screen, b.x, b.y, b.width, b.height) |
|
} |
|
} |
|
|
|
// ShowFocus sets the flag indicating whether or not the borders of this |
|
// primitive should change thickness when focused. |
|
func (b *Box) ShowFocus(showFocus bool) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.showFocus = showFocus |
|
} |
|
|
|
// Focus is called when this primitive receives focus. |
|
func (b *Box) Focus(delegate func(p Primitive)) { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.hasFocus = true |
|
} |
|
|
|
// Blur is called when this primitive loses focus. |
|
func (b *Box) Blur() { |
|
b.l.Lock() |
|
defer b.l.Unlock() |
|
|
|
b.hasFocus = false |
|
} |
|
|
|
// HasFocus returns whether or not this primitive has focus. |
|
func (b *Box) HasFocus() bool { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
|
|
return b.hasFocus |
|
} |
|
|
|
// GetFocusable returns the item's Focusable. |
|
func (b *Box) GetFocusable() Focusable { |
|
b.l.RLock() |
|
defer b.l.RUnlock() |
|
|
|
return b.focus |
|
} |
|
|
|
// GetBorderPadding returns the size of the padding around the box content. |
|
// |
|
// Deprecated: This function is provided for backwards compatibility. |
|
// Developers should use GetPadding instead. |
|
func (b *Box) GetBorderPadding() (top, bottom, left, right int) { |
|
return b.GetPadding() |
|
} |
|
|
|
// SetBorderPadding sets the size of the padding around the box content. |
|
// |
|
// Deprecated: This function is provided for backwards compatibility. |
|
// Developers should use SetPadding instead. |
|
func (b *Box) SetBorderPadding(top, bottom, left, right int) { |
|
b.SetPadding(top, bottom, left, right) |
|
}
|
|
|