Added button types.

Go image can be reconstructed from ui.Image/allg5.Bitmap.
Changed allg5.Scale from interface to struct.
This commit is contained in:
Sander Schobers 2019-03-13 19:49:00 +01:00
parent a3aa398909
commit ff51378aff
12 changed files with 253 additions and 85 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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()
}

View File

@ -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)))
}

View File

@ -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
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)
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)
}
}

12
ui/drawoptions.go Normal file
View File

@ -0,0 +1,12 @@
package ui
import (
"image/color"
"opslag.de/schobers/geom"
)
type DrawOptions struct {
Tint color.Color
Scale *geom.PointF32
}

View File

@ -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"},
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,7 +1,10 @@
package ui
import "image"
type Image interface {
Height() float32
Width() float32
Destroy()
Height() float32
Image() image.Image
Width() float32
}

View File

@ -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)

View File

@ -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)

View File

@ -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.
@ -66,7 +66,7 @@ func DefaultPalette() *Palette {
Background: color.White,
Primary: RGBA(0x3F, 0x51, 0xB5, 0xFF),
PrimaryHighlight: RGBA(0x5C, 0x6B, 0xC0, 0xFF),
PrimaryBackground: RGBA(0x9F, 0xA8, 0xDA, 0xFF),
PrimaryLight: RGBA(0xE8, 0xEA, 0xF6, 0xFF),
ShadedBackground: RGBA(0xFA, 0xFA, 0xFA, 0xFF),
Text: color.Black,
TextDisabled: RGBA(0xBD, 0xBD, 0xBD, 0xFF),