1package ui
2
3import (
4	"io"
5
6	"github.com/gdamore/tcell"
7	"github.com/mattn/go-runewidth"
8
9	"git.sr.ht/~sircmpwn/aerc/config"
10)
11
12type Tabs struct {
13	Tabs       []*Tab
14	TabStrip   *TabStrip
15	TabContent *TabContent
16	Selected   int
17	history    []int
18
19	uiConfig *config.UIConfig
20
21	onInvalidateStrip   func(d Drawable)
22	onInvalidateContent func(d Drawable)
23
24	parent   *Tabs
25	CloseTab func(index int)
26}
27
28type Tab struct {
29	Content        Drawable
30	Name           string
31	invalid        bool
32	pinned         bool
33	indexBeforePin int
34}
35
36type TabStrip Tabs
37type TabContent Tabs
38
39func NewTabs(uiConf *config.UIConfig) *Tabs {
40	tabs := &Tabs{}
41	tabs.uiConfig = uiConf
42	tabs.TabStrip = (*TabStrip)(tabs)
43	tabs.TabStrip.parent = tabs
44	tabs.TabContent = (*TabContent)(tabs)
45	tabs.TabContent.parent = tabs
46	tabs.history = []int{}
47	return tabs
48}
49
50func (tabs *Tabs) Add(content Drawable, name string) *Tab {
51	tab := &Tab{
52		Content: content,
53		Name:    name,
54	}
55	tabs.Tabs = append(tabs.Tabs, tab)
56	tabs.TabStrip.Invalidate()
57	content.OnInvalidate(tabs.invalidateChild)
58	return tab
59}
60
61func (tabs *Tabs) invalidateChild(d Drawable) {
62	if tabs.Selected >= len(tabs.Tabs) {
63		return
64	}
65
66	if tabs.Tabs[tabs.Selected].Content == d {
67		if tabs.onInvalidateContent != nil {
68			tabs.onInvalidateContent(tabs.TabContent)
69		}
70	}
71}
72
73func (tabs *Tabs) Remove(content Drawable) {
74	indexToRemove := -1
75	for i, tab := range tabs.Tabs {
76		if tab.Content == content {
77			tabs.Tabs = append(tabs.Tabs[:i], tabs.Tabs[i+1:]...)
78			tabs.removeHistory(i)
79			indexToRemove = i
80			break
81		}
82	}
83	if indexToRemove < 0 {
84		return
85	}
86	// only pop the tab history if the closing tab is selected
87	if indexToRemove == tabs.Selected {
88		index, ok := tabs.popHistory()
89		if ok {
90			tabs.Select(index)
91			interactive, ok := tabs.Tabs[tabs.Selected].Content.(Interactive)
92			if ok {
93				interactive.Focus(true)
94			}
95		}
96	} else if indexToRemove < tabs.Selected {
97		// selected tab is now one to the left of where it was
98		tabs.Selected--
99	}
100	tabs.TabStrip.Invalidate()
101}
102
103func (tabs *Tabs) Replace(contentSrc Drawable, contentTarget Drawable, name string) {
104	replaceTab := &Tab{
105		Content: contentTarget,
106		Name:    name,
107	}
108	for i, tab := range tabs.Tabs {
109		if tab.Content == contentSrc {
110			tabs.Tabs[i] = replaceTab
111			tabs.Select(i)
112			if c, ok := contentSrc.(io.Closer); ok {
113				c.Close()
114			}
115			break
116		}
117	}
118	tabs.TabStrip.Invalidate()
119	contentTarget.OnInvalidate(tabs.invalidateChild)
120}
121
122func (tabs *Tabs) Select(index int) {
123	if index >= len(tabs.Tabs) {
124		index = len(tabs.Tabs) - 1
125	}
126
127	if tabs.Selected != index {
128		// only push valid tabs onto the history
129		if tabs.Selected < len(tabs.Tabs) {
130			tabs.pushHistory(tabs.Selected)
131		}
132		tabs.Selected = index
133		tabs.TabStrip.Invalidate()
134		tabs.TabContent.Invalidate()
135	}
136}
137
138func (tabs *Tabs) SelectPrevious() bool {
139	index, ok := tabs.popHistory()
140	if !ok {
141		return false
142	}
143	tabs.Select(index)
144	return true
145}
146
147func (tabs *Tabs) MoveTab(to int) {
148	from := tabs.Selected
149
150	if to < 0 {
151		to = 0
152	}
153
154	if to >= len(tabs.Tabs) {
155		to = len(tabs.Tabs) - 1
156	}
157
158	tab := tabs.Tabs[from]
159	if to > from {
160		copy(tabs.Tabs[from:to], tabs.Tabs[from+1:to+1])
161		for i, h := range tabs.history {
162			if h == from {
163				tabs.history[i] = to
164			}
165			if h > from && h <= to {
166				tabs.history[i] -= 1
167			}
168		}
169	} else if from > to {
170		copy(tabs.Tabs[to+1:from+1], tabs.Tabs[to:from])
171		for i, h := range tabs.history {
172			if h == from {
173				tabs.history[i] = to
174			}
175			if h >= to && h < from {
176				tabs.history[i] += 1
177			}
178		}
179	} else {
180		return
181	}
182
183	tabs.Tabs[to] = tab
184	tabs.Selected = to
185	tabs.TabStrip.Invalidate()
186}
187
188func (tabs *Tabs) PinTab() {
189	if tabs.Tabs[tabs.Selected].pinned {
190		return
191	}
192
193	pinEnd := len(tabs.Tabs)
194	for i, t := range tabs.Tabs {
195		if !t.pinned {
196			pinEnd = i
197			break
198		}
199	}
200
201	for _, t := range tabs.Tabs {
202		if t.pinned && t.indexBeforePin > tabs.Selected-pinEnd {
203			t.indexBeforePin -= 1
204		}
205	}
206
207	tabs.Tabs[tabs.Selected].pinned = true
208	tabs.Tabs[tabs.Selected].indexBeforePin = tabs.Selected - pinEnd
209
210	tabs.MoveTab(pinEnd)
211}
212
213func (tabs *Tabs) UnpinTab() {
214	if !tabs.Tabs[tabs.Selected].pinned {
215		return
216	}
217
218	pinEnd := len(tabs.Tabs)
219	for i, t := range tabs.Tabs {
220		if i != tabs.Selected && t.pinned && t.indexBeforePin > tabs.Tabs[tabs.Selected].indexBeforePin {
221			t.indexBeforePin += 1
222		}
223		if !t.pinned {
224			pinEnd = i
225			break
226		}
227	}
228
229	tabs.Tabs[tabs.Selected].pinned = false
230
231	tabs.MoveTab(tabs.Tabs[tabs.Selected].indexBeforePin + pinEnd - 1)
232}
233
234func (tabs *Tabs) NextTab() {
235	next := tabs.Selected + 1
236	if next >= len(tabs.Tabs) {
237		next = 0
238	}
239	tabs.Select(next)
240}
241
242func (tabs *Tabs) PrevTab() {
243	next := tabs.Selected - 1
244	if next < 0 {
245		next = len(tabs.Tabs) - 1
246	}
247	tabs.Select(next)
248}
249
250func (tabs *Tabs) pushHistory(index int) {
251	tabs.history = append(tabs.history, index)
252}
253
254func (tabs *Tabs) popHistory() (int, bool) {
255	lastIdx := len(tabs.history) - 1
256	if lastIdx < 0 {
257		return 0, false
258	}
259	item := tabs.history[lastIdx]
260	tabs.history = tabs.history[:lastIdx]
261	return item, true
262}
263
264func (tabs *Tabs) removeHistory(index int) {
265	newHist := make([]int, 0, len(tabs.history))
266	for i, item := range tabs.history {
267		if item == index {
268			continue
269		}
270		if item > index {
271			item = item - 1
272		}
273		// dedup
274		if i > 0 && len(newHist) > 0 && item == newHist[len(newHist)-1] {
275			continue
276		}
277		newHist = append(newHist, item)
278	}
279	tabs.history = newHist
280}
281
282// TODO: Color repository
283func (strip *TabStrip) Draw(ctx *Context) {
284	x := 0
285	for i, tab := range strip.Tabs {
286		style := tcell.StyleDefault.Reverse(true)
287		if strip.Selected == i {
288			style = tcell.StyleDefault
289		}
290		tabWidth := 32
291		if ctx.Width()-x < tabWidth {
292			tabWidth = ctx.Width() - x - 2
293		}
294		name := tab.Name
295		if tab.pinned {
296			name = strip.uiConfig.PinnedTabMarker + name
297		}
298		trunc := runewidth.Truncate(name, tabWidth, "…")
299		x += ctx.Printf(x, 0, style, " %s ", trunc)
300		if x >= ctx.Width() {
301			break
302		}
303	}
304	style := tcell.StyleDefault.Reverse(true)
305	ctx.Fill(x, 0, ctx.Width()-x, 1, ' ', style)
306}
307
308func (strip *TabStrip) Invalidate() {
309	if strip.onInvalidateStrip != nil {
310		strip.onInvalidateStrip(strip)
311	}
312}
313
314func (strip *TabStrip) MouseEvent(localX int, localY int, event tcell.Event) {
315	changeFocus := func(focus bool) {
316		interactive, ok := strip.parent.Tabs[strip.parent.Selected].Content.(Interactive)
317		if ok {
318			interactive.Focus(focus)
319		}
320	}
321	unfocus := func() { changeFocus(false) }
322	refocus := func() { changeFocus(true) }
323	switch event := event.(type) {
324	case *tcell.EventMouse:
325		switch event.Buttons() {
326		case tcell.Button1:
327			selectedTab, ok := strip.Clicked(localX, localY)
328			if !ok || selectedTab == strip.parent.Selected {
329				return
330			}
331			unfocus()
332			strip.parent.Select(selectedTab)
333			refocus()
334		case tcell.WheelDown:
335			unfocus()
336			strip.parent.NextTab()
337			refocus()
338		case tcell.WheelUp:
339			unfocus()
340			strip.parent.PrevTab()
341			refocus()
342		case tcell.Button3:
343			selectedTab, ok := strip.Clicked(localX, localY)
344			if !ok {
345				return
346			}
347			unfocus()
348			if selectedTab == strip.parent.Selected {
349				strip.parent.CloseTab(selectedTab)
350			} else {
351				current := strip.parent.Selected
352				strip.parent.CloseTab(selectedTab)
353				strip.parent.Select(current)
354			}
355			refocus()
356		}
357	}
358}
359
360func (strip *TabStrip) OnInvalidate(onInvalidate func(d Drawable)) {
361	strip.onInvalidateStrip = onInvalidate
362}
363
364func (strip *TabStrip) Clicked(mouseX int, mouseY int) (int, bool) {
365	x := 0
366	for i, tab := range strip.Tabs {
367		trunc := runewidth.Truncate(tab.Name, 32, "…")
368		length := len(trunc) + 2
369		if x <= mouseX && mouseX < x+length {
370			return i, true
371		}
372		x += length
373	}
374	return 0, false
375}
376
377func (content *TabContent) Children() []Drawable {
378	children := make([]Drawable, len(content.Tabs))
379	for i, tab := range content.Tabs {
380		children[i] = tab.Content
381	}
382	return children
383}
384
385func (content *TabContent) Draw(ctx *Context) {
386	if content.Selected >= len(content.Tabs) {
387		width := ctx.Width()
388		height := ctx.Height()
389		ctx.Fill(0, 0, width, height, ' ', tcell.StyleDefault)
390	}
391
392	tab := content.Tabs[content.Selected]
393	tab.Content.Draw(ctx)
394}
395
396func (content *TabContent) MouseEvent(localX int, localY int, event tcell.Event) {
397	tab := content.Tabs[content.Selected]
398	switch tabContent := tab.Content.(type) {
399	case Mouseable:
400		tabContent.MouseEvent(localX, localY, event)
401	}
402}
403
404func (content *TabContent) Invalidate() {
405	if content.onInvalidateContent != nil {
406		content.onInvalidateContent(content)
407	}
408	tab := content.Tabs[content.Selected]
409	tab.Content.Invalidate()
410}
411
412func (content *TabContent) OnInvalidate(onInvalidate func(d Drawable)) {
413	content.onInvalidateContent = onInvalidate
414}
415