From 365a74a2d9e21a0c168db3d433173984dfe8d9b8 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Thu, 19 Aug 2021 12:27:34 +0200 Subject: [PATCH] Added support for scrolling a control into view (Overflow, only vertically currently). --- ui/containerbase.go | 16 ++++++++++++++ ui/control.go | 23 ++++++++++++++++++++ ui/controlbase.go | 21 +++++++++++++++++++ ui/overflow.go | 26 +++++++++++++++++++++++ ui/proxy.go | 14 +++++++++++++ ui/stackpanel.go | 51 +++++++++++++++++++++++++++++++++++++++------ 6 files changed, 145 insertions(+), 6 deletions(-) diff --git a/ui/containerbase.go b/ui/containerbase.go index 534a4b8..d046baa 100644 --- a/ui/containerbase.go +++ b/ui/containerbase.go @@ -17,6 +17,9 @@ func BuildContainerBase(controls ...Control) ContainerBase { func (c *ContainerBase) AddChild(child ...Control) { c.Children = append(c.Children, child...) + for _, child := range child { + child.SetSelf(child) + } } func (c *ContainerBase) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) { @@ -26,6 +29,19 @@ func (c *ContainerBase) Arrange(ctx Context, bounds geom.RectangleF32, offset ge c.ControlBase.Arrange(ctx, bounds, offset, parent) } +func (c *ContainerBase) BoundsUnclipped(ctx Context, path ControlPath) geom.RectangleF32 { + if len(path) == 0 { + return c.bounds + } + next := path[0] + for _, child := range c.Children { + if child == next { + return child.BoundsUnclipped(ctx, path[1:]) + } + } + panic("child not found in path") +} + func (c *ContainerBase) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 { var max geom.PointF32 for _, child := range c.Children { diff --git a/ui/control.go b/ui/control.go index 22f6762..cc893ec 100644 --- a/ui/control.go +++ b/ui/control.go @@ -11,16 +11,39 @@ type Control interface { Render(Context) Bounds() geom.RectangleF32 + BoundsUnclipped(Context, ControlPath) geom.RectangleF32 Disable() Enable() IsDisabled() bool IsInBounds(p geom.PointF32) bool IsOver() bool Offset() geom.PointF32 + ScrollIntoView(Context, ControlPath) + Self() Control + SetSelf(Control) Parent() Control } +type ControlPath []Control + +func (p ControlPath) BoundsUnclipped(ctx Context, c Control) geom.RectangleF32 { + if len(p) == 0 { + return c.Bounds() + } + // switch next := p[0].(type) { + // case *ContainerBase: + // return next.BoundsUnclipped(ctx, p[1:]) + // case *StackPanel: + // return next.BoundsUnclipped(ctx, p[1:]) + // } + return p[0].BoundsUnclipped(ctx, p[1:]) +} + +func (p ControlPath) Prepend(control Control) ControlPath { + return append(ControlPath{control}, p...) +} + type RootControl interface { Control diff --git a/ui/controlbase.go b/ui/controlbase.go index 97e11e7..a7e730e 100644 --- a/ui/controlbase.go +++ b/ui/controlbase.go @@ -90,6 +90,7 @@ var _ Control = &ControlBase{} type ControlBase struct { bounds geom.RectangleF32 offset geom.PointF32 + self Control parent Control drag Dragable over bool @@ -118,6 +119,10 @@ func (c *ControlBase) Arrange(ctx Context, bounds geom.RectangleF32, offset geom func (c *ControlBase) Bounds() geom.RectangleF32 { return c.bounds } +func (c *ControlBase) BoundsUnclipped(ctx Context, path ControlPath) geom.RectangleF32 { + return path.BoundsUnclipped(ctx, c) +} + func (c *ControlBase) ControlClicked() ControlClickedEventHandler { return &c.clicked } func (c *ControlBase) DesiredSize(Context, geom.PointF32) geom.PointF32 { return geom.ZeroPtF32 } @@ -282,4 +287,20 @@ func (c *ControlBase) TextColor(ctx Context) color.Color { return c.FontColor(ctx, ctx.Style().Palette.Text) } +func (c *ControlBase) ScrollIntoView(ctx Context, path ControlPath) { + if c.parent == nil { + return + } + c.parent.ScrollIntoView(ctx, path.Prepend(c)) +} + +func (c *ControlBase) Self() Control { + if c.self == nil { + return c + } + return c.self +} + +func (c *ControlBase) SetSelf(self Control) { c.self = self } + func (c *ControlBase) ToControlPosition(p geom.PointF32) geom.PointF32 { return p.Sub(c.offset) } diff --git a/ui/overflow.go b/ui/overflow.go index 133ec2b..2560bc3 100644 --- a/ui/overflow.go +++ b/ui/overflow.go @@ -114,6 +114,10 @@ func (o *overflow) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.Po func (o *overflow) Bounds() geom.RectangleF32 { return o.bounds } +func (o *overflow) BoundsUnclipped(ctx Context, path ControlPath) geom.RectangleF32 { + return path.BoundsUnclipped(ctx, o) +} + func (o *overflow) DesiredSize(Context, geom.PointF32) geom.PointF32 { return geom.PtF32(geom.NaN32(), geom.NaN32()) } @@ -193,3 +197,25 @@ func (o *overflow) SetScrollbarColor(bar color.Color, hover color.Color) { o.ver.BarColor = bar o.ver.BarHoverColor = hover } + +func (o *overflow) ScrollIntoView(ctx Context, path ControlPath) { + view := path.BoundsUnclipped(ctx, o) + size := geom.PtF32(o.hor.Bounds().Dx(), o.ver.Bounds().Dy()) + view.Min.Y += o.ver.ContentOffset + view.Max.Y += o.ver.ContentOffset + if view.Max.Y > o.ver.ContentLength { + view.Max.Y = o.ver.ContentLength + if view.Min.Y > view.Max.Y { + view.Min.Y = view.Max.Y + } + } + if view.Min.Y < 0 { + view.Min.Y = 0 + } + if view.Max.Y > o.ver.ContentOffset+size.Y { + o.ver.ContentOffset = view.Max.Y - size.Y + } + if view.Min.Y < o.ver.ContentOffset { + o.ver.ContentOffset = view.Min.Y + } +} diff --git a/ui/proxy.go b/ui/proxy.go index c82e493..3ff822d 100644 --- a/ui/proxy.go +++ b/ui/proxy.go @@ -29,6 +29,10 @@ func (p *Proxy) Bounds() geom.RectangleF32 { return p.Content.Bounds() } +func (p *Proxy) BoundsUnclipped(ctx Context, path ControlPath) geom.RectangleF32 { + return path.BoundsUnclipped(ctx, p) +} + func (p *Proxy) Disable() { p.Content.Disable() } func (p *Proxy) Enable() { p.Content.Enable() } @@ -56,3 +60,13 @@ func (p *Proxy) Shown() { overlay.Shown() } } + +func (p *Proxy) ScrollIntoView(ctx Context, path ControlPath) { + p.Content.Parent().ScrollIntoView(ctx, path.Prepend(p)) +} + +func (p *Proxy) Self() Control { + return p.Content.Self() +} + +func (p *Proxy) SetSelf(self Control) { p.Content.SetSelf(self) } diff --git a/ui/stackpanel.go b/ui/stackpanel.go index 74c5819..a6a129a 100644 --- a/ui/stackpanel.go +++ b/ui/stackpanel.go @@ -17,13 +17,13 @@ func BuildStackPanel(o Orientation, fn func(*StackPanel)) *StackPanel { return p } -func (p *StackPanel) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) { +func (p *StackPanel) preArrange(ctx Context, bounds geom.RectangleF32, clip bool) []geom.RectangleF32 { bounds = p.Orientation.FlipRect(bounds) var length float32 var stretch int var desired = make([]geom.PointF32, len(p.Children)) for i, child := range p.Children { - var size = p.Orientation.FlipPt(child.DesiredSize(ctx, bounds.Size())) + var size = p.Orientation.FlipPt(child.DesiredSize(ctx, p.Orientation.FlipPt(bounds.Size()))) if geom.IsNaN32(size.Y) { stretch++ } else { @@ -33,7 +33,8 @@ func (p *StackPanel) Arrange(ctx Context, bounds geom.RectangleF32, offset geom. } var remainder = bounds.Dy() - length var childOffset float32 - for i, size := range desired { + childBounds := make([]geom.RectangleF32, 0, len(p.Children)) + for _, size := range desired { var height = size.Y if geom.IsNaN32(size.Y) { if remainder < 0 { @@ -42,15 +43,46 @@ func (p *StackPanel) Arrange(ctx Context, bounds geom.RectangleF32, offset geom. height = remainder / float32(stretch) } } - minY := geom.Min32(bounds.Max.Y, bounds.Min.Y+childOffset) - maxY := geom.Min32(bounds.Max.Y, bounds.Min.Y+childOffset+height) + minY := bounds.Min.Y + childOffset + maxY := minY + height + if clip { + if minY > bounds.Max.Y { + minY = bounds.Max.Y + } + if maxY > bounds.Max.Y { + maxY = bounds.Max.Y + } + } var child = geom.RectF32(bounds.Min.X, minY, bounds.Max.X, maxY) - p.Children[i].Arrange(ctx, p.Orientation.FlipRect(child), offset, p) + childBounds = append(childBounds, p.Orientation.FlipRect(child)) childOffset += height } + return childBounds +} + +func (p *StackPanel) Arrange(ctx Context, bounds geom.RectangleF32, offset geom.PointF32, parent Control) { + childBounds := p.preArrange(ctx, bounds, true) + for i := range p.Children { + p.Children[i].Arrange(ctx, p.Orientation.FlipRect(childBounds[i]), offset, p) + } p.ControlBase.Arrange(ctx, p.Orientation.FlipRect(bounds), offset, parent) } +func (p *StackPanel) BoundsUnclipped(ctx Context, path ControlPath) geom.RectangleF32 { + if len(path) == 0 { + return p.bounds + } + next := path[0] + childBounds := p.preArrange(ctx, p.bounds, false) + for i, child := range p.Children { + if child != next && next.Self() != child.Self() { + continue + } + return childBounds[i] + } + panic("child not found in path") +} + func (p *StackPanel) DesiredSize(ctx Context, size geom.PointF32) geom.PointF32 { var length float32 var width float32 @@ -87,3 +119,10 @@ func (p *StackPanel) Render(ctx Context) { child.Render(ctx) } } + +func (p *StackPanel) ScrollIntoView(ctx Context, path ControlPath) { + if p.parent == nil { + return + } + p.parent.ScrollIntoView(ctx, path.Prepend(p)) +}