From ff51378affa68fd93254d6e9303f493a2211bf0f Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Wed, 13 Mar 2019 19:49:00 +0100 Subject: [PATCH] Added button types. Go image can be reconstructed from ui.Image/allg5.Bitmap. Changed allg5.Scale from interface to struct. --- allg5/bitmap.go | 80 +++++++------- allg5/color.go | 5 + ui/allg5ui/image.go | 14 ++- ui/allg5ui/renderer.go | 35 ++++-- ui/button.go | 147 ++++++++++++++++++++++---- ui/drawoptions.go | 12 +++ ui/examples/01_basic/basic.go | 13 ++- ui/examples/resources/images/plus.png | Bin 0 -> 1539 bytes ui/image.go | 7 +- ui/overflow.go | 2 +- ui/renderer.go | 3 +- ui/style.go | 20 ++-- 12 files changed, 253 insertions(+), 85 deletions(-) create mode 100644 ui/drawoptions.go create mode 100644 ui/examples/resources/images/plus.png diff --git a/allg5/bitmap.go b/allg5/bitmap.go index 4eba487..e001574 100644 --- a/allg5/bitmap.go +++ b/allg5/bitmap.go @@ -7,7 +7,6 @@ import "C" import ( "errors" "image" - "image/color" "unsafe" ) @@ -21,30 +20,22 @@ type Bitmap struct { type DrawOptions struct { Center bool - Scale Scale + Scale *Scale Tint *Color Rotation *Rotation } -type Scale interface { - Horizontal() float32 - Vertical() float32 +type Scale struct { + Horizontal float32 + Vertical float32 } -type scale struct { - horizontal float32 - vertical float32 +func NewScale(hor, ver float32) *Scale { + return &Scale{hor, ver} } -func (s *scale) Horizontal() float32 { return s.horizontal } -func (s *scale) Vertical() float32 { return s.vertical } - -func NewScale(horizontal, vertical float32) Scale { - return &scale{horizontal, vertical} -} - -func NewUniformScale(s float32) Scale { - return &scale{s, s} +func NewUniformScale(s float32) *Scale { + return &Scale{s, s} } type Rotation struct { @@ -92,7 +83,7 @@ func NewMemoryBitmap(width, height int, flags ...NewBitmapFlag) (*Bitmap, error) } // NewBitmapFromImage creates a new bitmap starting from a Go native image (image.Image) -func NewBitmapFromImage(im image.Image, video bool) (*Bitmap, error) { +func NewBitmapFromImage(src image.Image, video bool) (*Bitmap, error) { var newBmpFlags = CaptureNewBitmapFlags() defer newBmpFlags.Revert() newBmpFlags.Mutate(func(m FlagMutation) { @@ -100,30 +91,27 @@ func NewBitmapFromImage(im image.Image, video bool) (*Bitmap, error) { m.Set(NewBitmapFlagMemoryBitmap) m.Set(NewBitmapFlagMinLinear) }) - var bnd = im.Bounds() + var bnd = src.Bounds() width, height := bnd.Dx(), bnd.Dy() var b = C.al_create_bitmap(C.int(width), C.int(height)) if b == nil { return nil, errors.New("error creating memory bitmap") } - row := make([]uint8, width*4) - rgn := C.al_lock_bitmap(b, C.ALLEGRO_PIXEL_FORMAT_ABGR_8888, C.ALLEGRO_LOCK_WRITEONLY) - if rgn == nil { + region := C.al_lock_bitmap(b, C.ALLEGRO_PIXEL_FORMAT_ABGR_8888, C.ALLEGRO_LOCK_WRITEONLY) + if region == nil { C.al_destroy_bitmap(b) return nil, errors.New("unable to lock bitmap") } - data := (*[1 << 30]uint8)(rgn.data) - offset := 0 + dst := (*[1 << 30]uint8)(region.data) for y := 0; y < height; y++ { + row := dst[y*int(region.pitch):] for x := 0; x < width; x++ { - pix := color.RGBAModel.Convert(im.At(x, y)).(color.RGBA) - row[x*4] = pix.R - row[x*4+1] = pix.G - row[x*4+2] = pix.B - row[x*4+3] = pix.A + r, g, b, a := src.At(x, y).RGBA() + row[x*4] = uint8(r >> 8) + row[x*4+1] = uint8(g >> 8) + row[x*4+2] = uint8(b >> 8) + row[x*4+3] = uint8(a >> 8) } - copy(data[offset:], row) - offset += int(rgn.pitch) } C.al_unlock_bitmap(b) if video { @@ -159,16 +147,16 @@ func (b *Bitmap) DrawOptions(left, top float32, options DrawOptions) { width := float32(b.width) height := float32(b.height) - scale := nil != options.Scale + scale := options.Scale != nil if scale { - width *= options.Scale.Horizontal() - height *= options.Scale.Vertical() + width *= options.Scale.Horizontal + height *= options.Scale.Vertical } if options.Center { left -= width * 0.5 top -= height * 0.5 } - rotated := nil != options.Rotation + rotated := options.Rotation != nil var centerX C.float var centerY C.float if rotated && options.Rotation.Center { @@ -179,13 +167,13 @@ func (b *Bitmap) DrawOptions(left, top float32, options DrawOptions) { if scale { if options.Tint == nil { // scaled if rotated { // scaled & rotated - C.al_draw_scaled_rotated_bitmap(b.bitmap, centerX, centerY, C.float(left), C.float(top), C.float(options.Scale.Horizontal()), C.float(options.Scale.Vertical()), C.float(options.Rotation.Angle), 0) + C.al_draw_scaled_rotated_bitmap(b.bitmap, centerX, centerY, C.float(left), C.float(top), C.float(options.Scale.Horizontal), C.float(options.Scale.Vertical), C.float(options.Rotation.Angle), 0) } else { // scaled C.al_draw_scaled_bitmap(b.bitmap, 0, 0, C.float(b.width), C.float(b.height), C.float(left), C.float(top), C.float(width), C.float(height), 0) } } else { // tinted & scaled if rotated { // scaled, tinted & rotated - C.al_draw_tinted_scaled_rotated_bitmap(b.bitmap, options.Tint.color, centerX, centerY, C.float(left), C.float(top), C.float(options.Scale.Horizontal()), C.float(options.Scale.Vertical()), C.float(options.Rotation.Angle), 0) + C.al_draw_tinted_scaled_rotated_bitmap(b.bitmap, options.Tint.color, centerX, centerY, C.float(left), C.float(top), C.float(options.Scale.Horizontal), C.float(options.Scale.Vertical), C.float(options.Rotation.Angle), 0) } else { // tinted, scaled C.al_draw_tinted_scaled_bitmap(b.bitmap, options.Tint.color, 0, 0, C.float(b.width), C.float(b.height), C.float(left), C.float(top), C.float(width), C.float(height), 0) } @@ -231,6 +219,24 @@ func (b *Bitmap) Height() int { return b.height } +func (b *Bitmap) Image() image.Image { + im := image.NewRGBA(image.Rect(0, 0, b.width, b.height)) + region := C.al_lock_bitmap(b.bitmap, C.ALLEGRO_PIXEL_FORMAT_ABGR_8888, C.ALLEGRO_LOCK_READONLY) + if region == nil { + return nil + } + defer C.al_unlock_bitmap(b.bitmap) + src := (*[1 << 30]uint8)(region.data) + dst := im.Pix + var srcOff, dstOff int + for y := 0; y < b.height; y++ { + copy(dst[dstOff:], src[srcOff:srcOff+b.width*4]) + srcOff += int(region.pitch) + dstOff += im.Stride + } + return im +} + func (b *Bitmap) SetAsTarget() { C.al_set_target_bitmap(b.bitmap) } diff --git a/allg5/color.go b/allg5/color.go index ae30853..8e43305 100644 --- a/allg5/color.go +++ b/allg5/color.go @@ -18,6 +18,11 @@ func NewColorAlpha(r, g, b, a byte) Color { return Color{C.al_map_rgba(C.uchar(r), C.uchar(g), C.uchar(b), C.uchar(a))} } +func NewColorGo(c color.Color) Color { + r, g, b, a := c.RGBA() + return Color{C.al_premul_rgba(C.uchar(r>>8), C.uchar(g>>8), C.uchar(b>>8), C.uchar(a>>8))} +} + // RGBA implements the color.Color interface. func (c Color) RGBA() (r, g, b, a uint32) { var cr, cg, cb, ca C.uchar diff --git a/ui/allg5ui/image.go b/ui/allg5ui/image.go index 0612402..4fe88e0 100644 --- a/ui/allg5ui/image.go +++ b/ui/allg5ui/image.go @@ -1,6 +1,8 @@ package allg5ui import ( + "image" + "opslag.de/schobers/zntg/allg5" "opslag.de/schobers/zntg/ui" ) @@ -11,14 +13,18 @@ type uiImage struct { bmp *allg5.Bitmap } +func (i *uiImage) Destroy() { + i.bmp.Destroy() +} + func (i *uiImage) Height() float32 { return float32(i.bmp.Height()) } +func (i *uiImage) Image() image.Image { + return i.bmp.Image() +} + func (i *uiImage) Width() float32 { return float32(i.bmp.Width()) } - -func (i *uiImage) Destroy() { - i.bmp.Destroy() -} diff --git a/ui/allg5ui/renderer.go b/ui/allg5ui/renderer.go index e199e77..292b72e 100644 --- a/ui/allg5ui/renderer.go +++ b/ui/allg5ui/renderer.go @@ -6,7 +6,6 @@ import ( "math" "opslag.de/schobers/geom" - "opslag.de/schobers/zntg/allg5" "opslag.de/schobers/zntg/ui" ) @@ -141,9 +140,24 @@ func (r *Renderer) DefaultTarget() ui.Image { return &uiImage{r.disp.Target()} } -func (r *Renderer) DrawImage(p geom.PointF32, im ui.Image) { +func (r *Renderer) DrawImage(im ui.Image, p geom.PointF32) { bmp := r.mustGetBitmap(im) - bmp.Draw(p.X, p.Y) + x, y := snap(p) + bmp.Draw(x, y) +} + +func (r *Renderer) DrawImageOptions(im ui.Image, p geom.PointF32, opts ui.DrawOptions) { + bmp := r.mustGetBitmap(im) + var o allg5.DrawOptions + if opts.Tint != nil { + tint := newColor(opts.Tint) + o.Tint = &tint + } + if opts.Scale != nil { + o.Scale = &allg5.Scale{Horizontal: opts.Scale.X, Vertical: opts.Scale.Y} + } + x, y := snap(p) + bmp.DrawOptions(x, y, o) } func (r *Renderer) FillRectangle(rect geom.RectangleF32, c color.Color) { @@ -163,7 +177,9 @@ func (r *Renderer) mustGetBitmap(im ui.Image) *allg5.Bitmap { } func (r *Renderer) Rectangle(rect geom.RectangleF32, c color.Color, thickness float32) { - allg5.DrawRectangle(rect.Min.X, rect.Min.Y, rect.Max.X, rect.Max.Y, newColor(c), thickness) + minX, minY := snap(rect.Min) + maxX, maxY := snap(rect.Max) + allg5.DrawRectangle(minX, minY, maxX, maxY, newColor(c), thickness) } func (r *Renderer) RegisterFont(path, name string, size int) error { @@ -224,8 +240,7 @@ func (r *Renderer) Text(p geom.PointF32, font string, c color.Color, t string) { if f == nil { return } - x := float32(math.Round(float64(p.X))) - y := float32(math.Round(float64(p.Y))) + x, y := snap(p) f.f.Draw(x, y, newColor(c), allg5.AlignLeft, t) } @@ -250,7 +265,9 @@ func newColor(c color.Color) allg5.Color { if c == nil { return newColor(color.Black) } - var r, g, b, a = c.RGBA() - var r8, g8, b8, a8 = byte(r >> 8), byte(g >> 8), byte(b >> 8), byte(a >> 8) - return allg5.NewColorAlpha(r8, g8, b8, a8) + return allg5.NewColorGo(c) +} + +func snap(p geom.PointF32) (float32, float32) { + return float32(math.Round(float64(p.X))), float32(math.Round(float64(p.Y))) } diff --git a/ui/button.go b/ui/button.go index 6345c23..1607a8e 100644 --- a/ui/button.go +++ b/ui/button.go @@ -1,30 +1,60 @@ package ui import ( + "image/color" + + "github.com/nfnt/resize" "opslag.de/schobers/geom" ) type Button struct { ControlBase + Type ButtonType Text string + Icon Image + + scale float32 + icon Image } -func BuildButton(text string, fn func(b *Button)) *Button { - var b = &Button{Text: text} +type ButtonType int + +const ( + ButtonTypeContained ButtonType = iota + ButtonTypeIcon + ButtonTypeOutlined + ButtonTypeText +) + +func BuildButton(text string, fn func(b *Button)) *Button { return BuildIconButton(nil, text, fn) } + +func BuildIconButton(i Image, text string, fn func(b *Button)) *Button { + var b = &Button{Text: text, Icon: i} if fn != nil { fn(b) } return b } -func (b *Button) DesiredSize(ctx Context) geom.PointF32 { - var fontName = b.FontName(ctx) - var font = ctx.Renderer().Font(fontName) - var width = font.WidthOf(b.Text) - var height = font.Height() +func (b *Button) desiredSize(ctx Context) geom.PointF32 { var pad = ctx.Style().Dimensions.TextPadding - return geom.PtF32(width+pad*2, height+pad*2) + var font = ctx.Renderer().Font(b.FontName(ctx)) + var w, h float32 = 0, font.Height() + if len(b.Text) != 0 { + w += font.WidthOf(b.Text) + pad + } + if b.Icon != nil && b.Icon.Height() > 0 { + w += b.Icon.Width()*h/b.Icon.Height() + pad + } + if w == 0 { + return geom.ZeroPtF32 + } + return geom.PtF32(pad+w, pad+h+pad) +} + +func (b *Button) DesiredSize(ctx Context) geom.PointF32 { + return b.desiredSize(ctx) } func (b *Button) Handle(ctx Context, e Event) { @@ -34,18 +64,97 @@ func (b *Button) Handle(ctx Context, e Event) { } } -func (b *Button) Render(ctx Context) { - var fore = b.Font.Color - var style = ctx.Style() - if fore == nil { - fore = style.Palette.TextOnPrimary +func (b *Button) fillColor(p *Palette) color.Color { + if b.Background != nil { + return b.Background } - var fill = style.Palette.Primary if b.over { - fill = style.Palette.PrimaryHighlight + switch b.Type { + case ButtonTypeContained: + return p.PrimaryHighlight + case ButtonTypeIcon: + default: + return p.PrimaryLight + } + } + switch b.Type { + case ButtonTypeContained: + return p.Primary + case ButtonTypeIcon: + default: + } + return nil +} + +func (b *Button) scaledIcon(ctx Context, height float32) Image { + scale := height / b.Icon.Height() + if scale == 1 { + b.scale = 1 + return b.Icon + } + if b.icon == nil || b.scale != scale { + if b.icon != nil { + b.icon.Destroy() + b.icon = nil + } + im := resize.Resize(uint(b.Icon.Width()*scale), 0, b.Icon.Image(), resize.Bilinear) + icon, err := ctx.Renderer().CreateImage(im) + if err != nil { + return nil + } + b.icon = icon + b.scale = scale + } + return b.icon +} + +func (b *Button) textColor(p *Palette) color.Color { + if b.Font.Color != nil { + return b.Font.Color + } + switch b.Type { + case ButtonTypeContained: + return p.TextOnPrimary + case ButtonTypeIcon: + if b.over { + return p.Primary + } + return p.Text + default: + return p.Primary + } +} + +func (b *Button) Render(ctx Context) { + var style = ctx.Style() + var palette = style.Palette + textColor := b.textColor(palette) + fillColor := b.fillColor(palette) + if fillColor != nil { + ctx.Renderer().FillRectangle(b.bounds, fillColor) + } + size := b.desiredSize(ctx) + bounds := b.bounds + deltaX, deltaY := bounds.Dx()-size.X, bounds.Dy()-size.Y + bounds.Min.X += .5 * deltaX + bounds.Min.Y += .5 * deltaY + + var pad = style.Dimensions.TextPadding + bounds = bounds.Inset(pad) + pos := bounds.Min + if b.Icon != nil && b.Icon.Height() > 0 { + icon := b.scaledIcon(ctx, bounds.Dy()) + if icon != nil { + ctx.Renderer().DrawImageOptions(icon, pos, DrawOptions{Tint: textColor}) + pos.X += icon.Width() + pad + } + } + if len(b.Text) != 0 { + var fontName = b.FontName(ctx) + var font = ctx.Renderer().Font(fontName) + ctx.Renderer().Text(geom.PtF32(pos.X, pos.Y+.5*(bounds.Dy()-font.Height())), fontName, textColor, b.Text) + } + if b.Type == ButtonTypeOutlined { + ctx.Renderer().Rectangle(b.bounds, palette.TextDisabled, 1) } - var pad = style.Dimensions.TextPadding - var font = b.FontName(ctx) - ctx.Renderer().FillRectangle(b.bounds, fill) - ctx.Renderer().Text(b.bounds.Min.Add(geom.PtF32(pad, pad)), font, fore, b.Text) } diff --git a/ui/drawoptions.go b/ui/drawoptions.go new file mode 100644 index 0000000..9266607 --- /dev/null +++ b/ui/drawoptions.go @@ -0,0 +1,12 @@ +package ui + +import ( + "image/color" + + "opslag.de/schobers/geom" +) + +type DrawOptions struct { + Tint color.Color + Scale *geom.PointF32 +} diff --git a/ui/examples/01_basic/basic.go b/ui/examples/01_basic/basic.go index ec230b8..72c1393 100644 --- a/ui/examples/01_basic/basic.go +++ b/ui/examples/01_basic/basic.go @@ -22,16 +22,25 @@ func run() error { if err != nil { return err } + plus, err := render.CreateImagePath("../resources/images/plus.png") + if err != nil { + return err + } + defer plus.Destroy() var view = &ui.StackPanel{ContainerBase: ui.ContainerBase{ ControlBase: ui.ControlBase{Background: color.White}, Children: []ui.Control{ &ui.Label{Text: "Hello, world!"}, - ui.BuildButton("Quit", func(b *ui.Button) { + ui.Margin(ui.BuildIconButton(plus, "Contained", func(b *ui.Button) { b.Type = ui.ButtonTypeContained }), 8), + ui.Margin(ui.BuildIconButton(plus, "Icon", func(b *ui.Button) { b.Type = ui.ButtonTypeIcon }), 8), + ui.Margin(ui.BuildIconButton(plus, "Outlined", func(b *ui.Button) { b.Type = ui.ButtonTypeOutlined }), 8), + ui.Margin(ui.BuildIconButton(plus, "Text", func(b *ui.Button) { b.Type = ui.ButtonTypeText }), 8), + ui.Margin(ui.BuildButton("Quit", func(b *ui.Button) { b.OnClick(func(ctx ui.Context, _ ui.Control, _ geom.PointF32, _ ui.MouseButton) { ctx.Quit() }) - }), + }), 8), ui.Stretch(&ui.Label{Text: "Content"}), &ui.Label{Text: "Status"}, }, diff --git a/ui/examples/resources/images/plus.png b/ui/examples/resources/images/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..47532fc280d6314d081d0db86a712ee9721aac8f GIT binary patch literal 1539 zcmeAS@N?(olHy`uVBq!ia0vp^2N)O_893O0tf$VE>_Cbo-O<;Pfnj4m_n$;oAfL0q zBeIx*f$tCqGm2_>H2?)AN?apKg7ec#$`gxH8440J^GfvcQcDy}^bGY(Q`{qfCbKqr zx;TbZ#J#<9Hf!EhrnW@(unWn8u3v0qf7pL;5MiI!dh4@*$dXxI^UJopd*b}& z$j|xw*Yf^XJ8JQ|F&6 zo?3VN^Zq}j=h|m8_6k4zdN}jwf2J*dE!XoiwD#w*nfSHT+ijciBiVStMge1>lF}JB zc{0`#8dA@F1SobxZ!pVyS1 zyIfpn&wei@{BZc``}01(H2S~%b3ZiZ8@FG)`t9q{zk5^vzo>cex46!Z{oRA*#s8`~ z&skp&uiG&>Kkmr?la~A5KFs@P_5AQ_rWkvf1G2Tnu??@)Q+6zlU(fyF`QzIK*7Y&s z40)$nA6$!J$U93CwT}A%f6|WC?e-l%-+s*9F*kl4_lK__Bje-551d|9;2!>2et$*o zg7tECEZ;vKb^bH`^SQ~zbu~W>_WZ50cz*IzeM#+uIp@#sj5FlE=T<+z*!17KO<*6+ z{ru?Z#lKxs%gA56yMYaX%n#>}iwM^P^S@uqep_I9k!rl4 zQ0dRFjYp462bU}A&uywte7w4A-si_>8_sclT-~k`zgGX-mkU;gc9zeNx9i0B%Ac#; zw7mVE=o8EN^FLp=)IWXydCd8b@8*fc>8qdXhZcC!`<3>}J+4}lfBvJbJ+|@@DE6VP c^7-8V3{3}hRy(QW`-4<@y85}Sb4q9e0HD{xBme*a literal 0 HcmV?d00001 diff --git a/ui/image.go b/ui/image.go index d5b1066..b9983f0 100644 --- a/ui/image.go +++ b/ui/image.go @@ -1,7 +1,10 @@ package ui +import "image" + type Image interface { - Height() float32 - Width() float32 Destroy() + Height() float32 + Image() image.Image + Width() float32 } diff --git a/ui/overflow.go b/ui/overflow.go index 8fd8b57..ca76c79 100644 --- a/ui/overflow.go +++ b/ui/overflow.go @@ -141,7 +141,7 @@ func (o *overflow) Render(ctx Context) { renderer.Clear(color.Transparent) o.Content.Render(ctx) renderer.RenderTo(target) - renderer.DrawImage(o.bounds.Min, o.buffer) + renderer.DrawImage(o.buffer, o.bounds.Min) o.doOnVisibleBars(func(bar *Scrollbar) { bar.Render(ctx) diff --git a/ui/renderer.go b/ui/renderer.go index 08009b5..ecbbb2a 100644 --- a/ui/renderer.go +++ b/ui/renderer.go @@ -21,7 +21,8 @@ type Renderer interface { CreateImagePath(path string) (Image, error) CreateImageSize(w, h float32) (Image, error) DefaultTarget() Image - DrawImage(p geom.PointF32, im Image) + DrawImage(im Image, p geom.PointF32) + DrawImageOptions(im Image, p geom.PointF32, opts DrawOptions) FillRectangle(r geom.RectangleF32, c color.Color) Font(font string) Font Rectangle(r geom.RectangleF32, c color.Color, thickness float32) diff --git a/ui/style.go b/ui/style.go index 97c3e95..8548ca3 100644 --- a/ui/style.go +++ b/ui/style.go @@ -23,8 +23,8 @@ type Palette struct { Primary color.Color // PrimaryHighlight is a highlighted version of the main contrast color. PrimaryHighlight color.Color - // PrimaryBackground is a background version of the main contrast color. - PrimaryBackground color.Color + // PrimaryLight is a background version of the main contrast color. + PrimaryLight color.Color // ShadedBackground is a darker version of the background color. ShadedBackground color.Color // Text is the default text color. @@ -63,14 +63,14 @@ func DefaultFonts() *Fonts { func DefaultPalette() *Palette { if defaultPalette == nil { defaultPalette = &Palette{ - Background: color.White, - Primary: RGBA(0x3F, 0x51, 0xB5, 0xFF), - PrimaryHighlight: RGBA(0x5C, 0x6B, 0xC0, 0xFF), - PrimaryBackground: RGBA(0x9F, 0xA8, 0xDA, 0xFF), - ShadedBackground: RGBA(0xFA, 0xFA, 0xFA, 0xFF), - Text: color.Black, - TextDisabled: RGBA(0xBD, 0xBD, 0xBD, 0xFF), - TextOnPrimary: color.White, + Background: color.White, + Primary: RGBA(0x3F, 0x51, 0xB5, 0xFF), + PrimaryHighlight: RGBA(0x5C, 0x6B, 0xC0, 0xFF), + PrimaryLight: RGBA(0xE8, 0xEA, 0xF6, 0xFF), + ShadedBackground: RGBA(0xFA, 0xFA, 0xFA, 0xFF), + Text: color.Black, + TextDisabled: RGBA(0xBD, 0xBD, 0xBD, 0xFF), + TextOnPrimary: color.White, } } return defaultPalette