// Package iconx provides a simple command line utility to browse
// icons from the `golang.org/x/exp/shiny/materialdesign/icons` package.
package main
import (
"fmt"
"image"
"image/color"
"os"
"sort"
"strconv"
"strings"
"time"
"gioui.org/app"
"gioui.org/font/gofont"
"gioui.org/io/clipboard"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"git.sr.ht/~pierrec/giox/layoutx"
"git.sr.ht/~pierrec/giox/widgetx"
"git.sr.ht/~pierrec/giox/widgetx/materialx"
"golang.org/x/exp/shiny/materialdesign/icons"
"golang.org/x/image/colornames"
)
//go:generate go get -u golang.org/x/exp/shiny/materialdesign/icons
var (
iconsMap map[string][]iconData
iconsTree tree
)
func init() {
var err error
iconsMap, err = loadIcons()
if err != nil {
panic(err)
}
// Sort icon names.
keys := make([]string, 0, len(iconsMap))
for name, d := range iconsMap {
keys = append(keys, name)
iconsMap[name] = d[:len(d):len(d)]
}
sort.Strings(keys)
for _, name := range keys {
iconsTree.Add(name + "/list")
}
}
func main() {
go func() {
w := app.NewWindow(
app.Size(unit.Dp(500), unit.Dp(500)),
app.Title("Material icons"),
)
if err := loop(w); err != nil {
fmt.Println(err)
}
os.Exit(0)
}()
app.Main()
}
func loop(w *app.Window) error {
th := material.NewTheme(gofont.Collection())
ui := &uiMain{th: th}
var ops op.Ops
for ev := range w.Events() {
switch e := ev.(type) {
case system.DestroyEvent:
return e.Err
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
ui.Layout(gtx)
e.Frame(gtx.Ops)
}
}
return nil
}
type uiMain struct {
Hover widgetx.Hover
th *material.Theme
tree widgetx.Tree
menu widgetx.Icons
num widgetx.Input
search widgetx.Input
lists map[int]*uiIconList
overlay time.Time
copied string
}
type uiIconList struct {
clicks []widget.Clickable
list layoutx.ListWrap
}
// menu icon indexes.
const (
menuFoldLess = iota
menuFoldMore
)
// Apply the filter to icon names, moving them around in place.
func updateChildren(name, filter string) {
icons := iconsMap[name]
icons = icons[:cap(icons)]
if filter != "" {
n := len(icons)
for i := 0; i < n; {
if strings.Contains(icons[i].Name, filter) {
i++
continue
}
n--
icons[i], icons[n] = icons[n], icons[i]
}
icons = icons[:n]
}
sort.Slice(icons, func(i, j int) bool { return icons[i].Name < icons[j].Name })
iconsMap[name] = icons
}
func (ui *uiMain) init() {
if ui.lists == nil {
ui.lists = make(map[int]*uiIconList)
ui.tree = widgetx.Tree{
Root: func() []int {
s := iconsTree.Root()
filter := ui.search.Text()
n := len(s)
for i := 0; i < n; {
name := iconsTree.Key(s[i])
updateChildren(name, filter)
if len(iconsMap[name]) == 0 {
n--
s[i], s[n] = s[n], s[i]
} else {
i++
}
}
s = s[:n]
sort.Slice(s, func(i, j int) bool { return iconsTree.Key(s[i]) < iconsTree.Key(s[j]) })
return s
},
Children: iconsTree.Children,
Axis: layout.Vertical,
}
ui.Hover = widgetx.Hover{
Background: color.NRGBA(colornames.Lightgrey),
Foreground: ui.th.Palette.Fg,
}
ui.menu = widgetx.Icons{
Hover: ui.Hover,
Color: ui.th.Palette.Fg,
Size: ui.th.TextSize,
Icons: widgetx.LoadIcons(icons.NavigationUnfoldLess, icons.NavigationUnfoldMore),
List: layout.List{Axis: layout.Horizontal},
}
ui.num.Editor = &widget.Editor{
Alignment: text.Middle,
SingleLine: true,
Submit: true,
}
ui.num.Editor.SetText("10")
ui.menu.Hide(menuFoldLess)
}
}
func (ui *uiMain) onClick(gtx layout.Context, idx int) {
switch root := ui.tree.Root(); idx {
case menuFoldLess:
ui.menu.Hide(menuFoldLess)
ui.menu.Show(menuFoldMore)
for _, id := range root {
ui.tree.Close(id)
}
case menuFoldMore:
ui.menu.Hide(menuFoldMore)
ui.menu.Show(menuFoldLess)
for _, id := range root {
ui.tree.Open(id)
}
}
}
func (ui *uiMain) layoutOverlay(gtx layout.Context) {
if ui.overlay.IsZero() {
return
}
if ui.overlay.Before(gtx.Now) {
ui.overlay = time.Time{}
ui.copied = ""
return
}
op.InvalidateOp{At: ui.overlay}.Add(gtx.Ops)
m := op.Record(gtx.Ops)
col := colornames.Gray
col.A = 32
paint.Fill(gtx.Ops, color.NRGBA(col))
pointer.InputOp{
Tag: ui,
Types: pointer.Cancel | pointer.Press,
}.Add(gtx.Ops)
layout.Stack{
Alignment: layout.Center,
}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
size := gtx.Constraints.Min
paint.FillShape(gtx.Ops, ui.th.Palette.ContrastBg, clip.Rect{Max: size}.Op())
return layout.Dimensions{Size: size}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(4)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
txt := fmt.Sprintf("%s copied to clipboard", ui.copied)
style := material.Body1(ui.th, txt)
style.Color = ui.th.Palette.ContrastFg
return style.Layout(gtx)
})
}),
)
op.Defer(gtx.Ops, m.Stop())
}
func (ui *uiMain) Layout(gtx layout.Context) layout.Dimensions {
defer ui.layoutOverlay(gtx)
ui.init()
padding := layout.Inset{Right: unit.Dp(4)}
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if ui.search.Submitted() {
op.InvalidateOp{}.Add(gtx.Ops)
}
return layout.Flex{}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return padding.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return ui.menu.Layout(gtx, ui.onClick, nil)
})
}),
layout.Flexed(0.05, func(gtx layout.Context) layout.Dimensions {
return padding.Layout(gtx, materialx.InputLayout(ui.th, &ui.num, "Num/column").Layout)
}),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return padding.Layout(gtx, materialx.InputLayout(ui.th, &ui.search, "Search").Layout)
}),
)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
defer ui.tree.Reset()
return materialx.TreeLayout(ui.th, &ui.tree).Layout(gtx, func(gtx layout.Context, idx int) layout.Dimensions {
name := iconsTree.Key(idx)
icons, ok := iconsMap[name]
if ok {
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min.X = gtx.Constraints.Max.X
size := gtx.Constraints.Min
col := ui.th.Palette.ContrastBg
paint.FillShape(gtx.Ops, col, clip.Rect{Max: size}.Op())
return layout.Dimensions{Size: size}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
txt := fmt.Sprintf("%s (%d)", name, len(icons))
style := material.Body1(ui.th, txt)
style.Color = ui.th.Palette.ContrastFg
return style.Layout(gtx)
}),
)
}
name = strings.TrimSuffix(name, "/list")
icons = iconsMap[name]
list, ok := ui.lists[idx]
if !ok {
list = &uiIconList{
clicks: make([]widget.Clickable, cap(icons)),
list: layoutx.ListWrap{
Axis: layout.Vertical,
Alignment: layout.Start,
},
}
ui.lists[idx] = list
}
for i := range list.clicks {
if list.clicks[i].Clicked() {
txt := name + icons[i].Name
ui.copied = txt
clipboard.WriteOp{Text: txt}.Add(gtx.Ops)
ui.overlay = gtx.Now.Add(1500 * time.Millisecond)
break
}
}
num := 5
if n, _ := strconv.Atoi(ui.num.Text()); n > 0 {
num = n
}
icSize := unit.Dp(20)
gtx.Constraints.Max.Y = gtx.Metric.Px(icSize) * num
return list.list.Layout(gtx, len(icons), func(gtx layout.Context, i int) layout.Dimensions {
ic := icons[i]
click := &list.clicks[i]
return click.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Axis: layout.Horizontal,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return padding.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min = image.Pt(gtx.Px(icSize), 0)
return ic.Icon.Layout(gtx, ui.th.Palette.ContrastBg)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
pointer.CursorNameOp{Name: pointer.CursorPointer}.Add(gtx.Ops)
return padding.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
txt := strings.TrimPrefix(ic.Name, name)
style := material.Body1(ui.th, txt)
if click.Hovered() {
style.Font.Weight = text.Bold
}
return style.Layout(gtx)
})
}),
)
})
}, nil)
})
}),
)
}