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:
ila
2026-07-23 16:35:01 +08:00
co-authored by Claude Opus 4.8
parent 97c1c4a974
commit f58728cddd
972 changed files with 597802 additions and 0 deletions
+297
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)}
}