~eliasnaur/gio#421: 
Dynamic Border Radius.

Hello @gioui team,

I believe it would be nice to have a dynamic border with dynamic radius. Currently, on Godcr, we have something implemented that could help.

// Border lays out a widget and draws a border inside it.
type Border struct {
	Color  color.NRGBA
	Radius CornerRadius
	Width  unit.Dp
}

func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
	dims := w(gtx)
	sz := dims.Size

	width := gtx.Dp(b.Width)
	sz.X -= width
	sz.Y -= width

	r := image.Rectangle{Max: sz}
	r = r.Add(image.Point{X: width * 0.5, Y: width * 0.5})

	tr := float32(gtx.Dp(unit.Dp(b.Radius.TopRight)))
	tl := float32(gtx.Dp(unit.Dp(b.Radius.TopLeft)))
	br := float32(gtx.Dp(unit.Dp(b.Radius.BottomRight)))
	bl := float32(gtx.Dp(unit.Dp(b.Radius.BottomLeft)))
	radius := clip.RRect{
		Rect: r,
		NW:   tl, NE: tr, SE: br, SW: bl,
	}

	paint.FillShape(gtx.Ops,
		b.Color,
		clip.Stroke{
			Path:  radius.Path(gtx.Ops),
			Width: width,
		}.Op(),
	)

	return dims
}

This way we can decide the redius for all 4 corners of the border rather than just one for all.

Warm regards

Status
REPORTED
Submitter
~sirmorrison
Assigned to
No-one
Submitted
6 months ago
Updated
6 months ago
Labels
No labels applied.

~eliasnaur 6 months ago

It's useful. The question is whether it's useful enough to warrant inclusion in package widget. How often do you need it? Maybe ~whereswaldon has some intuition of the need as well.

~sirmorrison referenced this from #421 6 months ago

~sirmorrison 6 months ago

Hello

Thanks for the prompt feedback.

Looking through most of the widgets like the button widget, I see we stuck with a fixed border radius, meaning I either use a squared box or chamfered on all edges.

I feel this is too rigid. What if I want a button with the top left and top right edges curved and have the bottom left and bottom right edges squared?

It seems you don’t give me much of a choice in this regard.

We currently have a card widget for rendering blocks, and it is coverable too.

type Card struct {
	layout.Inset
	Color      color.NRGBA
	HoverColor color.NRGBA
	Radius     CornerRadius
}

type CornerRadius struct {
	TopLeft     float32
	TopRight    float32
	BottomRight float32
	BottomLeft  float32
}

func Radius(radius float32) CornerRadius {
	return CornerRadius{
		TopLeft:     radius,
		TopRight:    radius,
		BottomRight: radius,
		BottomLeft:  radius,
	}
}

func TopRadius(radius float32) CornerRadius {
	return CornerRadius{
		TopLeft:  radius,
		TopRight: radius,
	}
}

func BottomRadius(radius float32) CornerRadius {
	return CornerRadius{
		BottomRight: radius,
		BottomLeft:  radius,
	}
}

const (
	defaultRadius = 14
)

func (t *Theme) Card() Card {
	return Card{
		Color:      t.Color.Surface,
		HoverColor: t.Color.Gray4,
		Radius:     Radius(defaultRadius),
	}
}

func (c Card) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
	dims := c.Inset.Layout(gtx, func(gtx C) D {
		return layout.Stack{}.Layout(gtx,
			layout.Expanded(func(gtx C) D {
				tr := gtx.Dp(unit.Dp(c.Radius.TopRight))
				tl := gtx.Dp(unit.Dp(c.Radius.TopLeft))
				br := gtx.Dp(unit.Dp(c.Radius.BottomRight))
				bl := gtx.Dp(unit.Dp(c.Radius.BottomLeft))
				defer clip.RRect{
					Rect: image.Rectangle{Max: image.Point{
						X: gtx.Constraints.Min.X,
						Y: gtx.Constraints.Min.Y,
					}},
					NW: tl, NE: tr, SE: br, SW: bl,
				}.Push(gtx.Ops).Pop()
				return fill(gtx, c.Color)
			}),
			layout.Stacked(w),
		)
	})

	return dims
}

func (c Card) HoverableLayout(gtx layout.Context, btn *Clickable, w layout.Widget) layout.Dimensions {
	background := c.Color
	dims := c.Inset.Layout(gtx, func(gtx C) D {
		return layout.Stack{}.Layout(gtx,
			layout.Expanded(func(gtx C) D {
				tr := gtx.Dp(unit.Dp(c.Radius.TopRight))
				tl := gtx.Dp(unit.Dp(c.Radius.TopLeft))
				br := gtx.Dp(unit.Dp(c.Radius.BottomRight))
				bl := gtx.Dp(unit.Dp(c.Radius.BottomLeft))
				defer clip.RRect{
					Rect: image.Rectangle{Max: image.Point{
						X: gtx.Constraints.Min.X,
						Y: gtx.Constraints.Min.Y,
					}},
					NW: tl, NE: tr, SE: br, SW: bl,
				}.Push(gtx.Ops).Pop()

				if btn.Hoverable && btn.button.Hovered() {
					background = btn.style.HoverColor
				}

				return fill(gtx, background)
			}),
			layout.Stacked(w),
		)
	})

	return dims
}

For this, we would like radius to be dynamic.

I just feel it will make working with Gioui more convenient with me having to write too many custom widgets.

On 2 Jun 2022, at 11:39, ~eliasnaur outgoing@sr.ht wrote:

It's useful. The question is whether it's useful enough to warrant inclusion in package widget. How often do you need it? Maybe ~whereswaldon has some intuition of the need as well.

-- View on the web: https://todo.sr.ht/~eliasnaur/gio/421#event-177463

~whereswaldon 6 months ago

I haven't personally experienced the need for this very often, but that's definitely just because the themeing of my applications doesn't generally require it. I'd like to hear from more of the community about whether this is a common need. I think we could make this reasonably ergonomic if we did it by providing a Uniform constructor that pre-populated all of the Radii fields.

~egonelbre 6 months ago

I think it has only come up a few times for me and, similarly, I've needed chamfered boxes. But, I don't think it makes sense to include all possible button variations in the core. Although, it might make sense to create a repository of "extra drawing primitives".

~pierrec 6 months ago

my 2ct: I have run into the same thing and implemented my own widget to have only top/bottom rounded buttons.

~mearaj 6 months ago

I do agree with ~egonelbre, it's a useful feature but not frequently required and creating a separate repository where may be infrequent but useful and helpful gio widgets resides.

~qiannian 6 months ago

I find this useful when I need to control the radius dynamically without having to write too much code

~eliasnaur 6 months ago

I haven't personally experienced the need for this very often, but that's definitely just because the themeing of my applications doesn't generally require it. I'd like to hear from more of the community about whether this is a common need. I think we could make this reasonably ergonomic if we did it by providing a Uniform constructor that pre-populated all of the Radii fields.

SGTM. A general Border with a UniformBorder constuctor would also mirror layout.Inset and layout.UniformInset.

~blankblanket 6 months ago

I have a cloned material repo where I make use of global variables. That way I can declare styles inside init() once and those styles are used across all material widgets without much effort. I can't mix different styles this way.

Register here or Log in to comment, or comment via email.