Added Rect method to Control interface.

Fixed ContentScrollbar.
Added horizontal alignment to label.
Removed some dimensional constants.
Added several controls:
- Margin;
- Button;
- Columns;
- Scroll;
- Wrapper (reusable layout control) that wraps around existing control.
This commit is contained in:
Sander Schobers 2018-09-09 21:07:53 +02:00
parent eb0b660ab6
commit 9d4b097352
12 changed files with 435 additions and 49 deletions

54
ui/button.go Normal file
View File

@ -0,0 +1,54 @@
package ui
import (
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
type Button struct {
ControlBase
Text string
HorizontalAlignment allegro5.HorizontalAlignment
}
func NewButton(text string, click MouseClickFn) *Button {
return &Button{ControlBase: ControlBase{OnClick: click}, Text: text}
}
func NewButtonAlign(text string, click MouseClickFn, align allegro5.HorizontalAlignment) *Button {
return &Button{ControlBase: ControlBase{OnClick: click}, Text: text, HorizontalAlignment: align}
}
func (b *Button) DesiredSize(ctx Context) geom.PointF {
var fonts = ctx.Fonts()
var fnt = fonts.Get("default")
var w = fnt.TextWidth(b.Text)
var fntH = fnt.Height()
return geom.PtF(float64(w+fntH), float64(2*fntH))
}
func (b *Button) Render(ctx Context) {
var fonts = ctx.Fonts()
var min = b.Bounds.Min.To32()
var max = b.Bounds.Max.To32()
var fnt = fonts.Get("default")
var fntH = fnt.Height()
var back = ctx.Palette().Primary()
if b.IsOver && !b.IsPressed {
back = ctx.Palette().PrimaryHighlight()
}
allegro5.DrawFilledRectangle(min.X, min.Y, max.X, max.Y, back)
switch b.HorizontalAlignment {
case allegro5.AlignLeft:
fnt.Draw(min.X+.5*fntH, min.Y+.5*fntH, ctx.Palette().Lightest(), allegro5.AlignLeft, b.Text)
case allegro5.AlignCenter:
fnt.Draw(.5*(min.X+max.X), min.Y+.5*fntH, ctx.Palette().Lightest(), allegro5.AlignCenter, b.Text)
case allegro5.AlignRight:
fnt.Draw(min.X-.5*fntH, min.Y+.5*fntH, ctx.Palette().Lightest(), allegro5.AlignRight, b.Text)
}
b.ControlBase.Render(ctx)
}

View File

@ -84,7 +84,7 @@ func (c *Checkbox) DesiredSize(ctx Context) geom.PointF {
var fonts = ctx.Fonts()
var fnt = fonts.Get("default")
var w = fnt.TextWidth(c.Text)
return geom.PtF(float64(w+checkboxSize+leftMargin), checkboxSize)
return geom.PtF(float64(w+checkboxSize), checkboxSize)
}
func (c *Checkbox) box() *allegro5.Bitmap {

114
ui/columns.go Normal file
View File

@ -0,0 +1,114 @@
package ui
import (
"math"
"opslag.de/schobers/geom"
)
type columns struct {
ContainerBase
cols []*column
col int
}
type column struct {
p DockPanel
}
func (c *column) Append(ctrl Control) {
c.p.Append(DockTop, ctrl)
}
func (c *column) AppendCreate(ctx Context, ctrl Control) {
c.p.AppendCreate(ctx, DockTop, ctrl)
}
type Columns interface {
Container
Columns() []Column
Append(Control)
AppendCreate(Context, Control)
}
type Column interface {
Append(Control)
AppendCreate(Context, Control)
}
func NewColumns(cols int, ctrls ...Control) Columns {
var c = &columns{}
for i := 0; i < cols; i++ {
c.addColumn()
}
for _, ctrl := range ctrls {
c.Append(ctrl)
}
return c
}
func (c *columns) addColumn() {
var p = NewDockPanel(c)
c.ContainerBase.Append(p)
c.cols = append(c.cols, &column{p})
}
func (c *columns) next() {
c.col = (c.col + 1) % len(c.cols)
}
func (c *columns) Columns() []Column {
var cols = make([]Column, len(c.cols))
for i, col := range c.cols {
cols[i] = col
}
return cols
}
func (c *columns) Append(ctrl Control) {
c.cols[c.col].Append(ctrl)
c.next()
}
func (c *columns) AppendCreate(ctx Context, ctrl Control) {
c.cols[c.col].AppendCreate(ctx, ctrl)
c.next()
}
func (c *columns) DesiredSize(ctx Context) geom.PointF {
var w float64
var h float64
for _, col := range c.cols {
var sz = col.p.DesiredSize(ctx)
if !math.IsNaN(w) {
if math.IsNaN(sz.X) {
w = sz.X
} else {
w += sz.X
}
}
if !math.IsNaN(h) {
if math.IsNaN(sz.Y) {
h = sz.Y
} else {
h = math.Max(h, sz.Y)
}
}
}
return geom.PtF(w, h)
}
func (c *columns) Arrange(ctx Context, rect geom.RectangleF) {
c.ContainerBase.SetRect(rect)
var w, h = rect.Dx(), rect.Dy()
var cols = float64(len(c.cols))
for i, col := range c.cols {
var ii = float64(i)
var colH = col.p.DesiredSize(ctx).Y
if colH > h {
colH = h
}
var colR = geom.RectF(rect.Min.X+ii*w/cols, rect.Min.Y, rect.Min.X+(ii+1)*w/cols, rect.Min.Y+colH)
Arrange(ctx, col.p, colR)
}
}

View File

@ -18,6 +18,8 @@ type ContentScrollbar struct {
Orientation Orientation
OnChanged ContentScrollbarValueChangedFn
handle *contentScrollbarHandle
barLength float64
scrollDistance float64
}
type contentScrollbarHandle struct {
@ -47,17 +49,21 @@ func (s *ContentScrollbar) Destroyed(ctx Context) {
s.handle.Destroyed(ctx)
}
func (s *ContentScrollbar) barViewRange() (float64, float64) {
func (s *ContentScrollbar) length() (float64, float64) {
var min, max float64
switch s.Orientation {
case OrientationHorizontal:
min = s.Bounds.Min.X
max = s.Bounds.Max.X
default:
min = s.Bounds.Max.Y
max = s.Bounds.Min.Y
min = s.Bounds.Min.Y
max = s.Bounds.Max.Y
}
var length = (max - min)
return max - min, min
}
func (s *ContentScrollbar) updateBarLength() {
var length, _ = s.length()
var bar = length
if s.Length > length {
bar = length * length / s.Length
@ -69,7 +75,12 @@ func (s *ContentScrollbar) barViewRange() (float64, float64) {
bar = 20
}
}
return min + .5*bar, max - .5*bar
s.barLength = bar
var d = s.Length - length
if d < 0 {
d = 0
}
s.scrollDistance = d
}
func (s *ContentScrollbar) barViewCenter() float64 {
@ -82,23 +93,21 @@ func (s *ContentScrollbar) barViewCenter() float64 {
}
func (s *ContentScrollbar) toValue(x, y float64) float64 {
var n = y
var pos = y
if OrientationHorizontal == s.Orientation {
n = x
pos = x
}
var min, max = s.barViewRange()
if min == max {
var length, min = s.length()
if length == 0 {
return 0
}
var delta = s.Length - (max - min)
var off = (n - min) / (max - min)
var v = off * (s.Length - (max - min))
if v < 0 {
v = 0
} else if v > delta {
v = delta
var offset = (pos - .5*s.barLength - min) / (length - s.barLength)
if offset < 0 {
return 0
} else if offset > 1 {
return s.scrollDistance
}
return v
return s.scrollDistance * offset
}
func (s *ContentScrollbar) change(v float64) {
@ -117,13 +126,16 @@ func (s *ContentScrollbar) snapTo(x, y int) {
}
func (s *ContentScrollbar) increment(d int) {
// var val = s.Value + d
// if val < s.Minimum {
// val = s.Minimum
// } else if val > s.Maximum {
// val = s.Maximum
// }
// s.change(val)
if s.Orientation == OrientationVertical {
d *= -1
}
var val = s.Value + float64(d)*ScrollbarWidth
if val < 0 {
val = 0
} else if val > s.scrollDistance {
val = s.scrollDistance
}
s.change(val)
}
func (s *ContentScrollbar) Handle(ctx Context, ev allegro5.Event) {
@ -168,19 +180,21 @@ func (s *ContentScrollbar) SetRect(rect geom.RectangleF) {
}
}
s.ControlBase.SetRect(rect)
s.updateBarLength()
// var min, max = s.barViewRange()
// var off = float64(s.Value-s.Minimum) / float64(s.Maximum-s.Minimum)
// var centerH = min + (max-min)*off
// var r = 0.5 * float64(s.handles[0].Width())
// var center = s.barViewCenter()
// switch s.Orientation {
// case OrientationHorizontal:
// s.handle.SetRect(geom.RectF(centerH-r, center-r, centerH+r, center+r))
// default:
// s.handle.SetRect(geom.RectF(center-r, centerH-r, center+r, centerH+r))
// }
var offset float64
if 0 < s.scrollDistance {
offset = s.Value / s.scrollDistance
}
var length, min = s.length()
var begin = min + (length-s.barLength)*offset
var end = begin + s.barLength
switch s.Orientation {
case OrientationHorizontal:
s.handle.SetRect(geom.RectF(begin, rect.Min.Y, end, rect.Max.Y))
default:
s.handle.SetRect(geom.RectF(rect.Min.X, begin, rect.Max.X, end))
}
}
func (s *ContentScrollbar) Render(ctx Context) {

View File

@ -14,6 +14,7 @@ type Control interface {
Update(Context, time.Duration)
Handle(Context, allegro5.Event)
DesiredSize(Context) geom.PointF
Rect() geom.RectangleF
SetRect(geom.RectangleF)
Render(Context)
}

View File

@ -1,7 +1,5 @@
package ui
const topMargin = 4
const leftMargin = 8
const lineHeight = 16
const checkboxMargin = 4
const checkboxSize = 24

View File

@ -30,6 +30,14 @@ func NewDockPanel(parent Container) DockPanel {
return &dockPanel{ContainerBase: ContainerBase{parent: parent}}
}
func NewDockPanelContent(parent Container, d Dock, controls ...Control) DockPanel {
var p = NewDockPanel(parent)
for _, c := range controls {
p.Append(d, c)
}
return p
}
func (p *dockPanel) Append(d Dock, children ...Control) error {
for _, child := range children {
p.ContainerBase.Append(child)

View File

@ -1,10 +1,21 @@
package ui
import "opslag.de/schobers/galleg/allegro5"
import (
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
type Label struct {
ControlBase
Text string
HorizontalAlignment allegro5.HorizontalAlignment
}
func (l *Label) DesiredSize(ctx Context) geom.PointF {
var fonts = ctx.Fonts()
var fnt = fonts.Get("default")
var _, _, w, h = fnt.TextDimensions(l.Text)
return geom.PtF(float64(w), float64(h))
}
func (l *Label) Render(ctx Context) {
@ -13,7 +24,7 @@ func (l *Label) Render(ctx Context) {
var min = l.Bounds.Min.To32()
var fnt = fonts.Get("default")
fnt.Draw(min.X+leftMargin, min.Y+lineHeight-fnt.Ascent(), ctx.Palette().Darkest(), allegro5.AlignLeft, l.Text)
fnt.Draw(min.X, min.Y+fnt.Height()-fnt.Ascent(), ctx.Palette().Darkest(), l.HorizontalAlignment, l.Text)
l.ControlBase.Render(ctx)
}

50
ui/margin.go Normal file
View File

@ -0,0 +1,50 @@
package ui
import "opslag.de/schobers/geom"
type margin struct {
Wrapper
Left, Top, Right, Bottom float64
}
func Margin(c Control, m float64) Control {
return &margin{Wrap(c), m, m, m, m}
}
func LeftMargin(c Control, m float64) Control {
return &margin{Wrap(c), m, 0, 0, 0}
}
func TopMargin(c Control, m float64) Control {
return &margin{Wrap(c), 0, m, 0, 0}
}
func HorizontalMargin(c Control, m float64) Control {
return &margin{Wrap(c), m, 0, m, 0}
}
func VerticalMargin(c Control, m float64) Control {
return &margin{Wrap(c), 0, m, 0, m}
}
func (m *margin) DesiredSize(ctx Context) geom.PointF {
var sz = m.Wrapper.DesiredSize(ctx)
return geom.PtF(sz.X+m.Left+m.Right, sz.Y+m.Top+m.Bottom)
}
func (m *margin) Arrange(ctx Context, rect geom.RectangleF) {
m.Wrapper.SetRect(rect)
rect.Min.X += m.Left
rect.Min.Y += m.Top
rect.Max.X -= m.Right
rect.Max.Y -= m.Bottom
if rect.Min.X > rect.Max.X {
var x = .5 * (rect.Min.X + rect.Max.X)
rect.Min.X, rect.Max.X = x, x
}
if rect.Min.Y > rect.Max.Y {
var y = .5 * (rect.Min.Y + rect.Max.Y)
rect.Min.Y, rect.Max.Y = y, y
}
Arrange(ctx, m.Wrapped, rect)
}

77
ui/scroll.go Normal file
View File

@ -0,0 +1,77 @@
package ui
import (
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
type scroll struct {
Wrapper
Content Control
Bar *ContentScrollbar
}
func Scroll(c Control, o Orientation) Control {
var dock = NewDockPanel(nil)
var bar = &ContentScrollbar{Orientation: o}
switch o {
case OrientationHorizontal:
dock.Append(DockBottom, TopMargin(bar, ScrollbarWidth))
dock.Append(DockBottom, c)
case OrientationVertical:
dock.Append(DockRight, LeftMargin(bar, ScrollbarWidth))
dock.Append(DockRight, c)
}
var s = &scroll{Wrap(dock), c, bar}
bar.OnChanged = func(v float64) {
}
return s
}
func (s *scroll) Handle(ctx Context, ev allegro5.Event) {
s.Wrapper.Handle(ctx, ev)
switch e := ev.(type) {
case *allegro5.MouseMoveEvent:
if 0 != e.DeltaZ && !s.Bar.IsOver && geom.PtF(float64(e.X), float64(e.Y)).In(s.Bounds) {
var d = e.DeltaZ
if allegro5.IsAnyKeyDown(allegro5.KeyLShift, allegro5.KeyRShift) {
d *= 10
}
s.Bar.increment(d)
}
}
}
func (s *scroll) Render(ctx Context) {
var bounds = s.Content.Rect()
var w, h = bounds.Dx(), bounds.Dy()
var bmp, err = allegro5.NewVideoBitmap(int(w), int(h))
if nil != err {
return
}
defer bmp.Destroy()
bmp.SetAsTarget()
switch s.Bar.Orientation {
case OrientationHorizontal:
Arrange(ctx, s.Content, geom.RectF(-s.Bar.Value, 0, w, h))
case OrientationVertical:
Arrange(ctx, s.Content, geom.RectF(0, -s.Bar.Value, w, h))
}
s.Content.Render(ctx)
ctx.Display().SetAsTarget()
var min = s.Bounds.Min.To32()
bmp.Draw(min.X, min.Y)
s.Bar.Render(ctx)
}
func (s *scroll) Arrange(ctx Context, rect geom.RectangleF) {
var sz = s.Content.DesiredSize(ctx)
switch s.Bar.Orientation {
case OrientationHorizontal:
s.Bar.Length = sz.X
case OrientationVertical:
s.Bar.Length = sz.Y
}
s.Wrapper.Arrange(ctx, rect)
}

View File

@ -18,6 +18,7 @@ type State interface {
type StateBase struct {
Control Control
ChangeTo State
}
func (s *StateBase) Enter(ctx Context) error {
@ -42,7 +43,7 @@ func (s *StateBase) Update(ctx Context, dt time.Duration) (State, error) {
if nil != s.Control {
s.Control.Update(ctx, dt)
}
return nil, nil
return s.ChangeTo, nil
}
func (s *StateBase) Handle(ctx Context, ev allegro5.Event) error {

58
ui/wrapper.go Normal file
View File

@ -0,0 +1,58 @@
package ui
import (
"time"
"opslag.de/schobers/galleg/allegro5"
"opslag.de/schobers/geom"
)
type Wrapper struct {
Wrapped Control
Bounds geom.RectangleF
}
func Wrap(c Control) Wrapper {
return Wrapper{Wrapped: c}
}
func (w *Wrapper) Created(ctx Context, p Container) error {
return w.Wrapped.Created(ctx, p)
}
func (w *Wrapper) Destroyed(ctx Context) {
w.Wrapped.Destroyed(ctx)
}
func (w *Wrapper) Update(ctx Context, d time.Duration) {
w.Wrapped.Update(ctx, d)
}
func (w *Wrapper) Handle(ctx Context, ev allegro5.Event) {
w.Wrapped.Handle(ctx, ev)
}
func (w *Wrapper) DesiredSize(ctx Context) geom.PointF {
return w.Wrapped.DesiredSize(ctx)
}
func (w *Wrapper) Rect() geom.RectangleF {
return w.Bounds
}
func (w *Wrapper) SetRect(rect geom.RectangleF) {
w.Bounds = rect
}
func (w *Wrapper) Render(ctx Context) {
w.Wrapped.Render(ctx)
}
func (w *Wrapper) Children() []Control {
return []Control{w.Wrapped}
}
func (w *Wrapper) Arrange(ctx Context, rect geom.RectangleF) {
w.Bounds = rect
Arrange(ctx, w.Wrapped, rect)
}