Created
August 9, 2017 18:15
-
-
Save influx6/028af5871740d5e73e6eb0ed07ffe175 to your computer and use it in GitHub Desktop.
Layouts from gomatcha
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Package absolute implements a fixed layout system similar to HTML absolute positioning. It does not support animations or flexible sizing. For more complex layouts, see the constraint package. | |
l := &absolute.Layouter{ | |
Guide: layout.Guide{Frame: layout.Rt(0, 0, 100, 100)} | |
} | |
childView := NewChildView(...) | |
l.Add(childView, layout.Guide{Frame: layout.Rt(10, 10, 90, 90)}) | |
return view.Model{ | |
Views: l.Views(), | |
Layouter:l, | |
} | |
*/ | |
package absolute | |
import ( | |
"gomatcha.io/matcha/comm" | |
"gomatcha.io/matcha/layout" | |
"gomatcha.io/matcha/view" | |
) | |
type Layouter struct { | |
// Layout guide for the view. | |
Guide layout.Guide | |
childGuides []layout.Guide | |
views []view.View | |
} | |
// Add adds v to the layouter and positions it with g. | |
func (l *Layouter) Add(v view.View, g layout.Guide) { | |
l.childGuides = append(l.childGuides, g) | |
l.views = append(l.views, v) | |
} | |
// Views returns all views that have been added to l. | |
func (l *Layouter) Views() []view.View { | |
return l.views | |
} | |
// Layout implements the view.Layouter interface. | |
func (l *Layouter) Layout(ctx *layout.Context) (layout.Guide, []layout.Guide) { | |
// TODO(KD): Need to call layoutChild. | |
for i := 0; i < len(l.childGuides); i++ { | |
g := l.childGuides[i] | |
p := layout.Pt(g.Width(), g.Height()) | |
ctx.LayoutChild(i, p, p) | |
} | |
return l.Guide, l.childGuides | |
} | |
// Notify implements the view.Layouter interface. | |
func (l *Layouter) Notify(f func()) comm.Id { | |
return 0 // no-op | |
} | |
// Unnotify implements the view.Layouter interface. | |
func (l *Layouter) Unnotify(id comm.Id) { | |
// no-op | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Package constraint implements a constraint-based layout system. | |
func (v *View) Build(ctx *view.Context) view.Model { | |
// Create a new constraint system. | |
l := &constraint.Layouter{} | |
// Solves for the position of v, given the constraints on s. The result is a 400x100 frame. | |
l.Solve(func(s *constraint.Solver) { | |
s.Width(400) | |
s.Width(200) // If two constraints conflict, the later one is ignored. | |
s.Height(100) | |
}) | |
// Adds a child view and solves for its position relative to v. The result is a 5x10 frame pinned to the lower right corner of v. | |
child1 := basicview.New() | |
guide1 := l.Add(child1, func(s *constraint.Solver) { | |
s.Width(5) // Left(), Top(), CenterX()... methods support constraining to floats. | |
s.Height(10) | |
s.TopEqual(l.Bottom()) // LeftEqual(), TopLess(), CenterXGreater()... methods support constraining to anchors. | |
s.LeftEqual(l.Right()) | |
}) | |
// Anchors can be manipulated outside of the solver function. | |
verticalCenter := l.CenterX().Add(10) | |
// Adds a child view that is twice as large as child1 and 10 points above the center v. | |
child2 := basicview.New() | |
_ = l.Add(child1, func(s *constraint.Solver) { | |
s.WidthEqual(guide1.Width().Mul(2)) // Anchors can be added to and multiplied by constants. | |
s.HeightEqual(guide1.Height().Mul(2)) | |
s.CenterXEqual(l.CenterX()) | |
s.CenterYEqual(verticalCenter.Add(10)) | |
}) | |
// Recalulates the constraints for child1. | |
guide1.Solve(func(s *constraint.Solver) { | |
s.Width(40) | |
s.Height(30) | |
s.TopEqual(l.Bottom()) // The top and left position must be respecified, even though only the width and height have been updated. | |
s.LeftEqual(l.Right()) | |
}) | |
// Solvers do not run simultaneously! Child2 is still 10x20 since at the time it was added Child1 was 5x10. | |
return view.Model{ | |
Views: l.Views(), | |
Layouter: l, | |
} | |
} | |
If a child view is unconstrained in x or y, it will try to move as close to the center of the parent as possible. | |
If the view is unconstrained in width or height, it will try to match the minGuide as close as possible. | |
*/ | |
package constraint | |
import ( | |
"fmt" | |
"math" | |
"gomatcha.io/matcha/comm" | |
"gomatcha.io/matcha/internal/device" | |
"gomatcha.io/matcha/layout" | |
"gomatcha.io/matcha/view" | |
) | |
type comparison int | |
const ( | |
equal comparison = iota | |
greater | |
less | |
) | |
func (c comparison) String() string { | |
switch c { | |
case equal: | |
return "=" | |
case greater: | |
return ">" | |
case less: | |
return "<" | |
} | |
return "" | |
} | |
type attribute int | |
const ( | |
leftAttr attribute = iota | |
rightAttr | |
topAttr | |
bottomAttr | |
widthAttr | |
heightAttr | |
centerXAttr | |
centerYAttr | |
) | |
func (a attribute) String() string { | |
switch a { | |
case leftAttr: | |
return "Left" | |
case rightAttr: | |
return "Right" | |
case topAttr: | |
return "Top" | |
case bottomAttr: | |
return "Bottom" | |
case widthAttr: | |
return "Width" | |
case heightAttr: | |
return "Height" | |
case centerXAttr: | |
return "CenterX" | |
case centerYAttr: | |
return "CenterY" | |
} | |
return "" | |
} | |
// Anchor represents a float64 value that is materialized during the layout phase. | |
type Anchor struct { | |
anchor anchor | |
} | |
// Add returns a new Anchor that is offset by v. | |
func (a *Anchor) Add(v float64) *Anchor { | |
return &Anchor{ | |
offsetAnchor{ | |
offset: v, | |
underlying: a.anchor, | |
}, | |
} | |
} | |
// Mul returns a new anchor that is multiplied by v. | |
func (a *Anchor) Mul(v float64) *Anchor { | |
return &Anchor{ | |
multiplierAnchor{ | |
multiplier: v, | |
underlying: a.anchor, | |
}, | |
} | |
} | |
type anchor interface { | |
value(*Layouter) float64 | |
} | |
type multiplierAnchor struct { | |
multiplier float64 | |
underlying anchor | |
} | |
func (a multiplierAnchor) value(sys *Layouter) float64 { | |
return a.underlying.value(sys) * a.multiplier | |
} | |
type offsetAnchor struct { | |
offset float64 | |
underlying anchor | |
} | |
func (a offsetAnchor) value(sys *Layouter) float64 { | |
return a.underlying.value(sys) + a.offset | |
} | |
type constAnchor float64 | |
func (a constAnchor) value(sys *Layouter) float64 { | |
return float64(a) | |
} | |
type notifierAnchor struct { | |
n comm.Float64Notifier | |
} | |
func (a notifierAnchor) value(sys *Layouter) float64 { | |
return a.n.Value() | |
} | |
type guideAnchor struct { | |
guide *Guide | |
attribute attribute | |
} | |
func (a guideAnchor) value(sys *Layouter) float64 { | |
var g layout.Guide | |
switch a.guide.index { | |
case rootId: | |
g = *sys.Guide.matchaGuide | |
case minId: | |
g = *sys.min.matchaGuide | |
case maxId: | |
g = *sys.max.matchaGuide | |
default: | |
g = *sys.children2[a.guide.index].matchaGuide | |
} | |
// if g == nil { | |
// return 0 | |
// } | |
switch a.attribute { | |
case leftAttr: | |
return g.Left() | |
case rightAttr: | |
return g.Right() | |
case topAttr: | |
return g.Top() | |
case bottomAttr: | |
return g.Bottom() | |
case widthAttr: | |
return g.Width() | |
case heightAttr: | |
return g.Height() | |
case centerXAttr: | |
return g.CenterX() | |
case centerYAttr: | |
return g.CenterY() | |
} | |
return 0 | |
} | |
// Const returns a new Anchor with a constant value f. | |
func Const(f float64) *Anchor { | |
return &Anchor{constAnchor(f)} | |
} | |
// Notifier returns a new Anchor whose value is equal to n.Value(). | |
func Notifier(n comm.Float64Notifier) *Anchor { | |
return &Anchor{notifierAnchor{n}} | |
} | |
// Guide represents a layout.Guide that is materialized during the layout phase. | |
type Guide struct { | |
index int | |
system *Layouter | |
children2 []*Guide | |
matchaGuide *layout.Guide | |
} | |
// Top returns the minimum Y coordinate as an Anchor. | |
func (g *Guide) Top() *Anchor { | |
return &Anchor{guideAnchor{guide: g, attribute: topAttr}} | |
} | |
// Right returns the maximum X coordinate as an Anchor. | |
func (g *Guide) Right() *Anchor { | |
return &Anchor{guideAnchor{guide: g, attribute: rightAttr}} | |
} | |
// Bottom returns the maximum Y coordinate as an Anchor. | |
func (g *Guide) Bottom() *Anchor { | |
return &Anchor{guideAnchor{guide: g, attribute: bottomAttr}} | |
} | |
// Left returns the minimum X coordinate as an Anchor. | |
func (g *Guide) Left() *Anchor { | |
return &Anchor{guideAnchor{guide: g, attribute: leftAttr}} | |
} | |
// Width returns the width of g as an Anchor. | |
func (g *Guide) Width() *Anchor { | |
return &Anchor{guideAnchor{guide: g, attribute: widthAttr}} | |
} | |
// Height returns the height of g as an Anchor. | |
func (g *Guide) Height() *Anchor { | |
return &Anchor{guideAnchor{guide: g, attribute: heightAttr}} | |
} | |
// CenterX returns the center of g along the X axis as an Anchor. | |
func (g *Guide) CenterX() *Anchor { | |
return &Anchor{guideAnchor{guide: g, attribute: centerXAttr}} | |
} | |
// CenterY returns the center of g along the Y axis as an Anchor. | |
func (g *Guide) CenterY() *Anchor { | |
return &Anchor{guideAnchor{guide: g, attribute: centerYAttr}} | |
} | |
// Solve immediately calls solveFunc to update the constraints for g. | |
func (g *Guide) Solve(solveFunc func(*Solver)) { | |
s := &Solver{index: g.index} | |
if solveFunc != nil { | |
solveFunc(s) | |
} | |
g.system.solvers = append(g.system.solvers, s) | |
// Add any new notifier anchors to our notifier list. | |
for _, i := range s.constraints { | |
if anchor, ok := i.anchor.(notifierAnchor); ok { | |
g.system.notifiers = append(g.system.notifiers, anchor.n) | |
} | |
} | |
} | |
func (g *Guide) add(v view.View, solveFunc func(*Solver)) *Guide { | |
chl := &Guide{ | |
index: len(g.children2), | |
system: g.system, | |
matchaGuide: nil, | |
} | |
s := &Solver{index: chl.index} | |
if solveFunc != nil { | |
solveFunc(s) | |
} | |
g.children2 = append(g.children2, chl) | |
g.system.solvers = append(g.system.solvers, s) | |
g.system.views = append(g.system.views, v) | |
// Add any new notifier anchors to our notifier list. | |
for _, i := range s.constraints { | |
if anchor, ok := i.anchor.(notifierAnchor); ok { | |
g.system.notifiers = append(g.system.notifiers, anchor.n) | |
} | |
} | |
return chl | |
} | |
type constraint struct { | |
attribute attribute | |
comparison comparison | |
anchor anchor | |
} | |
func (c constraint) String() string { | |
return fmt.Sprintf("%v%v%v", c.attribute, c.comparison, c.anchor) | |
} | |
// Solver is a list of constraints to be applied to a view. | |
type Solver struct { | |
debug bool | |
index int | |
constraints []constraint | |
} | |
func (s *Solver) solve(sys *Layouter, ctx *layout.Context) { | |
cr := newConstrainedRect() | |
for _, i := range s.constraints { | |
copy := cr | |
// Generate the range from constraint | |
var r _range | |
switch i.comparison { | |
case equal: | |
r = _range{min: i.anchor.value(sys), max: i.anchor.value(sys)} | |
case greater: | |
r = _range{min: i.anchor.value(sys), max: math.Inf(1)} | |
case less: | |
r = _range{min: math.Inf(-1), max: i.anchor.value(sys)} | |
} | |
// Update the solver | |
switch i.attribute { | |
case leftAttr: | |
copy.left = copy.left.intersect(r) | |
case rightAttr: | |
copy.right = copy.right.intersect(r) | |
case topAttr: | |
copy.top = copy.top.intersect(r) | |
case bottomAttr: | |
copy.bottom = copy.bottom.intersect(r) | |
case widthAttr: | |
copy.width = copy.width.intersect(r) | |
case heightAttr: | |
copy.height = copy.height.intersect(r) | |
case centerXAttr: | |
copy.centerX = copy.centerX.intersect(r) | |
case centerYAttr: | |
copy.centerY = copy.centerY.intersect(r) | |
} | |
// Validate that the new system is well-formed. Otherwise ignore the changes. | |
if !copy.isValid() { | |
if s.debug { | |
fmt.Println("constraint: Debug 0", i, copy) // TODO(KD): Better debugging. | |
} | |
continue | |
} | |
cr = copy | |
} | |
if s.debug { | |
fmt.Println("constraint: Debug 1", cr, s.constraints) | |
} | |
// Get parent guide. | |
var parent layout.Guide | |
if s.index == rootId { | |
parent = *sys.min.matchaGuide | |
} else { | |
parent = *sys.Guide.matchaGuide | |
} | |
// Solve for width & height. | |
var width, height float64 | |
var g layout.Guide | |
if s.index == rootId { | |
g = layout.Guide{} | |
width, _ = cr.solveWidth(parent.Width()) | |
height, _ = cr.solveHeight(parent.Height()) | |
} else { | |
// Update the width and height ranges based on other constraints. | |
_, cr = cr.solveWidth(0) | |
_, cr = cr.solveHeight(0) | |
g = ctx.LayoutChild(s.index, layout.Pt(cr.width.min, cr.height.min), layout.Pt(cr.width.max, cr.height.max)) | |
width = g.Width() | |
height = g.Height() | |
// Round width and height to screen scale | |
width = math.Floor(width*device.ScreenScale+0.5) / device.ScreenScale | |
if width < cr.width.min || height < cr.height.min || width > cr.width.max || height > cr.height.max { | |
// fmt.Printf("constraint: child guide is outside of bounds. Min:%v Max:%v Actual:%v\n", layout.Pt(cr.width.min, cr.height.min), layout.Pt(cr.width.max, cr.height.max), layout.Pt(width, height)) | |
width = cr.width.min | |
height = cr.height.min | |
} | |
} | |
// Solve for centerX & centerY using new width & height. | |
cr.width = cr.width.intersect(_range{min: width, max: width}) | |
cr.height = cr.height.intersect(_range{min: height, max: height}) | |
if !cr.isValid() { | |
panic("constraint: system inconsistency") | |
} | |
var centerX, centerY float64 | |
if s.index == rootId { | |
centerX = width / 2 | |
centerY = height / 2 | |
} else { | |
centerX, _ = cr.solveCenterX(parent.CenterX()) | |
centerY, _ = cr.solveCenterY(parent.CenterY()) | |
} | |
// Set zIndex | |
g.ZIndex = sys.zIndex | |
sys.zIndex += 1 | |
// Update the guide and the system. | |
g.Frame = layout.Rt(centerX-width/2, centerY-height/2, centerX+width/2, centerY+height/2) | |
if s.index == rootId { | |
sys.Guide.matchaGuide = &g | |
} else { | |
sys.Guide.children2[s.index].matchaGuide = &g | |
} | |
if s.debug { | |
fmt.Println("constraint: Debug 2", g) | |
} | |
} | |
// Debug adds debug logging for the solver. | |
func (s *Solver) Debug() { | |
s.debug = true | |
} | |
func (s *Solver) Top(v float64) { | |
s.TopEqual(Const(v)) | |
} | |
func (s *Solver) TopEqual(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: topAttr, comparison: equal, anchor: a.anchor}) | |
} | |
func (s *Solver) TopLess(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: topAttr, comparison: less, anchor: a.anchor}) | |
} | |
func (s *Solver) TopGreater(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: topAttr, comparison: greater, anchor: a.anchor}) | |
} | |
func (s *Solver) Right(v float64) { | |
s.RightEqual(Const(v)) | |
} | |
func (s *Solver) RightEqual(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: rightAttr, comparison: equal, anchor: a.anchor}) | |
} | |
func (s *Solver) RightLess(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: rightAttr, comparison: less, anchor: a.anchor}) | |
} | |
func (s *Solver) RightGreater(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: rightAttr, comparison: greater, anchor: a.anchor}) | |
} | |
func (s *Solver) Bottom(v float64) { | |
s.BottomEqual(Const(v)) | |
} | |
func (s *Solver) BottomEqual(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: bottomAttr, comparison: equal, anchor: a.anchor}) | |
} | |
func (s *Solver) BottomLess(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: bottomAttr, comparison: less, anchor: a.anchor}) | |
} | |
func (s *Solver) BottomGreater(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: bottomAttr, comparison: greater, anchor: a.anchor}) | |
} | |
func (s *Solver) Left(v float64) { | |
s.LeftEqual(Const(v)) | |
} | |
func (s *Solver) LeftEqual(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: leftAttr, comparison: equal, anchor: a.anchor}) | |
} | |
func (s *Solver) LeftLess(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: leftAttr, comparison: less, anchor: a.anchor}) | |
} | |
func (s *Solver) LeftGreater(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: leftAttr, comparison: greater, anchor: a.anchor}) | |
} | |
func (s *Solver) Width(v float64) { | |
s.WidthEqual(Const(v)) | |
} | |
func (s *Solver) WidthEqual(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: widthAttr, comparison: equal, anchor: a.anchor}) | |
} | |
func (s *Solver) WidthLess(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: widthAttr, comparison: less, anchor: a.anchor}) | |
} | |
func (s *Solver) WidthGreater(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: widthAttr, comparison: greater, anchor: a.anchor}) | |
} | |
func (s *Solver) Height(v float64) { | |
s.HeightEqual(Const(v)) | |
} | |
func (s *Solver) HeightEqual(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: heightAttr, comparison: equal, anchor: a.anchor}) | |
} | |
func (s *Solver) HeightLess(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: heightAttr, comparison: less, anchor: a.anchor}) | |
} | |
func (s *Solver) HeightGreater(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: heightAttr, comparison: greater, anchor: a.anchor}) | |
} | |
func (s *Solver) CenterX(v float64) { | |
s.CenterXEqual(Const(v)) | |
} | |
func (s *Solver) CenterXEqual(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: centerXAttr, comparison: equal, anchor: a.anchor}) | |
} | |
func (s *Solver) CenterXLess(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: centerXAttr, comparison: less, anchor: a.anchor}) | |
} | |
func (s *Solver) CenterXGreater(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: centerXAttr, comparison: greater, anchor: a.anchor}) | |
} | |
func (s *Solver) CenterY(v float64) { | |
s.CenterYEqual(Const(v)) | |
} | |
func (s *Solver) CenterYEqual(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: centerYAttr, comparison: equal, anchor: a.anchor}) | |
} | |
func (s *Solver) CenterYLess(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: centerYAttr, comparison: less, anchor: a.anchor}) | |
} | |
func (s *Solver) CenterYGreater(a *Anchor) { | |
s.constraints = append(s.constraints, constraint{attribute: centerYAttr, comparison: greater, anchor: a.anchor}) | |
} | |
func (s *Solver) String() string { | |
return fmt.Sprintf("Solver{%v, %v}", s.index, s.constraints) | |
} | |
type systemId int | |
const ( | |
rootId int = -1 | |
minId int = -2 | |
maxId int = -3 | |
) | |
type Layouter struct { | |
// Guide represents the size of the view that the layouter is attached to. By default, Guide is the same size as MinGuide. | |
Guide | |
min Guide | |
max Guide | |
solvers []*Solver | |
zIndex int | |
notifiers []comm.Notifier | |
groupNotifiers map[comm.Id]notifier | |
maxId comm.Id | |
views []view.View | |
} | |
func (l *Layouter) initialize() { | |
if l.groupNotifiers == nil { | |
l.Guide = Guide{index: rootId, system: l} | |
l.min = Guide{index: minId, system: l} | |
l.max = Guide{index: maxId, system: l} | |
l.groupNotifiers = map[comm.Id]notifier{} | |
} | |
} | |
// View returns a list of all views added to l. | |
func (l *Layouter) Views() []view.View { | |
return l.views | |
} | |
// MinGuide returns a guide representing the smallest allowed size for the view. | |
func (l *Layouter) MinGuide() *Guide { | |
l.initialize() | |
return &l.min | |
} | |
// MaxGuide returns a guide representing the largest allowed size for the view. | |
func (l *Layouter) MaxGuide() *Guide { | |
l.initialize() | |
return &l.max | |
} | |
// Layout evaluates the constraints and returns the calculated guide and child guides. | |
func (l *Layouter) Layout(ctx *layout.Context) (layout.Guide, []layout.Guide) { | |
l.initialize() | |
l.min.matchaGuide = &layout.Guide{ | |
Frame: layout.Rt(0, 0, ctx.MinSize.X, ctx.MinSize.Y), | |
} | |
l.max.matchaGuide = &layout.Guide{ | |
Frame: layout.Rt(0, 0, ctx.MaxSize.X, ctx.MaxSize.Y), | |
} | |
l.Guide.matchaGuide = &layout.Guide{ | |
Frame: layout.Rt(0, 0, ctx.MinSize.X, ctx.MinSize.Y), | |
} | |
// TODO(KD): reset all guides | |
for _, i := range l.solvers { | |
i.solve(l, ctx) | |
} | |
g := *l.Guide.matchaGuide | |
gs := []layout.Guide{} | |
for _, i := range l.Guide.children2 { | |
gs = append(gs, *i.matchaGuide) | |
} | |
return g, gs | |
} | |
// Add immediately calls solveFunc to generate the constraints for v. These constraints are solved by l during the layout phase. | |
// A corresponding guide is returned, which can be used to position other views or reposition v. If the view is not fully constrained | |
// it will try to match the MinGuide in dimension and center. If the child view is not fully constrained it will try to match the parent in center. | |
func (l *Layouter) Add(v view.View, solveFunc func(*Solver)) *Guide { | |
l.initialize() | |
return l.Guide.add(v, solveFunc) | |
} | |
func (l *Layouter) Solve(solveFunc func(*Solver)) { | |
l.initialize() | |
l.Guide.Solve(solveFunc) | |
} | |
type notifier struct { | |
notifier *comm.Relay | |
id comm.Id | |
} | |
// Notify calls f anytime a Notifier anchor changes. Other updates to the layouter, such as adding guides will not trigger the notification. | |
func (l *Layouter) Notify(f func()) comm.Id { | |
if len(l.notifiers) == 0 { | |
return 0 | |
} | |
n := &comm.Relay{} | |
for _, i := range l.notifiers { | |
n.Subscribe(i) | |
} | |
l.maxId += 1 | |
l.groupNotifiers[l.maxId] = notifier{ | |
notifier: n, | |
id: n.Notify(f), | |
} | |
return l.maxId | |
} | |
// Unnotify stops notifications for id. | |
func (l *Layouter) Unnotify(id comm.Id) { | |
n, ok := l.groupNotifiers[id] | |
if ok { | |
n.notifier.Unnotify(n.id) | |
delete(l.groupNotifiers, id) | |
} | |
} | |
type _range struct { | |
min float64 | |
max float64 | |
} | |
func (r _range) intersectMin(v float64) _range { | |
r.min = math.Max(r.min, v) | |
return r | |
} | |
func (r _range) intersectMax(v float64) _range { | |
r.max = math.Min(r.max, v) | |
return r | |
} | |
func (r _range) intersect(r2 _range) _range { | |
return _range{min: math.Max(r.min, r2.min), max: math.Min(r.max, r2.max)} | |
} | |
func (r _range) isValid() bool { | |
if r.max < r.min { | |
fmt.Println("invalid2", r.max-r.min) | |
} | |
return r.max >= r.min | |
} | |
func (r _range) nearest(v float64) float64 { | |
// return a sane value even if range is invalid | |
if r.max < r.min { | |
r.max, r.min = r.min, r.max | |
} | |
switch { | |
case r.min == r.max: | |
return r.min | |
case r.min >= v: | |
return r.min | |
case r.max <= v: | |
return r.max | |
default: | |
return v | |
} | |
} | |
type constrainedRect struct { | |
left, right, top, bottom, width, height, centerX, centerY _range | |
} | |
func newConstrainedRect() constrainedRect { | |
all := _range{min: math.Inf(-1), max: math.Inf(1)} | |
pos := _range{min: 0, max: math.Inf(1)} | |
return constrainedRect{ | |
left: all, right: all, top: all, bottom: all, width: pos, height: pos, centerX: all, centerY: all, | |
} | |
} | |
func (cr constrainedRect) isValid() bool { | |
_, r1 := cr.solveWidth(0) | |
_, r2 := cr.solveHeight(0) | |
_, r3 := cr.solveCenterX(0) | |
_, r4 := cr.solveCenterY(0) | |
return r1.width.isValid() && r2.height.isValid() && r3.centerX.isValid() && r4.centerY.isValid() | |
} | |
func (r constrainedRect) solveWidth(b float64) (float64, constrainedRect) { | |
centerXMax, centerXMin := r.centerX.max, r.centerX.min | |
rightMax, rightMin := r.right.max, r.right.min | |
leftMax, leftMin := r.left.max, r.left.min | |
// Width = (Right - CenterX) * 2 | |
if !math.IsInf(centerXMin, 0) && !math.IsInf(rightMax, 0) { | |
r.width = r.width.intersectMax((rightMax - centerXMin) * 2) | |
} | |
if !math.IsInf(centerXMax, 0) && !math.IsInf(rightMin, 0) { | |
r.width = r.width.intersectMin((rightMin - centerXMax) * 2) | |
} | |
// Width = Right - Left | |
if !math.IsInf(rightMax, 0) && !math.IsInf(leftMin, 0) { | |
r.width = r.width.intersectMax(rightMax - leftMin) | |
} | |
if !math.IsInf(rightMin, 0) && !math.IsInf(leftMax, 0) { | |
r.width = r.width.intersectMin(rightMin - leftMax) | |
} | |
// Width = (CenterX - Left) * 2 | |
if !math.IsInf(centerXMax, 0) && !math.IsInf(leftMin, 0) { | |
r.width = r.width.intersectMax((centerXMax - leftMin) * 2) | |
} | |
if !math.IsInf(centerXMin, 0) && !math.IsInf(leftMax, 0) { | |
r.width = r.width.intersectMin((centerXMin - leftMax) * 2) | |
} | |
return r.width.nearest(b), r | |
} | |
func (r constrainedRect) solveCenterX(b float64) (float64, constrainedRect) { | |
rightMax, rightMin := r.right.max, r.right.min | |
leftMax, leftMin := r.left.max, r.left.min | |
widthMax, widthMin := r.width.max, r.width.min | |
// CenterX = (Right + Left)/2 | |
if !math.IsInf(rightMax, 0) && !math.IsInf(leftMax, 0) { | |
r.centerX = r.centerX.intersectMax((rightMax + leftMax) / 2) | |
} | |
if !math.IsInf(rightMin, 0) && !math.IsInf(leftMin, 0) { | |
r.centerX = r.centerX.intersectMin((rightMin + leftMin) / 2) | |
} | |
// CenterX = Right - Width / 2 | |
if !math.IsInf(rightMax, 0) && !math.IsInf(widthMin, 0) { | |
r.centerX = r.centerX.intersectMax(rightMax - widthMin/2) | |
} | |
if !math.IsInf(rightMin, 0) && !math.IsInf(widthMax, 0) { | |
r.centerX = r.centerX.intersectMin(rightMin - widthMax/2) | |
} | |
// CenterX = Left + Width / 2 | |
if !math.IsInf(leftMax, 0) && !math.IsInf(widthMax, 0) { | |
r.centerX = r.centerX.intersectMax(leftMax + widthMax/2) | |
} | |
if !math.IsInf(leftMin, 0) && !math.IsInf(widthMin, 0) { | |
r.centerX = r.centerX.intersectMin(leftMin + widthMin/2) | |
} | |
return r.centerX.nearest(b), r | |
} | |
func (r constrainedRect) solveHeight(b float64) (float64, constrainedRect) { | |
centerYMax, centerYMin := r.centerY.max, r.centerY.min | |
bottomMax, bottomMin := r.bottom.max, r.bottom.min | |
topMax, topMin := r.top.max, r.top.min | |
// height = (bottom - centerY) * 2 | |
if !math.IsInf(centerYMin, 0) && !math.IsInf(bottomMax, 0) { | |
r.height = r.height.intersectMax((bottomMax - centerYMin) * 2) | |
} | |
if !math.IsInf(centerYMax, 0) && !math.IsInf(bottomMin, 0) { | |
r.height = r.height.intersectMin((bottomMin - centerYMax) * 2) | |
} | |
// height = bottom - top | |
if !math.IsInf(bottomMax, 0) && !math.IsInf(topMin, 0) { | |
r.height = r.height.intersectMax(bottomMax - topMin) | |
} | |
if !math.IsInf(bottomMin, 0) && !math.IsInf(topMax, 0) { | |
r.height = r.height.intersectMin(bottomMin - topMax) | |
} | |
// height = (centerY - top) * 2 | |
if !math.IsInf(centerYMax, 0) && !math.IsInf(topMin, 0) { | |
r.height = r.height.intersectMax((centerYMax - topMin) * 2) | |
} | |
if !math.IsInf(centerYMin, 0) && !math.IsInf(topMax, 0) { | |
r.height = r.height.intersectMin((centerYMin - topMax) * 2) | |
} | |
return r.height.nearest(b), r | |
} | |
func (r constrainedRect) solveCenterY(b float64) (float64, constrainedRect) { | |
bottomMax, bottomMin := r.bottom.max, r.bottom.min | |
topMax, topMin := r.top.max, r.top.min | |
heightMax, heightMin := r.height.max, r.height.min | |
// centerY = (bottom + top)/2 | |
if !math.IsInf(bottomMax, 0) && !math.IsInf(topMax, 0) { | |
r.centerY = r.centerY.intersectMax((bottomMax + topMax) / 2) | |
} | |
if !math.IsInf(bottomMin, 0) && !math.IsInf(topMin, 0) { | |
r.centerY = r.centerY.intersectMin((bottomMin + topMin) / 2) | |
} | |
// centerY = bottom - height / 2 | |
if !math.IsInf(bottomMax, 0) && !math.IsInf(heightMin, 0) { | |
r.centerY = r.centerY.intersectMax(bottomMax - heightMin/2) | |
} | |
if !math.IsInf(bottomMin, 0) && !math.IsInf(heightMax, 0) { | |
r.centerY = r.centerY.intersectMin(bottomMin - heightMax/2) | |
} | |
// centerY = top + height / 2 | |
if !math.IsInf(topMax, 0) && !math.IsInf(heightMax, 0) { | |
r.centerY = r.centerY.intersectMax(topMax + heightMax/2) | |
} | |
if !math.IsInf(topMin, 0) && !math.IsInf(heightMin, 0) { | |
r.centerY = r.centerY.intersectMin(topMin + heightMin/2) | |
} | |
return r.centerY.nearest(b), r | |
} | |
func (r constrainedRect) String() string { | |
return fmt.Sprintf("{left:%v, right:%v, top:%v, bottom:%v, width:%v, height:%v, centerX:%v, centerY:%v}", r.left, r.right, r.top, r.bottom, r.width, r.height, r.centerX, r.centerY) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ( | |
"math" | |
"testing" | |
) | |
func TestConstrainedRect(t *testing.T) { | |
cr := newConstrainedRect() | |
if !cr.isValid() { | |
t.Error("NewConstrainedRect is invalid") | |
} | |
} | |
func TestIntersect(t *testing.T) { | |
r := _range{0, 10} | |
if n := r.intersectMin(math.Inf(-1)); n != (_range{0, 10}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersectMin(-5); n != (_range{0, 10}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersectMin(0); n != (_range{0, 10}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersectMin(5); n != (_range{5, 10}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersectMin(15); n != (_range{15, 10}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersectMax(-5); n != (_range{0, -5}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersectMax(5); n != (_range{0, 5}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersectMax(10); n != (_range{0, 10}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersectMax(15); n != (_range{0, 10}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersectMax(math.Inf(1)); n != (_range{0, 10}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersect(_range{-5, -5}); n != (_range{0, -5}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersect(_range{-5, 0}); n != (_range{0, 0}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersect(_range{-5, 5}); n != (_range{0, 5}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersect(_range{-5, 10}); n != (_range{0, 10}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersect(_range{-5, 15}); n != (_range{0, 10}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
if n := r.intersect(_range{0, -5}); n != (_range{0, -5}) { | |
t.Errorf("Incorrect result: %v", n) | |
} | |
} | |
func TestIsValid(t *testing.T) { | |
if b := (_range{0, 10}).isValid(); !b { | |
t.Errorf("Incorrect result: %v", b) | |
} | |
if b := (_range{0, 0}).isValid(); !b { | |
t.Errorf("Incorrect result: %v", b) | |
} | |
if b := (_range{math.Inf(1), math.Inf(1)}).isValid(); !b { | |
t.Errorf("Incorrect result: %v", b) | |
} | |
if b := (_range{math.Inf(-1), math.Inf(1)}).isValid(); !b { | |
t.Errorf("Incorrect result: %v", b) | |
} | |
if b := (_range{math.Inf(1), math.Inf(-1)}).isValid(); b { | |
t.Errorf("Incorrect result: %v", b) | |
} | |
if b := (_range{10, 0}).isValid(); b { | |
t.Errorf("Incorrect result: %v", b) | |
} | |
} | |
func TestNearest(t *testing.T) { | |
r := _range{0, 10} | |
if n := r.nearest(100); n != 10 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
if n := r.nearest(10); n != 10 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
if n := r.nearest(math.Inf(1)); n != 10 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
if n := r.nearest(8); n != 8 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
if n := r.nearest(-10); n != 0 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
if n := r.nearest(math.Inf(-1)); n != 0 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
// Reversed range. | |
r = _range{10, 0} | |
if n := r.nearest(100); n != 10 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
if n := r.nearest(10); n != 10 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
if n := r.nearest(math.Inf(1)); n != 10 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
if n := r.nearest(8); n != 8 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
if n := r.nearest(-10); n != 0 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
if n := r.nearest(math.Inf(-1)); n != 0 { | |
t.Errorf("Incorrect nearest: %v", n) | |
} | |
} | |
func TestSolveWidth(t *testing.T) { | |
cr := newConstrainedRect() | |
if w, ok := cr.solveWidth(10); w != 10 || !ok.isValid() { | |
t.Errorf("Incorrect solution: (%v, %v)", w, ok) | |
} | |
cr = newConstrainedRect() | |
if w, ok := cr.solveWidth(-10); w != 0 || !ok.isValid() { | |
t.Errorf("Incorrect solution: (%v, %v)", w, ok) | |
} | |
cr = newConstrainedRect() | |
cr.width = _range{0, 10} | |
if w, ok := cr.solveWidth(5); w != 5 || !ok.isValid() { | |
t.Errorf("Incorrect solution: (%v, %v)", w, ok) | |
} | |
cr = newConstrainedRect() | |
cr.width = _range{0, 10} | |
cr.centerX = _range{0, 10} | |
if w, ok := cr.solveWidth(5); w != 5 || !ok.isValid() { | |
t.Errorf("Incorrect solution: (%v, %v)", w, ok) | |
} | |
cr = newConstrainedRect() | |
cr.width = _range{0, 10} | |
cr.centerX = _range{3, 3} | |
cr.left = _range{0, 0} | |
if w, ok := cr.solveWidth(6); w != 6 || !ok.isValid() { | |
t.Errorf("Incorrect solution: (%v, %v)", w, ok) | |
} | |
cr = newConstrainedRect() | |
cr.width = _range{0, 10} | |
cr.right = _range{4, 10} | |
cr.left = _range{-5, 0} | |
if w, ok := cr.solveWidth(2); w != 4 || !ok.isValid() { | |
t.Errorf("Incorrect solution: (%v, %v)", w, ok) | |
} | |
cr = newConstrainedRect() | |
cr.width = _range{0, 10} | |
cr.right = _range{-5, 0} | |
cr.left = _range{-5, 0} | |
if w, ok := cr.solveWidth(15); w != 5 || !ok.isValid() { | |
t.Errorf("Incorrect solution: (%v, %v)", w, ok) | |
} | |
cr = newConstrainedRect() | |
cr.width = _range{0, 10} | |
cr.right = _range{-5, 0} | |
cr.left = _range{-5, 0} | |
cr.centerX = _range{-4, -3} | |
if w, ok := cr.solveWidth(15); w != 4 || !ok.isValid() { | |
t.Errorf("Incorrect solution: (%v, %v)", w, ok) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ( | |
"gomatcha.io/matcha/comm" | |
"gomatcha.io/matcha/layout" | |
) | |
type Layouter struct { | |
} | |
// Layout implements the view.Layouter interface. | |
func (l *Layouter) Layout(ctx *layout.Context) (layout.Guide, []layout.Guide) { | |
g := layout.Guide{Frame: layout.Rect{Max: ctx.MinSize}} | |
gs := []layout.Guide{} | |
for i := 0; i < ctx.ChildCount; i++ { | |
gs = append(gs, ctx.LayoutChild(i, ctx.MinSize, ctx.MinSize)) | |
} | |
return g, gs | |
} | |
// Notify implements the view.Layouter interface. | |
func (l *Layouter) Notify(f func()) comm.Id { | |
return 0 // no-op | |
} | |
// Unnotify implements the view.Layouter interface. | |
func (l *Layouter) Unnotify(id comm.Id) { | |
// no-op | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package layout | |
import ( | |
"fmt" | |
"gomatcha.io/matcha/comm" | |
pblayout "gomatcha.io/matcha/pb/layout" | |
) | |
// Rect represents a 2D rectangle with the top left corner at Min and the bottom | |
// right corner at Max. | |
type Rect struct { | |
Min, Max Point | |
} | |
// Rt creates a rectangle with the min at X0, Y0 and the max at X1, Y1. | |
func Rt(x0, y0, x1, y1 float64) Rect { | |
return Rect{Min: Point{X: x0, Y: y0}, Max: Point{X: x1, Y: y1}} | |
} | |
// MarshalProtobuf serializes r into a protobuf object. | |
func (r *Rect) MarshalProtobuf() *pblayout.Rect { | |
return &pblayout.Rect{ | |
Min: r.Min.MarshalProtobuf(), | |
Max: r.Max.MarshalProtobuf(), | |
} | |
} | |
// UnmarshalProtobuf deserializes r from a protobuf object. | |
func (r *Rect) UnmarshalProtobuf(pbrect *pblayout.Rect) { | |
r.Min.UnmarshalProtobuf(pbrect.Min) | |
r.Max.UnmarshalProtobuf(pbrect.Max) | |
} | |
// Add translates rect r by p. | |
func (r Rect) Add(p Point) Rect { | |
n := r | |
n.Min.X += p.X | |
n.Min.Y += p.Y | |
n.Max.X += p.X | |
n.Max.Y += p.Y | |
return n | |
} | |
// String returns a string description of r. | |
func (r Rect) String() string { | |
return fmt.Sprintf("Rect{%v, %v, %v, %v}", r.Min.X, r.Min.Y, r.Max.X, r.Max.Y) | |
} | |
// Point represents a point on the XY coordinate system. | |
type Point struct { | |
X float64 | |
Y float64 | |
} | |
// Pt creates a point with x and y. | |
func Pt(x, y float64) Point { | |
return Point{X: x, Y: y} | |
} | |
// MarshalProtobuf serializes p into a protobuf object. | |
func (p *Point) MarshalProtobuf() *pblayout.Point { | |
return &pblayout.Point{ | |
X: p.X, | |
Y: p.Y, | |
} | |
} | |
// UnmarshalProtobuf deserializes p from a protobuf object. | |
func (p *Point) UnmarshalProtobuf(pbpoint *pblayout.Point) { | |
p.X = pbpoint.X | |
p.Y = pbpoint.Y | |
} | |
// PointNotifier wraps the comm.Notifier interface with an additional Value() method which returns a Point. | |
type PointNotifier interface { | |
comm.Notifier | |
Value() Point | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Code generated by protoc-gen-go. DO NOT EDIT. | |
// source: gomatcha.io/matcha/pb/layout/layout.proto | |
/* | |
Package layout is a generated protocol buffer package. | |
It is generated from these files: | |
gomatcha.io/matcha/pb/layout/layout.proto | |
It has these top-level messages: | |
Point | |
Rect | |
Insets | |
Guide | |
*/ | |
package layout | |
import proto "github.com/golang/protobuf/proto" | |
import fmt "fmt" | |
import math "math" | |
// Reference imports to suppress errors if they are not otherwise used. | |
var _ = proto.Marshal | |
var _ = fmt.Errorf | |
var _ = math.Inf | |
// This is a compile-time assertion to ensure that this generated file | |
// is compatible with the proto package it is being compiled against. | |
// A compilation error at this line likely means your copy of the | |
// proto package needs to be updated. | |
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package | |
type Point struct { | |
X float64 `protobuf:"fixed64,1,opt,name=x" json:"x,omitempty"` | |
Y float64 `protobuf:"fixed64,2,opt,name=y" json:"y,omitempty"` | |
} | |
func (m *Point) Reset() { *m = Point{} } | |
func (m *Point) String() string { return proto.CompactTextString(m) } | |
func (*Point) ProtoMessage() {} | |
func (*Point) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } | |
func (m *Point) GetX() float64 { | |
if m != nil { | |
return m.X | |
} | |
return 0 | |
} | |
func (m *Point) GetY() float64 { | |
if m != nil { | |
return m.Y | |
} | |
return 0 | |
} | |
type Rect struct { | |
Min *Point `protobuf:"bytes,1,opt,name=min" json:"min,omitempty"` | |
Max *Point `protobuf:"bytes,2,opt,name=max" json:"max,omitempty"` | |
} | |
func (m *Rect) Reset() { *m = Rect{} } | |
func (m *Rect) String() string { return proto.CompactTextString(m) } | |
func (*Rect) ProtoMessage() {} | |
func (*Rect) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } | |
func (m *Rect) GetMin() *Point { | |
if m != nil { | |
return m.Min | |
} | |
return nil | |
} | |
func (m *Rect) GetMax() *Point { | |
if m != nil { | |
return m.Max | |
} | |
return nil | |
} | |
type Insets struct { | |
Top float64 `protobuf:"fixed64,1,opt,name=top" json:"top,omitempty"` | |
Left float64 `protobuf:"fixed64,2,opt,name=left" json:"left,omitempty"` | |
Bottom float64 `protobuf:"fixed64,3,opt,name=bottom" json:"bottom,omitempty"` | |
Right float64 `protobuf:"fixed64,4,opt,name=right" json:"right,omitempty"` | |
} | |
func (m *Insets) Reset() { *m = Insets{} } | |
func (m *Insets) String() string { return proto.CompactTextString(m) } | |
func (*Insets) ProtoMessage() {} | |
func (*Insets) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } | |
func (m *Insets) GetTop() float64 { | |
if m != nil { | |
return m.Top | |
} | |
return 0 | |
} | |
func (m *Insets) GetLeft() float64 { | |
if m != nil { | |
return m.Left | |
} | |
return 0 | |
} | |
func (m *Insets) GetBottom() float64 { | |
if m != nil { | |
return m.Bottom | |
} | |
return 0 | |
} | |
func (m *Insets) GetRight() float64 { | |
if m != nil { | |
return m.Right | |
} | |
return 0 | |
} | |
type Guide struct { | |
Frame *Rect `protobuf:"bytes,1,opt,name=frame" json:"frame,omitempty"` | |
ZIndex int64 `protobuf:"varint,3,opt,name=zIndex" json:"zIndex,omitempty"` | |
} | |
func (m *Guide) Reset() { *m = Guide{} } | |
func (m *Guide) String() string { return proto.CompactTextString(m) } | |
func (*Guide) ProtoMessage() {} | |
func (*Guide) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } | |
func (m *Guide) GetFrame() *Rect { | |
if m != nil { | |
return m.Frame | |
} | |
return nil | |
} | |
func (m *Guide) GetZIndex() int64 { | |
if m != nil { | |
return m.ZIndex | |
} | |
return 0 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Package layout provides geometric primitives and interfaces for view layout. | |
Layouter | |
While view.View handles the rendering of a component, the actual positioning of the | |
views is delegated to the Layouter interface. Each view can specify it's Layouter | |
in the view.Model returned by Build(). If no layouter is given, all of | |
its children will be positioned to size of its parent view. | |
Understanding the details of this is not too important for most day to day development. | |
For the most part you will be using predefined Layouters such as the one provided by | |
the constraint package. | |
Layout occurs in a separate pass after the view hierarchy has been built. Like view.Build(), | |
each layouter is only responsible for returning its own frame and the frame of its direct descendents. | |
To determine the correct sizing for a child, the layouter will call *Context.LayoutChild() | |
passing a minSize and a maxSize. The child will return a desired size within the | |
min and max, which the parent can then position. Here is an example Layout function | |
that centers its children within itself. | |
func (l *Layouter) Layout(ctx *layout.Context) (layout.Guide, []layout.Guide) { | |
// Specify that the view wants to be the minSize given by its parent. | |
g := layout.Guide{ | |
Frame: layout.Rt(0, 0, ctx.MinSize.X, ctx.MinSize.Y), | |
} | |
// Iterate over all child ids. | |
gs := []layout.Guide{} | |
for i := 0; i< ctx.ChildCount; i++ { | |
// Get the desired size of the children. In this case we let the children be any size. | |
child := ctx.LayoutChild(idx, layout.Pt(0, 0), layout.Pt(math.Inf(1), math.Inf(1))) | |
// Position the children to be centered in the view. | |
child.Frame = child.Frame.Add(layout.Pt(g.CenterX()-child.Width()/2, g.CenterY()-child.Height()/2)) | |
child.ZIndex = i | |
gs = append(gs, child) | |
} | |
// Return the view's size, and the frames of its children. | |
return g, gs | |
} | |
Layouters also implement the comm.Notifier interface. This allows layouts to update | |
without rebuilding the view. It is light-weight and useful for animations. | |
*/ | |
package layout | |
import ( | |
"reflect" | |
"gomatcha.io/bridge" | |
"gomatcha.io/matcha/comm" | |
pblayout "gomatcha.io/matcha/pb/layout" | |
) | |
func init() { | |
bridge.RegisterType("layout.Point", reflect.TypeOf(Point{})) | |
bridge.RegisterType("layout.Rect", reflect.TypeOf(Rect{})) | |
} | |
type Layouter interface { | |
Layout(ctx *Context) (Guide, []Guide) | |
comm.Notifier | |
} | |
type Context struct { | |
MinSize Point | |
MaxSize Point | |
ChildCount int | |
LayoutFunc func(int, Point, Point) Guide | |
} | |
func (l *Context) LayoutChild(idx int, minSize, maxSize Point) Guide { | |
g := l.LayoutFunc(idx, minSize, maxSize) | |
g.Frame = g.Frame.Add(Pt(-g.Frame.Min.X, -g.Frame.Min.Y)) | |
return g | |
} | |
// Guide represents the position of a view. | |
type Guide struct { | |
Frame Rect | |
ZIndex int | |
} | |
// MarshalProtobuf serializes g into a protobuf object. | |
func (g Guide) MarshalProtobuf() *pblayout.Guide { | |
return &pblayout.Guide{ | |
Frame: g.Frame.MarshalProtobuf(), | |
ZIndex: int64(g.ZIndex), | |
} | |
} | |
// Left returns the left edge of g. | |
func (g Guide) Left() float64 { | |
return g.Frame.Min.X | |
} | |
// Right returns the right edge of g. | |
func (g Guide) Right() float64 { | |
return g.Frame.Max.X | |
} | |
// Top returns the top edge of g. | |
func (g Guide) Top() float64 { | |
return g.Frame.Min.Y | |
} | |
// Bottom returns the bottom edge of g. | |
func (g Guide) Bottom() float64 { | |
return g.Frame.Max.Y | |
} | |
// Width returns the width of g. | |
func (g Guide) Width() float64 { | |
return g.Frame.Max.X - g.Frame.Min.X | |
} | |
// Height returns the height of g. | |
func (g Guide) Height() float64 { | |
return g.Frame.Max.Y - g.Frame.Min.Y | |
} | |
// CenterX returns the horizontal center of g. | |
func (g Guide) CenterX() float64 { | |
return (g.Frame.Max.X - g.Frame.Min.X) / 2 | |
} | |
// CenterY returns the vertical center of g. | |
func (g Guide) CenterY() float64 { | |
return (g.Frame.Max.Y - g.Frame.Min.Y) / 2 | |
} | |
// Fit adjusts the frame of the guide to be within MinSize and MaxSize of the LayoutContext. | |
func (g Guide) Fit(ctx *Context) Guide { | |
if g.Width() < ctx.MinSize.X { | |
g.Frame.Max.X = ctx.MinSize.X - g.Frame.Min.X | |
} | |
if g.Height() < ctx.MinSize.Y { | |
g.Frame.Max.Y = ctx.MinSize.Y - g.Frame.Min.Y | |
} | |
if g.Width() > ctx.MaxSize.X { | |
g.Frame.Max.X = ctx.MaxSize.X - g.Frame.Min.X | |
} | |
if g.Height() > ctx.MaxSize.Y { | |
g.Frame.Max.Y = ctx.MaxSize.Y - g.Frame.Min.Y | |
} | |
return g | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func TestFit(t *testing.T) { | |
g := Guide{Frame: Rt(0, 0, 100, 100)} | |
ctx := &Context{MinSize: Pt(50, 50), MaxSize: Pt(75, 75)} | |
fitG := g.Fit(ctx) | |
expect := Guide{Frame: Rt(0, 0, 75, 75)} | |
if fitG != expect { | |
t.Error("Error") | |
} | |
} | |
func TestFit2(t *testing.T) { | |
g := Guide{Frame: Rt(0, 0, 25, 25)} | |
ctx := &Context{MinSize: Pt(50, 50), MaxSize: Pt(75, 75)} | |
fitG := g.Fit(ctx) | |
expect := Guide{Frame: Rt(0, 0, 50, 50)} | |
if fitG != expect { | |
t.Error("Error") | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Package table implements a vertical, single column layout system. Views are layed out from top to bottom. | |
l := &table.Layouter{} | |
childView := NewChildView(...) | |
l.Add(childView, nil) // The height of the view is determined by the child's layouter. | |
return view.Model{ | |
Views: l.Views(), | |
Layouter:l, | |
} | |
*/ | |
package table | |
import ( | |
"math" | |
"gomatcha.io/matcha/comm" | |
"gomatcha.io/matcha/layout" | |
"gomatcha.io/matcha/view" | |
) | |
type ScrollBehavior interface { | |
} | |
type Layouter struct { | |
views []view.View | |
} | |
// Views returns all views that have been added to l. | |
func (l *Layouter) Views() []view.View { | |
return l.views | |
} | |
// Add adds v to the layouter and positions it with g. | |
func (l *Layouter) Add(v view.View, b ScrollBehavior) { | |
l.views = append(l.views, v) | |
} | |
// Layout implements the view.Layouter interface. | |
func (l *Layouter) Layout(ctx *layout.Context) (layout.Guide, []layout.Guide) { | |
g := layout.Guide{} | |
gs := []layout.Guide{} | |
y := 0.0 | |
x := ctx.MinSize.X | |
for i := range l.views { | |
g := ctx.LayoutChild(i, layout.Pt(x, 0), layout.Pt(x, math.Inf(1))) | |
g.Frame = layout.Rt(0, y, g.Width(), y+g.Height()) | |
g.ZIndex = i | |
gs = append(gs, g) | |
y += g.Height() | |
} | |
g.Frame = layout.Rt(0, 0, x, y) | |
return g, gs | |
} | |
// Notify implements the view.Layouter interface. | |
func (l *Layouter) Notify(f func()) comm.Id { | |
return 0 // no-op | |
} | |
// Unnotify implements the view.Layouter interface. | |
func (l *Layouter) Unnotify(id comm.Id) { | |
// no-op | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment