| 🛟 Updated. 28d5985 k33g 17h ago | 1 | package ui |
| 2 | |
| 3 | import ( |
| 4 | "github.com/gdamore/tcell/v2" |
| 5 | |
| 📦 Turbo Core f3ade8d k33g 9h ago | 6 | "rickub.com/turbo-editors/turbo-core/theme" |
| 🛟 Updated. 28d5985 k33g 17h ago | 7 | ) |
| 8 | |
| 9 | // StatusItem is one clickable hint on the status bar, such as "F2 Save". |
| 10 | type StatusItem struct { |
| 11 | Key string |
| 12 | Label string |
| 13 | Action func() |
| 14 | } |
| 15 | |
| 16 | // width returns how many cells the item occupies, the separating space |
| 17 | // included. |
| 18 | func (s StatusItem) width() int { |
| 19 | return len([]rune(s.Key)) + 1 + len([]rune(s.Label)) + 2 |
| 20 | } |
| 21 | |
| 22 | // StatusBar is the row of key hints along the bottom of the screen. |
| 23 | // |
| 24 | // A message set with SetMessage temporarily replaces the hints, which is how |
| 25 | // the editor reports "File saved" or a language-server error without opening a |
| 26 | // dialog for it. |
| 27 | type StatusBar struct { |
| 28 | Box |
| 29 | items []StatusItem |
| 30 | message string |
| 31 | extra string |
| 32 | } |
| 33 | |
| 34 | // NewStatusBar returns a status bar showing the given hints. |
| 35 | func NewStatusBar(items ...StatusItem) *StatusBar { |
| 36 | return &StatusBar{items: items} |
| 37 | } |
| 38 | |
| 39 | // SetItems replaces the hints, as changing context does. |
| 40 | func (s *StatusBar) SetItems(items ...StatusItem) { s.items = items } |
| 41 | |
| 42 | // Message returns the message currently shown instead of the hints. |
| 43 | func (s *StatusBar) Message() string { return s.message } |
| 44 | |
| 45 | // SetMessage shows a message in place of the hints. Pass the empty string to |
| 46 | // bring the hints back. |
| 47 | func (s *StatusBar) SetMessage(message string) { s.message = message } |
| 48 | |
| 49 | // SetExtra sets the text shown right-aligned, which the editor uses for the |
| 50 | // cursor position and the language-server state. |
| 51 | func (s *StatusBar) SetExtra(extra string) { s.extra = extra } |
| 52 | |
| 53 | // Draw paints the bar. |
| 54 | func (s *StatusBar) Draw(p *Painter, th *theme.Theme) { |
| 55 | bar := p.Sub(s.Bounds()) |
| 56 | normal := th.Style(theme.KeyStatusBar) |
| 57 | bar.Clear(normal) |
| 58 | |
| 59 | // The right-aligned text is drawn first and its start becomes the limit |
| 60 | // for everything on the left, so the two can never overwrite each other. |
| 61 | limit := s.drawExtra(bar, th) |
| 62 | |
| 63 | if s.message != "" { |
| 64 | bar.TextLimited(1, 0, limit-1, s.message, normal) |
| 65 | return |
| 66 | } |
| 67 | s.drawItems(bar, th, limit) |
| 68 | } |
| 69 | |
| 70 | // drawItems paints the key hints from the left, stopping at limit. |
| 71 | func (s *StatusBar) drawItems(p *Painter, th *theme.Theme, limit int) { |
| 72 | normal, key := th.Style(theme.KeyStatusBar), th.Style(theme.KeyStatusBarKey) |
| 73 | |
| 74 | x := 1 |
| 75 | for _, item := range s.items { |
| 76 | if x+item.width() > limit { |
| 77 | return // the rest would run into the right-aligned text |
| 78 | } |
| 79 | x = p.Text(x, 0, item.Key, key) |
| 80 | x = p.Text(x, 0, " "+item.Label+" ", normal) |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | // drawExtra paints the right-aligned text and returns the column it starts at, |
| 85 | // which is where everything drawn from the left has to stop. |
| 86 | func (s *StatusBar) drawExtra(p *Painter, th *theme.Theme) int { |
| 87 | width := p.Size().W |
| 88 | if s.extra == "" { |
| 89 | return width |
| 90 | } |
| 91 | |
| 92 | // The text is cut from the left when it is too long, so that the part |
| 93 | // that matters most — the cursor position — is the part that survives. |
| 94 | text := []rune(s.extra) |
| 95 | if len(text) > width-2 { |
| 96 | text = text[:max(width-2, 0)] |
| 97 | } |
| 98 | |
| 99 | x := max(width-len(text)-1, 0) |
| 100 | p.Text(x, 0, string(text), th.Style(theme.KeyStatusBarHint)) |
| 101 | return x |
| 102 | } |
| 103 | |
| 104 | // HandleMouse runs the action of the hint that was clicked. |
| 105 | func (s *StatusBar) HandleMouse(ev *tcell.EventMouse) bool { |
| 106 | if !s.hit(ev) || ev.Buttons() != tcell.Button1 || s.message != "" { |
| 107 | return s.hit(ev) |
| 108 | } |
| 109 | |
| 110 | x, _ := ev.Position() |
| 111 | cursor := s.Bounds().X + 1 |
| 112 | for _, item := range s.items { |
| 113 | if x >= cursor && x < cursor+item.width() && item.Action != nil { |
| 114 | item.Action() |
| 115 | return true |
| 116 | } |
| 117 | cursor += item.width() |
| 118 | } |
| 119 | return true |
| 120 | } |