Merge branch 'panels'
This commit is contained in:
commit
70e32e0b5c
135
main.go
135
main.go
@ -21,15 +21,15 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var theme = ui.Theme{
|
var theme = ui.Theme{
|
||||||
"StatusBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
"StatusBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorLightGray),
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
screen *tcell.Screen
|
screen *tcell.Screen
|
||||||
|
|
||||||
menuBar *ui.MenuBar
|
menuBar *ui.MenuBar
|
||||||
tabContainer *ui.TabContainer
|
panelContainer *ui.PanelContainer
|
||||||
dialog ui.Component // nil if not present (has exclusive focus)
|
dialog ui.Component // nil if not present (has exclusive focus)
|
||||||
|
|
||||||
focusedComponent ui.Component = nil
|
focusedComponent ui.Component = nil
|
||||||
)
|
)
|
||||||
@ -48,15 +48,23 @@ func showErrorDialog(title string, message string, callback func()) {
|
|||||||
callback()
|
callback()
|
||||||
} else {
|
} else {
|
||||||
dialog = nil
|
dialog = nil
|
||||||
changeFocus(tabContainer) // Default behavior: focus tabContainer
|
changeFocus(panelContainer) // Default behavior: focus panelContainer
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
changeFocus(dialog)
|
changeFocus(dialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getActiveTabContainer() *ui.TabContainer {
|
||||||
|
if panelContainer.GetSelected() != nil {
|
||||||
|
return panelContainer.GetSelected().(*ui.TabContainer)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// returns nil if no TextEdit is visible
|
// returns nil if no TextEdit is visible
|
||||||
func getActiveTextEdit() *ui.TextEdit {
|
func getActiveTextEdit() *ui.TextEdit {
|
||||||
if tabContainer.GetTabCount() > 0 {
|
tabContainer := getActiveTabContainer()
|
||||||
|
if tabContainer != nil && tabContainer.GetTabCount() > 0 {
|
||||||
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
|
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
|
||||||
te := tab.Child.(*ui.TextEdit)
|
te := tab.Child.(*ui.TextEdit)
|
||||||
return te
|
return te
|
||||||
@ -68,6 +76,7 @@ func getActiveTextEdit() *ui.TextEdit {
|
|||||||
func saveAs() {
|
func saveAs() {
|
||||||
callback := func(filePaths []string) {
|
callback := func(filePaths []string) {
|
||||||
te := getActiveTextEdit() // te should have value if we are here
|
te := getActiveTextEdit() // te should have value if we are here
|
||||||
|
tabContainer := getActiveTabContainer()
|
||||||
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
|
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
|
||||||
|
|
||||||
// If we got the callback, it is safe to assume there are one or more files
|
// If we got the callback, it is safe to assume there are one or more files
|
||||||
@ -89,7 +98,8 @@ func saveAs() {
|
|||||||
tab.Name = filePaths[0]
|
tab.Name = filePaths[0]
|
||||||
|
|
||||||
dialog = nil // Hide the file selector
|
dialog = nil // Hide the file selector
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
|
tab.Name = filePaths[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog = ui.NewFileSelectorDialog(
|
dialog = ui.NewFileSelectorDialog(
|
||||||
@ -100,7 +110,7 @@ func saveAs() {
|
|||||||
callback,
|
callback,
|
||||||
func() { // Dialog canceled
|
func() { // Dialog canceled
|
||||||
dialog = nil
|
dialog = nil
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
changeFocus(dialog)
|
changeFocus(dialog)
|
||||||
@ -135,11 +145,13 @@ func main() {
|
|||||||
var closing bool
|
var closing bool
|
||||||
sizex, sizey := s.Size()
|
sizex, sizey := s.Size()
|
||||||
|
|
||||||
tabContainer = ui.NewTabContainer(&theme)
|
panelContainer = ui.NewPanelContainer(&theme)
|
||||||
tabContainer.SetPos(0, 1)
|
panelContainer.SetPos(0, 1)
|
||||||
tabContainer.SetSize(sizex, sizey-2)
|
panelContainer.SetSize(sizex, sizey-2)
|
||||||
|
|
||||||
changeFocus(tabContainer) // tabContainer focused by default
|
panelContainer.SetSelected(ui.NewTabContainer(&theme))
|
||||||
|
|
||||||
|
changeFocus(panelContainer) // panelContainer focused by default
|
||||||
|
|
||||||
// Open files from command-line arguments
|
// Open files from command-line arguments
|
||||||
if flag.NArg() > 0 {
|
if flag.NArg() > 0 {
|
||||||
@ -169,9 +181,9 @@ func main() {
|
|||||||
|
|
||||||
textEdit := ui.NewTextEdit(screen, arg, bytes, &theme)
|
textEdit := ui.NewTextEdit(screen, arg, bytes, &theme)
|
||||||
textEdit.Dirty = dirty
|
textEdit.Dirty = dirty
|
||||||
tabContainer.AddTab(arg, textEdit)
|
getActiveTabContainer().AddTab(arg, textEdit)
|
||||||
}
|
}
|
||||||
tabContainer.SetFocused(true) // Lets any opened TextEdit component know to be focused
|
panelContainer.SetFocused(true) // Lets any opened TextEdit component know to be focused
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = ClipInitialize(ClipExternal)
|
_, err = ClipInitialize(ClipExternal)
|
||||||
@ -185,12 +197,18 @@ func main() {
|
|||||||
|
|
||||||
fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New File", Shortcut: "Ctrl+N", Callback: func() {
|
fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New File", Shortcut: "Ctrl+N", Callback: func() {
|
||||||
textEdit := ui.NewTextEdit(screen, "", []byte{}, &theme) // No file path, no contents
|
textEdit := ui.NewTextEdit(screen, "", []byte{}, &theme) // No file path, no contents
|
||||||
|
tabContainer := getActiveTabContainer()
|
||||||
|
if tabContainer == nil {
|
||||||
|
tabContainer = ui.NewTabContainer(&theme)
|
||||||
|
panelContainer.SetSelected(tabContainer)
|
||||||
|
}
|
||||||
tabContainer.AddTab("noname", textEdit)
|
tabContainer.AddTab("noname", textEdit)
|
||||||
|
|
||||||
changeFocus(tabContainer)
|
|
||||||
tabContainer.FocusTab(tabContainer.GetTabCount() - 1)
|
tabContainer.FocusTab(tabContainer.GetTabCount() - 1)
|
||||||
|
changeFocus(panelContainer)
|
||||||
}}, &ui.ItemEntry{Name: "Open...", Shortcut: "Ctrl+O", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Open...", Shortcut: "Ctrl+O", Callback: func() {
|
||||||
callback := func(filePaths []string) {
|
callback := func(filePaths []string) {
|
||||||
|
tabContainer := getActiveTabContainer()
|
||||||
|
|
||||||
var errOccurred bool
|
var errOccurred bool
|
||||||
for _, path := range filePaths {
|
for _, path := range filePaths {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
@ -209,12 +227,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textEdit := ui.NewTextEdit(screen, path, bytes, &theme)
|
textEdit := ui.NewTextEdit(screen, path, bytes, &theme)
|
||||||
|
if tabContainer == nil {
|
||||||
|
tabContainer = ui.NewTabContainer(&theme)
|
||||||
|
panelContainer.SetSelected(tabContainer)
|
||||||
|
}
|
||||||
tabContainer.AddTab(path, textEdit)
|
tabContainer.AddTab(path, textEdit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errOccurred { // Prevent hiding the error dialog
|
if !errOccurred { // Prevent hiding the error dialog
|
||||||
dialog = nil // Hide the file selector
|
dialog = nil // Hide the file selector
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
if tabContainer.GetTabCount() > 0 {
|
if tabContainer.GetTabCount() > 0 {
|
||||||
tabContainer.FocusTab(tabContainer.GetTabCount() - 1)
|
tabContainer.FocusTab(tabContainer.GetTabCount() - 1)
|
||||||
}
|
}
|
||||||
@ -228,7 +250,7 @@ func main() {
|
|||||||
callback,
|
callback,
|
||||||
func() { // Dialog is canceled
|
func() { // Dialog is canceled
|
||||||
dialog = nil
|
dialog = nil
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
changeFocus(dialog)
|
changeFocus(dialog)
|
||||||
@ -250,24 +272,36 @@ func main() {
|
|||||||
}
|
}
|
||||||
te.Dirty = false
|
te.Dirty = false
|
||||||
|
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
} else {
|
} else {
|
||||||
saveAs()
|
saveAs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}, &ui.ItemEntry{Name: "Save As...", QuickChar: 5, Callback: saveAs}, &ui.ItemSeparator{},
|
}}, &ui.ItemEntry{Name: "Save As...", QuickChar: 5, Callback: saveAs}, &ui.ItemSeparator{},
|
||||||
&ui.ItemEntry{Name: "Close", Shortcut: "Ctrl+Q", Callback: func() {
|
&ui.ItemEntry{Name: "Close", Shortcut: "Ctrl+Q", Callback: func() {
|
||||||
if tabContainer.GetTabCount() > 0 {
|
tabContainer := getActiveTabContainer()
|
||||||
|
if tabContainer != nil && tabContainer.GetTabCount() > 0 {
|
||||||
tabContainer.RemoveTab(tabContainer.GetSelectedTabIdx())
|
tabContainer.RemoveTab(tabContainer.GetSelectedTabIdx())
|
||||||
} else { // No tabs open; close the editor
|
} else {
|
||||||
closing = true
|
// if the selected is root: close editor. otherwise close panel
|
||||||
|
if panelContainer.IsRootSelected() {
|
||||||
|
closing = true
|
||||||
|
} else {
|
||||||
|
panelContainer.DeleteSelected()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}})
|
}}})
|
||||||
|
|
||||||
panelMenu := ui.NewMenu("Panel", 0, &theme)
|
panelMenu := ui.NewMenu("Panel", 0, &theme)
|
||||||
|
|
||||||
panelMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Focus Up", QuickChar: -1, Shortcut: "Alt+Up", Callback: func() {
|
panelMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Focus Next", Shortcut: "Alt+.", Callback: func() {
|
||||||
|
panelContainer.SelectNext()
|
||||||
|
changeFocus(panelContainer)
|
||||||
|
}}, &ui.ItemEntry{Name: "Focus Prev", Shortcut: "Alt+,", Callback: func() {
|
||||||
|
panelContainer.SelectPrev()
|
||||||
|
changeFocus(panelContainer)
|
||||||
|
}}, &ui.ItemEntry{Name: "Focus Up", QuickChar: -1, Shortcut: "Alt+Up", Callback: func() {
|
||||||
|
|
||||||
}}, &ui.ItemEntry{Name: "Focus Down", QuickChar: -1, Shortcut: "Alt+Down", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Focus Down", QuickChar: -1, Shortcut: "Alt+Down", Callback: func() {
|
||||||
|
|
||||||
}}, &ui.ItemEntry{Name: "Focus Left", QuickChar: -1, Shortcut: "Alt+Left", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Focus Left", QuickChar: -1, Shortcut: "Alt+Left", Callback: func() {
|
||||||
@ -275,19 +309,33 @@ func main() {
|
|||||||
}}, &ui.ItemEntry{Name: "Focus Right", QuickChar: -1, Shortcut: "Alt+Right", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Focus Right", QuickChar: -1, Shortcut: "Alt+Right", Callback: func() {
|
||||||
|
|
||||||
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Split Top", QuickChar: 6, Callback: func() {
|
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Split Top", QuickChar: 6, Callback: func() {
|
||||||
|
panelContainer.SplitSelected(ui.SplitVertical, ui.NewTabContainer(&theme))
|
||||||
|
panelContainer.SwapNeighborsSelected()
|
||||||
|
panelContainer.SelectPrev()
|
||||||
|
changeFocus(panelContainer)
|
||||||
}}, &ui.ItemEntry{Name: "Split Bottom", QuickChar: 6, Callback: func() {
|
}}, &ui.ItemEntry{Name: "Split Bottom", QuickChar: 6, Callback: func() {
|
||||||
|
panelContainer.SplitSelected(ui.SplitVertical, ui.NewTabContainer(&theme))
|
||||||
|
panelContainer.SelectNext()
|
||||||
|
changeFocus(panelContainer)
|
||||||
}}, &ui.ItemEntry{Name: "Split Left", QuickChar: 6, Callback: func() {
|
}}, &ui.ItemEntry{Name: "Split Left", QuickChar: 6, Callback: func() {
|
||||||
|
panelContainer.SplitSelected(ui.SplitHorizontal, ui.NewTabContainer(&theme))
|
||||||
|
panelContainer.SwapNeighborsSelected()
|
||||||
|
panelContainer.SelectPrev()
|
||||||
|
changeFocus(panelContainer)
|
||||||
}}, &ui.ItemEntry{Name: "Split Right", QuickChar: 6, Callback: func() {
|
}}, &ui.ItemEntry{Name: "Split Right", QuickChar: 6, Callback: func() {
|
||||||
|
panelContainer.SplitSelected(ui.SplitHorizontal, ui.NewTabContainer(&theme))
|
||||||
|
panelContainer.SelectNext()
|
||||||
|
changeFocus(panelContainer)
|
||||||
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Move", Shortcut: "Ctrl+M", Callback: func() {
|
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Move", Shortcut: "Ctrl+M", Callback: func() {
|
||||||
|
|
||||||
}}, &ui.ItemEntry{Name: "Resize", Shortcut: "Ctrl+R", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Resize", Shortcut: "Ctrl+R", Callback: func() {
|
||||||
|
|
||||||
}}, &ui.ItemEntry{Name: "Float", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Toggle Floating", Callback: func() {
|
||||||
|
panelContainer.FloatSelected()
|
||||||
|
if !panelContainer.GetFloatingFocused() {
|
||||||
|
panelContainer.SetFloatingFocused(true)
|
||||||
|
}
|
||||||
|
changeFocus(panelContainer)
|
||||||
}}})
|
}}})
|
||||||
|
|
||||||
editMenu := ui.NewMenu("Edit", 0, &theme)
|
editMenu := ui.NewMenu("Edit", 0, &theme)
|
||||||
@ -305,7 +353,7 @@ func main() {
|
|||||||
te.Delete(false) // Delete selection
|
te.Delete(false) // Delete selection
|
||||||
}
|
}
|
||||||
if err == nil { // Prevent hiding error dialog
|
if err == nil { // Prevent hiding error dialog
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}, &ui.ItemEntry{Name: "Copy", Shortcut: "Ctrl+C", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Copy", Shortcut: "Ctrl+C", Callback: func() {
|
||||||
@ -320,7 +368,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}, &ui.ItemEntry{Name: "Paste", Shortcut: "Ctrl+V", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Paste", Shortcut: "Ctrl+V", Callback: func() {
|
||||||
@ -331,7 +379,7 @@ func main() {
|
|||||||
showErrorDialog("Clipboard Failure", fmt.Sprintf("%v", err), nil)
|
showErrorDialog("Clipboard Failure", fmt.Sprintf("%v", err), nil)
|
||||||
} else {
|
} else {
|
||||||
te.Insert(contents)
|
te.Insert(contents)
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Select All", QuickChar: 7, Shortcut: "Ctrl+A", Callback: func() {
|
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Select All", QuickChar: 7, Shortcut: "Ctrl+A", Callback: func() {
|
||||||
@ -354,12 +402,12 @@ func main() {
|
|||||||
te.SetLineCol(line-1, 0)
|
te.SetLineCol(line-1, 0)
|
||||||
// Hide dialog
|
// Hide dialog
|
||||||
dialog = nil
|
dialog = nil
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
}
|
}
|
||||||
dialog = NewGotoLineDialog(screen, &theme, callback, func() {
|
dialog = NewGotoLineDialog(screen, &theme, callback, func() {
|
||||||
// Dialog canceled
|
// Dialog canceled
|
||||||
dialog = nil
|
dialog = nil
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
})
|
})
|
||||||
changeFocus(dialog)
|
changeFocus(dialog)
|
||||||
}
|
}
|
||||||
@ -378,10 +426,8 @@ func main() {
|
|||||||
//ui.DrawRect(screen, 0, 0, sizex, sizey, '▚', tcell.Style{}.Foreground(tcell.ColorGrey).Background(tcell.ColorBlack))
|
//ui.DrawRect(screen, 0, 0, sizex, sizey, '▚', tcell.Style{}.Foreground(tcell.ColorGrey).Background(tcell.ColorBlack))
|
||||||
ui.DrawRect(s, 0, 1, sizex, sizey-1, ' ', tcell.Style{}.Background(tcell.ColorBlack))
|
ui.DrawRect(s, 0, 1, sizex, sizey-1, ' ', tcell.Style{}.Background(tcell.ColorBlack))
|
||||||
|
|
||||||
if tabContainer.GetTabCount() > 0 { // Draw the tab container only if a tab is open
|
panelContainer.Draw(s)
|
||||||
tabContainer.Draw(s)
|
menuBar.Draw(s)
|
||||||
}
|
|
||||||
menuBar.Draw(s) // Always draw the menu bar
|
|
||||||
|
|
||||||
if dialog != nil {
|
if dialog != nil {
|
||||||
// Update fileSelector dialog pos and size
|
// Update fileSelector dialog pos and size
|
||||||
@ -394,10 +440,7 @@ func main() {
|
|||||||
|
|
||||||
// Draw statusbar
|
// Draw statusbar
|
||||||
ui.DrawRect(s, 0, sizey-1, sizex, 1, ' ', theme["StatusBar"])
|
ui.DrawRect(s, 0, sizey-1, sizex, 1, ' ', theme["StatusBar"])
|
||||||
if tabContainer.GetTabCount() > 0 {
|
if te := getActiveTextEdit(); te != nil {
|
||||||
focusedTab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
|
|
||||||
te := focusedTab.Child.(*ui.TextEdit)
|
|
||||||
|
|
||||||
var delim string
|
var delim string
|
||||||
if te.IsCRLF {
|
if te.IsCRLF {
|
||||||
delim = "CRLF"
|
delim = "CRLF"
|
||||||
@ -425,17 +468,17 @@ func main() {
|
|||||||
sizex, sizey = s.Size()
|
sizex, sizey = s.Size()
|
||||||
|
|
||||||
menuBar.SetSize(sizex, 1)
|
menuBar.SetSize(sizex, 1)
|
||||||
tabContainer.SetSize(sizex, sizey-2)
|
panelContainer.SetSize(sizex, sizey-2)
|
||||||
|
|
||||||
s.Sync() // Redraw everything
|
s.Sync() // Redraw everything
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
// On Escape, we change focus between editor and the MenuBar.
|
// On Escape, we change focus between editor and the MenuBar.
|
||||||
if dialog == nil {
|
if dialog == nil {
|
||||||
if ev.Key() == tcell.KeyEscape {
|
if ev.Key() == tcell.KeyEscape {
|
||||||
if focusedComponent == tabContainer {
|
if focusedComponent == panelContainer {
|
||||||
changeFocus(menuBar)
|
changeFocus(menuBar)
|
||||||
} else {
|
} else {
|
||||||
changeFocus(tabContainer)
|
changeFocus(panelContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ type Syntax uint8
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
Default Syntax = iota
|
Default Syntax = iota
|
||||||
|
Column // Not necessarily a Syntax; useful for Colorscheming editor column
|
||||||
Keyword
|
Keyword
|
||||||
String
|
String
|
||||||
Special
|
Special
|
||||||
|
@ -15,8 +15,8 @@ type Component interface {
|
|||||||
// A component knows its position and size, which is used to draw itself in
|
// A component knows its position and size, which is used to draw itself in
|
||||||
// its bounding rectangle.
|
// its bounding rectangle.
|
||||||
Draw(tcell.Screen)
|
Draw(tcell.Screen)
|
||||||
// Components can be focused, which may affect how it handles events. For
|
// Components can be focused, which may affect how it handles events or draws.
|
||||||
// example, when a button is focused, the Return key may be pressed to
|
// For example, when a button is focused, the Return key may be pressed to
|
||||||
// activate the button.
|
// activate the button.
|
||||||
SetFocused(bool)
|
SetFocused(bool)
|
||||||
// Applies the theme to the component and all of its children.
|
// Applies the theme to the component and all of its children.
|
||||||
|
10
ui/menu.go
10
ui/menu.go
@ -148,7 +148,12 @@ func (b *MenuBar) CursorRight() {
|
|||||||
|
|
||||||
// Draw renders the MenuBar and its sub-menus.
|
// Draw renders the MenuBar and its sub-menus.
|
||||||
func (b *MenuBar) Draw(s tcell.Screen) {
|
func (b *MenuBar) Draw(s tcell.Screen) {
|
||||||
normalStyle := b.theme.GetOrDefault("MenuBar")
|
var normalStyle tcell.Style
|
||||||
|
if b.focused {
|
||||||
|
normalStyle = b.theme.GetOrDefault("MenuBarFocused")
|
||||||
|
} else {
|
||||||
|
normalStyle = b.theme.GetOrDefault("MenuBar")
|
||||||
|
}
|
||||||
|
|
||||||
// Draw menus based on whether b.focused and which is selected
|
// Draw menus based on whether b.focused and which is selected
|
||||||
DrawRect(s, b.x, b.y, b.width, 1, ' ', normalStyle)
|
DrawRect(s, b.x, b.y, b.width, 1, ' ', normalStyle)
|
||||||
@ -156,7 +161,8 @@ func (b *MenuBar) Draw(s tcell.Screen) {
|
|||||||
for i, item := range b.menus {
|
for i, item := range b.menus {
|
||||||
sty := normalStyle
|
sty := normalStyle
|
||||||
if b.focused && b.selected == i {
|
if b.focused && b.selected == i {
|
||||||
sty = b.theme.GetOrDefault("MenuBarSelected") // Use special style for selected item
|
fg, bg, attr := normalStyle.Decompose()
|
||||||
|
sty = tcell.Style{}.Foreground(bg).Background(fg).Attributes(attr)
|
||||||
}
|
}
|
||||||
|
|
||||||
str := fmt.Sprintf(" %s ", item.Name)
|
str := fmt.Sprintf(" %s ", item.Name)
|
||||||
|
227
ui/panel.go
Executable file
227
ui/panel.go
Executable file
@ -0,0 +1,227 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import "github.com/gdamore/tcell/v2"
|
||||||
|
|
||||||
|
// A PanelKind describes how to interpret the fields of a Panel.
|
||||||
|
type PanelKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
PanelKindEmpty PanelKind = iota
|
||||||
|
PanelKindSingle // Single item. Takes up all available space
|
||||||
|
PanelKindSplitVert // Items are above or below eachother
|
||||||
|
PanelKindSplitHor // Items are left or right of eachother
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Panel represents a container for a split view between two items. The Kind
|
||||||
|
// tells how to interpret the Left and Right fields. The SplitAt is the column
|
||||||
|
// between 0 and width or height, representing the position of the split between
|
||||||
|
// the Left and Right, respectively.
|
||||||
|
//
|
||||||
|
// If the Kind is equal to PanelKindEmpty, then both Left and Right are nil.
|
||||||
|
// If the Kind is equal to PanelKindSingle, then only Left has value,
|
||||||
|
// and its value will NOT be of type Panel. The SplitAt will not be used,
|
||||||
|
// as the Left will take up the whole space.
|
||||||
|
// If the Kind is equal to PanelKindSplitVert, then both Left and Right will
|
||||||
|
// have value, and they will both have to be of type Panel. The split will
|
||||||
|
// be represented vertically, and the SplitAt spans 0 to height; top to bottom,
|
||||||
|
// respectively.
|
||||||
|
// If the Kind is equal to PanelKindSplitHor, then both Left and Right will
|
||||||
|
// have value, and they will both have to be of type Panel. The split will
|
||||||
|
// be represented horizontally, and the SplitAt spans 0 to width; left to right.
|
||||||
|
type Panel struct {
|
||||||
|
Parent *Panel
|
||||||
|
Left Component
|
||||||
|
Right Component
|
||||||
|
SplitAt int
|
||||||
|
Kind PanelKind
|
||||||
|
Focused bool
|
||||||
|
|
||||||
|
x, y int
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSplits uses the position and size of the Panel, along with its Weight
|
||||||
|
// and Kind, to appropriately size and place its children. It calls UpdateSplits()
|
||||||
|
// on its child Panels.
|
||||||
|
func (p *Panel) UpdateSplits() {
|
||||||
|
switch p.Kind {
|
||||||
|
case PanelKindSingle:
|
||||||
|
p.Left.SetPos(p.x, p.y)
|
||||||
|
p.Left.SetSize(p.width, p.height)
|
||||||
|
case PanelKindSplitVert:
|
||||||
|
p.Left.SetPos(p.x, p.y)
|
||||||
|
p.Left.SetSize(p.width, p.SplitAt)
|
||||||
|
p.Right.SetPos(p.x, p.y+p.SplitAt)
|
||||||
|
p.Right.SetSize(p.width, p.height-p.SplitAt)
|
||||||
|
|
||||||
|
p.Left.(*Panel).UpdateSplits()
|
||||||
|
p.Right.(*Panel).UpdateSplits()
|
||||||
|
case PanelKindSplitHor:
|
||||||
|
p.Left.SetPos(p.x, p.y)
|
||||||
|
p.Left.SetSize(p.SplitAt, p.height)
|
||||||
|
p.Right.SetPos(p.x+p.SplitAt, p.y)
|
||||||
|
p.Right.SetSize(p.width-p.SplitAt, p.height)
|
||||||
|
|
||||||
|
p.Left.(*Panel).UpdateSplits()
|
||||||
|
p.Right.(*Panel).UpdateSplits()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as EachLeaf, but returns true if any call to `f` returned true.
|
||||||
|
func (p *Panel) eachLeaf(rightMost bool, f func(*Panel) bool) bool {
|
||||||
|
switch p.Kind {
|
||||||
|
case PanelKindEmpty:
|
||||||
|
fallthrough
|
||||||
|
case PanelKindSingle:
|
||||||
|
return f(p)
|
||||||
|
|
||||||
|
case PanelKindSplitVert:
|
||||||
|
fallthrough
|
||||||
|
case PanelKindSplitHor:
|
||||||
|
if rightMost {
|
||||||
|
if p.Right.(*Panel).eachLeaf(rightMost, f) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return p.Left.(*Panel).eachLeaf(rightMost, f)
|
||||||
|
} else {
|
||||||
|
if p.Left.(*Panel).eachLeaf(rightMost, f) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return p.Right.(*Panel).eachLeaf(rightMost, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EachLeaf visits the entire tree, and calls function `f` at each leaf Panel.
|
||||||
|
// If the function `f` returns true, then visiting stops. if `rtl` is true,
|
||||||
|
// the tree is traversed in right-most order. The default is to traverse
|
||||||
|
// in left-most order.
|
||||||
|
//
|
||||||
|
// The caller of this function can safely assert that Panel's Kind is always
|
||||||
|
// either `PanelKindSingle` or `PanelKindEmpty`.
|
||||||
|
func (p *Panel) EachLeaf(rightMost bool, f func(*Panel) bool) {
|
||||||
|
p.eachLeaf(rightMost, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLeaf returns whether the Panel is a leaf or not. A leaf is a panel with
|
||||||
|
// Kind `PanelKindEmpty` or `PanelKindSingle`.
|
||||||
|
func (p *Panel) IsLeaf() bool {
|
||||||
|
switch p.Kind {
|
||||||
|
case PanelKindEmpty:
|
||||||
|
fallthrough
|
||||||
|
case PanelKindSingle:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Panel) Draw(s tcell.Screen) {
|
||||||
|
switch p.Kind {
|
||||||
|
case PanelKindSplitVert:
|
||||||
|
fallthrough
|
||||||
|
case PanelKindSplitHor:
|
||||||
|
p.Right.Draw(s)
|
||||||
|
fallthrough
|
||||||
|
case PanelKindSingle:
|
||||||
|
p.Left.Draw(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFocused sets this Panel's Focused field to `v`. Then, if the Panel's Kind
|
||||||
|
// is PanelKindSingle, it sets its child (not a Panel) focused to `v`, also.
|
||||||
|
func (p *Panel) SetFocused(v bool) {
|
||||||
|
p.Focused = v
|
||||||
|
switch p.Kind {
|
||||||
|
case PanelKindSplitVert:
|
||||||
|
fallthrough
|
||||||
|
case PanelKindSplitHor:
|
||||||
|
p.Right.SetFocused(v)
|
||||||
|
fallthrough
|
||||||
|
case PanelKindSingle:
|
||||||
|
p.Left.SetFocused(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Panel) SetTheme(theme *Theme) {
|
||||||
|
switch p.Kind {
|
||||||
|
case PanelKindSplitVert:
|
||||||
|
fallthrough
|
||||||
|
case PanelKindSplitHor:
|
||||||
|
p.Right.SetTheme(theme)
|
||||||
|
fallthrough
|
||||||
|
case PanelKindSingle:
|
||||||
|
p.Left.SetTheme(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPos returns the position of the panel.
|
||||||
|
func (p *Panel) GetPos() (int, int) {
|
||||||
|
return p.width, p.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPos sets the position of the panel.
|
||||||
|
func (p *Panel) SetPos(x, y int) {
|
||||||
|
p.x, p.y = x, y
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMinSize returns the combined minimum sizes of the Panel's children.
|
||||||
|
func (p *Panel) GetMinSize() (int, int) {
|
||||||
|
switch p.Kind {
|
||||||
|
case PanelKindSingle:
|
||||||
|
return p.Left.GetMinSize()
|
||||||
|
case PanelKindSplitVert:
|
||||||
|
// use max width, add heights
|
||||||
|
lWidth, lHeight := p.Left.GetMinSize()
|
||||||
|
rWidth, rHeight := p.Right.GetMinSize()
|
||||||
|
return Max(lWidth, rWidth), lHeight + rHeight
|
||||||
|
case PanelKindSplitHor:
|
||||||
|
// use max height, add widths
|
||||||
|
lWidth, lHeight := p.Left.GetMinSize()
|
||||||
|
rWidth, rHeight := p.Right.GetMinSize()
|
||||||
|
return lWidth + rWidth, Max(lHeight, rHeight)
|
||||||
|
default:
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Panel) GetSize() (int, int) {
|
||||||
|
return p.width, p.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSize sets the Panel size to the given width, and height. It will not check
|
||||||
|
// against GetMinSize() because it may be costly to do so. SetSize clamps the
|
||||||
|
// Panel's SplitAt to be within the new size of the Panel.
|
||||||
|
func (p *Panel) SetSize(width, height int) {
|
||||||
|
p.width, p.height = width, height
|
||||||
|
switch p.Kind {
|
||||||
|
case PanelKindSplitVert:
|
||||||
|
p.SplitAt = Min(p.SplitAt, height)
|
||||||
|
case PanelKindSplitHor:
|
||||||
|
p.SplitAt = Min(p.SplitAt, width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleEvent propogates the event to all children, calling HandleEvent()
|
||||||
|
// on left-most items. As usual: returns true if handled, false if unhandled.
|
||||||
|
// This function relies on the behavior of the child Components to only handle
|
||||||
|
// events if they are focused.
|
||||||
|
func (p *Panel) HandleEvent(event tcell.Event) bool {
|
||||||
|
switch p.Kind {
|
||||||
|
case PanelKindSingle:
|
||||||
|
return p.Left.HandleEvent(event)
|
||||||
|
case PanelKindSplitVert:
|
||||||
|
fallthrough
|
||||||
|
case PanelKindSplitHor:
|
||||||
|
if p.Left.HandleEvent(event) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return p.Right.HandleEvent(event)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
337
ui/panelcontainer.go
Executable file
337
ui/panelcontainer.go
Executable file
@ -0,0 +1,337 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import "github.com/gdamore/tcell/v2"
|
||||||
|
|
||||||
|
type SplitKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
SplitVertical SplitKind = SplitKind(PanelKindSplitVert) + iota
|
||||||
|
SplitHorizontal
|
||||||
|
)
|
||||||
|
|
||||||
|
type PanelContainer struct {
|
||||||
|
root *Panel
|
||||||
|
floating []*Panel
|
||||||
|
selected **Panel // Only Panels with PanelKindSingle
|
||||||
|
lastNonFloatingSelected **Panel // Used only when focused on floating Panels
|
||||||
|
floatingMode bool // True if 'selected' is part of a floating Panel
|
||||||
|
focused bool
|
||||||
|
theme *Theme
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPanelContainer(theme *Theme) *PanelContainer {
|
||||||
|
root := &Panel{Kind: PanelKindEmpty}
|
||||||
|
return &PanelContainer{
|
||||||
|
root: root,
|
||||||
|
floating: make([]*Panel, 0, 3),
|
||||||
|
selected: &root,
|
||||||
|
theme: theme,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSelected makes the selected Panel empty, but does not delete it from
|
||||||
|
// the tree.
|
||||||
|
func (c *PanelContainer) ClearSelected() Component {
|
||||||
|
item := (**c.selected).Left
|
||||||
|
(**c.selected).Left = nil
|
||||||
|
(**c.selected).Kind = PanelKindEmpty
|
||||||
|
if p := (**c.selected).Parent; p != nil {
|
||||||
|
p.UpdateSplits()
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
// changeSelected sets c.selected to `new`. It also refocuses the Panel.
|
||||||
|
// Prefer to use this as opposed to performing the instructions manually.
|
||||||
|
func (c *PanelContainer) changeSelected(new **Panel) {
|
||||||
|
if c.focused {
|
||||||
|
(*c.selected).SetFocused(false)
|
||||||
|
}
|
||||||
|
c.selected = new
|
||||||
|
if c.focused {
|
||||||
|
(*c.selected).SetFocused(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSelected deletes the selected Panel and returns its child Component.
|
||||||
|
// If the selected Panel is the root Panel, ClearSelected() is called, instead.
|
||||||
|
func (c *PanelContainer) DeleteSelected() Component {
|
||||||
|
if !(*c.selected).IsLeaf() {
|
||||||
|
panic("selected is not leaf")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If selected is the root, just make it empty
|
||||||
|
if *c.selected == c.root {
|
||||||
|
return c.ClearSelected()
|
||||||
|
} else {
|
||||||
|
item := (**c.selected).Left
|
||||||
|
p := (**c.selected).Parent
|
||||||
|
|
||||||
|
if p != nil {
|
||||||
|
if *c.selected == (*p).Left { // If we're deleting the parent's Left
|
||||||
|
(*p).Left = (*p).Right
|
||||||
|
(*p).Right = nil
|
||||||
|
} else { // Deleting parent's Right
|
||||||
|
(*p).Right = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*p).Left != nil {
|
||||||
|
// Parent becomes the Left panel
|
||||||
|
panel := (*p).Left.(*Panel)
|
||||||
|
(*p).Left = (*panel).Left
|
||||||
|
(*p).Right = (*panel).Right
|
||||||
|
(*p).Kind = (*panel).Kind
|
||||||
|
(*p).SplitAt = (*panel).SplitAt
|
||||||
|
} else {
|
||||||
|
(*p).Kind = PanelKindEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide what Panel to select next
|
||||||
|
if !(*p).IsLeaf() { // If the new panel was a split panel...
|
||||||
|
// Select the leftmost child of it
|
||||||
|
(*p).EachLeaf(false, func(l *Panel) bool { c.changeSelected(&l); return true })
|
||||||
|
} else {
|
||||||
|
c.changeSelected(&p)
|
||||||
|
}
|
||||||
|
|
||||||
|
(*p).UpdateSplits()
|
||||||
|
} else if c.floatingMode { // Deleting a floating Panel without a parent
|
||||||
|
c.floating[0] = nil
|
||||||
|
copy(c.floating, c.floating[1:]) // Shift items to front
|
||||||
|
c.floating = c.floating[:len(c.floating)-1] // Shrink slice's len by one
|
||||||
|
|
||||||
|
if len(c.floating) <= 0 {
|
||||||
|
c.SetFloatingFocused(false)
|
||||||
|
} else {
|
||||||
|
c.changeSelected(&c.floating[0])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic("Panel does not have parent and is not floating")
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwapNeighborsSelected swaps two Left and Right child Panels of a vertical or
|
||||||
|
// horizontally split Panel. This is necessary to achieve a "split top" or
|
||||||
|
// "split left" effect, as Panels only split open to the bottom or right.
|
||||||
|
func (c *PanelContainer) SwapNeighborsSelected() {
|
||||||
|
parent := (**c.selected).Parent
|
||||||
|
if parent != nil {
|
||||||
|
left := (*parent).Left
|
||||||
|
(*parent).Left = parent.Right
|
||||||
|
(*parent).Right = left
|
||||||
|
parent.UpdateSplits() // Updates position and size of reordered children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turns the selected Panel into a split panel, moving its contents to its Left field,
|
||||||
|
// and putting the given Panel at the Right field. `panel` cannot be nil.
|
||||||
|
func (c *PanelContainer) splitSelectedWithPanel(kind SplitKind, panel *Panel) {
|
||||||
|
(**c.selected).Left = &Panel{Parent: *c.selected, Left: (**c.selected).Left, Kind: (**c.selected).Kind}
|
||||||
|
(**c.selected).Right = panel
|
||||||
|
(**c.selected).Right.(*Panel).Parent = *c.selected
|
||||||
|
|
||||||
|
// Update parent's split information
|
||||||
|
(**c.selected).Kind = PanelKind(kind)
|
||||||
|
if kind == SplitVertical {
|
||||||
|
(**c.selected).SplitAt = (**c.selected).height / 2
|
||||||
|
} else {
|
||||||
|
(**c.selected).SplitAt = (**c.selected).width / 2
|
||||||
|
}
|
||||||
|
(*c.selected).UpdateSplits()
|
||||||
|
|
||||||
|
// Change selected from parent to the previously selected Panel on the Left
|
||||||
|
panel = (**c.selected).Left.(*Panel)
|
||||||
|
c.changeSelected(&panel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitSelected splits the selected Panel with the given Component `item`.
|
||||||
|
// The type of split (vertical or horizontal) is determined with the `kind`.
|
||||||
|
// If `item` is nil, the new Panel will be of kind empty.
|
||||||
|
func (c *PanelContainer) SplitSelected(kind SplitKind, item Component) {
|
||||||
|
if !(*c.selected).IsLeaf() {
|
||||||
|
panic("selected is not leaf")
|
||||||
|
}
|
||||||
|
|
||||||
|
if item == nil {
|
||||||
|
c.splitSelectedWithPanel(kind, &Panel{Parent: *c.selected, Kind: PanelKindEmpty})
|
||||||
|
} else {
|
||||||
|
c.splitSelectedWithPanel(kind, &Panel{Parent: *c.selected, Left: item, Kind: PanelKindSingle})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) IsRootSelected() bool {
|
||||||
|
return *c.selected == c.root
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) GetSelected() Component {
|
||||||
|
if !(*c.selected).IsLeaf() {
|
||||||
|
panic("selected is not leaf")
|
||||||
|
}
|
||||||
|
return (**c.selected).Left
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) SetSelected(item Component) {
|
||||||
|
if !(*c.selected).IsLeaf() {
|
||||||
|
panic("selected is not leaf")
|
||||||
|
}
|
||||||
|
|
||||||
|
(**c.selected).Left = item
|
||||||
|
(**c.selected).Kind = PanelKindSingle
|
||||||
|
(*c.selected).UpdateSplits()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) raiseFloating(idx int) {
|
||||||
|
item := c.floating[idx]
|
||||||
|
copy(c.floating[1:], c.floating[:idx]) // Shift all items before idx right
|
||||||
|
c.floating[0] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFloatingFocused returns true if a floating window is selected or focused.
|
||||||
|
func (c *PanelContainer) GetFloatingFocused() bool {
|
||||||
|
return c.floatingMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFloatingFocused sets whether the floating Panels are focused. When true,
|
||||||
|
// the current Panel will be unselected and the front floating Panel will become
|
||||||
|
// the new selected if there any floating windows. If false, the same, but the
|
||||||
|
// last selected non-floating Panel will become focused.
|
||||||
|
//
|
||||||
|
// The returned boolean is whether floating windows were able to be focused. If
|
||||||
|
// there are no floating windows when trying to focus them, this will inevitably
|
||||||
|
// return false, for example.
|
||||||
|
func (c *PanelContainer) SetFloatingFocused(v bool) bool {
|
||||||
|
if v {
|
||||||
|
if len(c.floating) > 0 {
|
||||||
|
c.lastNonFloatingSelected = c.selected
|
||||||
|
c.changeSelected(&c.floating[0])
|
||||||
|
c.floatingMode = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.changeSelected(c.lastNonFloatingSelected)
|
||||||
|
c.floatingMode = false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// FloatSelected makes the selected Panel floating. This function does not focus
|
||||||
|
// the newly floated Panel. To focus the floating panel, call SetFloatingFocused().
|
||||||
|
func (c *PanelContainer) FloatSelected() {
|
||||||
|
if !(*c.selected).IsLeaf() {
|
||||||
|
panic("selected is not leaf")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.floatingMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.DeleteSelected()
|
||||||
|
(**c.selected).Parent = nil
|
||||||
|
(*c.selected).UpdateSplits()
|
||||||
|
|
||||||
|
c.floating = append(c.floating, *c.selected)
|
||||||
|
c.raiseFloating(len(c.floating) - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnfloatSelected moves any selected floating Panel to the normal tree that is
|
||||||
|
// accessible in the standard focus mode. This function will cause focus to go to
|
||||||
|
// the normal tree if there are no remaining floating windows after the operation.
|
||||||
|
//
|
||||||
|
// Like SetFloatingFocused(), the boolean returned is whether the PanelContainer
|
||||||
|
// is focusing floating windows after the operation.
|
||||||
|
func (c *PanelContainer) UnfloatSelected(kind SplitKind) bool {
|
||||||
|
if !(*c.selected).IsLeaf() {
|
||||||
|
panic("selected is not leaf")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.floatingMode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
c.DeleteSelected()
|
||||||
|
c.SetFloatingFocused(false)
|
||||||
|
c.splitSelectedWithPanel(kind, *c.selected)
|
||||||
|
|
||||||
|
// Try to return to floating focus
|
||||||
|
return c.SetFloatingFocused(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) selectNext(rightMost bool) {
|
||||||
|
var nextIsIt bool
|
||||||
|
c.root.EachLeaf(rightMost, func(p *Panel) bool {
|
||||||
|
if nextIsIt {
|
||||||
|
c.changeSelected(&p)
|
||||||
|
nextIsIt = false
|
||||||
|
return true
|
||||||
|
} else if p == *c.selected {
|
||||||
|
nextIsIt = true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// This boolean must be false if we found the next leaf.
|
||||||
|
// Therefore, if it is true, c.selected was the last leaf
|
||||||
|
// of the tree. We need to wrap around to the first leaf.
|
||||||
|
if nextIsIt {
|
||||||
|
// This gets the first leaf in left-most or right-most order
|
||||||
|
c.root.EachLeaf(rightMost, func(p *Panel) bool { c.changeSelected(&p); return true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) SelectNext() {
|
||||||
|
c.selectNext(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) SelectPrev() {
|
||||||
|
c.selectNext(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) Draw(s tcell.Screen) {
|
||||||
|
c.root.Draw(s)
|
||||||
|
for i := len(c.floating) - 1; i >= 0; i-- {
|
||||||
|
c.floating[i].Draw(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) SetFocused(v bool) {
|
||||||
|
c.focused = v
|
||||||
|
(*c.selected).SetFocused(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) SetTheme(theme *Theme) {
|
||||||
|
c.theme = theme
|
||||||
|
c.root.SetTheme(theme)
|
||||||
|
for i := range c.floating {
|
||||||
|
c.floating[i].SetTheme(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) GetPos() (int, int) {
|
||||||
|
return c.root.GetPos()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) SetPos(x, y int) {
|
||||||
|
c.root.SetPos(x, y)
|
||||||
|
c.root.UpdateSplits()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) GetMinSize() (int, int) {
|
||||||
|
return c.root.GetMinSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) GetSize() (int, int) {
|
||||||
|
return c.root.GetSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) SetSize(width, height int) {
|
||||||
|
c.root.SetSize(width, height)
|
||||||
|
c.root.UpdateSplits()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PanelContainer) HandleEvent(event tcell.Event) bool {
|
||||||
|
// Call handle event on selected Panel
|
||||||
|
return (*c.selected).HandleEvent(event)
|
||||||
|
}
|
@ -86,23 +86,29 @@ func (c *TabContainer) GetTab(idx int) *Tab {
|
|||||||
|
|
||||||
// Draw will draws the border of the BoxContainer, then it draws its child component.
|
// Draw will draws the border of the BoxContainer, then it draws its child component.
|
||||||
func (c *TabContainer) Draw(s tcell.Screen) {
|
func (c *TabContainer) Draw(s tcell.Screen) {
|
||||||
|
var styFocused tcell.Style
|
||||||
|
if c.focused {
|
||||||
|
styFocused = c.theme.GetOrDefault("TabContainerFocused")
|
||||||
|
} else {
|
||||||
|
styFocused = c.theme.GetOrDefault("TabContainer")
|
||||||
|
}
|
||||||
|
|
||||||
// Draw outline
|
// Draw outline
|
||||||
DrawRectOutlineDefault(s, c.x, c.y, c.width, c.height, c.theme.GetOrDefault("TabContainer"))
|
DrawRectOutlineDefault(s, c.x, c.y, c.width, c.height, styFocused)
|
||||||
|
|
||||||
combinedTabLength := 0
|
combinedTabLength := 0
|
||||||
for _, tab := range c.children {
|
for i := range c.children {
|
||||||
combinedTabLength += len(tab.Name) + 2 // 2 for padding
|
combinedTabLength += len(c.children[i].Name) + 2 // 2 for padding
|
||||||
}
|
}
|
||||||
combinedTabLength += len(c.children) - 1 // add for spacing between tabs
|
combinedTabLength += len(c.children) - 1 // add for spacing between tabs
|
||||||
|
|
||||||
// Draw tabs
|
// Draw tabs
|
||||||
col := c.x + c.width/2 - combinedTabLength/2 - 1 // Starting column
|
col := c.x + c.width/2 - combinedTabLength/2 // Starting column
|
||||||
for i, tab := range c.children {
|
for i, tab := range c.children {
|
||||||
var sty tcell.Style
|
sty := styFocused
|
||||||
if c.selected == i {
|
if c.selected == i {
|
||||||
sty = c.theme.GetOrDefault("TabSelected")
|
fg, bg, attr := styFocused.Decompose()
|
||||||
} else {
|
sty = tcell.Style{}.Foreground(bg).Background(fg).Attributes(attr)
|
||||||
sty = c.theme.GetOrDefault("Tab")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var dirty bool
|
var dirty bool
|
||||||
@ -118,7 +124,7 @@ func (c *TabContainer) Draw(s tcell.Screen) {
|
|||||||
|
|
||||||
str := fmt.Sprintf(" %s ", name)
|
str := fmt.Sprintf(" %s ", name)
|
||||||
|
|
||||||
DrawStr(s, c.x+col, c.y, str, sty)
|
DrawStr(s, col, c.y, str, sty)
|
||||||
col += len(str) + 1 // Add one for spacing between tabs
|
col += len(str) + 1 // Add one for spacing between tabs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ type TextEdit struct {
|
|||||||
curx, cury int // Zero-based: cursor points before the character at that position.
|
curx, cury int // Zero-based: cursor points before the character at that position.
|
||||||
prevCurCol int // Previous maximum column the cursor was at, when the user pressed left or right
|
prevCurCol int // Previous maximum column the cursor was at, when the user pressed left or right
|
||||||
scrollx, scrolly int // X and Y offset of view, known as scroll
|
scrollx, scrolly int // X and Y offset of view, known as scroll
|
||||||
|
theme *Theme
|
||||||
|
|
||||||
selection Region // Selection: selectMode determines if it should be used
|
selection Region // Selection: selectMode determines if it should be used
|
||||||
selectMode bool // Whether the user is actively selecting text
|
selectMode bool // Whether the user is actively selecting text
|
||||||
@ -115,6 +116,7 @@ loop:
|
|||||||
|
|
||||||
colorscheme := &buffer.Colorscheme{
|
colorscheme := &buffer.Colorscheme{
|
||||||
buffer.Default: tcell.Style{}.Foreground(tcell.ColorLightGray).Background(tcell.ColorBlack),
|
buffer.Default: tcell.Style{}.Foreground(tcell.ColorLightGray).Background(tcell.ColorBlack),
|
||||||
|
buffer.Column: tcell.Style{}.Foreground(tcell.ColorDarkGray).Background(tcell.ColorBlack),
|
||||||
buffer.Comment: tcell.Style{}.Foreground(tcell.ColorGray).Background(tcell.ColorBlack),
|
buffer.Comment: tcell.Style{}.Foreground(tcell.ColorGray).Background(tcell.ColorBlack),
|
||||||
buffer.String: tcell.Style{}.Foreground(tcell.ColorOlive).Background(tcell.ColorBlack),
|
buffer.String: tcell.Style{}.Foreground(tcell.ColorOlive).Background(tcell.ColorBlack),
|
||||||
buffer.Keyword: tcell.Style{}.Foreground(tcell.ColorNavy).Background(tcell.ColorBlack),
|
buffer.Keyword: tcell.Style{}.Foreground(tcell.ColorNavy).Background(tcell.ColorBlack),
|
||||||
@ -265,6 +267,20 @@ func (t *TextEdit) GetLineCol() (int, int) {
|
|||||||
return t.cury, t.curx
|
return t.cury, t.curx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The same as updateTerminalCursor but the caller can provide the tabOffset to
|
||||||
|
// save the original function a calculation.
|
||||||
|
func (t *TextEdit) updateTerminalCursorNoHelper(columnWidth, tabOffset int) {
|
||||||
|
(*t.screen).ShowCursor(t.x+columnWidth+t.curx+tabOffset-t.scrollx, t.y+t.cury-t.scrolly)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateTerminalCursor sets the position of the cursor with the cursor position
|
||||||
|
// properties of the TextEdit. Always sends a signal to *show* the cursor.
|
||||||
|
func (t *TextEdit) updateTerminalCursor() {
|
||||||
|
columnWidth := t.getColumnWidth()
|
||||||
|
tabOffset := t.getTabCountInLineAtCol(t.cury, t.curx) * (t.TabSize - 1)
|
||||||
|
t.updateTerminalCursorNoHelper(columnWidth, tabOffset)
|
||||||
|
}
|
||||||
|
|
||||||
// SetLineCol sets the cursor line and column position. Zero is origin for both.
|
// SetLineCol sets the cursor line and column position. Zero is origin for both.
|
||||||
// If `line` is out of bounds, `line` will be clamped to the closest available line.
|
// If `line` is out of bounds, `line` will be clamped to the closest available line.
|
||||||
// If `col` is out of bounds, `col` will be clamped to the closest column available for the line.
|
// If `col` is out of bounds, `col` will be clamped to the closest column available for the line.
|
||||||
@ -297,9 +313,8 @@ func (t *TextEdit) SetLineCol(line, col int) {
|
|||||||
|
|
||||||
t.cury, t.curx = line, col
|
t.cury, t.curx = line, col
|
||||||
if t.focused && !t.selectMode {
|
if t.focused && !t.selectMode {
|
||||||
(*t.screen).ShowCursor(t.x+columnWidth+col+tabOffset-t.scrollx, t.y+line-t.scrolly)
|
// Update terminal cursor position
|
||||||
} else {
|
t.updateTerminalCursorNoHelper(columnWidth, tabOffset)
|
||||||
(*t.screen).HideCursor()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,10 +372,10 @@ func (t *TextEdit) CursorRight() {
|
|||||||
|
|
||||||
// getColumnWidth returns the width of the line numbers column if it is present.
|
// getColumnWidth returns the width of the line numbers column if it is present.
|
||||||
func (t *TextEdit) getColumnWidth() int {
|
func (t *TextEdit) getColumnWidth() int {
|
||||||
columnWidth := 0
|
var columnWidth int
|
||||||
if t.LineNumbers {
|
if t.LineNumbers {
|
||||||
// Set columnWidth to max count of line number digits
|
// Set columnWidth to max count of line number digits
|
||||||
columnWidth = Max(2, len(strconv.Itoa(t.Buffer.Lines()))) // Column has minimum width of 2
|
columnWidth = Max(3, 1+len(strconv.Itoa(t.Buffer.Lines()))) // Column has minimum width of 2
|
||||||
}
|
}
|
||||||
return columnWidth
|
return columnWidth
|
||||||
}
|
}
|
||||||
@ -382,7 +397,7 @@ func (t *TextEdit) Draw(s tcell.Screen) {
|
|||||||
bufferLines := t.Buffer.Lines()
|
bufferLines := t.Buffer.Lines()
|
||||||
|
|
||||||
selectedStyle := t.theme.GetOrDefault("TextEditSelected")
|
selectedStyle := t.theme.GetOrDefault("TextEditSelected")
|
||||||
columnStyle := t.theme.GetOrDefault("TextEditColumn")
|
columnStyle := t.Highlighter.Colorscheme.GetStyle(buffer.Column)
|
||||||
|
|
||||||
t.Highlighter.UpdateInvalidatedLines(t.scrolly, t.scrolly+(t.height-1))
|
t.Highlighter.UpdateInvalidatedLines(t.scrolly, t.scrolly+(t.height-1))
|
||||||
|
|
||||||
@ -406,22 +421,7 @@ func (t *TextEdit) Draw(s tcell.Screen) {
|
|||||||
var origLineBytes []byte = t.Buffer.Line(line)
|
var origLineBytes []byte = t.Buffer.Line(line)
|
||||||
var lineBytes []byte = origLineBytes // Line to be drawn
|
var lineBytes []byte = origLineBytes // Line to be drawn
|
||||||
|
|
||||||
// When iterating lineTabs: the value at i is
|
|
||||||
// the rune index the tab was found at.
|
|
||||||
// var lineTabs [128]int // Rune index for each hard tab '\t' in lineBytes
|
|
||||||
// var tabs int // Length of lineTabs (number of hard tabs)
|
|
||||||
if t.UseHardTabs {
|
if t.UseHardTabs {
|
||||||
// var ri int // rune index
|
|
||||||
// var i int
|
|
||||||
// for i < len(lineBytes) {
|
|
||||||
// r, size := utf8.DecodeRune(lineBytes[i:])
|
|
||||||
// if r == '\t' {
|
|
||||||
// lineTabs[tabs] = ri
|
|
||||||
// tabs++
|
|
||||||
// }
|
|
||||||
// i += size
|
|
||||||
// ri++
|
|
||||||
// }
|
|
||||||
lineBytes = bytes.ReplaceAll(lineBytes, []byte{'\t'}, tabBytes)
|
lineBytes = bytes.ReplaceAll(lineBytes, []byte{'\t'}, tabBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -433,7 +433,6 @@ func (t *TextEdit) Draw(s tcell.Screen) {
|
|||||||
col := t.x + columnWidth
|
col := t.x + columnWidth
|
||||||
var runeIdx int // Index into lineStr (as runes) we draw the next character at
|
var runeIdx int // Index into lineStr (as runes) we draw the next character at
|
||||||
|
|
||||||
// REWRITE OF SCROLL FUNC:
|
|
||||||
for runeIdx < t.scrollx && byteIdx < len(lineBytes) {
|
for runeIdx < t.scrollx && byteIdx < len(lineBytes) {
|
||||||
_, size := utf8.DecodeRune(lineBytes[byteIdx:]) // Respect UTF-8
|
_, size := utf8.DecodeRune(lineBytes[byteIdx:]) // Respect UTF-8
|
||||||
byteIdx += size
|
byteIdx += size
|
||||||
@ -543,7 +542,7 @@ func (t *TextEdit) Draw(s tcell.Screen) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
columnStr := fmt.Sprintf("%s%s", strings.Repeat(" ", columnWidth-len(lineNumStr)), lineNumStr) // Right align line number
|
columnStr := fmt.Sprintf("%s%s│", strings.Repeat(" ", columnWidth-len(lineNumStr)-1), lineNumStr) // Right align line number
|
||||||
|
|
||||||
DrawStr(s, t.x, lineY, columnStr, columnStyle) // Draw column
|
DrawStr(s, t.x, lineY, columnStr, columnStyle) // Draw column
|
||||||
}
|
}
|
||||||
@ -557,7 +556,7 @@ func (t *TextEdit) Draw(s tcell.Screen) {
|
|||||||
func (t *TextEdit) SetFocused(v bool) {
|
func (t *TextEdit) SetFocused(v bool) {
|
||||||
t.focused = v
|
t.focused = v
|
||||||
if v {
|
if v {
|
||||||
t.SetLineCol(t.cury, t.curx)
|
t.updateTerminalCursor()
|
||||||
} else {
|
} else {
|
||||||
(*t.screen).HideCursor()
|
(*t.screen).HideCursor()
|
||||||
}
|
}
|
||||||
|
28
ui/theme.go
28
ui/theme.go
@ -29,19 +29,17 @@ func (theme *Theme) GetOrDefault(key string) tcell.Style {
|
|||||||
|
|
||||||
// DefaultTheme uses only the first 16 colors present in most colored terminals.
|
// DefaultTheme uses only the first 16 colors present in most colored terminals.
|
||||||
var DefaultTheme = Theme{
|
var DefaultTheme = Theme{
|
||||||
"Normal": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
"Normal": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||||
"Button": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite),
|
"Button": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
||||||
"InputField": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
"InputField": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||||
"MenuBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
"MenuBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorDarkGray),
|
||||||
"MenuBarSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
"MenuBarFocused": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorLightGray),
|
||||||
"Menu": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
"Menu": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
||||||
"MenuSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
"MenuSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||||
"Tab": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
"TabContainer": tcell.Style{}.Foreground(tcell.ColorGray).Background(tcell.ColorBlack),
|
||||||
"TabContainer": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
"TabContainerFocused": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||||
"TabSelected": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
"TextEdit": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
||||||
"TextEdit": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack),
|
"TextEditSelected": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
||||||
"TextEditColumn": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorGray),
|
"Window": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorDarkGray),
|
||||||
"TextEditSelected": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
"WindowHeader": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
||||||
"Window": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver),
|
|
||||||
"WindowHeader": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite),
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user