diff --git a/ui/button.go b/ui/button.go new file mode 100644 index 0000000..de2043f --- /dev/null +++ b/ui/button.go @@ -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) +} diff --git a/ui/checkbox.go b/ui/checkbox.go index e8790b6..dab8fd0 100644 --- a/ui/checkbox.go +++ b/ui/checkbox.go @@ -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 { diff --git a/ui/columns.go b/ui/columns.go new file mode 100644 index 0000000..157683f --- /dev/null +++ b/ui/columns.go @@ -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) + } +} diff --git a/ui/contentscrollbar.go b/ui/contentscrollbar.go index f2e415b..6269834 100644 --- a/ui/contentscrollbar.go +++ b/ui/contentscrollbar.go @@ -13,11 +13,13 @@ type ContentScrollbarValueChangedFn func(float64) type ContentScrollbar struct { ControlBase - Length float64 - Value float64 - Orientation Orientation - OnChanged ContentScrollbarValueChangedFn - handle *contentScrollbarHandle + Length float64 + Value float64 + 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) { diff --git a/ui/control.go b/ui/control.go index 2d547b3..6e6bdcc 100644 --- a/ui/control.go +++ b/ui/control.go @@ -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) } diff --git a/ui/dimensions.go b/ui/dimensions.go index 30f7444..8924dd1 100644 --- a/ui/dimensions.go +++ b/ui/dimensions.go @@ -1,7 +1,5 @@ package ui -const topMargin = 4 const leftMargin = 8 -const lineHeight = 16 const checkboxMargin = 4 const checkboxSize = 24 diff --git a/ui/dockpanel.go b/ui/dockpanel.go index 1f8d163..1a19397 100644 --- a/ui/dockpanel.go +++ b/ui/dockpanel.go @@ -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) diff --git a/ui/label.go b/ui/label.go index ef55689..c6773d7 100644 --- a/ui/label.go +++ b/ui/label.go @@ -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 + 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) } diff --git a/ui/margin.go b/ui/margin.go new file mode 100644 index 0000000..0d08f54 --- /dev/null +++ b/ui/margin.go @@ -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) +} diff --git a/ui/scroll.go b/ui/scroll.go new file mode 100644 index 0000000..8f07c30 --- /dev/null +++ b/ui/scroll.go @@ -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) +} diff --git a/ui/state.go b/ui/state.go index 9706f10..9fe4104 100644 --- a/ui/state.go +++ b/ui/state.go @@ -17,7 +17,8 @@ type State interface { } type StateBase struct { - Control Control + 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 { diff --git a/ui/wrapper.go b/ui/wrapper.go new file mode 100644 index 0000000..dfc18aa --- /dev/null +++ b/ui/wrapper.go @@ -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) +}