From 6c2aac7340cbef6b0cd1f50c81c4bf4beb8e2d8c Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 21 Oct 2019 06:54:13 -0400 Subject: exp with vert layout --- runtime/ui/app.go | 9 ++-- runtime/ui/controller.go | 21 ++++---- runtime/ui/layout/vertical.go | 58 +++++++++++++++++++++ runtime/ui/layout/view.go | 10 ++++ runtime/ui/layout_manager.go | 76 ++++++++++++++-------------- runtime/ui/view/details.go | 8 +++ runtime/ui/view/filetree.go | 8 +++ runtime/ui/view/filter.go | 9 ++++ runtime/ui/view/help.go | 114 ++++++++++++++++++++++++++++++++++++++++++ runtime/ui/view/layer.go | 9 ++++ runtime/ui/view/renderer.go | 9 ---- runtime/ui/view/status.go | 106 --------------------------------------- runtime/ui/view/view.go | 28 +++++++++++ 13 files changed, 298 insertions(+), 167 deletions(-) create mode 100644 runtime/ui/layout/vertical.go create mode 100644 runtime/ui/layout/view.go create mode 100644 runtime/ui/view/help.go delete mode 100644 runtime/ui/view/renderer.go delete mode 100644 runtime/ui/view/status.go create mode 100644 runtime/ui/view/view.go diff --git a/runtime/ui/app.go b/runtime/ui/app.go index ec7f5ad..2c9da10 100644 --- a/runtime/ui/app.go +++ b/runtime/ui/app.go @@ -16,7 +16,7 @@ const debug = false type app struct { gui *gocui.Gui controllers *Controller - layout *layoutManager + layout *LayoutManager } var ( @@ -35,11 +35,11 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC return } - lm := newLayoutManager(theControls) + lm := NewLayoutManager(theControls) gui.Cursor = false //g.Mouse = true - gui.SetManagerFunc(lm.layout) + gui.SetManagerFunc(lm.Layout) // var profileObj = profile.Start(profile.CPUProfile, profile.ProfilePath("."), profile.NoShutdownHook) // @@ -77,7 +77,7 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC return } - theControls.Status.AddHelpKeys(globalHelpKeys...) + theControls.Help.AddHelpKeys(globalHelpKeys...) // perform the first update and render now that all resources have been loaded err = theControls.UpdateAndRender() @@ -106,7 +106,6 @@ func newApp(gui *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeC // } // } -var lastX, lastY int // quit is the gocui callback invoked when the user hits Ctrl+C func (a *app) quit() error { diff --git a/runtime/ui/controller.go b/runtime/ui/controller.go index 0eef2c2..49d4eb7 100644 --- a/runtime/ui/controller.go +++ b/runtime/ui/controller.go @@ -14,10 +14,11 @@ type Controller struct { gui *gocui.Gui Tree *view.FileTree Layer *view.Layer - Status *view.Status + Help *view.Help Filter *view.Filter Details *view.Details - lookup map[string]view.Renderer + + lookup map[string]view.View } func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree.TreeCache) (*Controller, error) { @@ -26,7 +27,7 @@ func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree. controller := &Controller{ gui: g, } - controller.lookup = make(map[string]view.Renderer) + controller.lookup = make(map[string]view.View) controller.Layer, err = view.NewLayerView("layers", g, analysis.Layers) if err != nil { @@ -47,10 +48,10 @@ func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree. // layer view cursor down event should trigger an update in the file tree controller.Layer.AddLayerChangeListener(controller.onLayerChange) - controller.Status = view.NewStatusView("status", g) - controller.lookup[controller.Status.Name()] = controller.Status + controller.Help = view.NewHelpView("status", g) + controller.lookup[controller.Help.Name()] = controller.Help // set the layer view as the first selected view - controller.Status.SetCurrentView(controller.Layer) + controller.Help.SetCurrentView(controller.Layer) // update the status pane when a filetree option is changed by the user controller.Tree.AddViewOptionChangeListener(controller.onFileTreeViewOptionChange) @@ -79,11 +80,11 @@ func NewCollection(g *gocui.Gui, analysis *image.AnalysisResult, cache filetree. } func (c *Controller) onFileTreeViewOptionChange() error { - err := c.Status.Update() + err := c.Help.Update() if err != nil { return err } - return c.Status.Render() + return c.Help.Render() } func (c *Controller) onFilterEdit(filter string) error { @@ -173,10 +174,10 @@ func (c *Controller) ToggleView() (err error) { v := c.gui.CurrentView() if v == nil || v.Name() == c.Layer.Name() { _, err = c.gui.SetCurrentView(c.Tree.Name()) - c.Status.SetCurrentView(c.Tree) + c.Help.SetCurrentView(c.Tree) } else { _, err = c.gui.SetCurrentView(c.Layer.Name()) - c.Status.SetCurrentView(c.Layer) + c.Help.SetCurrentView(c.Layer) } if err != nil { diff --git a/runtime/ui/layout/vertical.go b/runtime/ui/layout/vertical.go new file mode 100644 index 0000000..67d12f4 --- /dev/null +++ b/runtime/ui/layout/vertical.go @@ -0,0 +1,58 @@ +package layout + +import ( + "fmt" + "github.com/wagoodman/dive/runtime/ui/view" +) + +type Vertical struct { + visible bool + width int + elements []View +} + +// how does overrun work? which view gets precidence? how does max possible height work? + +func NewVerticalLayout() *Vertical { + return &Vertical{ + visible: true, + width: view.WidthFull, + elements: make([]View, 0), + } +} + +func (v Vertical) SetWidth(w int) { + v.width = w +} + +func (v *Vertical) AddView(sub View) error { + for _, element := range v.elements { + if element.Name() == sub.Name() { + return fmt.Errorf("view already added") + } + } + v.elements = append(v.elements, sub) + return nil +} + +func (v *Vertical) Name() string { + return view.IdentityNone +} + +func (v *Vertical) IsVisible() bool { + return v.visible +} + +func (v *Vertical) Height() (height int) { + for _, element := range v.elements { + height += element.Height() + if height == view.HeightFull { + return view.HeightFull + } + } + return +} + +func (v *Vertical) Width() int { + return v.width +} diff --git a/runtime/ui/layout/view.go b/runtime/ui/layout/view.go new file mode 100644 index 0000000..62c5ef0 --- /dev/null +++ b/runtime/ui/layout/view.go @@ -0,0 +1,10 @@ +package layout + +import ( + "github.com/wagoodman/dive/runtime/ui/view" +) + +type View interface { + view.Identifiable + view.Dimensional +} diff --git a/runtime/ui/layout_manager.go b/runtime/ui/layout_manager.go index 72f8c26..95b227b 100644 --- a/runtime/ui/layout_manager.go +++ b/runtime/ui/layout_manager.go @@ -6,13 +6,15 @@ import ( "github.com/spf13/viper" ) -type layoutManager struct { +var lastY, lastX int + +type LayoutManager struct { fileTreeSplitRatio float64 - controllers *Controller + controller *Controller } // todo: this needs a major refactor (derive layout from view obj info, which should not live here) -func newLayoutManager(c *Controller) *layoutManager { +func NewLayoutManager(c *Controller) *LayoutManager { fileTreeSplitRatio := viper.GetFloat64("filetree.pane-width") if fileTreeSplitRatio >= 1 || fileTreeSplitRatio <= 0 { @@ -20,14 +22,14 @@ func newLayoutManager(c *Controller) *layoutManager { fileTreeSplitRatio = 0.5 } - return &layoutManager{ + return &LayoutManager{ fileTreeSplitRatio: fileTreeSplitRatio, - controllers: c, + controller: c, } } -// IsNewView determines if a view has already been created based on the set of errors given (a bit hokie) -func IsNewView(errs ...error) bool { +// isNewView determines if a view has already been created based on the set of errors given (a bit hokie) +func isNewView(errs ...error) bool { for _, err := range errs { if err == nil { return false @@ -41,7 +43,7 @@ func IsNewView(errs ...error) bool { // layout defines the definition of the window pane size and placement relations to one another. This // is invoked at application start and whenever the screen dimensions change. -func (lm *layoutManager) layout(g *gocui.Gui) error { +func (lm *LayoutManager) Layout(g *gocui.Gui) error { // TODO: this logic should be refactored into an abstraction that takes care of the math for us maxX, maxY := g.Size() @@ -64,12 +66,12 @@ func (lm *layoutManager) layout(g *gocui.Gui) error { headerRows := 2 filterBarHeight := 1 - statusBarHeight := 1 + helpBarHeight := 1 - statusBarIndex := 1 + helpBarIndex := 1 filterBarIndex := 2 - layersHeight := len(lm.controllers.Layer.Layers) + headerRows + 1 // layers + header + base image layer row + layersHeight := len(lm.controller.Layer.Layers) + headerRows + 1 // layers + header + base image layer row maxLayerHeight := int(0.75 * float64(maxY)) if layersHeight > maxLayerHeight { layersHeight = maxLayerHeight @@ -78,7 +80,7 @@ func (lm *layoutManager) layout(g *gocui.Gui) error { var view, header *gocui.View var viewErr, headerErr, err error - if !lm.controllers.Filter.IsVisible() { + if !lm.controller.Filter.IsVisible() { bottomRows-- filterBarHeight = 0 } @@ -93,21 +95,21 @@ func (lm *layoutManager) layout(g *gocui.Gui) error { } // Layers - view, viewErr = g.SetView(lm.controllers.Layer.Name(), -1, -1+headerRows, splitCols, layersHeight) - header, headerErr = g.SetView(lm.controllers.Layer.Name()+"header", -1, -1, splitCols, headerRows) - if IsNewView(viewErr, headerErr) { - err = lm.controllers.Layer.Setup(view, header) + view, viewErr = g.SetView(lm.controller.Layer.Name(), -1, -1+headerRows, splitCols, layersHeight) + header, headerErr = g.SetView(lm.controller.Layer.Name()+"header", -1, -1, splitCols, headerRows) + if isNewView(viewErr, headerErr) { + err = lm.controller.Layer.Setup(view, header) if err != nil { logrus.Error("unable to setup layer controller", err) return err } - if _, err = g.SetCurrentView(lm.controllers.Layer.Name()); err != nil { + if _, err = g.SetCurrentView(lm.controller.Layer.Name()); err != nil { logrus.Error("unable to set view to layer", err) return err } // since we are selecting the view, we should rerender to indicate it is selected - err = lm.controllers.Layer.Render() + err = lm.controller.Layer.Render() if err != nil { logrus.Error("unable to render layer view", err) return err @@ -115,10 +117,10 @@ func (lm *layoutManager) layout(g *gocui.Gui) error { } // Details - view, viewErr = g.SetView(lm.controllers.Details.Name(), -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows) - header, headerErr = g.SetView(lm.controllers.Details.Name()+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows) - if IsNewView(viewErr, headerErr) { - err = lm.controllers.Details.Setup(view, header) + view, viewErr = g.SetView(lm.controller.Details.Name(), -1, -1+layersHeight+headerRows, splitCols, maxY-bottomRows) + header, headerErr = g.SetView(lm.controller.Details.Name()+"header", -1, -1+layersHeight, splitCols, layersHeight+headerRows) + if isNewView(viewErr, headerErr) { + err = lm.controller.Details.Setup(view, header) if err != nil { return err } @@ -126,39 +128,39 @@ func (lm *layoutManager) layout(g *gocui.Gui) error { // Filetree offset := 0 - if !lm.controllers.Tree.AreAttributesVisible() { + if !lm.controller.Tree.AreAttributesVisible() { offset = 1 } - view, viewErr = g.SetView(lm.controllers.Tree.Name(), splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows) - header, headerErr = g.SetView(lm.controllers.Tree.Name()+"header", splitCols, -1, debugCols, headerRows-offset) - if IsNewView(viewErr, headerErr) { - err = lm.controllers.Tree.Setup(view, header) + view, viewErr = g.SetView(lm.controller.Tree.Name(), splitCols, -1+headerRows-offset, debugCols, maxY-bottomRows) + header, headerErr = g.SetView(lm.controller.Tree.Name()+"header", splitCols, -1, debugCols, headerRows-offset) + if isNewView(viewErr, headerErr) { + err = lm.controller.Tree.Setup(view, header) if err != nil { logrus.Error("unable to setup tree controller", err) return err } } - err = lm.controllers.Tree.OnLayoutChange(resized) + err = lm.controller.Tree.OnLayoutChange(resized) if err != nil { logrus.Error("unable to setup layer controller onLayoutChange", err) return err } - // Status Bar - view, viewErr = g.SetView(lm.controllers.Status.Name(), -1, maxY-statusBarHeight-statusBarIndex, maxX, maxY-(statusBarIndex-1)) - if IsNewView(viewErr, headerErr) { - err = lm.controllers.Status.Setup(view, nil) + // Help Bar + view, viewErr = g.SetView(lm.controller.Help.Name(), -1, maxY-helpBarHeight-helpBarIndex, maxX, maxY-(helpBarIndex-1)) + if isNewView(viewErr, headerErr) { + err = lm.controller.Help.Setup(view, nil) if err != nil { - logrus.Error("unable to setup status controller", err) + logrus.Error("unable to setup help controller", err) return err } } // Filter Bar - view, viewErr = g.SetView(lm.controllers.Filter.Name(), len(lm.controllers.Filter.HeaderStr())-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1)) - header, headerErr = g.SetView(lm.controllers.Filter.Name()+"header", -1, maxY-filterBarHeight-filterBarIndex, len(lm.controllers.Filter.HeaderStr()), maxY-(filterBarIndex-1)) - if IsNewView(viewErr, headerErr) { - err = lm.controllers.Filter.Setup(view, header) + view, viewErr = g.SetView(lm.controller.Filter.Name(), len(lm.controller.Filter.HeaderStr())-1, maxY-filterBarHeight-filterBarIndex, maxX, maxY-(filterBarIndex-1)) + header, headerErr = g.SetView(lm.controller.Filter.Name()+"header", -1, maxY-filterBarHeight-filterBarIndex, len(lm.controller.Filter.HeaderStr()), maxY-(filterBarIndex-1)) + if isNewView(viewErr, headerErr) { + err = lm.controller.Filter.Setup(view, header) if err != nil { logrus.Error("unable to setup filter controller", err) return err diff --git a/runtime/ui/view/details.go b/runtime/ui/view/details.go index 9cb5ce0..a60de7d 100644 --- a/runtime/ui/view/details.go +++ b/runtime/ui/view/details.go @@ -43,6 +43,14 @@ func NewDetailsView(name string, gui *gocui.Gui, efficiency float64, inefficienc return controller } +func (c *Details) Height() int { + return HeightFull +} + +func (c *Details) Width() int { + return WidthFull +} + func (c *Details) Name() string { return c.name } diff --git a/runtime/ui/view/filetree.go b/runtime/ui/view/filetree.go index 3185be4..107aeb7 100644 --- a/runtime/ui/view/filetree.go +++ b/runtime/ui/view/filetree.go @@ -175,6 +175,14 @@ func (c *FileTree) Setup(v *gocui.View, header *gocui.View) error { return nil } +func (c *FileTree) Height() int { + return HeightFull +} + +func (c *FileTree) Width() int { + return WidthFull +} + // IsVisible indicates if the file tree view pane is currently initialized func (c *FileTree) IsVisible() bool { return c != nil diff --git a/runtime/ui/view/filter.go b/runtime/ui/view/filter.go index e32957a..bb4a0ef 100644 --- a/runtime/ui/view/filter.go +++ b/runtime/ui/view/filter.go @@ -43,6 +43,15 @@ func (c *Filter) AddFilterEditListener(listener ...FilterEditListener) { c.filterEditListeners = append(c.filterEditListeners, listener...) } +func (c *Filter) Height() int { + return 1 +} + +func (c *Filter) Width() int { + return WidthFull +} + + func (c *Filter) Name() string { return c.name } diff --git a/runtime/ui/view/help.go b/runtime/ui/view/help.go new file mode 100644 index 0000000..486e328 --- /dev/null +++ b/runtime/ui/view/help.go @@ -0,0 +1,114 @@ +package view + +import ( + "fmt" + "github.com/sirupsen/logrus" + "github.com/wagoodman/dive/runtime/ui/format" + "github.com/wagoodman/dive/runtime/ui/key" + "strings" + + "github.com/jroimartin/gocui" +) + +// Help holds the UI objects and data models for populating the bottom-most pane. Specifically the panel +// shows the user a set of possible actions to take in the window and currently selected pane. +type Help struct { + name string + gui *gocui.Gui + view *gocui.View + + selectedView View + + helpKeys []*key.Binding +} + +// NewHelpView creates a new view object attached the the global [gocui] screen object. +func NewHelpView(name string, gui *gocui.Gui) (controller *Help) { + controller = new(Help) + + // populate main fields + controller.name = name + controller.gui = gui + controller.helpKeys = make([]*key.Binding, 0) + + return controller +} + +func (c *Help) SetCurrentView(r View) { + c.selectedView = r +} + +func (c *Help) Height() int { + return 1 +} + +func (c *Help) Width() int { + return WidthFull +} + +func (c *Help) Name() string { + return c.name +} + +func (c *Help) AddHelpKeys(keys ...*key.Binding) { + c.helpKeys = append(c.helpKeys, keys...) +} + +// Setup initializes the UI concerns within the context of a global [gocui] view object. +func (c *Help) Setup(v *gocui.View, header *gocui.View) error { + + // set controller options + c.view = v + c.view.Frame = false + + return c.Render() +} + +// IsVisible indicates if the status view pane is currently initialized. +func (c *Help) IsVisible() bool { + return c != nil +} + +// CursorDown moves the cursor down in the details pane (currently indicates nothing). +func (c *Help) CursorDown() error { + return nil +} + +// CursorUp moves the cursor up in the details pane (currently indicates nothing). +func (c *Help) CursorUp() error { + return nil +} + +// Update refreshes the state objects for future rendering (currently does nothing). +func (c *Help) Update() error { + return nil +} + +// Render flushes the state objects to the screen. +func (c *Help) Render() error { + c.gui.Update(func(g *gocui.Gui) error { + c.view.Clear() + + var selectedHelp string + if c.selectedView != nil { + selectedHelp = c.selectedView.KeyHelp() + } + + _, err := fmt.Fprintln(c.view, c.KeyHelp()+selectedHelp+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) + if err != nil { + logrus.Debug("unable to write to buffer: ", err) + } + + return err + }) + return nil +} + +// KeyHelp indicates all the possible global actions a user can take when any pane is selected. +func (c *Help) KeyHelp() string { + var help string + for _, binding := range c.helpKeys { + help += binding.RenderKeyHelp() + } + return help +} diff --git a/runtime/ui/view/layer.go b/runtime/ui/view/layer.go index 5d3c678..e578dc5 100644 --- a/runtime/ui/view/layer.go +++ b/runtime/ui/view/layer.go @@ -79,6 +79,15 @@ func (c *Layer) notifyLayerChangeListeners() error { return nil } +func (c *Layer) Height() int { + return HeightFull +} + +func (c *Layer) Width() int { + return WidthFull +} + + func (c *Layer) Name() string { return c.name } diff --git a/runtime/ui/view/renderer.go b/runtime/ui/view/renderer.go deleted file mode 100644 index c3fadf5..0000000 --- a/runtime/ui/view/renderer.go +++ /dev/null @@ -1,9 +0,0 @@ -package view - -// Controller defines the a renderable terminal screen pane. -type Renderer interface { - Update() error - Render() error - IsVisible() bool - KeyHelp() string -} diff --git a/runtime/ui/view/status.go b/runtime/ui/view/status.go deleted file mode 100644 index bdd0363..0000000 --- a/runtime/ui/view/status.go +++ /dev/null @@ -1,106 +0,0 @@ -package view - -import ( - "fmt" - "github.com/sirupsen/logrus" - "github.com/wagoodman/dive/runtime/ui/format" - "github.com/wagoodman/dive/runtime/ui/key" - "strings" - - "github.com/jroimartin/gocui" -) - -// Status holds the UI objects and data models for populating the bottom-most pane. Specifically the panel -// shows the user a set of possible actions to take in the window and currently selected pane. -type Status struct { - name string - gui *gocui.Gui - view *gocui.View - - selectedView Renderer - - helpKeys []*key.Binding -} - -// NewStatusView creates a new view object attached the the global [gocui] screen object. -func NewStatusView(name string, gui *gocui.Gui) (controller *Status) { - controller = new(Status) - - // populate main fields - controller.name = name - controller.gui = gui - controller.helpKeys = make([]*key.Binding, 0) - - return controller -} - -func (c *Status) SetCurrentView(r Renderer) { - c.selectedView = r -} - -func (c *Status) Name() string { - return c.name -} - -func (c *Status) AddHelpKeys(keys ...*key.Binding) { - c.helpKeys = append(c.helpKeys, keys...) -} - -// Setup initializes the UI concerns within the context of a global [gocui] view object. -func (c *Status) Setup(v *gocui.View, header *gocui.View) error { - - // set controller options - c.view = v - c.view.Frame = false - - return c.Render() -} - -// IsVisible indicates if the status view pane is currently initialized. -func (c *Status) IsVisible() bool { - return c != nil -} - -// CursorDown moves the cursor down in the details pane (currently indicates nothing). -func (c *Status) CursorDown() error { - return nil -} - -// CursorUp moves the cursor up in the details pane (currently indicates nothing). -func (c *Status) CursorUp() error { - return nil -} - -// Update refreshes the state objects for future rendering (currently does nothing). -func (c *Status) Update() error { - return nil -} - -// Render flushes the state objects to the screen. -func (c *Status) Render() error { - c.gui.Update(func(g *gocui.Gui) error { - c.view.Clear() - - var selectedHelp string - if c.selectedView != nil { - selectedHelp = c.selectedView.KeyHelp() - } - - _, err := fmt.Fprintln(c.view, c.KeyHelp()+selectedHelp+format.StatusNormal("▏"+strings.Repeat(" ", 1000))) - if err != nil { - logrus.Debug("unable to write to buffer: ", err) - } - - return err - }) - return nil -} - -// KeyHelp indicates all the possible global actions a user can take when any pane is selected. -func (c *Status) KeyHelp() string { - var help string - for _, binding := range c.helpKeys { - help += binding.RenderKeyHelp() - } - return help -} diff --git a/runtime/ui/view/view.go b/runtime/ui/view/view.go new file mode 100644 index 0000000..c6d9d1a --- /dev/null +++ b/runtime/ui/view/view.go @@ -0,0 +1,28 @@ +package view + +const ( + HeightFull = -1 + WidthFull = -1 + IdentityNone = "" +) + + +type Identifiable interface { + Name() string +} + +type Dimensional interface { + IsVisible() bool + Height() int + Width() int +} + + +// View defines the an element with state that can be updated, queried if visible, and render elements to the screen +type View interface { + Identifiable + Dimensional + Update() error + Render() error + KeyHelp() string +} -- cgit v1.2.3