diff --git a/runtime/ui/layout/area.go b/runtime/ui/layout/area.go new file mode 100644 index 0000000..497534b --- /dev/null +++ b/runtime/ui/layout/area.go @@ -0,0 +1,5 @@ +package layout + +type Area struct { + minX, minY, maxX, maxY int +} diff --git a/runtime/ui/layout/manager.go b/runtime/ui/layout/manager.go index d5693c2..d53a42a 100644 --- a/runtime/ui/layout/manager.go +++ b/runtime/ui/layout/manager.go @@ -23,24 +23,7 @@ func (lm *Manager) Add(element Layout, location Location) { lm.elements[location] = append(lm.elements[location], element) } -// 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. -// A few things to note: -// 1. gocui has borders around all views (even if Frame=false). This means there are a lot of +1/-1 magic numbers -// needed (but there are comments!). -// 2. since there are borders, in order for it to appear as if there aren't any spaces for borders, the views must -// overlap. To prevent screen artifacts, all elements must be layedout from the top of the screen to the bottom. -func (lm *Manager) Layout(g *gocui.Gui) error { - - minX, minY := -1, -1 - maxX, maxY := g.Size() - - var hasResized bool - if maxX != lm.lastX || maxY != lm.lastY { - hasResized = true - } - lm.lastX, lm.lastY = maxX, maxY - +func (lm *Manager) layoutHeaders(g *gocui.Gui, area Area) (Area, error) { // layout headers top down if elements, exists := lm.elements[LocationHeader]; exists { for _, element := range elements { @@ -48,7 +31,7 @@ func (lm *Manager) Layout(g *gocui.Gui) error { // this eliminates the need to discover a default size based on all element requests height := 0 if element.IsVisible() { - requestedHeight := element.RequestedSize(maxY) + requestedHeight := element.RequestedSize(area.maxY) if requestedHeight != nil { height = *requestedHeight } else { @@ -57,23 +40,22 @@ func (lm *Manager) Layout(g *gocui.Gui) error { } // layout the header within the allocated space - err := element.Layout(g, minX, minY, maxX, minY+height) + err := element.Layout(g, area.minX, area.minY, area.maxX, area.minY+height) if err != nil { logrus.Errorf("failed to layout '%s' header: %+v", element.Name(), err) + return area, err } // restrict the available screen real estate - minY += height + area.minY += height } } + return area, nil +} +func (lm *Manager) planFooters(g *gocui.Gui, area Area) (Area, []int) { var footerHeights = make([]int, 0) - // we need to keep the current maxY before carving out the space for the body columns - var footerMaxY = maxY - var footerMinX = minX - var footerMaxX = maxX - // we need to layout the footers last, but account for them when drawing the columns. This block is for planning // out the real estate needed for the footers now (but not laying out yet) if elements, exists := lm.elements[LocationFooter]; exists { @@ -87,7 +69,7 @@ func (lm *Manager) Layout(g *gocui.Gui) error { // this eliminates the need to discover a default size based on all element requests height := 0 if element.IsVisible() { - requestedHeight := element.RequestedSize(maxY) + requestedHeight := element.RequestedSize(area.maxY) if requestedHeight != nil { height = *requestedHeight } else { @@ -98,10 +80,13 @@ func (lm *Manager) Layout(g *gocui.Gui) error { } // restrict the available screen real estate for _, height := range footerHeights { - maxY -= height + area.maxY -= height } } + return area, footerHeights +} +func (lm *Manager) layoutColumns(g *gocui.Gui, area Area) (Area, error) { // layout columns left to right if elements, exists := lm.elements[LocationColumn]; exists { widths := make([]int, len(elements)) @@ -109,7 +94,7 @@ func (lm *Manager) Layout(g *gocui.Gui) error { widths[idx] = -1 } variableColumns := len(elements) - availableWidth := maxX + availableWidth := area.maxX // first pass: planout the column sizes based on the given requests for idx, element := range elements { @@ -138,17 +123,21 @@ func (lm *Manager) Layout(g *gocui.Gui) error { } // layout the column within the allocated space - err := element.Layout(g, minX, minY, minX+width, maxY) + err := element.Layout(g, area.minX, area.minY, area.minX+width, area.maxY) if err != nil { logrus.Errorf("failed to layout '%s' column: %+v", element.Name(), err) + return area, err } // move left to right, scratching off real estate as it is taken - minX += width + area.minX += width } } + return area, nil +} +func (lm *Manager) layoutFooters(g *gocui.Gui, area Area, footerHeights []int) (Area, error) { // layout footers top down (which is why the list is reversed). Top down is needed due to border overlap. if elements, exists := lm.elements[LocationFooter]; exists { for idx := len(elements) - 1; idx >= 0; idx-- { @@ -158,30 +147,84 @@ func (lm *Manager) Layout(g *gocui.Gui) error { for oIdx := 0; oIdx <= idx; oIdx++ { bottomPadding += footerHeights[oIdx] } - topY = footerMaxY - bottomPadding - height + topY = area.maxX - bottomPadding - height // +1 for border bottomY = topY + height + 1 // layout the footer within the allocated space // note: since the headers and rows are inclusive counting from -1 (to account for a border) we must // do the same vertically, thus a -1 is needed for a starting Y - err := element.Layout(g, footerMinX, topY, footerMaxX, bottomY) + err := element.Layout(g, area.minX, topY, area.maxX, bottomY) if err != nil { logrus.Errorf("failed to layout '%s' footer: %+v", element.Name(), err) + return area, err } } } + return area, nil +} + +func (lm *Manager) notifyLayoutChange() error { + for _, elements := range lm.elements { + for _, element := range elements { + err := element.OnLayoutChange() + if err != nil { + return err + } + } + } + return nil +} + +// 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. +// A few things to note: +// 1. gocui has borders around all views (even if Frame=false). This means there are a lot of +1/-1 magic numbers +// needed (but there are comments!). +// 2. since there are borders, in order for it to appear as if there aren't any spaces for borders, the views must +// overlap. To prevent screen artifacts, all elements must be layedout from the top of the screen to the bottom. +func (lm *Manager) Layout(g *gocui.Gui) error { + + curMaxX, curMaxY := g.Size() + area := Area{ + minX: -1, + minY: -1, + maxX: curMaxX, + maxY: curMaxY, + } + + var hasResized bool + if curMaxX != lm.lastX || curMaxY != lm.lastY { + hasResized = true + } + lm.lastX, lm.lastY = curMaxX, curMaxY + + // plan and layout all headers + area, err := lm.layoutHeaders(g, area) + if err != nil { + return err + } + + // plan all footers, don't layout until all columns have been layedout. This is necessary since we must layout from + // top to bottom, but we need the real estate planned for the footers to determine the bottom of the columns. + var footerArea = area + area, footerHeights := lm.planFooters(g, area) + + // plan and layout the main columns + area, err = lm.layoutColumns(g, area) + if err != nil { + return nil + } + + // layout the footers according to the original available area and planned heights + area, err = lm.layoutFooters(g, footerArea, footerHeights) + if err != nil { + return nil + } // notify everyone of a layout change (allow to update and render) if hasResized { - for _, elements := range lm.elements { - for _, element := range elements { - err := element.OnLayoutChange() - if err != nil { - return err - } - } - } + return lm.notifyLayoutChange() } return nil