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