package cview import ( "bytes" "fmt" "sync" "github.com/gdamore/tcell/v2" ) // TabbedPanels is a tabbed container for other primitives. The tab switcher // may be positioned vertically or horizontally, before or after the content. type TabbedPanels struct { *Flex Switcher *TextView panels *Panels tabLabels map[string]string currentTab string tabTextColor tcell.Color tabTextColorFocused tcell.Color tabBackgroundColor tcell.Color tabBackgroundColorFocused tcell.Color dividerStart string dividerMid string dividerEnd string switcherVertical bool switcherAfterContent bool width, lastWidth int setFocus func(Primitive) sync.RWMutex } // NewTabbedPanels returns a new TabbedPanels object. func NewTabbedPanels() *TabbedPanels { t := &TabbedPanels{ Flex: NewFlex(), Switcher: NewTextView(), panels: NewPanels(), tabTextColor: Styles.PrimaryTextColor, tabTextColorFocused: Styles.InverseTextColor, tabBackgroundColor: ColorUnset, tabBackgroundColorFocused: Styles.PrimaryTextColor, dividerMid: string(BoxDrawingsDoubleVertical), dividerEnd: string(BoxDrawingsLightVertical), tabLabels: make(map[string]string), } s := t.Switcher s.SetDynamicColors(true) s.SetRegions(true) s.SetWrap(true) s.SetWordWrap(true) s.SetHighlightedFunc(func(added, removed, remaining []string) { t.SetCurrentTab(added[0]) if t.setFocus != nil { t.setFocus(t.panels) } }) t.rebuild() return t } // AddTab adds a new tab. Tab names should consist only of letters, numbers // and spaces. func (t *TabbedPanels) AddTab(name, label string, item Primitive) { t.Lock() t.tabLabels[name] = label t.Unlock() t.panels.AddPanel(name, item, true, false) t.updateAll() } // RemoveTab removes a tab. func (t *TabbedPanels) RemoveTab(name string) { t.panels.RemovePanel(name) t.updateAll() } // HasTab returns true if a tab with the given name exists in this object. func (t *TabbedPanels) HasTab(name string) bool { t.RLock() defer t.RUnlock() for _, panel := range t.panels.panels { if panel.Name == name { return true } } return false } // SetCurrentTab sets the currently visible tab. func (t *TabbedPanels) SetCurrentTab(name string) { t.Lock() if t.currentTab == name { t.Unlock() return } t.currentTab = name t.updateAll() t.Unlock() t.Switcher.Highlight(t.currentTab) } // GetCurrentTab returns the currently visible tab. func (t *TabbedPanels) GetCurrentTab() string { t.RLock() defer t.RUnlock() return t.currentTab } // SetTabLabel sets the label of a tab. func (t *TabbedPanels) SetTabLabel(name, label string) { t.Lock() defer t.Unlock() if t.tabLabels[name] == label { return } t.tabLabels[name] = label t.updateTabLabels() } // SetTabTextColor sets the color of the tab text. func (t *TabbedPanels) SetTabTextColor(color tcell.Color) { t.Lock() defer t.Unlock() t.tabTextColor = color } // SetTabTextColorFocused sets the color of the tab text when the tab is in focus. func (t *TabbedPanels) SetTabTextColorFocused(color tcell.Color) { t.Lock() defer t.Unlock() t.tabTextColorFocused = color } // SetTabBackgroundColor sets the background color of the tab. func (t *TabbedPanels) SetTabBackgroundColor(color tcell.Color) { t.Lock() defer t.Unlock() t.tabBackgroundColor = color } // SetTabBackgroundColorFocused sets the background color of the tab when the // tab is in focus. func (t *TabbedPanels) SetTabBackgroundColorFocused(color tcell.Color) { t.Lock() defer t.Unlock() t.tabBackgroundColorFocused = color } // SetTabSwitcherDivider sets the tab switcher divider text. Color tags are supported. func (t *TabbedPanels) SetTabSwitcherDivider(start, mid, end string) { t.Lock() defer t.Unlock() t.dividerStart, t.dividerMid, t.dividerEnd = start, mid, end } // SetTabSwitcherVertical sets the orientation of the tab switcher. func (t *TabbedPanels) SetTabSwitcherVertical(vertical bool) { t.Lock() defer t.Unlock() if t.switcherVertical == vertical { return } t.switcherVertical = vertical t.rebuild() } // SetTabSwitcherAfterContent sets whether the tab switcher is positioned after content. func (t *TabbedPanels) SetTabSwitcherAfterContent(after bool) { t.Lock() defer t.Unlock() if t.switcherAfterContent == after { return } t.switcherAfterContent = after t.rebuild() } func (t *TabbedPanels) rebuild() { f := t.Flex if t.switcherVertical { f.SetDirection(FlexColumn) } else { f.SetDirection(FlexRow) } f.RemoveItem(t.panels) f.RemoveItem(t.Switcher) if t.switcherAfterContent { f.AddItem(t.panels, 0, 1, true) f.AddItem(t.Switcher, 1, 1, false) } else { f.AddItem(t.Switcher, 1, 1, false) f.AddItem(t.panels, 0, 1, true) } t.updateTabLabels() } func (t *TabbedPanels) updateTabLabels() { if len(t.panels.panels) == 0 { t.Switcher.SetText("") t.Flex.ResizeItem(t.Switcher, 0, 1) return } maxWidth := 0 for _, panel := range t.panels.panels { label := t.tabLabels[panel.Name] if len(label) > maxWidth { maxWidth = len(label) } } var b bytes.Buffer if !t.switcherVertical { b.WriteString(t.dividerStart) } l := len(t.panels.panels) spacer := []byte(" ") for i, panel := range t.panels.panels { if i > 0 && t.switcherVertical { b.WriteRune('\n') } if t.switcherVertical && t.switcherAfterContent { b.WriteString(t.dividerMid) b.WriteRune(' ') } textColor := t.tabTextColor backgroundColor := t.tabBackgroundColor if panel.Name == t.currentTab { textColor = t.tabTextColorFocused backgroundColor = t.tabBackgroundColorFocused } label := t.tabLabels[panel.Name] if !t.switcherVertical { label = " " + label } if t.switcherVertical { spacer = bytes.Repeat([]byte(" "), maxWidth-len(label)+1) } b.WriteString(fmt.Sprintf(`["%s"][%s:%s]%s%s[-:-][""]`, panel.Name, ColorHex(textColor), ColorHex(backgroundColor), label, spacer)) if i == l-1 && !t.switcherVertical { b.WriteString(t.dividerEnd) } else if !t.switcherAfterContent { b.WriteString(t.dividerMid) } } t.Switcher.SetText(b.String()) var reqLines int if t.switcherVertical { reqLines = maxWidth + 2 } else { reqLines = len(WordWrap(t.Switcher.GetText(true), t.width)) if reqLines < 1 { reqLines = 1 } } t.Flex.ResizeItem(t.Switcher, reqLines, 1) } func (t *TabbedPanels) updateVisibleTabs() { allPanels := t.panels.panels var newTab string var foundCurrent bool for _, panel := range allPanels { if panel.Name == t.currentTab { newTab = panel.Name foundCurrent = true break } } if !foundCurrent { for _, panel := range allPanels { if panel.Name != "" { newTab = panel.Name break } } } if t.currentTab != newTab { t.SetCurrentTab(newTab) return } for _, panel := range allPanels { if panel.Name == t.currentTab { t.panels.ShowPanel(panel.Name) } else { t.panels.HidePanel(panel.Name) } } } func (t *TabbedPanels) updateAll() { t.updateTabLabels() t.updateVisibleTabs() } // Draw draws this primitive onto the screen. func (t *TabbedPanels) Draw(screen tcell.Screen) { if !t.GetVisible() { return } t.Box.Draw(screen) _, _, t.width, _ = t.GetInnerRect() if t.width != t.lastWidth { t.updateTabLabels() } t.lastWidth = t.width t.Flex.Draw(screen) } // InputHandler returns the handler for this primitive. func (t *TabbedPanels) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { if t.setFocus == nil { t.setFocus = setFocus } t.Flex.InputHandler()(event, setFocus) }) } // MouseHandler returns the mouse handler for this primitive. func (t *TabbedPanels) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) { if t.setFocus == nil { t.setFocus = setFocus } x, y := event.Position() if !t.InRect(x, y) { return false, nil } if t.Switcher.InRect(x, y) { if t.setFocus != nil { defer t.setFocus(t.panels) } defer t.Switcher.MouseHandler()(action, event, setFocus) return true, nil } return t.Flex.MouseHandler()(action, event, setFocus) }) }