560 lines
13 KiB
Go
560 lines
13 KiB
Go
// Copyright 2010 The Walk Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// +build windows
|
|
|
|
package walk
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/lxn/win"
|
|
)
|
|
|
|
type Orientation byte
|
|
|
|
const (
|
|
NoOrientation Orientation = 0
|
|
Horizontal = 1 << 0
|
|
Vertical = 1 << 1
|
|
)
|
|
|
|
type BoxLayout struct {
|
|
LayoutBase
|
|
orientation Orientation
|
|
hwnd2StretchFactor map[win.HWND]int
|
|
}
|
|
|
|
func newBoxLayout(orientation Orientation) *BoxLayout {
|
|
l := &BoxLayout{
|
|
LayoutBase: LayoutBase{
|
|
margins96dpi: Margins{9, 9, 9, 9},
|
|
spacing96dpi: 6,
|
|
},
|
|
orientation: orientation,
|
|
hwnd2StretchFactor: make(map[win.HWND]int),
|
|
}
|
|
l.layout = l
|
|
|
|
return l
|
|
}
|
|
|
|
func NewHBoxLayout() *BoxLayout {
|
|
return newBoxLayout(Horizontal)
|
|
}
|
|
|
|
func NewVBoxLayout() *BoxLayout {
|
|
return newBoxLayout(Vertical)
|
|
}
|
|
|
|
func (l *BoxLayout) Orientation() Orientation {
|
|
return l.orientation
|
|
}
|
|
|
|
func (l *BoxLayout) SetOrientation(value Orientation) error {
|
|
if value != l.orientation {
|
|
switch value {
|
|
case Horizontal, Vertical:
|
|
|
|
default:
|
|
return newError("invalid Orientation value")
|
|
}
|
|
|
|
l.orientation = value
|
|
|
|
l.container.RequestLayout()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *BoxLayout) StretchFactor(widget Widget) int {
|
|
if factor, ok := l.hwnd2StretchFactor[widget.Handle()]; ok {
|
|
return factor
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
func (l *BoxLayout) SetStretchFactor(widget Widget, factor int) error {
|
|
if factor != l.StretchFactor(widget) {
|
|
if l.container == nil {
|
|
return newError("container required")
|
|
}
|
|
|
|
handle := widget.Handle()
|
|
|
|
if !l.container.Children().containsHandle(handle) {
|
|
return newError("unknown widget")
|
|
}
|
|
if factor < 1 {
|
|
return newError("factor must be >= 1")
|
|
}
|
|
|
|
l.hwnd2StretchFactor[handle] = factor
|
|
|
|
l.container.RequestLayout()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l *BoxLayout) CreateLayoutItem(ctx *LayoutContext) ContainerLayoutItem {
|
|
li := &boxLayoutItem{
|
|
size2MinSize: make(map[Size]Size),
|
|
orientation: l.orientation,
|
|
hwnd2StretchFactor: make(map[win.HWND]int),
|
|
}
|
|
|
|
for hwnd, sf := range l.hwnd2StretchFactor {
|
|
li.hwnd2StretchFactor[hwnd] = sf
|
|
}
|
|
|
|
return li
|
|
}
|
|
|
|
type boxLayoutItemInfo struct {
|
|
item LayoutItem
|
|
index int
|
|
prefSize int // in native pixels
|
|
minSize int // in native pixels
|
|
maxSize int // in native pixels
|
|
stretch int
|
|
greedy bool
|
|
}
|
|
|
|
type boxLayoutItemInfoList []boxLayoutItemInfo
|
|
|
|
func (l boxLayoutItemInfoList) Len() int {
|
|
return len(l)
|
|
}
|
|
|
|
func (l boxLayoutItemInfoList) Less(i, j int) bool {
|
|
_, iIsSpacer := l[i].item.(*spacerLayoutItem)
|
|
_, jIsSpacer := l[j].item.(*spacerLayoutItem)
|
|
|
|
if l[i].greedy == l[j].greedy {
|
|
if iIsSpacer == jIsSpacer {
|
|
minDiff := l[i].minSize - l[j].minSize
|
|
|
|
if minDiff == 0 {
|
|
return l[i].maxSize/l[i].stretch < l[j].maxSize/l[j].stretch
|
|
}
|
|
|
|
return minDiff > 0
|
|
}
|
|
|
|
return jIsSpacer
|
|
}
|
|
|
|
return l[i].greedy
|
|
}
|
|
|
|
func (l boxLayoutItemInfoList) Swap(i, j int) {
|
|
l[i], l[j] = l[j], l[i]
|
|
}
|
|
|
|
type boxLayoutItem struct {
|
|
ContainerLayoutItemBase
|
|
mutex sync.Mutex
|
|
size2MinSize map[Size]Size // in native pixels
|
|
orientation Orientation
|
|
hwnd2StretchFactor map[win.HWND]int
|
|
}
|
|
|
|
func (li *boxLayoutItem) LayoutFlags() LayoutFlags {
|
|
return boxLayoutFlags(li.orientation, li.children)
|
|
}
|
|
|
|
func (li *boxLayoutItem) IdealSize() Size {
|
|
return li.MinSize()
|
|
}
|
|
|
|
func (li *boxLayoutItem) MinSize() Size {
|
|
return li.MinSizeForSize(li.geometry.ClientSize)
|
|
}
|
|
|
|
func (li *boxLayoutItem) HeightForWidth(width int) int {
|
|
return li.MinSizeForSize(Size{width, li.geometry.ClientSize.Height}).Height
|
|
}
|
|
|
|
func (li *boxLayoutItem) MinSizeForSize(size Size) Size {
|
|
li.mutex.Lock()
|
|
defer li.mutex.Unlock()
|
|
|
|
if min, ok := li.size2MinSize[size]; ok {
|
|
return min
|
|
}
|
|
|
|
bounds := Rectangle{Width: size.Width, Height: size.Height}
|
|
|
|
items := boxLayoutItems(li, itemsToLayout(li.children), li.orientation, li.alignment, bounds, li.margins96dpi, li.spacing96dpi, li.hwnd2StretchFactor)
|
|
|
|
margins := MarginsFrom96DPI(li.margins96dpi, li.ctx.dpi)
|
|
spacing := IntFrom96DPI(li.spacing96dpi, li.ctx.dpi)
|
|
s := Size{margins.HNear + margins.HFar, margins.VNear + margins.VFar}
|
|
|
|
var maxSecondary int
|
|
for _, item := range items {
|
|
min := li.MinSizeEffectiveForChild(item.Item)
|
|
|
|
if hfw, ok := item.Item.(HeightForWidther); ok && hfw.HasHeightForWidth() {
|
|
item.Bounds.Height = hfw.HeightForWidth(item.Bounds.Width)
|
|
} else {
|
|
item.Bounds.Height = min.Height
|
|
}
|
|
item.Bounds.Width = min.Width
|
|
|
|
if li.orientation == Horizontal {
|
|
maxSecondary = maxi(maxSecondary, item.Bounds.Height)
|
|
|
|
s.Width += item.Bounds.Width
|
|
} else {
|
|
maxSecondary = maxi(maxSecondary, item.Bounds.Width)
|
|
|
|
s.Height += item.Bounds.Height
|
|
}
|
|
}
|
|
|
|
if li.orientation == Horizontal {
|
|
s.Width += (len(items) - 1) * spacing
|
|
s.Height += maxSecondary
|
|
} else {
|
|
s.Height += (len(items) - 1) * spacing
|
|
s.Width += maxSecondary
|
|
}
|
|
|
|
if s.Width > 0 && s.Height > 0 {
|
|
li.size2MinSize[size] = s
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (li *boxLayoutItem) PerformLayout() []LayoutResultItem {
|
|
cb := Rectangle{Width: li.geometry.ClientSize.Width, Height: li.geometry.ClientSize.Height}
|
|
return boxLayoutItems(li, itemsToLayout(li.children), li.orientation, li.alignment, cb, li.margins96dpi, li.spacing96dpi, li.hwnd2StretchFactor)
|
|
}
|
|
|
|
func boxLayoutFlags(orientation Orientation, children []LayoutItem) LayoutFlags {
|
|
if len(children) == 0 {
|
|
return ShrinkableHorz | ShrinkableVert | GrowableHorz | GrowableVert
|
|
}
|
|
|
|
var flags LayoutFlags
|
|
for i := 0; i < len(children); i++ {
|
|
item := children[i]
|
|
|
|
if _, ok := item.(*splitterHandleLayoutItem); ok || !shouldLayoutItem(item) {
|
|
continue
|
|
}
|
|
|
|
if s, ok := item.(*spacerLayoutItem); ok {
|
|
if s.greedyLocallyOnly {
|
|
continue
|
|
}
|
|
}
|
|
|
|
f := item.LayoutFlags()
|
|
flags |= f
|
|
}
|
|
|
|
return flags
|
|
}
|
|
|
|
// boxLayoutItems lays out items. bounds parameter is in native pixels.
|
|
func boxLayoutItems(container ContainerLayoutItem, items []LayoutItem, orientation Orientation, alignment Alignment2D, bounds Rectangle, margins96dpi Margins, spacing96dpi int, hwnd2StretchFactor map[win.HWND]int) []LayoutResultItem {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
dpi := container.Context().dpi
|
|
margins := MarginsFrom96DPI(margins96dpi, dpi)
|
|
spacing := IntFrom96DPI(spacing96dpi, dpi)
|
|
|
|
var greedyNonSpacerCount int
|
|
var greedySpacerCount int
|
|
var stretchFactorsTotal [3]int
|
|
stretchFactors := make([]int, len(items))
|
|
var minSizesRemaining int
|
|
minSizes := make([]int, len(items))
|
|
maxSizes := make([]int, len(items))
|
|
sizes := make([]int, len(items))
|
|
prefSizes2 := make([]int, len(items))
|
|
var shrinkableAmount1Total int
|
|
shrinkableAmount1 := make([]int, len(items))
|
|
shrinkable2 := make([]bool, len(items))
|
|
growable2 := make([]bool, len(items))
|
|
sortedItemInfo := boxLayoutItemInfoList(make([]boxLayoutItemInfo, len(items)))
|
|
|
|
for i, item := range items {
|
|
sf := hwnd2StretchFactor[item.Handle()]
|
|
if sf == 0 {
|
|
sf = 1
|
|
}
|
|
stretchFactors[i] = sf
|
|
|
|
geometry := item.Geometry()
|
|
|
|
flags := item.LayoutFlags()
|
|
|
|
max := geometry.MaxSize
|
|
var pref Size
|
|
if hfw, ok := item.(HeightForWidther); !ok || !hfw.HasHeightForWidth() {
|
|
if is, ok := item.(IdealSizer); ok {
|
|
pref = is.IdealSize()
|
|
}
|
|
}
|
|
|
|
if orientation == Horizontal {
|
|
growable2[i] = flags&GrowableVert > 0
|
|
|
|
minSizes[i] = container.MinSizeEffectiveForChild(item).Width
|
|
|
|
if max.Width > 0 {
|
|
maxSizes[i] = max.Width
|
|
} else if pref.Width > 0 && flags&GrowableHorz == 0 {
|
|
maxSizes[i] = pref.Width
|
|
} else {
|
|
maxSizes[i] = 32768
|
|
}
|
|
|
|
prefSizes2[i] = pref.Height
|
|
|
|
sortedItemInfo[i].prefSize = pref.Width
|
|
sortedItemInfo[i].greedy = flags&GreedyHorz > 0
|
|
} else {
|
|
growable2[i] = flags&GrowableHorz > 0
|
|
|
|
if hfw, ok := item.(HeightForWidther); ok && hfw.HasHeightForWidth() {
|
|
minSizes[i] = hfw.HeightForWidth(bounds.Width - margins.HNear - margins.HFar)
|
|
} else {
|
|
minSizes[i] = container.MinSizeEffectiveForChild(item).Height
|
|
}
|
|
|
|
if max.Height > 0 {
|
|
maxSizes[i] = max.Height
|
|
} else if hfw, ok := item.(HeightForWidther); ok && flags&GrowableVert == 0 && hfw.HasHeightForWidth() {
|
|
maxSizes[i] = minSizes[i]
|
|
} else if pref.Height > 0 && flags&GrowableVert == 0 {
|
|
maxSizes[i] = pref.Height
|
|
} else {
|
|
maxSizes[i] = 32768
|
|
}
|
|
|
|
prefSizes2[i] = pref.Width
|
|
|
|
sortedItemInfo[i].prefSize = pref.Height
|
|
sortedItemInfo[i].greedy = flags&GreedyVert > 0
|
|
}
|
|
|
|
sortedItemInfo[i].index = i
|
|
sortedItemInfo[i].minSize = minSizes[i]
|
|
sortedItemInfo[i].maxSize = maxSizes[i]
|
|
sortedItemInfo[i].stretch = sf
|
|
sortedItemInfo[i].item = item
|
|
|
|
if orientation == Horizontal && flags&(ShrinkableHorz|GrowableHorz|GreedyHorz) == ShrinkableHorz ||
|
|
orientation == Vertical && flags&(ShrinkableVert|GrowableVert|GreedyVert) == ShrinkableVert {
|
|
if amount := sortedItemInfo[i].prefSize - minSizes[i]; amount > 0 {
|
|
shrinkableAmount1[i] = amount
|
|
shrinkableAmount1Total += amount
|
|
}
|
|
}
|
|
shrinkable2[i] = orientation == Horizontal && flags&ShrinkableVert != 0 || orientation == Vertical && flags&ShrinkableHorz != 0
|
|
|
|
if shrinkableAmount1[i] > 0 {
|
|
minSizesRemaining += sortedItemInfo[i].prefSize
|
|
} else {
|
|
minSizesRemaining += minSizes[i]
|
|
}
|
|
|
|
if sortedItemInfo[i].greedy {
|
|
if _, isSpacer := item.(*spacerLayoutItem); !isSpacer {
|
|
greedyNonSpacerCount++
|
|
stretchFactorsTotal[0] += sf
|
|
} else {
|
|
greedySpacerCount++
|
|
stretchFactorsTotal[1] += sf
|
|
}
|
|
} else {
|
|
stretchFactorsTotal[2] += sf
|
|
}
|
|
}
|
|
|
|
sort.Stable(sortedItemInfo)
|
|
|
|
var start1, start2, space1, space2 int
|
|
if orientation == Horizontal {
|
|
start1 = bounds.X + margins.HNear
|
|
start2 = bounds.Y + margins.VNear
|
|
space1 = bounds.Width - margins.HNear - margins.HFar
|
|
space2 = bounds.Height - margins.VNear - margins.VFar
|
|
} else {
|
|
start1 = bounds.Y + margins.VNear
|
|
start2 = bounds.X + margins.HNear
|
|
space1 = bounds.Height - margins.VNear - margins.VFar
|
|
space2 = bounds.Width - margins.HNear - margins.HFar
|
|
}
|
|
|
|
spacingRemaining := spacing * (len(items) - 1)
|
|
excess := float64(space1 - minSizesRemaining - spacingRemaining)
|
|
|
|
offsets := [3]int{0, greedyNonSpacerCount, greedyNonSpacerCount + greedySpacerCount}
|
|
counts := [3]int{greedyNonSpacerCount, greedySpacerCount, len(items) - greedyNonSpacerCount - greedySpacerCount}
|
|
|
|
for i := 0; i < 3; i++ {
|
|
stretchFactorsRemaining := stretchFactorsTotal[i]
|
|
|
|
for j := 0; j < counts[i]; j++ {
|
|
info := sortedItemInfo[offsets[i]+j]
|
|
k := info.index
|
|
|
|
stretch := stretchFactors[k]
|
|
min := info.minSize
|
|
max := info.maxSize
|
|
var size int
|
|
var corrected bool
|
|
if shrinkableAmount1[k] > 0 {
|
|
size = info.prefSize
|
|
if excess < 0.0 {
|
|
size -= mini(shrinkableAmount1[k], int(math.Round(-excess/float64(shrinkableAmount1Total)*float64(shrinkableAmount1[k]))))
|
|
corrected = true
|
|
}
|
|
} else {
|
|
size = min
|
|
}
|
|
|
|
if !corrected && min < max {
|
|
excessSpace := float64(space1 - minSizesRemaining - spacingRemaining)
|
|
size += int(math.Round(excessSpace * float64(stretch) / float64(stretchFactorsRemaining)))
|
|
if size < min {
|
|
size = min
|
|
} else if size > max {
|
|
size = max
|
|
}
|
|
}
|
|
|
|
sizes[k] = size
|
|
|
|
if shrinkableAmount1[k] > 0 {
|
|
minSizesRemaining -= info.prefSize
|
|
} else {
|
|
minSizesRemaining -= min
|
|
}
|
|
stretchFactorsRemaining -= stretch
|
|
space1 -= (size + spacing)
|
|
spacingRemaining -= spacing
|
|
}
|
|
}
|
|
|
|
results := make([]LayoutResultItem, 0, len(items))
|
|
|
|
excessTotal := space1 - minSizesRemaining - spacingRemaining
|
|
excessShare := excessTotal / len(items)
|
|
halfExcessShare := excessTotal / (len(items) * 2)
|
|
p1 := start1
|
|
for i, item := range items {
|
|
s1 := sizes[i]
|
|
|
|
var s2 int
|
|
if hfw, ok := item.(HeightForWidther); ok && orientation == Horizontal && hfw.HasHeightForWidth() {
|
|
s2 = hfw.HeightForWidth(s1)
|
|
} else if shrinkable2[i] || growable2[i] {
|
|
s2 = space2
|
|
} else {
|
|
s2 = prefSizes2[i]
|
|
}
|
|
|
|
align := item.Geometry().Alignment
|
|
if align == AlignHVDefault {
|
|
align = alignment
|
|
}
|
|
|
|
var x, y, w, h, p2 int
|
|
if orientation == Horizontal {
|
|
switch align {
|
|
case AlignHNearVNear, AlignHNearVCenter, AlignHNearVFar:
|
|
// nop
|
|
|
|
case AlignHFarVNear, AlignHFarVCenter, AlignHFarVFar:
|
|
p1 += excessShare
|
|
|
|
default:
|
|
p1 += halfExcessShare
|
|
}
|
|
|
|
switch align {
|
|
case AlignHNearVNear, AlignHCenterVNear, AlignHFarVNear:
|
|
p2 = start2
|
|
|
|
case AlignHNearVFar, AlignHCenterVFar, AlignHFarVFar:
|
|
p2 = start2 + space2 - s2
|
|
|
|
default:
|
|
p2 = start2 + (space2-s2)/2
|
|
}
|
|
|
|
x, y, w, h = p1, p2, s1, s2
|
|
} else {
|
|
switch align {
|
|
case AlignHNearVNear, AlignHCenterVNear, AlignHFarVNear:
|
|
// nop
|
|
|
|
case AlignHNearVFar, AlignHCenterVFar, AlignHFarVFar:
|
|
p1 += excessShare
|
|
|
|
default:
|
|
p1 += halfExcessShare
|
|
}
|
|
|
|
switch align {
|
|
case AlignHNearVNear, AlignHNearVCenter, AlignHNearVFar:
|
|
p2 = start2
|
|
|
|
case AlignHFarVNear, AlignHFarVCenter, AlignHFarVFar:
|
|
p2 = start2 + space2 - s2
|
|
|
|
default:
|
|
p2 = start2 + (space2-s2)/2
|
|
}
|
|
|
|
x, y, w, h = p2, p1, s2, s1
|
|
}
|
|
|
|
if orientation == Horizontal {
|
|
switch align {
|
|
case AlignHNearVNear, AlignHNearVCenter, AlignHNearVFar:
|
|
p1 += excessShare
|
|
|
|
case AlignHFarVNear, AlignHFarVCenter, AlignHFarVFar:
|
|
// nop
|
|
|
|
default:
|
|
p1 += halfExcessShare
|
|
}
|
|
|
|
} else {
|
|
switch align {
|
|
case AlignHNearVNear, AlignHCenterVNear, AlignHFarVNear:
|
|
p1 += excessShare
|
|
|
|
case AlignHNearVFar, AlignHCenterVFar, AlignHFarVFar:
|
|
// nop
|
|
|
|
default:
|
|
p1 += halfExcessShare
|
|
}
|
|
}
|
|
|
|
p1 += s1 + spacing
|
|
|
|
results = append(results, LayoutResultItem{Item: item, Bounds: Rectangle{X: x, Y: y, Width: w, Height: h}})
|
|
}
|
|
|
|
return results
|
|
}
|