1package tview
2
3import (
4	"github.com/gdamore/tcell/v2"
5)
6
7// frameText holds information about a line of text shown in the frame.
8type frameText struct {
9	Text   string      // The text to be displayed.
10	Header bool        // true = place in header, false = place in footer.
11	Align  int         // One of the Align constants.
12	Color  tcell.Color // The text color.
13}
14
15// Frame is a wrapper which adds space around another primitive. In addition,
16// the top area (header) and the bottom area (footer) may also contain text.
17//
18// See https://github.com/rivo/tview/wiki/Frame for an example.
19type Frame struct {
20	*Box
21
22	// The contained primitive. May be nil.
23	primitive Primitive
24
25	// The lines of text to be displayed.
26	text []*frameText
27
28	// Border spacing.
29	top, bottom, header, footer, left, right int
30}
31
32// NewFrame returns a new frame around the given primitive. The primitive's
33// size will be changed to fit within this frame. The primitive may be nil, in
34// which case no other primitive is embedded in the frame.
35func NewFrame(primitive Primitive) *Frame {
36	box := NewBox()
37
38	f := &Frame{
39		Box:       box,
40		primitive: primitive,
41		top:       1,
42		bottom:    1,
43		header:    1,
44		footer:    1,
45		left:      1,
46		right:     1,
47	}
48
49	return f
50}
51
52// AddText adds text to the frame. Set "header" to true if the text is to appear
53// in the header, above the contained primitive. Set it to false for it to
54// appear in the footer, below the contained primitive. "align" must be one of
55// the Align constants. Rows in the header are printed top to bottom, rows in
56// the footer are printed bottom to top. Note that long text can overlap as
57// different alignments will be placed on the same row.
58func (f *Frame) AddText(text string, header bool, align int, color tcell.Color) *Frame {
59	f.text = append(f.text, &frameText{
60		Text:   text,
61		Header: header,
62		Align:  align,
63		Color:  color,
64	})
65	return f
66}
67
68// Clear removes all text from the frame.
69func (f *Frame) Clear() *Frame {
70	f.text = nil
71	return f
72}
73
74// SetBorders sets the width of the frame borders as well as "header" and
75// "footer", the vertical space between the header and footer text and the
76// contained primitive (does not apply if there is no text).
77func (f *Frame) SetBorders(top, bottom, header, footer, left, right int) *Frame {
78	f.top, f.bottom, f.header, f.footer, f.left, f.right = top, bottom, header, footer, left, right
79	return f
80}
81
82// Draw draws this primitive onto the screen.
83func (f *Frame) Draw(screen tcell.Screen) {
84	f.Box.DrawForSubclass(screen, f)
85
86	// Calculate start positions.
87	x, top, width, height := f.GetInnerRect()
88	bottom := top + height - 1
89	x += f.left
90	top += f.top
91	bottom -= f.bottom
92	width -= f.left + f.right
93	if width <= 0 || top >= bottom {
94		return // No space left.
95	}
96
97	// Draw text.
98	var rows [6]int // top-left, top-center, top-right, bottom-left, bottom-center, bottom-right.
99	topMax := top
100	bottomMin := bottom
101	for _, text := range f.text {
102		// Where do we place this text?
103		var y int
104		if text.Header {
105			y = top + rows[text.Align]
106			rows[text.Align]++
107			if y >= bottomMin {
108				continue
109			}
110			if y+1 > topMax {
111				topMax = y + 1
112			}
113		} else {
114			y = bottom - rows[3+text.Align]
115			rows[3+text.Align]++
116			if y <= topMax {
117				continue
118			}
119			if y-1 < bottomMin {
120				bottomMin = y - 1
121			}
122		}
123
124		// Draw text.
125		Print(screen, text.Text, x, y, width, text.Align, text.Color)
126	}
127
128	// Set the size of the contained primitive.
129	if f.primitive != nil {
130		if topMax > top {
131			top = topMax + f.header
132		}
133		if bottomMin < bottom {
134			bottom = bottomMin - f.footer
135		}
136		if top > bottom {
137			return // No space for the primitive.
138		}
139		f.primitive.SetRect(x, top, width, bottom+1-top)
140
141		// Finally, draw the contained primitive.
142		f.primitive.Draw(screen)
143	}
144}
145
146// Focus is called when this primitive receives focus.
147func (f *Frame) Focus(delegate func(p Primitive)) {
148	if f.primitive != nil {
149		delegate(f.primitive)
150	} else {
151		f.hasFocus = true
152	}
153}
154
155// HasFocus returns whether or not this primitive has focus.
156func (f *Frame) HasFocus() bool {
157	if f.primitive == nil {
158		return f.hasFocus
159	}
160	return f.primitive.HasFocus()
161}
162
163// MouseHandler returns the mouse handler for this primitive.
164func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
165	return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
166		if !f.InRect(event.Position()) {
167			return false, nil
168		}
169
170		// Pass mouse events on to contained primitive.
171		if f.primitive != nil {
172			return f.primitive.MouseHandler()(action, event, setFocus)
173		}
174
175		return false, nil
176	})
177}
178
179// InputHandler returns the handler for this primitive.
180func (f *Frame) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
181	return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
182		if f.primitive == nil {
183			return
184		}
185		if f.primitive.HasFocus() {
186			if handler := f.primitive.InputHandler(); handler != nil {
187				handler(event, setFocus)
188				return
189			}
190		}
191	})
192}
193