chore(v2): vendor dependencies for offline/China builds
go mod vendor pins onnxruntime_go v1.12.1, Gio and the rest into v2/vendor so go run/build work without hitting proxy.golang.org (blocked/slow in China). Verified: CGO_ENABLED=1 go build -mod=vendor ./internal/spike and GOOS=windows go build -mod=vendor ./internal/ui both pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+297
@@ -0,0 +1,297 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
type ButtonStyle struct {
|
||||
Text string
|
||||
// Color is the text color.
|
||||
Color color.NRGBA
|
||||
Font font.Font
|
||||
TextSize unit.Sp
|
||||
Background color.NRGBA
|
||||
CornerRadius unit.Dp
|
||||
Inset layout.Inset
|
||||
Button *widget.Clickable
|
||||
shaper *text.Shaper
|
||||
}
|
||||
|
||||
type ButtonLayoutStyle struct {
|
||||
Background color.NRGBA
|
||||
CornerRadius unit.Dp
|
||||
Button *widget.Clickable
|
||||
}
|
||||
|
||||
type IconButtonStyle struct {
|
||||
Background color.NRGBA
|
||||
// Color is the icon color.
|
||||
Color color.NRGBA
|
||||
Icon *widget.Icon
|
||||
// Size is the icon size.
|
||||
Size unit.Dp
|
||||
Inset layout.Inset
|
||||
Button *widget.Clickable
|
||||
Description string
|
||||
}
|
||||
|
||||
func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle {
|
||||
b := ButtonStyle{
|
||||
Text: txt,
|
||||
Color: th.Palette.ContrastFg,
|
||||
CornerRadius: 4,
|
||||
Background: th.Palette.ContrastBg,
|
||||
TextSize: th.TextSize * 14.0 / 16.0,
|
||||
Inset: layout.Inset{
|
||||
Top: 10, Bottom: 10,
|
||||
Left: 12, Right: 12,
|
||||
},
|
||||
Button: button,
|
||||
shaper: th.Shaper,
|
||||
}
|
||||
b.Font.Typeface = th.Face
|
||||
return b
|
||||
}
|
||||
|
||||
func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle {
|
||||
return ButtonLayoutStyle{
|
||||
Button: button,
|
||||
Background: th.Palette.ContrastBg,
|
||||
CornerRadius: 4,
|
||||
}
|
||||
}
|
||||
|
||||
func IconButton(th *Theme, button *widget.Clickable, icon *widget.Icon, description string) IconButtonStyle {
|
||||
return IconButtonStyle{
|
||||
Background: th.Palette.ContrastBg,
|
||||
Color: th.Palette.ContrastFg,
|
||||
Icon: icon,
|
||||
Size: 24,
|
||||
Inset: layout.UniformInset(12),
|
||||
Button: button,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
// Clickable lays out a rectangular clickable widget without further
|
||||
// decoration.
|
||||
func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) layout.Dimensions {
|
||||
return button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
semantic.Button.Add(gtx.Ops)
|
||||
return layout.Background{}.Layout(gtx,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
|
||||
if button.Hovered() || gtx.Focused(button) {
|
||||
paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{}))
|
||||
}
|
||||
for _, c := range button.History() {
|
||||
drawInk(gtx, c)
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
},
|
||||
w,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
return ButtonLayoutStyle{
|
||||
Background: b.Background,
|
||||
CornerRadius: b.CornerRadius,
|
||||
Button: b.Button,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
colMacro := op.Record(gtx.Ops)
|
||||
paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
|
||||
return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text, colMacro.Stop())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
|
||||
min := gtx.Constraints.Min
|
||||
return b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
semantic.Button.Add(gtx.Ops)
|
||||
return layout.Background{}.Layout(gtx,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
rr := gtx.Dp(b.CornerRadius)
|
||||
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
|
||||
background := b.Background
|
||||
switch {
|
||||
case !gtx.Enabled():
|
||||
background = f32color.Disabled(b.Background)
|
||||
case b.Button.Hovered() || gtx.Focused(b.Button):
|
||||
background = f32color.Hovered(b.Background)
|
||||
}
|
||||
paint.Fill(gtx.Ops, background)
|
||||
for _, c := range b.Button.History() {
|
||||
drawInk(gtx, c)
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min = min
|
||||
return layout.Center.Layout(gtx, w)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
m := op.Record(gtx.Ops)
|
||||
dims := b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
semantic.Button.Add(gtx.Ops)
|
||||
if d := b.Description; d != "" {
|
||||
semantic.DescriptionOp(b.Description).Add(gtx.Ops)
|
||||
}
|
||||
return layout.Background{}.Layout(gtx,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
rr := (gtx.Constraints.Min.X + gtx.Constraints.Min.Y) / 4
|
||||
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
|
||||
background := b.Background
|
||||
switch {
|
||||
case !gtx.Enabled():
|
||||
background = f32color.Disabled(b.Background)
|
||||
case b.Button.Hovered() || gtx.Focused(b.Button):
|
||||
background = f32color.Hovered(b.Background)
|
||||
}
|
||||
paint.Fill(gtx.Ops, background)
|
||||
for _, c := range b.Button.History() {
|
||||
drawInk(gtx, c)
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
size := gtx.Dp(b.Size)
|
||||
if b.Icon != nil {
|
||||
gtx.Constraints.Min = image.Point{X: size}
|
||||
b.Icon.Layout(gtx, b.Color)
|
||||
}
|
||||
return layout.Dimensions{
|
||||
Size: image.Point{X: size, Y: size},
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
c := m.Stop()
|
||||
bounds := image.Rectangle{Max: dims.Size}
|
||||
defer clip.Ellipse(bounds).Push(gtx.Ops).Pop()
|
||||
c.Add(gtx.Ops)
|
||||
return dims
|
||||
}
|
||||
|
||||
func drawInk(gtx layout.Context, c widget.Press) {
|
||||
// duration is the number of seconds for the
|
||||
// completed animation: expand while fading in, then
|
||||
// out.
|
||||
const (
|
||||
expandDuration = float32(0.5)
|
||||
fadeDuration = float32(0.9)
|
||||
)
|
||||
|
||||
now := gtx.Now
|
||||
|
||||
t := float32(now.Sub(c.Start).Seconds())
|
||||
|
||||
end := c.End
|
||||
if end.IsZero() {
|
||||
// If the press hasn't ended, don't fade-out.
|
||||
end = now
|
||||
}
|
||||
|
||||
endt := float32(end.Sub(c.Start).Seconds())
|
||||
|
||||
// Compute the fade-in/out position in [0;1].
|
||||
var alphat float32
|
||||
{
|
||||
var haste float32
|
||||
if c.Cancelled {
|
||||
// If the press was cancelled before the inkwell
|
||||
// was fully faded in, fast forward the animation
|
||||
// to match the fade-out.
|
||||
if h := 0.5 - endt/fadeDuration; h > 0 {
|
||||
haste = h
|
||||
}
|
||||
}
|
||||
// Fade in.
|
||||
half1 := t/fadeDuration + haste
|
||||
if half1 > 0.5 {
|
||||
half1 = 0.5
|
||||
}
|
||||
|
||||
// Fade out.
|
||||
half2 := float32(now.Sub(end).Seconds())
|
||||
half2 /= fadeDuration
|
||||
half2 += haste
|
||||
if half2 > 0.5 {
|
||||
// Too old.
|
||||
return
|
||||
}
|
||||
|
||||
alphat = half1 + half2
|
||||
}
|
||||
|
||||
// Compute the expand position in [0;1].
|
||||
sizet := t
|
||||
if c.Cancelled {
|
||||
// Freeze expansion of cancelled presses.
|
||||
sizet = endt
|
||||
}
|
||||
sizet /= expandDuration
|
||||
|
||||
// Animate only ended presses, and presses that are fading in.
|
||||
if !c.End.IsZero() || sizet <= 1.0 {
|
||||
gtx.Execute(op.InvalidateCmd{})
|
||||
}
|
||||
|
||||
if sizet > 1.0 {
|
||||
sizet = 1.0
|
||||
}
|
||||
|
||||
if alphat > .5 {
|
||||
// Start fadeout after half the animation.
|
||||
alphat = 1.0 - alphat
|
||||
}
|
||||
// Twice the speed to attain fully faded in at 0.5.
|
||||
t2 := alphat * 2
|
||||
// Beziér ease-in curve.
|
||||
alphaBezier := t2 * t2 * (3.0 - 2.0*t2)
|
||||
sizeBezier := sizet * sizet * (3.0 - 2.0*sizet)
|
||||
size := gtx.Constraints.Min.X
|
||||
if h := gtx.Constraints.Min.Y; h > size {
|
||||
size = h
|
||||
}
|
||||
// Cover the entire constraints min rectangle and
|
||||
// apply curve values to size and color.
|
||||
size = int(float32(size) * 2 * float32(math.Sqrt(2)) * sizeBezier)
|
||||
alpha := 0.7 * alphaBezier
|
||||
const col = 0.8
|
||||
ba, bc := byte(alpha*0xff), byte(col*0xff)
|
||||
rgba := f32color.MulAlpha(color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}, ba)
|
||||
ink := paint.ColorOp{Color: rgba}
|
||||
ink.Add(gtx.Ops)
|
||||
rr := size / 2
|
||||
defer op.Offset(c.Position.Add(image.Point{
|
||||
X: -rr,
|
||||
Y: -rr,
|
||||
})).Push(gtx.Ops).Pop()
|
||||
defer clip.UniformRRect(image.Rectangle{Max: image.Pt(size, size)}, rr).Push(gtx.Ops).Pop()
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
type checkable struct {
|
||||
Label string
|
||||
Color color.NRGBA
|
||||
Font font.Font
|
||||
TextSize unit.Sp
|
||||
IconColor color.NRGBA
|
||||
Size unit.Dp
|
||||
shaper *text.Shaper
|
||||
checkedStateIcon *widget.Icon
|
||||
uncheckedStateIcon *widget.Icon
|
||||
}
|
||||
|
||||
func (c *checkable) layout(gtx layout.Context, checked, hovered bool) layout.Dimensions {
|
||||
var icon *widget.Icon
|
||||
if checked {
|
||||
icon = c.checkedStateIcon
|
||||
} else {
|
||||
icon = c.uncheckedStateIcon
|
||||
}
|
||||
|
||||
dims := layout.Flex{Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
size := gtx.Dp(c.Size) * 4 / 3
|
||||
dims := layout.Dimensions{
|
||||
Size: image.Point{X: size, Y: size},
|
||||
}
|
||||
if !hovered {
|
||||
return dims
|
||||
}
|
||||
|
||||
background := f32color.MulAlpha(c.IconColor, 70)
|
||||
|
||||
b := image.Rectangle{Max: image.Pt(size, size)}
|
||||
paint.FillShape(gtx.Ops, background, clip.Ellipse(b).Op(gtx.Ops))
|
||||
|
||||
return dims
|
||||
}),
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
size := gtx.Dp(c.Size)
|
||||
col := c.IconColor
|
||||
if !gtx.Enabled() {
|
||||
col = f32color.Disabled(col)
|
||||
}
|
||||
gtx.Constraints.Min = image.Point{X: size}
|
||||
icon.Layout(gtx, col)
|
||||
return layout.Dimensions{
|
||||
Size: image.Point{X: size, Y: size},
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
colMacro := op.Record(gtx.Ops)
|
||||
paint.ColorOp{Color: c.Color}.Add(gtx.Ops)
|
||||
return widget.Label{}.Layout(gtx, c.shaper, c.Font, c.TextSize, c.Label, colMacro.Stop())
|
||||
})
|
||||
}),
|
||||
)
|
||||
return dims
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
type CheckBoxStyle struct {
|
||||
checkable
|
||||
CheckBox *widget.Bool
|
||||
}
|
||||
|
||||
func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle {
|
||||
c := CheckBoxStyle{
|
||||
CheckBox: checkBox,
|
||||
checkable: checkable{
|
||||
Label: label,
|
||||
Color: th.Palette.Fg,
|
||||
IconColor: th.Palette.ContrastBg,
|
||||
TextSize: th.TextSize * 14.0 / 16.0,
|
||||
Size: 26,
|
||||
shaper: th.Shaper,
|
||||
checkedStateIcon: th.Icon.CheckBoxChecked,
|
||||
uncheckedStateIcon: th.Icon.CheckBoxUnchecked,
|
||||
},
|
||||
}
|
||||
c.checkable.Font.Typeface = th.Face
|
||||
return c
|
||||
}
|
||||
|
||||
// Layout updates the checkBox and displays it.
|
||||
func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
return c.CheckBox.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
semantic.CheckBox.Add(gtx.Ops)
|
||||
return c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered() || gtx.Focused(c.CheckBox))
|
||||
})
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package material
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
// DecorationsStyle provides the style elements for Decorations.
|
||||
type DecorationsStyle struct {
|
||||
Decorations *widget.Decorations
|
||||
Actions system.Action
|
||||
Title LabelStyle
|
||||
Background color.NRGBA
|
||||
Foreground color.NRGBA
|
||||
}
|
||||
|
||||
// Decorations returns the style to decorate a window.
|
||||
func Decorations(th *Theme, deco *widget.Decorations, actions system.Action, title string) DecorationsStyle {
|
||||
titleStyle := Body1(th, title)
|
||||
titleStyle.Color = th.Palette.ContrastFg
|
||||
return DecorationsStyle{
|
||||
Decorations: deco,
|
||||
Actions: actions,
|
||||
Title: titleStyle,
|
||||
Background: th.Palette.ContrastBg,
|
||||
Foreground: th.Palette.ContrastFg,
|
||||
}
|
||||
}
|
||||
|
||||
// Layout a window with its title and action buttons.
|
||||
func (d DecorationsStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
rec := op.Record(gtx.Ops)
|
||||
dims := d.layoutDecorations(gtx)
|
||||
decos := rec.Stop()
|
||||
r := clip.Rect{Max: dims.Size}
|
||||
paint.FillShape(gtx.Ops, d.Background, r.Op())
|
||||
decos.Add(gtx.Ops)
|
||||
return dims
|
||||
}
|
||||
|
||||
func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimensions {
|
||||
gtx.Constraints.Min.Y = 0
|
||||
inset := layout.UniformInset(10)
|
||||
return layout.Flex{
|
||||
Axis: layout.Horizontal,
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return d.Decorations.LayoutMove(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return inset.Layout(gtx, d.Title.Layout)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
// Remove the unmaximize action as it is taken care of by maximize.
|
||||
actions := d.Actions &^ system.ActionUnmaximize
|
||||
var size image.Point
|
||||
for a := system.Action(1); actions != 0; a <<= 1 {
|
||||
if a&actions == 0 {
|
||||
continue
|
||||
}
|
||||
actions &^= a
|
||||
var w layout.Widget
|
||||
switch a {
|
||||
case system.ActionMinimize:
|
||||
w = minimizeWindow
|
||||
case system.ActionMaximize:
|
||||
if d.Decorations.Maximized {
|
||||
w = maximizedWindow
|
||||
} else {
|
||||
w = maximizeWindow
|
||||
}
|
||||
case system.ActionClose:
|
||||
w = closeWindow
|
||||
default:
|
||||
continue
|
||||
}
|
||||
cl := d.Decorations.Clickable(a)
|
||||
dims := Clickable(gtx, cl, func(gtx layout.Context) layout.Dimensions {
|
||||
system.ActionInputOp(a).Add(gtx.Ops)
|
||||
paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops)
|
||||
return inset.Layout(gtx, w)
|
||||
})
|
||||
size.X += dims.Size.X
|
||||
if size.Y < dims.Size.Y {
|
||||
size.Y = dims.Size.Y
|
||||
}
|
||||
op.Offset(image.Pt(dims.Size.X, 0)).Add(gtx.Ops)
|
||||
}
|
||||
return layout.Dimensions{Size: size}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const (
|
||||
winIconSize = unit.Dp(20)
|
||||
winIconMargin = unit.Dp(4)
|
||||
winIconStroke = unit.Dp(2)
|
||||
)
|
||||
|
||||
// minimizeWindows draws a line icon representing the minimize action.
|
||||
func minimizeWindow(gtx layout.Context) layout.Dimensions {
|
||||
size := gtx.Dp(winIconSize)
|
||||
size32 := float32(size)
|
||||
margin := float32(gtx.Dp(winIconMargin))
|
||||
width := float32(gtx.Dp(winIconStroke))
|
||||
var p clip.Path
|
||||
p.Begin(gtx.Ops)
|
||||
p.MoveTo(f32.Point{X: margin, Y: size32 - margin})
|
||||
p.LineTo(f32.Point{X: size32 - 2*margin, Y: size32 - margin})
|
||||
st := clip.Stroke{
|
||||
Path: p.End(),
|
||||
Width: width,
|
||||
}.Op().Push(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
st.Pop()
|
||||
return layout.Dimensions{Size: image.Pt(size, size)}
|
||||
}
|
||||
|
||||
// maximizeWindow draws a rectangle representing the maximize action.
|
||||
func maximizeWindow(gtx layout.Context) layout.Dimensions {
|
||||
size := gtx.Dp(winIconSize)
|
||||
margin := gtx.Dp(winIconMargin)
|
||||
width := gtx.Dp(winIconStroke)
|
||||
r := clip.RRect{
|
||||
Rect: image.Rect(margin, margin, size-margin, size-margin),
|
||||
}
|
||||
st := clip.Stroke{
|
||||
Path: r.Path(gtx.Ops),
|
||||
Width: float32(width),
|
||||
}.Op().Push(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
st.Pop()
|
||||
r.Rect.Max = image.Pt(size-margin, 2*margin)
|
||||
st = clip.Outline{
|
||||
Path: r.Path(gtx.Ops),
|
||||
}.Op().Push(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
st.Pop()
|
||||
return layout.Dimensions{Size: image.Pt(size, size)}
|
||||
}
|
||||
|
||||
// maximizedWindow draws interleaved rectangles representing the un-maximize action.
|
||||
func maximizedWindow(gtx layout.Context) layout.Dimensions {
|
||||
size := gtx.Dp(winIconSize)
|
||||
margin := gtx.Dp(winIconMargin)
|
||||
width := gtx.Dp(winIconStroke)
|
||||
r := clip.RRect{
|
||||
Rect: image.Rect(margin, margin, size-2*margin, size-2*margin),
|
||||
}
|
||||
st := clip.Stroke{
|
||||
Path: r.Path(gtx.Ops),
|
||||
Width: float32(width),
|
||||
}.Op().Push(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
st.Pop()
|
||||
r = clip.RRect{
|
||||
Rect: image.Rect(2*margin, 2*margin, size-margin, size-margin),
|
||||
}
|
||||
st = clip.Stroke{
|
||||
Path: r.Path(gtx.Ops),
|
||||
Width: float32(width),
|
||||
}.Op().Push(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
st.Pop()
|
||||
return layout.Dimensions{Size: image.Pt(size, size)}
|
||||
}
|
||||
|
||||
// closeWindow draws a cross representing the close action.
|
||||
func closeWindow(gtx layout.Context) layout.Dimensions {
|
||||
size := gtx.Dp(winIconSize)
|
||||
size32 := float32(size)
|
||||
margin := float32(gtx.Dp(winIconMargin))
|
||||
width := float32(gtx.Dp(winIconStroke))
|
||||
var p clip.Path
|
||||
p.Begin(gtx.Ops)
|
||||
p.MoveTo(f32.Point{X: margin, Y: margin})
|
||||
p.LineTo(f32.Point{X: size32 - margin, Y: size32 - margin})
|
||||
p.MoveTo(f32.Point{X: size32 - margin, Y: margin})
|
||||
p.LineTo(f32.Point{X: margin, Y: size32 - margin})
|
||||
st := clip.Stroke{
|
||||
Path: p.End(),
|
||||
Width: width,
|
||||
}.Op().Push(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
st.Pop()
|
||||
return layout.Dimensions{Size: image.Pt(size, size)}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
// Package material implements the Material design.
|
||||
//
|
||||
// To maximize reusability and visual flexibility, user interface controls are
|
||||
// split into two parts: the stateful widget and the stateless drawing of it.
|
||||
//
|
||||
// For example, widget.Clickable encapsulates the state and event
|
||||
// handling of all clickable areas, while the Theme is responsible to
|
||||
// draw a specific area, for example a button.
|
||||
//
|
||||
// This snippet defines a button that prints a message when clicked:
|
||||
//
|
||||
// var gtx layout.Context
|
||||
// button := new(widget.Clickable)
|
||||
//
|
||||
// for button.Clicked(gtx) {
|
||||
// fmt.Println("Clicked!")
|
||||
// }
|
||||
//
|
||||
// Use a Theme to draw the button:
|
||||
//
|
||||
// theme := material.NewTheme(...)
|
||||
//
|
||||
// material.Button(theme, button, "Click me!").Layout(gtx)
|
||||
//
|
||||
// # Customization
|
||||
//
|
||||
// Quite often, a program needs to customize the theme-provided defaults. Several
|
||||
// options are available, depending on the nature of the change.
|
||||
//
|
||||
// Mandatory parameters: Some parameters are not part of the widget state but
|
||||
// have no obvious default. In the program above, the button text is a
|
||||
// parameter to the Theme.Button method.
|
||||
//
|
||||
// Theme-global parameters: For changing the look of all widgets drawn with a
|
||||
// particular theme, adjust the `Theme` fields:
|
||||
//
|
||||
// theme.Palette.Fg = color.NRGBA{...}
|
||||
//
|
||||
// Widget-local parameters: For changing the look of a particular widget,
|
||||
// adjust the widget specific theme object:
|
||||
//
|
||||
// btn := material.Button(theme, button, "Click me!")
|
||||
// btn.Font.Style = text.Italic
|
||||
// btn.Layout(gtx)
|
||||
//
|
||||
// Widget variants: A widget can have several distinct representations even
|
||||
// though the underlying state is the same. A widget.Clickable can be drawn as a
|
||||
// round icon button:
|
||||
//
|
||||
// icon := widget.NewIcon(...)
|
||||
//
|
||||
// material.IconButton(theme, button, icon, "Click me!").Layout(gtx)
|
||||
//
|
||||
// Specialized widgets: Theme both define a generic Label method
|
||||
// that takes a text size, and specialized methods for standard text
|
||||
// sizes such as Theme.H1 and Theme.Body2.
|
||||
package material
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
type EditorStyle struct {
|
||||
Font font.Font
|
||||
// LineHeight controls the distance between the baselines of lines of text.
|
||||
// If zero, a sensible default will be used.
|
||||
LineHeight unit.Sp
|
||||
// LineHeightScale applies a scaling factor to the LineHeight. If zero, a
|
||||
// sensible default will be used.
|
||||
LineHeightScale float32
|
||||
TextSize unit.Sp
|
||||
// Color is the text color.
|
||||
Color color.NRGBA
|
||||
// Hint contains the text displayed when the editor is empty.
|
||||
Hint string
|
||||
// HintColor is the color of hint text.
|
||||
HintColor color.NRGBA
|
||||
// SelectionColor is the color of the background for selected text.
|
||||
SelectionColor color.NRGBA
|
||||
Editor *widget.Editor
|
||||
|
||||
shaper *text.Shaper
|
||||
}
|
||||
|
||||
func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle {
|
||||
return EditorStyle{
|
||||
Editor: editor,
|
||||
Font: font.Font{
|
||||
Typeface: th.Face,
|
||||
},
|
||||
TextSize: th.TextSize,
|
||||
Color: th.Palette.Fg,
|
||||
shaper: th.Shaper,
|
||||
Hint: hint,
|
||||
HintColor: f32color.MulAlpha(th.Palette.Fg, 0xbb),
|
||||
SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60),
|
||||
}
|
||||
}
|
||||
|
||||
func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
// Choose colors.
|
||||
textColorMacro := op.Record(gtx.Ops)
|
||||
paint.ColorOp{Color: e.Color}.Add(gtx.Ops)
|
||||
textColor := textColorMacro.Stop()
|
||||
hintColorMacro := op.Record(gtx.Ops)
|
||||
paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops)
|
||||
hintColor := hintColorMacro.Stop()
|
||||
selectionColorMacro := op.Record(gtx.Ops)
|
||||
paint.ColorOp{Color: blendDisabledColor(!gtx.Enabled(), e.SelectionColor)}.Add(gtx.Ops)
|
||||
selectionColor := selectionColorMacro.Stop()
|
||||
|
||||
var maxlines int
|
||||
if e.Editor.SingleLine {
|
||||
maxlines = 1
|
||||
}
|
||||
|
||||
macro := op.Record(gtx.Ops)
|
||||
tl := widget.Label{
|
||||
Alignment: e.Editor.Alignment,
|
||||
MaxLines: maxlines,
|
||||
LineHeight: e.LineHeight,
|
||||
LineHeightScale: e.LineHeightScale,
|
||||
}
|
||||
dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint, hintColor)
|
||||
call := macro.Stop()
|
||||
|
||||
if w := dims.Size.X; gtx.Constraints.Min.X < w {
|
||||
gtx.Constraints.Min.X = w
|
||||
}
|
||||
if h := dims.Size.Y; gtx.Constraints.Min.Y < h {
|
||||
gtx.Constraints.Min.Y = h
|
||||
}
|
||||
e.Editor.LineHeight = e.LineHeight
|
||||
e.Editor.LineHeightScale = e.LineHeightScale
|
||||
dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize, textColor, selectionColor)
|
||||
if e.Editor.Len() == 0 {
|
||||
call.Add(gtx.Ops)
|
||||
}
|
||||
return dims
|
||||
}
|
||||
|
||||
func blendDisabledColor(disabled bool, c color.NRGBA) color.NRGBA {
|
||||
if disabled {
|
||||
return f32color.Disabled(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
// LabelStyle configures the presentation of text. If the State field is set, the
|
||||
// label will be laid out as interactive (able to be selected and copied). Otherwise,
|
||||
// the label will be non-interactive.
|
||||
type LabelStyle struct {
|
||||
// Face defines the text style.
|
||||
Font font.Font
|
||||
// Color is the text color.
|
||||
Color color.NRGBA
|
||||
// SelectionColor is the color of the background for selected text.
|
||||
SelectionColor color.NRGBA
|
||||
// Alignment specify the text alignment.
|
||||
Alignment text.Alignment
|
||||
// MaxLines limits the number of lines. Zero means no limit.
|
||||
MaxLines int
|
||||
// WrapPolicy configures how displayed text will be broken into lines.
|
||||
WrapPolicy text.WrapPolicy
|
||||
// Truncator is the text that will be shown at the end of the final
|
||||
// line if MaxLines is exceeded. Defaults to "…" if empty.
|
||||
Truncator string
|
||||
// Text is the content displayed by the label.
|
||||
Text string
|
||||
// TextSize determines the size of the text glyphs.
|
||||
TextSize unit.Sp
|
||||
// LineHeight controls the distance between the baselines of lines of text.
|
||||
// If zero, a sensible default will be used.
|
||||
LineHeight unit.Sp
|
||||
// LineHeightScale applies a scaling factor to the LineHeight. If zero, a
|
||||
// sensible default will be used.
|
||||
LineHeightScale float32
|
||||
|
||||
// Shaper is the text shaper used to display this labe. This field is automatically
|
||||
// set using by all constructor functions. If constructing a LabelStyle literal, you
|
||||
// must provide a Shaper or displaying text will panic.
|
||||
Shaper *text.Shaper
|
||||
// State provides text selection state for the label. If not set, the label cannot
|
||||
// be selected or copied interactively.
|
||||
State *widget.Selectable
|
||||
}
|
||||
|
||||
func H1(th *Theme, txt string) LabelStyle {
|
||||
label := Label(th, th.TextSize*96.0/16.0, txt)
|
||||
label.Font.Weight = font.Light
|
||||
return label
|
||||
}
|
||||
|
||||
func H2(th *Theme, txt string) LabelStyle {
|
||||
label := Label(th, th.TextSize*60.0/16.0, txt)
|
||||
label.Font.Weight = font.Light
|
||||
return label
|
||||
}
|
||||
|
||||
func H3(th *Theme, txt string) LabelStyle {
|
||||
return Label(th, th.TextSize*48.0/16.0, txt)
|
||||
}
|
||||
|
||||
func H4(th *Theme, txt string) LabelStyle {
|
||||
return Label(th, th.TextSize*34.0/16.0, txt)
|
||||
}
|
||||
|
||||
func H5(th *Theme, txt string) LabelStyle {
|
||||
return Label(th, th.TextSize*24.0/16.0, txt)
|
||||
}
|
||||
|
||||
func H6(th *Theme, txt string) LabelStyle {
|
||||
label := Label(th, th.TextSize*20.0/16.0, txt)
|
||||
label.Font.Weight = font.Medium
|
||||
return label
|
||||
}
|
||||
|
||||
func Subtitle1(th *Theme, txt string) LabelStyle {
|
||||
return Label(th, th.TextSize*16.0/16.0, txt)
|
||||
}
|
||||
|
||||
func Subtitle2(th *Theme, txt string) LabelStyle {
|
||||
label := Label(th, th.TextSize*14.0/16.0, txt)
|
||||
label.Font.Weight = font.Medium
|
||||
return label
|
||||
}
|
||||
|
||||
func Body1(th *Theme, txt string) LabelStyle {
|
||||
return Label(th, th.TextSize, txt)
|
||||
}
|
||||
|
||||
func Body2(th *Theme, txt string) LabelStyle {
|
||||
return Label(th, th.TextSize*14.0/16.0, txt)
|
||||
}
|
||||
|
||||
func Caption(th *Theme, txt string) LabelStyle {
|
||||
return Label(th, th.TextSize*12.0/16.0, txt)
|
||||
}
|
||||
|
||||
func Overline(th *Theme, txt string) LabelStyle {
|
||||
return Label(th, th.TextSize*10.0/16.0, txt)
|
||||
}
|
||||
|
||||
func Label(th *Theme, size unit.Sp, txt string) LabelStyle {
|
||||
l := LabelStyle{
|
||||
Text: txt,
|
||||
Color: th.Palette.Fg,
|
||||
SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60),
|
||||
TextSize: size,
|
||||
Shaper: th.Shaper,
|
||||
}
|
||||
l.Font.Typeface = th.Face
|
||||
return l
|
||||
}
|
||||
|
||||
func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
textColorMacro := op.Record(gtx.Ops)
|
||||
paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
|
||||
textColor := textColorMacro.Stop()
|
||||
selectColorMacro := op.Record(gtx.Ops)
|
||||
paint.ColorOp{Color: l.SelectionColor}.Add(gtx.Ops)
|
||||
selectColor := selectColorMacro.Stop()
|
||||
|
||||
if l.State != nil {
|
||||
if l.State.Text() != l.Text {
|
||||
l.State.SetText(l.Text)
|
||||
}
|
||||
l.State.Alignment = l.Alignment
|
||||
l.State.MaxLines = l.MaxLines
|
||||
l.State.Truncator = l.Truncator
|
||||
l.State.WrapPolicy = l.WrapPolicy
|
||||
l.State.LineHeight = l.LineHeight
|
||||
l.State.LineHeightScale = l.LineHeightScale
|
||||
return l.State.Layout(gtx, l.Shaper, l.Font, l.TextSize, textColor, selectColor)
|
||||
}
|
||||
tl := widget.Label{
|
||||
Alignment: l.Alignment,
|
||||
MaxLines: l.MaxLines,
|
||||
Truncator: l.Truncator,
|
||||
WrapPolicy: l.WrapPolicy,
|
||||
LineHeight: l.LineHeight,
|
||||
LineHeightScale: l.LineHeightScale,
|
||||
}
|
||||
return tl.Layout(gtx, l.Shaper, l.Font, l.TextSize, l.Text, textColor)
|
||||
}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
// fromListPosition converts a layout.Position into two floats representing
|
||||
// the location of the viewport on the underlying content. It needs to know
|
||||
// the number of elements in the list and the major-axis size of the list
|
||||
// in order to do this. The returned values will be in the range [0,1], and
|
||||
// start will be less than or equal to end.
|
||||
func fromListPosition(lp layout.Position, elements int, majorAxisSize int) (start, end float32) {
|
||||
// Approximate the size of the scrollable content.
|
||||
lengthEstPx := float32(lp.Length)
|
||||
elementLenEstPx := lengthEstPx / float32(elements)
|
||||
|
||||
// Determine how much of the content is visible.
|
||||
listOffsetF := float32(lp.Offset)
|
||||
listOffsetL := float32(lp.OffsetLast)
|
||||
|
||||
// Compute the location of the beginning of the viewport using estimated element size and known
|
||||
// pixel offsets.
|
||||
viewportStart := clamp1((float32(lp.First)*elementLenEstPx + listOffsetF) / lengthEstPx)
|
||||
viewportEnd := clamp1((float32(lp.First+lp.Count)*elementLenEstPx + listOffsetL) / lengthEstPx)
|
||||
viewportFraction := viewportEnd - viewportStart
|
||||
|
||||
// Compute the expected visible proportion of the list content based solely on the ratio
|
||||
// of the visible size and the estimated total size.
|
||||
visiblePx := float32(majorAxisSize)
|
||||
visibleFraction := visiblePx / lengthEstPx
|
||||
|
||||
// Compute the error between the two methods of determining the viewport and diffuse the
|
||||
// error on either end of the viewport based on how close we are to each end.
|
||||
err := visibleFraction - viewportFraction
|
||||
adjStart := viewportStart
|
||||
adjEnd := viewportEnd
|
||||
if viewportFraction < 1 {
|
||||
startShare := viewportStart / (1 - viewportFraction)
|
||||
endShare := (1 - viewportEnd) / (1 - viewportFraction)
|
||||
startErr := startShare * err
|
||||
endErr := endShare * err
|
||||
|
||||
adjStart -= startErr
|
||||
adjEnd += endErr
|
||||
}
|
||||
return adjStart, adjEnd
|
||||
}
|
||||
|
||||
// rangeIsScrollable returns whether the viewport described by start and end
|
||||
// is smaller than the underlying content (such that it can be scrolled).
|
||||
// start and end are expected to each be in the range [0,1], and start
|
||||
// must be less than or equal to end.
|
||||
func rangeIsScrollable(start, end float32) bool {
|
||||
return end-start < 1
|
||||
}
|
||||
|
||||
// ScrollTrackStyle configures the presentation of a track for a scroll area.
|
||||
type ScrollTrackStyle struct {
|
||||
// MajorPadding and MinorPadding along the major and minor axis of the
|
||||
// scrollbar's track. This is used to keep the scrollbar from touching
|
||||
// the edges of the content area.
|
||||
MajorPadding, MinorPadding unit.Dp
|
||||
// Color of the track background.
|
||||
Color color.NRGBA
|
||||
}
|
||||
|
||||
// ScrollIndicatorStyle configures the presentation of a scroll indicator.
|
||||
type ScrollIndicatorStyle struct {
|
||||
// MajorMinLen is the smallest that the scroll indicator is allowed to
|
||||
// be along the major axis.
|
||||
MajorMinLen unit.Dp
|
||||
// MinorWidth is the width of the scroll indicator across the minor axis.
|
||||
MinorWidth unit.Dp
|
||||
// Color and HoverColor are the normal and hovered colors of the scroll
|
||||
// indicator.
|
||||
Color, HoverColor color.NRGBA
|
||||
// CornerRadius is the corner radius of the rectangular indicator. 0
|
||||
// will produce square corners. 0.5*MinorWidth will produce perfectly
|
||||
// round corners.
|
||||
CornerRadius unit.Dp
|
||||
}
|
||||
|
||||
// ScrollbarStyle configures the presentation of a scrollbar.
|
||||
type ScrollbarStyle struct {
|
||||
Scrollbar *widget.Scrollbar
|
||||
Track ScrollTrackStyle
|
||||
Indicator ScrollIndicatorStyle
|
||||
}
|
||||
|
||||
// Scrollbar configures the presentation of a scrollbar using the provided
|
||||
// theme and state.
|
||||
func Scrollbar(th *Theme, state *widget.Scrollbar) ScrollbarStyle {
|
||||
lightFg := th.Palette.Fg
|
||||
lightFg.A = 150
|
||||
darkFg := lightFg
|
||||
darkFg.A = 200
|
||||
|
||||
return ScrollbarStyle{
|
||||
Scrollbar: state,
|
||||
Track: ScrollTrackStyle{
|
||||
MajorPadding: 2,
|
||||
MinorPadding: 2,
|
||||
},
|
||||
Indicator: ScrollIndicatorStyle{
|
||||
MajorMinLen: th.FingerSize,
|
||||
MinorWidth: 6,
|
||||
CornerRadius: 3,
|
||||
Color: lightFg,
|
||||
HoverColor: darkFg,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Width returns the minor axis width of the scrollbar in its current
|
||||
// configuration (taking padding for the scroll track into account).
|
||||
func (s ScrollbarStyle) Width() unit.Dp {
|
||||
return s.Indicator.MinorWidth + s.Track.MinorPadding + s.Track.MinorPadding
|
||||
}
|
||||
|
||||
// Layout the scrollbar.
|
||||
func (s ScrollbarStyle) Layout(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) layout.Dimensions {
|
||||
if !rangeIsScrollable(viewportStart, viewportEnd) {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
|
||||
// Set minimum constraints in an axis-independent way, then convert to
|
||||
// the correct representation for the current axis.
|
||||
convert := axis.Convert
|
||||
maxMajorAxis := convert(gtx.Constraints.Max).X
|
||||
gtx.Constraints.Min.X = maxMajorAxis
|
||||
gtx.Constraints.Min.Y = gtx.Dp(s.Width())
|
||||
gtx.Constraints.Min = convert(gtx.Constraints.Min)
|
||||
gtx.Constraints.Max = gtx.Constraints.Min
|
||||
|
||||
s.Scrollbar.Update(gtx, axis, viewportStart, viewportEnd)
|
||||
|
||||
// Darken indicator if hovered.
|
||||
if s.Scrollbar.IndicatorHovered() {
|
||||
s.Indicator.Color = s.Indicator.HoverColor
|
||||
}
|
||||
|
||||
return s.layout(gtx, axis, viewportStart, viewportEnd)
|
||||
}
|
||||
|
||||
// layout the scroll track and indicator.
|
||||
func (s ScrollbarStyle) layout(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) layout.Dimensions {
|
||||
inset := layout.Inset{
|
||||
Top: s.Track.MajorPadding,
|
||||
Bottom: s.Track.MajorPadding,
|
||||
Left: s.Track.MinorPadding,
|
||||
Right: s.Track.MinorPadding,
|
||||
}
|
||||
if axis == layout.Horizontal {
|
||||
inset.Top, inset.Bottom, inset.Left, inset.Right = inset.Left, inset.Right, inset.Top, inset.Bottom
|
||||
}
|
||||
|
||||
return layout.Background{}.Layout(gtx,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
// Lay out the draggable track underneath the scroll indicator.
|
||||
area := image.Rectangle{
|
||||
Max: gtx.Constraints.Min,
|
||||
}
|
||||
pointerArea := clip.Rect(area)
|
||||
defer pointerArea.Push(gtx.Ops).Pop()
|
||||
s.Scrollbar.AddDrag(gtx.Ops)
|
||||
|
||||
// Stack a normal clickable area on top of the draggable area
|
||||
// to capture non-dragging clicks.
|
||||
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
|
||||
defer pointerArea.Push(gtx.Ops).Pop()
|
||||
s.Scrollbar.AddTrack(gtx.Ops)
|
||||
|
||||
paint.FillShape(gtx.Ops, s.Track.Color, clip.Rect(area).Op())
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
// Use axis-independent constraints.
|
||||
gtx.Constraints.Min = axis.Convert(gtx.Constraints.Min)
|
||||
gtx.Constraints.Max = axis.Convert(gtx.Constraints.Max)
|
||||
|
||||
// Compute the pixel size and position of the scroll indicator within
|
||||
// the track.
|
||||
trackLen := gtx.Constraints.Min.X
|
||||
viewStart := int(math.Round(float64(viewportStart) * float64(trackLen)))
|
||||
viewEnd := int(math.Round(float64(viewportEnd) * float64(trackLen)))
|
||||
indicatorLen := max(viewEnd-viewStart, gtx.Dp(s.Indicator.MajorMinLen))
|
||||
if viewStart+indicatorLen > trackLen {
|
||||
viewStart = trackLen - indicatorLen
|
||||
}
|
||||
indicatorDims := axis.Convert(image.Point{
|
||||
X: indicatorLen,
|
||||
Y: gtx.Dp(s.Indicator.MinorWidth),
|
||||
})
|
||||
radius := gtx.Dp(s.Indicator.CornerRadius)
|
||||
|
||||
// Lay out the indicator.
|
||||
offset := axis.Convert(image.Pt(viewStart, 0))
|
||||
defer op.Offset(offset).Push(gtx.Ops).Pop()
|
||||
paint.FillShape(gtx.Ops, s.Indicator.Color, clip.RRect{
|
||||
Rect: image.Rectangle{
|
||||
Max: indicatorDims,
|
||||
},
|
||||
SW: radius,
|
||||
NW: radius,
|
||||
NE: radius,
|
||||
SE: radius,
|
||||
}.Op(gtx.Ops))
|
||||
|
||||
// Add the indicator pointer hit area.
|
||||
area := clip.Rect(image.Rectangle{Max: indicatorDims})
|
||||
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
|
||||
defer area.Push(gtx.Ops).Pop()
|
||||
s.Scrollbar.AddIndicator(gtx.Ops)
|
||||
|
||||
return layout.Dimensions{Size: axis.Convert(gtx.Constraints.Min)}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// AnchorStrategy defines a means of attaching a scrollbar to content.
|
||||
type AnchorStrategy uint8
|
||||
|
||||
const (
|
||||
// Occupy reserves space for the scrollbar, making the underlying
|
||||
// content region smaller on one axis.
|
||||
Occupy AnchorStrategy = iota
|
||||
// Overlay causes the scrollbar to float atop the content without
|
||||
// occupying any space. Content in the underlying area can be occluded
|
||||
// by the scrollbar.
|
||||
Overlay
|
||||
)
|
||||
|
||||
// ListStyle configures the presentation of a layout.List with a scrollbar.
|
||||
type ListStyle struct {
|
||||
state *widget.List
|
||||
ScrollbarStyle
|
||||
AnchorStrategy
|
||||
}
|
||||
|
||||
// List constructs a ListStyle using the provided theme and state.
|
||||
func List(th *Theme, state *widget.List) ListStyle {
|
||||
return ListStyle{
|
||||
state: state,
|
||||
ScrollbarStyle: Scrollbar(th, &state.Scrollbar),
|
||||
}
|
||||
}
|
||||
|
||||
// Layout the list and its scrollbar.
|
||||
func (l ListStyle) Layout(gtx layout.Context, length int, w layout.ListElement) layout.Dimensions {
|
||||
originalConstraints := gtx.Constraints
|
||||
|
||||
// Determine how much space the scrollbar occupies.
|
||||
barWidth := gtx.Dp(l.Width())
|
||||
|
||||
if l.AnchorStrategy == Occupy {
|
||||
|
||||
// Reserve space for the scrollbar using the gtx constraints.
|
||||
max := l.state.Axis.Convert(gtx.Constraints.Max)
|
||||
min := l.state.Axis.Convert(gtx.Constraints.Min)
|
||||
max.Y -= barWidth
|
||||
if max.Y < 0 {
|
||||
max.Y = 0
|
||||
}
|
||||
min.Y -= barWidth
|
||||
if min.Y < 0 {
|
||||
min.Y = 0
|
||||
}
|
||||
gtx.Constraints.Max = l.state.Axis.Convert(max)
|
||||
gtx.Constraints.Min = l.state.Axis.Convert(min)
|
||||
}
|
||||
|
||||
listDims := l.state.List.Layout(gtx, length, w)
|
||||
gtx.Constraints = originalConstraints
|
||||
|
||||
// Draw the scrollbar.
|
||||
anchoring := layout.E
|
||||
if l.state.Axis == layout.Horizontal {
|
||||
anchoring = layout.S
|
||||
}
|
||||
majorAxisSize := l.state.Axis.Convert(listDims.Size).X
|
||||
start, end := fromListPosition(l.state.Position, length, majorAxisSize)
|
||||
// layout.Direction respects the minimum, so ensure that the
|
||||
// scrollbar will be drawn on the correct edge even if the provided
|
||||
// layout.Context had a zero minimum constraint.
|
||||
gtx.Constraints.Min = listDims.Size
|
||||
if l.AnchorStrategy == Occupy {
|
||||
min := l.state.Axis.Convert(gtx.Constraints.Min)
|
||||
min.Y += barWidth
|
||||
gtx.Constraints.Min = l.state.Axis.Convert(min)
|
||||
}
|
||||
anchoring.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return l.ScrollbarStyle.Layout(gtx, l.state.Axis, start, end)
|
||||
})
|
||||
|
||||
if delta := l.state.ScrollDistance(); delta != 0 {
|
||||
// Handle any changes to the list position as a result of user interaction
|
||||
// with the scrollbar.
|
||||
l.state.List.ScrollBy(delta * float32(length))
|
||||
}
|
||||
|
||||
if l.AnchorStrategy == Occupy {
|
||||
// Increase the width to account for the space occupied by the scrollbar.
|
||||
cross := l.state.Axis.Convert(listDims.Size)
|
||||
cross.Y += barWidth
|
||||
listDims.Size = l.state.Axis.Convert(cross)
|
||||
}
|
||||
|
||||
return listDims
|
||||
}
|
||||
|
||||
// LayoutWidgets the widgets and its scrollbar.
|
||||
func (l ListStyle) LayoutWidgets(gtx layout.Context, widgets ...layout.Widget) layout.Dimensions {
|
||||
return l.Layout(gtx, len(widgets), func(gtx layout.Context, index int) layout.Dimensions {
|
||||
return widgets[index](gtx)
|
||||
})
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
|
||||
type LoaderStyle struct {
|
||||
Color color.NRGBA
|
||||
}
|
||||
|
||||
func Loader(th *Theme) LoaderStyle {
|
||||
return LoaderStyle{
|
||||
Color: th.Palette.ContrastBg,
|
||||
}
|
||||
}
|
||||
|
||||
func (l LoaderStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
diam := gtx.Constraints.Min.X
|
||||
if minY := gtx.Constraints.Min.Y; minY > diam {
|
||||
diam = minY
|
||||
}
|
||||
if diam == 0 {
|
||||
diam = gtx.Dp(24)
|
||||
}
|
||||
sz := gtx.Constraints.Constrain(image.Pt(diam, diam))
|
||||
radius := sz.X / 2
|
||||
defer op.Offset(image.Pt(radius, radius)).Push(gtx.Ops).Pop()
|
||||
|
||||
dt := float32((time.Duration(gtx.Now.UnixNano()) % (time.Second)).Seconds())
|
||||
startAngle := dt * math.Pi * 2
|
||||
endAngle := startAngle + math.Pi*1.5
|
||||
|
||||
defer clipLoader(gtx.Ops, startAngle, endAngle, float32(radius)).Push(gtx.Ops).Pop()
|
||||
paint.ColorOp{
|
||||
Color: l.Color,
|
||||
}.Add(gtx.Ops)
|
||||
defer op.Offset(image.Pt(-radius, -radius)).Push(gtx.Ops).Pop()
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
gtx.Execute(op.InvalidateCmd{})
|
||||
return layout.Dimensions{
|
||||
Size: sz,
|
||||
}
|
||||
}
|
||||
|
||||
func clipLoader(ops *op.Ops, startAngle, endAngle, radius float32) clip.Op {
|
||||
const thickness = .25
|
||||
|
||||
var (
|
||||
width = radius * thickness
|
||||
delta = endAngle - startAngle
|
||||
|
||||
vy, vx = math.Sincos(float64(startAngle))
|
||||
|
||||
inner = radius * (1. - thickness*.5)
|
||||
pen = f32.Pt(float32(vx), float32(vy)).Mul(inner)
|
||||
center = f32.Pt(0, 0).Sub(pen)
|
||||
|
||||
p clip.Path
|
||||
)
|
||||
|
||||
p.Begin(ops)
|
||||
p.Move(pen)
|
||||
p.Arc(center, center, delta)
|
||||
return clip.Stroke{
|
||||
Path: p.End(),
|
||||
Width: width,
|
||||
}.Op()
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
type ProgressBarStyle struct {
|
||||
Color color.NRGBA
|
||||
Height unit.Dp
|
||||
Radius unit.Dp
|
||||
TrackColor color.NRGBA
|
||||
Progress float32
|
||||
}
|
||||
|
||||
func ProgressBar(th *Theme, progress float32) ProgressBarStyle {
|
||||
return ProgressBarStyle{
|
||||
Progress: progress,
|
||||
Height: unit.Dp(4),
|
||||
Radius: unit.Dp(2),
|
||||
Color: th.Palette.ContrastBg,
|
||||
TrackColor: f32color.MulAlpha(th.Palette.Fg, 0x88),
|
||||
}
|
||||
}
|
||||
|
||||
func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
shader := func(width int, color color.NRGBA) layout.Dimensions {
|
||||
d := image.Point{X: width, Y: gtx.Dp(p.Height)}
|
||||
rr := gtx.Dp(p.Radius)
|
||||
|
||||
defer clip.UniformRRect(image.Rectangle{Max: image.Pt(width, d.Y)}, rr).Push(gtx.Ops).Pop()
|
||||
paint.ColorOp{Color: color}.Add(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
|
||||
return layout.Dimensions{Size: d}
|
||||
}
|
||||
|
||||
progressBarWidth := gtx.Constraints.Max.X
|
||||
return layout.Stack{Alignment: layout.W}.Layout(gtx,
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
return shader(progressBarWidth, p.TrackColor)
|
||||
}),
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
fillWidth := int(float32(progressBarWidth) * clamp1(p.Progress))
|
||||
fillColor := p.Color
|
||||
if !gtx.Enabled() {
|
||||
fillColor = f32color.Disabled(fillColor)
|
||||
}
|
||||
if fillWidth < int(p.Radius*2) {
|
||||
fillWidth = int(p.Radius * 2)
|
||||
}
|
||||
return shader(fillWidth, fillColor)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// clamp1 limits v to range [0..1].
|
||||
func clamp1(v float32) float32 {
|
||||
if v >= 1 {
|
||||
return 1
|
||||
} else if v <= 0 {
|
||||
return 0
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
|
||||
type ProgressCircleStyle struct {
|
||||
Color color.NRGBA
|
||||
Progress float32
|
||||
}
|
||||
|
||||
func ProgressCircle(th *Theme, progress float32) ProgressCircleStyle {
|
||||
return ProgressCircleStyle{
|
||||
Color: th.Palette.ContrastBg,
|
||||
Progress: progress,
|
||||
}
|
||||
}
|
||||
|
||||
func (p ProgressCircleStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
diam := gtx.Constraints.Min.X
|
||||
if minY := gtx.Constraints.Min.Y; minY > diam {
|
||||
diam = minY
|
||||
}
|
||||
if diam == 0 {
|
||||
diam = gtx.Dp(24)
|
||||
}
|
||||
sz := gtx.Constraints.Constrain(image.Pt(diam, diam))
|
||||
radius := sz.X / 2
|
||||
defer op.Offset(image.Pt(radius, radius)).Push(gtx.Ops).Pop()
|
||||
|
||||
defer clipLoader(gtx.Ops, -math.Pi/2, -math.Pi/2+math.Pi*2*p.Progress, float32(radius)).Push(gtx.Ops).Pop()
|
||||
paint.ColorOp{
|
||||
Color: p.Color,
|
||||
}.Add(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
return layout.Dimensions{
|
||||
Size: sz,
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
type RadioButtonStyle struct {
|
||||
checkable
|
||||
Key string
|
||||
Group *widget.Enum
|
||||
}
|
||||
|
||||
// RadioButton returns a RadioButton with a label. The key specifies
|
||||
// the value for the Enum.
|
||||
func RadioButton(th *Theme, group *widget.Enum, key, label string) RadioButtonStyle {
|
||||
r := RadioButtonStyle{
|
||||
Group: group,
|
||||
checkable: checkable{
|
||||
Label: label,
|
||||
|
||||
Color: th.Palette.Fg,
|
||||
IconColor: th.Palette.ContrastBg,
|
||||
TextSize: th.TextSize * 14.0 / 16.0,
|
||||
Size: 26,
|
||||
shaper: th.Shaper,
|
||||
checkedStateIcon: th.Icon.RadioChecked,
|
||||
uncheckedStateIcon: th.Icon.RadioUnchecked,
|
||||
},
|
||||
Key: key,
|
||||
}
|
||||
r.checkable.Font.Typeface = th.Face
|
||||
return r
|
||||
}
|
||||
|
||||
// Layout updates enum and displays the radio button.
|
||||
func (r RadioButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
r.Group.Update(gtx)
|
||||
hovered, hovering := r.Group.Hovered()
|
||||
focus, focused := r.Group.Focused()
|
||||
return r.Group.Layout(gtx, r.Key, func(gtx layout.Context) layout.Dimensions {
|
||||
semantic.RadioButton.Add(gtx.Ops)
|
||||
highlight := hovering && hovered == r.Key || focused && focus == r.Key
|
||||
return r.layout(gtx, r.Group.Value == r.Key, highlight)
|
||||
})
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
// Slider is for selecting a value in a range.
|
||||
func Slider(th *Theme, float *widget.Float) SliderStyle {
|
||||
return SliderStyle{
|
||||
Color: th.Palette.ContrastBg,
|
||||
Float: float,
|
||||
FingerSize: th.FingerSize,
|
||||
}
|
||||
}
|
||||
|
||||
type SliderStyle struct {
|
||||
Axis layout.Axis
|
||||
Color color.NRGBA
|
||||
Float *widget.Float
|
||||
|
||||
FingerSize unit.Dp
|
||||
}
|
||||
|
||||
func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
const thumbRadius unit.Dp = 6
|
||||
tr := gtx.Dp(thumbRadius)
|
||||
trackWidth := gtx.Dp(2)
|
||||
|
||||
axis := s.Axis
|
||||
// Keep a minimum length so that the track is always visible.
|
||||
minLength := tr + 3*tr + tr
|
||||
// Try to expand to finger size, but only if the constraints
|
||||
// allow for it.
|
||||
touchSizePx := min(gtx.Dp(s.FingerSize), axis.Convert(gtx.Constraints.Max).Y)
|
||||
sizeMain := max(axis.Convert(gtx.Constraints.Min).X, minLength)
|
||||
sizeCross := max(2*tr, touchSizePx)
|
||||
size := axis.Convert(image.Pt(sizeMain, sizeCross))
|
||||
|
||||
o := axis.Convert(image.Pt(tr, 0))
|
||||
trans := op.Offset(o).Push(gtx.Ops)
|
||||
gtx.Constraints.Min = axis.Convert(image.Pt(sizeMain-2*tr, sizeCross))
|
||||
dims := s.Float.Layout(gtx, axis, thumbRadius)
|
||||
gtx.Constraints.Min = gtx.Constraints.Min.Add(axis.Convert(image.Pt(0, sizeCross)))
|
||||
thumbPos := tr + int(s.Float.Value*float32(axis.Convert(dims.Size).X))
|
||||
trans.Pop()
|
||||
|
||||
color := s.Color
|
||||
if !gtx.Enabled() {
|
||||
color = f32color.Disabled(color)
|
||||
}
|
||||
|
||||
rect := func(minx, miny, maxx, maxy int) image.Rectangle {
|
||||
r := image.Rect(minx, miny, maxx, maxy)
|
||||
if axis == layout.Vertical {
|
||||
r.Max.X, r.Min.X = sizeMain-r.Min.X, sizeMain-r.Max.X
|
||||
}
|
||||
r.Min = axis.Convert(r.Min)
|
||||
r.Max = axis.Convert(r.Max)
|
||||
return r
|
||||
}
|
||||
|
||||
// Draw track before thumb.
|
||||
track := rect(
|
||||
tr, sizeCross/2-trackWidth/2,
|
||||
thumbPos, sizeCross/2+trackWidth/2,
|
||||
)
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect(track).Op())
|
||||
|
||||
// Draw track after thumb.
|
||||
track = rect(
|
||||
thumbPos, axis.Convert(track.Min).Y,
|
||||
sizeMain-tr, axis.Convert(track.Max).Y,
|
||||
)
|
||||
paint.FillShape(gtx.Ops, f32color.MulAlpha(color, 96), clip.Rect(track).Op())
|
||||
|
||||
// Draw thumb.
|
||||
pt := image.Pt(thumbPos, sizeCross/2)
|
||||
thumb := rect(
|
||||
pt.X-tr, pt.Y-tr,
|
||||
pt.X+tr, pt.Y+tr,
|
||||
)
|
||||
paint.FillShape(gtx.Ops, color, clip.Ellipse(thumb).Op(gtx.Ops))
|
||||
|
||||
return layout.Dimensions{Size: size}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/io/semantic"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
type SwitchStyle struct {
|
||||
Description string
|
||||
Color struct {
|
||||
Enabled color.NRGBA
|
||||
Disabled color.NRGBA
|
||||
Track color.NRGBA
|
||||
}
|
||||
Switch *widget.Bool
|
||||
}
|
||||
|
||||
// Switch is for selecting a boolean value.
|
||||
func Switch(th *Theme, swtch *widget.Bool, description string) SwitchStyle {
|
||||
sw := SwitchStyle{
|
||||
Switch: swtch,
|
||||
Description: description,
|
||||
}
|
||||
sw.Color.Enabled = th.Palette.ContrastBg
|
||||
sw.Color.Disabled = th.Palette.Bg
|
||||
sw.Color.Track = f32color.MulAlpha(th.Palette.Fg, 0x88)
|
||||
return sw
|
||||
}
|
||||
|
||||
// Layout updates the switch and displays it.
|
||||
func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
s.Switch.Update(gtx)
|
||||
trackWidth := gtx.Dp(36)
|
||||
trackHeight := gtx.Dp(16)
|
||||
thumbSize := gtx.Dp(20)
|
||||
trackOff := (thumbSize - trackHeight) / 2
|
||||
|
||||
// Draw track.
|
||||
trackCorner := trackHeight / 2
|
||||
trackRect := image.Rectangle{Max: image.Point{
|
||||
X: trackWidth,
|
||||
Y: trackHeight,
|
||||
}}
|
||||
col := s.Color.Disabled
|
||||
if s.Switch.Value {
|
||||
col = s.Color.Enabled
|
||||
}
|
||||
if !gtx.Enabled() {
|
||||
col = f32color.Disabled(col)
|
||||
}
|
||||
trackColor := s.Color.Track
|
||||
t := op.Offset(image.Point{Y: trackOff}).Push(gtx.Ops)
|
||||
cl := clip.UniformRRect(trackRect, trackCorner).Push(gtx.Ops)
|
||||
paint.ColorOp{Color: trackColor}.Add(gtx.Ops)
|
||||
paint.PaintOp{}.Add(gtx.Ops)
|
||||
cl.Pop()
|
||||
t.Pop()
|
||||
|
||||
// Draw thumb ink.
|
||||
inkSize := gtx.Dp(44)
|
||||
rr := inkSize / 2
|
||||
inkOff := image.Point{
|
||||
X: trackWidth/2 - rr,
|
||||
Y: -rr + trackHeight/2 + trackOff,
|
||||
}
|
||||
t = op.Offset(inkOff).Push(gtx.Ops)
|
||||
gtx.Constraints.Min = image.Pt(inkSize, inkSize)
|
||||
cl = clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops)
|
||||
for _, p := range s.Switch.History() {
|
||||
drawInk(gtx, p)
|
||||
}
|
||||
cl.Pop()
|
||||
t.Pop()
|
||||
|
||||
// Compute thumb offset.
|
||||
if s.Switch.Value {
|
||||
xoff := trackWidth - thumbSize
|
||||
defer op.Offset(image.Point{X: xoff}).Push(gtx.Ops).Pop()
|
||||
}
|
||||
|
||||
thumbRadius := thumbSize / 2
|
||||
|
||||
circle := func(x, y, r int) clip.Op {
|
||||
b := image.Rectangle{
|
||||
Min: image.Pt(x-r, y-r),
|
||||
Max: image.Pt(x+r, y+r),
|
||||
}
|
||||
return clip.Ellipse(b).Op(gtx.Ops)
|
||||
}
|
||||
// Draw hover.
|
||||
if s.Switch.Hovered() || gtx.Focused(s.Switch) {
|
||||
r := thumbRadius * 10 / 17
|
||||
background := f32color.MulAlpha(s.Color.Enabled, 70)
|
||||
paint.FillShape(gtx.Ops, background, circle(thumbRadius, thumbRadius, r))
|
||||
}
|
||||
|
||||
// Draw thumb shadow, a translucent disc slightly larger than the
|
||||
// thumb itself.
|
||||
// Center shadow horizontally and slightly adjust its Y.
|
||||
paint.FillShape(gtx.Ops, argb(0x55000000), circle(thumbRadius, thumbRadius+gtx.Dp(.25), thumbRadius+1))
|
||||
|
||||
// Draw thumb.
|
||||
paint.FillShape(gtx.Ops, col, circle(thumbRadius, thumbRadius, thumbRadius))
|
||||
|
||||
// Set up click area.
|
||||
clickSize := gtx.Dp(40)
|
||||
clickOff := image.Point{
|
||||
X: (thumbSize - clickSize) / 2,
|
||||
Y: (trackHeight-clickSize)/2 + trackOff,
|
||||
}
|
||||
defer op.Offset(clickOff).Push(gtx.Ops).Pop()
|
||||
sz := image.Pt(clickSize, clickSize)
|
||||
defer clip.Ellipse(image.Rectangle{Max: sz}).Push(gtx.Ops).Pop()
|
||||
s.Switch.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
if d := s.Description; d != "" {
|
||||
semantic.DescriptionOp(d).Add(gtx.Ops)
|
||||
}
|
||||
semantic.Switch.Add(gtx.Ops)
|
||||
return layout.Dimensions{Size: sz}
|
||||
})
|
||||
|
||||
dims := image.Point{X: trackWidth, Y: thumbSize}
|
||||
return layout.Dimensions{Size: dims}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package material
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
// Palette contains the minimal set of colors that a widget may need to
|
||||
// draw itself.
|
||||
type Palette struct {
|
||||
// Bg is the background color atop which content is currently being
|
||||
// drawn.
|
||||
Bg color.NRGBA
|
||||
|
||||
// Fg is a color suitable for drawing on top of Bg.
|
||||
Fg color.NRGBA
|
||||
|
||||
// ContrastBg is a color used to draw attention to active,
|
||||
// important, interactive widgets such as buttons.
|
||||
ContrastBg color.NRGBA
|
||||
|
||||
// ContrastFg is a color suitable for content drawn on top of
|
||||
// ContrastBg.
|
||||
ContrastFg color.NRGBA
|
||||
}
|
||||
|
||||
// Theme holds the general theme of an app or window. Different top-level
|
||||
// windows should have different instances of Theme (with different Shapers;
|
||||
// see the godoc for [text.Shaper]), though their other fields can be equal.
|
||||
type Theme struct {
|
||||
Shaper *text.Shaper
|
||||
Palette
|
||||
TextSize unit.Sp
|
||||
Icon struct {
|
||||
CheckBoxChecked *widget.Icon
|
||||
CheckBoxUnchecked *widget.Icon
|
||||
RadioChecked *widget.Icon
|
||||
RadioUnchecked *widget.Icon
|
||||
}
|
||||
// Face selects the default typeface for text.
|
||||
Face font.Typeface
|
||||
|
||||
// FingerSize is the minimum touch target size.
|
||||
FingerSize unit.Dp
|
||||
}
|
||||
|
||||
// NewTheme constructs a theme (and underlying text shaper).
|
||||
func NewTheme() *Theme {
|
||||
t := &Theme{Shaper: &text.Shaper{}}
|
||||
t.Palette = Palette{
|
||||
Fg: rgb(0x000000),
|
||||
Bg: rgb(0xffffff),
|
||||
ContrastBg: rgb(0x3f51b5),
|
||||
ContrastFg: rgb(0xffffff),
|
||||
}
|
||||
t.TextSize = 16
|
||||
|
||||
t.Icon.CheckBoxChecked = mustIcon(widget.NewIcon(icons.ToggleCheckBox))
|
||||
t.Icon.CheckBoxUnchecked = mustIcon(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank))
|
||||
t.Icon.RadioChecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonChecked))
|
||||
t.Icon.RadioUnchecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonUnchecked))
|
||||
|
||||
// 38dp is on the lower end of possible finger size.
|
||||
t.FingerSize = 38
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func (t Theme) WithPalette(p Palette) Theme {
|
||||
t.Palette = p
|
||||
return t
|
||||
}
|
||||
|
||||
func mustIcon(ic *widget.Icon, err error) *widget.Icon {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ic
|
||||
}
|
||||
|
||||
func rgb(c uint32) color.NRGBA {
|
||||
return argb(0xff000000 | c)
|
||||
}
|
||||
|
||||
func argb(c uint32) color.NRGBA {
|
||||
return color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)}
|
||||
}
|
||||
Reference in New Issue
Block a user