shithub: mycel

ref: 88687b3cae7a352e9606d775b130b8b0d3d6d9c4
dir: /browser/duitx/box.go/

View raw version
package duitx

// Original code from github.com/mjl-/duit
//
// Copyright 2018 Mechiel Lukkien [email protected]
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import (
	"image"

	"9fans.net/go/draw"
	"github.com/mjl-/duit"
)

// NewBox returns a box containing all uis in its Kids field.
func NewBox(uis ...duit.UI) *Box {
	kids := make([]*duit.Kid, len(uis))
	for i, ui := range uis {
		kids[i] = &duit.Kid{UI: ui}
	}
	return &Box{Kids: kids}
}

// NewReverseBox returns a box containing all uis in original order in its Kids field, with the Reverse field set.
func NewReverseBox(uis ...duit.UI) *Box {
	kids := make([]*duit.Kid, len(uis))
	for i, ui := range uis {
		kids[i] = &duit.Kid{UI: ui}
	}
	return &Box{Kids: kids, Reverse: true}
}

type Display int

const (
	InlineBlock = iota // default
	Block              // always start a new line
	Inline             // flow inline but ignore margin, width and height
	Flex
)

type Dir int

const (
	Row = iota + 1
	Column
)

type Boxable interface {
	Display() Display
	FlexDir() Dir
}

// Box keeps elements on a line as long as they fit, then moves on to the next line.
type Box struct {
	Kids       []*duit.Kid      // Kids and UIs in this box.
	Reverse    bool        // Lay out children from bottom to top. First kid will be at the bottom.
	Margin     duit.Space // In lowDPI pixels, will be adjusted for highDPI screens.
	Padding    duit.Space       // Padding inside box, so children don't touch the sides; in lowDPI pixels, also adjusted for highDPI screens.
	Valign     duit.Valign      // How to align children on a line.
	Width      int         // 0 means dynamic (as much as needed), -1 means full width, >0 means that exact amount of lowDPI pixels.
	Height     int         // 0 means dynamic (as much as needed), -1 means full height, >0 means that exact amount of lowDPI pixels.
	MaxWidth   int         // if >0, the max number of lowDPI pixels that will be used.
	ContentBox bool        // Use ContentBox (BorderBox by default)
	Disp       Display
	Dir        Dir
	Background *draw.Image `json:"-"` // Background for this box, instead of default duit background.

	size image.Point // of entire box, including padding but excluding margin
}

var _ duit.UI = &Box{}
var _ Boxable = &Box{}

func (ui *Box) Display() Display {
	return ui.Disp
}

func (ui *Box) FlexDir() Dir {
	return ui.Dir
}

