From 03c8c3bcadcdfbfa31e905a12173087dfe523775 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Mon, 5 Apr 2021 16:22:40 -0500 Subject: [PATCH 01/17] Created Panel and PanelContainer untested code --- ui/component.go | 6 +- ui/panel.go | 212 +++++++++++++++++++++++++++++++++++++++++++ ui/panelcontainer.go | 145 +++++++++++++++++++++++++++++ 3 files changed, 361 insertions(+), 2 deletions(-) create mode 100755 ui/panel.go create mode 100755 ui/panelcontainer.go diff --git a/ui/component.go b/ui/component.go index d291b6f..107b270 100644 --- a/ui/component.go +++ b/ui/component.go @@ -15,8 +15,8 @@ type Component interface { // A component knows its position and size, which is used to draw itself in // its bounding rectangle. Draw(tcell.Screen) - // Components can be focused, which may affect how it handles events. For - // example, when a button is focused, the Return key may be pressed to + // Components can be focused, which may affect how it handles events or draws. + // For example, when a button is focused, the Return key may be pressed to // activate the button. SetFocused(bool) // Applies the theme to the component and all of its children. @@ -35,5 +35,7 @@ type Component interface { // used, instead. SetSize(int, int) + // It is good practice for a Component to check if it is focused before handling + // events. tcell.EventHandler // A Component can handle events } diff --git a/ui/panel.go b/ui/panel.go new file mode 100755 index 0000000..00a139e --- /dev/null +++ b/ui/panel.go @@ -0,0 +1,212 @@ +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(f func(*Component) bool) bool { + switch p.Kind { + case PanelKindSingle: + return f(&p.Left) + case PanelKindSplitVert: + fallthrough + case PanelKindSplitHor: + if p.Left.(*Panel).eachLeaf(f) { + return true + } + return p.Right.(*Panel).eachLeaf(f) + default: + return false + } +} + +// EachLeaf visits the entire tree in left-most order, and calls function `f` +// at each individual Component (never for Panels). If the function `f` returns +// true, then visiting stops. +func (p *Panel) EachLeaf(f func(*Component) bool) { + p.eachLeaf(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 + } +} diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go new file mode 100755 index 0000000..86c5cfd --- /dev/null +++ b/ui/panelcontainer.go @@ -0,0 +1,145 @@ +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 + 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 + (*c.selected).UpdateSplits() + return item +} + +// 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 root, just make it empty + if *c.selected == c.root { + return c.ClearSelected() + } else { + item := (**c.selected).Left + p := (**c.selected).Parent + 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 + } + (*p).Kind = PanelKindSingle + (*c.selected) = nil // Tell garbage collector to come pick up selected (being safe) + c.selected = &p + return item + } +} + +// 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") + } + + // It should be asserted that whatever is selected is either PanelKindEmpty or PanelKindSingle + if item == nil { + (**c.selected).Right = &Panel{Parent: *c.selected, Kind: PanelKindEmpty} + } else { + (**c.selected).Right = &Panel{Parent: *c.selected, Left: item, Kind: PanelKindSingle} + } + (**c.selected).Kind = PanelKind(kind) + (*c.selected).UpdateSplits() + panel := (**c.selected).Left.(*Panel) // TODO: watch me... might be a bug lurking in a hidden copy here + c.selected = &panel +} + +func (c *PanelContainer) SetSelected(item Component) { + if !(*c.selected).IsLeaf() { + panic("selected is not leaf") + } + + (**c.selected).Left = item + (**c.selected).Kind = PanelKindSingle +} + +func (c *PanelContainer) FloatSelected() { + +} + +func (c *PanelContainer) UnfloatSelected() { + +} + +func (c *PanelContainer) Draw(s tcell.Screen) { + c.root.Draw(s) +} + +func (c *PanelContainer) SetFocused(v bool) { + c.focused = v + // TODO: update focused on selected children +} + +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 false +} From 25b416da3d8047404b912488318fba16a9212b9d Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Mon, 5 Apr 2021 16:55:16 -0500 Subject: [PATCH 02/17] Injected new PanelContainer into mainloop to replace TabContainer --- main.go | 81 +++++++++++++++++++++++++------------------- ui/panelcontainer.go | 6 ++++ 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/main.go b/main.go index 1bf5e78..8f8be62 100644 --- a/main.go +++ b/main.go @@ -27,9 +27,10 @@ var theme = ui.Theme{ var ( screen *tcell.Screen - menuBar *ui.MenuBar - tabContainer *ui.TabContainer - dialog ui.Component // nil if not present (has exclusive focus) + menuBar *ui.MenuBar +// tabContainer *ui.TabContainer + panelContainer *ui.PanelContainer + dialog ui.Component // nil if not present (has exclusive focus) focusedComponent ui.Component = nil ) @@ -48,15 +49,20 @@ func showErrorDialog(title string, message string, callback func()) { callback() } else { dialog = nil - changeFocus(tabContainer) // Default behavior: focus tabContainer + changeFocus(panelContainer) // Default behavior: focus panelContainer } }) changeFocus(dialog) } +func getActiveTabContainer() *ui.TabContainer { + return panelContainer.GetSelected().(*ui.TabContainer) +} + // returns nil if no TextEdit is visible func getActiveTextEdit() *ui.TextEdit { - if tabContainer.GetTabCount() > 0 { + tabContainer := getActiveTabContainer() + if tabContainer != nil && tabContainer.GetTabCount() > 0 { tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) te := tab.Child.(*ui.TextEdit) return te @@ -68,6 +74,7 @@ func getActiveTextEdit() *ui.TextEdit { func saveAs() { callback := func(filePaths []string) { te := getActiveTextEdit() // te should have value if we are here + tabContainer := getActiveTabContainer() tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) // If we got the callback, it is safe to assume there are one or more files @@ -86,7 +93,7 @@ func saveAs() { te.Dirty = false dialog = nil // Hide the file selector - changeFocus(tabContainer) + changeFocus(panelContainer) tab.Name = filePaths[0] } @@ -98,7 +105,7 @@ func saveAs() { callback, func() { // Dialog canceled dialog = nil - changeFocus(tabContainer) + changeFocus(panelContainer) }, ) changeFocus(dialog) @@ -133,11 +140,16 @@ func main() { var closing bool sizex, sizey := s.Size() - tabContainer = ui.NewTabContainer(&theme) - tabContainer.SetPos(0, 1) - tabContainer.SetSize(sizex, sizey-2) +// tabContainer = ui.NewTabContainer(&theme) +// tabContainer.SetPos(0, 1) +// tabContainer.SetSize(sizex, sizey-2) + panelContainer = ui.NewPanelContainer(&theme) + panelContainer.SetPos(0, 1) + 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 if flag.NArg() > 0 { @@ -167,9 +179,9 @@ func main() { textEdit := ui.NewTextEdit(screen, arg, bytes, &theme) 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) @@ -183,12 +195,14 @@ func main() { 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 + tabContainer := getActiveTabContainer() tabContainer.AddTab("noname", textEdit) - - changeFocus(tabContainer) tabContainer.FocusTab(tabContainer.GetTabCount() - 1) + changeFocus(panelContainer) }}, &ui.ItemEntry{Name: "Open...", Shortcut: "Ctrl+O", Callback: func() { callback := func(filePaths []string) { + tabContainer := getActiveTabContainer() + var errOccurred bool for _, path := range filePaths { file, err := os.Open(path) @@ -212,7 +226,7 @@ func main() { if !errOccurred { // Prevent hiding the error dialog dialog = nil // Hide the file selector - changeFocus(tabContainer) + changeFocus(panelContainer) if tabContainer.GetTabCount() > 0 { tabContainer.FocusTab(tabContainer.GetTabCount() - 1) } @@ -226,7 +240,7 @@ func main() { callback, func() { // Dialog is canceled dialog = nil - changeFocus(tabContainer) + changeFocus(panelContainer) }, ) changeFocus(dialog) @@ -248,13 +262,14 @@ func main() { } te.Dirty = false - changeFocus(tabContainer) + changeFocus(panelContainer) } else { saveAs() } } }}, &ui.ItemEntry{Name: "Save As...", QuickChar: 5, Callback: saveAs}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Close", Shortcut: "Ctrl+Q", Callback: func() { + tabContainer := getActiveTabContainer() if tabContainer.GetTabCount() > 0 { tabContainer.RemoveTab(tabContainer.GetSelectedTabIdx()) } else { // No tabs open; close the editor @@ -303,7 +318,7 @@ func main() { te.Delete(false) // Delete selection } if err == nil { // Prevent hiding error dialog - changeFocus(tabContainer) + changeFocus(panelContainer) } } }}, &ui.ItemEntry{Name: "Copy", Shortcut: "Ctrl+C", Callback: func() { @@ -318,7 +333,7 @@ func main() { } } if err == nil { - changeFocus(tabContainer) + changeFocus(panelContainer) } } }}, &ui.ItemEntry{Name: "Paste", Shortcut: "Ctrl+V", Callback: func() { @@ -329,7 +344,7 @@ func main() { showErrorDialog("Clipboard Failure", fmt.Sprintf("%v", err), nil) } else { te.Insert(contents) - changeFocus(tabContainer) + changeFocus(panelContainer) } } }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Select All", QuickChar: 7, Shortcut: "Ctrl+A", Callback: func() { @@ -352,12 +367,12 @@ func main() { te.SetLineCol(line-1, 0) // Hide dialog dialog = nil - changeFocus(tabContainer) + changeFocus(panelContainer) } dialog = NewGotoLineDialog(screen, &theme, callback, func() { // Dialog canceled dialog = nil - changeFocus(tabContainer) + changeFocus(panelContainer) }) changeFocus(dialog) } @@ -376,10 +391,11 @@ func main() { //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)) - if tabContainer.GetTabCount() > 0 { // Draw the tab container only if a tab is open - tabContainer.Draw(s) - } - menuBar.Draw(s) // Always draw the menu bar +// if tabContainer.GetTabCount() > 0 { // Draw the tab container only if a tab is open +// tabContainer.Draw(s) +// } + panelContainer.Draw(s) + menuBar.Draw(s) if dialog != nil { // Update fileSelector dialog pos and size @@ -392,10 +408,7 @@ func main() { // Draw statusbar ui.DrawRect(s, 0, sizey-1, sizex, 1, ' ', theme["StatusBar"]) - if tabContainer.GetTabCount() > 0 { - focusedTab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) - te := focusedTab.Child.(*ui.TextEdit) - + if te := getActiveTextEdit(); te != nil { var delim string if te.IsCRLF { delim = "CRLF" @@ -423,17 +436,17 @@ func main() { sizex, sizey = s.Size() menuBar.SetSize(sizex, 1) - tabContainer.SetSize(sizex, sizey-2) + panelContainer.SetSize(sizex, sizey-2) s.Sync() // Redraw everything case *tcell.EventKey: // On Escape, we change focus between editor and the MenuBar. if dialog == nil { if ev.Key() == tcell.KeyEscape { - if focusedComponent == tabContainer { + if focusedComponent == panelContainer { changeFocus(menuBar) } else { - changeFocus(tabContainer) + changeFocus(panelContainer) } } diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go index 86c5cfd..53cfb11 100755 --- a/ui/panelcontainer.go +++ b/ui/panelcontainer.go @@ -59,6 +59,7 @@ func (c *PanelContainer) DeleteSelected() Component { (*p).Kind = PanelKindSingle (*c.selected) = nil // Tell garbage collector to come pick up selected (being safe) c.selected = &p + (*c.selected).UpdateSplits() return item } } @@ -83,6 +84,10 @@ func (c *PanelContainer) SplitSelected(kind SplitKind, item Component) { c.selected = &panel } +func (c *PanelContainer) GetSelected() Component { + return (**c.selected).Left +} + func (c *PanelContainer) SetSelected(item Component) { if !(*c.selected).IsLeaf() { panic("selected is not leaf") @@ -90,6 +95,7 @@ func (c *PanelContainer) SetSelected(item Component) { (**c.selected).Left = item (**c.selected).Kind = PanelKindSingle + (*c.selected).UpdateSplits() } func (c *PanelContainer) FloatSelected() { From eb8142ea8e1feec7d912fb8bde9a7dabd96839d0 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Mon, 5 Apr 2021 17:03:52 -0500 Subject: [PATCH 03/17] PanelContainer: SetFocus and HandleEvent properly --- ui/panelcontainer.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go index 53cfb11..6323bc0 100755 --- a/ui/panelcontainer.go +++ b/ui/panelcontainer.go @@ -57,9 +57,13 @@ func (c *PanelContainer) DeleteSelected() Component { (*p).Right = nil } (*p).Kind = PanelKindSingle + + (*c.selected).SetFocused(false) // Unfocus item (*c.selected) = nil // Tell garbage collector to come pick up selected (being safe) c.selected = &p (*c.selected).UpdateSplits() + (*c.selected).SetFocused(c.focused) + return item } } @@ -80,8 +84,10 @@ func (c *PanelContainer) SplitSelected(kind SplitKind, item Component) { } (**c.selected).Kind = PanelKind(kind) (*c.selected).UpdateSplits() + (*c.selected).SetFocused(false) panel := (**c.selected).Left.(*Panel) // TODO: watch me... might be a bug lurking in a hidden copy here c.selected = &panel + (*c.selected).SetFocused(c.focused) } func (c *PanelContainer) GetSelected() Component { @@ -112,7 +118,7 @@ func (c *PanelContainer) Draw(s tcell.Screen) { func (c *PanelContainer) SetFocused(v bool) { c.focused = v - // TODO: update focused on selected children + (*c.selected).SetFocused(v) } func (c *PanelContainer) SetTheme(theme *Theme) { @@ -147,5 +153,5 @@ func (c *PanelContainer) SetSize(width, height int) { func (c *PanelContainer) HandleEvent(event tcell.Event) bool { // Call handle event on selected Panel - return false + return (*c.selected).HandleEvent(event) } From cf5803c4640cd2164301c0ade1d6f76592f42522 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Mon, 5 Apr 2021 17:15:57 -0500 Subject: [PATCH 04/17] Splitting Panels --- main.go | 4 ++-- ui/panelcontainer.go | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 8f8be62..c0c853e 100644 --- a/main.go +++ b/main.go @@ -290,11 +290,11 @@ func main() { }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Split Top", QuickChar: 6, Callback: func() { }}, &ui.ItemEntry{Name: "Split Bottom", QuickChar: 6, Callback: func() { - + panelContainer.SplitSelected(ui.SplitVertical, ui.NewTabContainer(&theme)) }}, &ui.ItemEntry{Name: "Split Left", QuickChar: 6, Callback: func() { }}, &ui.ItemEntry{Name: "Split Right", QuickChar: 6, Callback: func() { - + panelContainer.SplitSelected(ui.SplitHorizontal, ui.NewTabContainer(&theme)) }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Move", Shortcut: "Ctrl+M", Callback: func() { }}, &ui.ItemEntry{Name: "Resize", Shortcut: "Ctrl+R", Callback: func() { diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go index 6323bc0..661dd86 100755 --- a/ui/panelcontainer.go +++ b/ui/panelcontainer.go @@ -77,12 +77,19 @@ func (c *PanelContainer) SplitSelected(kind SplitKind, item Component) { } // It should be asserted that whatever is selected is either PanelKindEmpty or PanelKindSingle + + (**c.selected).Left = &Panel{Parent: *c.selected, Left: (**c.selected).Left, Kind: PanelKindSingle} if item == nil { (**c.selected).Right = &Panel{Parent: *c.selected, Kind: PanelKindEmpty} } else { (**c.selected).Right = &Panel{Parent: *c.selected, Left: item, Kind: PanelKindSingle} } (**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() (*c.selected).SetFocused(false) panel := (**c.selected).Left.(*Panel) // TODO: watch me... might be a bug lurking in a hidden copy here From 95e4db4d3e284c00d13d390ecfeb8cd3edc5b77b Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Mon, 5 Apr 2021 17:31:16 -0500 Subject: [PATCH 05/17] PanelContainer: SwapNeighborsSelected() to split left and split top --- main.go | 6 ++++-- ui/panelcontainer.go | 14 +++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index c0c853e..f1ab7f8 100644 --- a/main.go +++ b/main.go @@ -288,11 +288,13 @@ func main() { }}, &ui.ItemEntry{Name: "Focus Right", QuickChar: -1, Shortcut: "Alt+Right", Callback: func() { }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Split Top", QuickChar: 6, Callback: func() { - + panelContainer.SplitSelected(ui.SplitVertical, ui.NewTabContainer(&theme)) + panelContainer.SwapNeighborsSelected() }}, &ui.ItemEntry{Name: "Split Bottom", QuickChar: 6, Callback: func() { panelContainer.SplitSelected(ui.SplitVertical, ui.NewTabContainer(&theme)) }}, &ui.ItemEntry{Name: "Split Left", QuickChar: 6, Callback: func() { - + panelContainer.SplitSelected(ui.SplitHorizontal, ui.NewTabContainer(&theme)) + panelContainer.SwapNeighborsSelected() }}, &ui.ItemEntry{Name: "Split Right", QuickChar: 6, Callback: func() { panelContainer.SplitSelected(ui.SplitHorizontal, ui.NewTabContainer(&theme)) }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Move", Shortcut: "Ctrl+M", Callback: func() { diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go index 661dd86..ff96b34 100755 --- a/ui/panelcontainer.go +++ b/ui/panelcontainer.go @@ -59,7 +59,6 @@ func (c *PanelContainer) DeleteSelected() Component { (*p).Kind = PanelKindSingle (*c.selected).SetFocused(false) // Unfocus item - (*c.selected) = nil // Tell garbage collector to come pick up selected (being safe) c.selected = &p (*c.selected).UpdateSplits() (*c.selected).SetFocused(c.focused) @@ -68,6 +67,19 @@ func (c *PanelContainer) DeleteSelected() Component { } } +// 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 + } +} + // 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. From c40be8956422a8f3095d13011d6bd310a6af952e Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Tue, 6 Apr 2021 10:06:02 -0500 Subject: [PATCH 06/17] PanelContainer: Floating Panels, but not quite? --- main.go | 14 ++- ui/panelcontainer.go | 200 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 171 insertions(+), 43 deletions(-) diff --git a/main.go b/main.go index f1ab7f8..62be962 100644 --- a/main.go +++ b/main.go @@ -56,7 +56,10 @@ func showErrorDialog(title string, message string, callback func()) { } func getActiveTabContainer() *ui.TabContainer { - return panelContainer.GetSelected().(*ui.TabContainer) + if panelContainer.GetSelected() != nil { + return panelContainer.GetSelected().(*ui.TabContainer) + } + return nil } // returns nil if no TextEdit is visible @@ -270,7 +273,7 @@ func main() { }}, &ui.ItemEntry{Name: "Save As...", QuickChar: 5, Callback: saveAs}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Close", Shortcut: "Ctrl+Q", Callback: func() { tabContainer := getActiveTabContainer() - if tabContainer.GetTabCount() > 0 { + if tabContainer != nil && tabContainer.GetTabCount() > 0 { tabContainer.RemoveTab(tabContainer.GetSelectedTabIdx()) } else { // No tabs open; close the editor closing = true @@ -301,8 +304,11 @@ func main() { }}, &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) + } }}}) editMenu := ui.NewMenu("Edit", 0, &theme) diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go index ff96b34..077361d 100755 --- a/ui/panelcontainer.go +++ b/ui/panelcontainer.go @@ -10,20 +10,22 @@ const ( ) type PanelContainer struct { - root *Panel - floating []*Panel - selected **Panel // Only Panels with PanelKindSingle - focused bool - theme *Theme + 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, + root: root, + floating: make([]*Panel, 0, 3), + selected: &root, + theme: theme, } } @@ -44,24 +46,45 @@ func (c *PanelContainer) DeleteSelected() Component { panic("selected is not leaf") } - // If selected is root, just make it empty + // 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 *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 - } - (*p).Kind = PanelKindSingle + 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 + } + (*p).Kind = PanelKindSingle - (*c.selected).SetFocused(false) // Unfocus item - c.selected = &p + if c.focused { + (*c.selected).SetFocused(false) // Unfocus item + } + c.selected = &p + } else if c.floatingMode { // Deleting a floating Panel without a parent + if c.focused { + c.floating[0].SetFocused(false) // Unfocus Panel and item + } + 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.selected = &c.floating[0] + } + } else { + panic("Panel does not have parent and is not floating") + } (*c.selected).UpdateSplits() - (*c.selected).SetFocused(c.focused) + if c.focused { + (*c.selected).SetFocused(c.focused) + } return item } @@ -80,6 +103,33 @@ func (c *PanelContainer) SwapNeighborsSelected() { } } +// 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: PanelKindSingle} + (**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 + if c.focused { + (*c.selected).SetFocused(false) + } + panel = (**c.selected).Left.(*Panel) + c.selected = &panel + if c.focused { + (*c.selected).SetFocused(c.focused) + } +} + // 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. @@ -88,25 +138,11 @@ func (c *PanelContainer) SplitSelected(kind SplitKind, item Component) { panic("selected is not leaf") } - // It should be asserted that whatever is selected is either PanelKindEmpty or PanelKindSingle - - (**c.selected).Left = &Panel{Parent: *c.selected, Left: (**c.selected).Left, Kind: PanelKindSingle} if item == nil { - (**c.selected).Right = &Panel{Parent: *c.selected, Kind: PanelKindEmpty} + c.splitSelectedWithPanel(kind, &Panel{Parent: *c.selected, Kind: PanelKindEmpty}) } else { - (**c.selected).Right = &Panel{Parent: *c.selected, Left: item, Kind: PanelKindSingle} + c.splitSelectedWithPanel(kind, &Panel{Parent: *c.selected, Left: item, Kind: PanelKindSingle}) } - (**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() - (*c.selected).SetFocused(false) - panel := (**c.selected).Left.(*Panel) // TODO: watch me... might be a bug lurking in a hidden copy here - c.selected = &panel - (*c.selected).SetFocused(c.focused) } func (c *PanelContainer) GetSelected() Component { @@ -123,16 +159,102 @@ func (c *PanelContainer) SetSelected(item Component) { (*c.selected).UpdateSplits() } -func (c *PanelContainer) FloatSelected() { - +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 } -func (c *PanelContainer) UnfloatSelected() { +// 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 { + if c.focused { + (*c.selected).SetFocused(false) // Unfocus in-tree window + } + c.lastNonFloatingSelected = c.selected + c.selected = &c.floating[0] + if c.focused { + (*c.selected).SetFocused(true) + } + c.floatingMode = true + return true + } + } else { + if c.focused { + (*c.selected).SetFocused(false) // Unfocus floating window + } + c.selected = c.lastNonFloatingSelected + if c.focused { + (*c.selected).SetFocused(true) // Focus in-tree window + } + 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 + } + + panel := *c.selected + c.DeleteSelected() + (*c.selected).UpdateSplits() + panel.Parent = nil + panel.UpdateSplits() + + c.floating = append(c.floating, panel) + 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 + } + + panel := *c.selected + c.DeleteSelected() + c.SetFloatingFocused(false) + c.splitSelectedWithPanel(kind, panel) + + // Try to return to floating focus + return c.SetFloatingFocused(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) { From 389276ee3461414cbb5768bc15ebca5ea8472455 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Tue, 6 Apr 2021 11:14:17 -0500 Subject: [PATCH 07/17] PanelContainer: Bug fixes and integration --- main.go | 15 ++++++++++++++- ui/panelcontainer.go | 46 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/main.go b/main.go index 62be962..f5f855c 100644 --- a/main.go +++ b/main.go @@ -199,6 +199,10 @@ func main() { 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 tabContainer := getActiveTabContainer() + if tabContainer == nil { + tabContainer = ui.NewTabContainer(&theme) + panelContainer.SetSelected(tabContainer) + } tabContainer.AddTab("noname", textEdit) tabContainer.FocusTab(tabContainer.GetTabCount() - 1) changeFocus(panelContainer) @@ -224,6 +228,10 @@ func main() { } textEdit := ui.NewTextEdit(screen, path, bytes, &theme) + if tabContainer == nil { + tabContainer = ui.NewTabContainer(&theme) + panelContainer.SetSelected(tabContainer) + } tabContainer.AddTab(path, textEdit) } @@ -276,7 +284,12 @@ func main() { if tabContainer != nil && tabContainer.GetTabCount() > 0 { tabContainer.RemoveTab(tabContainer.GetSelectedTabIdx()) } else { // No tabs open; close the editor - closing = true + // if the selected is root: close editor. otherwise close panel + if panelContainer.IsRootSelected() { + closing = true + } else { + panelContainer.DeleteSelected() + } } }}}) diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go index 077361d..a22c0a0 100755 --- a/ui/panelcontainer.go +++ b/ui/panelcontainer.go @@ -35,7 +35,9 @@ func (c *PanelContainer) ClearSelected() Component { item := (**c.selected).Left (**c.selected).Left = nil (**c.selected).Kind = PanelKindEmpty - (*c.selected).UpdateSplits() + if p := (**c.selected).Parent; p != nil { + p.UpdateSplits() + } return item } @@ -52,6 +54,13 @@ func (c *PanelContainer) DeleteSelected() Component { } else { item := (**c.selected).Left p := (**c.selected).Parent + + if c.focused { + (*c.selected).SetFocused(false) // Unfocus item + } + + // we're shifting panel right to left, + // need to focus left if p != nil { if *c.selected == (*p).Left { // If we're deleting the parent's Left (*p).Left = (*p).Right @@ -59,16 +68,28 @@ func (c *PanelContainer) DeleteSelected() Component { } else { // Deleting parent's Right (*p).Right = nil } - (*p).Kind = PanelKindSingle - if c.focused { - (*c.selected).SetFocused(false) // Unfocus item + if (*p).Left != nil { // Left == panel ; SHOULD NOT BE PANEL + // asserting left is panel: + // if left is not a Leaf !.IsLeaf(): + // make the parent match the left: + // p.Left = panel's Left + // p.Right = panel's Right + // p.Kind = panel's kind + // else: + // parent left = panel's left + // parent's kind = panel's kind + panel := (*p).Left.(*Panel) + + (*p).Left = (*panel).Left + (*p).Right = (*panel).Right + (*p).Kind = (*panel).Kind + } else { + (*p).Kind = PanelKindEmpty } c.selected = &p + (*p).UpdateSplits() } else if c.floatingMode { // Deleting a floating Panel without a parent - if c.focused { - c.floating[0].SetFocused(false) // Unfocus Panel and item - } 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 @@ -81,7 +102,7 @@ func (c *PanelContainer) DeleteSelected() Component { } else { panic("Panel does not have parent and is not floating") } - (*c.selected).UpdateSplits() + if c.focused { (*c.selected).SetFocused(c.focused) } @@ -106,7 +127,7 @@ func (c *PanelContainer) SwapNeighborsSelected() { // 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: PanelKindSingle} + (**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 @@ -145,7 +166,14 @@ func (c *PanelContainer) SplitSelected(kind SplitKind, item Component) { } } +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 } From 459087e6d817afe48a8831fd6487e48979da8346 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Tue, 6 Apr 2021 14:35:55 -0500 Subject: [PATCH 08/17] PanelContainer: removed multiple comments --- ui/panelcontainer.go | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go index a22c0a0..e433a1c 100755 --- a/ui/panelcontainer.go +++ b/ui/panelcontainer.go @@ -59,8 +59,6 @@ func (c *PanelContainer) DeleteSelected() Component { (*c.selected).SetFocused(false) // Unfocus item } - // we're shifting panel right to left, - // need to focus left if p != nil { if *c.selected == (*p).Left { // If we're deleting the parent's Left (*p).Left = (*p).Right @@ -69,18 +67,8 @@ func (c *PanelContainer) DeleteSelected() Component { (*p).Right = nil } - if (*p).Left != nil { // Left == panel ; SHOULD NOT BE PANEL - // asserting left is panel: - // if left is not a Leaf !.IsLeaf(): - // make the parent match the left: - // p.Left = panel's Left - // p.Right = panel's Right - // p.Kind = panel's kind - // else: - // parent left = panel's left - // parent's kind = panel's kind + if (*p).Left != nil { panel := (*p).Left.(*Panel) - (*p).Left = (*panel).Left (*p).Right = (*panel).Right (*p).Kind = (*panel).Kind From 56b89c607969010c0dd956ec48d7ace378f97373 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Wed, 7 Apr 2021 13:56:32 -0500 Subject: [PATCH 09/17] PanelContainer: Implement cycling selection --- main.go | 8 ++++++-- ui/panel.go | 35 +++++++++++++++++++++++++---------- ui/panelcontainer.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index f5f855c..09c2701 100644 --- a/main.go +++ b/main.go @@ -295,8 +295,12 @@ func main() { 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() + }}, &ui.ItemEntry{Name: "Focus Prev", Shortcut: "Alt+,", Callback: func() { + panelContainer.SelectPrev() + }}, &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 Left", QuickChar: -1, Shortcut: "Alt+Left", Callback: func() { diff --git a/ui/panel.go b/ui/panel.go index 00a139e..d056a30 100755 --- a/ui/panel.go +++ b/ui/panel.go @@ -69,27 +69,42 @@ func (p *Panel) UpdateSplits() { } // Same as EachLeaf, but returns true if any call to `f` returned true. -func (p *Panel) eachLeaf(f func(*Component) bool) bool { +func (p *Panel) eachLeaf(rightMost bool, f func(*Panel) bool) bool { switch p.Kind { + case PanelKindEmpty: + fallthrough case PanelKindSingle: - return f(&p.Left) + return f(p) + case PanelKindSplitVert: fallthrough case PanelKindSplitHor: - if p.Left.(*Panel).eachLeaf(f) { - return true + 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) } - return p.Right.(*Panel).eachLeaf(f) + default: return false } } -// EachLeaf visits the entire tree in left-most order, and calls function `f` -// at each individual Component (never for Panels). If the function `f` returns -// true, then visiting stops. -func (p *Panel) EachLeaf(f func(*Component) bool) { - p.eachLeaf(f) +// 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 diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go index e433a1c..e0a1cd4 100755 --- a/ui/panelcontainer.go +++ b/ui/panelcontainer.go @@ -266,6 +266,36 @@ func (c *PanelContainer) UnfloatSelected(kind SplitKind) bool { return c.SetFloatingFocused(true) } +func (c *PanelContainer) selectNext(rightMost bool) { + var nextIsIt bool + c.root.EachLeaf(rightMost, func(p *Panel) bool { + if nextIsIt { + c.selected = &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.selected = &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-- { From e7e459b16fbcbf8470e66bf33558660368885325 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Wed, 7 Apr 2021 14:55:20 -0500 Subject: [PATCH 10/17] PanelContainer: code cleanup --- ui/panel.go | 8 ++--- ui/panelcontainer.go | 76 +++++++++++++++++--------------------------- 2 files changed, 34 insertions(+), 50 deletions(-) diff --git a/ui/panel.go b/ui/panel.go index d056a30..8ca6242 100755 --- a/ui/panel.go +++ b/ui/panel.go @@ -6,10 +6,10 @@ import "github.com/gdamore/tcell/v2" 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 + 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 diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go index e0a1cd4..981fcfb 100755 --- a/ui/panelcontainer.go +++ b/ui/panelcontainer.go @@ -22,10 +22,10 @@ type PanelContainer struct { func NewPanelContainer(theme *Theme) *PanelContainer { root := &Panel{Kind: PanelKindEmpty} return &PanelContainer{ - root: root, - floating: make([]*Panel, 0, 3), - selected: &root, - theme: theme, + root: root, + floating: make([]*Panel, 0, 3), + selected: &root, + theme: theme, } } @@ -41,6 +41,18 @@ func (c *PanelContainer) ClearSelected() Component { 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 { @@ -55,10 +67,6 @@ func (c *PanelContainer) DeleteSelected() Component { item := (**c.selected).Left p := (**c.selected).Parent - if c.focused { - (*c.selected).SetFocused(false) // Unfocus item - } - if p != nil { if *c.selected == (*p).Left { // If we're deleting the parent's Left (*p).Left = (*p).Right @@ -75,25 +83,22 @@ func (c *PanelContainer) DeleteSelected() Component { } else { (*p).Kind = PanelKindEmpty } - c.selected = &p + + 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 + 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.selected = &c.floating[0] + c.changeSelected(&c.floating[0]) } } else { panic("Panel does not have parent and is not floating") } - - if c.focused { - (*c.selected).SetFocused(c.focused) - } return item } @@ -129,14 +134,8 @@ func (c *PanelContainer) splitSelectedWithPanel(kind SplitKind, panel *Panel) { (*c.selected).UpdateSplits() // Change selected from parent to the previously selected Panel on the Left - if c.focused { - (*c.selected).SetFocused(false) - } panel = (**c.selected).Left.(*Panel) - c.selected = &panel - if c.focused { - (*c.selected).SetFocused(c.focused) - } + c.changeSelected(&panel) } // SplitSelected splits the selected Panel with the given Component `item`. @@ -197,25 +196,13 @@ func (c *PanelContainer) GetFloatingFocused() bool { func (c *PanelContainer) SetFloatingFocused(v bool) bool { if v { if len(c.floating) > 0 { - if c.focused { - (*c.selected).SetFocused(false) // Unfocus in-tree window - } c.lastNonFloatingSelected = c.selected - c.selected = &c.floating[0] - if c.focused { - (*c.selected).SetFocused(true) - } + c.changeSelected(&c.floating[0]) c.floatingMode = true return true } } else { - if c.focused { - (*c.selected).SetFocused(false) // Unfocus floating window - } - c.selected = c.lastNonFloatingSelected - if c.focused { - (*c.selected).SetFocused(true) // Focus in-tree window - } + c.changeSelected(c.lastNonFloatingSelected) c.floatingMode = false } return false @@ -232,14 +219,12 @@ func (c *PanelContainer) FloatSelected() { return } - panel := *c.selected c.DeleteSelected() + (**c.selected).Parent = nil (*c.selected).UpdateSplits() - panel.Parent = nil - panel.UpdateSplits() - c.floating = append(c.floating, panel) - c.raiseFloating(len(c.floating)-1) + 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 @@ -257,10 +242,9 @@ func (c *PanelContainer) UnfloatSelected(kind SplitKind) bool { return false } - panel := *c.selected c.DeleteSelected() c.SetFloatingFocused(false) - c.splitSelectedWithPanel(kind, panel) + c.splitSelectedWithPanel(kind, *c.selected) // Try to return to floating focus return c.SetFloatingFocused(true) @@ -270,7 +254,7 @@ func (c *PanelContainer) selectNext(rightMost bool) { var nextIsIt bool c.root.EachLeaf(rightMost, func(p *Panel) bool { if nextIsIt { - c.selected = &p + c.changeSelected(&p) nextIsIt = false return true } else if p == *c.selected { @@ -284,7 +268,7 @@ func (c *PanelContainer) selectNext(rightMost bool) { // 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.selected = &p; return true }) + c.root.EachLeaf(rightMost, func(p *Panel) bool { c.changeSelected(&p); return true }) } } @@ -298,7 +282,7 @@ func (c *PanelContainer) SelectPrev() { func (c *PanelContainer) Draw(s tcell.Screen) { c.root.Draw(s) - for i := len(c.floating)-1; i >= 0; i-- { + for i := len(c.floating) - 1; i >= 0; i-- { c.floating[i].Draw(s) } } From eb17dfec2eb5d3e2a71e37cd39825c9d1709ebad Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Wed, 7 Apr 2021 19:55:11 -0500 Subject: [PATCH 11/17] Transfer focus to panelContainer on some Menu callbacks --- main.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/main.go b/main.go index 09c2701..b655b30 100644 --- a/main.go +++ b/main.go @@ -297,8 +297,10 @@ func main() { 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() { @@ -310,13 +312,17 @@ func main() { }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Split Top", QuickChar: 6, Callback: func() { panelContainer.SplitSelected(ui.SplitVertical, ui.NewTabContainer(&theme)) panelContainer.SwapNeighborsSelected() + changeFocus(panelContainer) }}, &ui.ItemEntry{Name: "Split Bottom", QuickChar: 6, Callback: func() { panelContainer.SplitSelected(ui.SplitVertical, ui.NewTabContainer(&theme)) + changeFocus(panelContainer) }}, &ui.ItemEntry{Name: "Split Left", QuickChar: 6, Callback: func() { panelContainer.SplitSelected(ui.SplitHorizontal, ui.NewTabContainer(&theme)) panelContainer.SwapNeighborsSelected() + changeFocus(panelContainer) }}, &ui.ItemEntry{Name: "Split Right", QuickChar: 6, Callback: func() { panelContainer.SplitSelected(ui.SplitHorizontal, ui.NewTabContainer(&theme)) + changeFocus(panelContainer) }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Move", Shortcut: "Ctrl+M", Callback: func() { }}, &ui.ItemEntry{Name: "Resize", Shortcut: "Ctrl+R", Callback: func() { @@ -326,6 +332,7 @@ func main() { if !panelContainer.GetFloatingFocused() { panelContainer.SetFloatingFocused(true) } + changeFocus(panelContainer) }}}) editMenu := ui.NewMenu("Edit", 0, &theme) From cdeb3be359b5e7209001485c1e64fc34d76ba76f Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Wed, 7 Apr 2021 21:06:36 -0500 Subject: [PATCH 12/17] TabContainer & Theme fixes to better show focused state of TabContainer and MenuBar --- main.go | 2 +- ui/menu.go | 10 ++++++++-- ui/tabcontainer.go | 24 +++++++++++++++--------- ui/theme.go | 29 ++++++++++++++--------------- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/main.go b/main.go index b655b30..df2ba7e 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,7 @@ var ( ) var theme = ui.Theme{ - "StatusBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver), + "StatusBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorLightGray), } var ( diff --git a/ui/menu.go b/ui/menu.go index 5f8c1c4..cf33dd2 100644 --- a/ui/menu.go +++ b/ui/menu.go @@ -152,7 +152,12 @@ func (b *MenuBar) CursorRight() { // Draw renders the MenuBar and its sub-menus. 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 DrawRect(s, b.x, b.y, b.width, 1, ' ', normalStyle) @@ -160,7 +165,8 @@ func (b *MenuBar) Draw(s tcell.Screen) { for i, item := range b.menus { sty := normalStyle 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) diff --git a/ui/tabcontainer.go b/ui/tabcontainer.go index d0c12f9..5759e6e 100644 --- a/ui/tabcontainer.go +++ b/ui/tabcontainer.go @@ -89,23 +89,29 @@ func (c *TabContainer) GetTab(idx int) *Tab { // Draw will draws the border of the BoxContainer, then it draws its child component. 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 - 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 - for _, tab := range c.children { - combinedTabLength += len(tab.Name) + 2 // 2 for padding + for i := range c.children { + combinedTabLength += len(c.children[i].Name) + 2 // 2 for padding } combinedTabLength += len(c.children) - 1 // add for spacing between 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 { - var sty tcell.Style + sty := styFocused if c.selected == i { - sty = c.Theme.GetOrDefault("TabSelected") - } else { - sty = c.Theme.GetOrDefault("Tab") + fg, bg, attr := styFocused.Decompose() + sty = tcell.Style{}.Foreground(bg).Background(fg).Attributes(attr) } var dirty bool @@ -121,7 +127,7 @@ func (c *TabContainer) Draw(s tcell.Screen) { 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 } diff --git a/ui/theme.go b/ui/theme.go index 5188df1..78ae46b 100644 --- a/ui/theme.go +++ b/ui/theme.go @@ -29,19 +29,18 @@ func (theme *Theme) GetOrDefault(key string) tcell.Style { // DefaultTheme uses only the first 16 colors present in most colored terminals. var DefaultTheme = Theme{ - "Normal": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), - "Button": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite), - "InputField": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), - "MenuBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver), - "MenuBarSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), - "Menu": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver), - "MenuSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), - "Tab": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), - "TabContainer": 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), - "TextEditColumn": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorGray), - "TextEditSelected": 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), + "Normal": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), + "Button": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver), + "InputField": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), + "MenuBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorDarkGray), + "MenuBarFocused": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorLightGray), + "Menu": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver), + "MenuSelected": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), + "TabContainer": tcell.Style{}.Foreground(tcell.ColorGray).Background(tcell.ColorBlack), + "TabContainerFocused": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), + "TextEdit": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), + "TextEditColumn": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorGray), + "TextEditSelected": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver), + "Window": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorDarkGray), + "WindowHeader": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver), } From baafcf3d5c7aaf223833c009cef2ac9392b81403 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Wed, 7 Apr 2021 21:45:38 -0500 Subject: [PATCH 13/17] TextEdit: Colorscheme "Column" style + changed column appearance --- ui/buffer/language.go | 1 + ui/textedit.go | 35 ++++++++++------------------------- ui/theme.go | 1 - 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/ui/buffer/language.go b/ui/buffer/language.go index 066e12f..a04f3c1 100755 --- a/ui/buffer/language.go +++ b/ui/buffer/language.go @@ -4,6 +4,7 @@ type Syntax uint8 const ( Default Syntax = iota + Column // Not necessarily a Syntax; useful for Colorscheming editor column Keyword String Special diff --git a/ui/textedit.go b/ui/textedit.go index 830d4db..55a1c7e 100644 --- a/ui/textedit.go +++ b/ui/textedit.go @@ -45,11 +45,10 @@ type TextEdit struct { 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 scrollx, scrolly int // X and Y offset of view, known as scroll + theme *Theme selection Region // Selection: selectMode determines if it should be used selectMode bool // Whether the user is actively selecting text - - Theme *Theme } // New will initialize the buffer using the given 'contents'. If the 'filePath' or 'FilePath' is empty, @@ -62,8 +61,9 @@ func NewTextEdit(screen *tcell.Screen, filePath string, contents []byte, theme * UseHardTabs: true, TabSize: 4, FilePath: filePath, + screen: screen, - Theme: theme, + theme: theme, } te.SetContents(contents) return te @@ -117,6 +117,7 @@ loop: colorscheme := &buffer.Colorscheme{ 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.String: tcell.Style{}.Foreground(tcell.ColorOlive).Background(tcell.ColorBlack), buffer.Keyword: tcell.Style{}.Foreground(tcell.ColorNavy).Background(tcell.ColorBlack), @@ -359,10 +360,10 @@ func (t *TextEdit) CursorRight() { // getColumnWidth returns the width of the line numbers column if it is present. func (t *TextEdit) getColumnWidth() int { - columnWidth := 0 + var columnWidth int if t.LineNumbers { // 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 } @@ -383,8 +384,8 @@ func (t *TextEdit) Draw(s tcell.Screen) { columnWidth := t.getColumnWidth() bufferLines := t.Buffer.Lines() - selectedStyle := t.Theme.GetOrDefault("TextEditSelected") - columnStyle := t.Theme.GetOrDefault("TextEditColumn") + selectedStyle := t.theme.GetOrDefault("TextEditSelected") + columnStyle := t.Highlighter.Colorscheme.GetStyle(buffer.Column) t.Highlighter.UpdateInvalidatedLines(t.scrolly, t.scrolly+(t.height-1)) @@ -408,22 +409,7 @@ func (t *TextEdit) Draw(s tcell.Screen) { var origLineBytes []byte = t.Buffer.Line(line) 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 { - // 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) } @@ -435,7 +421,6 @@ func (t *TextEdit) Draw(s tcell.Screen) { col := t.x + columnWidth 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) { _, size := utf8.DecodeRune(lineBytes[byteIdx:]) // Respect UTF-8 byteIdx += size @@ -545,7 +530,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 } @@ -566,7 +551,7 @@ func (t *TextEdit) SetFocused(v bool) { } func (t *TextEdit) SetTheme(theme *Theme) { - t.Theme = theme + t.theme = theme } // GetPos gets the position of the TextEdit. diff --git a/ui/theme.go b/ui/theme.go index 78ae46b..b6dda8d 100644 --- a/ui/theme.go +++ b/ui/theme.go @@ -39,7 +39,6 @@ var DefaultTheme = Theme{ "TabContainer": tcell.Style{}.Foreground(tcell.ColorGray).Background(tcell.ColorBlack), "TabContainerFocused": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), "TextEdit": tcell.Style{}.Foreground(tcell.ColorSilver).Background(tcell.ColorBlack), - "TextEditColumn": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorGray), "TextEditSelected": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver), "Window": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorDarkGray), "WindowHeader": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver), From 8142bb2de52104a2ca3e36a79336b53340af4898 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Wed, 7 Apr 2021 21:51:18 -0500 Subject: [PATCH 14/17] Autofocus new panels --- main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/main.go b/main.go index df2ba7e..cdb234f 100644 --- a/main.go +++ b/main.go @@ -312,16 +312,20 @@ func main() { }}, &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() { panelContainer.SplitSelected(ui.SplitVertical, ui.NewTabContainer(&theme)) + panelContainer.SelectNext() changeFocus(panelContainer) }}, &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() { panelContainer.SplitSelected(ui.SplitHorizontal, ui.NewTabContainer(&theme)) + panelContainer.SelectNext() changeFocus(panelContainer) }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Move", Shortcut: "Ctrl+M", Callback: func() { From 5df772e568003c4495ee6bf8a6b4d6b6dfe6c8b6 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Wed, 7 Apr 2021 22:05:58 -0500 Subject: [PATCH 15/17] TextEdit: only hide cursor when unfocused --- ui/textedit.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/ui/textedit.go b/ui/textedit.go index 55a1c7e..1958100 100644 --- a/ui/textedit.go +++ b/ui/textedit.go @@ -268,6 +268,20 @@ func (t *TextEdit) GetLineCol() (int, int) { 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. // 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. @@ -300,9 +314,8 @@ func (t *TextEdit) SetLineCol(line, col int) { t.cury, t.curx = line, col if t.focused && !t.selectMode { - (*t.screen).ShowCursor(t.x+columnWidth+col+tabOffset-t.scrollx, t.y+line-t.scrolly) - } else { - (*t.screen).HideCursor() + // Update terminal cursor position + t.updateTerminalCursorNoHelper(columnWidth, tabOffset) } } @@ -544,7 +557,7 @@ func (t *TextEdit) Draw(s tcell.Screen) { func (t *TextEdit) SetFocused(v bool) { t.focused = v if v { - t.SetLineCol(t.cury, t.curx) + t.updateTerminalCursor() } else { (*t.screen).HideCursor() } From 14ba338de914145cee55093ba8c866bc411c7f28 Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Wed, 7 Apr 2021 23:03:20 -0500 Subject: [PATCH 16/17] PanelContainer: bug fix on destroying a panel This commit fixes a bug where destroying a Panel with a parent of kind PanelKindSplit... causes the selected to become that split panel, instead of a child. Now, it chooses the leftmost child of that parent panel. --- ui/panelcontainer.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/panelcontainer.go b/ui/panelcontainer.go index 981fcfb..e0716e6 100755 --- a/ui/panelcontainer.go +++ b/ui/panelcontainer.go @@ -76,15 +76,24 @@ func (c *PanelContainer) DeleteSelected() Component { } 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 } - c.changeSelected(&p) + // 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 From 2e128e5b361600cfc03cf3b45ecc4ae9b4178c3d Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Thu, 8 Apr 2021 11:11:09 -0500 Subject: [PATCH 17/17] Remove temporary comments from main.go --- main.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/main.go b/main.go index cdb234f..9a002d1 100644 --- a/main.go +++ b/main.go @@ -28,7 +28,6 @@ var ( screen *tcell.Screen menuBar *ui.MenuBar -// tabContainer *ui.TabContainer panelContainer *ui.PanelContainer dialog ui.Component // nil if not present (has exclusive focus) @@ -143,9 +142,6 @@ func main() { var closing bool sizex, sizey := s.Size() -// tabContainer = ui.NewTabContainer(&theme) -// tabContainer.SetPos(0, 1) -// tabContainer.SetSize(sizex, sizey-2) panelContainer = ui.NewPanelContainer(&theme) panelContainer.SetPos(0, 1) panelContainer.SetSize(sizex, sizey-2) @@ -283,7 +279,7 @@ func main() { tabContainer := getActiveTabContainer() if tabContainer != nil && tabContainer.GetTabCount() > 0 { tabContainer.RemoveTab(tabContainer.GetSelectedTabIdx()) - } else { // No tabs open; close the editor + } else { // if the selected is root: close editor. otherwise close panel if panelContainer.IsRootSelected() { closing = true @@ -427,9 +423,6 @@ func main() { //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)) -// if tabContainer.GetTabCount() > 0 { // Draw the tab container only if a tab is open -// tabContainer.Draw(s) -// } panelContainer.Draw(s) menuBar.Draw(s)