♻️ Replace Fyne with Shirei
This commit is contained in:
@@ -1,26 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/sahilm/fuzzy"
|
||||
app "go.hasen.dev/shirei/app"
|
||||
. "go.hasen.dev/shirei"
|
||||
. "go.hasen.dev/shirei/widgets"
|
||||
)
|
||||
|
||||
//go:embed data/gitmojis.json data/images
|
||||
var data embed.FS
|
||||
|
||||
//go:embed Icon.png
|
||||
var iconPNG []byte
|
||||
|
||||
var preferredTheme = flag.String("theme", "", "Specify the theme to be used. dark/light")
|
||||
var linger = flag.String("linger", "", "If specified, linger for the given duration (e.g. 10s) before actually closing.")
|
||||
|
||||
@@ -37,25 +40,24 @@ type GitmojiData struct {
|
||||
|
||||
type GitmojiEntry struct {
|
||||
Gitmoji
|
||||
Icon fyne.Resource
|
||||
Icon *image.RGBA
|
||||
}
|
||||
|
||||
type keyedEntry struct {
|
||||
widget.Entry
|
||||
onKeyTyped func(keyEvent *fyne.KeyEvent) bool
|
||||
}
|
||||
var (
|
||||
allEntries []*GitmojiEntry
|
||||
allTerms []string
|
||||
currentEntries []*GitmojiEntry
|
||||
filter string
|
||||
appliedFilter string
|
||||
selectedId int
|
||||
listKey = new(int)
|
||||
lingerDuration *time.Duration
|
||||
shuttingDown bool
|
||||
)
|
||||
|
||||
func (e *keyedEntry) TypedKey(keyEvent *fyne.KeyEvent) {
|
||||
if e.onKeyTyped == nil || !e.onKeyTyped(keyEvent) {
|
||||
e.Entry.TypedKey(keyEvent)
|
||||
}
|
||||
}
|
||||
|
||||
func newKeyedEntry() *keyedEntry {
|
||||
e := &keyedEntry{}
|
||||
e.ExtendBaseWidget(e)
|
||||
return e
|
||||
}
|
||||
// minimum time to keep the process alive after a copy, so the current frame
|
||||
// can finish and the backend can flush the clipboard request
|
||||
const minQuitDelay = 100 * time.Millisecond
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
@@ -66,16 +68,16 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
a := app.New()
|
||||
loadEntries(gitmojiData)
|
||||
currentEntries = allEntries
|
||||
|
||||
switch *preferredTheme {
|
||||
case "dark":
|
||||
a.Settings().SetTheme(theme.DarkTheme())
|
||||
SetDarkMode(true)
|
||||
case "light":
|
||||
a.Settings().SetTheme(theme.LightTheme())
|
||||
SetDarkMode(false)
|
||||
}
|
||||
|
||||
var lingerDuration *time.Duration = nil
|
||||
if *linger != "" {
|
||||
if d, err := time.ParseDuration(*linger); err != nil {
|
||||
log.Fatalf("invalid linger time: %v\n", err)
|
||||
@@ -84,12 +86,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
w := a.NewWindow("Gitmoji Picker")
|
||||
w.CenterOnScreen()
|
||||
app.SetupWindow("Gitmoji Picker", 300, 300)
|
||||
app.SetupIconBytes(iconPNG)
|
||||
app.Run(RootView)
|
||||
}
|
||||
|
||||
allEntries := make([]*GitmojiEntry, len(gitmojiData.Gitmojis))
|
||||
allTerms := make([]string, len(gitmojiData.Gitmojis))
|
||||
currentId := 0
|
||||
func loadEntries(gitmojiData *GitmojiData) {
|
||||
allEntries = make([]*GitmojiEntry, len(gitmojiData.Gitmojis))
|
||||
allTerms = make([]string, len(gitmojiData.Gitmojis))
|
||||
|
||||
for i, gitmoji := range gitmojiData.Gitmojis {
|
||||
b, err := data.ReadFile("data/images/" + gitmoji.Name + ".png")
|
||||
@@ -98,130 +102,142 @@ func main() {
|
||||
}
|
||||
allEntries[i] = &GitmojiEntry{
|
||||
Gitmoji: gitmoji,
|
||||
Icon: fyne.NewStaticResource(gitmoji.Name, b),
|
||||
Icon: decodePNG(b),
|
||||
}
|
||||
allTerms[i] = gitmoji.Name + " " + gitmoji.Description
|
||||
}
|
||||
}
|
||||
|
||||
buildFilteredList := func(filter string) []*GitmojiEntry {
|
||||
r := fuzzy.Find(filter, allTerms)
|
||||
result := make([]*GitmojiEntry, len(r))
|
||||
for i := range r {
|
||||
result[i] = allEntries[r[i].Index]
|
||||
}
|
||||
return result
|
||||
func decodePNG(b []byte) *image.RGBA {
|
||||
img, err := png.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var m sync.Mutex
|
||||
currentEntries := allEntries
|
||||
|
||||
e := newKeyedEntry()
|
||||
|
||||
l := widget.NewList(
|
||||
func() int {
|
||||
return len(currentEntries)
|
||||
},
|
||||
func() fyne.CanvasObject {
|
||||
tn := canvas.NewText("Dummy", theme.ForegroundColor())
|
||||
tn.TextStyle.Bold = true
|
||||
|
||||
td := canvas.NewText("Dummy", theme.ForegroundColor())
|
||||
td.TextSize = theme.TextSize() * 8 / 10
|
||||
|
||||
return container.NewHBox(
|
||||
widget.NewIcon(allEntries[0].Icon),
|
||||
container.NewVBox(tn, td),
|
||||
)
|
||||
},
|
||||
func(id widget.ListItemID, object fyne.CanvasObject) {
|
||||
m.Lock()
|
||||
entry := currentEntries[id]
|
||||
m.Unlock()
|
||||
|
||||
hb := object.(*fyne.Container)
|
||||
hb.Objects[0].(*widget.Icon).SetResource(entry.Icon)
|
||||
vb := hb.Objects[1].(*fyne.Container)
|
||||
vb.Objects[0].(*canvas.Text).Text = entry.Name
|
||||
vb.Objects[1].(*canvas.Text).Text = entry.Description
|
||||
|
||||
vb.Objects[0].(*canvas.Text).Refresh()
|
||||
vb.Objects[1].(*canvas.Text).Refresh()
|
||||
},
|
||||
)
|
||||
|
||||
e.OnChanged = func(s string) {
|
||||
m.Lock()
|
||||
if s == "" {
|
||||
currentEntries = allEntries
|
||||
} else {
|
||||
currentEntries = buildFilteredList(s)
|
||||
}
|
||||
if len(currentEntries) > 0 {
|
||||
currentId = 0
|
||||
} else {
|
||||
currentId = -1
|
||||
}
|
||||
m.Unlock()
|
||||
|
||||
l.Select(currentId)
|
||||
l.Refresh()
|
||||
if rgba, ok := img.(*image.RGBA); ok {
|
||||
return rgba
|
||||
}
|
||||
rgba := image.NewRGBA(img.Bounds())
|
||||
draw.Draw(rgba, rgba.Bounds(), img, img.Bounds().Min, draw.Src)
|
||||
return rgba
|
||||
}
|
||||
|
||||
l.Select(currentId)
|
||||
l.OnSelected = func(id widget.ListItemID) {
|
||||
currentId = id
|
||||
w.Canvas().Focus(e)
|
||||
}
|
||||
func RootView() {
|
||||
handleKeys()
|
||||
applyFilterChange()
|
||||
|
||||
w.SetContent(container.NewBorder(e, nil, nil, nil, l))
|
||||
w.Resize(fyne.NewSize(300, 300))
|
||||
scheme := CurrentColorScheme
|
||||
Container(Attrs(Viewport, BackgroundVec(scheme.Surfaces.Canvas.Background)), func() {
|
||||
attrs := DefaultTextInputAttrs()
|
||||
attrs.NoUpDownLineEdges = true // leave Up/Down for list navigation
|
||||
TextInputExt(&filter, attrs)
|
||||
|
||||
e.Show()
|
||||
|
||||
e.onKeyTyped = func(event *fyne.KeyEvent) bool {
|
||||
switch event.Name {
|
||||
case fyne.KeyDown:
|
||||
m.Lock()
|
||||
if currentId < len(currentEntries)-1 {
|
||||
currentId++
|
||||
}
|
||||
m.Unlock()
|
||||
l.Select(currentId)
|
||||
return true
|
||||
case fyne.KeyUp:
|
||||
m.Lock()
|
||||
if currentId > 0 {
|
||||
currentId--
|
||||
}
|
||||
m.Unlock()
|
||||
l.Select(currentId)
|
||||
return true
|
||||
case fyne.KeyEscape:
|
||||
w.Close()
|
||||
return true
|
||||
case fyne.KeyReturn, fyne.KeyEnter:
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if currentId > -1 {
|
||||
w.Clipboard().SetContent(currentEntries[currentId].Emoji)
|
||||
if lingerDuration != nil {
|
||||
w.Hide()
|
||||
time.Sleep(*lingerDuration)
|
||||
}
|
||||
w.Close()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
w.Canvas().SetOnTypedKey(func(event *fyne.KeyEvent) {
|
||||
e.onKeyTyped(event)
|
||||
VirtualListView(listKey, len(currentEntries),
|
||||
func(i int) any { return i },
|
||||
nil, // auto-height via Measure
|
||||
rowView,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
w.Canvas().Focus(e)
|
||||
func applyFilterChange() {
|
||||
if filter == appliedFilter {
|
||||
return
|
||||
}
|
||||
appliedFilter = filter
|
||||
|
||||
w.ShowAndRun()
|
||||
if filter == "" {
|
||||
currentEntries = allEntries
|
||||
} else {
|
||||
r := fuzzy.Find(filter, allTerms)
|
||||
currentEntries = make([]*GitmojiEntry, len(r))
|
||||
for i := range r {
|
||||
currentEntries[i] = allEntries[r[i].Index]
|
||||
}
|
||||
}
|
||||
|
||||
if len(currentEntries) > 0 {
|
||||
selectedId = 0
|
||||
VirtualListScrollIntoView(listKey, selectedId)
|
||||
} else {
|
||||
selectedId = -1
|
||||
}
|
||||
}
|
||||
|
||||
func handleKeys() {
|
||||
if shuttingDown {
|
||||
return
|
||||
}
|
||||
|
||||
switch GetFrameInput().Key {
|
||||
case KeyDown:
|
||||
if selectedId < len(currentEntries)-1 {
|
||||
selectEntry(selectedId + 1)
|
||||
}
|
||||
case KeyUp:
|
||||
if selectedId > 0 {
|
||||
selectEntry(selectedId - 1)
|
||||
}
|
||||
case KeyEscape:
|
||||
app.Quit()
|
||||
case KeyEnter:
|
||||
if selectedId > -1 {
|
||||
pickGitmoji()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func selectEntry(idx int) {
|
||||
selectedId = idx
|
||||
VirtualListScrollIntoView(listKey, idx)
|
||||
}
|
||||
|
||||
func rowView(idx int, width float32) {
|
||||
entry := currentEntries[idx]
|
||||
icon := UseImage("gitmoji:"+entry.Name, entry.Icon)
|
||||
|
||||
scheme := CurrentColorScheme
|
||||
nameColor := scheme.Surfaces.Canvas.Text
|
||||
descColor := nameColor
|
||||
descColor[3] = 0.65
|
||||
hoverColor := nameColor
|
||||
hoverColor[3] = 0.08
|
||||
selectedColor := scheme.Buttons.Primary.Normal.Background
|
||||
selectedColor[3] = 0.35
|
||||
|
||||
Container(Attrs(Row, CrossMid, Gap(8), Pad2(5, 10), Expand, Clip), func() {
|
||||
if idx == selectedId {
|
||||
ModAttrs(BackgroundVec(selectedColor))
|
||||
} else if IsHovered() {
|
||||
ModAttrs(BackgroundVec(hoverColor))
|
||||
}
|
||||
if IsClicked() {
|
||||
selectEntry(idx)
|
||||
}
|
||||
|
||||
ImageView(icon, Vec2{20, 20})
|
||||
Container(Attrs(Expand), func() {
|
||||
Label(entry.Name, FontWeight(WeightBold), TextColorVec(nameColor))
|
||||
Label(entry.Description, FontSize(DefaultTextSize*8/10), TextColorVec(descColor))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func pickGitmoji() {
|
||||
if shuttingDown {
|
||||
return
|
||||
}
|
||||
shuttingDown = true
|
||||
|
||||
RequestTextCopy(currentEntries[selectedId].Emoji)
|
||||
|
||||
delay := minQuitDelay
|
||||
if lingerDuration != nil {
|
||||
delay = *lingerDuration
|
||||
}
|
||||
go func() {
|
||||
time.Sleep(delay)
|
||||
app.Quit()
|
||||
}()
|
||||
}
|
||||
|
||||
func loadData() (*GitmojiData, error) {
|
||||
|
||||
Reference in New Issue
Block a user