func (ui *Box) Layout(dui *duit.DUI, self *duit.Kid, sizeAvail image.Point, force bool) {
	debugLayout(dui, self)
	if duit.KidsLayout(dui, self, ui.Kids, force) {
		return
	}

	if ui.Width < 0 && ui.MaxWidth > 0 {
		panic("combination ui.Width < 0 and ui.MaxWidth > 0 invalid")
	}

	padding := dui.ScaleSpace(ui.Padding)
	margin := dui.ScaleSpace(ui.Margin)

	// widths and heights
	bbw := dui.Scale(ui.Width)
	bbmaxw := dui.Scale(ui.MaxWidth)
	bbh := dui.Scale(ui.Height)

	if ui.Disp == Inline {
		bbw = 0
		bbmaxw = 0
		bbh = 0
	}

	if ui.ContentBox {
		bbw += margin.Dx()+padding.Dx()
		bbmaxw += margin.Dx()+padding.Dx()
		bbh += margin.Dy()+padding.Dy()
	}

	osize := sizeAvail
	if ui.Width > 0 && bbw < sizeAvail.X {
		sizeAvail.X = bbw
	} else if ui.MaxWidth > 0 && bbmaxw < sizeAvail.X {
		// note: ui.Width is currently the same as MaxWidth, but that might change when we don't mind extending beyong given X, eg with horizontal scroll
		sizeAvail.X = bbmaxw
	}
	if ui.Height > 0 {
		sizeAvail.Y = bbh
	}
	sizeAvail = sizeAvail.Sub(padding.Size()).Sub(margin.Size())
	nx := 0 // number on current line

	// variables below are about box contents excluding offsets for padding and margin
	cur := image.ZP
	xmax := 0  // max x seen so far
	lineY := 0 // max y of current line

	fixValign := func(kids []*duit.Kid) {
		if len(kids) < 2 {
			return
		}
		for _, k := range kids {
			switch ui.Valign {
			case duit.ValignTop:
			case duit.ValignMiddle:
				k.R = k.R.Add(image.Pt(0, (lineY-k.R.Dy())/2))
			case duit.ValignBottom:
				k.R = k.R.Add(image.Pt(0, lineY-k.R.Dy()))
			}
		}
	}

	for i, k := range ui.Kids {
		k.UI.Layout(dui, k, sizeAvail.Sub(image.Pt(0, cur.Y+lineY)), true)
		childSize := k.R.Size()
		var kr image.Rectangle
		var shouldCol bool
		if ui.Disp == Flex {
			shouldCol = ui.Dir == Column
		} else if display(k) == Block {
			shouldCol = true
		}
		if (nx == 0 || cur.X+childSize.X <= sizeAvail.X) && !shouldCol {
			// Put on same line
			kr = rect(childSize).Add(cur).Add(padding.Topleft())
			cur.X += childSize.X
			lineY = maximum(lineY, childSize.Y)
			nx += 1
		} else {
			// Put on new line
			if nx > 0 {
				fixValign(ui.Kids[i-nx : i])
				cur.X = 0
				cur.Y += lineY + margin.Topleft().Y
			}
			// Add padding translation, so the child UI can be drawn right there
			kr = rect(childSize).Add(cur).Add(padding.Topleft())
			nx = 1
			cur.X = childSize.X
			lineY = childSize.Y
		}
		k.R = kr
		if xmax < cur.X {
			xmax = cur.X
		}
	}
	fixValign(ui.Kids[len(ui.Kids)-nx : len(ui.Kids)])
	cur.Y += lineY

	if ui.Reverse {
		bottomY := cur.Y + padding.Dy()
		for _, k := range ui.Kids {
			y1 := bottomY - k.R.Min.Y
			y0 := y1 - k.R.Dy()
			k.R = image.Rect(k.R.Min.X, y0, k.R.Max.X, y1)
		}
	}

	ui.size = image.Pt(xmax, cur.Y).Add(padding.Size())
	if ui.Width < 0 {
		ui.size.X = osize.X
	}
	if ui.Height < 0 && ui.size.Y < osize.Y {
		ui.size.Y = osize.Y
	}
	self.R = rect(ui.size.Add(margin.Size()))
}

func display(k *duit.Kid) (d Display) {
	if b, ok := k.UI.(Boxable); ok {
		return b.Display()
	}
	return InlineBlock
}

func (ui *Box) Draw(dui *duit.DUI, self *duit.Kid, img *draw.Image, orig image.Point, m draw.Mouse, force bool) {
	margin := dui.ScaleSpace(ui.Margin)
	orig = orig.Add(margin.Topleft())
	duit.KidsDraw(dui, self, ui.Kids, ui.size, ui.Background, img, orig, m, force)
}

func (ui *Box) Mouse(dui *duit.DUI, self *duit.Kid, m draw.Mouse, origM draw.Mouse, orig image.Point) (r duit.Result) {
	margin := dui.ScaleSpace(ui.Margin)
	origM.Point = origM.Point.Sub(margin.Topleft())
	m.Point = m.Point.Sub(margin.Topleft())
	return duit.KidsMouse(dui, self, ui.Kids, m, origM, orig)
}

func (ui *Box) Key(dui *duit.DUI, self *duit.Kid, k rune, m draw.Mouse, orig image.Point) (r duit.Result) {
	// nil check for tests
	if dui != nil {
		margin := dui.ScaleSpace(ui.Margin)
		m.Point = m.Point.Sub(margin.Topleft())
	}
	return duit.KidsKey(dui, self, ui.orderedKids(), k, m, orig)
}

func (ui *Box) orderedKids() []*duit.Kid {
	if !ui.Reverse {
		return ui.Kids
	}
	n := len(ui.Kids)
	kids := make([]*duit.Kid, n)
	for i := range ui.Kids {
		kids[i] = ui.Kids[n-1-i]
	}
	return kids
}

func (ui *Box) FirstFocus(dui *duit.DUI, self *duit.Kid) *image.Point {
	return duit.KidsFirstFocus(dui, self, ui.orderedKids())
}

func (ui *Box) Focus(dui *duit.DUI, self *duit.Kid, o duit.UI) *image.Point {
	return duit.KidsFocus(dui, self, ui.Kids, o)
}

func (ui *Box) Mark(self *duit.Kid, o duit.UI, forLayout bool) (marked bool) {
	return duit.KidsMark(self, ui.Kids, o, forLayout)
}

func (ui *Box) Print(self *duit.Kid, indent int) {
	duit.PrintUI("Box", self, indent)
	duit.KidsPrint(ui.Kids, indent+1)
}