forked from joejulian/gio
6ab3ff40a6
This commit supports rendering opentype glyphs containing bitmap data instead of
color data. In order to support returning the shaped bitmap glyphs from the Shaper's
Shape() method, it has gained a second return parameter, an op.CallOp. Adding
that CallOp immediately after or immediately before painting the returned path
will display the bitmap glyphs.
The consequences of supporting colored glyphs forced changes upon the widget APIs
for widgets that display text. Previously text always had a fixed paint material,
so we could rely upon the caller setting the material (e.g. adding a paint.ColorOp)
before painting the glyphs and everything would work. Now that we display image-
based glyphs, we end up changing the painting material to an image midway through
displaying text. This is an awkward consequence of how we currently manage the
painting material, and to work around it widgets now accept an op.CallOp that
is expected to set the proper paint material. Text widgets will use that op.CallOp
before painting text (or other paint operations) to ensure that they are painting
with the proper materials.
This, in turn, changed the APIs for laying out widget.Editor, widget.Label, and
widget.Selectable, and eliminated the need for them to accept a callback (the
callback was only really to set the colors). Dropping that callback function
allowed me to consolidate widget.Label to only need one exported Layout method,
and allowed me to unexport the PaintText, PaintCaret, and PaintSelection methods
from widget.Editor and widget.Selectable. Those methods are useless in the public
API now that they don't need to be invoked after applying a color operation.
Callers of the raw text shaper API will need to make the following changes:
- Where before you used:
var ops *op.Ops // Assume we have an operation list.
var shaper *text.Shaper // Assume we have a shaper.
var col color.NRGBA // Assume we have a text color.
var glyphs []text.Glyph // Assume we have already filled a slice of glyphs.
shape := shaper.Shape(glyphs)
paint.FillShape(ops, col, clip.Outline{Path:shape}.Op())
- Now you should do:
shape, call := shaper.Shape(glyphs)
paint.FillShape(ops, col, clip.Outline{Path:shape}.Op())
call.Add(ops)
Callers of the widget.{Label,Selectable,Editor} APIs will need to make the
following changes:
- Where before you used:
var gtx layout.Context // Assume we have an operation list.
var shaper *text.Shaper // Assume we have a shaper.
var textCol color.NRGBA // Assume we have a text color.
var selectCol color.NRGBA // Assume we have a selection color.
var ed widget.Editor // Assume we have an editor.
var sel widget.Selectable // Assume we have a selectable.
// Lay out an editor.
ed.Layout(gtx, shaper, text.Font{}, unit.Sp(30), func(layout.Context) layout.Dimensions {
// Paint the editor.
})
// Lay out a selectable.
sel.Layout(gtx, shaper, text.Font{}, unit.Sp(30), func(layout.Context) layout.Dimensions {
// Paint the selectable.
})
// Lay out an interactive label.
widget.Label{}.LayoutSelectable(gtx, shaper, text.Font{}, unit.Sp(30), "hello", func(layout.Context) layout.Dimensions {
// Paint the label.
})
// Lay out a non-interactive label.
widget.Label{}.Layout(gtx, shaper, text.Font{}, unit.Sp(30), "hello")
- Now you should do:
// Capture setting the text paint material in a macro.
textColMacro := op.Record(gtx.Ops)
paint.ColorOp{Color: textCol}.Add(gtx.Ops)
textMaterial := textColMacro.Stop()
// Capture setting the selection paint material in a macro.
selectColMacro := op.Record(gtx.Ops)
paint.ColorOp{Color: selectCol}.Add(gtx.Ops)
selectMaterial := selectColMacro.Stop()
// Lay out an editor.
ed.Layout(gtx, shaper, text.Font{}, unit.Sp(30), textMaterial, selectMaterial)
// Lay out a selectable.
sel.Layout(gtx, shaper, text.Font{}, unit.Sp(30), textMaterial, selectMaterial)
// Lay out a label (no difference between interactive and non-interactive)
widget.Label{}.Layout(gtx, shaper, text.Font{}, unit.Sp(30), "hello", textMaterial, selectMaterial)
Callers of the material package API do not need to make any changes.
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
299 lines
7.9 KiB
Go
299 lines
7.9 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package material
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
|
|
"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 text.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 {
|
|
return 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,
|
|
}
|
|
}
|
|
|
|
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)
|
|
constraints := gtx.Constraints
|
|
return layout.Stack{}.Layout(gtx,
|
|
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
|
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
|
|
if button.Hovered() || button.Focused() {
|
|
paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{}))
|
|
}
|
|
for _, c := range button.History() {
|
|
drawInk(gtx, c)
|
|
}
|
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
|
}),
|
|
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
|
gtx.Constraints = constraints
|
|
return w(gtx)
|
|
}),
|
|
)
|
|
})
|
|
}
|
|
|
|
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(), op.CallOp{})
|
|
})
|
|
})
|
|
}
|
|
|
|
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.Stack{Alignment: layout.Center}.Layout(gtx,
|
|
layout.Expanded(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.Queue == nil:
|
|
background = f32color.Disabled(b.Background)
|
|
case b.Button.Hovered() || b.Button.Focused():
|
|
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}
|
|
}),
|
|
layout.Stacked(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.Stack{Alignment: layout.Center}.Layout(gtx,
|
|
layout.Expanded(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.Queue == nil:
|
|
background = f32color.Disabled(b.Background)
|
|
case b.Button.Hovered() || b.Button.Focused():
|
|
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}
|
|
}),
|
|
layout.Stacked(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 {
|
|
op.InvalidateOp{}.Add(gtx.Ops)
|
|
}
|
|
|
|
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)
|
|
}
|