...

Source file src/codeberg.org/tslocum/cview/box.go

Documentation: codeberg.org/tslocum/cview

     1  package cview
     2  
     3  import (
     4  	"sync"
     5  
     6  	"github.com/gdamore/tcell/v3"
     7  )
     8  
     9  // Box is the base Primitive for all widgets. It has a background color and
    10  // optional surrounding elements such as a border and a title. It does not have
    11  // inner text. Widgets embed Box and draw their text over it.
    12  type Box struct {
    13  	// The position of the rect.
    14  	x, y, width, height int
    15  
    16  	// Padding.
    17  	paddingTop, paddingBottom, paddingLeft, paddingRight int
    18  
    19  	// The inner rect reserved for the box's content.
    20  	innerX, innerY, innerWidth, innerHeight int
    21  
    22  	// Whether or not the box is visible.
    23  	visible bool
    24  
    25  	// The border color when the box has focus.
    26  	borderColorFocused tcell.Color
    27  
    28  	// The box's background color.
    29  	backgroundColor tcell.Color
    30  
    31  	// Whether or not the box's background is transparent.
    32  	backgroundTransparent bool
    33  
    34  	// Whether or not a border is drawn, reducing the box's space for content by
    35  	// two in width and height.
    36  	border bool
    37  
    38  	// The color of the border.
    39  	borderColor tcell.Color
    40  
    41  	// The style attributes of the border.
    42  	borderAttributes tcell.AttrMask
    43  
    44  	// The title. Only visible if there is a border, too.
    45  	title []byte
    46  
    47  	// The title buffer. When a title is set, the title buffer contains the padded title.
    48  	titleBuffer []byte
    49  
    50  	// The color of the title.
    51  	titleColor tcell.Color
    52  
    53  	// The alignment of the title.
    54  	titleAlign int
    55  
    56  	// Provides a way to find out if this box has focus. We always go through
    57  	// this interface because it may be overridden by implementing classes.
    58  	focus Focusable
    59  
    60  	// Whether or not this box has focus.
    61  	hasFocus bool
    62  
    63  	// Whether or not this box shows its focus.
    64  	showFocus bool
    65  
    66  	// An optional capture function which receives a key event and returns the
    67  	// event to be forwarded to the primitive's default input handler (nil if
    68  	// nothing should be forwarded).
    69  	inputCapture func(event *tcell.EventKey) *tcell.EventKey
    70  
    71  	// An optional function which is called before the box is drawn.
    72  	draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)
    73  
    74  	// An optional capture function which receives a mouse event and returns the
    75  	// event to be forwarded to the primitive's default mouse event handler (at
    76  	// least one nil if nothing should be forwarded).
    77  	mouseCapture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)
    78  
    79  	l sync.RWMutex
    80  }
    81  
    82  // NewBox returns a Box without a border.
    83  func NewBox() *Box {
    84  	b := &Box{
    85  		width:              15,
    86  		height:             10,
    87  		visible:            true,
    88  		backgroundColor:    Styles.PrimitiveBackgroundColor,
    89  		borderColor:        Styles.BorderColor,
    90  		titleColor:         Styles.TitleColor,
    91  		borderColorFocused: ColorUnset,
    92  		titleAlign:         AlignCenter,
    93  		showFocus:          true,
    94  	}
    95  	b.focus = b
    96  	b.updateInnerRect()
    97  	return b
    98  }
    99  
   100  func (b *Box) updateInnerRect() {
   101  	x, y, width, height := b.x, b.y, b.width, b.height
   102  
   103  	// Subtract border space
   104  	if b.border {
   105  		x++
   106  		y++
   107  		width -= 2
   108  		height -= 2
   109  	}
   110  
   111  	// Subtract padding
   112  	x, y, width, height =
   113  		x+b.paddingLeft,
   114  		y+b.paddingTop,
   115  		width-b.paddingLeft-b.paddingRight,
   116  		height-b.paddingTop-b.paddingBottom
   117  
   118  	if width < 0 {
   119  		width = 0
   120  	}
   121  	if height < 0 {
   122  		height = 0
   123  	}
   124  
   125  	b.innerX, b.innerY, b.innerWidth, b.innerHeight = x, y, width, height
   126  }
   127  
   128  // GetPadding returns the size of the padding around the box content.
   129  func (b *Box) GetPadding() (top, bottom, left, right int) {
   130  	b.l.RLock()
   131  	defer b.l.RUnlock()
   132  
   133  	return b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight
   134  }
   135  
   136  // SetPadding sets the size of the padding around the box content.
   137  func (b *Box) SetPadding(top, bottom, left, right int) {
   138  	b.l.Lock()
   139  	defer b.l.Unlock()
   140  
   141  	b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight = top, bottom, left, right
   142  
   143  	b.updateInnerRect()
   144  }
   145  
   146  // GetRect returns the current position of the rectangle, x, y, width, and
   147  // height.
   148  func (b *Box) GetRect() (int, int, int, int) {
   149  	b.l.RLock()
   150  	defer b.l.RUnlock()
   151  
   152  	return b.x, b.y, b.width, b.height
   153  }
   154  
   155  // GetInnerRect returns the position of the inner rectangle (x, y, width,
   156  // height), without the border and without any padding. Width and height values
   157  // will clamp to 0 and thus never be negative.
   158  func (b *Box) GetInnerRect() (int, int, int, int) {
   159  	b.l.RLock()
   160  	defer b.l.RUnlock()
   161  
   162  	return b.innerX, b.innerY, b.innerWidth, b.innerHeight
   163  }
   164  
   165  // SetRect sets a new position of the primitive. Note that this has no effect
   166  // if this primitive is part of a layout (e.g. Flex, Grid) or if it was added
   167  // like this:
   168  //
   169  //	application.SetRoot(b, true)
   170  func (b *Box) SetRect(x, y, width, height int) {
   171  	b.l.Lock()
   172  	defer b.l.Unlock()
   173  
   174  	b.x, b.y, b.width, b.height = x, y, width, height
   175  
   176  	b.updateInnerRect()
   177  }
   178  
   179  // SetVisible sets the flag indicating whether or not the box is visible.
   180  func (b *Box) SetVisible(v bool) {
   181  	b.l.Lock()
   182  	defer b.l.Unlock()
   183  
   184  	b.visible = v
   185  }
   186  
   187  // GetVisible returns a value indicating whether or not the box is visible.
   188  func (b *Box) GetVisible() bool {
   189  	b.l.RLock()
   190  	defer b.l.RUnlock()
   191  
   192  	return b.visible
   193  }
   194  
   195  // SetDrawFunc sets a callback function which is invoked after the box primitive
   196  // has been drawn. This allows you to add a more individual style to the box
   197  // (and all primitives which extend it).
   198  //
   199  // The function is provided with the box's dimensions (set via SetRect()). It
   200  // must return the box's inner dimensions (x, y, width, height) which will be
   201  // returned by GetInnerRect(), used by descendent primitives to draw their own
   202  // content.
   203  func (b *Box) SetDrawFunc(handler func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)) {
   204  	b.l.Lock()
   205  	defer b.l.Unlock()
   206  
   207  	b.draw = handler
   208  }
   209  
   210  // GetDrawFunc returns the callback function which was installed with
   211  // SetDrawFunc() or nil if no such function has been installed.
   212  func (b *Box) GetDrawFunc() func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
   213  	b.l.RLock()
   214  	defer b.l.RUnlock()
   215  
   216  	return b.draw
   217  }
   218  
   219  // WrapInputHandler wraps an input handler (see InputHandler()) with the
   220  // functionality to capture input (see SetInputCapture()) before passing it
   221  // on to the provided (default) input handler.
   222  //
   223  // This is only meant to be used by subclassing primitives.
   224  func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primitive))) func(*tcell.EventKey, func(p Primitive)) {
   225  	return func(event *tcell.EventKey, setFocus func(p Primitive)) {
   226  		if b.inputCapture != nil {
   227  			event = b.inputCapture(event)
   228  		}
   229  		if event != nil && inputHandler != nil {
   230  			inputHandler(event, setFocus)
   231  		}
   232  	}
   233  }
   234  
   235  // InputHandler returns nil.
   236  func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
   237  	b.l.RLock()
   238  	defer b.l.RUnlock()
   239  
   240  	return b.WrapInputHandler(nil)
   241  }
   242  
   243  // SetInputCapture installs a function which captures key events before they are
   244  // forwarded to the primitive's default key event handler. This function can
   245  // then choose to forward that key event (or a different one) to the default
   246  // handler by returning it. If nil is returned, the default handler will not
   247  // be called.
   248  //
   249  // Providing a nil handler will remove a previously existing handler.
   250  //
   251  // Note that this function will not have an effect on primitives composed of
   252  // other primitives, such as Form, Flex, or Grid. Key events are only captured
   253  // by the primitives that have focus (e.g. InputField) and only one primitive
   254  // can have focus at a time. Composing primitives such as Form pass the focus on
   255  // to their contained primitives and thus never receive any key events
   256  // themselves. Therefore, they cannot intercept key events.
   257  func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) {
   258  	b.l.Lock()
   259  	defer b.l.Unlock()
   260  
   261  	b.inputCapture = capture
   262  }
   263  
   264  // GetInputCapture returns the function installed with SetInputCapture() or nil
   265  // if no such function has been installed.
   266  func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
   267  	b.l.RLock()
   268  	defer b.l.RUnlock()
   269  
   270  	return b.inputCapture
   271  }
   272  
   273  // WrapMouseHandler wraps a mouse event handler (see MouseHandler()) with the
   274  // functionality to capture mouse events (see SetMouseCapture()) before passing
   275  // them on to the provided (default) event handler.
   276  //
   277  // This is only meant to be used by subclassing primitives.
   278  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) {
   279  	return func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   280  		if b.mouseCapture != nil {
   281  			action, event = b.mouseCapture(action, event)
   282  		}
   283  		if event != nil && mouseHandler != nil {
   284  			consumed, capture = mouseHandler(action, event, setFocus)
   285  		}
   286  		return
   287  	}
   288  }
   289  
   290  // MouseHandler returns nil.
   291  func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   292  	return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
   293  		if action == MouseLeftClick && b.InRect(event.Position()) {
   294  			setFocus(b)
   295  			consumed = true
   296  		}
   297  		return
   298  	})
   299  }
   300  
   301  // SetMouseCapture sets a function which captures mouse events (consisting of
   302  // the original tcell mouse event and the semantic mouse action) before they are
   303  // forwarded to the primitive's default mouse event handler. This function can
   304  // then choose to forward that event (or a different one) by returning it or
   305  // returning a nil mouse event, in which case the default handler will not be
   306  // called.
   307  //
   308  // Providing a nil handler will remove a previously existing handler.
   309  func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) {
   310  	b.mouseCapture = capture
   311  }
   312  
   313  // InRect returns true if the given coordinate is within the bounds of the box's
   314  // rectangle.
   315  func (b *Box) InRect(x, y int) bool {
   316  	rectX, rectY, width, height := b.GetRect()
   317  	return x >= rectX && x < rectX+width && y >= rectY && y < rectY+height
   318  }
   319  
   320  // GetMouseCapture returns the function installed with SetMouseCapture() or nil
   321  // if no such function has been installed.
   322  func (b *Box) GetMouseCapture() func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) {
   323  	return b.mouseCapture
   324  }
   325  
   326  // SetBackgroundColor sets the box's background color.
   327  func (b *Box) SetBackgroundColor(color tcell.Color) {
   328  	b.l.Lock()
   329  	defer b.l.Unlock()
   330  
   331  	b.backgroundColor = color
   332  }
   333  
   334  // GetBackgroundColor returns the box's background color.
   335  func (b *Box) GetBackgroundColor() tcell.Color {
   336  	b.l.RLock()
   337  	defer b.l.RUnlock()
   338  	return b.backgroundColor
   339  }
   340  
   341  // SetBackgroundTransparent sets the flag indicating whether or not the box's
   342  // background is transparent. The screen is not cleared before drawing the
   343  // application. Overlaying transparent widgets directly onto the screen may
   344  // result in artifacts. To resolve this, add a blank, non-transparent Box to
   345  // the bottom layer of the interface via Panels, or set a handler via
   346  // SetBeforeDrawFunc which clears the screen.
   347  func (b *Box) SetBackgroundTransparent(transparent bool) {
   348  	b.l.Lock()
   349  	defer b.l.Unlock()
   350  
   351  	b.backgroundTransparent = transparent
   352  }
   353  
   354  // GetBorder returns a value indicating whether the box have a border
   355  // or not.
   356  func (b *Box) GetBorder() bool {
   357  	b.l.RLock()
   358  	defer b.l.RUnlock()
   359  	return b.border
   360  }
   361  
   362  // SetBorder sets the flag indicating whether or not the box should have a
   363  // border.
   364  func (b *Box) SetBorder(show bool) {
   365  	b.l.Lock()
   366  	defer b.l.Unlock()
   367  
   368  	b.border = show
   369  
   370  	b.updateInnerRect()
   371  }
   372  
   373  // SetBorderColor sets the box's border color.
   374  func (b *Box) SetBorderColor(color tcell.Color) {
   375  	b.l.Lock()
   376  	defer b.l.Unlock()
   377  
   378  	b.borderColor = color
   379  }
   380  
   381  // SetBorderColorFocused sets the box's border color when the box is focused.
   382  func (b *Box) SetBorderColorFocused(color tcell.Color) {
   383  	b.l.Lock()
   384  	defer b.l.Unlock()
   385  	b.borderColorFocused = color
   386  }
   387  
   388  // SetBorderAttributes sets the border's style attributes. You can combine
   389  // different attributes using bitmask operations:
   390  //
   391  //	box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold)
   392  func (b *Box) SetBorderAttributes(attr tcell.AttrMask) {
   393  	b.l.Lock()
   394  	defer b.l.Unlock()
   395  
   396  	b.borderAttributes = attr
   397  }
   398  
   399  // SetTitle sets the box's title.
   400  func (b *Box) SetTitle(title string) {
   401  	b.l.Lock()
   402  	defer b.l.Unlock()
   403  
   404  	// Pad title to improve readability.
   405  	if title != "" {
   406  		b.titleBuffer = []byte(" " + title + "[-:-:-] ")
   407  	} else {
   408  		b.titleBuffer = nil
   409  	}
   410  
   411  	b.title = []byte(title)
   412  }
   413  
   414  // GetTitle returns the box's current title.
   415  func (b *Box) GetTitle() string {
   416  	b.l.RLock()
   417  	defer b.l.RUnlock()
   418  
   419  	return string(b.title)
   420  }
   421  
   422  // SetTitleColor sets the box's title color.
   423  func (b *Box) SetTitleColor(color tcell.Color) {
   424  	b.l.Lock()
   425  	defer b.l.Unlock()
   426  
   427  	b.titleColor = color
   428  }
   429  
   430  // SetTitleAlign sets the alignment of the title, one of AlignLeft, AlignCenter,
   431  // or AlignRight.
   432  func (b *Box) SetTitleAlign(align int) {
   433  	b.l.Lock()
   434  	defer b.l.Unlock()
   435  
   436  	b.titleAlign = align
   437  }
   438  
   439  // Draw draws this primitive onto the screen.
   440  func (b *Box) Draw(screen tcell.Screen) {
   441  	b.l.Lock()
   442  	defer b.l.Unlock()
   443  
   444  	// Don't draw anything if the box is hidden
   445  	if !b.visible {
   446  		return
   447  	}
   448  
   449  	// Don't draw anything if there is no space.
   450  	if b.width <= 0 || b.height <= 0 {
   451  		return
   452  	}
   453  
   454  	def := tcell.StyleDefault
   455  
   456  	// Fill background.
   457  	background := def.Background(b.backgroundColor)
   458  	if !b.backgroundTransparent {
   459  		for y := b.y; y < b.y+b.height; y++ {
   460  			for x := b.x; x < b.x+b.width; x++ {
   461  				screen.Put(x, y, " ", background)
   462  			}
   463  		}
   464  	}
   465  
   466  	// Draw border.
   467  	if b.border && b.width >= 2 && b.height >= 2 {
   468  		border := SetAttributes(background.Foreground(b.borderColor), b.borderAttributes)
   469  		var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
   470  
   471  		var hasFocus bool
   472  		if b.focus == b {
   473  			hasFocus = b.hasFocus
   474  		} else {
   475  			hasFocus = b.focus.HasFocus()
   476  		}
   477  
   478  		if hasFocus && b.borderColorFocused != ColorUnset {
   479  			border = SetAttributes(background.Foreground(b.borderColorFocused), b.borderAttributes)
   480  		}
   481  
   482  		if hasFocus && b.showFocus {
   483  			horizontal = Borders.HorizontalFocus
   484  			vertical = Borders.VerticalFocus
   485  			topLeft = Borders.TopLeftFocus
   486  			topRight = Borders.TopRightFocus
   487  			bottomLeft = Borders.BottomLeftFocus
   488  			bottomRight = Borders.BottomRightFocus
   489  		} else {
   490  			horizontal = Borders.Horizontal
   491  			vertical = Borders.Vertical
   492  			topLeft = Borders.TopLeft
   493  			topRight = Borders.TopRight
   494  			bottomLeft = Borders.BottomLeft
   495  			bottomRight = Borders.BottomRight
   496  		}
   497  		for x := b.x + 1; x < b.x+b.width-1; x++ {
   498  			screen.Put(x, b.y, string(horizontal), border)
   499  			screen.Put(x, b.y+b.height-1, string(horizontal), border)
   500  		}
   501  		for y := b.y + 1; y < b.y+b.height-1; y++ {
   502  			screen.Put(b.x, y, string(vertical), border)
   503  			screen.Put(b.x+b.width-1, y, string(vertical), border)
   504  		}
   505  		screen.Put(b.x, b.y, string(topLeft), border)
   506  		screen.Put(b.x+b.width-1, b.y, string(topRight), border)
   507  		screen.Put(b.x, b.y+b.height-1, string(bottomLeft), border)
   508  		screen.Put(b.x+b.width-1, b.y+b.height-1, string(bottomRight), border)
   509  
   510  		// Draw title.
   511  		if len(b.title) > 0 && b.width >= 4 {
   512  			printed, _ := Print(screen, b.titleBuffer, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
   513  			if len(b.titleBuffer)-printed > 0 && printed > 0 {
   514  				_, style, _ := screen.Get(b.x+b.width-2, b.y)
   515  				fg := style.GetForeground()
   516  				Print(screen, []byte(string(SemigraphicsHorizontalEllipsis)), b.x+b.width-2, b.y, 1, AlignLeft, fg)
   517  			}
   518  		}
   519  	}
   520  
   521  	// Call custom draw function.
   522  	if b.draw != nil {
   523  		b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.draw(screen, b.x, b.y, b.width, b.height)
   524  	}
   525  }
   526  
   527  // ShowFocus sets the flag indicating whether or not the borders of this
   528  // primitive should change thickness when focused.
   529  func (b *Box) ShowFocus(showFocus bool) {
   530  	b.l.Lock()
   531  	defer b.l.Unlock()
   532  
   533  	b.showFocus = showFocus
   534  }
   535  
   536  // Focus is called when this primitive receives focus.
   537  func (b *Box) Focus(delegate func(p Primitive)) {
   538  	b.l.Lock()
   539  	defer b.l.Unlock()
   540  
   541  	b.hasFocus = true
   542  }
   543  
   544  // Blur is called when this primitive loses focus.
   545  func (b *Box) Blur() {
   546  	b.l.Lock()
   547  	defer b.l.Unlock()
   548  
   549  	b.hasFocus = false
   550  }
   551  
   552  // HasFocus returns whether or not this primitive has focus.
   553  func (b *Box) HasFocus() bool {
   554  	b.l.RLock()
   555  	defer b.l.RUnlock()
   556  
   557  	return b.hasFocus
   558  }
   559  
   560  // GetFocusable returns the item's Focusable.
   561  func (b *Box) GetFocusable() Focusable {
   562  	b.l.RLock()
   563  	defer b.l.RUnlock()
   564  
   565  	return b.focus
   566  }
   567  
   568  // GetBorderPadding returns the size of the padding around the box content.
   569  //
   570  // Deprecated: This function is provided for backwards compatibility.
   571  // Developers should use GetPadding instead.
   572  func (b *Box) GetBorderPadding() (top, bottom, left, right int) {
   573  	return b.GetPadding()
   574  }
   575  
   576  // SetBorderPadding sets the size of the padding around the box content.
   577  //
   578  // Deprecated: This function is provided for backwards compatibility.
   579  // Developers should use SetPadding instead.
   580  func (b *Box) SetBorderPadding(top, bottom, left, right int) {
   581  	b.SetPadding(top, bottom, left, right)
   582  }
   583  

View as plain text