aerc/lib/ui/tab.go
Robin Jarry 171fefd209 tabs: make fields private
The Tabs object exposes an array of Tab objects and the current selected
index in that array. The these two fields are sometimes modified in
goroutines, which can lead to data races causing fatal out of bounds
accesses on the tab array.

Hide these fields as private API. Expose only what needs to be seen from
the outside. This will prepare for protecting concurrent access with
a lock in the next commit.

Signed-off-by: Robin Jarry <robin@jarry.cc>
Acked-by: Koni Marti <koni.marti@gmail.com>
2022-07-23 22:00:25 +02:00

447 lines
9.1 KiB
Go

package ui
import (
"io"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
"git.sr.ht/~rjarry/aerc/config"
)
type Tabs struct {
tabs []*Tab
TabStrip *TabStrip
TabContent *TabContent
curIndex int
history []int
uiConfig *config.UIConfig
onInvalidateStrip func(d Drawable)
onInvalidateContent func(d Drawable)
parent *Tabs
CloseTab func(index int)
}
type Tab struct {
Content Drawable
Name string
invalid bool
pinned bool
indexBeforePin int
uiConf *config.UIConfig
}
type TabStrip Tabs
type TabContent Tabs
func NewTabs(uiConf *config.UIConfig) *Tabs {
tabs := &Tabs{}
tabs.uiConfig = uiConf
tabs.TabStrip = (*TabStrip)(tabs)
tabs.TabStrip.parent = tabs
tabs.TabContent = (*TabContent)(tabs)
tabs.TabContent.parent = tabs
tabs.history = []int{}
return tabs
}
func (tabs *Tabs) Add(content Drawable, name string, uiConf *config.UIConfig) *Tab {
tab := &Tab{
Content: content,
Name: name,
uiConf: uiConf,
}
tabs.tabs = append(tabs.tabs, tab)
tabs.Select(len(tabs.tabs) - 1)
content.OnInvalidate(tabs.invalidateChild)
return tab
}
func (tabs *Tabs) Names() []string {
var names []string
for _, tab := range tabs.tabs {
names = append(names, tab.Name)
}
return names
}
func (tabs *Tabs) invalidateChild(d Drawable) {
if tabs.curIndex >= len(tabs.tabs) {
return
}
if tabs.tabs[tabs.curIndex].Content == d {
if tabs.onInvalidateContent != nil {
tabs.onInvalidateContent(tabs.TabContent)
}
}
}
func (tabs *Tabs) Remove(content Drawable) {
indexToRemove := -1
for i, tab := range tabs.tabs {
if tab.Content == content {
tabs.tabs = append(tabs.tabs[:i], tabs.tabs[i+1:]...)
tabs.removeHistory(i)
indexToRemove = i
break
}
}
if indexToRemove < 0 {
return
}
// only pop the tab history if the closing tab is selected
if indexToRemove == tabs.curIndex {
index, ok := tabs.popHistory()
if ok {
tabs.selectPriv(index)
}
} else if indexToRemove < tabs.curIndex {
// selected tab is now one to the left of where it was
tabs.Select(tabs.curIndex - 1)
}
interactive, ok := tabs.tabs[tabs.curIndex].Content.(Interactive)
if ok {
interactive.Focus(true)
}
}
func (tabs *Tabs) Replace(contentSrc Drawable, contentTarget Drawable, name string) {
replaceTab := &Tab{
Content: contentTarget,
Name: name,
}
for i, tab := range tabs.tabs {
if tab.Content == contentSrc {
tabs.tabs[i] = replaceTab
tabs.Select(i)
if c, ok := contentSrc.(io.Closer); ok {
c.Close()
}
break
}
}
tabs.TabStrip.Invalidate()
contentTarget.OnInvalidate(tabs.invalidateChild)
}
func (tabs *Tabs) Get(index int) *Tab {
if index < 0 || index >= len(tabs.tabs) {
return nil
}
return tabs.tabs[index]
}
func (tabs *Tabs) Selected() *Tab {
if tabs.curIndex < 0 || tabs.curIndex >= len(tabs.tabs) {
return nil
}
return tabs.tabs[tabs.curIndex]
}
func (tabs *Tabs) Select(index int) bool {
if index < 0 || index >= len(tabs.tabs) {
return false
}
if tabs.curIndex != index {
// only push valid tabs onto the history
if tabs.curIndex < len(tabs.tabs) {
tabs.pushHistory(tabs.curIndex)
}
tabs.curIndex = index
tabs.TabStrip.Invalidate()
tabs.TabContent.Invalidate()
}
return true
}
func (tabs *Tabs) SelectName(name string) bool {
for i, tab := range tabs.tabs {
if tab.Name == name {
return tabs.Select(i)
}
}
return false
}
func (tabs *Tabs) SelectPrevious() bool {
index, ok := tabs.popHistory()
if !ok {
return false
}
return tabs.Select(index)
}
func (tabs *Tabs) MoveTab(to int, relative bool) {
from := tabs.curIndex
if relative {
to = from + to
}
if to < 0 {
to = 0
}
if to >= len(tabs.tabs) {
to = len(tabs.tabs) - 1
}
tab := tabs.tabs[from]
if to > from {
copy(tabs.tabs[from:to], tabs.tabs[from+1:to+1])
for i, h := range tabs.history {
if h == from {
tabs.history[i] = to
}
if h > from && h <= to {
tabs.history[i] -= 1
}
}
} else if from > to {
copy(tabs.tabs[to+1:from+1], tabs.tabs[to:from])
for i, h := range tabs.history {
if h == from {
tabs.history[i] = to
}
if h >= to && h < from {
tabs.history[i] += 1
}
}
} else {
return
}
tabs.tabs[to] = tab
tabs.curIndex = to
tabs.TabStrip.Invalidate()
}
func (tabs *Tabs) PinTab() {
if tabs.tabs[tabs.curIndex].pinned {
return
}
pinEnd := len(tabs.tabs)
for i, t := range tabs.tabs {
if !t.pinned {
pinEnd = i
break
}
}
for _, t := range tabs.tabs {
if t.pinned && t.indexBeforePin > tabs.curIndex-pinEnd {
t.indexBeforePin -= 1
}
}
tabs.tabs[tabs.curIndex].pinned = true
tabs.tabs[tabs.curIndex].indexBeforePin = tabs.curIndex - pinEnd
tabs.MoveTab(pinEnd, false)
}
func (tabs *Tabs) UnpinTab() {
if !tabs.tabs[tabs.curIndex].pinned {
return
}
pinEnd := len(tabs.tabs)
for i, t := range tabs.tabs {
if i != tabs.curIndex && t.pinned && t.indexBeforePin > tabs.tabs[tabs.curIndex].indexBeforePin {
t.indexBeforePin += 1
}
if !t.pinned {
pinEnd = i
break
}
}
tabs.tabs[tabs.curIndex].pinned = false
tabs.MoveTab(tabs.tabs[tabs.curIndex].indexBeforePin+pinEnd-1, false)
}
func (tabs *Tabs) NextTab() {
next := tabs.curIndex + 1
if next >= len(tabs.tabs) {
next = 0
}
tabs.Select(next)
}
func (tabs *Tabs) PrevTab() {
next := tabs.curIndex - 1
if next < 0 {
next = len(tabs.tabs) - 1
}
tabs.Select(next)
}
func (tabs *Tabs) pushHistory(index int) {
tabs.history = append(tabs.history, index)
}
func (tabs *Tabs) popHistory() (int, bool) {
lastIdx := len(tabs.history) - 1
if lastIdx < 0 {
return 0, false
}
item := tabs.history[lastIdx]
tabs.history = tabs.history[:lastIdx]
return item, true
}
func (tabs *Tabs) removeHistory(index int) {
newHist := make([]int, 0, len(tabs.history))
for i, item := range tabs.history {
if item == index {
continue
}
if item > index {
item = item - 1
}
// dedup
if i > 0 && len(newHist) > 0 && item == newHist[len(newHist)-1] {
continue
}
newHist = append(newHist, item)
}
tabs.history = newHist
}
// TODO: Color repository
func (strip *TabStrip) Draw(ctx *Context) {
x := 0
for i, tab := range strip.tabs {
uiConfig := strip.uiConfig
if tab.uiConf != nil {
uiConfig = tab.uiConf
}
style := uiConfig.GetStyle(config.STYLE_TAB)
if strip.curIndex == i {
style = uiConfig.GetStyleSelected(config.STYLE_TAB)
}
tabWidth := 32
if ctx.Width()-x < tabWidth {
tabWidth = ctx.Width() - x - 2
}
name := tab.Name
if tab.pinned {
name = uiConfig.PinnedTabMarker + name
}
trunc := runewidth.Truncate(name, tabWidth, "…")
x += ctx.Printf(x, 0, style, " %s ", trunc)
if x >= ctx.Width() {
break
}
}
ctx.Fill(x, 0, ctx.Width()-x, 1, ' ',
strip.uiConfig.GetStyle(config.STYLE_TAB))
}
func (strip *TabStrip) Invalidate() {
if strip.onInvalidateStrip != nil {
strip.onInvalidateStrip(strip)
}
}
func (strip *TabStrip) MouseEvent(localX int, localY int, event tcell.Event) {
changeFocus := func(focus bool) {
interactive, ok := strip.parent.tabs[strip.parent.curIndex].Content.(Interactive)
if ok {
interactive.Focus(focus)
}
}
unfocus := func() { changeFocus(false) }
refocus := func() { changeFocus(true) }
switch event := event.(type) {
case *tcell.EventMouse:
switch event.Buttons() {
case tcell.Button1:
selectedTab, ok := strip.clicked(localX, localY)
if !ok || selectedTab == strip.parent.curIndex {
return
}
unfocus()
strip.parent.Select(selectedTab)
refocus()
case tcell.WheelDown:
unfocus()
strip.parent.NextTab()
refocus()
case tcell.WheelUp:
unfocus()
strip.parent.PrevTab()
refocus()
case tcell.Button3:
selectedTab, ok := strip.clicked(localX, localY)
if !ok {
return
}
unfocus()
strip.parent.CloseTab(selectedTab)
refocus()
}
}
}
func (strip *TabStrip) OnInvalidate(onInvalidate func(d Drawable)) {
strip.onInvalidateStrip = onInvalidate
}
func (strip *TabStrip) clicked(mouseX int, mouseY int) (int, bool) {
x := 0
for i, tab := range strip.tabs {
trunc := runewidth.Truncate(tab.Name, 32, "…")
length := len(trunc) + 2
if x <= mouseX && mouseX < x+length {
return i, true
}
x += length
}
return 0, false
}
func (content *TabContent) Children() []Drawable {
children := make([]Drawable, len(content.tabs))
for i, tab := range content.tabs {
children[i] = tab.Content
}
return children
}
func (content *TabContent) Draw(ctx *Context) {
if content.curIndex >= len(content.tabs) {
width := ctx.Width()
height := ctx.Height()
ctx.Fill(0, 0, width, height, ' ',
content.uiConfig.GetStyle(config.STYLE_TAB))
}
tab := content.tabs[content.curIndex]
tab.Content.Draw(ctx)
}
func (content *TabContent) MouseEvent(localX int, localY int, event tcell.Event) {
tab := content.tabs[content.curIndex]
switch tabContent := tab.Content.(type) {
case Mouseable:
tabContent.MouseEvent(localX, localY, event)
}
}
func (content *TabContent) Invalidate() {
if content.onInvalidateContent != nil {
content.onInvalidateContent(content)
}
tab := content.tabs[content.curIndex]
tab.Content.Invalidate()
}
func (content *TabContent) OnInvalidate(onInvalidate func(d Drawable)) {
content.onInvalidateContent = onInvalidate
}