feature/ui - zatim ne uplne uhlazena ale celkem pouzitelna appka #1

Merged
luke-20 merged 17 commits from feature/ui into main 2025-09-28 21:05:52 +02:00
1980 changed files with 833471 additions and 348 deletions

View File

@ -1,3 +0,0 @@
{
"nuxt.isNuxtApp": false
}

125
README.md Normal file → Executable file
View File

@ -1,12 +1,23 @@
# fckeuspy-go
## Co to je
Mini Go/HTMX aplikace pro šifrování zpráv cizím veřejným klíčem.
Nástroj v Go pro asymetrické šifrování zpráv cizím veřejným klíčem.
* Při startu vygeneruje nebo načte RSA identitu (2048 bitů).
* Umožní sdílet veřejný klíč (`public.pem`) a self-signed cert (`identity.crt`).
* Šifruje hybridně: **RSA-OAEP (SHA-256) + AES-256-GCM**.
* Rozhraní přes **HTMX**, moderní responzivní layout (2 sloupce na desktopu, 1 sloupec na mobilu).
* Všude jsou tlačítka **Copy** a **Clear** pro snadnou práci s texty.
Má dvě rozhraní:
1. Web (HTMX) jednoduché formuláře (encrypt / decrypt)
2. Desktop (Fyne v2) 3 záložky: Identita, Šifrování, Dešifrování (dark UI)
Vlastnosti:
* Při startu vygeneruje nebo načte RSA identitu (2048 bitů)
* Sdílí veřejný klíč (`public.pem`) a selfsigned cert (`identity.crt`)
* Hybridní šifra: **RSA-OAEP (SHA-256) + AES-256-GCM**
* JSON payload: `{ek,n,ct}` (base64 komponenty)
* V GUI nad každým polem sada tlačítek (Paste, Clear, Encrypt/Decrypt, Copy)
* Output pole jsou readonly
* Pamatuje poslední záložku a velikost okna
---
@ -17,22 +28,18 @@ Mini Go/HTMX aplikace pro šifrování zpráv cizím veřejným klíčem.
---
## Rychlý start
## Rychlý start (gui)
```bash
# spustí server
go run .
# otevři v prohlížeči
http://localhost:8080/
go run . gui
```
Na prvním startu se vytvoří:
```
```text
identity_key.pem # RSA private key (PKCS#1)
public.pem # veřejný klíč PEM (PKIX)
identity.crt # self-signed cert (vizitka pro sdílení)
identity.crt # self-signed cert (vizitka)
```
---
@ -51,26 +58,29 @@ REGEN_KEYS=1 go run .
---
## Jak to používat (UI)
## Web UI použití
1. Otevři `http://localhost:8080/`.
2. V sekci **Můj veřejný klíč** stáhni/kopíruj `public.pem` nebo `identity.crt` a pošli je kontaktům.
3. V sekci **Šifrovat pro cizí klíč**:
```bash
# přes flag
go run . web
```
* napiš zprávu,
* vlož **cizí** public key nebo cert (PEM blok),
* klikni **Encrypt**.
* Výsledek (**Zašifrovaný payload**) se zobrazí hned pod formulářem jako JSON:
```json
{
1. Otevři `http://localhost:8080/`
2. Zkopíruj / stáhni svůj veřejný klíč nebo cert
3. Do Encrypt formuláře vlož cizí public key/cert + zprávu → Encrypt
4. JSON payload odešli příjemci
5. Příjemce vloží payload do Decrypt → plaintext
Formát payloadu:
```json
{
"ek": "base64(RSA-OAEP(aesKey))",
"n": "base64(nonce)",
"ct": "base64(aes-gcm-ciphertext)"
}
```
* tlačítkem **Copy** zkopíruješ, **Clear** vymaže obsah.
4. Příjemce vloží payload do sekce **Dešifrovat** a klikne **Decrypt** → objeví se plaintext.
}
```
---
@ -90,17 +100,18 @@ REGEN_KEYS=1 go run .
---
## Struktura projektu
## Struktura projektu (zkráceně)
```
```text
.
├─ main.go
├─ templates/
│ ├─ index.html # UI s Copy/Clear tlačítky
│ ├─ encrypt.html # výstup šifrování
│ └─ decrypt.html # výstup dešifrování
└─ static/
└─ style.css # responzivní layout (1 sloupec mobile, 2 desktop)
├─ main.go # cobra entry
├─ cmd.go # CLI příkazy (gui, web)
├─ ui.go # Fyne komponenty
├─ fyne_ui.go # init okna + prefs
├─ server.go # HTTP handlery
├─ lib/crypto.go # šifrovací logika
├─ templates/ # HTMX šablony
└─ static/ # CSS
```
---
@ -122,15 +133,41 @@ REGEN_KEYS=1 go run .
---
## Fyne GUI režim
Spuštění desktopu:
```bash
go run . gui
```
Záložky:
1. Identita kopírování `public.pem` / `identity.crt`
2. Šifrování peer key, zpráva, tlačítka: Paste, Clear, Encrypt, Copy
3. Dešifrování payload: Paste+Decrypt, Clear (čistí i výsledek), Copy
GUI je trvale v dark stylu. Pamatuje poslední tab a velikost okna.
HTTP režim zůstává:
```bash
go run . web
```
---
---
# Jak to funguje podle chatGPT
## Jak to funguje podle chatGPT
Ok, pojďme úplně jednoduše, krok za krokem bez odborných keců 👇
---
### Jak to celé funguje
## Jak to celé funguje
1. **Máš dva klíče**:
@ -200,9 +237,9 @@ Ok, pojďme úplně jednoduše, krok za krokem bez odborných keců 👇
# Jak je to s certifikaty podle chatGPT
## Jak je to s certifikaty podle chatGPT
### `identity_key.pem`
## `identity_key.pem`
* **Soukromý klíč** (private key)
* Tenhle soubor je jen pro tebe.
@ -215,7 +252,7 @@ Ok, pojďme úplně jednoduše, krok za krokem bez odborných keců 👇
---
### `public.pem`
## `public.pem`
* **Veřejný klíč** (public key)
* To je „vizitka“, kterou pošleš ostatním.
@ -225,7 +262,7 @@ Ok, pojďme úplně jednoduše, krok za krokem bez odborných keců 👇
---
### `identity.crt`
## `identity.crt`
* **Certifikát** (self-signed = podepsaný sám sebou)
* Je to vlastně balíček, který obsahuje:
@ -238,7 +275,7 @@ Ok, pojďme úplně jednoduše, krok za krokem bez odborných keců 👇
---
### Shrnutí v řeči pro normální lidi
## Shrnutí v řeči pro normální lidi
* `identity_key.pem`**tajný klíč** = schovej do trezoru, nikomu nedávej.
* `public.pem`**veřejný klíč** = pošli komukoli, aby ti mohl šifrovat zprávy.

35
cmd.go Executable file
View File

@ -0,0 +1,35 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "fckeuspy",
Short: "Hybrid RSA+AES šifrovací nástroj (GUI / HTTP)",
Long: "Fckeuspy: nástroj pro šifrování a dešifrování pomocí hybridního RSA-OAEP + AES-GCM, s Fyne GUI a HTTP serverem.",
}
var guiCmd = &cobra.Command{
Use: "gui",
Short: "Spustí Fyne GUI",
Run: func(cmd *cobra.Command, args []string) {
runFyne()
},
}
var webCmd = &cobra.Command{
Use: "web",
Short: "Spustí HTTP server",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Spouštím HTTP server na :8080 ...")
RunWebApp()
},
}
func init() {
rootCmd.AddCommand(guiCmd)
rootCmd.AddCommand(webCmd)
}

378
fyne_ui.go Executable file
View File

@ -0,0 +1,378 @@
package main
import (
encrypt "fckeuspy-go/lib"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
// strengthRowLayout rozdělí dostupnou šířku v poměru ~70:30 (bar+label : tlačítko)
type strengthRowLayout struct{}
func (l *strengthRowLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) {
if len(objs) != 2 {
return
}
left := objs[0]
right := objs[1]
leftW := float32(float64(size.Width) * 0.70)
if leftW < 10 {
leftW = size.Width / 2
}
left.Resize(fyne.NewSize(leftW, size.Height))
left.Move(fyne.NewPos(0, 0))
right.Resize(fyne.NewSize(size.Width-leftW, size.Height))
right.Move(fyne.NewPos(leftW, 0))
}
func (l *strengthRowLayout) MinSize(objs []fyne.CanvasObject) fyne.Size {
var w, h float32
if len(objs) == 2 {
m1 := objs[0].MinSize()
m2 := objs[1].MinSize()
w = m1.Width + m2.Width
if m1.Height > m2.Height {
h = m1.Height
} else {
h = m2.Height
}
} else {
for _, o := range objs {
ms := o.MinSize()
w += ms.Width
if ms.Height > h {
h = ms.Height
}
}
}
return fyne.NewSize(w, h)
}
func NewUI() (stprageDir string, window fyne.Window) {
// App + storage dir
a := app.NewWithID("fckeuspy")
w := a.NewWindow("Encryptor (Fyne)")
prefs := a.Preferences()
width := prefs.IntWithFallback("winW", 1100)
height := prefs.IntWithFallback("winH", 720)
w.Resize(fyne.NewSize(float32(width), float32(height)))
w.SetOnClosed(func() {
sz := w.Canvas().Size()
prefs.SetInt("winW", int(sz.Width))
prefs.SetInt("winH", int(sz.Height))
})
// docasny bypass
/*
base, err := os.UserConfigDir()
if err != nil {
base, _ = os.UserHomeDir()
}
*/
base := "./"
//return filepath.Join(base, "fckeuspy-go"), w
return filepath.Join(base, ""), w
}
// ShowPasswordVaultDialog zobrazí moderní odemykací / registrační dialog s countdown animací při špatném hesle.
// Callback je zavolán pouze při úspěchu (ne při neplatném hesle). Zrušení vrací password="".
func ShowPasswordVaultDialog(w fyne.Window, vaultPath string, onResult func(create bool, password string, commonName string)) {
modeCreate := false
if _, err := os.Stat(vaultPath); err != nil {
modeCreate = true
}
statusLabel := widget.NewLabel("")
statusLabel.Wrapping = fyne.TextWrapWord
statusLabel.Hide()
// Unlock
unlockPw := widget.NewPasswordEntry()
unlockPw.SetPlaceHolder("Heslo…")
unlockBtn := widget.NewButton("Odemknout", nil)
unlockForm := container.NewVBox(widget.NewForm(widget.NewFormItem("Heslo", unlockPw)), unlockBtn)
// Create
createPw1 := widget.NewPasswordEntry()
createPw1.SetPlaceHolder("Heslo…")
createPw2 := widget.NewPasswordEntry()
createPw2.SetPlaceHolder("Znovu heslo…")
commonNameEntry := widget.NewEntry()
commonNameEntry.SetPlaceHolder("Jméno identity (např. Alice)…")
strengthBar := widget.NewProgressBar()
strengthBar.Min = 0
strengthBar.Max = 100
// Skryj defaultní procenta uvnitř progress baru, protože máme vlastní overlay label
strengthBar.TextFormatter = func() string { return "" }
strengthLabel := widget.NewLabel("")
updateStrength := func(pw string) {
s, d := passwordStrengthScore(pw)
strengthBar.SetValue(float64(s))
strengthLabel.SetText(fmt.Sprintf("%s: %d%%", d, s))
}
createPw1.OnChanged = updateStrength
genBtn := widget.NewButton("Generovat", func() {
if v, err := encrypt.GenerateRandomPassword(24); err == nil {
createPw1.SetText(v)
createPw2.SetText(v)
updateStrength(v)
}
})
createBtn := widget.NewButton("Vytvořit trezor", nil)
// Obě pole stejné šířky žádné tlačítko uvnitř prvního řádku
form := widget.NewForm(
widget.NewFormItem("Jméno", commonNameEntry),
widget.NewFormItem("Heslo", createPw1),
widget.NewFormItem("Potvrzení", createPw2),
)
// Řádek: progress bar s textem uvnitř (overlay) : tlačítko (70:30)
strengthLabel.Alignment = fyne.TextAlignCenter
leftStack := container.NewStack(
strengthBar,
container.NewCenter(strengthLabel),
)
strengthRow := container.New(&strengthRowLayout{}, leftStack, genBtn)
createForm := container.NewVBox(
form,
strengthRow,
createBtn,
)
stack := container.NewStack(unlockForm, createForm)
showMode := func(c bool) {
modeCreate = c
if c {
unlockForm.Hide()
createForm.Show()
} else {
createForm.Hide()
unlockForm.Show()
}
statusLabel.Hide()
}
showMode(modeCreate)
segment := widget.NewRadioGroup([]string{"Odemknout", "Vytvořit"}, func(val string) { showMode(val == "Vytvořit") })
segment.Horizontal = true
segment.Refresh()
if modeCreate {
segment.SetSelected("Vytvořit")
} else {
segment.SetSelected("Odemknout")
}
header := container.NewVBox(
widget.NewLabelWithStyle("🔐 Trezor", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
segment,
widget.NewSeparator(),
)
var d dialog.Dialog
completed := false
var countdownCancel chan struct{}
startCountdown := func(sec int) {
if countdownCancel != nil {
close(countdownCancel)
}
countdownCancel = make(chan struct{})
statusLabel.Show()
unlockBtn.Disable()
unlockPw.Disable()
go func() {
for i := sec; i > 0; i-- {
select {
case <-countdownCancel:
return
default:
}
iLocal := i
fyne.Do(func() { statusLabel.SetText(fmt.Sprintf("Neplatné heslo. Nový pokus za %d s", iLocal)) })
time.Sleep(time.Second)
}
fyne.Do(func() { unlockBtn.Enable(); unlockPw.Enable(); unlockPw.SetText(""); statusLabel.Hide() })
}()
}
unlockBtn.OnTapped = func() {
pw := unlockPw.Text
if pw == "" {
statusLabel.SetText("Zadejte heslo")
statusLabel.Show()
return
}
if _, err := encrypt.OpenEncryptedStore(vaultPath, pw); err != nil {
statusLabel.SetText("Neplatné heslo")
statusLabel.Show()
startCountdown(3)
return
}
completed = true
onResult(false, pw, "")
d.Hide()
}
createBtn.OnTapped = func() {
pw1, pw2 := createPw1.Text, createPw2.Text
if pw1 != pw2 {
statusLabel.SetText("Hesla se neshodují")
statusLabel.Show()
return
}
if err := encrypt.ValidatePasswordForUI(pw1); err != nil {
statusLabel.SetText(err.Error())
statusLabel.Show()
return
}
completed = true
cn := strings.TrimSpace(commonNameEntry.Text)
onResult(true, pw1, cn)
d.Hide()
}
body := container.NewVBox(header, container.NewPadded(stack), statusLabel)
d = dialog.NewCustom("", "Zrušit", body, w)
d.SetOnClosed(func() {
if countdownCancel != nil {
close(countdownCancel)
}
if !completed {
onResult(false, "", "")
}
})
d.Resize(fyne.NewSize(480, 320))
d.Show()
if modeCreate {
w.Canvas().Focus(createPw1)
} else {
w.Canvas().Focus(unlockPw)
}
}
// passwordStrengthScore: heuristický odhad síly (0-100) s ohledem na entropii, třídy znaků a penalizace.
func passwordStrengthScore(pw string) (int, string) {
if pw == "" {
return 0, "Prázdné"
}
length := len(pw)
// Charakteristiky
hasUpper, hasLower, hasDigit, hasSymbol := false, false, false, false
symbols := "!@#$%^&*()_-+=[]{}/?:.,<>|~`'\""
freq := make(map[rune]int)
for _, r := range pw {
freq[r]++
switch {
case r >= 'A' && r <= 'Z':
hasUpper = true
case r >= 'a' && r <= 'z':
hasLower = true
case r >= '0' && r <= '9':
hasDigit = true
default:
if strings.ContainsRune(symbols, r) {
hasSymbol = true
}
}
}
classes := 0
if hasUpper {
classes++
}
if hasLower {
classes++
}
if hasDigit {
classes++
}
if hasSymbol {
classes++
}
// Shannon entropie per char
var shannon float64
for _, c := range freq {
p := float64(c) / float64(length)
shannon += -p * (log2(p))
}
// log2 hrubě implementujeme konverzí přes Ln, zde jednoduchá aproximace: log2(p) ~ (ln p)/0.693
// aby nebyla složitost, definujeme pomocnou funkci nahoře - zjednodušeno níže (inline hack):
// Použijeme předpočítané p^ využitím math (neimportujeme math -> krátký lookup) => nahradíme vlastní small table -> zvolíme fallback.
// Kvůli jednoduchosti: pokud shannon > 4.5 považuj za 4.5 (limit pro běžná hesla)
if shannon > 4.5 {
shannon = 4.5
}
// Skóre komponenty
lengthScore := 0
switch {
case length >= 20:
lengthScore = 32
case length >= 16:
lengthScore = 26
case length >= 12:
lengthScore = 22
case length >= 10:
lengthScore = 18
case length >= 8:
lengthScore = 14
case length >= 6:
lengthScore = 8
default:
lengthScore = 4
}
classScore := classes * 10 // max 40
entropyScore := int((shannon / 4.5) * 28) // max ~28
// Penalizace monotónnosti & nízké diverzity
uniqueChars := len(freq)
diversityPenalty := 0
if uniqueChars <= 2 {
diversityPenalty = 25
} else if uniqueChars <= 4 {
diversityPenalty = 15
} else if uniqueChars <= 6 {
diversityPenalty = 5
}
// Sekvenční vzory
lower := strings.ToLower(pw)
for _, pat := range []string{"abc", "abcd", "qwert", "1234", "password", "heslo"} {
if strings.Contains(lower, pat) {
diversityPenalty += 10
}
}
raw := lengthScore + classScore + entropyScore - diversityPenalty
if raw < 0 {
raw = 0
}
if raw > 100 {
raw = 100
}
desc := "Slabé"
switch {
case raw >= 85:
desc = "Silné"
case raw >= 65:
desc = "Dobré"
case raw >= 45:
desc = "Střední"
case raw >= 25:
desc = "Nízké"
}
return raw, desc
}
// log2 pomocná aproximace (rychlá, bez importu math)
func log2(x float64) float64 {
// racionální aproximace: log2(x) ≈ ln(x)/ln(2); použijeme jednoduchý Newton pro ln
if x <= 0 {
return 0
}
// Polynomická aproximace pro ln v okolí 1: ln(x) ~ 2*( (y) + y^3/3 ), y=(x-1)/(x+1) (rychlý log)
y := (x - 1) / (x + 1)
y2 := y * y
ln := 2 * (y + y2*y/3)
return ln / 0.69314718056
}

49
go.mod Normal file → Executable file
View File

@ -1,3 +1,50 @@
module fckeusp-go
module fckeuspy-go
go 1.24.0
require (
fyne.io/fyne/v2 v2.6.3
github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea
github.com/makiuchi-d/gozxing v0.1.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cobra v1.10.1
golang.org/x/crypto v0.42.0
)
require (
fyne.io/systray v1.11.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.1.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.4.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

102
go.sum Executable file
View File

@ -0,0 +1,102 @@
fyne.io/fyne/v2 v2.6.3 h1:cvtM2KHeRuH+WhtHiA63z5wJVBkQ9+Ay0UMl9PxFHyA=
fyne.io/fyne/v2 v2.6.3/go.mod h1:NGSurpRElVoI1G3h+ab2df3O5KLGh1CGbsMMcX0bPIs=
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3HnVhDalIyZ4RJAyVfDrbnfFeJoC4=
github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea/go.mod h1:w4pGU9PkiX2hAWyF0yuHEHmYTQFAd6WHzp6+IY7JVjE=
github.com/makiuchi-d/gozxing v0.1.0 h1:bLJdKoi5G2wGQnFirTQI9aOSCwNm5N2e0P8ov04Hltk=
github.com/makiuchi-d/gozxing v0.1.0/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

74
lib/crypto.go Executable file
View File

@ -0,0 +1,74 @@
package encrypt
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"time"
)
// No envelope here; server/UI define their own payload structs.
func parsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(pemOrCert))
if block == nil {
return nil, errors.New("no PEM block found")
}
switch block.Type {
case "PUBLIC KEY":
k, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
rsaPub, ok := k.(*rsa.PublicKey)
if !ok {
return nil, errors.New("expecting RSA PUBLIC KEY")
}
return rsaPub, nil
case "RSA PUBLIC KEY":
return x509.ParsePKCS1PublicKey(block.Bytes)
case "CERTIFICATE":
c, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
rsaPub, ok := c.PublicKey.(*rsa.PublicKey)
if !ok {
return nil, errors.New("certificate does not contain RSA key")
}
return rsaPub, nil
default:
return nil, fmt.Errorf("unsupported PEM type: %s", block.Type)
}
}
func ParsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) {
return parsePeerPublicKey(pemOrCert)
}
func generateSelfSignedCert(priv *rsa.PrivateKey, commonName string) ([]byte, error) {
if commonName == "" {
commonName = "Encryptor Local Identity"
}
tpl := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{CommonName: commonName},
NotBefore: time.Now().Add(-1 * time.Hour),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, tpl, tpl, &priv.PublicKey, priv)
if err != nil {
return nil, err
}
buf := &bytes.Buffer{}
_ = pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: der})
return buf.Bytes(), nil
}

486
lib/crypto_storage.go Executable file
View File

@ -0,0 +1,486 @@
package encrypt
// Nový návrh: Password-based šifrované úložiště s derivací klíče (scrypt) +
// vytvoření uživatelských RSA klíčů při prvotní inicializaci. Private/Public
// (a volitelně self-signed cert) se uloží jako položky uvnitř zašifrovaného
// JSON. Soubor není možno otevřít bez hesla.
//
// Formát na disku (JSON):
// {
// "v":1,
// "kdf":"scrypt",
// "N":32768, "r":8, "p":1,
// "salt":"base64(...)",
// "n":"base64(nonce)",
// "c":"base64(ciphertext)"
// }
// Plaintext (po dešifrování):
// {
// "updated":"RFC3339",
// "data":{ key: <json>, ... }
// }
// Povinné interní klíče:
// _identity_private_pem
// _identity_public_pem
// _identity_cert_pem (volitelně)
//
// API: CreateEncryptedStore (nový), OpenEncryptedStore, ChangePassword.
// Původní NewSecureJSONStore (s KeyProvider) je odstraněn přechod na heslo.
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"golang.org/x/crypto/scrypt"
)
// SecureJSONStore definuje metody úložiště.
type SecureJSONStore interface {
Put(key string, value any) error
Get(key string, dst any) error
Delete(key string) error
ListKeys() []string
Flush() error
Close() error
ChangePassword(newPassword string) error
Has(key string) bool
// Identity getters (pem). Vrací prázdný string, pokud neexistují.
IdentityPrivatePEM() string
IdentityPublicPEM() string
IdentityCertPEM() string
}
type internalPlain struct {
Updated time.Time `json:"updated"`
Data map[string]json.RawMessage `json:"data"`
}
type fileEnvelope struct {
Version int `json:"v"`
KDF string `json:"kdf"`
N int `json:"N"`
R int `json:"r"`
P int `json:"p"`
Salt string `json:"salt"`
Nonce string `json:"n"`
Cipher string `json:"c"`
}
type secureJSONStore struct {
mu sync.RWMutex
path string
kdfN int
kdfR int
kdfP int
salt []byte
key []byte // odvozený AES-256 klíč
dirty bool
plain internalPlain
hasIdent bool
}
const (
defaultKDFN = 32768
defaultKDFR = 8
defaultKDFP = 1
)
// CreateEncryptedStore vytvoří nový soubor; error pokud již existuje.
func CreateEncryptedStore(path, password string, generateIdentity bool, commonName string) (SecureJSONStore, error) {
if err := validatePasswordStrength(password); err != nil {
return nil, err
}
if _, err := os.Stat(path); err == nil {
return nil, fmt.Errorf("store already exists: %s", path)
} else if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
s := &secureJSONStore{
path: filepath.Clean(path),
kdfN: defaultKDFN,
kdfR: defaultKDFR,
kdfP: defaultKDFP,
plain: internalPlain{Updated: time.Now().UTC(), Data: make(map[string]json.RawMessage)},
}
if err := s.initNew(password, generateIdentity, commonName); err != nil {
return nil, err
}
if err := s.Flush(); err != nil {
return nil, err
}
return s, nil
}
// OpenEncryptedStore načte existující soubor a ověří heslo.
func OpenEncryptedStore(path, password string) (SecureJSONStore, error) {
if password == "" {
return nil, errors.New("empty password")
}
if _, err := os.Stat(path); err != nil {
return nil, err
}
s := &secureJSONStore{path: filepath.Clean(path)}
if err := s.load(password); err != nil {
return nil, err
}
return s, nil
}
// initNew vytvoří salt, odvodí klíč a vytvoří identitu pokud je třeba.
func (s *secureJSONStore) initNew(password string, generateIdentity bool, commonName string) error {
s.mu.Lock()
defer s.mu.Unlock()
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return err
}
key, err := deriveKey(password, salt, s.kdfN, s.kdfR, s.kdfP)
if err != nil {
return err
}
s.salt = salt
s.key = key
if generateIdentity {
if err := s.generateIdentityLocked(commonName); err != nil {
return err
}
}
return nil
}
func (s *secureJSONStore) generateIdentityLocked(commonName string) error {
// RSA 2048
pk, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)})
pubASN1, _ := x509.MarshalPKIXPublicKey(&pk.PublicKey)
pubPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1})
// Optional self-signed cert s CommonName uživatele (fallback pokud prázdný)
if strings.TrimSpace(commonName) == "" {
commonName = "Local User"
}
certPEM, _ := generateSelfSignedCert(pk, commonName)
_ = s.putLocked("_identity_private_pem", string(privPEM))
_ = s.putLocked("_identity_public_pem", string(pubPEM))
_ = s.putLocked("_identity_cert_pem", string(certPEM))
s.hasIdent = true
return nil
}
// deriveKey scrypt.
func deriveKey(password string, salt []byte, N, r, p int) ([]byte, error) {
return scrypt.Key([]byte(password), salt, N, r, p, 32)
}
// load načte soubor, přečte parametry, odvodí klíč a dešifruje.
func (s *secureJSONStore) load(password string) error {
b, err := os.ReadFile(s.path)
if err != nil {
return err
}
var env fileEnvelope
if err := json.Unmarshal(b, &env); err != nil {
return fmt.Errorf("invalid envelope: %w", err)
}
if env.Version != 1 || env.KDF != "scrypt" {
return errors.New("unsupported version/kdf")
}
salt, err := base64.StdEncoding.DecodeString(env.Salt)
if err != nil {
return fmt.Errorf("salt b64: %w", err)
}
key, err := deriveKey(password, salt, env.N, env.R, env.P)
if err != nil {
return err
}
nonce, err := base64.StdEncoding.DecodeString(env.Nonce)
if err != nil {
return fmt.Errorf("nonce b64: %w", err)
}
cBytes, err := base64.StdEncoding.DecodeString(env.Cipher)
if err != nil {
return fmt.Errorf("cipher b64: %w", err)
}
block, err := aes.NewCipher(key)
if err != nil {
return err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return err
}
plainBytes, err := gcm.Open(nil, nonce, cBytes, nil)
if err != nil {
return errors.New("invalid password or corrupted store")
}
var pl internalPlain
if err := json.Unmarshal(plainBytes, &pl); err != nil {
return fmt.Errorf("plaintext json: %w", err)
}
if pl.Data == nil {
pl.Data = make(map[string]json.RawMessage)
}
s.mu.Lock()
s.kdfN, s.kdfR, s.kdfP = env.N, env.R, env.P
s.salt = salt
s.key = key
s.plain = pl
s.dirty = false
s.hasIdent = s.hasIdentityLocked()
s.mu.Unlock()
return nil
}
func (s *secureJSONStore) hasIdentityLocked() bool {
_, ok1 := s.plain.Data["_identity_private_pem"]
_, ok2 := s.plain.Data["_identity_public_pem"]
return ok1 && ok2
}
// Put
func (s *secureJSONStore) Put(key string, value any) error {
if strings.HasPrefix(key, "_identity_") {
return errors.New("reserved identity key prefix")
}
s.mu.Lock()
defer s.mu.Unlock()
return s.putLocked(key, value)
}
func (s *secureJSONStore) putLocked(key string, value any) error {
if key == "" {
return errors.New("empty key")
}
b, err := json.Marshal(value)
if err != nil {
return err
}
if s.plain.Data == nil {
s.plain.Data = make(map[string]json.RawMessage)
}
s.plain.Data[key] = json.RawMessage(b)
s.plain.Updated = time.Now().UTC()
s.dirty = true
return nil
}
// Get
func (s *secureJSONStore) Get(key string, dst any) error {
s.mu.RLock()
raw, ok := s.plain.Data[key]
s.mu.RUnlock()
if !ok {
return fmt.Errorf("key not found: %s", key)
}
return json.Unmarshal(raw, dst)
}
// Delete
func (s *secureJSONStore) Delete(key string) error {
if strings.HasPrefix(key, "_identity_") {
return errors.New("cannot delete identity key")
}
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.plain.Data[key]; !ok {
return fmt.Errorf("key not found: %s", key)
}
delete(s.plain.Data, key)
s.plain.Updated = time.Now().UTC()
s.dirty = true
return nil
}
func (s *secureJSONStore) Has(key string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
_, ok := s.plain.Data[key]
return ok
}
// ListKeys (bez identity interních klíčů)
func (s *secureJSONStore) ListKeys() []string {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]string, 0, len(s.plain.Data))
for k := range s.plain.Data {
if !strings.HasPrefix(k, "_identity_") {
out = append(out, k)
}
}
return out
}
// Flush uloží na disk.
func (s *secureJSONStore) Flush() error {
s.mu.Lock()
defer s.mu.Unlock()
if !s.dirty {
return nil
}
if len(s.key) == 0 {
return errors.New("store not initialized")
}
plainBytes, err := json.Marshal(s.plain)
if err != nil {
return err
}
block, err := aes.NewCipher(s.key)
if err != nil {
return err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return err
}
cipherBytes := gcm.Seal(nil, nonce, plainBytes, nil)
env := fileEnvelope{
Version: 1,
KDF: "scrypt",
N: s.kdfN,
R: s.kdfR,
P: s.kdfP,
Salt: base64.StdEncoding.EncodeToString(s.salt),
Nonce: base64.StdEncoding.EncodeToString(nonce),
Cipher: base64.StdEncoding.EncodeToString(cipherBytes),
}
out, err := json.MarshalIndent(env, "", " ")
if err != nil {
return err
}
tmp := s.path + ".tmp"
if err := os.WriteFile(tmp, out, 0o600); err != nil {
return err
}
if err := os.Rename(tmp, s.path); err != nil {
return err
}
s.dirty = false
return nil
}
func (s *secureJSONStore) Close() error { return s.Flush() }
// ChangePassword re-derivuje klíč a re-encryptne celý soubor atomicky.
func (s *secureJSONStore) ChangePassword(newPassword string) error {
if err := validatePasswordStrength(newPassword); err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
newSalt := make([]byte, 16)
if _, err := rand.Read(newSalt); err != nil {
return err
}
newKey, err := deriveKey(newPassword, newSalt, s.kdfN, s.kdfR, s.kdfP)
if err != nil {
return err
}
// Dočasně si zapamatuj starý klíč pro rollback, i když tady rollback neimplementujeme chyba se projeví před přepsáním.
oldKey, oldSalt := s.key, s.salt
s.key, s.salt = newKey, newSalt
s.dirty = true
if err := s.Flush(); err != nil {
// rollback (best effort)
s.key, s.salt = oldKey, oldSalt
s.dirty = true
return err
}
return nil
}
// Identity helpers
func (s *secureJSONStore) IdentityPrivatePEM() string { return s.getString("_identity_private_pem") }
func (s *secureJSONStore) IdentityPublicPEM() string { return s.getString("_identity_public_pem") }
func (s *secureJSONStore) IdentityCertPEM() string { return s.getString("_identity_cert_pem") }
func (s *secureJSONStore) getString(key string) string {
s.mu.RLock()
defer s.mu.RUnlock()
if raw, ok := s.plain.Data[key]; ok {
var v string
if err := json.Unmarshal(raw, &v); err == nil {
return v
}
}
return ""
}
// GenerateRandomPassword vytvoří kryptograficky silné heslo ze sady znaků.
func GenerateRandomPassword(length int) (string, error) {
if length <= 0 {
return "", errors.New("invalid length")
}
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#-_+="
buf := make([]byte, length)
if _, err := rand.Read(buf); err != nil {
return "", err
}
for i, b := range buf {
buf[i] = alphabet[int(b)%len(alphabet)]
}
return string(buf), nil
}
// validatePasswordStrength provede základní kontrolu síly hesla.
// Požadavky: min délka 10, alespoň 3 z kategorií (upper, lower, digit, special !@#-_+=)
func validatePasswordStrength(pw string) error {
if len(pw) < 10 {
return errors.New("password too short (min 10)")
}
var hasU, hasL, hasD, hasS bool
for _, r := range pw {
switch {
case r >= 'A' && r <= 'Z':
hasU = true
case r >= 'a' && r <= 'z':
hasL = true
case r >= '0' && r <= '9':
hasD = true
default:
if strings.ContainsRune("!@#-_+=", r) {
hasS = true
}
}
}
cats := 0
if hasU {
cats++
}
if hasL {
cats++
}
if hasD {
cats++
}
if hasS {
cats++
}
if cats < 3 {
return errors.New("password too weak (need 3 types: upper, lower, digit, special)")
}
return nil
}
// ValidatePasswordForUI exportuje validaci pro použití ve Fyne UI.
func ValidatePasswordForUI(pw string) error { return validatePasswordStrength(pw) }

394
main.go Normal file → Executable file
View File

@ -1,25 +1,20 @@
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
encrypt "fckeuspy-go/lib"
"html/template"
"io/fs"
"log"
"math/big"
"net/http"
"os"
"path/filepath"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
var (
@ -27,10 +22,6 @@ var (
pubPEM []byte
certPEM []byte // self-signed cert jen pro sdílení identity (volitelné)
tmpl *template.Template
privPath = "identity_key.pem"
pubPath = "public.pem"
certPath = "identity.crt"
)
type envelope struct {
@ -41,292 +32,101 @@ type envelope struct {
}
func main() {
// 1) načti nebo vygeneruj klíče
if err := loadOrGenerateKeys(); err != nil {
if err := rootCmd.Execute(); err != nil {
log.Printf("Chyba: %v", err)
os.Exit(1)
}
// Malé zpoždění pro případné async logy
time.Sleep(50 * time.Millisecond)
}
func runFyne() {
storageDir, w := NewUI()
vaultPath := filepath.Join(storageDir, "vault.enc")
placeholder := widget.NewLabel("Inicializace trezoru…")
w.SetContent(placeholder)
showDialog := func() {
ShowPasswordVaultDialog(w, vaultPath, func(create bool, password string, commonName string) {
if password == "" { // Cancel nebo zavření dialogu => ukonči app
fyne.CurrentApp().Quit()
return
}
var store encrypt.SecureJSONStore
var err error
if create {
store, err = encrypt.CreateEncryptedStore(vaultPath, password, true, commonName)
} else {
store, err = encrypt.OpenEncryptedStore(vaultPath, password)
if err != nil {
dialog.NewError(err, w).Show()
return
}
}
if err != nil {
dialog.NewError(err, w).Show()
return
}
vs, err := NewVaultService(store)
if err != nil {
dialog.NewError(err, w).Show()
return
}
parts := buildEntries()
fyne.CurrentApp().Driver().AllWindows()[0].SetTitle("Encryptor (Vault)")
w.SetContent(buildTabbedUI(parts, vs, vaultPath))
})
}
// Pokud soubor neexistuje, dialog v režimu vytvořit (default). Pokud existuje, uživatel může přepnout.
showDialog()
w.ShowAndRun()
}
func RunWebApp() {
// Otevři nebo vytvoř šifrovaný trezor a načti identitu pouze z něj
vaultPath := os.Getenv("VAULT_PATH")
if vaultPath == "" {
vaultPath = "./vault.enc"
}
pw := os.Getenv("VAULT_PASSWORD")
if pw == "" {
log.Fatal("VAULT_PASSWORD must be set for web mode")
}
var store encrypt.SecureJSONStore
if _, statErr := os.Stat(vaultPath); os.IsNotExist(statErr) {
s, err := encrypt.CreateEncryptedStore(vaultPath, pw, true, "")
if err != nil {
log.Fatal(err)
}
store = s
} else {
s, err := encrypt.OpenEncryptedStore(vaultPath, pw)
if err != nil {
log.Fatal(err)
}
store = s
}
// Načti privátní klíč a veřejné materiály
privPEM := store.IdentityPrivatePEM()
if privPEM == "" {
log.Fatal("missing private key in vault")
}
block, _ := pem.Decode([]byte(privPEM))
if block == nil || block.Type != "RSA PRIVATE KEY" {
log.Fatal("invalid private key PEM in vault")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
log.Fatal(err)
}
priv = key
pubPEM = []byte(store.IdentityPublicPEM())
certPEM = []byte(store.IdentityCertPEM())
// 2) šablony
tmpl = template.Must(template.ParseGlob("templates/*.html"))
// 3) routing
http.HandleFunc("/", indexHandler)
http.HandleFunc("/public.pem", publicKeyHandler)
http.HandleFunc("/public.crt", publicCertHandler)
http.HandleFunc("/encrypt", encryptHandler)
http.HandleFunc("/decrypt", decryptHandler)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
log.Println("Server běží na http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
_ = tmpl.ExecuteTemplate(w, "index.html", nil)
}
func publicKeyHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-pem-file")
w.Write(pubPEM)
}
func publicCertHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-pem-file")
w.Write(certPEM)
}
func encryptHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad form", http.StatusBadRequest)
return
}
msg := r.Form.Get("message")
peer := r.Form.Get("pubkey") // může to být PEM PUBLIC KEY nebo CERT
pubKey, err := parsePeerPublicKey(peer)
if err != nil {
http.Error(w, "Neplatný public key/cert: "+err.Error(), http.StatusBadRequest)
return
}
// --- hybrid: AES-GCM + RSA-OAEP(SHA-256) ---
// 1) vygeneruj náhodný sym. klíč
aesKey := make([]byte, 32) // AES-256
if _, err := rand.Read(aesKey); err != nil {
http.Error(w, "Rand fail", 500)
return
}
block, err := aes.NewCipher(aesKey)
if err != nil {
http.Error(w, "AES fail", 500)
return
}
gcm, err := cipher.NewGCM(block)
if err != nil {
http.Error(w, "GCM fail", 500)
return
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
http.Error(w, "Nonce fail", 500)
return
}
ciphertext := gcm.Seal(nil, nonce, []byte(msg), nil)
// 2) zašifruj AES klíč cizím RSA klíčem (OAEP SHA-256)
label := []byte{} // prázdný label
ek, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, aesKey, label)
if err != nil {
http.Error(w, "RSA-OAEP fail: "+err.Error(), 500)
return
}
env := envelope{
EK: base64.StdEncoding.EncodeToString(ek),
N: base64.StdEncoding.EncodeToString(nonce),
CT: base64.StdEncoding.EncodeToString(ciphertext),
}
payload, _ := json.MarshalIndent(env, "", " ")
_ = tmpl.ExecuteTemplate(w, "encrypt.html", string(payload))
}
func decryptHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad form", http.StatusBadRequest)
return
}
in := r.Form.Get("payload")
var env envelope
if err := json.Unmarshal([]byte(in), &env); err != nil {
http.Error(w, "Neplatné JSON", 400)
return
}
ek, err := base64.StdEncoding.DecodeString(env.EK)
if err != nil {
http.Error(w, "ek base64", 400)
return
}
nonce, err := base64.StdEncoding.DecodeString(env.N)
if err != nil {
http.Error(w, "nonce base64", 400)
return
}
ct, err := base64.StdEncoding.DecodeString(env.CT)
if err != nil {
http.Error(w, "ct base64", 400)
return
}
// 1) rozšifruj AES klíč naším RSA
label := []byte{}
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, ek, label)
if err != nil {
http.Error(w, "RSA-OAEP decrypt fail: "+err.Error(), 400)
return
}
block, err := aes.NewCipher(aesKey)
if err != nil {
http.Error(w, "AES fail", 500)
return
}
gcm, err := cipher.NewGCM(block)
if err != nil {
http.Error(w, "GCM fail", 500)
return
}
plain, err := gcm.Open(nil, nonce, ct, nil)
if err != nil {
http.Error(w, "GCM open fail: "+err.Error(), 400)
return
}
_ = tmpl.ExecuteTemplate(w, "decrypt.html", string(plain))
}
// parsePeerPublicKey umí vzít buď PEM PUBLIC KEY, nebo PEM CERT a vrátí *rsa.PublicKey
func parsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(pemOrCert))
if block == nil {
return nil, errf("nenašel jsem PEM blok")
}
switch block.Type {
case "PUBLIC KEY":
k, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
rsaPub, ok := k.(*rsa.PublicKey)
if !ok {
return nil, errf("očekávám RSA PUBLIC KEY")
}
return rsaPub, nil
case "CERTIFICATE":
c, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
rsaPub, ok := c.PublicKey.(*rsa.PublicKey)
if !ok {
return nil, errf("cert neobsahuje RSA klíč")
}
return rsaPub, nil
default:
return nil, errf("nepodporovaný PEM typ: %s", block.Type)
}
}
func generateSelfSignedCert(pub *rsa.PublicKey) []byte {
tpl := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "Encryptor Local Identity",
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
// Tenhle cert je jen „vizitka“ (není pro TLS).
BasicConstraintsValid: true,
}
der, _ := x509.CreateCertificate(rand.Reader, &tpl, &tpl, pub, priv)
buf := &bytes.Buffer{}
_ = pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: der})
return buf.Bytes()
}
// malá helper chyba
type strErr string
func (e strErr) Error() string { return string(e) }
func errf(s string, a ...any) error { return strErr(fmtS(s, a...)) }
func fmtS(format string, a ...any) string {
var b bytes.Buffer
b.WriteString(format)
if len(a) > 0 {
b.WriteString(": ")
}
for i, v := range a {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(anyToString(v))
}
return b.String()
}
func anyToString(v any) string {
switch t := v.(type) {
case string:
return t
default:
j, _ := json.Marshal(t)
return string(j)
}
}
func loadOrGenerateKeys() error {
// existuje private key?
if fileExists(privPath) && fileExists(pubPath) && fileExists(certPath) {
// načti privátní klíč
pkBytes, err := os.ReadFile(privPath)
if err != nil {
return err
}
block, _ := pem.Decode(pkBytes)
if block == nil || block.Type != "RSA PRIVATE KEY" {
return fmt.Errorf("invalid private key PEM")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return err
}
priv = key
// načti public & cert
pubPEM, _ = os.ReadFile(pubPath)
certPEM, _ = os.ReadFile(certPath)
log.Println("Načtena existující identita z disku.")
return nil
}
// jinak vygeneruj nové
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return err
}
priv = key
// public
pubASN1, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey)
pubPEM = pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1})
// cert
certPEM = generateSelfSignedCert(&priv.PublicKey)
// ulož
if err := os.WriteFile(privPath,
pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}),
fs.FileMode(0600)); err != nil {
return err
}
if err := os.WriteFile(pubPath, pubPEM, 0644); err != nil {
return err
}
if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
return err
}
log.Println("Vygenerována nová identita a uložena na disk.")
return nil
}
func fileExists(p string) bool {
info, err := os.Stat(p)
return err == nil && !info.IsDir()
muxServer := NewServer()
log.Fatal(http.ListenAndServe(":8080", muxServer))
}

31
qr_manual_test.go Normal file
View File

@ -0,0 +1,31 @@
package main
import (
"image/png"
"os"
"testing"
)
// go test -run TestDecodeQR -v
func TestDecodeQR(t *testing.T) {
path := os.Getenv("QR_TEST_FILE")
if path == "" {
t.Skip("set QR_TEST_FILE to a PNG path to run")
return
}
f, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
img, err := png.Decode(f)
if err != nil {
t.Fatal(err)
}
txt, err := DecodeQR(img)
if err != nil {
t.Fatalf("decode error: %v", err)
}
t.Logf("decoded: %d bytes", len(txt))
}

520
qr_support.go Executable file
View File

@ -0,0 +1,520 @@
package main
import (
"bytes"
"errors"
"image"
"image/color"
"image/draw"
"image/png"
"log"
"math"
"os/exec"
"github.com/liyue201/goqr"
"github.com/makiuchi-d/gozxing"
qrx "github.com/makiuchi-d/gozxing/qrcode"
qrgen "github.com/skip2/go-qrcode"
)
// GenerateQRPNG returns PNG bytes for the given text.
func GenerateQRPNG(text string, size int) ([]byte, error) {
if size <= 0 {
size = 256
}
// Use Low EC for larger modules; improves decode reliability for long payloads
png, err := qrgen.Encode(text, qrgen.Low, size)
if err != nil {
return nil, err
}
return png, nil
}
// DecodeQR decodes first QR code text from an image.
func DecodeQR(img image.Image) (string, error) {
log.Printf("[qr] start decode: %dx%d %T", img.Bounds().Dx(), img.Bounds().Dy(), img)
// Try basic decode first
codes, err := goqr.Recognize(img)
if err == nil && len(codes) > 0 {
log.Printf("[qr] basic success length=%d", len(codes))
return string(codes[0].Payload), nil
}
if err != nil {
log.Printf("[qr] basic error: %v", err)
} else {
log.Printf("[qr] basic no codes")
}
// Convert palette/indexed images to RGBA first to avoid issues with goqr
if _, ok := img.ColorModel().(color.Palette); ok {
bounds := img.Bounds()
rgba := image.NewRGBA(bounds)
draw.Draw(rgba, bounds, img, bounds.Min, draw.Src)
img = rgba
// Try again after conversion
codes, err := goqr.Recognize(img)
if err == nil && len(codes) > 0 {
log.Printf("[qr] palette-convert success")
return string(codes[0].Payload), nil
}
if err != nil {
log.Printf("[qr] palette-convert err: %v", err)
} else {
log.Printf("[qr] palette-convert no codes")
}
}
// Try multiple preprocessing strategies to improve recognition from clipboard screenshots
attempts := []image.Image{}
add := func(im image.Image) {
if im != nil {
attempts = append(attempts, im)
}
}
add(img)
bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy()
// Helper: nearest-neighbour scale
scale := func(src image.Image, fw, fh int) image.Image {
if fw <= 0 || fh <= 0 {
return src
}
dst := image.NewNRGBA(image.Rect(0, 0, fw, fh))
sb := src.Bounds()
sw, sh := float64(sb.Dx()), float64(sb.Dy())
for y := 0; y < fh; y++ {
sy := int(float64(y)/float64(fh)*sh + 0.5)
if sy >= sb.Dy() {
sy = sb.Dy() - 1
}
for x := 0; x < fw; x++ {
sx := int(float64(x)/float64(fw)*sw + 0.5)
if sx >= sb.Dx() {
sx = sb.Dx() - 1
}
dst.Set(x, y, src.At(sb.Min.X+sx, sb.Min.Y+sy))
}
}
return dst
}
// Helper: binarize with adaptive threshold (simple mean + variance fudge)
binarize := func(src image.Image) image.Image {
b := src.Bounds()
gray := image.NewGray(b)
var sum, sum2 float64
total := float64(b.Dx() * b.Dy())
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, bl, _ := src.At(x, y).RGBA()
lum := float64((r*299 + g*587 + bl*114) / 1000 >> 8) // 0-255 approx
sum += lum
sum2 += lum * lum
}
}
mean := sum / total
variance := (sum2 / total) - mean*mean
stddev := math.Sqrt(math.Max(variance, 0))
// threshold tweaks: lower slightly for dark backgrounds
thr := mean - stddev*0.15
if thr < 50 {
thr = 50
}
if thr > 200 {
thr = 200
}
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, bl, _ := src.At(x, y).RGBA()
lum := float64((r*299 + g*587 + bl*114) / 1000 >> 8)
var c color.Gray
if lum < thr {
c = color.Gray{Y: 0}
} else {
c = color.Gray{Y: 255}
}
gray.Set(x, y, c)
}
}
return gray
}
// Scale strategy: even if image is not tiny, scaled variants sometimes help goqr lock onto module grid
// 1) If very small, aggressive scale
if w < 280 || h < 280 {
factor := 2
if w < 160 {
factor = 3
}
if w < 100 {
factor = 4
}
add(scale(img, w*factor, h*factor))
}
// 2) Always try moderate upscale variants for mid-sized screenshots (helps when original was downscaled with interpolation)
if w >= 180 && w <= 900 { // avoid exploding huge images
add(scale(img, w*2, h*2))
if w*3 < 2000 && h*3 < 2000 { // guard upper bound
add(scale(img, w*3, h*3))
}
}
// Add binarized versions (original + scaled)
add(binarize(img))
if w < 320 || h < 320 {
factor := 2
add(binarize(scale(img, w*factor, h*factor)))
}
// Also binarize any newly added large upscales (skip if too big)
for _, cand := range attempts { // safe: attempts already contains originals
cw := cand.Bounds().Dx()
ch := cand.Bounds().Dy()
if (cw <= 1400 && ch <= 1400) && (cw > w || ch > h) { // only for upscaled variants within limit
add(binarize(cand))
}
}
// Add contrast-enhanced alpha-flattened variant
flatten := func(src image.Image) image.Image {
b := src.Bounds()
dst := image.NewRGBA(b)
draw.Draw(dst, b, image.Black, image.Point{}, draw.Src)
draw.Draw(dst, b, src, b.Min, draw.Over)
return dst
}
add(flatten(img))
// Rotations (90,180,270) in case of orientation issues
rotate := func(src image.Image) []image.Image {
b := src.Bounds()
w := b.Dx()
h := b.Dy()
r90 := image.NewRGBA(image.Rect(0, 0, h, w))
r180 := image.NewRGBA(image.Rect(0, 0, w, h))
r270 := image.NewRGBA(image.Rect(0, 0, h, w))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
c := src.At(b.Min.X+x, b.Min.Y+y)
// 90
r90.Set(h-1-y, x, c)
// 180
r180.Set(w-1-x, h-1-y, c)
// 270
r270.Set(y, w-1-x, c)
}
}
return []image.Image{r90, r180, r270}
}
for _, r := range rotate(img) {
add(r)
}
var firstErr error
for idx, attempt := range attempts {
codes, err := goqr.Recognize(attempt)
if err != nil {
if firstErr == nil {
firstErr = err
}
log.Printf("[qr] attempt %d error: %v", idx, err)
continue
}
if len(codes) > 0 {
log.Printf("[qr] attempt %d success (%d codes)", idx, len(codes))
return string(codes[0].Payload), nil
}
log.Printf("[qr] attempt %d no codes", idx)
}
// ZXing helper used across multiple branches
decodeZX := func(src image.Image, hints map[gozxing.DecodeHintType]interface{}) (string, error) {
binSrc := gozxing.NewLuminanceSourceFromImage(src)
// Try hybrid first (usually better for photos)
bmpH, _ := gozxing.NewBinaryBitmap(gozxing.NewHybridBinarizer(binSrc))
reader := qrx.NewQRCodeReader()
if res, err := reader.Decode(bmpH, hints); err == nil {
return res.GetText(), nil
}
// Fallback to global histogram
bmpG, _ := gozxing.NewBinaryBitmap(gozxing.NewGlobalHistgramBinarizer(binSrc))
if res, err := reader.Decode(bmpG, hints); err == nil {
return res.GetText(), nil
} else {
return "", err
}
}
// Also try ZXing on all preprocessed attempts
for idx, attempt := range attempts {
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
gozxing.DecodeHintType_PURE_BARCODE: true,
}); err == nil && txt != "" {
log.Printf("[qr] zxing attempt %d success (pure)", idx)
return txt, nil
}
if txt, err := decodeZX(attempt, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
}); err == nil && txt != "" {
log.Printf("[qr] zxing attempt %d success (try-harder)", idx)
return txt, nil
}
}
if firstErr != nil {
log.Printf("[qr] preprocess firstErr: %v", firstErr)
}
// --- Heuristic fallback: auto-invert + quiet-zone pad + gozxing ---
addQuietZone := func(src image.Image) image.Image {
b := src.Bounds()
pad := int(math.Max(float64(b.Dx()/32), 8))
out := image.NewRGBA(image.Rect(0, 0, b.Dx()+pad*2, b.Dy()+pad*2))
// fill white
for y := 0; y < out.Bounds().Dy(); y++ {
for x := 0; x < out.Bounds().Dx(); x++ {
out.Set(x, y, color.White)
}
}
draw.Draw(out, b.Add(image.Point{X: pad, Y: pad}), src, b.Min, draw.Src)
return out
}
invert := func(src image.Image) image.Image {
b := src.Bounds()
out := image.NewRGBA(b)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, bb, a := src.At(x, y).RGBA()
out.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - bb/257), uint8(a / 257)})
}
}
return out
}
// estimate dark pixel ratio
b := img.Bounds()
var dark, total int
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
r, g, bb, _ := img.At(x, y).RGBA()
lum := (r*299 + g*587 + bb*114) / 1000
if lum < 128<<8 {
dark++
}
total++
}
}
ratio := float64(dark) / float64(total)
needInvert := ratio > 0.85 // almost full dark background (white modules?)
var modified []image.Image
base := img
if needInvert {
base = invert(base)
}
modified = append(modified, addQuietZone(base))
modified = append(modified, addQuietZone(invert(base)))
for _, m := range modified {
if codes, err := goqr.Recognize(m); err == nil && len(codes) > 0 {
log.Printf("[qr] quiet-zone/invert success")
return string(codes[0].Payload), nil
}
// Also try ZXing on these variants before moving on
if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
gozxing.DecodeHintType_PURE_BARCODE: true,
}); err == nil && txt != "" {
log.Printf("[qr] gozxing success (quiet-zone+pure)")
return txt, nil
}
if txt, err := decodeZX(m, map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
}); err == nil && txt != "" {
log.Printf("[qr] gozxing success (quiet-zone+try-harder)")
return txt, nil
}
}
// ZXing with TRY_HARDER and PURE_BARCODE hints which often fix dense PNGs
hints := map[gozxing.DecodeHintType]interface{}{
gozxing.DecodeHintType_TRY_HARDER: true,
// Many of our images are perfect render PNGs from go-qrcode -> enable PURE_BARCODE
gozxing.DecodeHintType_PURE_BARCODE: true,
}
if txt, err := decodeZX(base, hints); err == nil && txt != "" {
log.Printf("[qr] gozxing success (hints)")
return txt, nil
} else if err != nil {
log.Printf("[qr] gozxing(hints) err: %v", err)
}
// Try again without PURE_BARCODE in case quiet zone is cropped
hints2 := map[gozxing.DecodeHintType]interface{}{gozxing.DecodeHintType_TRY_HARDER: true}
if txt, err := decodeZX(base, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing success (try-harder)")
return txt, nil
} else if err != nil {
log.Printf("[qr] gozxing(try-harder) err: %v", err)
}
// Try ZXing on rotations as well
for _, r := range rotate(base) {
if txt, err := decodeZX(r, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing success (rotated)")
return txt, nil
}
if txt, err := decodeZX(r, hints); err == nil && txt != "" {
log.Printf("[qr] gozxing success (rotated pure)")
return txt, nil
}
}
// Extra pure-QR recovery: try small insets and simple upscales with PURE_BARCODE.
inset := func(src image.Image, px int) image.Image {
if px <= 0 {
return src
}
b := src.Bounds()
r := image.Rect(b.Min.X+px, b.Min.Y+px, b.Max.X-px, b.Max.Y-px)
if r.Dx() <= 10 || r.Dy() <= 10 {
return src
}
out := image.NewRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
draw.Draw(out, out.Bounds(), src, r.Min, draw.Src)
return out
}
cropSides := func(src image.Image, l, t, r, b int) image.Image {
bb := src.Bounds()
rr := image.Rect(bb.Min.X+l, bb.Min.Y+t, bb.Max.X-r, bb.Max.Y-b)
if rr.Dx() <= 10 || rr.Dy() <= 10 || rr.Min.X >= rr.Max.X || rr.Min.Y >= rr.Max.Y {
return src
}
out := image.NewRGBA(image.Rect(0, 0, rr.Dx(), rr.Dy()))
draw.Draw(out, out.Bounds(), src, rr.Min, draw.Src)
return out
}
tryPure := func(src image.Image) (string, bool) {
for _, px := range []int{1, 2, 3} {
if txt, err := decodeZX(inset(src, px), hints); err == nil && txt != "" {
log.Printf("[qr] gozxing pure+inset %d success", px)
return txt, true
}
}
return "", false
}
// Try on base, binarized base and scaled variants
if txt, ok := tryPure(base); ok {
return txt, nil
}
if txt, ok := tryPure(binarize(base)); ok {
return txt, nil
}
if w < 800 && h < 800 { // upscale to mitigate rounding of module size
if txt, ok := tryPure(scale(base, w*2, h*2)); ok {
return txt, nil
}
if txt, ok := tryPure(scale(base, w*3, h*3)); ok {
return txt, nil
}
}
// Brute-force small asymmetric crops to satisfy ZXing's 1 (mod 4) dimension constraint.
for _, src := range []image.Image{base, binarize(base)} {
for l := 0; l <= 2; l++ {
for t := 0; t <= 2; t++ {
for r := 0; r <= 2; r++ {
for btm := 0; btm <= 2; btm++ {
cand := cropSides(src, l, t, r, btm)
if cand == src {
continue
}
if txt, err := decodeZX(cand, hints); err == nil && txt != "" {
log.Printf("[qr] gozxing pure+asym inset %d,%d,%d,%d success", l, t, r, btm)
return txt, nil
}
if txt, err := decodeZX(cand, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing asym inset %d,%d,%d,%d success", l, t, r, btm)
return txt, nil
}
}
}
}
}
}
// Bounding box crop of dark pixels, then upscale and retry
boundsAll := base.Bounds()
minX, minY := boundsAll.Max.X, boundsAll.Max.Y
maxX, maxY := boundsAll.Min.X, boundsAll.Min.Y
for y := boundsAll.Min.Y; y < boundsAll.Max.Y; y++ {
for x := boundsAll.Min.X; x < boundsAll.Max.X; x++ {
r, g, bb, _ := base.At(x, y).RGBA()
lum := (r*299 + g*587 + bb*114) / 1000
if lum < 180<<8 { // treat as dark
if x < minX {
minX = x
}
if y < minY {
minY = y
}
if x > maxX {
maxX = x
}
if y > maxY {
maxY = y
}
}
}
}
if minX < maxX && minY < maxY {
crop := image.NewRGBA(image.Rect(0, 0, maxX-minX+1, maxY-minY+1))
draw.Draw(crop, crop.Bounds(), base, image.Point{X: minX, Y: minY}, draw.Src)
log.Printf("[qr] crop bbox size=%dx%d", crop.Bounds().Dx(), crop.Bounds().Dy())
for _, up := range []int{2, 3, 4} {
cw := crop.Bounds().Dx() * up
ch := crop.Bounds().Dy() * up
if cw > 2400 || ch > 2400 {
continue
}
scaled := image.NewRGBA(image.Rect(0, 0, cw, ch))
for y := 0; y < ch; y++ {
sy := y / up
for x := 0; x < cw; x++ {
sx := x / up
scaled.Set(x, y, crop.At(sx, sy))
}
}
if txt, err := decodeZX(scaled, hints2); err == nil && txt != "" {
log.Printf("[qr] gozxing crop+scale success up=%d", up)
return txt, nil
} else if err != nil {
log.Printf("[qr] crop up=%d fail: %v", up, err)
}
}
}
if firstErr != nil {
return "", firstErr
}
// Final external fallback: try zbarimg if available on the system
if cmd, lookErr := exec.LookPath("zbarimg"); lookErr == nil {
buf := &bytes.Buffer{}
if err := png.Encode(buf, img); err == nil {
c := exec.Command(cmd, "-q", "--raw", "-")
c.Stdin = bytes.NewReader(buf.Bytes())
out, err := c.Output()
if err == nil && len(out) > 0 {
log.Printf("[qr] zbarimg success")
return string(out), nil
}
if err != nil {
log.Printf("[qr] zbarimg error: %v", err)
}
}
}
return "", errors.New("no qr code found")
}
// LoadPNG decodes raw PNG bytes to image.Image.
func LoadPNG(b []byte) (image.Image, error) {
im, err := png.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
}
return im, nil
}

159
server.go Executable file
View File

@ -0,0 +1,159 @@
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"net/http"
encrypt "fckeuspy-go/lib"
)
func NewServer() *http.ServeMux {
server := &http.ServeMux{}
// 3) routing
server.HandleFunc("/", indexHandler)
server.HandleFunc("/public.pem", publicKeyHandler)
server.HandleFunc("/public.crt", publicCertHandler)
server.HandleFunc("/encrypt", encryptHandler)
server.HandleFunc("/decrypt", decryptHandler)
server.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
// log.Println("Server běží na http://localhost:8080")
return server
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
_ = tmpl.ExecuteTemplate(w, "index.html", nil)
}
func publicKeyHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-pem-file")
w.Write(pubPEM)
}
func publicCertHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-pem-file")
w.Write(certPEM)
}
func encryptHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad form", http.StatusBadRequest)
return
}
msg := r.Form.Get("message")
peer := r.Form.Get("pubkey") // může to být PEM PUBLIC KEY nebo CERT
pubKey, err := encrypt.ParsePeerPublicKey(peer)
if err != nil {
http.Error(w, "Neplatný public key/cert: "+err.Error(), http.StatusBadRequest)
return
}
// --- hybrid: AES-GCM + RSA-OAEP(SHA-256) ---
// 1) vygeneruj náhodný sym. klíč
aesKey := make([]byte, 32) // AES-256
if _, err := rand.Read(aesKey); err != nil {
http.Error(w, "Rand fail", 500)
return
}
block, err := aes.NewCipher(aesKey)
if err != nil {
http.Error(w, "AES fail", 500)
return
}
gcm, err := cipher.NewGCM(block)
if err != nil {
http.Error(w, "GCM fail", 500)
return
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
http.Error(w, "Nonce fail", 500)
return
}
ciphertext := gcm.Seal(nil, nonce, []byte(msg), nil)
// 2) zašifruj AES klíč cizím RSA klíčem (OAEP SHA-256)
label := []byte{} // prázdný label
ek, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, aesKey, label)
if err != nil {
http.Error(w, "RSA-OAEP fail: "+err.Error(), 500)
return
}
env := envelope{
EK: base64.StdEncoding.EncodeToString(ek),
N: base64.StdEncoding.EncodeToString(nonce),
CT: base64.StdEncoding.EncodeToString(ciphertext),
}
payload, _ := json.MarshalIndent(env, "", " ")
_ = tmpl.ExecuteTemplate(w, "encrypt.html", string(payload))
}
func decryptHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad form", http.StatusBadRequest)
return
}
in := r.Form.Get("payload")
var env envelope
if err := json.Unmarshal([]byte(in), &env); err != nil {
http.Error(w, "Neplatné JSON", 400)
return
}
ek, err := base64.StdEncoding.DecodeString(env.EK)
if err != nil {
http.Error(w, "ek base64", 400)
return
}
nonce, err := base64.StdEncoding.DecodeString(env.N)
if err != nil {
http.Error(w, "nonce base64", 400)
return
}
ct, err := base64.StdEncoding.DecodeString(env.CT)
if err != nil {
http.Error(w, "ct base64", 400)
return
}
// 1) rozšifruj AES klíč naším RSA
label := []byte{}
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, ek, label)
if err != nil {
http.Error(w, "RSA-OAEP decrypt fail: "+err.Error(), 400)
return
}
block, err := aes.NewCipher(aesKey)
if err != nil {
http.Error(w, "AES fail", 500)
return
}
gcm, err := cipher.NewGCM(block)
if err != nil {
http.Error(w, "GCM fail", 500)
return
}
plain, err := gcm.Open(nil, nonce, ct, nil)
if err != nil {
http.Error(w, "GCM open fail: "+err.Error(), 400)
return
}
_ = tmpl.ExecuteTemplate(w, "decrypt.html", string(plain))
}

0
static/style.css Normal file → Executable file
View File

0
templates/decrypt.html Normal file → Executable file
View File

0
templates/encrypt.html Normal file → Executable file
View File

0
templates/index.html Normal file → Executable file
View File

846
ui.go Executable file
View File

@ -0,0 +1,846 @@
package main
import (
"bytes"
"encoding/base64"
"errors"
encrypt "fckeuspy-go/lib"
"fmt"
"image"
"image/color"
"image/png"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// --- Core UI model ---
type uiParts struct {
outKey, msg, peer, cipherOut, payload, plainOut *widget.Entry
toastLabel *widget.Label
cipherQR, pubQR, crtQR, peerQR, payloadQR *canvas.Image
showQR, showPeerQR, showPayloadQR bool
}
func buildEntries() *uiParts {
p := &uiParts{
outKey: widget.NewMultiLineEntry(),
msg: widget.NewMultiLineEntry(),
peer: widget.NewMultiLineEntry(),
cipherOut: widget.NewMultiLineEntry(),
payload: widget.NewMultiLineEntry(),
plainOut: widget.NewMultiLineEntry(),
toastLabel: widget.NewLabel(""),
cipherQR: canvas.NewImageFromImage(nil),
pubQR: canvas.NewImageFromImage(nil),
crtQR: canvas.NewImageFromImage(nil),
peerQR: canvas.NewImageFromImage(nil),
payloadQR: canvas.NewImageFromImage(nil),
showQR: true, showPeerQR: true, showPayloadQR: true,
}
p.cipherQR.SetMinSize(fyne.NewSize(220, 220))
p.pubQR.SetMinSize(fyne.NewSize(200, 200))
p.peerQR.SetMinSize(fyne.NewSize(200, 200))
p.payloadQR.SetMinSize(fyne.NewSize(220, 220))
p.toastLabel.Hide()
return p
}
func (p *uiParts) showToast(s string) {
fyne.Do(func() { p.toastLabel.SetText(s); p.toastLabel.Show() })
time.AfterFunc(1500*time.Millisecond, func() {
fyne.Do(func() {
if p.toastLabel.Text == s {
p.toastLabel.Hide()
}
})
})
}
// Theme
type simpleTheme struct{}
func (simpleTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
if n == theme.ColorNameBackground {
return color.NRGBA{24, 27, 31, 255}
}
return theme.DefaultTheme().Color(n, v)
}
func (simpleTheme) Font(st fyne.TextStyle) fyne.Resource { return theme.DefaultTheme().Font(st) }
func (simpleTheme) Icon(n fyne.ThemeIconName) fyne.Resource { return theme.DefaultTheme().Icon(n) }
func (simpleTheme) Size(n fyne.ThemeSizeName) float32 { return theme.DefaultTheme().Size(n) }
// Facade interface
type ServiceFacade interface {
Encrypt(msg, peer string) (string, error)
Decrypt(json string) (string, error)
PublicPEM() string
PublicCert() string
ListContacts() ([]Contact, error)
SaveContact(c Contact) error
DeleteContact(id string) error
}
// Clipboard helpers
func copyClip(s string, parts *uiParts) {
fyne.CurrentApp().Clipboard().SetContent(s)
parts.showToast("Zkopírováno")
}
func copyImageToClipboard(img image.Image, parts *uiParts) {
if img == nil {
return
}
buf := &bytes.Buffer{}
if err := png.Encode(buf, img); err != nil {
parts.showToast("Chyba PNG")
return
}
choose := func() *exec.Cmd {
wayland := os.Getenv("WAYLAND_DISPLAY") != ""
has := func(b string) bool { _, e := exec.LookPath(b); return e == nil }
if wayland {
if has("wl-copy") {
return exec.Command("wl-copy", "--type", "image/png")
}
if has("xclip") {
return exec.Command("xclip", "-selection", "clipboard", "-t", "image/png")
}
} else {
if has("xclip") {
return exec.Command("xclip", "-selection", "clipboard", "-t", "image/png")
}
if has("wl-copy") {
return exec.Command("wl-copy", "--type", "image/png")
}
}
return nil
}()
if choose == nil {
parts.showToast("Chybí wl-copy/xclip")
return
}
stdin, _ := choose.StdinPipe()
if err := choose.Start(); err != nil {
parts.showToast("Nelze spustit")
return
}
_, _ = stdin.Write(buf.Bytes())
_ = stdin.Close()
if err := choose.Wait(); err != nil {
parts.showToast("Selhalo")
return
}
parts.showToast("QR obrázek ve schránce")
}
func readImageClipboard() (image.Image, error) {
has := func(b string) bool { _, e := exec.LookPath(b); return e == nil }
tryDecode := func(d []byte) (image.Image, bool) {
if len(d) == 0 {
return nil, false
}
if img, _, e := image.Decode(bytes.NewReader(d)); e == nil {
return img, true
}
s := strings.TrimSpace(string(d))
if strings.HasPrefix(s, "data:image") {
if p := strings.Index(s, ","); p > 0 {
s = s[p+1:]
}
}
if raw, err := base64.StdEncoding.DecodeString(s); err == nil {
if img, _, e2 := image.Decode(bytes.NewReader(raw)); e2 == nil {
return img, true
}
}
return nil, false
}
if os.Getenv("WAYLAND_DISPLAY") != "" && has("wl-paste") {
if types, err := exec.Command("wl-paste", "--list-types").Output(); err == nil {
log.Printf("[clip] wl types: %s", strings.TrimSpace(string(types)))
pref := []string{"image/png", "image/jpeg", "image/jpg", "image/webp"}
seen := map[string]bool{}
for _, p := range pref {
seen[p] = true
}
order := append([]string{}, pref...)
for _, t := range strings.Split(string(types), "\n") {
t = strings.TrimSpace(t)
if t == "" || !strings.HasPrefix(t, "image/") {
continue
}
if !seen[t] {
order = append(order, t)
}
}
for _, t := range order {
if data, err := exec.Command("wl-paste", "--type", t).Output(); err == nil {
log.Printf("[clip] got type %s size=%d", t, len(data))
if img, ok := tryDecode(data); ok {
// save debug
_ = os.MkdirAll("qr_debug", 0o755)
fn := filepath.Join("qr_debug", fmt.Sprintf("clip_raw_%d.png", time.Now().UnixNano()))
if f, e := os.Create(fn); e == nil {
_ = png.Encode(f, img)
f.Close()
log.Printf("[clip] saved %s", fn)
}
return img, nil
}
}
}
}
if data, err := exec.Command("wl-paste").Output(); err == nil {
log.Printf("[clip] generic wl-paste size=%d", len(data))
if img, ok := tryDecode(data); ok {
return img, nil
}
}
}
if has("xclip") {
for _, m := range []string{"image/png", "image/jpeg", "image/jpg", "image/bmp"} {
if data, err := exec.Command("xclip", "-selection", "clipboard", "-t", m, "-o").Output(); err == nil {
log.Printf("[clip] xclip type %s size=%d", m, len(data))
if img, ok := tryDecode(data); ok {
return img, nil
}
}
}
if data, err := exec.Command("xclip", "-selection", "clipboard", "-o").Output(); err == nil {
log.Printf("[clip] xclip raw size=%d", len(data))
if img, ok := tryDecode(data); ok {
return img, nil
}
}
}
return nil, errors.New("nenalezen žádný obrázek ve schránce")
}
// Identity tab
func buildIdentityTab(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
// Toolbar: choose what to encode into the QR to keep density manageable
mode := "combined" // combined, cert, pub
chooser := widget.NewSelect([]string{"Veřejný+Cert", "Jen Cert", "Jen Veřejný"}, func(string) {})
chooser.Selected = "Veřejný+Cert"
var update func()
chooser.OnChanged = func(v string) {
switch v {
case "Jen Cert":
mode = "cert"
case "Jen Veřejný":
mode = "pub"
default:
mode = "combined"
}
update()
}
deleteBtn := widget.NewButton("Smazat identitu", func() {
pw := widget.NewPasswordEntry()
form := widget.NewForm(widget.NewFormItem("Heslo", pw))
warn := widget.NewLabel("Smazat vše?")
d := dialog.NewCustomConfirm("Potvrdit smazání", "Smazat", "Zrušit", container.NewVBox(warn, form), func(ok bool) {
if !ok {
return
}
if _, err := encrypt.OpenEncryptedStore(vaultPath, pw.Text); err != nil {
dialog.NewError(errors.New("neplatné heslo"), fyne.CurrentApp().Driver().AllWindows()[0]).Show()
return
}
_ = os.Remove(vaultPath)
fyne.CurrentApp().Quit()
}, fyne.CurrentApp().Driver().AllWindows()[0])
d.Resize(fyne.NewSize(420, 200))
d.Show()
})
makeQR := func(data string, target *canvas.Image) {
if data == "" {
target.Image = nil
target.Refresh()
return
}
if b, err := GenerateQRPNG(data, 512); err == nil {
if im, err2 := LoadPNG(b); err2 == nil {
target.Image = im
target.Refresh()
}
}
}
update = func() {
if parts.showQR {
var text string
switch mode {
case "cert":
text = strings.TrimSpace(svc.PublicCert())
case "pub":
text = strings.TrimSpace(svc.PublicPEM())
default:
text = strings.TrimSpace(svc.PublicPEM() + "\n" + svc.PublicCert())
}
makeQR(text, parts.pubQR)
// debug save for comparison
if parts.pubQR.Image != nil {
_ = os.MkdirAll("qr_debug", 0o755)
if f, e := os.Create(filepath.Join("qr_debug", "identity_current.png")); e == nil {
png.Encode(f, parts.pubQR.Image)
f.Close()
}
}
} else {
parts.pubQR.Image = nil
parts.pubQR.Refresh()
}
}
update()
box := container.NewVBox(container.NewHBox(widget.NewLabel("QR obsah:"), chooser, layout.NewSpacer(), widget.NewButtonWithIcon("Kopírovat jako obrázek", theme.ContentPasteIcon(), func() { copyImageToClipboard(parts.pubQR.Image, parts) })), parts.pubQR)
return container.NewVScroll(container.NewVBox(container.NewHBox(widget.NewLabelWithStyle("Moje identita", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer()), box, deleteBtn))
}
// Decrypt tab
func buildDecryptTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
parts.plainOut.Disable()
parts.payload.Disable()
parts.payload.SetPlaceHolder("Cyphertext se načte po vložení QR kódu…")
parts.payload.OnChanged = nil
parts.payloadQR.FillMode = canvas.ImageFillContain
parts.payloadQR.SetMinSize(fyne.NewSize(260, 260))
decrypt := func(text string) {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
parts.plainOut.SetText("")
return
}
go func(j string) {
res, err := svc.Decrypt(j)
if err != nil {
fyne.Do(func() {
parts.plainOut.SetText("")
parts.showToast("Chyba dešifrování")
})
return
}
fyne.Do(func() { parts.plainOut.SetText(res) })
}(trimmed)
}
setPayload := func(cipher string, img image.Image) {
parts.payload.SetText(cipher)
parts.payloadQR.Image = img
parts.payloadQR.Refresh()
decrypt(cipher)
}
decodeFromImage := func(img image.Image) {
if img == nil {
parts.showToast("Žádný QR obrázek")
return
}
txt, err := DecodeQR(img)
if err != nil {
bounds := img.Bounds()
inv := image.NewRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, a := img.At(x, y).RGBA()
inv.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - b/257), uint8(a / 257)})
}
}
if txt2, err2 := DecodeQR(inv); err2 == nil {
setPayload(txt2, img)
parts.showToast("Načteno z invert QR")
return
}
parts.showToast("QR nenalezen: " + err.Error())
return
}
setPayload(txt, img)
parts.showToast("Načteno z QR")
}
pasteQRBtn := widget.NewButtonWithIcon("Vložit ze schránky", theme.ContentPasteIcon(), func() {
img, err := readImageClipboard()
if err != nil {
parts.showToast("Chyba schránky: " + err.Error())
return
}
decodeFromImage(img)
})
openQRBtn := widget.NewButtonWithIcon("Otevřít obrázek", theme.FolderOpenIcon(), func() {
win := fyne.CurrentApp().Driver().AllWindows()[0]
fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
if err != nil || rc == nil {
return
}
defer rc.Close()
data, readErr := io.ReadAll(rc)
if readErr != nil {
parts.showToast("Chyba čtení souboru")
return
}
img, _, decErr := image.Decode(bytes.NewReader(data))
if decErr != nil {
parts.showToast("Neplatný obrázek: " + decErr.Error())
return
}
decodeFromImage(img)
}, win)
fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
fd.Show()
})
copyPayloadBtn := widget.NewButtonWithIcon("Kopírovat payload", theme.ContentCopyIcon(), func() {
if parts.payload.Text == "" {
return
}
copyClip(parts.payload.Text, parts)
})
clearBtn := widget.NewButtonWithIcon("Vymazat", theme.ContentClearIcon(), func() {
parts.payload.SetText("")
parts.payloadQR.Image = nil
parts.payloadQR.Refresh()
parts.plainOut.SetText("")
parts.showToast("Vymazáno")
})
toolbar := container.NewHBox(pasteQRBtn, openQRBtn, copyPayloadBtn, clearBtn, layout.NewSpacer())
return container.NewVBox(
widget.NewLabelWithStyle("Dešifrování", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
toolbar,
container.NewHBox(parts.payloadQR, layout.NewSpacer()),
widget.NewLabel("Payload"),
parts.payload,
widget.NewLabel("Výsledek"),
parts.plainOut,
)
}
// Per-contact encryption popup (QR-only output)
func openEncryptPopup(parts *uiParts, svc ServiceFacade, ct Contact) {
msgEntry := widget.NewMultiLineEntry()
msgEntry.SetMinRowsVisible(6)
msgEntry.Wrapping = fyne.TextWrapWord
status := widget.NewLabel("Zadej zprávu…")
qrImg := canvas.NewImageFromImage(nil)
qrImg.FillMode = canvas.ImageFillContain
qrImg.SetMinSize(fyne.NewSize(300, 300))
updateQR := func(text string) {
if strings.TrimSpace(text) == "" {
qrImg.Image = nil
qrImg.Refresh()
return
}
if b, err := GenerateQRPNG(text, 512); err == nil {
if im, err2 := LoadPNG(b); err2 == nil {
qrImg.Image = im
qrImg.Refresh()
}
}
}
win := fyne.CurrentApp().Driver().AllWindows()[0]
lastCipher := ""
doEncrypt := func() {
m := strings.TrimSpace(msgEntry.Text)
fyne.Do(func() {
if m == "" {
status.SetText("Zpráva je prázdná")
lastCipher = ""
updateQR("")
return
}
status.SetText("Šifruji…")
})
if m == "" {
return
}
go func(txt string) {
res, err := svc.Encrypt(txt, ct.Cert)
if err != nil {
fyne.Do(func() { status.SetText("Chyba: " + err.Error()) })
return
}
fyne.Do(func() { lastCipher = res; updateQR(res); status.SetText("Hotovo") })
}(m)
}
var tmr *time.Timer
msgEntry.OnChanged = func(string) {
if tmr != nil {
tmr.Stop()
}
tmr = time.AfterFunc(300*time.Millisecond, doEncrypt)
}
copyPayloadBtn := widget.NewButton("Kopírovat payload", func() {
if lastCipher != "" {
copyClip(lastCipher, parts)
status.SetText("Zkopírováno")
}
})
copyQRBtn := widget.NewButton("Kopírovat QR", func() { copyImageToClipboard(qrImg.Image, parts) })
saveQRBtn := widget.NewButton("Uložit QR", func() {
if qrImg.Image == nil {
return
}
img := qrImg.Image
fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
if err != nil || wc == nil {
return
}
defer wc.Close()
_ = png.Encode(wc, img)
status.SetText("QR uložen")
}, win)
fd.SetFileName("message_qr.png")
fd.Show()
})
content := container.NewVBox(widget.NewLabel("Zpráva"), msgEntry, widget.NewSeparator(), container.NewHBox(widget.NewLabel("QR kód"), layout.NewSpacer(), copyPayloadBtn, copyQRBtn, saveQRBtn), qrImg, status)
title := ct.Name
if title == "" {
title = "(bez názvu)"
}
if cn := extractCN(ct.Cert); cn != "" && !strings.Contains(title, cn) {
title = fmt.Sprintf("%s (%s)", title, cn)
}
dlg := dialog.NewCustom(fmt.Sprintf("Poslat zprávu: %s", title), "Zavřít", content, win)
dlg.Resize(fyne.NewSize(640, 520))
dlg.Show()
}
// Contacts tab with QR-only popup
func buildContactsTab(parts *uiParts, svc ServiceFacade) fyne.CanvasObject {
var all, filtered []Contact
load := func() { items, _ := svc.ListContacts(); all = items; filtered = items }
apply := func(q string) {
if q == "" {
filtered = all
return
}
low := strings.ToLower(q)
tmp := make([]Contact, 0, len(all))
for _, c := range all {
if strings.Contains(strings.ToLower(c.Name), low) || strings.Contains(strings.ToLower(c.Cert), low) {
tmp = append(tmp, c)
}
}
filtered = tmp
}
makeDefault := func() string {
base := "Nový kontakt"
exists := false
maxN := 1
for _, c := range all {
if c.Name == base {
exists = true
}
if strings.HasPrefix(c.Name, base+" ") {
var n int
if _, err := fmt.Sscanf(c.Name, "Nový kontakt %d", &n); err == nil && n >= maxN {
maxN = n + 1
}
}
}
if !exists {
return base
}
return fmt.Sprintf("%s %d", base, maxN)
}
search := widget.NewEntry()
var list *widget.List
openPopup := func(existing *Contact) {
nameEntry := widget.NewEntry()
if existing != nil {
nameEntry.SetText(existing.Name)
} else {
nameEntry.SetText(makeDefault())
}
var certValue string
if existing != nil {
certValue = existing.Cert
}
manualEntry := widget.NewMultiLineEntry()
manualEntry.SetMinRowsVisible(4)
manualEntry.Wrapping = fyne.TextWrapWord
manualEntry.SetPlaceHolder("Sem lze vložit text PEM pokud QR selže…")
if certValue != "" {
manualEntry.SetText(certValue)
}
qrImg := canvas.NewImageFromImage(nil)
qrImg.FillMode = canvas.ImageFillContain
qrImg.SetMinSize(fyne.NewSize(300, 300))
updateQR := func() {
if strings.TrimSpace(certValue) == "" {
qrImg.Image = nil
qrImg.Refresh()
return
}
if b, err := GenerateQRPNG(certValue, 512); err == nil {
if im, err2 := LoadPNG(b); err2 == nil {
qrImg.Image = im
qrImg.Refresh()
}
}
}
updateQR()
// now that updateQR exists, bind manualEntry changes
manualEntry.OnChanged = func(s string) {
certValue = s
updateQR()
}
pasteText := widget.NewToolbarAction(theme.ContentPasteIcon(), func() {
clip := fyne.CurrentApp().Clipboard().Content()
if strings.TrimSpace(clip) == "" {
parts.showToast("Schránka prázdná")
return
}
certValue = clip
manualEntry.SetText(certValue)
updateQR()
parts.showToast("Vloženo")
})
pasteQR := widget.NewToolbarAction(theme.ComputerIcon(), func() {
img, err := readImageClipboard()
if err != nil {
parts.showToast("Chyba čtení schránky: " + err.Error())
return
}
if img == nil {
parts.showToast("Žádný QR obrázek")
return
}
txt, decErr := DecodeQR(img)
if decErr != nil {
bounds := img.Bounds()
inv := image.NewRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, a := img.At(x, y).RGBA()
inv.Set(x, y, color.RGBA{uint8(255 - r/257), uint8(255 - g/257), uint8(255 - b/257), uint8(a / 257)})
}
}
if txt2, err2 := DecodeQR(inv); err2 == nil {
certValue = txt2
manualEntry.SetText(certValue)
updateQR()
parts.showToast("Načteno z invert QR")
return
}
debugDir := "qr_debug"
_ = os.MkdirAll(debugDir, 0o755)
fp := filepath.Join(debugDir, fmt.Sprintf("qr_clip_%d.png", time.Now().UnixNano()))
if f, e := os.Create(fp); e == nil {
_ = png.Encode(f, img)
_ = f.Close()
parts.showToast("QR nenalezen: " + decErr.Error() + " (" + fp + ")")
} else {
parts.showToast("QR nenalezen: " + decErr.Error())
}
return
}
certValue = txt
manualEntry.SetText(certValue)
updateQR()
parts.showToast("Načteno z QR")
})
openImg := widget.NewToolbarAction(theme.FolderOpenIcon(), func() {
win := fyne.CurrentApp().Driver().AllWindows()[0]
fd := dialog.NewFileOpen(func(rc fyne.URIReadCloser, err error) {
if err != nil || rc == nil {
return
}
defer rc.Close()
data, _ := io.ReadAll(rc)
img, _, e2 := image.Decode(bytes.NewReader(data))
if e2 != nil {
parts.showToast("Neplatný obrázek: " + e2.Error())
return
}
txt, e3 := DecodeQR(img)
if e3 != nil {
parts.showToast("QR nenalezeno: " + e3.Error())
return
}
certValue = txt
manualEntry.SetText(certValue)
updateQR()
parts.showToast("Načteno z QR")
}, win)
fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
fd.Show()
})
clearAct := widget.NewToolbarAction(theme.ContentClearIcon(), func() { certValue = ""; manualEntry.SetText(""); updateQR() })
copyAct := widget.NewToolbarAction(theme.ContentCopyIcon(), func() {
if certValue != "" {
copyClip(certValue, parts)
}
})
saveAct := widget.NewToolbarAction(theme.DocumentSaveIcon(), func() {
if qrImg.Image == nil {
return
}
win := fyne.CurrentApp().Driver().AllWindows()[0]
img := qrImg.Image
fd := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
if err != nil || wc == nil {
return
}
defer wc.Close()
_ = png.Encode(wc, img)
parts.showToast("QR uložen")
}, win)
fd.SetFileName("contact_qr.png")
fd.Show()
})
toolbar := widget.NewToolbar(pasteText, pasteQR, openImg, widget.NewToolbarSeparator(), copyAct, saveAct, clearAct)
win := fyne.CurrentApp().Driver().AllWindows()[0]
var popup dialog.Dialog
save := func(useEncrypt bool) {
name := strings.TrimSpace(nameEntry.Text)
cert := strings.TrimSpace(certValue)
if cert == "" {
parts.showToast("Chybí cert")
return
}
cn := extractCN(cert)
ask := cn != "" && (name == "" || name == "Kontakt" || name == "Nový kontakt" || name != cn)
proceed := func(final string) {
if final == "" || final == "Kontakt" || final == "Nový kontakt" {
final = makeDefault()
}
if existing == nil {
_ = svc.SaveContact(Contact{Name: final, Cert: cert})
} else {
c := *existing
c.Name = final
c.Cert = cert
_ = svc.SaveContact(c)
}
load()
apply(strings.TrimSpace(search.Text))
list.Refresh()
parts.showToast("Uloženo")
if useEncrypt {
parts.peer.SetText(cert)
}
if popup != nil {
popup.Hide()
}
}
if ask {
dialog.NewCustomConfirm("Common Name", "Použít", "Ponechat", widget.NewLabel(fmt.Sprintf("Common Name: %s\nPoužít jako název?", cn)), func(ok bool) {
if ok {
proceed(cn)
} else {
proceed(name)
}
}, win).Show()
return
}
proceed(name)
}
delBtn := widget.NewButtonWithIcon("Smazat", theme.DeleteIcon(), func() {
if existing == nil {
popup.Hide()
return
}
dialog.NewConfirm("Smazat", "Opravdu smazat?", func(ok bool) {
if !ok {
return
}
_ = svc.DeleteContact(existing.ID)
load()
apply(strings.TrimSpace(search.Text))
list.Refresh()
popup.Hide()
parts.showToast("Smazáno")
}, win).Show()
})
useBtn := widget.NewButton("Použít ve Šifrování", func() { save(true) })
saveBtn := widget.NewButton("Uložit", func() { save(false) })
row := container.NewHBox(saveBtn, useBtn, layout.NewSpacer(), delBtn)
title := "Nový kontakt"
if existing != nil {
title = "Upravit kontakt"
}
// manual entry area below QR for fallback or direct edit
popup = dialog.NewCustom(title, "Zavřít", container.NewVBox(
widget.NewLabel("Název"), nameEntry,
widget.NewLabel("Certifikát / Public key (QR)"), toolbar, qrImg,
widget.NewLabel("Text PEM"), manualEntry,
widget.NewSeparator(), row), win)
popup.Resize(fyne.NewSize(640, 520))
popup.Show()
}
list = widget.NewList(func() int { return len(filtered) }, func() fyne.CanvasObject {
lbl := widget.NewLabel("")
msg := widget.NewButton("Zpráva", nil)
edit := widget.NewButton("Upravit", nil)
msg.Importance = widget.LowImportance
edit.Importance = widget.LowImportance
return container.NewBorder(nil, nil, lbl, container.NewHBox(msg, edit))
}, func(i widget.ListItemID, o fyne.CanvasObject) {
if int(i) < 0 || int(i) >= len(filtered) {
return
}
c := filtered[i]
row := o.(*fyne.Container)
lbl := row.Objects[0].(*widget.Label)
btnBox := row.Objects[1].(*fyne.Container)
msgBtn := btnBox.Objects[0].(*widget.Button)
editBtn := btnBox.Objects[1].(*widget.Button)
name := c.Name
if name == "" {
name = "(bez názvu)"
}
if cn := extractCN(c.Cert); cn != "" {
lbl.SetText(fmt.Sprintf("%s (%s)", name, cn))
} else {
lbl.SetText(name)
}
msgBtn.OnTapped = func() { openEncryptPopup(parts, svc, c) }
editBtn.OnTapped = func() {
var ptr *Contact
for i := range all {
if all[i].ID == c.ID {
ptr = &all[i]
break
}
}
openPopup(ptr)
}
})
search.SetPlaceHolder("Hledat…")
search.OnChanged = func(s string) { apply(s); list.Refresh() }
addBtn := widget.NewButtonWithIcon("Přidat", theme.ContentAddIcon(), func() { openPopup(nil) })
header := container.NewHBox(widget.NewLabelWithStyle("Kontakty", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), layout.NewSpacer(), addBtn)
load()
list.Refresh()
return container.NewBorder(header, nil, nil, nil, container.NewBorder(search, nil, nil, nil, list))
}
func buildTabbedUI(parts *uiParts, svc ServiceFacade, vaultPath string) fyne.CanvasObject {
tabs := container.NewAppTabs(
container.NewTabItem("Identita", buildIdentityTab(parts, svc, vaultPath)),
container.NewTabItem("Kontakty", buildContactsTab(parts, svc)),
container.NewTabItem("Dešifrování", buildDecryptTab(parts, svc)),
)
fyne.CurrentApp().Settings().SetTheme(simpleTheme{})
return container.NewBorder(nil, parts.toastLabel, nil, nil, tabs)
}

252
vault_service.go Executable file
View File

@ -0,0 +1,252 @@
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
encrypt "fckeuspy-go/lib"
"fmt"
mrand "math/rand"
)
// VaultService implementuje ServiceFacade nad SecureJSONStore.
// Využívá existující hybridní šifrování z encrypt.Service (re-use kódu voláním helperů?)
// Pro jednoduchost zopakujeme potřebnou část: Encrypt/Decrypt využijí dočasný encrypt.Service
// inicializovaný z načtených klíčů.
type VaultService struct {
store encrypt.SecureJSONStore
priv *rsa.PrivateKey
pubPEM string
certPEM string
}
func NewVaultService(store encrypt.SecureJSONStore) (*VaultService, error) {
vs := &VaultService{store: store}
if err := vs.loadIdentity(); err != nil {
return nil, err
}
return vs, nil
}
func (v *VaultService) loadIdentity() error {
privPem := v.store.IdentityPrivatePEM()
if privPem == "" {
return errors.New("missing private key in store")
}
block, _ := pem.Decode([]byte(privPem))
if block == nil || block.Type != "RSA PRIVATE KEY" {
return errors.New("invalid private key PEM")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return err
}
v.priv = key
v.pubPEM = v.store.IdentityPublicPEM()
v.certPEM = v.store.IdentityCertPEM()
if v.pubPEM == "" { // derive public if missing
pubASN1, _ := x509.MarshalPKIXPublicKey(&key.PublicKey)
v.pubPEM = string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1}))
}
return nil
}
func (v *VaultService) PublicPEM() string { return v.pubPEM }
func (v *VaultService) PublicCert() string { return v.certPEM }
// Encrypt provede hybridní šifrování stejně jako původní Service.Encrypt.
func (v *VaultService) Encrypt(message, peerPEMorCert string) (string, error) {
// Vytvoříme ad-hoc Service s privátním klíčem jen pro volání existující logiky? Nejjednodušší je zkopírovat potřebnou část kódu.
return encryptHybrid(v.priv, message, peerPEMorCert)
}
// Decrypt provede rozšifrování.
func (v *VaultService) Decrypt(payload string) (string, error) { return decryptHybrid(v.priv, payload) }
// --- Lokální helpery (duplikace z encrypt.Service, zredukované) ---
type hybridEnvelope struct {
EK string `json:"ek"`
N string `json:"n"`
CT string `json:"ct"`
}
// --- Contacts management ---
type Contact struct {
ID string `json:"id"`
Name string `json:"name"`
Cert string `json:"cert"` // full PEM cert (or public key PEM)
}
const contactsKey = "contacts"
func (v *VaultService) ListContacts() ([]Contact, error) {
var list []Contact
if !v.store.Has(contactsKey) {
return []Contact{}, nil
}
if err := v.store.Get(contactsKey, &list); err != nil {
return nil, err
}
return list, nil
}
func (v *VaultService) SaveContact(c Contact) error {
list, _ := v.ListContacts()
// upsert by ID; if empty ID, assign a new unique random ID to avoid overwriting
if c.ID == "" {
c.ID = generateUniqueContactID(list)
}
replaced := false
for i := range list {
if list[i].ID == c.ID {
list[i] = c
replaced = true
break
}
}
if !replaced {
list = append(list, c)
}
if err := v.store.Put(contactsKey, list); err != nil {
return err
}
return v.store.Flush()
}
func (v *VaultService) DeleteContact(id string) error {
list, _ := v.ListContacts()
out := list[:0]
for _, c := range list {
if c.ID != id {
out = append(out, c)
}
}
if err := v.store.Put(contactsKey, out); err != nil {
return err
}
return v.store.Flush()
}
func deriveContactID(c Contact) string {
// simple stable ID: prefer CN from cert; fallback to sha256 of cert
if cn := extractCN(c.Cert); cn != "" {
return cn
}
sum := sha256.Sum256([]byte(c.Cert))
return fmt.Sprintf("%x", sum[:8])
}
// generateUniqueContactID creates a short random hex ID that does not collide with existing list.
func generateUniqueContactID(existing []Contact) string {
exists := make(map[string]struct{}, len(existing))
for _, c := range existing {
exists[c.ID] = struct{}{}
}
for {
b := make([]byte, 6)
if _, err := rand.Read(b); err != nil {
// fallback to time-based seed rand
n := mrand.Int63()
return fmt.Sprintf("%x", n)
}
id := fmt.Sprintf("%x", b)
if _, ok := exists[id]; !ok {
return id
}
}
}
func extractCN(pemText string) string {
// Support multiple concatenated PEM blocks (public key + cert, or chain)
rest := []byte(pemText)
for len(rest) > 0 {
var block *pem.Block
block, rest = pem.Decode(rest)
if block == nil {
break
}
if block.Type == "CERTIFICATE" {
if cert, err := x509.ParseCertificate(block.Bytes); err == nil {
return cert.Subject.CommonName
}
}
// continue to next block until certificate found
}
return ""
}
func encryptHybrid(priv *rsa.PrivateKey, message, peerPEMorCert string) (string, error) {
pubKey, err := encrypt.ParsePeerPublicKey(peerPEMorCert)
if err != nil {
return "", err
}
aesKey := make([]byte, 32)
if _, err := rand.Read(aesKey); err != nil {
return "", err
}
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return "", err
}
ct := gcm.Seal(nil, nonce, []byte(message), nil)
ek, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, aesKey, []byte{})
if err != nil {
return "", err
}
env := hybridEnvelope{EK: base64.StdEncoding.EncodeToString(ek), N: base64.StdEncoding.EncodeToString(nonce), CT: base64.StdEncoding.EncodeToString(ct)}
out, _ := json.MarshalIndent(env, "", " ")
return string(out), nil
}
func decryptHybrid(priv *rsa.PrivateKey, payload string) (string, error) {
var env hybridEnvelope
if err := json.Unmarshal([]byte(payload), &env); err != nil {
return "", fmt.Errorf("invalid JSON: %w", err)
}
ek, err := base64.StdEncoding.DecodeString(env.EK)
if err != nil {
return "", fmt.Errorf("ek b64: %w", err)
}
nonce, err := base64.StdEncoding.DecodeString(env.N)
if err != nil {
return "", fmt.Errorf("n b64: %w", err)
}
ct, err := base64.StdEncoding.DecodeString(env.CT)
if err != nil {
return "", fmt.Errorf("ct b64: %w", err)
}
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, ek, []byte{})
if err != nil {
return "", fmt.Errorf("RSA-OAEP decrypt: %w", err)
}
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
pt, err := gcm.Open(nil, nonce, ct, nil)
if err != nil {
return "", fmt.Errorf("GCM open: %w", err)
}
return string(pt), nil
}

49
vendor/fyne.io/fyne/v2/.gitignore generated vendored Normal file
View File

@ -0,0 +1,49 @@
### Binaries and project specific files
cmd/fyne/fyne
cmd/fyne_demo/fyne_demo
cmd/fyne_settings/fyne_settings
cmd/hello/hello
fyne-cross
*.exe
*.apk
*.app
*.tar.xz
*.zip
### Tests
**/testdata/failed
### Go
# Output of the coverage tool
*.out
### macOS
# General
.DS_Store
# Thumbnails
._*
### JetBrains
.idea
### VSCode
.vscode
### Vim
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# Session
Session.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~

1
vendor/fyne.io/fyne/v2/.godocdown.import generated vendored Normal file
View File

@ -0,0 +1 @@
fyne.io/fyne/v2

16
vendor/fyne.io/fyne/v2/AUTHORS generated vendored Normal file
View File

@ -0,0 +1,16 @@
Andy Williams <andy@andy.xyz>
Steve OConnor <steveoc64@gmail.com>
Luca Corbo <lu.corbo@gmail.com>
Paul Hovey <paul@paulhovey.org>
Charles Corbett <nafredy@gmail.com>
Tilo Prütz <tilo@pruetz.net>
Stephen Houston <smhouston88@gmail.com>
Storm Hess <stormhess@gloryskulls.com>
Stuart Scott <stuart.murray.scott@gmail.com>
Jacob Alzén <jacalz@tutanota.com>
Charles A. Daniels <charles@cdaniels.net>
Pablo Fuentes <f.pablo1@hotmail.com>
Changkun Ou <hi@changkun.de>
Cedric Bail
Drew Weymouth
Simon Dassow

1609
vendor/fyne.io/fyne/v2/CHANGELOG.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

76
vendor/fyne.io/fyne/v2/CODE_OF_CONDUCT.md generated vendored Normal file
View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at info@fyne.io. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

63
vendor/fyne.io/fyne/v2/CONTRIBUTING.md generated vendored Normal file
View File

@ -0,0 +1,63 @@
Thanks very much for your interest in contributing to Fyne!
The community is what makes this project successful and we are glad to welcome you on board.
There are various ways to contribute, perhaps the following helps you know how to get started.
## Reporting a bug
If you've found something wrong we want to know about it, please help us understand the problem so we can resolve it.
1. Check to see if this already is recorded, if so add some more information [issue list](https://github.com/fyne-io/fyne/issues)
2. If not then create a new issue using the [bug report template](https://github.com/fyne-io/fyne/issues/new?assignees=&labels=&template=bug_report.md&title=)
3. Stay involved in the conversation on the issue as it is triaged and progressed.
## Fixing an issue
Great! You found an issue and figured you can fix it for us.
If you can follow these steps then your code should get accepted fast.
1. Read through the "Contributing Code" section further down this page.
2. Write a unit test to show it is broken.
3. Create the fix and you should see the test passes.
4. Run the tests and make sure everything still works as expected using `go test ./...`.
5. [Open a PR](https://github.com/fyne-io/fyne/compare) and work through the review checklist.
## Adding a feature
It's always good news to hear that people want to contribute functionality.
But first of all check that it fits within our [Vision](https://github.com/fyne-io/fyne/wiki/Vision) and if we are already considering it on our [Roadmap](https://github.com/fyne-io/fyne/wiki/Roadmap).
If you're not sure then you should join our #fyne-contributors channel on the [Gophers Slack server](https://gophers.slack.com/app_redirect?channel=fyne-contributors).
Once you are ready to code then the following steps should give you a smooth process:
1. Read through the [Contributing Code](#contributing-code) section further down this page.
2. Think about how you would structure your code and how it can be tested.
3. Write some code and enjoy the ease of writing Go code for even a complex project :).
4. Run the tests and make sure everything still works as expected using `go test ./...`.
5. [Open a PR](https://github.com/fyne-io/fyne/compare) and work through the review checklist.
# Contributing Code
We aim to maintain a very high standard of code, through design, test and implementation.
To manage this we have various checks and processes in place that everyone should follow, including:
* We use the Go standard format (with tabs not spaces) - you can run `gofmt` before committing
* Imports should be ordered according to the GoImports spec - you can use the `goimports` tool instead of `gofmt`.
* Everything should have a unit test attached (as much as possible, to keep our coverage up)
For detailed Code style, check [Contributing](https://github.com/fyne-io/fyne/wiki/Contributing#code-style) in our wiki please.
# Decision Process
The following points apply to our decision making process:
* Any decisions or votes will be opened on the #fyne-contributors channel and follows lazy consensus.
* Any contributors not responding in 4 days will be deemed in agreement.
* Any PR that has not been responded to within 7 days can be automatically approved.
* No functionality will be added unless at least 2 developers agree it belongs.
Bear in mind that this is a cross platform project so any new features would normally
be required to work on multiple desktop and mobile platforms.

28
vendor/fyne.io/fyne/v2/LICENSE generated vendored Normal file
View File

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (C) 2018 Fyne.io developers (see AUTHORS)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Fyne.io nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

184
vendor/fyne.io/fyne/v2/README.md generated vendored Normal file
View File

@ -0,0 +1,184 @@
<p align="center">
<a href="https://pkg.go.dev/fyne.io/fyne/v2?tab=doc" title="Go API Reference" rel="nofollow"><img src="https://img.shields.io/badge/go-documentation-blue.svg?style=flat" alt="Go API Reference"></a>
<a href="https://img.shields.io/github/v/release/fyne-io/fyne?include_prereleases" title="Latest Release" rel="nofollow"><img src="https://img.shields.io/github/v/release/fyne-io/fyne?include_prereleases" alt="Latest Release"></a>
<a href='https://gophers.slack.com/messages/fyne'><img src='https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=blue' alt='Join us on Slack' /></a>
<br />
<a href="https://goreportcard.com/report/fyne.io/fyne/v2"><img src="https://goreportcard.com/badge/fyne.io/fyne/v2" alt="Code Status" /></a>
<a href="https://github.com/fyne-io/fyne/actions"><img src="https://github.com/fyne-io/fyne/workflows/Platform%20Tests/badge.svg" alt="Build Status" /></a>
<a href='https://coveralls.io/github/fyne-io/fyne?branch=develop'><img src='https://coveralls.io/repos/github/fyne-io/fyne/badge.svg?branch=develop' alt='Coverage Status' /></a>
</p>
# About
[Fyne](https://fyne.io) is an easy-to-use UI toolkit and app API written in Go.
It is designed to build applications that run on desktop and mobile devices with a
single codebase.
# Prerequisites
To develop apps using Fyne you will need Go version 1.17 or later, a C compiler and your system's development tools.
If you're not sure if that's all installed or you don't know how then check out our
[Getting Started](https://fyne.io/develop/) document.
Using the standard go tools you can install Fyne's core library using:
go get fyne.io/fyne/v2@latest
After importing a new module, run the following command before compiling the code for the first time. Avoid running it before writing code that uses the module to prevent accidental removal of dependencies:
go mod tidy
# Widget demo
To run a showcase of the features of Fyne execute the following:
go install fyne.io/fyne/v2/cmd/fyne_demo@latest
fyne_demo
And you should see something like this (after you click a few buttons):
<p align="center" markdown="1" style="max-width: 100%">
<img src="img/widgets-dark.png" width="752" alt="Fyne Demo Dark Theme" style="max-width: 100%" />
</p>
Or if you are using the light theme:
<p align="center" markdown="1" style="max-width: 100%">
<img src="img/widgets-light.png" width="752" alt="Fyne Demo Light Theme" style="max-width: 100%" />
</p>
And even running on a mobile device:
<p align="center" markdown="1" style="max-width: 100%">
<img src="img/widgets-mobile-light.png" width="348" alt="Fyne Demo Mobile Light Theme" style="max-width: 100%" />
</p>
# Getting Started
Fyne is designed to be really easy to code with.
If you have followed the prerequisite steps above then all you need is a
Go IDE (or a text editor).
Open a new file and you're ready to write your first app!
```go
package main
import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func main() {
a := app.New()
w := a.NewWindow("Hello")
hello := widget.NewLabel("Hello Fyne!")
w.SetContent(container.NewVBox(
hello,
widget.NewButton("Hi!", func() {
hello.SetText("Welcome :)")
}),
))
w.ShowAndRun()
}
```
And you can run that simply as:
go run main.go
> [!NOTE]
> The first compilation of Fyne on Windows _can_ take up to 10 minutes, depending on your hardware. Subsequent builds will be fast.
It should look like this:
<div align="center">
<table cellpadding="0" cellspacing="0" style="margin: auto; border-collapse: collapse;">
<tr style="border: none;"><td style="border: none;">
<img src="img/hello-light.png" width="207" alt="Fyne Hello Dark Theme" />
</td><td style="border: none;">
<img src="img/hello-dark.png" width="207" alt="Fyne Hello Dark Theme" />
</td></tr>
</table>
</div>
## Run in mobile simulation
There is a helpful mobile simulation mode that gives a hint of how your app would work on a mobile device:
go run -tags mobile main.go
Another option is to use `fyne` command, see [Packaging for mobile](#packaging-for-mobile).
# Installing
Using `go install` will copy the executable into your go `bin` dir.
To install the application with icons etc into your operating system's standard
application location you can use the fyne utility and the "install" subcommand.
go install fyne.io/fyne/v2/cmd/fyne@latest
fyne install
# Packaging for mobile
To run on a mobile device it is necessary to package up the application.
To do this we can use the fyne utility "package" subcommand.
You will need to add appropriate parameters as prompted, but the basic command is shown below.
Once packaged you can install using the platform development tools or the fyne "install" subcommand.
fyne package -os android -appID my.domain.appname
fyne install -os android
The built Android application can run either in a real device or an Android emulator.
However, building for iOS is slightly different.
If the "-os" argument is "ios", it is build only for a real iOS device.
Specify "-os" to "iossimulator" allows the application be able to run in an iOS simulator:
fyne package -os ios -appID my.domain.appname
fyne package -os iossimulator -appID my.domain.appname
# Preparing a release
Using the fyne utility "release" subcommand you can package up your app for release
to app stores and market places. Make sure you have the standard build tools installed
and have followed the platform documentation for setting up accounts and signing.
Then you can execute something like the following, notice the `-os ios` parameter allows
building an iOS app from macOS computer. Other combinations work as well :)
$ fyne release -os ios -certificate "Apple Distribution" -profile "My App Distribution" -appID "com.example.myapp"
The above command will create a '.ipa' file that can then be uploaded to the iOS App Store.
# Documentation
More documentation is available at the [Fyne developer website](https://developer.fyne.io/) or on [pkg.go.dev](https://pkg.go.dev/fyne.io/fyne/v2?tab=doc).
# Examples
You can find many example applications in the [examples repository](https://github.com/fyne-io/examples/).
Alternatively a list of applications using fyne can be found at [our website](https://apps.fyne.io/).
# Shipping the Fyne Toolkit
All Fyne apps will work without pre-installed libraries, this is one reason the apps are so portable.
However, if looking to support Fyne in a bigger way on your operating system then you can install some utilities that help to make a more complete experience.
## Additional apps
It is recommended that you install the following additional apps:
| app | go install | description |
| ------------- | ----------------------------------- | ---------------------------------------------------------------------- |
| fyne_settings | `fyne.io/fyne/v2/cmd/fyne_settings` | A GUI for managing your global Fyne settings like theme and scaling |
| apps | `github.com/fyne-io/apps` | A graphical installer for the Fyne apps listed at https://apps.fyne.io |
These are optional applications but can help to create a more complete desktop experience.
## FyneDesk (Linux / BSD)
To go all the way with Fyne on your desktop / laptop computer you could install [FyneDesk](https://github.com/fyshos/fynedesk) as well :)
![FyneDesk screenshopt in dark mode](https://fyshos.com/img/desktop.png)

15
vendor/fyne.io/fyne/v2/SECURITY.md generated vendored Normal file
View File

@ -0,0 +1,15 @@
# Security Policy
## Supported Versions
Minor releases will receive security updates and fixes until the next minor or major release.
| Version | Supported |
| ------- | ------------------ |
| 2.6.x | :white_check_mark: |
| < 2.6.0 | :x: |
## Reporting a Vulnerability
Report security vulnerabilities using the [advisories](https://github.com/fyne-io/fyne/security/advisories) page on GitHub.
The team of core developers will evaluate and address the issue as appropriate.

84
vendor/fyne.io/fyne/v2/animation.go generated vendored Normal file
View File

@ -0,0 +1,84 @@
package fyne
import "time"
// AnimationCurve represents an animation algorithm for calculating the progress through a timeline.
// Custom animations can be provided by implementing the "func(float32) float32" definition.
// The input parameter will start at 0.0 when an animation starts and travel up to 1.0 at which point it will end.
// A linear animation would return the same output value as is passed in.
type AnimationCurve func(float32) float32
// AnimationRepeatForever is an AnimationCount value that indicates it should not stop looping.
//
// Since: 2.0
const AnimationRepeatForever = -1
var (
// AnimationEaseInOut is the default easing, it starts slowly, accelerates to the middle and slows to the end.
//
// Since: 2.0
AnimationEaseInOut = animationEaseInOut
// AnimationEaseIn starts slowly and accelerates to the end.
//
// Since: 2.0
AnimationEaseIn = animationEaseIn
// AnimationEaseOut starts at speed and slows to the end.
//
// Since: 2.0
AnimationEaseOut = animationEaseOut
// AnimationLinear is a linear mapping for animations that progress uniformly through their duration.
//
// Since: 2.0
AnimationLinear = animationLinear
)
// Animation represents an animated element within a Fyne canvas.
// These animations may control individual objects or entire scenes.
//
// Since: 2.0
type Animation struct {
AutoReverse bool
Curve AnimationCurve
Duration time.Duration
RepeatCount int
Tick func(float32)
}
// NewAnimation creates a very basic animation where the callback function will be called for every
// rendered frame between [time.Now] and the specified duration. The callback values start at 0.0 and
// will be 1.0 when the animation completes.
//
// Since: 2.0
func NewAnimation(d time.Duration, fn func(float32)) *Animation {
return &Animation{Duration: d, Tick: fn}
}
// Start registers the animation with the application run-loop and starts its execution.
func (a *Animation) Start() {
CurrentApp().Driver().StartAnimation(a)
}
// Stop will end this animation and remove it from the run-loop.
func (a *Animation) Stop() {
CurrentApp().Driver().StopAnimation(a)
}
func animationEaseIn(val float32) float32 {
return val * val
}
func animationEaseInOut(val float32) float32 {
if val <= 0.5 {
return val * val * 2
}
return -1 + (4-val*2)*val
}
func animationEaseOut(val float32) float32 {
return val * (2 - val)
}
func animationLinear(val float32) float32 {
return val
}

145
vendor/fyne.io/fyne/v2/app.go generated vendored Normal file
View File

@ -0,0 +1,145 @@
package fyne
import (
"net/url"
"sync/atomic"
)
// An App is the definition of a graphical application.
// Apps can have multiple windows, by default they will exit when all windows
// have been closed. This can be modified using SetMaster or SetCloseIntercept.
// To start an application you need to call Run somewhere in your main function.
// Alternatively use the [fyne.io/fyne/v2.Window.ShowAndRun] function for your main window.
type App interface {
// Create a new window for the application.
// The first window to open is considered the "master" and when closed
// the application will exit.
NewWindow(title string) Window
// Open a URL in the default browser application.
OpenURL(url *url.URL) error
// Icon returns the application icon, this is used in various ways
// depending on operating system.
// This is also the default icon for new windows.
Icon() Resource
// SetIcon sets the icon resource used for this application instance.
SetIcon(Resource)
// Run the application - this starts the event loop and waits until [App.Quit]
// is called or the last window closes.
// This should be called near the end of a main() function as it will block.
Run()
// Calling Quit on the application will cause the application to exit
// cleanly, closing all open windows.
// This function does no thing on a mobile device as the application lifecycle is
// managed by the operating system.
Quit()
// Driver returns the driver that is rendering this application.
// Typically not needed for day to day work, mostly internal functionality.
Driver() Driver
// UniqueID returns the application unique identifier, if set.
// This must be set for use of the [App.Preferences]. see [NewWithID].
UniqueID() string
// SendNotification sends a system notification that will be displayed in the operating system's notification area.
SendNotification(*Notification)
// Settings return the globally set settings, determining theme and so on.
Settings() Settings
// Preferences returns the application preferences, used for storing configuration and state
Preferences() Preferences
// Storage returns a storage handler specific to this application.
Storage() Storage
// Lifecycle returns a type that allows apps to hook in to lifecycle events.
//
// Since: 2.1
Lifecycle() Lifecycle
// Metadata returns the application metadata that was set at compile time.
// The items of metadata are available after "fyne package" or when running "go run"
// Building with "go build" may cause this to be unavailable.
//
// Since: 2.2
Metadata() AppMetadata
// CloudProvider returns the current app cloud provider,
// if one has been registered by the developer or chosen by the user.
//
// Since: 2.3
CloudProvider() CloudProvider // get the (if any) configured provider
// SetCloudProvider allows developers to specify how this application should integrate with cloud services.
// See [fyne.io/cloud] package for implementation details.
//
// Since: 2.3
SetCloudProvider(CloudProvider) // configure cloud for this app
// Clipboard returns the system clipboard.
//
// Since: 2.6
Clipboard() Clipboard
}
var app atomic.Pointer[App]
// SetCurrentApp is an internal function to set the app instance currently running.
func SetCurrentApp(current App) {
app.Store(&current)
}
// CurrentApp returns the current application, for which there is only 1 per process.
func CurrentApp() App {
val := app.Load()
if val == nil {
LogError("Attempt to access current Fyne app when none is started", nil)
return nil
}
return *val
}
// AppMetadata captures the build metadata for an application.
//
// Since: 2.2
type AppMetadata struct {
// ID is the unique ID of this application, used by many distribution platforms.
ID string
// Name is the human friendly name of this app.
Name string
// Version represents the version of this application, normally following semantic versioning.
Version string
// Build is the build number of this app, some times appended to the version number.
Build int
// Icon contains, if present, a resource of the icon that was bundled at build time.
Icon Resource
// Release if true this binary was build in release mode
// Since: 2.3
Release bool
// Custom contain the custom metadata defined either in FyneApp.toml or on the compile command line
// Since: 2.3
Custom map[string]string
// Migrations allows an app to opt into features before they are standard
// Since: 2.6
Migrations map[string]bool
}
// Lifecycle represents the various phases that an app can transition through.
//
// Since: 2.1
type Lifecycle interface {
// SetOnEnteredForeground hooks into the app becoming foreground and gaining focus.
SetOnEnteredForeground(func())
// SetOnExitedForeground hooks into the app losing input focus and going into the background.
SetOnExitedForeground(func())
// SetOnStarted hooks into an event that says the app is now running.
SetOnStarted(func())
// SetOnStopped hooks into an event that says the app is no longer running.
SetOnStopped(func())
}

189
vendor/fyne.io/fyne/v2/app/app.go generated vendored Normal file
View File

@ -0,0 +1,189 @@
// Package app provides app implementations for working with Fyne graphical interfaces.
// The fastest way to get started is to call app.New() which will normally load a new desktop application.
// If the "ci" tag is passed to go (go run -tags ci myapp.go) it will run an in-memory application.
package app // import "fyne.io/fyne/v2/app"
import (
"strconv"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal"
"fyne.io/fyne/v2/internal/app"
intRepo "fyne.io/fyne/v2/internal/repository"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/storage/repository"
)
// Declare conformity with App interface
var _ fyne.App = (*fyneApp)(nil)
type fyneApp struct {
driver fyne.Driver
clipboard fyne.Clipboard
icon fyne.Resource
uniqueID string
cloud fyne.CloudProvider
lifecycle app.Lifecycle
settings *settings
storage fyne.Storage
prefs fyne.Preferences
}
func (a *fyneApp) CloudProvider() fyne.CloudProvider {
return a.cloud
}
func (a *fyneApp) Icon() fyne.Resource {
if a.icon != nil {
return a.icon
}
if a.Metadata().Icon == nil || len(a.Metadata().Icon.Content()) == 0 {
return nil
}
return a.Metadata().Icon
}
func (a *fyneApp) SetIcon(icon fyne.Resource) {
a.icon = icon
}
func (a *fyneApp) UniqueID() string {
if a.uniqueID != "" {
return a.uniqueID
}
if a.Metadata().ID != "" {
return a.Metadata().ID
}
fyne.LogError("Preferences API requires a unique ID, use app.NewWithID() or the FyneApp.toml ID field", nil)
a.uniqueID = "missing-id-" + strconv.FormatInt(time.Now().Unix(), 10) // This is a fake unique - it just has to not be reused...
return a.uniqueID
}
func (a *fyneApp) NewWindow(title string) fyne.Window {
return a.driver.CreateWindow(title)
}
func (a *fyneApp) Run() {
go a.lifecycle.RunEventQueue(a.driver.DoFromGoroutine)
if !a.driver.Device().IsMobile() {
a.settings.watchSettings()
}
a.driver.Run()
}
func (a *fyneApp) Quit() {
for _, window := range a.driver.AllWindows() {
window.Close()
}
a.driver.Quit()
a.settings.stopWatching()
}
func (a *fyneApp) Driver() fyne.Driver {
return a.driver
}
// Settings returns the application settings currently configured.
func (a *fyneApp) Settings() fyne.Settings {
return a.settings
}
func (a *fyneApp) Storage() fyne.Storage {
return a.storage
}
func (a *fyneApp) Preferences() fyne.Preferences {
if a.UniqueID() == "" {
fyne.LogError("Preferences API requires a unique ID, use app.NewWithID() or the FyneApp.toml ID field", nil)
}
return a.prefs
}
func (a *fyneApp) Lifecycle() fyne.Lifecycle {
return &a.lifecycle
}
func (a *fyneApp) newDefaultPreferences() *preferences {
p := newPreferences(a)
if a.uniqueID != "" {
p.load()
}
return p
}
func (a *fyneApp) Clipboard() fyne.Clipboard {
return a.clipboard
}
// New returns a new application instance with the default driver and no unique ID (unless specified in FyneApp.toml)
func New() fyne.App {
if meta.ID == "" {
checkLocalMetadata() // if no ID passed, check if it was in toml
if meta.ID == "" {
internal.LogHint("Applications should be created with a unique ID using app.NewWithID()")
}
}
return NewWithID(meta.ID)
}
func makeStoreDocs(id string, s *store) *internal.Docs {
if id == "" {
return &internal.Docs{} // an empty impl to avoid crashes
}
if root := s.a.storageRoot(); root != "" {
uri, err := storage.ParseURI(root)
if err != nil {
uri = storage.NewFileURI(root)
}
exists, err := storage.Exists(uri)
if !exists || err != nil {
err = storage.CreateListable(uri)
if err != nil {
fyne.LogError("Failed to create app storage space", err)
}
}
root, _ := s.docRootURI()
return &internal.Docs{RootDocURI: root}
} else {
return &internal.Docs{} // an empty impl to avoid crashes
}
}
func newAppWithDriver(d fyne.Driver, clipboard fyne.Clipboard, id string) fyne.App {
newApp := &fyneApp{uniqueID: id, clipboard: clipboard, driver: d}
fyne.SetCurrentApp(newApp)
newApp.prefs = newApp.newDefaultPreferences()
newApp.lifecycle.InitEventQueue()
newApp.lifecycle.SetOnStoppedHookExecuted(func() {
if prefs, ok := newApp.prefs.(*preferences); ok {
prefs.forceImmediateSave()
}
})
newApp.registerRepositories() // for web this may provide docs / settings
newApp.settings = loadSettings()
store := &store{a: newApp}
store.Docs = makeStoreDocs(id, store)
newApp.storage = store
httpHandler := intRepo.NewHTTPRepository()
repository.Register("http", httpHandler)
repository.Register("https", httpHandler)
return newApp
}
// marker interface to pass system tray to supporting drivers
type systrayDriver interface {
SetSystemTrayMenu(*fyne.Menu)
SetSystemTrayIcon(resource fyne.Resource)
}

59
vendor/fyne.io/fyne/v2/app/app_darwin.go generated vendored Normal file
View File

@ -0,0 +1,59 @@
//go:build !ci && !wasm && !test_web_driver && !mobile
package app
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#include <stdbool.h>
#include <stdlib.h>
bool isBundled();
void sendNotification(char *title, char *content);
*/
import "C"
import (
"fmt"
"os/exec"
"strings"
"unsafe"
"fyne.io/fyne/v2"
)
func (a *fyneApp) SendNotification(n *fyne.Notification) {
if C.isBundled() {
titleStr := C.CString(n.Title)
defer C.free(unsafe.Pointer(titleStr))
contentStr := C.CString(n.Content)
defer C.free(unsafe.Pointer(contentStr))
C.sendNotification(titleStr, contentStr)
return
}
fallbackNotification(n.Title, n.Content)
}
func escapeNotificationString(in string) string {
noSlash := strings.ReplaceAll(in, "\\", "\\\\")
return strings.ReplaceAll(noSlash, "\"", "\\\"")
}
//export fallbackSend
func fallbackSend(cTitle, cContent *C.char) {
title := C.GoString(cTitle)
content := C.GoString(cContent)
fallbackNotification(title, content)
}
func fallbackNotification(title, content string) {
template := `display notification "%s" with title "%s"`
script := fmt.Sprintf(template, escapeNotificationString(content), escapeNotificationString(title))
err := exec.Command("osascript", "-e", script).Start()
if err != nil {
fyne.LogError("Failed to launch darwin notify script", err)
}
}

60
vendor/fyne.io/fyne/v2/app/app_darwin.m generated vendored Normal file
View File

@ -0,0 +1,60 @@
//go:build !ci && !wasm && !test_web_driver && !mobile
#import <Foundation/Foundation.h>
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 || TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
#import <UserNotifications/UserNotifications.h>
#endif
static int notifyNum = 0;
extern void fallbackSend(char *cTitle, char *cBody);
bool isBundled() {
return [[NSBundle mainBundle] bundleIdentifier] != nil;
}
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 || TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
void doSendNotification(UNUserNotificationCenter *center, NSString *title, NSString *body) {
UNMutableNotificationContent *content = [UNMutableNotificationContent new];
[content autorelease];
content.title = title;
content.body = body;
notifyNum++;
NSString *identifier = [NSString stringWithFormat:@"fyne-notify-%d", notifyNum];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier
content:content trigger:nil];
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error != nil) {
NSLog(@"Could not send notification: %@", error);
}
}];
}
void sendNotification(char *cTitle, char *cBody) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
NSString *title = [NSString stringWithUTF8String:cTitle];
NSString *body = [NSString stringWithUTF8String:cBody];
UNAuthorizationOptions options = UNAuthorizationOptionAlert;
[center requestAuthorizationWithOptions:options
completionHandler:^(BOOL granted, NSError *_Nullable error) {
if (!granted) {
if (error != NULL) {
NSLog(@"Error asking for permission to send notifications %@", error);
// this happens if our app was not signed, so do it the old way
fallbackSend((char *)[title UTF8String], (char *)[body UTF8String]);
} else {
NSLog(@"Unable to get permission to send notifications");
}
} else {
doSendNotification(center, title, body);
}
}];
}
#else
void sendNotification(char *cTitle, char *cBody) {
fallbackSend(cTitle, cBody);
}
#endif

54
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go generated vendored Normal file
View File

@ -0,0 +1,54 @@
//go:build !ci && !ios && !wasm && !test_web_driver && !mobile
package app
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#include <AppKit/AppKit.h>
bool isBundled();
void watchTheme();
*/
import "C"
import (
"net/url"
"os"
"os/exec"
"fyne.io/fyne/v2"
)
func (a *fyneApp) OpenURL(url *url.URL) error {
cmd := exec.Command("open", url.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
return cmd.Run()
}
// SetSystemTrayIcon sets a custom image for the system tray icon.
// You should have previously called `SetSystemTrayMenu` to initialise the menu icon.
func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) {
a.Driver().(systrayDriver).SetSystemTrayIcon(icon)
}
// SetSystemTrayMenu creates a system tray item and attaches the specified menu.
// By default this will use the application icon.
func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) {
if desk, ok := a.Driver().(systrayDriver); ok {
desk.SetSystemTrayMenu(menu)
}
}
//export themeChanged
func themeChanged() {
fyne.CurrentApp().Settings().(*settings).setupTheme()
}
func watchTheme(_ *settings) {
C.watchTheme()
}
func (a *fyneApp) registerRepositories() {
// no-op
}

12
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.m generated vendored Normal file
View File

@ -0,0 +1,12 @@
//go:build !ci && !ios && !wasm && !test_web_driver && !mobile
extern void themeChanged();
#import <Foundation/Foundation.h>
void watchTheme() {
[[NSDistributedNotificationCenter defaultCenter] addObserverForName:@"AppleInterfaceThemeChangedNotification" object:nil queue:nil
usingBlock:^(NSNotification *note) {
themeChanged(); // calls back into Go
}];
}

14
vendor/fyne.io/fyne/v2/app/app_gl.go generated vendored Normal file
View File

@ -0,0 +1,14 @@
//go:build !ci && !android && !ios && !mobile
package app
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/driver/glfw"
)
// NewWithID returns a new app instance using the appropriate runtime driver.
// The ID string should be globally unique to this app.
func NewWithID(id string) fyne.App {
return newAppWithDriver(glfw.NewGLDriver(), glfw.NewClipboard(), id)
}

26
vendor/fyne.io/fyne/v2/app/app_mobile.go generated vendored Normal file
View File

@ -0,0 +1,26 @@
//go:build !ci && (android || ios || mobile)
package app
import (
"fyne.io/fyne/v2"
internalapp "fyne.io/fyne/v2/internal/app"
"fyne.io/fyne/v2/internal/driver/mobile"
)
// NewWithID returns a new app instance using the appropriate runtime driver.
// The ID string should be globally unique to this app.
func NewWithID(id string) fyne.App {
d := mobile.NewGoMobileDriver()
a := newAppWithDriver(d, mobile.NewClipboard(), id)
d.(mobile.ConfiguredDriver).SetOnConfigurationChanged(func(c *mobile.Configuration) {
internalapp.SystemTheme = c.SystemTheme
a.Settings().(*settings).setupTheme()
})
return a
}
func (a *fyneApp) registerRepositories() {
// no-op
}

130
vendor/fyne.io/fyne/v2/app/app_mobile_and.c generated vendored Normal file
View File

@ -0,0 +1,130 @@
//go:build !ci && android
#include <android/log.h>
#include <jni.h>
#include <stdbool.h>
#include <stdlib.h>
#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "Fyne", __VA_ARGS__)
static jclass find_class(JNIEnv *env, const char *class_name) {
jclass clazz = (*env)->FindClass(env, class_name);
if (clazz == NULL) {
(*env)->ExceptionClear(env);
LOG_FATAL("cannot find %s", class_name);
return NULL;
}
return clazz;
}
static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
jmethodID m = (*env)->GetMethodID(env, clazz, name, sig);
if (m == 0) {
(*env)->ExceptionClear(env);
LOG_FATAL("cannot find method %s %s", name, sig);
return 0;
}
return m;
}
static jmethodID find_static_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
jmethodID m = (*env)->GetStaticMethodID(env, clazz, name, sig);
if (m == 0) {
(*env)->ExceptionClear(env);
LOG_FATAL("cannot find method %s %s", name, sig);
return 0;
}
return m;
}
jobject getSystemService(uintptr_t jni_env, uintptr_t ctx, char *service) {
JNIEnv *env = (JNIEnv*)jni_env;
jstring serviceStr = (*env)->NewStringUTF(env, service);
jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx);
jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");
return (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, serviceStr);
}
int nextId = 1;
bool isOreoOrLater(JNIEnv *env) {
jclass versionClass = find_class(env, "android/os/Build$VERSION" );
jfieldID sdkIntFieldID = (*env)->GetStaticFieldID(env, versionClass, "SDK_INT", "I" );
int sdkVersion = (*env)->GetStaticIntField(env, versionClass, sdkIntFieldID );
return sdkVersion >= 26; // O = Oreo, will not be defined for older builds
}
jobject parseURL(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) {
JNIEnv *env = (JNIEnv*)jni_env;
jstring uriStr = (*env)->NewStringUTF(env, uriCstr);
jclass uriClass = find_class(env, "android/net/Uri");
jmethodID parse = find_static_method(env, uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;");
return (jobject)(*env)->CallStaticObjectMethod(env, uriClass, parse, uriStr);
}
void openURL(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *url) {
JNIEnv *env = (JNIEnv*)jni_env;
jobject uri = parseURL(jni_env, ctx, url);
jclass intentClass = find_class(env, "android/content/Intent");
jfieldID viewFieldID = (*env)->GetStaticFieldID(env, intentClass, "ACTION_VIEW", "Ljava/lang/String;" );
jstring view = (*env)->GetStaticObjectField(env, intentClass, viewFieldID);
jmethodID constructor = find_method(env, intentClass, "<init>", "(Ljava/lang/String;Landroid/net/Uri;)V");
jobject intent = (*env)->NewObject(env, intentClass, constructor, view, uri);
jclass contextClass = find_class(env, "android/content/Context");
jmethodID start = find_method(env, contextClass, "startActivity", "(Landroid/content/Intent;)V");
(*env)->CallVoidMethod(env, (jobject)ctx, start, intent);
}
void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *title, char *body) {
JNIEnv *env = (JNIEnv*)jni_env;
jstring titleStr = (*env)->NewStringUTF(env, title);
jstring bodyStr = (*env)->NewStringUTF(env, body);
jclass cls = find_class(env, "android/app/Notification$Builder");
jmethodID constructor = find_method(env, cls, "<init>", "(Landroid/content/Context;)V");
jobject builder = (*env)->NewObject(env, cls, constructor, ctx);
jclass mgrCls = find_class(env, "android/app/NotificationManager");
jobject mgr = getSystemService((uintptr_t)env, ctx, "notification");
if (isOreoOrLater(env)) {
jstring channelId = (*env)->NewStringUTF(env, "fyne-notif");
jstring name = (*env)->NewStringUTF(env, "Fyne Notification");
int importance = 4; // IMPORTANCE_HIGH
jclass chanCls = find_class(env, "android/app/NotificationChannel");
jmethodID constructor = find_method(env, chanCls, "<init>", "(Ljava/lang/String;Ljava/lang/CharSequence;I)V");
jobject channel = (*env)->NewObject(env, chanCls, constructor, channelId, name, importance);
jmethodID createChannel = find_method(env, mgrCls, "createNotificationChannel", "(Landroid/app/NotificationChannel;)V");
(*env)->CallVoidMethod(env, mgr, createChannel, channel);
jmethodID setChannelId = find_method(env, cls, "setChannelId", "(Ljava/lang/String;)Landroid/app/Notification$Builder;");
(*env)->CallObjectMethod(env, builder, setChannelId, channelId);
}
jmethodID setContentTitle = find_method(env, cls, "setContentTitle", "(Ljava/lang/CharSequence;)Landroid/app/Notification$Builder;");
(*env)->CallObjectMethod(env, builder, setContentTitle, titleStr);
jmethodID setContentText = find_method(env, cls, "setContentText", "(Ljava/lang/CharSequence;)Landroid/app/Notification$Builder;");
(*env)->CallObjectMethod(env, builder, setContentText, bodyStr);
int iconID = 17629184; // constant of "unknown app icon"
jmethodID setSmallIcon = find_method(env, cls, "setSmallIcon", "(I)Landroid/app/Notification$Builder;");
(*env)->CallObjectMethod(env, builder, setSmallIcon, iconID);
jmethodID build = find_method(env, cls, "build", "()Landroid/app/Notification;");
jobject notif = (*env)->CallObjectMethod(env, builder, build);
jmethodID notify = find_method(env, mgrCls, "notify", "(ILandroid/app/Notification;)V");
(*env)->CallVoidMethod(env, mgr, notify, nextId, notif);
nextId++;
}

43
vendor/fyne.io/fyne/v2/app/app_mobile_and.go generated vendored Normal file
View File

@ -0,0 +1,43 @@
//go:build !ci && android
package app
/*
#cgo LDFLAGS: -landroid -llog
#include <stdlib.h>
void openURL(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *url);
void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *title, char *content);
*/
import "C"
import (
"net/url"
"unsafe"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/driver/mobile/app"
)
func (a *fyneApp) OpenURL(url *url.URL) error {
urlStr := C.CString(url.String())
defer C.free(unsafe.Pointer(urlStr))
app.RunOnJVM(func(vm, env, ctx uintptr) error {
C.openURL(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), urlStr)
return nil
})
return nil
}
func (a *fyneApp) SendNotification(n *fyne.Notification) {
titleStr := C.CString(n.Title)
defer C.free(unsafe.Pointer(titleStr))
contentStr := C.CString(n.Content)
defer C.free(unsafe.Pointer(contentStr))
app.RunOnJVM(func(vm, env, ctx uintptr) error {
C.sendNotification(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), titleStr, contentStr)
return nil
})
}

26
vendor/fyne.io/fyne/v2/app/app_mobile_ios.go generated vendored Normal file
View File

@ -0,0 +1,26 @@
//go:build !ci && ios && !mobile
package app
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework UIKit -framework UserNotifications
#include <stdlib.h>
void openURL(char *urlStr);
void sendNotification(char *title, char *content);
*/
import "C"
import (
"net/url"
"unsafe"
)
func (a *fyneApp) OpenURL(url *url.URL) error {
urlStr := C.CString(url.String())
C.openURL(urlStr)
C.free(unsafe.Pointer(urlStr))
return nil
}

10
vendor/fyne.io/fyne/v2/app/app_mobile_ios.m generated vendored Normal file
View File

@ -0,0 +1,10 @@
//go:build !ci && ios
#import <UIKit/UIKit.h>
void openURL(char *urlStr) {
UIApplication *app = [UIApplication sharedApplication];
NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:urlStr]];
[app openURL:url options:@{} completionHandler:nil];
}

22
vendor/fyne.io/fyne/v2/app/app_mobile_xdg.go generated vendored Normal file
View File

@ -0,0 +1,22 @@
//go:build !ci && mobile && !android && !ios
package app
import (
"errors"
"net/url"
"fyne.io/fyne/v2"
)
func (a *fyneApp) OpenURL(_ *url.URL) error {
return errors.New("mobile simulator does not support open URLs yet")
}
func (a *fyneApp) SendNotification(_ *fyne.Notification) {
fyne.LogError("Notifications are not supported in the mobile simulator yet", nil)
}
func watchTheme(_ *settings) {
// not implemented yet
}

8
vendor/fyne.io/fyne/v2/app/app_notlegacy_darwin.go generated vendored Normal file
View File

@ -0,0 +1,8 @@
//go:build !ci && !legacy && !wasm && !test_web_driver
package app
/*
#cgo LDFLAGS: -framework Foundation -framework UserNotifications
*/
import "C"

18
vendor/fyne.io/fyne/v2/app/app_openurl_wasm.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
//go:build !ci && wasm
package app
import (
"fmt"
"net/url"
"syscall/js"
)
func (a *fyneApp) OpenURL(url *url.URL) error {
window := js.Global().Call("open", url.String(), "_blank", "")
if window.Equal(js.Null()) {
return fmt.Errorf("Unable to open a new window/tab for URL: %v.", url)
}
window.Call("focus")
return nil
}

12
vendor/fyne.io/fyne/v2/app/app_openurl_web.go generated vendored Normal file
View File

@ -0,0 +1,12 @@
//go:build !ci && !wasm && test_web_driver
package app
import (
"errors"
"net/url"
)
func (a *fyneApp) OpenURL(url *url.URL) error {
return errors.New("OpenURL is not supported with the test web driver.")
}

26
vendor/fyne.io/fyne/v2/app/app_other.go generated vendored Normal file
View File

@ -0,0 +1,26 @@
//go:build ci || (!ios && !android && !linux && !darwin && !windows && !freebsd && !openbsd && !netbsd && !wasm && !test_web_driver)
package app
import (
"errors"
"net/url"
"fyne.io/fyne/v2"
)
func (a *fyneApp) OpenURL(_ *url.URL) error {
return errors.New("Unable to open url for unknown operating system")
}
func (a *fyneApp) SendNotification(_ *fyne.Notification) {
fyne.LogError("Refusing to show notification for unknown operating system", nil)
}
func watchTheme(_ *settings) {
// no-op
}
func (a *fyneApp) registerRepositories() {
// no-op
}

15
vendor/fyne.io/fyne/v2/app/app_software.go generated vendored Normal file
View File

@ -0,0 +1,15 @@
//go:build ci
package app
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/painter/software"
"fyne.io/fyne/v2/test"
)
// NewWithID returns a new app instance using the test (headless) driver.
// The ID string should be globally unique to this app.
func NewWithID(id string) fyne.App {
return newAppWithDriver(test.NewDriverWithPainter(software.NewPainter()), test.NewClipboard(), id)
}

77
vendor/fyne.io/fyne/v2/app/app_wasm.go generated vendored Normal file
View File

@ -0,0 +1,77 @@
//go:build !ci && (!android || !ios || !mobile) && (wasm || test_web_driver)
package app
import (
"encoding/base64"
"fmt"
"net/http"
"syscall/js"
"fyne.io/fyne/v2"
intRepo "fyne.io/fyne/v2/internal/repository"
"fyne.io/fyne/v2/storage/repository"
)
func (a *fyneApp) SendNotification(n *fyne.Notification) {
window := js.Global().Get("window")
if window.IsUndefined() {
fyne.LogError("Current browser does not support notifications.", nil)
return
}
notification := window.Get("Notification")
if window.IsUndefined() {
fyne.LogError("Current browser does not support notifications.", nil)
return
}
// check permission
permission := notification.Get("permission")
showNotification := func() {
icon := a.icon.Content()
base64Str := base64.StdEncoding.EncodeToString(icon)
mimeType := http.DetectContentType(icon)
base64Img := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Str)
notification.New(n.Title, map[string]any{
"body": n.Content,
"icon": base64Img,
})
fyne.LogError("done show...", nil)
}
if permission.Type() != js.TypeString || permission.String() != "granted" {
// need to request for permission
notification.Call("requestPermission", js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) > 0 && args[0].Type() == js.TypeString && args[0].String() == "granted" {
showNotification()
} else {
fyne.LogError("User rejected the request for notifications.", nil)
}
return nil
}))
} else {
showNotification()
}
}
var themeChanged = js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) > 0 && args[0].Type() == js.TypeObject {
fyne.CurrentApp().Settings().(*settings).setupTheme()
}
return nil
})
func watchTheme(_ *settings) {
js.Global().Call("matchMedia", "(prefers-color-scheme: dark)").Call("addEventListener", "change", themeChanged)
}
func stopWatchingTheme() {
js.Global().Call("matchMedia", "(prefers-color-scheme: dark)").Call("removeEventListener", "change", themeChanged)
}
func (a *fyneApp) registerRepositories() {
repo, err := intRepo.NewIndexDBRepository()
if err != nil {
fyne.LogError("failed to create repository: %v", err)
return
}
repository.Register("idbfile", repo)
}

100
vendor/fyne.io/fyne/v2/app/app_windows.go generated vendored Normal file
View File

@ -0,0 +1,100 @@
//go:build !ci && !android && !ios && !wasm && !test_web_driver
package app
import (
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"fyne.io/fyne/v2"
internalapp "fyne.io/fyne/v2/internal/app"
)
const notificationTemplate = `$title = "%s"
$content = "%s"
$iconPath = "file:///%s"
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastImageAndText02)
$toastXml = [xml] $template.GetXml()
$toastXml.GetElementsByTagName("text")[0].AppendChild($toastXml.CreateTextNode($title)) > $null
$toastXml.GetElementsByTagName("text")[1].AppendChild($toastXml.CreateTextNode($content)) > $null
$toastXml.GetElementsByTagName("image")[0].SetAttribute("src", $iconPath) > $null
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($toastXml.OuterXml)
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("%s").Show($toast);`
func (a *fyneApp) OpenURL(url *url.URL) error {
cmd := exec.Command("rundll32", "url.dll,FileProtocolHandler", url.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
return cmd.Run()
}
var scriptNum = 0
func (a *fyneApp) SendNotification(n *fyne.Notification) {
title := escapeNotificationString(n.Title)
content := escapeNotificationString(n.Content)
iconFilePath := a.cachedIconPath()
appID := a.UniqueID()
if appID == "" || strings.Index(appID, "missing-id") == 0 {
appID = a.Metadata().Name
}
script := fmt.Sprintf(notificationTemplate, title, content, iconFilePath, appID)
go runScript("notify", script)
}
// SetSystemTrayMenu creates a system tray item and attaches the specified menu.
// By default this will use the application icon.
func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) {
a.Driver().(systrayDriver).SetSystemTrayMenu(menu)
}
// SetSystemTrayIcon sets a custom image for the system tray icon.
// You should have previously called `SetSystemTrayMenu` to initialise the menu icon.
func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) {
a.Driver().(systrayDriver).SetSystemTrayIcon(icon)
}
func escapeNotificationString(in string) string {
noSlash := strings.ReplaceAll(in, "`", "``")
return strings.ReplaceAll(noSlash, "\"", "`\"")
}
func runScript(name, script string) {
scriptNum++
appID := fyne.CurrentApp().UniqueID()
fileName := fmt.Sprintf("fyne-%s-%s-%d.ps1", appID, name, scriptNum)
tmpFilePath := filepath.Join(os.TempDir(), fileName)
err := os.WriteFile(tmpFilePath, []byte(script), 0600)
if err != nil {
fyne.LogError("Could not write script to show notification", err)
return
}
defer os.Remove(tmpFilePath)
launch := "(Get-Content -Encoding UTF8 -Path " + tmpFilePath + " -Raw) | Invoke-Expression"
cmd := exec.Command("PowerShell", "-ExecutionPolicy", "Bypass", launch)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
err = cmd.Run()
if err != nil {
fyne.LogError("Failed to launch windows notify script", err)
}
}
func watchTheme(s *settings) {
go internalapp.WatchTheme(func() {
fyne.Do(s.setupTheme)
})
}
func (a *fyneApp) registerRepositories() {
// no-op
}

139
vendor/fyne.io/fyne/v2/app/app_xdg.go generated vendored Normal file
View File

@ -0,0 +1,139 @@
//go:build !ci && !wasm && !test_web_driver && !android && !ios && !mobile && (linux || openbsd || freebsd || netbsd)
package app
import (
"net/url"
"os"
"os/exec"
"sync/atomic"
"github.com/godbus/dbus/v5"
"github.com/rymdport/portal/notification"
"github.com/rymdport/portal/openuri"
portalSettings "github.com/rymdport/portal/settings"
"github.com/rymdport/portal/settings/appearance"
"fyne.io/fyne/v2"
internalapp "fyne.io/fyne/v2/internal/app"
"fyne.io/fyne/v2/internal/build"
"fyne.io/fyne/v2/theme"
)
const systemTheme = fyne.ThemeVariant(99)
func (a *fyneApp) OpenURL(url *url.URL) error {
if build.IsFlatpak {
err := openuri.OpenURI("", url.String(), nil)
if err != nil {
fyne.LogError("Opening url in portal failed", err)
}
return err
}
cmd := exec.Command("xdg-open", url.String())
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
return cmd.Start()
}
// fetch color variant from dbus portal desktop settings.
func findFreedesktopColorScheme() fyne.ThemeVariant {
colorScheme, err := appearance.GetColorScheme()
if err != nil {
return systemTheme
}
return colorSchemeToThemeVariant(colorScheme)
}
func colorSchemeToThemeVariant(colorScheme appearance.ColorScheme) fyne.ThemeVariant {
switch colorScheme {
case appearance.Light:
return theme.VariantLight
case appearance.Dark:
return theme.VariantDark
}
// Default to light theme to support Gnome's default see https://github.com/fyne-io/fyne/pull/3561
return theme.VariantLight
}
func (a *fyneApp) SendNotification(n *fyne.Notification) {
if build.IsFlatpak {
err := a.sendNotificationThroughPortal(n)
if err != nil {
fyne.LogError("Sending notification using portal failed", err)
}
return
}
conn, err := dbus.SessionBus() // shared connection, don't close
if err != nil {
fyne.LogError("Unable to connect to session D-Bus", err)
return
}
appIcon := a.cachedIconPath()
timeout := int32(0) // we don't support this yet
obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
call := obj.Call("org.freedesktop.Notifications.Notify", 0, a.uniqueID, uint32(0),
appIcon, n.Title, n.Content, []string{}, map[string]dbus.Variant{}, timeout)
if call.Err != nil {
fyne.LogError("Failed to send message to bus", call.Err)
}
}
// Sending with same ID replaces the old notification.
var notificationID atomic.Uint64
// See https://flatpak.github.io/xdg-desktop-portal/docs/#gdbus-org.freedesktop.portal.Notification.
func (a *fyneApp) sendNotificationThroughPortal(n *fyne.Notification) error {
return notification.Add(
uint(notificationID.Add(1)),
notification.Content{
Title: n.Title,
Body: n.Content,
Icon: a.uniqueID,
},
)
}
// SetSystemTrayMenu creates a system tray item and attaches the specified menu.
// By default this will use the application icon.
func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) {
if desk, ok := a.Driver().(systrayDriver); ok { // don't use this on mobile tag
desk.SetSystemTrayMenu(menu)
}
}
// SetSystemTrayIcon sets a custom image for the system tray icon.
// You should have previously called `SetSystemTrayMenu` to initialise the menu icon.
func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) {
if desk, ok := a.Driver().(systrayDriver); ok { // don't use this on mobile tag
desk.SetSystemTrayIcon(icon)
}
}
func watchTheme(s *settings) {
go func() {
// Theme lookup hangs on some desktops. Update theme variant cache from within goroutine.
themeVariant := findFreedesktopColorScheme()
if themeVariant != systemTheme {
internalapp.CurrentVariant.Store(uint64(themeVariant))
fyne.Do(func() { s.applyVariant(themeVariant) })
}
portalSettings.OnSignalSettingChanged(func(changed portalSettings.Changed) {
if changed.Namespace == appearance.Namespace && changed.Key == "color-scheme" {
themeVariant := colorSchemeToThemeVariant(appearance.ColorScheme(changed.Value.(uint32)))
internalapp.CurrentVariant.Store(uint64(themeVariant))
fyne.Do(func() { s.applyVariant(themeVariant) })
}
})
}()
}
func (a *fyneApp) registerRepositories() {
// no-op
}

47
vendor/fyne.io/fyne/v2/app/cloud.go generated vendored Normal file
View File

@ -0,0 +1,47 @@
package app
import "fyne.io/fyne/v2"
func (a *fyneApp) SetCloudProvider(p fyne.CloudProvider) {
if p == nil {
a.cloud = nil
return
}
a.transitionCloud(p)
}
func (a *fyneApp) transitionCloud(p fyne.CloudProvider) {
if a.cloud != nil {
a.cloud.Cleanup(a)
}
err := p.Setup(a)
if err != nil {
fyne.LogError("Failed to set up cloud provider "+p.ProviderName(), err)
return
}
a.cloud = p
listeners := a.prefs.ChangeListeners()
if pp, ok := p.(fyne.CloudProviderPreferences); ok {
a.prefs = pp.CloudPreferences(a)
} else {
a.prefs = a.newDefaultPreferences()
}
if cloud, ok := p.(fyne.CloudProviderStorage); ok {
a.storage = cloud.CloudStorage(a)
} else {
store := &store{a: a}
store.Docs = makeStoreDocs(a.uniqueID, store)
a.storage = store
}
for _, l := range listeners {
a.prefs.AddChangeListener(l)
l() // assume that preferences have changed because we replaced the provider
}
// after transition ensure settings listener is fired
a.settings.apply()
}

59
vendor/fyne.io/fyne/v2/app/icon_cache_file.go generated vendored Normal file
View File

@ -0,0 +1,59 @@
package app
import (
"os"
"path/filepath"
"sync"
"fyne.io/fyne/v2"
)
var once sync.Once
func (a *fyneApp) cachedIconPath() string {
if a.Icon() == nil {
return ""
}
dirPath := filepath.Join(rootCacheDir(), a.UniqueID())
filePath := filepath.Join(dirPath, "icon.png")
once.Do(func() {
err := a.saveIconToCache(dirPath, filePath)
if err != nil {
filePath = ""
}
})
return filePath
}
func rootCacheDir() string {
desktopCache, _ := os.UserCacheDir()
return filepath.Join(desktopCache, "fyne")
}
func (a *fyneApp) saveIconToCache(dirPath, filePath string) error {
err := os.MkdirAll(dirPath, 0700)
if err != nil {
fyne.LogError("Unable to create application cache directory", err)
return err
}
file, err := os.Create(filePath)
if err != nil {
fyne.LogError("Unable to create icon file", err)
return err
}
defer file.Close()
if icon := a.Icon(); icon != nil {
_, err = file.Write(icon.Content())
if err != nil {
fyne.LogError("Unable to write icon contents", err)
return err
}
}
return nil
}

36
vendor/fyne.io/fyne/v2/app/meta.go generated vendored Normal file
View File

@ -0,0 +1,36 @@
package app
import (
"fyne.io/fyne/v2"
)
var meta = fyne.AppMetadata{
ID: "",
Name: "",
Version: "0.0.1",
Build: 1,
Release: false,
Custom: map[string]string{},
Migrations: map[string]bool{},
}
// SetMetadata overrides the packaged application metadata.
// This data can be used in many places like notifications and about screens.
func SetMetadata(m fyne.AppMetadata) {
meta = m
if meta.Custom == nil {
meta.Custom = map[string]string{}
}
if meta.Migrations == nil {
meta.Migrations = map[string]bool{}
}
}
func (a *fyneApp) Metadata() fyne.AppMetadata {
if meta.ID == "" && meta.Name == "" {
checkLocalMetadata()
}
return meta
}

65
vendor/fyne.io/fyne/v2/app/meta_development.go generated vendored Normal file
View File

@ -0,0 +1,65 @@
package app
import (
"os"
"path/filepath"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/build"
"fyne.io/fyne/v2/internal/metadata"
)
func checkLocalMetadata() {
if build.NoMetadata || build.Mode == fyne.BuildRelease {
return
}
dir := getProjectPath()
file := filepath.Join(dir, "FyneApp.toml")
ref, err := os.Open(file)
if err != nil { // no worries, this is just an optional fallback
return
}
defer ref.Close()
data, err := metadata.Load(ref)
if err != nil || data == nil {
fyne.LogError("failed to parse FyneApp.toml", err)
return
}
meta.ID = data.Details.ID
meta.Name = data.Details.Name
meta.Version = data.Details.Version
meta.Build = data.Details.Build
if data.Details.Icon != "" {
res, err := fyne.LoadResourceFromPath(data.Details.Icon)
if err == nil {
meta.Icon = metadata.ScaleIcon(res, 512)
}
}
meta.Release = false
meta.Custom = data.Development
meta.Migrations = data.Migrations
}
func getProjectPath() string {
exe, err := os.Executable()
work, _ := os.Getwd()
if err != nil {
fyne.LogError("failed to lookup build executable", err)
return work
}
temp := os.TempDir()
if strings.Contains(exe, temp) || strings.Contains(exe, "go-build") { // this happens with "go run"
return work
}
// we were called with an executable from "go build"
return filepath.Dir(exe)
}

191
vendor/fyne.io/fyne/v2/app/preferences.go generated vendored Normal file
View File

@ -0,0 +1,191 @@
package app
import (
"encoding/json"
"errors"
"io"
"sync"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal"
)
type preferences struct {
*internal.InMemoryPreferences
prefLock sync.RWMutex
savedRecently bool
changedDuringSaving bool
app *fyneApp
needsSaveBeforeExit bool
}
// Declare conformity with Preferences interface
var _ fyne.Preferences = (*preferences)(nil)
// sentinel error to signal an empty preferences storage backend was loaded
var errEmptyPreferencesStore = errors.New("empty preferences store")
// returned from storageWriter() - may be a file, browser local storage, etc
type writeSyncCloser interface {
io.WriteCloser
Sync() error
}
// forceImmediateSave writes preferences to storage immediately, ignoring the debouncing
// logic in the change listener. Does nothing if preferences are not backed with a persistent store.
func (p *preferences) forceImmediateSave() {
if !p.needsSaveBeforeExit {
return
}
err := p.save()
if err != nil {
fyne.LogError("Failed on force saving preferences", err)
}
}
func (p *preferences) resetSavedRecently() {
go func() {
time.Sleep(time.Millisecond * 100) // writes are not always atomic. 10ms worked, 100 is safer.
// For test reasons we need to use current app not what we were initialised with as they can differ
fyne.DoAndWait(func() {
p.prefLock.Lock()
p.savedRecently = false
changedDuringSaving := p.changedDuringSaving
p.changedDuringSaving = false
p.prefLock.Unlock()
if changedDuringSaving {
p.save()
}
})
}()
}
func (p *preferences) save() error {
storage, err := p.storageWriter()
if err != nil {
return err
}
return p.saveToStorage(storage)
}
func (p *preferences) saveToStorage(writer writeSyncCloser) error {
p.prefLock.Lock()
p.savedRecently = true
p.prefLock.Unlock()
defer p.resetSavedRecently()
defer writer.Close()
encode := json.NewEncoder(writer)
var err error
p.InMemoryPreferences.ReadValues(func(values map[string]any) {
err = encode.Encode(&values)
})
err2 := writer.Sync()
if err == nil {
err = err2
}
return err
}
func (p *preferences) load() {
storage, err := p.storageReader()
if err == nil {
err = p.loadFromStorage(storage)
}
if err != nil && err != errEmptyPreferencesStore {
fyne.LogError("Preferences load error:", err)
}
}
func (p *preferences) loadFromStorage(storage io.ReadCloser) (err error) {
defer func() {
if r := storage.Close(); r != nil && err == nil {
err = r
}
}()
decode := json.NewDecoder(storage)
p.InMemoryPreferences.WriteValues(func(values map[string]any) {
err = decode.Decode(&values)
if err != nil {
return
}
convertLists(values)
})
return err
}
func newPreferences(app *fyneApp) *preferences {
p := &preferences{}
p.app = app
p.InMemoryPreferences = internal.NewInMemoryPreferences()
// don't load or watch if not setup
if app.uniqueID == "" && app.Metadata().ID == "" {
return p
}
p.needsSaveBeforeExit = true
p.AddChangeListener(func() {
if p != app.prefs {
return
}
p.prefLock.Lock()
shouldIgnoreChange := p.savedRecently
if p.savedRecently {
p.changedDuringSaving = true
}
p.prefLock.Unlock()
if shouldIgnoreChange { // callback after loading from storage, or too many updates in a row
return
}
err := p.save()
if err != nil {
fyne.LogError("Failed on saving preferences", err)
}
})
p.watch()
return p
}
func convertLists(values map[string]any) {
for k, v := range values {
if items, ok := v.([]any); ok {
if len(items) == 0 {
continue
}
switch items[0].(type) {
case bool:
bools := make([]bool, len(items))
for i, item := range items {
bools[i] = item.(bool)
}
values[k] = bools
case float64:
floats := make([]float64, len(items))
for i, item := range items {
floats[i] = item.(float64)
}
values[k] = floats
//case int: // json has no int!
case string:
strings := make([]string, len(items))
for i, item := range items {
strings[i] = item.(string)
}
values[k] = strings
}
}
}
}

24
vendor/fyne.io/fyne/v2/app/preferences_android.go generated vendored Normal file
View File

@ -0,0 +1,24 @@
//go:build android
package app
import (
"path/filepath"
"fyne.io/fyne/v2/internal/app"
)
// storagePath returns the location of the settings storage
func (p *preferences) storagePath() string {
// we have no global storage, use app global instead - rootConfigDir looks up in app_mobile_and.go
return filepath.Join(p.app.storageRoot(), "preferences.json")
}
// storageRoot returns the location of the app storage
func (a *fyneApp) storageRoot() string {
return app.RootConfigDir() // we are in a sandbox, so no app ID added to this path
}
func (p *preferences) watch() {
// no-op on mobile
}

25
vendor/fyne.io/fyne/v2/app/preferences_ios.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
//go:build ios
package app
import (
"path/filepath"
"fyne.io/fyne/v2/internal/app"
)
import "C"
// storagePath returns the location of the settings storage
func (p *preferences) storagePath() string {
ret := filepath.Join(p.app.storageRoot(), "preferences.json")
return ret
}
// storageRoot returns the location of the app storage
func (a *fyneApp) storageRoot() string {
return app.RootConfigDir() // we are in a sandbox, so no app ID added to this path
}
func (p *preferences) watch() {
// no-op on mobile
}

23
vendor/fyne.io/fyne/v2/app/preferences_mobile.go generated vendored Normal file
View File

@ -0,0 +1,23 @@
//go:build mobile
package app
import (
"path/filepath"
"fyne.io/fyne/v2/internal/app"
)
// storagePath returns the location of the settings storage
func (p *preferences) storagePath() string {
return filepath.Join(p.app.storageRoot(), "preferences.json")
}
// storageRoot returns the location of the app storage
func (a *fyneApp) storageRoot() string {
return filepath.Join(app.RootConfigDir(), a.UniqueID())
}
func (p *preferences) watch() {
// no-op as we are in mobile simulation mode
}

67
vendor/fyne.io/fyne/v2/app/preferences_nonweb.go generated vendored Normal file
View File

@ -0,0 +1,67 @@
//go:build !wasm
package app
import (
"io"
"os"
"path/filepath"
)
func (p *preferences) storageWriter() (writeSyncCloser, error) {
return p.storageWriterForPath(p.storagePath())
}
func (p *preferences) storageReader() (io.ReadCloser, error) {
return p.storageReaderForPath(p.storagePath())
}
func (p *preferences) storageWriterForPath(path string) (writeSyncCloser, error) {
err := os.MkdirAll(filepath.Dir(path), 0700)
if err != nil { // this is not an exists error according to docs
return nil, err
}
file, err := os.Create(path)
if err != nil {
if !os.IsExist(err) {
return nil, err
}
file, err = os.Open(path) // #nosec
if err != nil {
return nil, err
}
}
return file, nil
}
func (p *preferences) storageReaderForPath(path string) (io.ReadCloser, error) {
file, err := os.Open(path) // #nosec
if err != nil {
if os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return nil, err
}
return nil, errEmptyPreferencesStore
}
return nil, err
}
return file, nil
}
// the following are only used in tests to save preferences to a tmp file
func (p *preferences) saveToFile(path string) error {
file, err := p.storageWriterForPath(path)
if err != nil {
return err
}
return p.saveToStorage(file)
}
func (p *preferences) loadFromFile(path string) error {
file, err := p.storageReaderForPath(path)
if err != nil {
return err
}
return p.loadFromStorage(file)
}

32
vendor/fyne.io/fyne/v2/app/preferences_other.go generated vendored Normal file
View File

@ -0,0 +1,32 @@
//go:build !ios && !android && !mobile && !wasm
package app
import (
"path/filepath"
"fyne.io/fyne/v2/internal/app"
)
// storagePath returns the location of the settings storage
func (p *preferences) storagePath() string {
return filepath.Join(p.app.storageRoot(), "preferences.json")
}
// storageRoot returns the location of the app storage
func (a *fyneApp) storageRoot() string {
return filepath.Join(app.RootConfigDir(), a.UniqueID())
}
func (p *preferences) watch() {
watchFile(p.storagePath(), func() {
p.prefLock.RLock()
shouldIgnoreChange := p.savedRecently
p.prefLock.RUnlock()
if shouldIgnoreChange {
return
}
p.load()
})
}

62
vendor/fyne.io/fyne/v2/app/preferences_wasm.go generated vendored Normal file
View File

@ -0,0 +1,62 @@
//go:build wasm
package app
import (
"bytes"
"io"
"strings"
"syscall/js"
)
const preferencesLocalStorageKey = "fyne-preferences.json"
func (a *fyneApp) storageRoot() string {
return "idbfile:///fyne/"
}
func (p *preferences) storageReader() (io.ReadCloser, error) {
key := js.ValueOf(preferencesLocalStorageKey)
data := js.Global().Get("localStorage").Call("getItem", key)
if data.IsNull() || data.IsUndefined() {
return nil, errEmptyPreferencesStore
}
return readerNopCloser{reader: strings.NewReader(data.String())}, nil
}
func (p *preferences) storageWriter() (writeSyncCloser, error) {
return &localStorageWriter{key: preferencesLocalStorageKey}, nil
}
func (p *preferences) watch() {
// no-op for web driver
}
type readerNopCloser struct {
reader io.Reader
}
func (r readerNopCloser) Read(b []byte) (int, error) {
return r.reader.Read(b)
}
func (r readerNopCloser) Close() error {
return nil
}
type localStorageWriter struct {
bytes.Buffer
key string
}
func (s *localStorageWriter) Sync() error {
text := s.String()
s.Reset()
js.Global().Get("localStorage").Call("setItem", js.ValueOf(s.key), js.ValueOf(text))
return nil
}
func (s *localStorageWriter) Close() error {
return nil
}

176
vendor/fyne.io/fyne/v2/app/settings.go generated vendored Normal file
View File

@ -0,0 +1,176 @@
package app
import (
"bytes"
"os"
"path/filepath"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/app"
"fyne.io/fyne/v2/internal/async"
"fyne.io/fyne/v2/internal/build"
"fyne.io/fyne/v2/theme"
)
// SettingsSchema is used for loading and storing global settings
type SettingsSchema struct {
// these items are used for global settings load
ThemeName string `json:"theme"`
Scale float32 `json:"scale"`
PrimaryColor string `json:"primary_color"`
CloudName string `json:"cloud_name"`
CloudConfig string `json:"cloud_config"`
DisableAnimations bool `json:"no_animations"`
}
// StoragePath returns the location of the settings storage
func (sc *SettingsSchema) StoragePath() string {
return filepath.Join(app.RootConfigDir(), "settings.json")
}
// Declare conformity with Settings interface
var _ fyne.Settings = (*settings)(nil)
type settings struct {
theme fyne.Theme
themeSpecified bool
variant fyne.ThemeVariant
listeners []func(fyne.Settings)
changeListeners async.Map[chan fyne.Settings, bool]
watcher any // normally *fsnotify.Watcher or nil - avoid import in this file
schema SettingsSchema
}
func (s *settings) BuildType() fyne.BuildType {
return build.Mode
}
func (s *settings) PrimaryColor() string {
return s.schema.PrimaryColor
}
// OverrideTheme allows the settings app to temporarily preview different theme details.
// Please make sure that you remember the original settings and call this again to revert the change.
//
// Deprecated: Use container.NewThemeOverride to change the appearance of part of your application.
func (s *settings) OverrideTheme(theme fyne.Theme, name string) {
s.schema.PrimaryColor = name
s.theme = theme
}
func (s *settings) Theme() fyne.Theme {
if s == nil {
fyne.LogError("Attempt to access current Fyne theme when no app is started", nil)
return nil
}
return s.theme
}
func (s *settings) SetTheme(theme fyne.Theme) {
s.themeSpecified = true
s.applyTheme(theme, s.variant)
}
func (s *settings) ShowAnimations() bool {
return !s.schema.DisableAnimations && !build.NoAnimations
}
func (s *settings) ThemeVariant() fyne.ThemeVariant {
return s.variant
}
func (s *settings) applyTheme(theme fyne.Theme, variant fyne.ThemeVariant) {
s.variant = variant
s.theme = theme
s.apply()
}
func (s *settings) applyVariant(variant fyne.ThemeVariant) {
s.variant = variant
s.apply()
}
func (s *settings) Scale() float32 {
if s.schema.Scale < 0.0 {
return 1.0 // catching any really old data still using the `-1` value for "auto" scale
}
return s.schema.Scale
}
func (s *settings) AddChangeListener(listener chan fyne.Settings) {
s.changeListeners.Store(listener, true) // the boolean is just a dummy value here.
}
func (s *settings) AddListener(listener func(fyne.Settings)) {
s.listeners = append(s.listeners, listener)
}
func (s *settings) apply() {
s.changeListeners.Range(func(listener chan fyne.Settings, _ bool) bool {
select {
case listener <- s:
default:
l := listener
go func() { l <- s }()
}
return true
})
for _, l := range s.listeners {
l(s)
}
}
func (s *settings) fileChanged() {
s.load()
s.apply()
}
func (s *settings) loadSystemTheme() fyne.Theme {
path := filepath.Join(app.RootConfigDir(), "theme.json")
data, err := fyne.LoadResourceFromPath(path)
if err != nil {
if !os.IsNotExist(err) {
fyne.LogError("Failed to load user theme file: "+path, err)
}
return theme.DefaultTheme()
}
if data != nil && data.Content() != nil {
th, err := theme.FromJSONReader(bytes.NewReader(data.Content()))
if err == nil {
return th
}
fyne.LogError("Failed to parse user theme file: "+path, err)
}
return theme.DefaultTheme()
}
func (s *settings) setupTheme() {
name := s.schema.ThemeName
if env := os.Getenv("FYNE_THEME"); env != "" {
name = env
}
variant := app.DefaultVariant()
effectiveTheme := s.theme
if !s.themeSpecified {
effectiveTheme = s.loadSystemTheme()
}
switch name {
case "light":
variant = theme.VariantLight
case "dark":
variant = theme.VariantDark
}
s.applyTheme(effectiveTheme, variant)
}
func loadSettings() *settings {
s := &settings{}
s.load()
return s
}

80
vendor/fyne.io/fyne/v2/app/settings_desktop.go generated vendored Normal file
View File

@ -0,0 +1,80 @@
//go:build !android && !ios && !mobile && !wasm && !test_web_driver
package app
import (
"os"
"path/filepath"
"fyne.io/fyne/v2"
"github.com/fsnotify/fsnotify"
)
func watchFileAddTarget(watcher *fsnotify.Watcher, path string) {
dir := filepath.Dir(path)
ensureDirExists(dir)
err := watcher.Add(dir)
if err != nil {
fyne.LogError("Settings watch error:", err)
}
}
func ensureDirExists(dir string) {
if stat, err := os.Stat(dir); err == nil && stat.IsDir() {
return
}
err := os.MkdirAll(dir, 0700)
if err != nil {
fyne.LogError("Unable to create settings storage:", err)
}
}
func watchFile(path string, callback func()) *fsnotify.Watcher {
watcher, err := fsnotify.NewWatcher()
if err != nil {
fyne.LogError("Failed to watch settings file:", err)
return nil
}
go func() {
for event := range watcher.Events {
if event.Op.Has(fsnotify.Remove) { // if it was deleted then watch again
watcher.Remove(path) // fsnotify returns false positives, see https://github.com/fsnotify/fsnotify/issues/268
watchFileAddTarget(watcher, path)
} else {
fyne.Do(callback)
}
}
err = watcher.Close()
if err != nil {
fyne.LogError("Settings un-watch error:", err)
}
}()
watchFileAddTarget(watcher, path)
return watcher
}
func (s *settings) watchSettings() {
if s.themeSpecified {
return // we only watch for theme changes at this time so don't bother
}
s.watcher = watchFile(s.schema.StoragePath(), s.fileChanged)
a := fyne.CurrentApp()
if a != nil && s != nil && a.Settings() == s { // ignore if testing
watchTheme(s)
}
}
func (s *settings) stopWatching() {
if s.watcher == nil {
return
}
s.watcher.(*fsnotify.Watcher).Close() // fsnotify returns false positives, see https://github.com/fsnotify/fsnotify/issues/268
}

34
vendor/fyne.io/fyne/v2/app/settings_file.go generated vendored Normal file
View File

@ -0,0 +1,34 @@
//go:build !wasm && !test_web_driver
package app
import (
"encoding/json"
"io"
"os"
"fyne.io/fyne/v2"
)
func (s *settings) load() {
err := s.loadFromFile(s.schema.StoragePath())
if err != nil && err != io.EOF { // we can get an EOF in windows settings writes
fyne.LogError("Settings load error:", err)
}
s.setupTheme()
}
func (s *settings) loadFromFile(path string) error {
file, err := os.Open(path) // #nosec
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer file.Close()
decode := json.NewDecoder(file)
return decode.Decode(&s.schema)
}

11
vendor/fyne.io/fyne/v2/app/settings_mobile.go generated vendored Normal file
View File

@ -0,0 +1,11 @@
//go:build android || ios || mobile
package app
func (s *settings) watchSettings() {
// no-op on mobile
}
func (s *settings) stopWatching() {
// no-op on mobile
}

25
vendor/fyne.io/fyne/v2/app/settings_wasm.go generated vendored Normal file
View File

@ -0,0 +1,25 @@
//go:build wasm || test_web_driver
package app
// TODO: #2734
func (s *settings) load() {
s.setupTheme()
s.schema.Scale = 1
}
func (s *settings) loadFromFile(path string) error {
return nil
}
func watchFile(path string, callback func()) {
}
func (s *settings) watchSettings() {
watchTheme(s)
}
func (s *settings) stopWatching() {
stopWatchingTheme()
}

31
vendor/fyne.io/fyne/v2/app/storage.go generated vendored Normal file
View File

@ -0,0 +1,31 @@
package app
import (
"os"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal"
"fyne.io/fyne/v2/storage"
)
type store struct {
*internal.Docs
a *fyneApp
}
func (s *store) RootURI() fyne.URI {
if s.a.UniqueID() == "" {
fyne.LogError("Storage API requires a unique ID, use app.NewWithID()", nil)
return storage.NewFileURI(os.TempDir())
}
u, err := storage.ParseURI(s.a.storageRoot())
if err == nil {
return u
}
return storage.NewFileURI(s.a.storageRoot())
}
func (s *store) docRootURI() (fyne.URI, error) {
return storage.Child(s.RootURI(), "Documents")
}

58
vendor/fyne.io/fyne/v2/canvas.go generated vendored Normal file
View File

@ -0,0 +1,58 @@
package fyne
import "image"
// Canvas defines a graphical canvas to which a [CanvasObject] or Container can be added.
// Each canvas has a scale which is automatically applied during the render process.
type Canvas interface {
Content() CanvasObject
SetContent(CanvasObject)
Refresh(CanvasObject)
// Focus makes the provided item focused.
// The item has to be added to the contents of the canvas before calling this.
Focus(Focusable)
// FocusNext focuses the next focusable item.
// If no item is currently focused, the first focusable item is focused.
// If the last focusable item is currently focused, the first focusable item is focused.
//
// Since: 2.0
FocusNext()
// FocusPrevious focuses the previous focusable item.
// If no item is currently focused, the last focusable item is focused.
// If the first focusable item is currently focused, the last focusable item is focused.
//
// Since: 2.0
FocusPrevious()
Unfocus()
Focused() Focusable
// Size returns the current size of this canvas
Size() Size
// Scale returns the current scale (multiplication factor) this canvas uses to render
// The pixel size of a [CanvasObject] can be found by multiplying by this value.
Scale() float32
// Overlays returns the overlay stack.
Overlays() OverlayStack
OnTypedRune() func(rune)
SetOnTypedRune(func(rune))
OnTypedKey() func(*KeyEvent)
SetOnTypedKey(func(*KeyEvent))
AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut))
RemoveShortcut(shortcut Shortcut)
Capture() image.Image
// PixelCoordinateForPosition returns the x and y pixel coordinate for a given position on this canvas.
// This can be used to find absolute pixel positions or pixel offsets relative to an object top left.
PixelCoordinateForPosition(Position) (int, int)
// InteractiveArea returns the position and size of the central interactive area.
// Operating system elements may overlap the portions outside this area and widgets should avoid being outside.
//
// Since: 1.4
InteractiveArea() (Position, Size)
}

86
vendor/fyne.io/fyne/v2/canvas/animation.go generated vendored Normal file
View File

@ -0,0 +1,86 @@
package canvas
import (
"image/color"
"time"
"fyne.io/fyne/v2"
)
const (
// DurationStandard is the time a standard interface animation will run.
//
// Since: 2.0
DurationStandard = time.Millisecond * 300
// DurationShort is the time a subtle or small transition should use.
//
// Since: 2.0
DurationShort = time.Millisecond * 150
)
// NewColorRGBAAnimation sets up a new animation that will transition from the start to stop Color over
// the specified Duration. The colour transition will move linearly through the RGB colour space.
// The content of fn should apply the color values to an object and refresh it.
// You should call Start() on the returned animation to start it.
//
// Since: 2.0
func NewColorRGBAAnimation(start, stop color.Color, d time.Duration, fn func(color.Color)) *fyne.Animation {
r1, g1, b1, a1 := start.RGBA()
r2, g2, b2, a2 := stop.RGBA()
rStart := int(r1 >> 8)
gStart := int(g1 >> 8)
bStart := int(b1 >> 8)
aStart := int(a1 >> 8)
rDelta := float32(int(r2>>8) - rStart)
gDelta := float32(int(g2>>8) - gStart)
bDelta := float32(int(b2>>8) - bStart)
aDelta := float32(int(a2>>8) - aStart)
return &fyne.Animation{
Duration: d,
Tick: func(done float32) {
fn(color.RGBA{R: scaleChannel(rStart, rDelta, done), G: scaleChannel(gStart, gDelta, done),
B: scaleChannel(bStart, bDelta, done), A: scaleChannel(aStart, aDelta, done)})
}}
}
// NewPositionAnimation sets up a new animation that will transition from the start to stop Position over
// the specified Duration. The content of fn should apply the position value to an object for the change
// to be visible. You should call Start() on the returned animation to start it.
//
// Since: 2.0
func NewPositionAnimation(start, stop fyne.Position, d time.Duration, fn func(fyne.Position)) *fyne.Animation {
xDelta := float32(stop.X - start.X)
yDelta := float32(stop.Y - start.Y)
return &fyne.Animation{
Duration: d,
Tick: func(done float32) {
fn(fyne.NewPos(scaleVal(start.X, xDelta, done), scaleVal(start.Y, yDelta, done)))
}}
}
// NewSizeAnimation sets up a new animation that will transition from the start to stop Size over
// the specified Duration. The content of fn should apply the size value to an object for the change
// to be visible. You should call Start() on the returned animation to start it.
//
// Since: 2.0
func NewSizeAnimation(start, stop fyne.Size, d time.Duration, fn func(fyne.Size)) *fyne.Animation {
widthDelta := float32(stop.Width - start.Width)
heightDelta := float32(stop.Height - start.Height)
return &fyne.Animation{
Duration: d,
Tick: func(done float32) {
fn(fyne.NewSize(scaleVal(start.Width, widthDelta, done), scaleVal(start.Height, heightDelta, done)))
}}
}
func scaleChannel(start int, diff, done float32) uint8 {
return uint8(start + int(diff*done))
}
func scaleVal(start float32, delta, done float32) float32 {
return start + delta*done
}

69
vendor/fyne.io/fyne/v2/canvas/base.go generated vendored Normal file
View File

@ -0,0 +1,69 @@
// Package canvas contains all of the primitive CanvasObjects that make up a Fyne GUI.
//
// The types implemented in this package are used as building blocks in order
// to build higher order functionality. These types are designed to be
// non-interactive, by design. If additional functionality is required,
// it's usually a sign that this type should be used as part of a custom
// widget.
package canvas // import "fyne.io/fyne/v2/canvas"
import (
"fyne.io/fyne/v2"
)
type baseObject struct {
size fyne.Size // The current size of the canvas object
position fyne.Position // The current position of the object
Hidden bool // Is this object currently hidden
min fyne.Size // The minimum size this object can be
}
// Hide will set this object to not be visible.
func (o *baseObject) Hide() {
o.Hidden = true
}
// MinSize returns the specified minimum size, if set, or {1, 1} otherwise.
func (o *baseObject) MinSize() fyne.Size {
if o.min.IsZero() {
return fyne.Size{Width: 1, Height: 1}
}
return o.min
}
// Move the object to a new position, relative to its parent.
func (o *baseObject) Move(pos fyne.Position) {
o.position = pos
}
// Position gets the current position of this canvas object, relative to its parent.
func (o *baseObject) Position() fyne.Position {
return o.position
}
// Resize sets a new size for the canvas object.
func (o *baseObject) Resize(size fyne.Size) {
o.size = size
}
// SetMinSize specifies the smallest size this object should be.
func (o *baseObject) SetMinSize(size fyne.Size) {
o.min = size
}
// Show will set this object to be visible.
func (o *baseObject) Show() {
o.Hidden = false
}
// Size returns the current size of this canvas object.
func (o *baseObject) Size() fyne.Size {
return o.size
}
// Visible returns true if this object is visible, false otherwise.
func (o *baseObject) Visible() bool {
return !o.Hidden
}

49
vendor/fyne.io/fyne/v2/canvas/canvas.go generated vendored Normal file
View File

@ -0,0 +1,49 @@
package canvas
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/svg"
)
// Refresh instructs the containing canvas to refresh the specified obj.
func Refresh(obj fyne.CanvasObject) {
app := fyne.CurrentApp()
if app == nil || app.Driver() == nil {
return
}
c := app.Driver().CanvasForObject(obj)
if c != nil {
c.Refresh(obj)
}
}
// RecolorSVG takes a []byte containing SVG content, and returns
// new SVG content, re-colorized to be monochrome with the given color.
// The content can be assigned to a new fyne.StaticResource with an appropriate name
// to be used in a widget.Button, canvas.Image, etc.
//
// If an error occurs, the returned content will be the original un-modified content,
// and a non-nil error is returned.
//
// Since: 2.6
func RecolorSVG(svgContent []byte, color color.Color) ([]byte, error) {
return svg.Colorize(svgContent, color)
}
// repaint instructs the containing canvas to redraw, even if nothing changed.
func repaint(obj fyne.CanvasObject) {
app := fyne.CurrentApp()
if app == nil || app.Driver() == nil {
return
}
c := app.Driver().CanvasForObject(obj)
if c != nil {
if paint, ok := c.(interface{ SetDirty() }); ok {
paint.SetDirty()
}
}
}

95
vendor/fyne.io/fyne/v2/canvas/circle.go generated vendored Normal file
View File

@ -0,0 +1,95 @@
package canvas
import (
"image/color"
"math"
"fyne.io/fyne/v2"
)
// Declare conformity with CanvasObject interface
var _ fyne.CanvasObject = (*Circle)(nil)
// Circle describes a colored circle primitive in a Fyne canvas
type Circle struct {
Position1 fyne.Position // The current top-left position of the Circle
Position2 fyne.Position // The current bottomright position of the Circle
Hidden bool // Is this circle currently hidden
FillColor color.Color // The circle fill color
StrokeColor color.Color // The circle stroke color
StrokeWidth float32 // The stroke width of the circle
}
// NewCircle returns a new Circle instance
func NewCircle(color color.Color) *Circle {
return &Circle{FillColor: color}
}
// Hide will set this circle to not be visible
func (c *Circle) Hide() {
c.Hidden = true
repaint(c)
}
// MinSize for a Circle simply returns Size{1, 1} as there is no
// explicit content
func (c *Circle) MinSize() fyne.Size {
return fyne.NewSize(1, 1)
}
// Move the circle object to a new position, relative to its parent / canvas
func (c *Circle) Move(pos fyne.Position) {
if c.Position1 == pos {
return
}
size := c.Size()
c.Position1 = pos
c.Position2 = c.Position1.Add(size)
repaint(c)
}
// Position gets the current top-left position of this circle object, relative to its parent / canvas
func (c *Circle) Position() fyne.Position {
return c.Position1
}
// Refresh causes this object to be redrawn with its configured state.
func (c *Circle) Refresh() {
Refresh(c)
}
// Resize sets a new bottom-right position for the circle object
// If it has a stroke width this will cause it to Refresh.
func (c *Circle) Resize(size fyne.Size) {
if size == c.Size() {
return
}
c.Position2 = c.Position1.Add(size)
Refresh(c)
}
// Show will set this circle to be visible
func (c *Circle) Show() {
c.Hidden = false
c.Refresh()
}
// Size returns the current size of bounding box for this circle object
func (c *Circle) Size() fyne.Size {
return fyne.NewSize(
float32(math.Abs(float64(c.Position2.X)-float64(c.Position1.X))),
float32(math.Abs(float64(c.Position2.Y)-float64(c.Position1.Y))),
)
}
// Visible returns true if this circle is visible, false otherwise
func (c *Circle) Visible() bool {
return !c.Hidden
}

238
vendor/fyne.io/fyne/v2/canvas/gradient.go generated vendored Normal file
View File

@ -0,0 +1,238 @@
package canvas
import (
"image"
"image/color"
"math"
"fyne.io/fyne/v2"
)
// LinearGradient defines a Gradient travelling straight at a given angle.
// The only supported values for the angle are `0.0` (vertical) and `90.0` (horizontal), currently.
type LinearGradient struct {
baseObject
StartColor color.Color // The beginning color of the gradient
EndColor color.Color // The end color of the gradient
Angle float64 // The angle of the gradient (0/180 for vertical; 90/270 for horizontal)
}
// Generate calculates an image of the gradient with the specified width and height.
func (g *LinearGradient) Generate(iw, ih int) image.Image {
w, h := float64(iw), float64(ih)
var generator func(x, y float64) float64
switch g.Angle {
case 90: // horizontal flipped
generator = func(x, _ float64) float64 {
return (w - x) / w
}
case 270: // horizontal
generator = func(x, _ float64) float64 {
return x / w
}
case 45: // diagonal negative flipped
generator = func(x, y float64) float64 {
return math.Abs((w - x + y) / (w + h)) // ((w+h)-(x+h-y)) / (w+h)
}
case 225: // diagonal negative
generator = func(x, y float64) float64 {
return math.Abs((x + h - y) / (w + h))
}
case 135: // diagonal positive flipped
generator = func(x, y float64) float64 {
return math.Abs((w + h - (x + y)) / (w + h))
}
case 315: // diagonal positive
generator = func(x, y float64) float64 {
return math.Abs((x + y) / (w + h))
}
case 180: // vertical flipped
generator = func(_, y float64) float64 {
return (h - y) / h
}
default: // vertical
generator = func(_, y float64) float64 {
return y / h
}
}
return computeGradient(generator, iw, ih, g.StartColor, g.EndColor)
}
// Hide will set this gradient to not be visible
func (g *LinearGradient) Hide() {
g.baseObject.Hide()
repaint(g)
}
// Move the gradient to a new position, relative to its parent / canvas
func (g *LinearGradient) Move(pos fyne.Position) {
if g.Position() == pos {
return
}
g.baseObject.Move(pos)
repaint(g)
}
// Resize resizes the gradient to a new size.
func (g *LinearGradient) Resize(size fyne.Size) {
if size == g.Size() {
return
}
g.baseObject.Resize(size)
// refresh needed to invalidate cached textures
g.Refresh()
}
// Refresh causes this gradient to be redrawn with its configured state.
func (g *LinearGradient) Refresh() {
Refresh(g)
}
// RadialGradient defines a Gradient travelling radially from a center point outward.
type RadialGradient struct {
baseObject
StartColor color.Color // The beginning color of the gradient
EndColor color.Color // The end color of the gradient
// The offset of the center for generation of the gradient.
// This is not a DP measure but relates to the width/height.
// A value of 0.5 would move the center by the half width/height.
CenterOffsetX, CenterOffsetY float64
}
// Generate calculates an image of the gradient with the specified width and height.
func (g *RadialGradient) Generate(iw, ih int) image.Image {
w, h := float64(iw), float64(ih)
// define center plus offset
centerX := w/2 + w*g.CenterOffsetX
centerY := h/2 + h*g.CenterOffsetY
// handle negative offsets
var a, b float64
if g.CenterOffsetX < 0 {
a = w - centerX
} else {
a = centerX
}
if g.CenterOffsetY < 0 {
b = h - centerY
} else {
b = centerY
}
generator := func(x, y float64) float64 {
// calculate distance from center for gradient multiplier
dx, dy := centerX-x, centerY-y
da := math.Sqrt(dx*dx + dy*dy*a*a/b/b)
if da > a {
return 1
}
return da / a
}
return computeGradient(generator, iw, ih, g.StartColor, g.EndColor)
}
// Hide will set this gradient to not be visible
func (g *RadialGradient) Hide() {
g.baseObject.Hide()
repaint(g)
}
// Move the gradient to a new position, relative to its parent / canvas
func (g *RadialGradient) Move(pos fyne.Position) {
g.baseObject.Move(pos)
repaint(g)
}
// Resize resizes the gradient to a new size.
func (g *RadialGradient) Resize(size fyne.Size) {
if size == g.Size() {
return
}
g.baseObject.Resize(size)
// refresh needed to invalidate cached textures
g.Refresh()
}
// Refresh causes this gradient to be redrawn with its configured state.
func (g *RadialGradient) Refresh() {
Refresh(g)
}
func calculatePixel(d float64, startColor, endColor color.Color) color.Color {
// fetch RGBA values
aR, aG, aB, aA := startColor.RGBA()
bR, bG, bB, bA := endColor.RGBA()
// Get difference
dR := float64(bR) - float64(aR)
dG := float64(bG) - float64(aG)
dB := float64(bB) - float64(aB)
dA := float64(bA) - float64(aA)
// Apply gradations
pixel := &color.RGBA64{
R: uint16(float64(aR) + d*dR),
B: uint16(float64(aB) + d*dB),
G: uint16(float64(aG) + d*dG),
A: uint16(float64(aA) + d*dA),
}
return pixel
}
func computeGradient(generator func(x, y float64) float64, w, h int, startColor, endColor color.Color) image.Image {
img := image.NewNRGBA(image.Rect(0, 0, w, h))
if startColor == nil && endColor == nil {
return img
} else if startColor == nil {
startColor = color.Transparent
} else if endColor == nil {
endColor = color.Transparent
}
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
distance := generator(float64(x)+0.5, float64(y)+0.5)
img.Set(x, y, calculatePixel(distance, startColor, endColor))
}
}
return img
}
// NewHorizontalGradient creates a new horizontally travelling linear gradient.
// The start color will be at the left of the gradient and the end color will be at the right.
func NewHorizontalGradient(start, end color.Color) *LinearGradient {
g := &LinearGradient{StartColor: start, EndColor: end}
g.Angle = 270
return g
}
// NewLinearGradient creates a linear gradient at the specified angle.
// The angle parameter is the degree angle along which the gradient is calculated.
// A NewHorizontalGradient uses 270 degrees and NewVerticalGradient is 0 degrees.
func NewLinearGradient(start, end color.Color, angle float64) *LinearGradient {
g := &LinearGradient{StartColor: start, EndColor: end}
g.Angle = angle
return g
}
// NewRadialGradient creates a new radial gradient.
func NewRadialGradient(start, end color.Color) *RadialGradient {
return &RadialGradient{StartColor: start, EndColor: end}
}
// NewVerticalGradient creates a new vertically travelling linear gradient.
// The start color will be at the top of the gradient and the end color will be at the bottom.
func NewVerticalGradient(start color.Color, end color.Color) *LinearGradient {
return &LinearGradient{StartColor: start, EndColor: end}
}

388
vendor/fyne.io/fyne/v2/canvas/image.go generated vendored Normal file
View File

@ -0,0 +1,388 @@
package canvas
import (
"bytes"
"errors"
"image"
_ "image/jpeg" // avoid users having to import when using image widget
_ "image/png" // avoid the same for PNG images
"io"
"os"
"path/filepath"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/cache"
"fyne.io/fyne/v2/internal/scale"
"fyne.io/fyne/v2/internal/svg"
"fyne.io/fyne/v2/storage"
)
// ImageFill defines the different type of ways an image can stretch to fill its space.
type ImageFill int
const (
// ImageFillStretch will scale the image to match the Size() values.
// This is the default and does not maintain aspect ratio.
ImageFillStretch ImageFill = iota
// ImageFillContain makes the image fit within the object Size(),
// centrally and maintaining aspect ratio.
// There may be transparent sections top and bottom or left and right.
ImageFillContain // (Fit)
// ImageFillOriginal ensures that the container grows to the pixel dimensions
// required to fit the original image. The aspect of the image will be maintained so,
// as with ImageFillContain there may be transparent areas around the image.
// Note that the minSize may be smaller than the image dimensions if scale > 1.
ImageFillOriginal
)
// ImageScale defines the different scaling filters used to scaling images
type ImageScale int32
const (
// ImageScaleSmooth will scale the image using ApproxBiLinear filter (or GL equivalent)
ImageScaleSmooth ImageScale = iota
// ImageScalePixels will scale the image using NearestNeighbor filter (or GL equivalent)
ImageScalePixels
// ImageScaleFastest will scale the image using hardware GPU if available
//
// Since: 2.0
ImageScaleFastest
)
// Declare conformity with CanvasObject interface
var _ fyne.CanvasObject = (*Image)(nil)
// Image describes a drawable image area that can render in a Fyne canvas
// The image may be a vector or a bitmap representation, it will fill the area.
// The fill mode can be changed by setting FillMode to a different ImageFill.
type Image struct {
baseObject
aspect float32
icon *svg.Decoder
isSVG bool
// one of the following sources will provide our image data
File string // Load the image from a file
Resource fyne.Resource // Load the image from an in-memory resource
Image image.Image // Specify a loaded image to use in this canvas object
Translucency float64 // Set a translucency value > 0.0 to fade the image
FillMode ImageFill // Specify how the image should expand to fill or fit the available space
ScaleMode ImageScale // Specify the type of scaling interpolation applied to the image
previousRender bool // did we successfully draw before? if so a nil content will need a reset
}
// Alpha is a convenience function that returns the alpha value for an image
// based on its Translucency value. The result is 1.0 - Translucency.
func (i *Image) Alpha() float64 {
return 1.0 - i.Translucency
}
// Aspect will return the original content aspect after it was last refreshed.
//
// Since: 2.4
func (i *Image) Aspect() float32 {
if i.aspect == 0 {
i.Refresh()
}
return i.aspect
}
// Hide will set this image to not be visible
func (i *Image) Hide() {
i.baseObject.Hide()
repaint(i)
}
// MinSize returns the specified minimum size, if set, or {1, 1} otherwise.
func (i *Image) MinSize() fyne.Size {
if i.Image == nil || i.aspect == 0 {
if i.File != "" || i.Resource != nil {
i.Refresh()
}
}
return i.baseObject.MinSize()
}
// Move the image object to a new position, relative to its parent top, left corner.
func (i *Image) Move(pos fyne.Position) {
if i.Position() == pos {
return
}
i.baseObject.Move(pos)
repaint(i)
}
// Refresh causes this image to be redrawn with its configured state.
func (i *Image) Refresh() {
rc, err := i.updateReader()
if err != nil {
fyne.LogError("Failed to load image", err)
return
}
if rc != nil {
rcMem := rc
defer rcMem.Close()
}
if i.File != "" || i.Resource != nil || i.Image != nil {
r, err := i.updateAspectAndMinSize(rc)
if err != nil {
fyne.LogError("Failed to load image", err)
return
}
rc = io.NopCloser(r)
} else if i.previousRender {
i.previousRender = false
Refresh(i)
return
} else {
return
}
if i.File != "" || i.Resource != nil {
size := i.Size()
width := size.Width
height := size.Height
if width == 0 || height == 0 {
return
}
if i.isSVG {
tex, err := i.renderSVG(width, height)
if err != nil {
fyne.LogError("Failed to render SVG", err)
return
}
i.Image = tex
} else {
if rc == nil {
return
}
img, _, err := image.Decode(rc)
if err != nil {
fyne.LogError("Failed to render image", err)
return
}
i.Image = img
}
}
i.previousRender = true
Refresh(i)
}
// Resize on an image will scale the content or reposition it according to FillMode.
// It will normally cause a Refresh to ensure the pixels are recalculated.
func (i *Image) Resize(s fyne.Size) {
if s == i.Size() {
return
}
i.baseObject.Resize(s)
if i.FillMode == ImageFillOriginal && i.Size().Height > 2 { // we can just ask for a GPU redraw to align
Refresh(i)
return
}
i.baseObject.Resize(s)
if i.isSVG || i.Image == nil {
i.Refresh() // we need to rasterise at the new size
} else {
Refresh(i) // just re-size using GPU scaling
}
}
// NewImageFromFile creates a new image from a local file.
// Images returned from this method will scale to fit the canvas object.
// The method for scaling can be set using the Fill field.
func NewImageFromFile(file string) *Image {
return &Image{File: file}
}
// NewImageFromURI creates a new image from named resource.
// File URIs will read the file path and other schemes will download the data into a resource.
// HTTP and HTTPs URIs will use the GET method by default to request the resource.
// Images returned from this method will scale to fit the canvas object.
// The method for scaling can be set using the Fill field.
//
// Since: 2.0
func NewImageFromURI(uri fyne.URI) *Image {
if uri.Scheme() == "file" && len(uri.String()) > 7 {
return NewImageFromFile(uri.Path())
}
var read io.ReadCloser
read, err := storage.Reader(uri) // attempt unknown / http file type
if err != nil {
fyne.LogError("Failed to open image URI", err)
return &Image{}
}
defer read.Close()
return NewImageFromReader(read, filepath.Base(uri.String()))
}
// NewImageFromReader creates a new image from a data stream.
// The name parameter is required to uniquely identify this image (for caching etc.).
// If the image in this io.Reader is an SVG, the name should end ".svg".
// Images returned from this method will scale to fit the canvas object.
// The method for scaling can be set using the Fill field.
//
// Since: 2.0
func NewImageFromReader(read io.Reader, name string) *Image {
data, err := io.ReadAll(read)
if err != nil {
fyne.LogError("Unable to read image data", err)
return nil
}
res := &fyne.StaticResource{
StaticName: name,
StaticContent: data,
}
return NewImageFromResource(res)
}
// NewImageFromResource creates a new image by loading the specified resource.
// Images returned from this method will scale to fit the canvas object.
// The method for scaling can be set using the Fill field.
func NewImageFromResource(res fyne.Resource) *Image {
return &Image{Resource: res}
}
// NewImageFromImage returns a new Image instance that is rendered from the Go
// image.Image passed in.
// Images returned from this method will scale to fit the canvas object.
// The method for scaling can be set using the Fill field.
func NewImageFromImage(img image.Image) *Image {
return &Image{Image: img}
}
func (i *Image) name() string {
if i.Resource != nil {
return i.Resource.Name()
} else if i.File != "" {
return i.File
}
return ""
}
func (i *Image) updateReader() (io.ReadCloser, error) {
i.isSVG = false
if i.Resource != nil {
i.isSVG = svg.IsResourceSVG(i.Resource)
content := i.Resource.Content()
if res, ok := i.Resource.(fyne.ThemedResource); i.isSVG && ok {
th := cache.WidgetTheme(i)
if th != nil {
col := th.Color(res.ThemeColorName(), fyne.CurrentApp().Settings().ThemeVariant())
var err error
content, err = svg.Colorize(content, col)
if err != nil {
fyne.LogError("", err)
}
}
}
return io.NopCloser(bytes.NewReader(content)), nil
} else if i.File != "" {
var err error
fd, err := os.Open(i.File)
if err != nil {
return nil, err
}
i.isSVG = svg.IsFileSVG(i.File)
return fd, nil
}
return nil, nil
}
func (i *Image) updateAspectAndMinSize(reader io.Reader) (io.Reader, error) {
var pixWidth, pixHeight int
if reader != nil {
r, width, height, aspect, err := i.imageDetailsFromReader(reader)
if err != nil {
return nil, err
}
reader = r
i.aspect = aspect
pixWidth, pixHeight = width, height
} else if i.Image != nil {
original := i.Image.Bounds().Size()
i.aspect = float32(original.X) / float32(original.Y)
pixWidth, pixHeight = original.X, original.Y
} else {
return nil, errors.New("no matching image source")
}
if i.FillMode == ImageFillOriginal {
i.SetMinSize(scale.ToFyneSize(i, pixWidth, pixHeight))
}
return reader, nil
}
func (i *Image) imageDetailsFromReader(source io.Reader) (reader io.Reader, width, height int, aspect float32, err error) {
if source == nil {
return nil, 0, 0, 0, errors.New("no matching reading reader")
}
if i.isSVG {
var err error
i.icon, err = svg.NewDecoder(source)
if err != nil {
return nil, 0, 0, 0, err
}
config := i.icon.Config()
width, height = config.Width, config.Height
aspect = config.Aspect
} else {
var buf bytes.Buffer
tee := io.TeeReader(source, &buf)
reader = io.MultiReader(&buf, source)
config, _, err := image.DecodeConfig(tee)
if err != nil {
return nil, 0, 0, 0, err
}
width, height = config.Width, config.Height
aspect = float32(width) / float32(height)
}
return
}
func (i *Image) renderSVG(width, height float32) (image.Image, error) {
c := fyne.CurrentApp().Driver().CanvasForObject(i)
screenWidth, screenHeight := int(width), int(height)
if c != nil {
// We want real output pixel count not just the screen coordinate space (i.e. macOS Retina)
screenWidth, screenHeight = c.PixelCoordinateForPosition(fyne.Position{X: width, Y: height})
} else { // no canvas info, assume HiDPI
screenWidth *= 2
screenHeight *= 2
}
tex := cache.GetSvg(i.name(), i, screenWidth, screenHeight)
if tex != nil {
return tex, nil
}
var err error
tex, err = i.icon.Draw(screenWidth, screenHeight)
if err != nil {
return nil, err
}
cache.SetSvg(i.name(), i, tex, screenWidth, screenHeight)
return tex, nil
}

108
vendor/fyne.io/fyne/v2/canvas/line.go generated vendored Normal file
View File

@ -0,0 +1,108 @@
package canvas
import (
"image/color"
"math"
"fyne.io/fyne/v2"
)
// Declare conformity with CanvasObject interface
var _ fyne.CanvasObject = (*Line)(nil)
// Line describes a colored line primitive in a Fyne canvas.
// Lines are special as they can have a negative width or height to indicate
// an inverse slope (i.e. slope up vs down).
type Line struct {
Position1 fyne.Position // The current top-left position of the Line
Position2 fyne.Position // The current bottom-right position of the Line
Hidden bool // Is this Line currently hidden
StrokeColor color.Color // The line stroke color
StrokeWidth float32 // The stroke width of the line
}
// Size returns the current size of bounding box for this line object
func (l *Line) Size() fyne.Size {
return fyne.NewSize(
float32(math.Abs(float64(l.Position2.X)-float64(l.Position1.X))),
float32(math.Abs(float64(l.Position2.Y)-float64(l.Position1.Y))),
)
}
// Resize sets a new bottom-right position for the line object, then it will then be refreshed.
func (l *Line) Resize(size fyne.Size) {
if size == l.Size() {
return
}
if l.Position1.X <= l.Position2.X {
l.Position2.X = l.Position1.X + size.Width
} else {
l.Position1.X = l.Position2.X + size.Width
}
if l.Position1.Y <= l.Position2.Y {
l.Position2.Y = l.Position1.Y + size.Height
} else {
l.Position1.Y = l.Position2.Y + size.Height
}
Refresh(l)
}
// Position gets the current top-left position of this line object, relative to its parent / canvas
func (l *Line) Position() fyne.Position {
return fyne.NewPos(fyne.Min(l.Position1.X, l.Position2.X), fyne.Min(l.Position1.Y, l.Position2.Y))
}
// Move the line object to a new position, relative to its parent / canvas
func (l *Line) Move(pos fyne.Position) {
oldPos := l.Position()
if oldPos == pos {
return
}
deltaX := pos.X - oldPos.X
deltaY := pos.Y - oldPos.Y
l.Position1 = l.Position1.AddXY(deltaX, deltaY)
l.Position2 = l.Position2.AddXY(deltaX, deltaY)
repaint(l)
}
// MinSize for a Line simply returns Size{1, 1} as there is no
// explicit content
func (l *Line) MinSize() fyne.Size {
return fyne.NewSize(1, 1)
}
// Visible returns true if this line// Show will set this circle to be visible is visible, false otherwise
func (l *Line) Visible() bool {
return !l.Hidden
}
// Show will set this line to be visible
func (l *Line) Show() {
l.Hidden = false
l.Refresh()
}
// Hide will set this line to not be visible
func (l *Line) Hide() {
l.Hidden = true
repaint(l)
}
// Refresh causes this line to be redrawn with its configured state.
func (l *Line) Refresh() {
Refresh(l)
}
// NewLine returns a new Line instance
func NewLine(color color.Color) *Line {
return &Line{
StrokeColor: color,
StrokeWidth: 1,
}
}

200
vendor/fyne.io/fyne/v2/canvas/raster.go generated vendored Normal file
View File

@ -0,0 +1,200 @@
package canvas
import (
"image"
"image/color"
"image/draw"
"fyne.io/fyne/v2"
)
// Declare conformity with CanvasObject interface
var _ fyne.CanvasObject = (*Raster)(nil)
// Raster describes a raster image area that can render in a Fyne canvas
type Raster struct {
baseObject
// Render the raster image from code
Generator func(w, h int) image.Image
// Set a translucency value > 0.0 to fade the raster
Translucency float64
// Specify the type of scaling interpolation applied to the raster if it is not full-size
// Since: 1.4.1
ScaleMode ImageScale
}
// Alpha is a convenience function that returns the alpha value for a raster
// based on its Translucency value. The result is 1.0 - Translucency.
func (r *Raster) Alpha() float64 {
return 1.0 - r.Translucency
}
// Hide will set this raster to not be visible
func (r *Raster) Hide() {
r.baseObject.Hide()
repaint(r)
}
// Move the raster to a new position, relative to its parent / canvas
func (r *Raster) Move(pos fyne.Position) {
if r.Position() == pos {
return
}
r.baseObject.Move(pos)
repaint(r)
}
// Resize on a raster image causes the new size to be set and then calls Refresh.
// This causes the underlying data to be recalculated and a new output to be drawn.
func (r *Raster) Resize(s fyne.Size) {
if s == r.Size() {
return
}
r.baseObject.Resize(s)
Refresh(r)
}
// Refresh causes this raster to be redrawn with its configured state.
func (r *Raster) Refresh() {
Refresh(r)
}
// NewRaster returns a new Image instance that is rendered dynamically using
// the specified generate function.
// Images returned from this method should draw dynamically to fill the width
// and height parameters passed to pixelColor.
func NewRaster(generate func(w, h int) image.Image) *Raster {
return &Raster{Generator: generate}
}
type pixelRaster struct {
r *Raster
img draw.Image
}
// NewRasterWithPixels returns a new Image instance that is rendered dynamically
// by iterating over the specified pixelColor function for each x, y pixel.
// Images returned from this method should draw dynamically to fill the width
// and height parameters passed to pixelColor.
func NewRasterWithPixels(pixelColor func(x, y, w, h int) color.Color) *Raster {
pix := &pixelRaster{}
pix.r = &Raster{
Generator: func(w, h int) image.Image {
if pix.img == nil || pix.img.Bounds().Size().X != w || pix.img.Bounds().Size().Y != h {
// raster first pixel, figure out color type
var dst draw.Image
rect := image.Rect(0, 0, w, h)
switch pixelColor(0, 0, w, h).(type) {
case color.Alpha:
dst = image.NewAlpha(rect)
case color.Alpha16:
dst = image.NewAlpha16(rect)
case color.CMYK:
dst = image.NewCMYK(rect)
case color.Gray:
dst = image.NewGray(rect)
case color.Gray16:
dst = image.NewGray16(rect)
case color.NRGBA:
dst = image.NewNRGBA(rect)
case color.NRGBA64:
dst = image.NewNRGBA64(rect)
case color.RGBA:
dst = image.NewRGBA(rect)
case color.RGBA64:
dst = image.NewRGBA64(rect)
default:
dst = image.NewRGBA(rect)
}
pix.img = dst
}
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
pix.img.Set(x, y, pixelColor(x, y, w, h))
}
}
return pix.img
},
}
return pix.r
}
type subImg interface {
SubImage(r image.Rectangle) image.Image
}
// NewRasterFromImage returns a new Raster instance that is rendered from the Go
// image.Image passed in.
// Rasters returned from this method will map pixel for pixel to the screen
// starting img.Bounds().Min pixels from the top left of the canvas object.
// Truncates rather than scales the image.
// If smaller than the target space, the image will be padded with zero-pixels to the target size.
func NewRasterFromImage(img image.Image) *Raster {
return &Raster{
Generator: func(w int, h int) image.Image {
bounds := img.Bounds()
rect := image.Rect(0, 0, w, h)
switch {
case w == bounds.Max.X && h == bounds.Max.Y:
return img
case w >= bounds.Max.X && h >= bounds.Max.Y:
// try quickly truncating
if sub, ok := img.(subImg); ok {
return sub.SubImage(image.Rectangle{
Min: bounds.Min,
Max: image.Point{
X: bounds.Min.X + w,
Y: bounds.Min.Y + h,
},
})
}
default:
if !rect.Overlaps(bounds) {
return image.NewUniform(color.RGBA{})
}
bounds = bounds.Intersect(rect)
}
// respect the user's pixel format (if possible)
var dst draw.Image
switch i := img.(type) {
case *image.Alpha:
dst = image.NewAlpha(rect)
case *image.Alpha16:
dst = image.NewAlpha16(rect)
case *image.CMYK:
dst = image.NewCMYK(rect)
case *image.Gray:
dst = image.NewGray(rect)
case *image.Gray16:
dst = image.NewGray16(rect)
case *image.NRGBA:
dst = image.NewNRGBA(rect)
case *image.NRGBA64:
dst = image.NewNRGBA64(rect)
case *image.Paletted:
dst = image.NewPaletted(rect, i.Palette)
case *image.RGBA:
dst = image.NewRGBA(rect)
case *image.RGBA64:
dst = image.NewRGBA64(rect)
default:
dst = image.NewRGBA(rect)
}
draw.Draw(dst, bounds, img, bounds.Min, draw.Over)
return dst
},
}
}

68
vendor/fyne.io/fyne/v2/canvas/rectangle.go generated vendored Normal file
View File

@ -0,0 +1,68 @@
package canvas
import (
"image/color"
"fyne.io/fyne/v2"
)
// Declare conformity with CanvasObject interface
var _ fyne.CanvasObject = (*Rectangle)(nil)
// Rectangle describes a colored rectangle primitive in a Fyne canvas
type Rectangle struct {
baseObject
FillColor color.Color // The rectangle fill color
StrokeColor color.Color // The rectangle stroke color
StrokeWidth float32 // The stroke width of the rectangle
// The radius of the rectangle corners
//
// Since: 2.4
CornerRadius float32
}
// Hide will set this rectangle to not be visible
func (r *Rectangle) Hide() {
r.baseObject.Hide()
repaint(r)
}
// Move the rectangle to a new position, relative to its parent / canvas
func (r *Rectangle) Move(pos fyne.Position) {
if r.Position() == pos {
return
}
r.baseObject.Move(pos)
repaint(r)
}
// Refresh causes this rectangle to be redrawn with its configured state.
func (r *Rectangle) Refresh() {
Refresh(r)
}
// Resize on a rectangle updates the new size of this object.
// If it has a stroke width this will cause it to Refresh.
func (r *Rectangle) Resize(s fyne.Size) {
if s == r.Size() {
return
}
r.baseObject.Resize(s)
if r.StrokeWidth == 0 {
return
}
Refresh(r)
}
// NewRectangle returns a new Rectangle instance
func NewRectangle(color color.Color) *Rectangle {
return &Rectangle{
FillColor: color,
}
}

85
vendor/fyne.io/fyne/v2/canvas/text.go generated vendored Normal file
View File

@ -0,0 +1,85 @@
package canvas
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)
// Declare conformity with CanvasObject interface
var _ fyne.CanvasObject = (*Text)(nil)
// Text describes a text primitive in a Fyne canvas.
// A text object can have a style set which will apply to the whole string.
// No formatting or text parsing will be performed
type Text struct {
baseObject
Alignment fyne.TextAlign // The alignment of the text content
Color color.Color // The main text draw color
Text string // The string content of this Text
TextSize float32 // Size of the text - if the Canvas scale is 1.0 this will be equivalent to point size
TextStyle fyne.TextStyle // The style of the text content
// FontSource defines a resource that can be used instead of the theme for looking up the font.
// When a font source is set the `TextStyle` may not be effective, as it will be limited to the styles
// present in the data provided.
//
// Since: 2.5
FontSource fyne.Resource
}
// Hide will set this text to not be visible
func (t *Text) Hide() {
t.baseObject.Hide()
repaint(t)
}
// MinSize returns the minimum size of this text object based on its font size and content.
// This is normally determined by the render implementation.
func (t *Text) MinSize() fyne.Size {
s, _ := fyne.CurrentApp().Driver().RenderedTextSize(t.Text, t.TextSize, t.TextStyle, t.FontSource)
return s
}
// Move the text to a new position, relative to its parent / canvas
func (t *Text) Move(pos fyne.Position) {
if t.Position() == pos {
return
}
t.baseObject.Move(pos)
repaint(t)
}
// Resize on a text updates the new size of this object, which may not result in a visual change, depending on alignment.
func (t *Text) Resize(s fyne.Size) {
if s == t.Size() {
return
}
t.baseObject.Resize(s)
Refresh(t)
}
// SetMinSize has no effect as the smallest size this canvas object can be is based on its font size and content.
func (t *Text) SetMinSize(fyne.Size) {
// no-op
}
// Refresh causes this text to be redrawn with its configured state.
func (t *Text) Refresh() {
Refresh(t)
}
// NewText returns a new Text implementation
func NewText(text string, color color.Color) *Text {
return &Text{
Color: color,
Text: text,
TextSize: theme.TextSize(),
}
}

107
vendor/fyne.io/fyne/v2/canvasobject.go generated vendored Normal file
View File

@ -0,0 +1,107 @@
package fyne
// CanvasObject describes any graphical object that can be added to a canvas.
// Objects have a size and position that can be controlled through this API.
// MinSize is used to determine the minimum size which this object should be displayed.
// An object will be visible by default but can be hidden with Hide() and re-shown with Show().
//
// Note: If this object is controlled as part of a Layout you should not call
// Resize(Size) or Move(Position).
type CanvasObject interface {
// geometry
// MinSize returns the minimum size this object needs to be drawn.
MinSize() Size
// Move moves this object to the given position relative to its parent.
// This should only be called if your object is not in a container with a layout manager.
Move(Position)
// Position returns the current position of the object relative to its parent.
Position() Position
// Resize resizes this object to the given size.
// This should only be called if your object is not in a container with a layout manager.
Resize(Size)
// Size returns the current size of this object.
Size() Size
// visibility
// Hide hides this object.
Hide()
// Visible returns whether this object is visible or not.
Visible() bool
// Show shows this object.
Show()
// Refresh must be called if this object should be redrawn because its inner state changed.
Refresh()
}
// Disableable describes any [CanvasObject] that can be disabled.
// This is primarily used with objects that also implement the Tappable interface.
type Disableable interface {
Enable()
Disable()
Disabled() bool
}
// DoubleTappable describes any [CanvasObject] that can also be double tapped.
type DoubleTappable interface {
DoubleTapped(*PointEvent)
}
// Draggable indicates that a [CanvasObject] can be dragged.
// This is used for any item that the user has indicated should be moved across the screen.
type Draggable interface {
Dragged(*DragEvent)
DragEnd()
}
// Focusable describes any [CanvasObject] that can respond to being focused.
// It will receive the FocusGained and FocusLost events appropriately.
// When focused it will also have TypedRune called as text is input and
// TypedKey called when other keys are pressed.
//
// Note: You must not change canvas state (including overlays or focus) in FocusGained or FocusLost
// or you would end up with a dead-lock.
type Focusable interface {
// FocusGained is a hook called by the focus handling logic after this object gained the focus.
FocusGained()
// FocusLost is a hook called by the focus handling logic after this object lost the focus.
FocusLost()
// TypedRune is a hook called by the input handling logic on text input events if this object is focused.
TypedRune(rune)
// TypedKey is a hook called by the input handling logic on key events if this object is focused.
TypedKey(*KeyEvent)
}
// Scrollable describes any [CanvasObject] that can also be scrolled.
// This is mostly used to implement the widget.ScrollContainer.
type Scrollable interface {
Scrolled(*ScrollEvent)
}
// SecondaryTappable describes a [CanvasObject] that can be right-clicked or long-tapped.
type SecondaryTappable interface {
TappedSecondary(*PointEvent)
}
// Shortcutable describes any [CanvasObject] that can respond to shortcut commands (quit, cut, copy, and paste).
type Shortcutable interface {
TypedShortcut(Shortcut)
}
// Tabbable describes any object that needs to accept the Tab key presses.
//
// Since: 2.1
type Tabbable interface {
// AcceptsTab is a hook called by the key press handling logic.
// If it returns true then the Tab key events will be sent using TypedKey.
AcceptsTab() bool
}
// Tappable describes any [CanvasObject] that can also be tapped.
// This should be implemented by buttons etc that wish to handle pointer interactions.
type Tappable interface {
Tapped(*PointEvent)
}

9
vendor/fyne.io/fyne/v2/clipboard.go generated vendored Normal file
View File

@ -0,0 +1,9 @@
package fyne
// Clipboard represents the system clipboard interface
type Clipboard interface {
// Content returns the clipboard content
Content() string
// SetContent sets the clipboard content
SetContent(content string)
}

39
vendor/fyne.io/fyne/v2/cloud.go generated vendored Normal file
View File

@ -0,0 +1,39 @@
package fyne
// CloudProvider specifies the identifying information of a cloud provider.
// This information is mostly used by the [fyne.io/cloud.ShowSettings] user flow.
//
// Since: 2.3
type CloudProvider interface {
// ProviderDescription returns a more detailed description of this cloud provider.
ProviderDescription() string
// ProviderIcon returns an icon resource that is associated with the given cloud service.
ProviderIcon() Resource
// ProviderName returns the name of this cloud provider, usually the name of the service it uses.
ProviderName() string
// Cleanup is called when this provider is no longer used and should be disposed.
// This is guaranteed to execute before a new provider is `Setup`
Cleanup(App)
// Setup is called when this provider is being used for the first time.
// Returning an error will exit the cloud setup process, though it can be retried.
Setup(App) error
}
// CloudProviderPreferences interface defines the functionality that a cloud provider will include if it is capable
// of synchronizing user preferences.
//
// Since: 2.3
type CloudProviderPreferences interface {
// CloudPreferences returns a preference provider that will sync values to the cloud this provider uses.
CloudPreferences(App) Preferences
}
// CloudProviderStorage interface defines the functionality that a cloud provider will include if it is capable
// of synchronizing user documents.
//
// Since: 2.3
type CloudProviderStorage interface {
// CloudStorage returns a storage provider that will sync documents to the cloud this provider uses.
CloudStorage(App) Storage
}

202
vendor/fyne.io/fyne/v2/container.go generated vendored Normal file
View File

@ -0,0 +1,202 @@
package fyne
// Declare conformity to [CanvasObject]
var _ CanvasObject = (*Container)(nil)
// Container is a [CanvasObject] that contains a collection of child objects.
// The layout of the children is set by the specified Layout.
type Container struct {
size Size // The current size of the Container
position Position // The current position of the Container
Hidden bool // Is this Container hidden
Layout Layout // The Layout algorithm for arranging child [CanvasObject]s
Objects []CanvasObject // The set of [CanvasObject]s this container holds
}
// NewContainer returns a new [Container] instance holding the specified [CanvasObject]s.
//
// Deprecated: Use [fyne.io/fyne/v2/container.NewWithoutLayout] to create a container that uses manual layout.
func NewContainer(objects ...CanvasObject) *Container {
return NewContainerWithoutLayout(objects...)
}
// NewContainerWithoutLayout returns a new [Container] instance holding the specified
// [CanvasObject]s that are manually arranged.
//
// Deprecated: Use [fyne.io/fyne/v2/container.NewWithoutLayout] instead.
func NewContainerWithoutLayout(objects ...CanvasObject) *Container {
ret := &Container{
Objects: objects,
}
ret.size = ret.MinSize()
return ret
}
// NewContainerWithLayout returns a new [Container] instance holding the specified
// [CanvasObject]s which will be laid out according to the specified Layout.
//
// Deprecated: Use [fyne.io/fyne/v2/container.New] instead.
func NewContainerWithLayout(layout Layout, objects ...CanvasObject) *Container {
ret := &Container{
Objects: objects,
Layout: layout,
}
ret.size = layout.MinSize(objects)
ret.layout()
return ret
}
// Add appends the specified object to the items this container manages.
//
// Since: 1.4
func (c *Container) Add(add CanvasObject) {
if add == nil {
return
}
c.Objects = append(c.Objects, add)
c.layout()
}
// AddObject adds another [CanvasObject] to the set this Container holds.
//
// Deprecated: Use [Container.Add] instead.
func (c *Container) AddObject(o CanvasObject) {
c.Add(o)
}
// Hide sets this container, and all its children, to be not visible.
func (c *Container) Hide() {
if c.Hidden {
return
}
c.Hidden = true
repaint(c)
}
// MinSize calculates the minimum size of c.
// This is delegated to the [Container.Layout], if specified, otherwise it will be calculated.
func (c *Container) MinSize() Size {
if c.Layout != nil {
return c.Layout.MinSize(c.Objects)
}
minSize := NewSize(1, 1)
for _, child := range c.Objects {
minSize = minSize.Max(child.MinSize())
}
return minSize
}
// Move the container (and all its children) to a new position, relative to its parent.
func (c *Container) Move(pos Position) {
c.position = pos
repaint(c)
}
// Position gets the current position of c relative to its parent.
func (c *Container) Position() Position {
return c.position
}
// Refresh causes this object to be redrawn in its current state
func (c *Container) Refresh() {
c.layout()
for _, child := range c.Objects {
child.Refresh()
}
// this is basically just canvas.Refresh(c) without the package loop
o := CurrentApp().Driver().CanvasForObject(c)
if o == nil {
return
}
o.Refresh(c)
}
// Remove updates the contents of this container to no longer include the specified object.
// This method is not intended to be used inside a loop, to remove all the elements.
// It is much more efficient to call [Container.RemoveAll) instead.
func (c *Container) Remove(rem CanvasObject) {
if len(c.Objects) == 0 {
return
}
for i, o := range c.Objects {
if o != rem {
continue
}
copy(c.Objects[i:], c.Objects[i+1:])
c.Objects[len(c.Objects)-1] = nil
c.Objects = c.Objects[:len(c.Objects)-1]
c.layout()
return
}
}
// RemoveAll updates the contents of this container to no longer include any objects.
//
// Since: 2.2
func (c *Container) RemoveAll() {
c.Objects = nil
c.layout()
}
// Resize sets a new size for c.
func (c *Container) Resize(size Size) {
if c.size == size {
return
}
c.size = size
c.layout()
}
// Show sets this container, and all its children, to be visible.
func (c *Container) Show() {
if !c.Hidden {
return
}
c.Hidden = false
}
// Size returns the current size c.
func (c *Container) Size() Size {
return c.size
}
// Visible returns true if the container is currently visible, false otherwise.
func (c *Container) Visible() bool {
return !c.Hidden
}
func (c *Container) layout() {
if c.Layout == nil {
return
}
c.Layout.Layout(c.Objects, c.size)
}
// repaint instructs the containing canvas to redraw, even if nothing changed.
// This method is a duplicate of what is in `canvas/canvas.go` to avoid a dependency loop or public API.
func repaint(obj *Container) {
app := CurrentApp()
if app == nil || app.Driver() == nil {
return
}
c := app.Driver().CanvasForObject(obj)
if c != nil {
if paint, ok := c.(interface{ SetDirty() }); ok {
paint.SetDirty()
}
}
}

473
vendor/fyne.io/fyne/v2/container/apptabs.go generated vendored Normal file
View File

@ -0,0 +1,473 @@
package container
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// Declare conformity with Widget interface.
var _ fyne.Widget = (*AppTabs)(nil)
// AppTabs container is used to split your application into various different areas identified by tabs.
// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
// Each item is represented by a button at the edge of the container.
//
// Since: 1.4
type AppTabs struct {
widget.BaseWidget
Items []*TabItem
// Deprecated: Use `OnSelected func(*TabItem)` instead.
OnChanged func(*TabItem) `json:"-"`
OnSelected func(*TabItem) `json:"-"`
OnUnselected func(*TabItem) `json:"-"`
current int
location TabLocation
isTransitioning bool
popUpMenu *widget.PopUpMenu
}
// NewAppTabs creates a new tab container that allows the user to choose between different areas of an app.
//
// Since: 1.4
func NewAppTabs(items ...*TabItem) *AppTabs {
tabs := &AppTabs{}
tabs.BaseWidget.ExtendBaseWidget(tabs)
tabs.SetItems(items)
return tabs
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
//
// Implements: fyne.Widget
func (t *AppTabs) CreateRenderer() fyne.WidgetRenderer {
t.BaseWidget.ExtendBaseWidget(t)
th := t.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
r := &appTabsRenderer{
baseTabsRenderer: baseTabsRenderer{
bar: &fyne.Container{},
divider: canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)),
indicator: canvas.NewRectangle(th.Color(theme.ColorNamePrimary, v)),
},
appTabs: t,
}
r.action = r.buildOverflowTabsButton()
r.tabs = t
// Initially setup the tab bar to only show one tab, all others will be in overflow.
// When the widget is laid out, and we know the size, the tab bar will be updated to show as many as can fit.
r.updateTabs(1)
r.updateIndicator(false)
r.applyTheme(t)
return r
}
// Append adds a new TabItem to the end of the tab bar.
func (t *AppTabs) Append(item *TabItem) {
t.SetItems(append(t.Items, item))
}
// CurrentTab returns the currently selected TabItem.
//
// Deprecated: Use `AppTabs.Selected() *TabItem` instead.
func (t *AppTabs) CurrentTab() *TabItem {
if t.current < 0 || t.current >= len(t.Items) {
return nil
}
return t.Items[t.current]
}
// CurrentTabIndex returns the index of the currently selected TabItem.
//
// Deprecated: Use `AppTabs.SelectedIndex() int` instead.
func (t *AppTabs) CurrentTabIndex() int {
return t.current
}
// DisableIndex disables the TabItem at the specified index.
//
// Since: 2.3
func (t *AppTabs) DisableIndex(i int) {
disableIndex(t, i)
}
// DisableItem disables the specified TabItem.
//
// Since: 2.3
func (t *AppTabs) DisableItem(item *TabItem) {
disableItem(t, item)
}
// EnableIndex enables the TabItem at the specified index.
//
// Since: 2.3
func (t *AppTabs) EnableIndex(i int) {
enableIndex(t, i)
}
// EnableItem enables the specified TabItem.
//
// Since: 2.3
func (t *AppTabs) EnableItem(item *TabItem) {
enableItem(t, item)
}
// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
//
// Deprecated: Support for extending containers is being removed
func (t *AppTabs) ExtendBaseWidget(wid fyne.Widget) {
t.BaseWidget.ExtendBaseWidget(wid)
}
// Hide hides the widget.
//
// Implements: fyne.CanvasObject
func (t *AppTabs) Hide() {
if t.popUpMenu != nil {
t.popUpMenu.Hide()
t.popUpMenu = nil
}
t.BaseWidget.Hide()
}
// MinSize returns the size that this widget should not shrink below
//
// Implements: fyne.CanvasObject
func (t *AppTabs) MinSize() fyne.Size {
t.BaseWidget.ExtendBaseWidget(t)
return t.BaseWidget.MinSize()
}
// Remove tab by value.
func (t *AppTabs) Remove(item *TabItem) {
removeItem(t, item)
t.Refresh()
}
// RemoveIndex removes tab by index.
func (t *AppTabs) RemoveIndex(index int) {
removeIndex(t, index)
t.Refresh()
}
// Select sets the specified TabItem to be selected and its content visible.
func (t *AppTabs) Select(item *TabItem) {
selectItem(t, item)
}
// SelectIndex sets the TabItem at the specific index to be selected and its content visible.
func (t *AppTabs) SelectIndex(index int) {
selectIndex(t, index)
}
// SelectTab sets the specified TabItem to be selected and its content visible.
//
// Deprecated: Use `AppTabs.Select(*TabItem)` instead.
func (t *AppTabs) SelectTab(item *TabItem) {
for i, child := range t.Items {
if child == item {
t.SelectTabIndex(i)
return
}
}
}
// SelectTabIndex sets the TabItem at the specific index to be selected and its content visible.
//
// Deprecated: Use `AppTabs.SelectIndex(int)` instead.
func (t *AppTabs) SelectTabIndex(index int) {
if index < 0 || index >= len(t.Items) || t.current == index {
return
}
t.current = index
t.Refresh()
if t.OnChanged != nil {
t.OnChanged(t.Items[t.current])
}
}
// Selected returns the currently selected TabItem.
func (t *AppTabs) Selected() *TabItem {
return selected(t)
}
// SelectedIndex returns the index of the currently selected TabItem.
func (t *AppTabs) SelectedIndex() int {
return t.current
}
// SetItems sets the containers items and refreshes.
func (t *AppTabs) SetItems(items []*TabItem) {
setItems(t, items)
t.Refresh()
}
// SetTabLocation sets the location of the tab bar
func (t *AppTabs) SetTabLocation(l TabLocation) {
t.location = tabsAdjustedLocation(l, t)
t.Refresh()
}
// Show this widget, if it was previously hidden
//
// Implements: fyne.CanvasObject
func (t *AppTabs) Show() {
t.BaseWidget.Show()
t.SelectIndex(t.current)
}
func (t *AppTabs) onUnselected() func(*TabItem) {
return t.OnUnselected
}
func (t *AppTabs) onSelected() func(*TabItem) {
return func(tab *TabItem) {
if f := t.OnChanged; f != nil {
f(tab)
}
if f := t.OnSelected; f != nil {
f(tab)
}
}
}
func (t *AppTabs) items() []*TabItem {
return t.Items
}
func (t *AppTabs) selected() int {
return t.current
}
func (t *AppTabs) setItems(items []*TabItem) {
t.Items = items
}
func (t *AppTabs) setSelected(selected int) {
t.current = selected
}
func (t *AppTabs) setTransitioning(transitioning bool) {
t.isTransitioning = transitioning
}
func (t *AppTabs) tabLocation() TabLocation {
return t.location
}
func (t *AppTabs) transitioning() bool {
return t.isTransitioning
}
// Declare conformity with WidgetRenderer interface.
var _ fyne.WidgetRenderer = (*appTabsRenderer)(nil)
type appTabsRenderer struct {
baseTabsRenderer
appTabs *AppTabs
}
func (r *appTabsRenderer) Layout(size fyne.Size) {
// Try render as many tabs as will fit, others will appear in the overflow
if len(r.appTabs.Items) == 0 {
r.updateTabs(0)
} else {
for i := len(r.appTabs.Items); i > 0; i-- {
r.updateTabs(i)
barMin := r.bar.MinSize()
if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
if barMin.Height <= size.Height {
// Tab bar is short enough to fit
break
}
} else {
if barMin.Width <= size.Width {
// Tab bar is thin enough to fit
break
}
}
}
}
r.layout(r.appTabs, size)
r.updateIndicator(r.appTabs.transitioning())
if r.appTabs.transitioning() {
r.appTabs.setTransitioning(false)
}
}
func (r *appTabsRenderer) MinSize() fyne.Size {
return r.minSize(r.appTabs)
}
func (r *appTabsRenderer) Objects() []fyne.CanvasObject {
return r.objects(r.appTabs)
}
func (r *appTabsRenderer) Refresh() {
r.Layout(r.appTabs.Size())
r.refresh(r.appTabs)
canvas.Refresh(r.appTabs)
}
func (r *appTabsRenderer) buildOverflowTabsButton() (overflow *widget.Button) {
overflow = &widget.Button{Icon: moreIcon(r.appTabs), Importance: widget.LowImportance, OnTapped: func() {
// Show pop up containing all tabs which did not fit in the tab bar
itemLen, objLen := len(r.appTabs.Items), len(r.bar.Objects[0].(*fyne.Container).Objects)
items := make([]*fyne.MenuItem, 0, itemLen-objLen)
for i := objLen; i < itemLen; i++ {
index := i // capture
// FIXME MenuItem doesn't support icons (#1752)
// FIXME MenuItem can't show if it is the currently selected tab (#1753)
ti := r.appTabs.Items[i]
mi := fyne.NewMenuItem(ti.Text, func() {
r.appTabs.SelectIndex(index)
if r.appTabs.popUpMenu != nil {
r.appTabs.popUpMenu.Hide()
r.appTabs.popUpMenu = nil
}
})
if ti.Disabled() {
mi.Disabled = true
}
items = append(items, mi)
}
r.appTabs.popUpMenu = buildPopUpMenu(r.appTabs, overflow, items)
}}
return overflow
}
func (r *appTabsRenderer) buildTabButtons(count int) *fyne.Container {
buttons := &fyne.Container{}
var iconPos buttonIconPosition
if isMobile(r.tabs) {
cells := count
if cells == 0 {
cells = 1
}
if r.appTabs.location == TabLocationTop || r.appTabs.location == TabLocationBottom {
buttons.Layout = layout.NewGridLayoutWithColumns(cells)
} else {
buttons.Layout = layout.NewGridLayoutWithRows(cells)
}
iconPos = buttonIconTop
} else if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
buttons.Layout = layout.NewVBoxLayout()
iconPos = buttonIconTop
} else {
buttons.Layout = layout.NewHBoxLayout()
iconPos = buttonIconInline
}
for i := 0; i < count; i++ {
item := r.appTabs.Items[i]
if item.button == nil {
item.button = &tabButton{
onTapped: func() { r.appTabs.Select(item) },
tabs: r.tabs,
}
}
button := item.button
button.icon = item.Icon
button.iconPosition = iconPos
if i == r.appTabs.current {
button.importance = widget.HighImportance
} else {
button.importance = widget.MediumImportance
}
button.text = item.Text
button.textAlignment = fyne.TextAlignCenter
button.Refresh()
buttons.Objects = append(buttons.Objects, button)
}
return buttons
}
func (r *appTabsRenderer) updateIndicator(animate bool) {
if r.appTabs.current < 0 {
r.indicator.Hide()
return
}
r.indicator.Show()
var selectedPos fyne.Position
var selectedSize fyne.Size
buttons := r.bar.Objects[0].(*fyne.Container).Objects
if r.appTabs.current >= len(buttons) {
if a := r.action; a != nil {
selectedPos = a.Position()
selectedSize = a.Size()
}
} else {
selected := buttons[r.appTabs.current]
selectedPos = selected.Position()
selectedSize = selected.Size()
}
var indicatorPos fyne.Position
var indicatorSize fyne.Size
th := r.appTabs.Theme()
pad := th.Size(theme.SizeNamePadding)
switch r.appTabs.location {
case TabLocationTop:
indicatorPos = fyne.NewPos(selectedPos.X, r.bar.MinSize().Height)
indicatorSize = fyne.NewSize(selectedSize.Width, pad)
case TabLocationLeading:
indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y)
indicatorSize = fyne.NewSize(pad, selectedSize.Height)
case TabLocationBottom:
indicatorPos = fyne.NewPos(selectedPos.X, r.bar.Position().Y-pad)
indicatorSize = fyne.NewSize(selectedSize.Width, pad)
case TabLocationTrailing:
indicatorPos = fyne.NewPos(r.bar.Position().X-pad, selectedPos.Y)
indicatorSize = fyne.NewSize(pad, selectedSize.Height)
}
r.moveIndicator(indicatorPos, indicatorSize, th, animate)
}
func (r *appTabsRenderer) updateTabs(max int) {
tabCount := len(r.appTabs.Items)
// Set overflow action
if tabCount <= max {
r.action.Hide()
r.bar.Layout = layout.NewStackLayout()
} else {
tabCount = max
r.action.Show()
// Set layout of tab bar containing tab buttons and overflow action
if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
r.bar.Layout = layout.NewBorderLayout(nil, r.action, nil, nil)
} else {
r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.action)
}
}
buttons := r.buildTabButtons(tabCount)
r.bar.Objects = []fyne.CanvasObject{buttons}
if a := r.action; a != nil {
r.bar.Objects = append(r.bar.Objects, a)
}
r.bar.Refresh()
}

20
vendor/fyne.io/fyne/v2/container/container.go generated vendored Normal file
View File

@ -0,0 +1,20 @@
// Package container provides containers that are used to lay out and organise applications.
package container
import (
"fyne.io/fyne/v2"
)
// New returns a new Container instance holding the specified CanvasObjects which will be laid out according to the specified Layout.
//
// Since: 2.0
func New(layout fyne.Layout, objects ...fyne.CanvasObject) *fyne.Container {
return &fyne.Container{Layout: layout, Objects: objects}
}
// NewWithoutLayout returns a new Container instance holding the specified CanvasObjects that are manually arranged.
//
// Since: 2.0
func NewWithoutLayout(objects ...fyne.CanvasObject) *fyne.Container {
return &fyne.Container{Objects: objects}
}

492
vendor/fyne.io/fyne/v2/container/doctabs.go generated vendored Normal file
View File

@ -0,0 +1,492 @@
package container
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// Declare conformity with Widget interface.
var _ fyne.Widget = (*DocTabs)(nil)
// DocTabs container is used to display various pieces of content identified by tabs.
// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
// Each item is represented by a button at the edge of the container.
//
// Since: 2.1
type DocTabs struct {
widget.BaseWidget
Items []*TabItem
CreateTab func() *TabItem `json:"-"`
CloseIntercept func(*TabItem) `json:"-"`
OnClosed func(*TabItem) `json:"-"`
OnSelected func(*TabItem) `json:"-"`
OnUnselected func(*TabItem) `json:"-"`
current int
location TabLocation
isTransitioning bool
popUpMenu *widget.PopUpMenu
}
// NewDocTabs creates a new tab container that allows the user to choose between various pieces of content.
//
// Since: 2.1
func NewDocTabs(items ...*TabItem) *DocTabs {
tabs := &DocTabs{}
tabs.ExtendBaseWidget(tabs)
tabs.SetItems(items)
return tabs
}
// Append adds a new TabItem to the end of the tab bar.
func (t *DocTabs) Append(item *TabItem) {
t.SetItems(append(t.Items, item))
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
//
// Implements: fyne.Widget
func (t *DocTabs) CreateRenderer() fyne.WidgetRenderer {
t.ExtendBaseWidget(t)
th := t.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
r := &docTabsRenderer{
baseTabsRenderer: baseTabsRenderer{
bar: &fyne.Container{},
divider: canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)),
indicator: canvas.NewRectangle(th.Color(theme.ColorNamePrimary, v)),
},
docTabs: t,
scroller: NewScroll(&fyne.Container{}),
}
r.action = r.buildAllTabsButton()
r.create = r.buildCreateTabsButton()
r.tabs = t
r.box = NewHBox(r.create, r.action)
r.scroller.OnScrolled = func(offset fyne.Position) {
r.updateIndicator(false)
}
r.updateAllTabs()
r.updateCreateTab()
r.updateTabs()
r.updateIndicator(false)
r.applyTheme(t)
return r
}
// DisableIndex disables the TabItem at the specified index.
//
// Since: 2.3
func (t *DocTabs) DisableIndex(i int) {
disableIndex(t, i)
}
// DisableItem disables the specified TabItem.
//
// Since: 2.3
func (t *DocTabs) DisableItem(item *TabItem) {
disableItem(t, item)
}
// EnableIndex enables the TabItem at the specified index.
//
// Since: 2.3
func (t *DocTabs) EnableIndex(i int) {
enableIndex(t, i)
}
// EnableItem enables the specified TabItem.
//
// Since: 2.3
func (t *DocTabs) EnableItem(item *TabItem) {
enableItem(t, item)
}
// Hide hides the widget.
//
// Implements: fyne.CanvasObject
func (t *DocTabs) Hide() {
if t.popUpMenu != nil {
t.popUpMenu.Hide()
t.popUpMenu = nil
}
t.BaseWidget.Hide()
}
// MinSize returns the size that this widget should not shrink below
//
// Implements: fyne.CanvasObject
func (t *DocTabs) MinSize() fyne.Size {
t.ExtendBaseWidget(t)
return t.BaseWidget.MinSize()
}
// Remove tab by value.
func (t *DocTabs) Remove(item *TabItem) {
removeItem(t, item)
t.Refresh()
}
// RemoveIndex removes tab by index.
func (t *DocTabs) RemoveIndex(index int) {
removeIndex(t, index)
t.Refresh()
}
// Select sets the specified TabItem to be selected and its content visible.
func (t *DocTabs) Select(item *TabItem) {
selectItem(t, item)
t.Refresh()
}
// SelectIndex sets the TabItem at the specific index to be selected and its content visible.
func (t *DocTabs) SelectIndex(index int) {
selectIndex(t, index)
}
// Selected returns the currently selected TabItem.
func (t *DocTabs) Selected() *TabItem {
return selected(t)
}
// SelectedIndex returns the index of the currently selected TabItem.
func (t *DocTabs) SelectedIndex() int {
return t.current
}
// SetItems sets the containers items and refreshes.
func (t *DocTabs) SetItems(items []*TabItem) {
setItems(t, items)
t.Refresh()
}
// SetTabLocation sets the location of the tab bar
func (t *DocTabs) SetTabLocation(l TabLocation) {
t.location = tabsAdjustedLocation(l, t)
t.Refresh()
}
// Show this widget, if it was previously hidden
//
// Implements: fyne.CanvasObject
func (t *DocTabs) Show() {
t.BaseWidget.Show()
t.SelectIndex(t.current)
}
func (t *DocTabs) close(item *TabItem) {
if f := t.CloseIntercept; f != nil {
f(item)
} else {
t.Remove(item)
if f := t.OnClosed; f != nil {
f(item)
}
}
}
func (t *DocTabs) onUnselected() func(*TabItem) {
return t.OnUnselected
}
func (t *DocTabs) onSelected() func(*TabItem) {
return t.OnSelected
}
func (t *DocTabs) items() []*TabItem {
return t.Items
}
func (t *DocTabs) selected() int {
return t.current
}
func (t *DocTabs) setItems(items []*TabItem) {
t.Items = items
}
func (t *DocTabs) setSelected(selected int) {
t.current = selected
}
func (t *DocTabs) setTransitioning(transitioning bool) {
t.isTransitioning = transitioning
}
func (t *DocTabs) tabLocation() TabLocation {
return t.location
}
func (t *DocTabs) transitioning() bool {
return t.isTransitioning
}
// Declare conformity with WidgetRenderer interface.
var _ fyne.WidgetRenderer = (*docTabsRenderer)(nil)
type docTabsRenderer struct {
baseTabsRenderer
docTabs *DocTabs
scroller *Scroll
box *fyne.Container
create *widget.Button
lastSelected int
}
func (r *docTabsRenderer) Layout(size fyne.Size) {
r.updateAllTabs()
r.updateCreateTab()
r.updateTabs()
r.layout(r.docTabs, size)
// lay out buttons before updating indicator, which is relative to their position
buttons := r.scroller.Content.(*fyne.Container)
buttons.Layout.Layout(buttons.Objects, buttons.Size())
r.updateIndicator(r.docTabs.transitioning())
if r.docTabs.transitioning() {
r.docTabs.setTransitioning(false)
}
}
func (r *docTabsRenderer) MinSize() fyne.Size {
return r.minSize(r.docTabs)
}
func (r *docTabsRenderer) Objects() []fyne.CanvasObject {
return r.objects(r.docTabs)
}
func (r *docTabsRenderer) Refresh() {
r.Layout(r.docTabs.Size())
if c := r.docTabs.current; c != r.lastSelected {
if c >= 0 && c < len(r.docTabs.Items) {
r.scrollToSelected()
}
r.lastSelected = c
}
r.refresh(r.docTabs)
canvas.Refresh(r.docTabs)
}
func (r *docTabsRenderer) buildAllTabsButton() (all *widget.Button) {
all = &widget.Button{Importance: widget.LowImportance, OnTapped: func() {
// Show pop up containing all tabs
items := make([]*fyne.MenuItem, len(r.docTabs.Items))
for i := 0; i < len(r.docTabs.Items); i++ {
index := i // capture
// FIXME MenuItem doesn't support icons (#1752)
items[i] = fyne.NewMenuItem(r.docTabs.Items[i].Text, func() {
r.docTabs.SelectIndex(index)
if r.docTabs.popUpMenu != nil {
r.docTabs.popUpMenu.Hide()
r.docTabs.popUpMenu = nil
}
})
items[i].Checked = index == r.docTabs.current
}
r.docTabs.popUpMenu = buildPopUpMenu(r.docTabs, all, items)
}}
return all
}
func (r *docTabsRenderer) buildCreateTabsButton() *widget.Button {
create := widget.NewButton("", func() {
if f := r.docTabs.CreateTab; f != nil {
if tab := f(); tab != nil {
r.docTabs.Append(tab)
r.docTabs.SelectIndex(len(r.docTabs.Items) - 1)
}
}
})
create.Importance = widget.LowImportance
return create
}
func (r *docTabsRenderer) buildTabButtons(count int, buttons *fyne.Container) {
buttons.Objects = nil
var iconPos buttonIconPosition
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
buttons.Layout = layout.NewVBoxLayout()
iconPos = buttonIconTop
} else {
buttons.Layout = layout.NewHBoxLayout()
iconPos = buttonIconInline
}
for i := 0; i < count; i++ {
item := r.docTabs.Items[i]
if item.button == nil {
item.button = &tabButton{
onTapped: func() { r.docTabs.Select(item) },
onClosed: func() { r.docTabs.close(item) },
tabs: r.tabs,
}
}
button := item.button
button.icon = item.Icon
button.iconPosition = iconPos
if i == r.docTabs.current {
button.importance = widget.HighImportance
} else {
button.importance = widget.MediumImportance
}
button.text = item.Text
button.textAlignment = fyne.TextAlignLeading
button.Refresh()
buttons.Objects = append(buttons.Objects, button)
}
}
func (r *docTabsRenderer) scrollToSelected() {
buttons := r.scroller.Content.(*fyne.Container)
// https://github.com/fyne-io/fyne/issues/3909
// very dirty temporary fix to this crash!
if r.docTabs.current < 0 || r.docTabs.current >= len(buttons.Objects) {
return
}
button := buttons.Objects[r.docTabs.current]
pos := button.Position()
size := button.Size()
offset := r.scroller.Offset
viewport := r.scroller.Size()
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
if pos.Y < offset.Y {
offset.Y = pos.Y
} else if pos.Y+size.Height > offset.Y+viewport.Height {
offset.Y = pos.Y + size.Height - viewport.Height
}
} else {
if pos.X < offset.X {
offset.X = pos.X
} else if pos.X+size.Width > offset.X+viewport.Width {
offset.X = pos.X + size.Width - viewport.Width
}
}
r.scroller.Offset = offset
r.updateIndicator(false)
}
func (r *docTabsRenderer) updateIndicator(animate bool) {
th := r.docTabs.Theme()
if r.docTabs.current < 0 {
r.indicator.FillColor = color.Transparent
r.moveIndicator(fyne.NewPos(0, 0), fyne.NewSize(0, 0), th, animate)
return
}
var selectedPos fyne.Position
var selectedSize fyne.Size
buttons := r.scroller.Content.(*fyne.Container).Objects
if r.docTabs.current >= len(buttons) {
if a := r.action; a != nil {
selectedPos = a.Position()
selectedSize = a.Size()
minSize := a.MinSize()
if minSize.Width > selectedSize.Width {
selectedSize = minSize
}
}
} else {
selected := buttons[r.docTabs.current]
selectedPos = selected.Position()
selectedSize = selected.Size()
minSize := selected.MinSize()
if minSize.Width > selectedSize.Width {
selectedSize = minSize
}
}
scrollOffset := r.scroller.Offset
scrollSize := r.scroller.Size()
var indicatorPos fyne.Position
var indicatorSize fyne.Size
pad := th.Size(theme.SizeNamePadding)
switch r.docTabs.location {
case TabLocationTop:
indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.MinSize().Height)
indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), pad)
case TabLocationLeading:
indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y-scrollOffset.Y)
indicatorSize = fyne.NewSize(pad, fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y))
case TabLocationBottom:
indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.Position().Y-pad)
indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), pad)
case TabLocationTrailing:
indicatorPos = fyne.NewPos(r.bar.Position().X-pad, selectedPos.Y-scrollOffset.Y)
indicatorSize = fyne.NewSize(pad, fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y))
}
if indicatorPos.X < 0 {
indicatorSize.Width = indicatorSize.Width + indicatorPos.X
indicatorPos.X = 0
}
if indicatorPos.Y < 0 {
indicatorSize.Height = indicatorSize.Height + indicatorPos.Y
indicatorPos.Y = 0
}
if indicatorSize.Width < 0 || indicatorSize.Height < 0 {
r.indicator.FillColor = color.Transparent
r.indicator.Refresh()
return
}
r.moveIndicator(indicatorPos, indicatorSize, th, animate)
}
func (r *docTabsRenderer) updateAllTabs() {
if len(r.docTabs.Items) > 0 {
r.action.Show()
} else {
r.action.Hide()
}
}
func (r *docTabsRenderer) updateCreateTab() {
if r.docTabs.CreateTab != nil {
r.create.SetIcon(theme.ContentAddIcon())
r.create.Show()
} else {
r.create.Hide()
}
}
func (r *docTabsRenderer) updateTabs() {
tabCount := len(r.docTabs.Items)
r.buildTabButtons(tabCount, r.scroller.Content.(*fyne.Container))
// Set layout of tab bar containing tab buttons and overflow action
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
r.bar.Layout = layout.NewBorderLayout(nil, r.box, nil, nil)
r.scroller.Direction = ScrollVerticalOnly
} else {
r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.box)
r.scroller.Direction = ScrollHorizontalOnly
}
r.bar.Objects = []fyne.CanvasObject{r.scroller, r.box}
r.bar.Refresh()
}

442
vendor/fyne.io/fyne/v2/container/innerwindow.go generated vendored Normal file
View File

@ -0,0 +1,442 @@
package container
import (
"image/color"
"runtime"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
intWidget "fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type titleBarButtonMode int
const (
modeClose titleBarButtonMode = iota
modeMinimize
modeMaximize
modeIcon
)
var _ fyne.Widget = (*InnerWindow)(nil)
// InnerWindow defines a container that wraps content in a window border - that can then be placed inside
// a regular container/canvas.
//
// Since: 2.5
type InnerWindow struct {
widget.BaseWidget
CloseIntercept func() `json:"-"`
OnDragged, OnResized func(*fyne.DragEvent) `json:"-"`
OnMinimized, OnMaximized, OnTappedBar, OnTappedIcon func() `json:"-"`
Icon fyne.Resource
// Alignment allows an inner window to specify if the buttons should be on the left
// (`ButtonAlignLeading`) or right of the window border.
//
// Since: 2.6
Alignment widget.ButtonAlign
title string
content *fyne.Container
maximized bool
}
// NewInnerWindow creates a new window border around the given `content`, displaying the `title` along the top.
// This will behave like a normal contain and will probably want to be added to a `MultipleWindows` parent.
//
// Since: 2.5
func NewInnerWindow(title string, content fyne.CanvasObject) *InnerWindow {
w := &InnerWindow{title: title, content: NewPadded(content)}
w.ExtendBaseWidget(w)
return w
}
func (w *InnerWindow) Close() {
w.Hide()
}
func (w *InnerWindow) CreateRenderer() fyne.WidgetRenderer {
w.ExtendBaseWidget(w)
th := w.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
min := newBorderButton(theme.WindowMinimizeIcon(), modeMinimize, th, w.OnMinimized)
if w.OnMinimized == nil {
min.Disable()
}
max := newBorderButton(theme.WindowMaximizeIcon(), modeMaximize, th, w.OnMaximized)
if w.OnMaximized == nil {
max.Disable()
}
close := newBorderButton(theme.WindowCloseIcon(), modeClose, th, func() {
if f := w.CloseIntercept; f != nil {
f()
} else {
w.Close()
}
})
buttons := NewCenter(NewHBox(close, min, max))
borderIcon := newBorderButton(w.Icon, modeIcon, th, func() {
if f := w.OnTappedIcon; f != nil {
f()
}
})
if w.OnTappedIcon == nil {
borderIcon.Disable()
}
if w.Icon == nil {
borderIcon.Hide()
}
title := newDraggableLabel(w.title, w)
title.Truncation = fyne.TextTruncateEllipsis
height := w.Theme().Size(theme.SizeNameWindowTitleBarHeight)
off := (height - title.labelMinSize().Height) / 2
barMid := New(layout.NewCustomPaddedLayout(off, 0, 0, 0), title)
if w.buttonPosition() == widget.ButtonAlignTrailing {
buttons = NewCenter(NewHBox(min, max, close))
}
bg := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v))
contentBG := canvas.NewRectangle(th.Color(theme.ColorNameBackground, v))
corner := newDraggableCorner(w)
bar := New(&titleBarLayout{buttons: buttons, icon: borderIcon, title: barMid, win: w},
buttons, borderIcon, barMid)
if w.content == nil {
w.content = NewPadded(canvas.NewRectangle(color.Transparent))
}
objects := []fyne.CanvasObject{bg, contentBG, bar, w.content, corner}
r := &innerWindowRenderer{ShadowingRenderer: intWidget.NewShadowingRenderer(objects, intWidget.DialogLevel),
win: w, bar: bar, buttonBox: buttons, buttons: []*borderButton{close, min, max}, bg: bg,
corner: corner, contentBG: contentBG, icon: borderIcon}
r.Layout(w.Size())
return r
}
func (w *InnerWindow) SetContent(obj fyne.CanvasObject) {
w.content.Objects[0] = obj
w.content.Refresh()
}
// SetMaximized tells the window if the maximized state should be set or not.
//
// Since: 2.6
func (w *InnerWindow) SetMaximized(max bool) {
w.maximized = max
w.Refresh()
}
func (w *InnerWindow) SetPadded(pad bool) {
if pad {
w.content.Layout = layout.NewPaddedLayout()
} else {
w.content.Layout = layout.NewStackLayout()
}
w.content.Refresh()
}
func (w *InnerWindow) SetTitle(title string) {
w.title = title
w.Refresh()
}
func (w *InnerWindow) buttonPosition() widget.ButtonAlign {
if w.Alignment != widget.ButtonAlignCenter {
return w.Alignment
}
if runtime.GOOS == "windows" || runtime.GOOS == "linux" || strings.Contains(runtime.GOOS, "bsd") {
return widget.ButtonAlignTrailing
}
// macOS
return widget.ButtonAlignLeading
}
var _ fyne.WidgetRenderer = (*innerWindowRenderer)(nil)
type innerWindowRenderer struct {
*intWidget.ShadowingRenderer
win *InnerWindow
bar, buttonBox *fyne.Container
buttons []*borderButton
icon *borderButton
bg, contentBG *canvas.Rectangle
corner fyne.CanvasObject
}
func (i *innerWindowRenderer) Layout(size fyne.Size) {
th := i.win.Theme()
pad := th.Size(theme.SizeNamePadding)
i.LayoutShadow(size, fyne.Position{})
i.bg.Resize(size)
barHeight := i.win.Theme().Size(theme.SizeNameWindowTitleBarHeight)
i.bar.Move(fyne.NewPos(pad, 0))
i.bar.Resize(fyne.NewSize(size.Width-pad*2, barHeight))
innerPos := fyne.NewPos(pad, barHeight)
innerSize := fyne.NewSize(size.Width-pad*2, size.Height-pad-barHeight)
i.contentBG.Move(innerPos)
i.contentBG.Resize(innerSize)
i.win.content.Move(innerPos)
i.win.content.Resize(innerSize)
cornerSize := i.corner.MinSize()
i.corner.Move(fyne.NewPos(size.Components()).Subtract(cornerSize).AddXY(1, 1))
i.corner.Resize(cornerSize)
}
func (i *innerWindowRenderer) MinSize() fyne.Size {
th := i.win.Theme()
pad := th.Size(theme.SizeNamePadding)
contentMin := i.win.content.MinSize()
barHeight := th.Size(theme.SizeNameWindowTitleBarHeight)
innerWidth := fyne.Max(i.bar.MinSize().Width, contentMin.Width)
return fyne.NewSize(innerWidth+pad*2, contentMin.Height+pad+barHeight)
}
func (i *innerWindowRenderer) Refresh() {
th := i.win.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
i.bg.FillColor = th.Color(theme.ColorNameOverlayBackground, v)
i.bg.Refresh()
i.contentBG.FillColor = th.Color(theme.ColorNameBackground, v)
i.contentBG.Refresh()
if i.win.buttonPosition() == widget.ButtonAlignTrailing {
i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[1], i.buttons[2], i.buttons[0]}
} else {
i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[0], i.buttons[1], i.buttons[2]}
}
for _, b := range i.buttons {
b.setTheme(th)
}
i.bar.Refresh()
if i.win.OnMinimized == nil {
i.buttons[1].Disable()
} else {
i.buttons[1].SetOnTapped(i.win.OnMinimized)
i.buttons[1].Enable()
}
max := i.buttons[2]
if i.win.OnMaximized == nil {
i.buttons[2].Disable()
} else {
max.SetOnTapped(i.win.OnMaximized)
max.Enable()
}
if i.win.maximized {
max.b.SetIcon(theme.ViewRestoreIcon())
} else {
max.b.SetIcon(theme.WindowMaximizeIcon())
}
title := i.bar.Objects[2].(*fyne.Container).Objects[0].(*draggableLabel)
title.SetText(i.win.title)
i.ShadowingRenderer.RefreshShadow()
if i.win.OnTappedIcon == nil {
i.icon.Disable()
} else {
i.icon.Enable()
}
if i.win.Icon != nil {
i.icon.b.SetIcon(i.win.Icon)
i.icon.Show()
} else {
i.icon.Hide()
}
}
type draggableLabel struct {
widget.Label
win *InnerWindow
}
func newDraggableLabel(title string, win *InnerWindow) *draggableLabel {
d := &draggableLabel{win: win}
d.ExtendBaseWidget(d)
d.Text = title
return d
}
func (d *draggableLabel) Dragged(ev *fyne.DragEvent) {
if f := d.win.OnDragged; f != nil {
f(ev)
}
}
func (d *draggableLabel) DragEnd() {
}
func (d *draggableLabel) MinSize() fyne.Size {
width := d.Label.MinSize().Width
height := d.Label.Theme().Size(theme.SizeNameWindowButtonHeight)
return fyne.NewSize(width, height)
}
func (d *draggableLabel) Tapped(_ *fyne.PointEvent) {
if f := d.win.OnTappedBar; f != nil {
f()
}
}
func (d *draggableLabel) labelMinSize() fyne.Size {
return d.Label.MinSize()
}
type draggableCorner struct {
widget.BaseWidget
win *InnerWindow
}
func newDraggableCorner(w *InnerWindow) *draggableCorner {
d := &draggableCorner{win: w}
d.ExtendBaseWidget(d)
return d
}
func (c *draggableCorner) CreateRenderer() fyne.WidgetRenderer {
prop := canvas.NewImageFromResource(fyne.CurrentApp().Settings().Theme().Icon(theme.IconNameDragCornerIndicator))
prop.SetMinSize(fyne.NewSquareSize(16))
return widget.NewSimpleRenderer(prop)
}
func (c *draggableCorner) Dragged(ev *fyne.DragEvent) {
if f := c.win.OnResized; f != nil {
c.win.OnResized(ev)
}
}
func (c *draggableCorner) DragEnd() {
}
type borderButton struct {
widget.BaseWidget
b *widget.Button
c *ThemeOverride
mode titleBarButtonMode
}
func newBorderButton(icon fyne.Resource, mode titleBarButtonMode, th fyne.Theme, fn func()) *borderButton {
buttonImportance := widget.MediumImportance
if mode == modeIcon {
buttonImportance = widget.LowImportance
}
b := &widget.Button{Icon: icon, Importance: buttonImportance, OnTapped: fn}
c := NewThemeOverride(b, &buttonTheme{Theme: th, mode: mode})
ret := &borderButton{b: b, c: c, mode: mode}
ret.ExtendBaseWidget(ret)
return ret
}
func (b *borderButton) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(b.c)
}
func (b *borderButton) Disable() {
b.b.Disable()
}
func (b *borderButton) Enable() {
b.b.Enable()
}
func (b *borderButton) SetOnTapped(fn func()) {
b.b.OnTapped = fn
}
func (b *borderButton) MinSize() fyne.Size {
height := b.Theme().Size(theme.SizeNameWindowButtonHeight)
return fyne.NewSquareSize(height)
}
func (b *borderButton) setTheme(th fyne.Theme) {
b.c.Theme = &buttonTheme{Theme: th, mode: b.mode}
}
type buttonTheme struct {
fyne.Theme
mode titleBarButtonMode
}
func (b *buttonTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
switch n {
case theme.ColorNameHover:
if b.mode == modeClose {
n = theme.ColorNameError
}
}
return b.Theme.Color(n, v)
}
func (b *buttonTheme) Size(n fyne.ThemeSizeName) float32 {
switch n {
case theme.SizeNameInputRadius:
if b.mode == modeIcon {
return 0
}
n = theme.SizeNameWindowButtonRadius
case theme.SizeNameInlineIcon:
n = theme.SizeNameWindowButtonIcon
}
return b.Theme.Size(n)
}
type titleBarLayout struct {
win *InnerWindow
buttons, icon, title fyne.CanvasObject
}
func (t *titleBarLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
buttonMinWidth := t.buttons.MinSize().Width
t.buttons.Resize(fyne.NewSize(buttonMinWidth, s.Height))
t.icon.Resize(fyne.NewSquareSize(s.Height))
usedWidth := buttonMinWidth
if t.icon.Visible() {
usedWidth += s.Height
}
t.title.Resize(fyne.NewSize(s.Width-usedWidth, s.Height))
if t.win.buttonPosition() == widget.ButtonAlignTrailing {
t.buttons.Move(fyne.NewPos(s.Width-buttonMinWidth, 0))
t.icon.Move(fyne.Position{})
if t.icon.Visible() {
t.title.Move(fyne.NewPos(s.Height, 0))
} else {
t.title.Move(fyne.Position{})
}
} else {
t.buttons.Move(fyne.NewPos(0, 0))
t.icon.Move(fyne.NewPos(s.Width-s.Height, 0))
t.title.Move(fyne.NewPos(buttonMinWidth, 0))
}
}
func (t *titleBarLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
buttonMin := t.buttons.MinSize()
iconMin := t.icon.MinSize()
titleMin := t.title.MinSize() // can truncate
return fyne.NewSize(buttonMin.Width+iconMin.Width+titleMin.Width,
fyne.Max(fyne.Max(buttonMin.Height, iconMin.Height), titleMin.Height))
}

124
vendor/fyne.io/fyne/v2/container/layouts.go generated vendored Normal file
View File

@ -0,0 +1,124 @@
package container // import "fyne.io/fyne/v2/container"
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal"
"fyne.io/fyne/v2/layout"
)
// NewAdaptiveGrid creates a new container with the specified objects and using the grid layout.
// When in a horizontal arrangement the rowcols parameter will specify the column count, when in vertical
// it will specify the rows. On mobile this will dynamically refresh when device is rotated.
//
// Since: 1.4
func NewAdaptiveGrid(rowcols int, objects ...fyne.CanvasObject) *fyne.Container {
return New(layout.NewAdaptiveGridLayout(rowcols), objects...)
}
// NewBorder creates a new container with the specified objects and using the border layout.
// The top, bottom, left and right parameters specify the items that should be placed around edges.
// Nil can be used to an edge if it should not be filled.
// Passed objects not assigned to any edge (parameters 5 onwards) will be used to fill the space
// remaining in the middle.
// Parameters 6 onwards will be stacked over the middle content in the specified order as a Stack container.
//
// Since: 1.4
func NewBorder(top, bottom, left, right fyne.CanvasObject, objects ...fyne.CanvasObject) *fyne.Container {
all := objects
if top != nil {
all = append(all, top)
}
if bottom != nil {
all = append(all, bottom)
}
if left != nil {
all = append(all, left)
}
if right != nil {
all = append(all, right)
}
if len(objects) == 1 && objects[0] == nil {
internal.LogHint("Border layout requires only 4 parameters, optional items cannot be nil")
all = all[1:]
}
return New(layout.NewBorderLayout(top, bottom, left, right), all...)
}
// NewCenter creates a new container with the specified objects centered in the available space.
//
// Since: 1.4
func NewCenter(objects ...fyne.CanvasObject) *fyne.Container {
return New(layout.NewCenterLayout(), objects...)
}
// NewGridWithColumns creates a new container with the specified objects and using the grid layout with
// a specified number of columns. The number of rows will depend on how many children are in the container.
//
// Since: 1.4
func NewGridWithColumns(cols int, objects ...fyne.CanvasObject) *fyne.Container {
return New(layout.NewGridLayoutWithColumns(cols), objects...)
}
// NewGridWithRows creates a new container with the specified objects and using the grid layout with
// a specified number of rows. The number of columns will depend on how many children are in the container.
//
// Since: 1.4
func NewGridWithRows(rows int, objects ...fyne.CanvasObject) *fyne.Container {
return New(layout.NewGridLayoutWithRows(rows), objects...)
}
// NewGridWrap creates a new container with the specified objects and using the gridwrap layout.
// Every element will be resized to the size parameter and the content will arrange along a row and flow to a
// new row if the elements don't fit.
//
// Since: 1.4
func NewGridWrap(size fyne.Size, objects ...fyne.CanvasObject) *fyne.Container {
return New(layout.NewGridWrapLayout(size), objects...)
}
// NewHBox creates a new container with the specified objects and using the HBox layout.
// The objects will be placed in the container from left to right and always displayed
// at their horizontal MinSize. Use a different layout if the objects are intended
// to be larger then their horizontal MinSize.
//
// Since: 1.4
func NewHBox(objects ...fyne.CanvasObject) *fyne.Container {
return New(layout.NewHBoxLayout(), objects...)
}
// NewMax creates a new container with the specified objects filling the available space.
//
// Since: 1.4
//
// Deprecated: Use container.NewStack() instead.
func NewMax(objects ...fyne.CanvasObject) *fyne.Container {
return NewStack(objects...)
}
// NewPadded creates a new container with the specified objects inset by standard padding size.
//
// Since: 1.4
func NewPadded(objects ...fyne.CanvasObject) *fyne.Container {
return New(layout.NewPaddedLayout(), objects...)
}
// NewStack returns a new container that stacks objects on top of each other.
// Objects at the end of the container will be stacked on top of objects before.
// Having only a single object has no impact as CanvasObjects will
// fill the available space even without a Stack.
//
// Since: 2.4
func NewStack(objects ...fyne.CanvasObject) *fyne.Container {
return New(layout.NewStackLayout(), objects...)
}
// NewVBox creates a new container with the specified objects and using the VBox layout.
// The objects will be stacked in the container from top to bottom and always displayed
// at their vertical MinSize. Use a different layout if the objects are intended
// to be larger then their vertical MinSize.
//
// Since: 1.4
func NewVBox(objects ...fyne.CanvasObject) *fyne.Container {
return New(layout.NewVBoxLayout(), objects...)
}

105
vendor/fyne.io/fyne/v2/container/multiplewindows.go generated vendored Normal file
View File

@ -0,0 +1,105 @@
package container
import (
"fyne.io/fyne/v2"
intWidget "fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/widget"
)
// MultipleWindows is a container that handles multiple `InnerWindow` containers.
// Each inner window can be dragged, resized and the stacking will change when the title bar is tapped.
//
// Since: 2.5
type MultipleWindows struct {
widget.BaseWidget
Windows []*InnerWindow
content *fyne.Container
}
// NewMultipleWindows creates a new `MultipleWindows` container to manage many inner windows.
// The initial window list is passed optionally to this constructor function.
// You can add new more windows to this container by calling `Add` or updating the `Windows`
// field and calling `Refresh`.
//
// Since: 2.5
func NewMultipleWindows(wins ...*InnerWindow) *MultipleWindows {
m := &MultipleWindows{Windows: wins}
m.ExtendBaseWidget(m)
return m
}
func (m *MultipleWindows) Add(w *InnerWindow) {
m.Windows = append(m.Windows, w)
m.refreshChildren()
}
func (m *MultipleWindows) CreateRenderer() fyne.WidgetRenderer {
m.content = New(&multiWinLayout{})
m.refreshChildren()
return widget.NewSimpleRenderer(intWidget.NewScroll(m.content))
}
func (m *MultipleWindows) Refresh() {
m.refreshChildren()
// m.BaseWidget.Refresh()
}
func (m *MultipleWindows) raise(w *InnerWindow) {
id := -1
for i, ww := range m.Windows {
if ww == w {
id = i
break
}
}
if id == -1 {
return
}
windows := append(m.Windows[:id], m.Windows[id+1:]...)
m.Windows = append(windows, w)
m.refreshChildren()
}
func (m *MultipleWindows) refreshChildren() {
if m.content == nil {
return
}
objs := make([]fyne.CanvasObject, len(m.Windows))
for i, w := range m.Windows {
objs[i] = w
m.setupChild(w)
}
m.content.Objects = objs
m.content.Refresh()
}
func (m *MultipleWindows) setupChild(w *InnerWindow) {
w.OnDragged = func(ev *fyne.DragEvent) {
w.Move(w.Position().Add(ev.Dragged))
}
w.OnResized = func(ev *fyne.DragEvent) {
size := w.Size().Add(ev.Dragged)
w.Resize(size.Max(w.MinSize()))
}
w.OnTappedBar = func() {
m.raise(w)
}
}
type multiWinLayout struct {
}
func (m *multiWinLayout) Layout(objects []fyne.CanvasObject, _ fyne.Size) {
for _, w := range objects { // update the windows so they have real size
w.Resize(w.MinSize().Max(w.Size()))
}
}
func (m *multiWinLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
return fyne.Size{}
}

55
vendor/fyne.io/fyne/v2/container/scroll.go generated vendored Normal file
View File

@ -0,0 +1,55 @@
package container
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/widget"
)
// Scroll defines a container that is smaller than the Content.
// The Offset is used to determine the position of the child widgets within the container.
//
// Since: 1.4
type Scroll = widget.Scroll
// ScrollDirection represents the directions in which a Scroll container can scroll its child content.
//
// Since: 1.4
type ScrollDirection = fyne.ScrollDirection
// Constants for valid values of ScrollDirection.
const (
// ScrollBoth supports horizontal and vertical scrolling.
ScrollBoth ScrollDirection = fyne.ScrollBoth
// ScrollHorizontalOnly specifies the scrolling should only happen left to right.
ScrollHorizontalOnly = fyne.ScrollHorizontalOnly
// ScrollVerticalOnly specifies the scrolling should only happen top to bottom.
ScrollVerticalOnly = fyne.ScrollVerticalOnly
// ScrollNone turns off scrolling for this container.
//
// Since: 2.1
ScrollNone = fyne.ScrollNone
)
// NewScroll creates a scrollable parent wrapping the specified content.
// Note that this may cause the MinSize to be smaller than that of the passed object.
//
// Since: 1.4
func NewScroll(content fyne.CanvasObject) *Scroll {
return widget.NewScroll(content)
}
// NewHScroll create a scrollable parent wrapping the specified content.
// Note that this may cause the MinSize.Width to be smaller than that of the passed object.
//
// Since: 1.4
func NewHScroll(content fyne.CanvasObject) *Scroll {
return widget.NewHScroll(content)
}
// NewVScroll a scrollable parent wrapping the specified content.
// Note that this may cause the MinSize.Height to be smaller than that of the passed object.
//
// Since: 1.4
func NewVScroll(content fyne.CanvasObject) *Scroll {
return widget.NewVScroll(content)
}

404
vendor/fyne.io/fyne/v2/container/split.go generated vendored Normal file
View File

@ -0,0 +1,404 @@
package container
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// Declare conformity with CanvasObject interface
var _ fyne.CanvasObject = (*Split)(nil)
// Split defines a container whose size is split between two children.
//
// Since: 1.4
type Split struct {
widget.BaseWidget
Offset float64
Horizontal bool
Leading fyne.CanvasObject
Trailing fyne.CanvasObject
// to communicate to the renderer that the next refresh
// is just an offset update (ie a resize and move only)
// cleared by renderer in Refresh()
offsetUpdated bool
}
// NewHSplit creates a horizontally arranged container with the specified leading and trailing elements.
// A vertical split bar that can be dragged will be added between the elements.
//
// Since: 1.4
func NewHSplit(leading, trailing fyne.CanvasObject) *Split {
return newSplitContainer(true, leading, trailing)
}
// NewVSplit creates a vertically arranged container with the specified top and bottom elements.
// A horizontal split bar that can be dragged will be added between the elements.
//
// Since: 1.4
func NewVSplit(top, bottom fyne.CanvasObject) *Split {
return newSplitContainer(false, top, bottom)
}
func newSplitContainer(horizontal bool, leading, trailing fyne.CanvasObject) *Split {
s := &Split{
Offset: 0.5, // Sensible default, can be overridden with SetOffset
Horizontal: horizontal,
Leading: leading,
Trailing: trailing,
}
s.BaseWidget.ExtendBaseWidget(s)
return s
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (s *Split) CreateRenderer() fyne.WidgetRenderer {
s.BaseWidget.ExtendBaseWidget(s)
d := newDivider(s)
return &splitContainerRenderer{
split: s,
divider: d,
objects: []fyne.CanvasObject{s.Leading, d, s.Trailing},
}
}
// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
//
// Deprecated: Support for extending containers is being removed
func (s *Split) ExtendBaseWidget(wid fyne.Widget) {
s.BaseWidget.ExtendBaseWidget(wid)
}
// SetOffset sets the offset (0.0 to 1.0) of the Split divider.
// 0.0 - Leading is min size, Trailing uses all remaining space.
// 0.5 - Leading & Trailing equally share the available space.
// 1.0 - Trailing is min size, Leading uses all remaining space.
func (s *Split) SetOffset(offset float64) {
if s.Offset == offset {
return
}
s.Offset = offset
s.offsetUpdated = true
s.Refresh()
}
var _ fyne.WidgetRenderer = (*splitContainerRenderer)(nil)
type splitContainerRenderer struct {
split *Split
divider *divider
objects []fyne.CanvasObject
}
func (r *splitContainerRenderer) Destroy() {
}
func (r *splitContainerRenderer) Layout(size fyne.Size) {
var dividerPos, leadingPos, trailingPos fyne.Position
var dividerSize, leadingSize, trailingSize fyne.Size
if r.split.Horizontal {
lw, tw := r.computeSplitLengths(size.Width, r.minLeadingWidth(), r.minTrailingWidth())
leadingPos.X = 0
leadingSize.Width = lw
leadingSize.Height = size.Height
dividerPos.X = lw
dividerSize.Width = dividerThickness(r.divider)
dividerSize.Height = size.Height
trailingPos.X = lw + dividerSize.Width
trailingSize.Width = tw
trailingSize.Height = size.Height
} else {
lh, th := r.computeSplitLengths(size.Height, r.minLeadingHeight(), r.minTrailingHeight())
leadingPos.Y = 0
leadingSize.Width = size.Width
leadingSize.Height = lh
dividerPos.Y = lh
dividerSize.Width = size.Width
dividerSize.Height = dividerThickness(r.divider)
trailingPos.Y = lh + dividerSize.Height
trailingSize.Width = size.Width
trailingSize.Height = th
}
r.divider.Move(dividerPos)
r.divider.Resize(dividerSize)
r.split.Leading.Move(leadingPos)
r.split.Leading.Resize(leadingSize)
r.split.Trailing.Move(trailingPos)
r.split.Trailing.Resize(trailingSize)
canvas.Refresh(r.divider)
}
func (r *splitContainerRenderer) MinSize() fyne.Size {
s := fyne.NewSize(0, 0)
for _, o := range r.objects {
min := o.MinSize()
if r.split.Horizontal {
s.Width += min.Width
s.Height = fyne.Max(s.Height, min.Height)
} else {
s.Width = fyne.Max(s.Width, min.Width)
s.Height += min.Height
}
}
return s
}
func (r *splitContainerRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *splitContainerRenderer) Refresh() {
if r.split.offsetUpdated {
r.Layout(r.split.Size())
r.split.offsetUpdated = false
return
}
r.objects[0] = r.split.Leading
// [1] is divider which doesn't change
r.objects[2] = r.split.Trailing
r.Layout(r.split.Size())
r.split.Leading.Refresh()
r.divider.Refresh()
r.split.Trailing.Refresh()
canvas.Refresh(r.split)
}
func (r *splitContainerRenderer) computeSplitLengths(total, lMin, tMin float32) (float32, float32) {
available := float64(total - dividerThickness(r.divider))
if available <= 0 {
return 0, 0
}
ld := float64(lMin)
tr := float64(tMin)
offset := r.split.Offset
min := ld / available
max := 1 - tr/available
if min <= max {
if offset < min {
offset = min
}
if offset > max {
offset = max
}
} else {
offset = ld / (ld + tr)
}
ld = offset * available
tr = available - ld
return float32(ld), float32(tr)
}
func (r *splitContainerRenderer) minLeadingWidth() float32 {
if r.split.Leading.Visible() {
return r.split.Leading.MinSize().Width
}
return 0
}
func (r *splitContainerRenderer) minLeadingHeight() float32 {
if r.split.Leading.Visible() {
return r.split.Leading.MinSize().Height
}
return 0
}
func (r *splitContainerRenderer) minTrailingWidth() float32 {
if r.split.Trailing.Visible() {
return r.split.Trailing.MinSize().Width
}
return 0
}
func (r *splitContainerRenderer) minTrailingHeight() float32 {
if r.split.Trailing.Visible() {
return r.split.Trailing.MinSize().Height
}
return 0
}
// Declare conformity with interfaces
var _ fyne.CanvasObject = (*divider)(nil)
var _ fyne.Draggable = (*divider)(nil)
var _ desktop.Cursorable = (*divider)(nil)
var _ desktop.Hoverable = (*divider)(nil)
type divider struct {
widget.BaseWidget
split *Split
hovered bool
startDragOff *fyne.Position
currentDragPos fyne.Position
}
func newDivider(split *Split) *divider {
d := &divider{
split: split,
}
d.ExtendBaseWidget(d)
return d
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (d *divider) CreateRenderer() fyne.WidgetRenderer {
d.ExtendBaseWidget(d)
th := d.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
background := canvas.NewRectangle(th.Color(theme.ColorNameShadow, v))
foreground := canvas.NewRectangle(th.Color(theme.ColorNameForeground, v))
return &dividerRenderer{
divider: d,
background: background,
foreground: foreground,
objects: []fyne.CanvasObject{background, foreground},
}
}
func (d *divider) Cursor() desktop.Cursor {
if d.split.Horizontal {
return desktop.HResizeCursor
}
return desktop.VResizeCursor
}
func (d *divider) DragEnd() {
d.startDragOff = nil
}
func (d *divider) Dragged(e *fyne.DragEvent) {
if d.startDragOff == nil {
d.currentDragPos = d.Position().Add(e.Position)
start := e.Position.Subtract(e.Dragged)
d.startDragOff = &start
} else {
d.currentDragPos = d.currentDragPos.Add(e.Dragged)
}
x, y := d.currentDragPos.Components()
var offset, leadingRatio, trailingRatio float64
if d.split.Horizontal {
widthFree := float64(d.split.Size().Width - dividerThickness(d))
leadingRatio = float64(d.split.Leading.MinSize().Width) / widthFree
trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Width) / widthFree)
offset = float64(x-d.startDragOff.X) / widthFree
} else {
heightFree := float64(d.split.Size().Height - dividerThickness(d))
leadingRatio = float64(d.split.Leading.MinSize().Height) / heightFree
trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Height) / heightFree)
offset = float64(y-d.startDragOff.Y) / heightFree
}
if offset < leadingRatio {
offset = leadingRatio
}
if offset > trailingRatio {
offset = trailingRatio
}
d.split.SetOffset(offset)
}
func (d *divider) MouseIn(event *desktop.MouseEvent) {
d.hovered = true
d.split.Refresh()
}
func (d *divider) MouseMoved(event *desktop.MouseEvent) {}
func (d *divider) MouseOut() {
d.hovered = false
d.split.Refresh()
}
var _ fyne.WidgetRenderer = (*dividerRenderer)(nil)
type dividerRenderer struct {
divider *divider
background *canvas.Rectangle
foreground *canvas.Rectangle
objects []fyne.CanvasObject
}
func (r *dividerRenderer) Destroy() {
}
func (r *dividerRenderer) Layout(size fyne.Size) {
r.background.Resize(size)
var x, y, w, h float32
if r.divider.split.Horizontal {
x = (dividerThickness(r.divider) - handleThickness(r.divider)) / 2
y = (size.Height - handleLength(r.divider)) / 2
w = handleThickness(r.divider)
h = handleLength(r.divider)
} else {
x = (size.Width - handleLength(r.divider)) / 2
y = (dividerThickness(r.divider) - handleThickness(r.divider)) / 2
w = handleLength(r.divider)
h = handleThickness(r.divider)
}
r.foreground.Move(fyne.NewPos(x, y))
r.foreground.Resize(fyne.NewSize(w, h))
}
func (r *dividerRenderer) MinSize() fyne.Size {
if r.divider.split.Horizontal {
return fyne.NewSize(dividerThickness(r.divider), dividerLength(r.divider))
}
return fyne.NewSize(dividerLength(r.divider), dividerThickness(r.divider))
}
func (r *dividerRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *dividerRenderer) Refresh() {
th := r.divider.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
if r.divider.hovered {
r.background.FillColor = th.Color(theme.ColorNameHover, v)
} else {
r.background.FillColor = th.Color(theme.ColorNameShadow, v)
}
r.background.Refresh()
r.foreground.FillColor = th.Color(theme.ColorNameForeground, v)
r.foreground.Refresh()
r.Layout(r.divider.Size())
}
func dividerTheme(d *divider) fyne.Theme {
if d == nil {
return theme.Current()
}
return d.Theme()
}
func dividerThickness(d *divider) float32 {
th := dividerTheme(d)
return th.Size(theme.SizeNamePadding) * 2
}
func dividerLength(d *divider) float32 {
th := dividerTheme(d)
return th.Size(theme.SizeNamePadding) * 6
}
func handleThickness(d *divider) float32 {
th := dividerTheme(d)
return th.Size(theme.SizeNamePadding) / 2
}
func handleLength(d *divider) float32 {
th := dividerTheme(d)
return th.Size(theme.SizeNamePadding) * 4
}

876
vendor/fyne.io/fyne/v2/container/tabs.go generated vendored Normal file
View File

@ -0,0 +1,876 @@
package container
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/internal"
"fyne.io/fyne/v2/internal/build"
intTheme "fyne.io/fyne/v2/internal/theme"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// TabItem represents a single view in a tab view.
// The Text and Icon are used for the tab button and the Content is shown when the corresponding tab is active.
//
// Since: 1.4
type TabItem struct {
Text string
Icon fyne.Resource
Content fyne.CanvasObject
button *tabButton
}
// Disabled returns whether or not the TabItem is disabled.
//
// Since: 2.3
func (ti *TabItem) Disabled() bool {
if ti.button != nil {
return ti.button.Disabled()
}
return false
}
func (ti *TabItem) disable() {
if ti.button != nil {
ti.button.Disable()
}
}
func (ti *TabItem) enable() {
if ti.button != nil {
ti.button.Enable()
}
}
// TabLocation is the location where the tabs of a tab container should be rendered
//
// Since: 1.4
type TabLocation int
// TabLocation values
const (
TabLocationTop TabLocation = iota
TabLocationLeading
TabLocationBottom
TabLocationTrailing
)
// NewTabItem creates a new item for a tabbed widget - each item specifies the content and a label for its tab.
//
// Since: 1.4
func NewTabItem(text string, content fyne.CanvasObject) *TabItem {
return &TabItem{Text: text, Content: content}
}
// NewTabItemWithIcon creates a new item for a tabbed widget - each item specifies the content and a label with an icon for its tab.
//
// Since: 1.4
func NewTabItemWithIcon(text string, icon fyne.Resource, content fyne.CanvasObject) *TabItem {
return &TabItem{Text: text, Icon: icon, Content: content}
}
type baseTabs interface {
fyne.Widget
onUnselected() func(*TabItem)
onSelected() func(*TabItem)
items() []*TabItem
setItems([]*TabItem)
selected() int
setSelected(int)
tabLocation() TabLocation
transitioning() bool
setTransitioning(bool)
}
func isMobile(b baseTabs) bool {
d := fyne.CurrentDevice()
mobile := intTheme.FeatureForWidget(intTheme.FeatureNameDeviceIsMobile, b)
if is, ok := mobile.(bool); ok {
return is
}
return d.IsMobile()
}
func tabsAdjustedLocation(l TabLocation, b baseTabs) TabLocation {
// Mobile has limited screen space, so don't put app tab bar on long edges
if isMobile(b) {
if o := fyne.CurrentDevice().Orientation(); fyne.IsVertical(o) {
if l == TabLocationLeading {
return TabLocationTop
} else if l == TabLocationTrailing {
return TabLocationBottom
}
} else {
if l == TabLocationTop {
return TabLocationLeading
} else if l == TabLocationBottom {
return TabLocationTrailing
}
}
}
return l
}
func buildPopUpMenu(t baseTabs, button *widget.Button, items []*fyne.MenuItem) *widget.PopUpMenu {
d := fyne.CurrentApp().Driver()
c := d.CanvasForObject(button)
popUpMenu := widget.NewPopUpMenu(fyne.NewMenu("", items...), c)
buttonPos := d.AbsolutePositionForObject(button)
buttonSize := button.Size()
popUpMin := popUpMenu.MinSize()
var popUpPos fyne.Position
switch t.tabLocation() {
case TabLocationLeading:
popUpPos.X = buttonPos.X + buttonSize.Width
popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height
case TabLocationTrailing:
popUpPos.X = buttonPos.X - popUpMin.Width
popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height
case TabLocationTop:
popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width
popUpPos.Y = buttonPos.Y + buttonSize.Height
case TabLocationBottom:
popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width
popUpPos.Y = buttonPos.Y - popUpMin.Height
}
if popUpPos.X < 0 {
popUpPos.X = 0
}
if popUpPos.Y < 0 {
popUpPos.Y = 0
}
popUpMenu.ShowAtPosition(popUpPos)
return popUpMenu
}
func removeIndex(t baseTabs, index int) {
items := t.items()
if index < 0 || index >= len(items) {
return
}
setItems(t, append(items[:index], items[index+1:]...))
if s := t.selected(); index < s {
t.setSelected(s - 1)
}
}
func removeItem(t baseTabs, item *TabItem) {
for index, existingItem := range t.items() {
if existingItem == item {
removeIndex(t, index)
break
}
}
}
func selected(t baseTabs) *TabItem {
selected := t.selected()
items := t.items()
if selected < 0 || selected >= len(items) {
return nil
}
return items[selected]
}
func selectIndex(t baseTabs, index int) {
selected := t.selected()
if selected == index {
// No change, so do nothing
return
}
items := t.items()
if f := t.onUnselected(); f != nil && selected >= 0 && selected < len(items) {
// Notification of unselected
f(items[selected])
}
if index < 0 || index >= len(items) {
// Out of bounds, so do nothing
return
}
t.setTransitioning(true)
t.setSelected(index)
t.Refresh()
if f := t.onSelected(); f != nil {
// Notification of selected
f(items[index])
}
}
func selectItem(t baseTabs, item *TabItem) {
for i, child := range t.items() {
if child == item {
selectIndex(t, i)
return
}
}
}
func setItems(t baseTabs, items []*TabItem) {
if build.HasHints && mismatchedTabItems(items) {
internal.LogHint("Tab items should all have the same type of content (text, icons or both)")
}
t.setItems(items)
selected := t.selected()
count := len(items)
switch {
case count == 0:
// No items available to be selected
selectIndex(t, -1) // Unsure OnUnselected gets called if applicable
t.setSelected(-1)
case selected < 0:
// Current is first tab item
selectIndex(t, 0)
case selected >= count:
// Current doesn't exist, select last tab
selectIndex(t, count-1)
}
}
func disableIndex(t baseTabs, index int) {
items := t.items()
if index < 0 || index >= len(items) {
return
}
item := items[index]
item.disable()
if selected(t) == item {
// the disabled tab is currently selected, so select the first enabled tab
for i, it := range items {
if !it.Disabled() {
selectIndex(t, i)
break
}
}
}
if selected(t) == item {
selectIndex(t, -1) // no other tab is able to be selected
}
}
func disableItem(t baseTabs, item *TabItem) {
for i, it := range t.items() {
if it == item {
disableIndex(t, i)
return
}
}
}
func enableIndex(t baseTabs, index int) {
items := t.items()
if index < 0 || index >= len(items) {
return
}
item := items[index]
item.enable()
}
func enableItem(t baseTabs, item *TabItem) {
for i, it := range t.items() {
if it == item {
enableIndex(t, i)
return
}
}
}
type baseTabsRenderer struct {
positionAnimation, sizeAnimation *fyne.Animation
lastIndicatorPos fyne.Position
lastIndicatorSize fyne.Size
lastIndicatorHidden bool
action *widget.Button
bar *fyne.Container
divider, indicator *canvas.Rectangle
tabs baseTabs
}
func (r *baseTabsRenderer) Destroy() {
}
func (r *baseTabsRenderer) applyTheme(t baseTabs) {
if r.action != nil {
r.action.SetIcon(moreIcon(t))
}
th := theme.CurrentForWidget(t)
v := fyne.CurrentApp().Settings().ThemeVariant()
r.divider.FillColor = th.Color(theme.ColorNameShadow, v)
r.indicator.FillColor = th.Color(theme.ColorNamePrimary, v)
r.indicator.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
for _, tab := range r.tabs.items() {
tab.Content.Refresh()
}
}
func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) {
var (
barPos, dividerPos, contentPos fyne.Position
barSize, dividerSize, contentSize fyne.Size
)
barMin := r.bar.MinSize()
th := theme.CurrentForWidget(t)
padding := th.Size(theme.SizeNamePadding)
switch t.tabLocation() {
case TabLocationTop:
barHeight := barMin.Height
barPos = fyne.NewPos(0, 0)
barSize = fyne.NewSize(size.Width, barHeight)
dividerPos = fyne.NewPos(0, barHeight)
dividerSize = fyne.NewSize(size.Width, padding)
contentPos = fyne.NewPos(0, barHeight+padding)
contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
case TabLocationLeading:
barWidth := barMin.Width
barPos = fyne.NewPos(0, 0)
barSize = fyne.NewSize(barWidth, size.Height)
dividerPos = fyne.NewPos(barWidth, 0)
dividerSize = fyne.NewSize(padding, size.Height)
contentPos = fyne.NewPos(barWidth+padding, 0)
contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
case TabLocationBottom:
barHeight := barMin.Height
barPos = fyne.NewPos(0, size.Height-barHeight)
barSize = fyne.NewSize(size.Width, barHeight)
dividerPos = fyne.NewPos(0, size.Height-barHeight-padding)
dividerSize = fyne.NewSize(size.Width, padding)
contentPos = fyne.NewPos(0, 0)
contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
case TabLocationTrailing:
barWidth := barMin.Width
barPos = fyne.NewPos(size.Width-barWidth, 0)
barSize = fyne.NewSize(barWidth, size.Height)
dividerPos = fyne.NewPos(size.Width-barWidth-padding, 0)
dividerSize = fyne.NewSize(padding, size.Height)
contentPos = fyne.NewPos(0, 0)
contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
}
r.bar.Move(barPos)
r.bar.Resize(barSize)
r.divider.Move(dividerPos)
r.divider.Resize(dividerSize)
selected := t.selected()
for i, ti := range t.items() {
if i == selected {
ti.Content.Move(contentPos)
ti.Content.Resize(contentSize)
ti.Content.Show()
} else {
ti.Content.Hide()
}
}
}
func (r *baseTabsRenderer) minSize(t baseTabs) fyne.Size {
th := theme.CurrentForWidget(t)
pad := th.Size(theme.SizeNamePadding)
buttonPad := pad
barMin := r.bar.MinSize()
tabsMin := r.bar.Objects[0].MinSize()
accessory := r.bar.Objects[1]
accessoryMin := accessory.MinSize()
if scroll, ok := r.bar.Objects[0].(*Scroll); ok && len(scroll.Content.(*fyne.Container).Objects) == 0 {
tabsMin = fyne.Size{} // scroller forces 32 where we don't need any space
buttonPad = 0
} else if group, ok := r.bar.Objects[0].(*fyne.Container); ok && len(group.Objects) > 0 {
tabsMin = group.Objects[0].MinSize()
buttonPad = 0
}
if !accessory.Visible() || accessoryMin.Width == 0 {
buttonPad = 0
accessoryMin = fyne.Size{}
}
contentMin := fyne.NewSize(0, 0)
for _, content := range t.items() {
contentMin = contentMin.Max(content.Content.MinSize())
}
switch t.tabLocation() {
case TabLocationLeading, TabLocationTrailing:
return fyne.NewSize(barMin.Width+contentMin.Width+pad,
fyne.Max(contentMin.Height, accessoryMin.Height+buttonPad+tabsMin.Height))
default:
return fyne.NewSize(fyne.Max(contentMin.Width, accessoryMin.Width+buttonPad+tabsMin.Width),
barMin.Height+contentMin.Height+pad)
}
}
func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, th fyne.Theme, animate bool) {
isSameState := r.lastIndicatorPos.Subtract(pos).IsZero() && r.lastIndicatorSize.Subtract(siz).IsZero() &&
r.lastIndicatorHidden == r.indicator.Hidden
if isSameState {
return
}
if r.positionAnimation != nil {
r.positionAnimation.Stop()
r.positionAnimation = nil
}
if r.sizeAnimation != nil {
r.sizeAnimation.Stop()
r.sizeAnimation = nil
}
v := fyne.CurrentApp().Settings().ThemeVariant()
r.indicator.FillColor = th.Color(theme.ColorNamePrimary, v)
if r.indicator.Position().IsZero() {
r.indicator.Move(pos)
r.indicator.Resize(siz)
r.indicator.Refresh()
return
}
r.lastIndicatorPos = pos
r.lastIndicatorSize = siz
r.lastIndicatorHidden = r.indicator.Hidden
if animate && fyne.CurrentApp().Settings().ShowAnimations() {
r.positionAnimation = canvas.NewPositionAnimation(r.indicator.Position(), pos, canvas.DurationShort, func(p fyne.Position) {
r.indicator.Move(p)
r.indicator.Refresh()
if pos == p {
r.positionAnimation.Stop()
r.positionAnimation = nil
}
})
r.sizeAnimation = canvas.NewSizeAnimation(r.indicator.Size(), siz, canvas.DurationShort, func(s fyne.Size) {
r.indicator.Resize(s)
r.indicator.Refresh()
if siz == s {
r.sizeAnimation.Stop()
r.sizeAnimation = nil
}
})
r.positionAnimation.Start()
r.sizeAnimation.Start()
} else {
r.indicator.Move(pos)
r.indicator.Resize(siz)
r.indicator.Refresh()
}
}
func (r *baseTabsRenderer) objects(t baseTabs) []fyne.CanvasObject {
objects := []fyne.CanvasObject{r.bar, r.divider, r.indicator}
if i, is := t.selected(), t.items(); i >= 0 && i < len(is) {
objects = append(objects, is[i].Content)
}
return objects
}
func (r *baseTabsRenderer) refresh(t baseTabs) {
r.applyTheme(t)
r.bar.Refresh()
r.divider.Refresh()
r.indicator.Refresh()
}
type buttonIconPosition int
const (
buttonIconInline buttonIconPosition = iota
buttonIconTop
)
var _ fyne.Widget = (*tabButton)(nil)
var _ fyne.Tappable = (*tabButton)(nil)
var _ desktop.Hoverable = (*tabButton)(nil)
type tabButton struct {
widget.DisableableWidget
hovered bool
icon fyne.Resource
iconPosition buttonIconPosition
importance widget.Importance
onTapped func()
onClosed func()
text string
textAlignment fyne.TextAlign
tabs baseTabs
}
func (b *tabButton) CreateRenderer() fyne.WidgetRenderer {
b.ExtendBaseWidget(b)
th := b.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v))
background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
background.Hide()
icon := canvas.NewImageFromResource(b.icon)
if b.icon == nil {
icon.Hide()
}
label := canvas.NewText(b.text, th.Color(theme.ColorNameForeground, v))
label.TextStyle.Bold = true
close := &tabCloseButton{
parent: b,
onTapped: func() {
if f := b.onClosed; f != nil {
f()
}
},
}
close.ExtendBaseWidget(close)
close.Hide()
objects := []fyne.CanvasObject{background, label, close, icon}
return &tabButtonRenderer{
button: b,
background: background,
icon: icon,
label: label,
close: close,
objects: objects,
}
}
func (b *tabButton) MinSize() fyne.Size {
b.ExtendBaseWidget(b)
return b.BaseWidget.MinSize()
}
func (b *tabButton) MouseIn(*desktop.MouseEvent) {
b.hovered = true
b.Refresh()
}
func (b *tabButton) MouseMoved(*desktop.MouseEvent) {
}
func (b *tabButton) MouseOut() {
b.hovered = false
b.Refresh()
}
func (b *tabButton) Tapped(*fyne.PointEvent) {
if b.Disabled() {
return
}
b.onTapped()
}
type tabButtonRenderer struct {
button *tabButton
background *canvas.Rectangle
icon *canvas.Image
label *canvas.Text
close *tabCloseButton
objects []fyne.CanvasObject
}
func (r *tabButtonRenderer) Destroy() {
}
func (r *tabButtonRenderer) Layout(size fyne.Size) {
th := r.button.Theme()
pad := th.Size(theme.SizeNamePadding)
r.background.Resize(size)
padding := r.padding()
innerSize := size.Subtract(padding)
innerOffset := fyne.NewPos(padding.Width/2, padding.Height/2)
labelShift := float32(0)
if r.icon.Visible() {
iconSize := r.iconSize()
var iconOffset fyne.Position
if r.button.iconPosition == buttonIconTop {
iconOffset = fyne.NewPos((innerSize.Width-iconSize)/2, 0)
} else {
iconOffset = fyne.NewPos(0, (innerSize.Height-iconSize)/2)
}
r.icon.Resize(fyne.NewSquareSize(iconSize))
r.icon.Move(innerOffset.Add(iconOffset))
labelShift = iconSize + pad
}
if r.label.Text != "" {
var labelOffset fyne.Position
var labelSize fyne.Size
if r.button.iconPosition == buttonIconTop {
labelOffset = fyne.NewPos(0, labelShift)
labelSize = fyne.NewSize(innerSize.Width, r.label.MinSize().Height)
} else {
labelOffset = fyne.NewPos(labelShift, 0)
labelSize = fyne.NewSize(innerSize.Width-labelShift, innerSize.Height)
}
r.label.Resize(labelSize)
r.label.Move(innerOffset.Add(labelOffset))
}
inlineIconSize := th.Size(theme.SizeNameInlineIcon)
r.close.Move(fyne.NewPos(size.Width-inlineIconSize-pad, (size.Height-inlineIconSize)/2))
r.close.Resize(fyne.NewSquareSize(inlineIconSize))
}
func (r *tabButtonRenderer) MinSize() fyne.Size {
th := r.button.Theme()
var contentWidth, contentHeight float32
textSize := r.label.MinSize()
iconSize := r.iconSize()
padding := th.Size(theme.SizeNamePadding)
if r.button.iconPosition == buttonIconTop {
contentWidth = fyne.Max(textSize.Width, iconSize)
if r.icon.Visible() {
contentHeight += iconSize
}
if r.label.Text != "" {
if r.icon.Visible() {
contentHeight += padding
}
contentHeight += textSize.Height
}
} else {
contentHeight = fyne.Max(textSize.Height, iconSize)
if r.icon.Visible() {
contentWidth += iconSize
}
if r.label.Text != "" {
if r.icon.Visible() {
contentWidth += padding
}
contentWidth += textSize.Width
}
}
if r.button.onClosed != nil {
inlineIconSize := th.Size(theme.SizeNameInlineIcon)
contentWidth += inlineIconSize + padding
contentHeight = fyne.Max(contentHeight, inlineIconSize)
}
return fyne.NewSize(contentWidth, contentHeight).Add(r.padding())
}
func (r *tabButtonRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *tabButtonRenderer) Refresh() {
th := r.button.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
if r.button.hovered && !r.button.Disabled() {
r.background.FillColor = th.Color(theme.ColorNameHover, v)
r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
r.background.Show()
} else {
r.background.Hide()
}
r.background.Refresh()
r.label.Text = r.button.text
r.label.Alignment = r.button.textAlignment
if !r.button.Disabled() {
if r.button.importance == widget.HighImportance {
r.label.Color = th.Color(theme.ColorNamePrimary, v)
} else {
r.label.Color = th.Color(theme.ColorNameForeground, v)
}
} else {
r.label.Color = th.Color(theme.ColorNameDisabled, v)
}
r.label.TextSize = th.Size(theme.SizeNameText)
if r.button.text == "" {
r.label.Hide()
} else {
r.label.Show()
}
r.icon.Resource = r.button.icon
if r.icon.Resource != nil {
r.icon.Show()
switch res := r.icon.Resource.(type) {
case *theme.ThemedResource:
if r.button.importance == widget.HighImportance {
r.icon.Resource = theme.NewPrimaryThemedResource(res)
}
case *theme.PrimaryThemedResource:
if r.button.importance != widget.HighImportance {
r.icon.Resource = res.Original()
}
}
r.icon.Refresh()
} else {
r.icon.Hide()
}
if r.button.onClosed != nil && (isMobile(r.button.tabs) || r.button.hovered || r.close.hovered) {
r.close.Show()
} else {
r.close.Hide()
}
r.close.Refresh()
canvas.Refresh(r.button)
}
func (r *tabButtonRenderer) iconSize() float32 {
iconSize := r.button.Theme().Size(theme.SizeNameInlineIcon)
if r.button.iconPosition == buttonIconTop {
return 2 * iconSize
}
return iconSize
}
func (r *tabButtonRenderer) padding() fyne.Size {
padding := r.button.Theme().Size(theme.SizeNameInnerPadding)
if r.label.Text != "" && r.button.iconPosition == buttonIconInline {
return fyne.NewSquareSize(padding * 2)
}
return fyne.NewSize(padding, padding*2)
}
var _ fyne.Widget = (*tabCloseButton)(nil)
var _ fyne.Tappable = (*tabCloseButton)(nil)
var _ desktop.Hoverable = (*tabCloseButton)(nil)
type tabCloseButton struct {
widget.BaseWidget
parent *tabButton
hovered bool
onTapped func()
}
func (b *tabCloseButton) CreateRenderer() fyne.WidgetRenderer {
b.ExtendBaseWidget(b)
th := b.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v))
background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
background.Hide()
icon := canvas.NewImageFromResource(theme.CancelIcon())
return &tabCloseButtonRenderer{
button: b,
background: background,
icon: icon,
objects: []fyne.CanvasObject{background, icon},
}
}
func (b *tabCloseButton) MinSize() fyne.Size {
b.ExtendBaseWidget(b)
return b.BaseWidget.MinSize()
}
func (b *tabCloseButton) MouseIn(*desktop.MouseEvent) {
b.hovered = true
b.parent.Refresh()
}
func (b *tabCloseButton) MouseMoved(*desktop.MouseEvent) {
}
func (b *tabCloseButton) MouseOut() {
b.hovered = false
b.parent.Refresh()
}
func (b *tabCloseButton) Tapped(*fyne.PointEvent) {
b.onTapped()
}
type tabCloseButtonRenderer struct {
button *tabCloseButton
background *canvas.Rectangle
icon *canvas.Image
objects []fyne.CanvasObject
}
func (r *tabCloseButtonRenderer) Destroy() {
}
func (r *tabCloseButtonRenderer) Layout(size fyne.Size) {
r.background.Resize(size)
r.icon.Resize(size)
}
func (r *tabCloseButtonRenderer) MinSize() fyne.Size {
return fyne.NewSquareSize(r.button.Theme().Size(theme.SizeNameInlineIcon))
}
func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *tabCloseButtonRenderer) Refresh() {
th := r.button.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
if r.button.hovered {
r.background.FillColor = th.Color(theme.ColorNameHover, v)
r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
r.background.Show()
} else {
r.background.Hide()
}
r.background.Refresh()
switch res := r.icon.Resource.(type) {
case *theme.ThemedResource:
if r.button.parent.importance == widget.HighImportance {
r.icon.Resource = theme.NewPrimaryThemedResource(res)
}
case *theme.PrimaryThemedResource:
if r.button.parent.importance != widget.HighImportance {
r.icon.Resource = res.Original()
}
}
r.icon.Refresh()
}
func mismatchedTabItems(items []*TabItem) bool {
var hasText, hasIcon bool
for _, tab := range items {
hasText = hasText || tab.Text != ""
hasIcon = hasIcon || tab.Icon != nil
}
mismatch := false
for _, tab := range items {
if (hasText && tab.Text == "") || (hasIcon && tab.Icon == nil) {
mismatch = true
break
}
}
return mismatch
}
func moreIcon(t baseTabs) fyne.Resource {
if l := t.tabLocation(); l == TabLocationLeading || l == TabLocationTrailing {
return theme.MoreVerticalIcon()
}
return theme.MoreHorizontalIcon()
}

116
vendor/fyne.io/fyne/v2/container/theme.go generated vendored Normal file
View File

@ -0,0 +1,116 @@
package container
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/cache"
intTheme "fyne.io/fyne/v2/internal/theme"
"fyne.io/fyne/v2/widget"
)
// ThemeOverride is a container where the child widgets are themed by the specified theme.
// Containers will be traversed and all child widgets will reflect the theme in this container.
// This should be used sparingly to avoid a jarring user experience.
//
// Since: 2.5
type ThemeOverride struct {
widget.BaseWidget
Content fyne.CanvasObject
Theme fyne.Theme
holder *fyne.Container
mobile bool
}
// NewThemeOverride provides a container where the child widgets are themed by the specified theme.
// Containers will be traversed and all child widgets will reflect the theme in this container.
// This should be used sparingly to avoid a jarring user experience.
//
// If the content `obj` of this theme override is a container and items are later added to the container or any
// sub-containers ensure that you call `Refresh()` on this `ThemeOverride` to ensure the new items match the theme.
//
// Since: 2.5
func NewThemeOverride(obj fyne.CanvasObject, th fyne.Theme) *ThemeOverride {
t := &ThemeOverride{Content: obj, Theme: th, holder: NewStack(obj)}
t.ExtendBaseWidget(t)
cache.OverrideTheme(obj, addFeatures(th, t))
obj.Refresh() // required as the widgets passed in could have been initially rendered with default theme
return t
}
func (t *ThemeOverride) CreateRenderer() fyne.WidgetRenderer {
cache.OverrideTheme(t.Content, addFeatures(t.Theme, t))
return &overrideRenderer{parent: t, objs: []fyne.CanvasObject{t.holder}}
}
func (t *ThemeOverride) Refresh() {
if t.holder.Objects[0] != t.Content {
t.holder.Objects[0] = t.Content
t.holder.Refresh()
}
cache.OverrideTheme(t.Content, addFeatures(t.Theme, t))
t.Content.Refresh()
t.BaseWidget.Refresh()
}
// SetDeviceIsMobile allows a ThemeOverride container to shape the contained widgets as a mobile device.
// This will impact containers such as AppTabs and DocTabs, and more in the future, to display a layout
// that would automatically be used for a mobile device runtime.
//
// Since: 2.6
func (t *ThemeOverride) SetDeviceIsMobile(on bool) {
t.mobile = on
t.BaseWidget.Refresh()
}
type featureTheme struct {
fyne.Theme
over *ThemeOverride
}
func addFeatures(th fyne.Theme, o *ThemeOverride) fyne.Theme {
return &featureTheme{Theme: th, over: o}
}
func (f *featureTheme) Feature(n intTheme.FeatureName) any {
if n == intTheme.FeatureNameDeviceIsMobile {
return f.over.mobile
}
return nil
}
type overrideRenderer struct {
parent *ThemeOverride
objs []fyne.CanvasObject
}
func (r *overrideRenderer) Destroy() {
}
func (r *overrideRenderer) Layout(s fyne.Size) {
intTheme.PushRenderingTheme(r.parent.Theme)
defer intTheme.PopRenderingTheme()
r.parent.holder.Resize(s)
}
func (r *overrideRenderer) MinSize() fyne.Size {
intTheme.PushRenderingTheme(r.parent.Theme)
defer intTheme.PopRenderingTheme()
return r.parent.Content.MinSize()
}
func (r *overrideRenderer) Objects() []fyne.CanvasObject {
return r.objs
}
func (r *overrideRenderer) Refresh() {
}

169
vendor/fyne.io/fyne/v2/data/binding/binding.go generated vendored Normal file
View File

@ -0,0 +1,169 @@
//go:generate go run gen.go
// Package binding provides support for binding data to widgets.
// All APIs in the binding package are safe to invoke directly from any goroutine.
package binding
import (
"errors"
"reflect"
"sync"
"fyne.io/fyne/v2"
)
var (
errKeyNotFound = errors.New("key not found")
errOutOfBounds = errors.New("index out of bounds")
errParseFailed = errors.New("format did not match 1 value")
// As an optimisation we connect any listeners asking for the same key, so that there is only 1 per preference item.
prefBinds = newPreferencesMap()
)
// DataItem is the base interface for all bindable data items.
// All APIs on bindable data items are safe to invoke directly fron any goroutine.
//
// Since: 2.0
type DataItem interface {
// AddListener attaches a new change listener to this DataItem.
// Listeners are called each time the data inside this DataItem changes.
// Additionally, the listener will be triggered upon successful connection to get the current value.
AddListener(DataListener)
// RemoveListener will detach the specified change listener from the DataItem.
// Disconnected listener will no longer be triggered when changes occur.
RemoveListener(DataListener)
}
// DataListener is any object that can register for changes in a bindable DataItem.
// See NewDataListener to define a new listener using just an inline function.
//
// Since: 2.0
type DataListener interface {
DataChanged()
}
// NewDataListener is a helper function that creates a new listener type from a simple callback function.
//
// Since: 2.0
func NewDataListener(fn func()) DataListener {
return &listener{fn}
}
type listener struct {
callback func()
}
func (l *listener) DataChanged() {
l.callback()
}
type base struct {
listeners []DataListener
lock sync.RWMutex
}
// AddListener allows a data listener to be informed of changes to this item.
func (b *base) AddListener(l DataListener) {
fyne.Do(func() {
b.listeners = append(b.listeners, l)
l.DataChanged()
})
}
// RemoveListener should be called if the listener is no longer interested in being informed of data change events.
func (b *base) RemoveListener(l DataListener) {
fyne.Do(func() {
for i, listener := range b.listeners {
if listener == l {
// Delete without preserving order:
lastIndex := len(b.listeners) - 1
b.listeners[i] = b.listeners[lastIndex]
b.listeners[lastIndex] = nil
b.listeners = b.listeners[:lastIndex]
return
}
}
})
}
func (b *base) trigger() {
fyne.Do(b.triggerFromMain)
}
func (b *base) triggerFromMain() {
for _, listen := range b.listeners {
listen.DataChanged()
}
}
// Untyped supports binding an any value.
//
// Since: 2.1
type Untyped = Item[any]
// NewUntyped returns a bindable any value that is managed internally.
//
// Since: 2.1
func NewUntyped() Untyped {
return NewItem(func(a1, a2 any) bool { return a1 == a2 })
}
// ExternalUntyped supports binding a any value to an external value.
//
// Since: 2.1
type ExternalUntyped = ExternalItem[any]
// BindUntyped returns a bindable any value that is bound to an external type.
// The parameter must be a pointer to the type you wish to bind.
//
// Since: 2.1
func BindUntyped(v any) ExternalUntyped {
t := reflect.TypeOf(v)
if t.Kind() != reflect.Ptr {
fyne.LogError("Invalid type passed to BindUntyped, must be a pointer", nil)
v = nil
}
if v == nil {
v = new(any) // never allow a nil value pointer
}
b := &boundExternalUntyped{}
b.val = reflect.ValueOf(v).Elem()
b.old = b.val.Interface()
return b
}
type boundExternalUntyped struct {
base
val reflect.Value
old any
}
func (b *boundExternalUntyped) Get() (any, error) {
b.lock.RLock()
defer b.lock.RUnlock()
return b.val.Interface(), nil
}
func (b *boundExternalUntyped) Set(val any) error {
b.lock.Lock()
if b.old == val {
b.lock.Unlock()
return nil
}
b.val.Set(reflect.ValueOf(val))
b.old = val
b.lock.Unlock()
b.trigger()
return nil
}
func (b *boundExternalUntyped) Reload() error {
return b.Set(b.val.Interface())
}

118
vendor/fyne.io/fyne/v2/data/binding/bool.go generated vendored Normal file
View File

@ -0,0 +1,118 @@
package binding
type not struct {
Bool
}
var _ Bool = (*not)(nil)
// Not returns a Bool binding that invert the value of the given data binding.
// This is providing the logical Not boolean operation as a data binding.
//
// Since 2.4
func Not(data Bool) Bool {
return &not{Bool: data}
}
func (n *not) Get() (bool, error) {
v, err := n.Bool.Get()
return !v, err
}
func (n *not) Set(value bool) error {
return n.Bool.Set(!value)
}
type and struct {
booleans
}
var _ Bool = (*and)(nil)
// And returns a Bool binding that return true when all the passed Bool binding are
// true and false otherwise. It does apply a logical and boolean operation on all passed
// Bool bindings. This binding is two way. In case of a Set, it will propagate the value
// identically to all the Bool bindings used for its construction.
//
// Since 2.4
func And(data ...Bool) Bool {
return &and{booleans: booleans{data: data}}
}
func (a *and) Get() (bool, error) {
for _, d := range a.data {
v, err := d.Get()
if err != nil {
return false, err
}
if !v {
return false, nil
}
}
return true, nil
}
func (a *and) Set(value bool) error {
for _, d := range a.data {
err := d.Set(value)
if err != nil {
return err
}
}
return nil
}
type or struct {
booleans
}
var _ Bool = (*or)(nil)
// Or returns a Bool binding that return true when at least one of the passed Bool binding
// is true and false otherwise. It does apply a logical or boolean operation on all passed
// Bool bindings. This binding is two way. In case of a Set, it will propagate the value
// identically to all the Bool bindings used for its construction.
//
// Since 2.4
func Or(data ...Bool) Bool {
return &or{booleans: booleans{data: data}}
}
func (o *or) Get() (bool, error) {
for _, d := range o.data {
v, err := d.Get()
if err != nil {
return false, err
}
if v {
return true, nil
}
}
return false, nil
}
func (o *or) Set(value bool) error {
for _, d := range o.data {
err := d.Set(value)
if err != nil {
return err
}
}
return nil
}
type booleans struct {
data []Bool
}
func (g *booleans) AddListener(listener DataListener) {
for _, d := range g.data {
d.AddListener(listener)
}
}
func (g *booleans) RemoveListener(listener DataListener) {
for _, d := range g.data {
d.RemoveListener(listener)
}
}

409
vendor/fyne.io/fyne/v2/data/binding/convert.go generated vendored Normal file
View File

@ -0,0 +1,409 @@
package binding
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/storage"
)
// BoolToString creates a binding that connects a Bool data item to a String.
// Changes to the Bool will be pushed to the String and setting the string will parse and set the
// Bool if the parse was successful.
//
// Since: 2.0
func BoolToString(v Bool) String {
return toStringComparable[bool](v, formatBool, parseBool)
}
// BoolToStringWithFormat creates a binding that connects a Bool data item to a String and is
// presented using the specified format. Changes to the Bool will be pushed to the String and setting
// the string will parse and set the Bool if the string matches the format and its parse was successful.
//
// Since: 2.0
func BoolToStringWithFormat(v Bool, format string) String {
return toStringWithFormatComparable[bool](v, format, "%t", formatBool, parseBool)
}
// FloatToString creates a binding that connects a Float data item to a String.
// Changes to the Float will be pushed to the String and setting the string will parse and set the
// Float if the parse was successful.
//
// Since: 2.0
func FloatToString(v Float) String {
return toStringComparable[float64](v, formatFloat, parseFloat)
}
// FloatToStringWithFormat creates a binding that connects a Float data item to a String and is
// presented using the specified format. Changes to the Float will be pushed to the String and setting
// the string will parse and set the Float if the string matches the format and its parse was successful.
//
// Since: 2.0
func FloatToStringWithFormat(v Float, format string) String {
return toStringWithFormatComparable[float64](v, format, "%f", formatFloat, parseFloat)
}
// IntToFloat creates a binding that connects an Int data item to a Float.
//
// Since: 2.5
func IntToFloat(val Int) Float {
v := &fromIntTo[float64]{from: val, parser: internalFloatToInt, formatter: internalIntToFloat}
val.AddListener(v)
return v
}
// FloatToInt creates a binding that connects a Float data item to an Int.
//
// Since: 2.5
func FloatToInt(v Float) Int {
i := &toInt[float64]{from: v, parser: internalFloatToInt, formatter: internalIntToFloat}
v.AddListener(i)
return i
}
// IntToString creates a binding that connects a Int data item to a String.
// Changes to the Int will be pushed to the String and setting the string will parse and set the
// Int if the parse was successful.
//
// Since: 2.0
func IntToString(v Int) String {
return toStringComparable[int](v, formatInt, parseInt)
}
// IntToStringWithFormat creates a binding that connects a Int data item to a String and is
// presented using the specified format. Changes to the Int will be pushed to the String and setting
// the string will parse and set the Int if the string matches the format and its parse was successful.
//
// Since: 2.0
func IntToStringWithFormat(v Int, format string) String {
return toStringWithFormatComparable[int](v, format, "%d", formatInt, parseInt)
}
// URIToString creates a binding that connects a URI data item to a String.
// Changes to the URI will be pushed to the String and setting the string will parse and set the
// URI if the parse was successful.
//
// Since: 2.1
func URIToString(v URI) String {
return toString[fyne.URI](v, uriToString, storage.EqualURI, uriFromString)
}
// StringToBool creates a binding that connects a String data item to a Bool.
// Changes to the String will be parsed and pushed to the Bool if the parse was successful, and setting
// the Bool update the String binding.
//
// Since: 2.0
func StringToBool(str String) Bool {
v := &fromStringTo[bool]{from: str, formatter: parseBool, parser: formatBool}
str.AddListener(v)
return v
}
// StringToBoolWithFormat creates a binding that connects a String data item to a Bool and is
// presented using the specified format. Changes to the Bool will be parsed and if the format matches and
// the parse is successful it will be pushed to the String. Setting the Bool will push a formatted value
// into the String.
//
// Since: 2.0
func StringToBoolWithFormat(str String, format string) Bool {
if format == "%t" { // Same as not using custom format.
return StringToBool(str)
}
v := &fromStringTo[bool]{from: str, format: format}
str.AddListener(v)
return v
}
// StringToFloat creates a binding that connects a String data item to a Float.
// Changes to the String will be parsed and pushed to the Float if the parse was successful, and setting
// the Float update the String binding.
//
// Since: 2.0
func StringToFloat(str String) Float {
v := &fromStringTo[float64]{from: str, formatter: parseFloat, parser: formatFloat}
str.AddListener(v)
return v
}
// StringToFloatWithFormat creates a binding that connects a String data item to a Float and is
// presented using the specified format. Changes to the Float will be parsed and if the format matches and
// the parse is successful it will be pushed to the String. Setting the Float will push a formatted value
// into the String.
//
// Since: 2.0
func StringToFloatWithFormat(str String, format string) Float {
if format == "%f" { // Same as not using custom format.
return StringToFloat(str)
}
v := &fromStringTo[float64]{from: str, format: format}
str.AddListener(v)
return v
}
// StringToInt creates a binding that connects a String data item to a Int.
// Changes to the String will be parsed and pushed to the Int if the parse was successful, and setting
// the Int update the String binding.
//
// Since: 2.0
func StringToInt(str String) Int {
v := &fromStringTo[int]{from: str, parser: formatInt, formatter: parseInt}
str.AddListener(v)
return v
}
// StringToIntWithFormat creates a binding that connects a String data item to a Int and is
// presented using the specified format. Changes to the Int will be parsed and if the format matches and
// the parse is successful it will be pushed to the String. Setting the Int will push a formatted value
// into the String.
//
// Since: 2.0
func StringToIntWithFormat(str String, format string) Int {
if format == "%d" { // Same as not using custom format.
return StringToInt(str)
}
v := &fromStringTo[int]{from: str, format: format}
str.AddListener(v)
return v
}
// StringToURI creates a binding that connects a String data item to a URI.
// Changes to the String will be parsed and pushed to the URI if the parse was successful, and setting
// the URI update the String binding.
//
// Since: 2.1
func StringToURI(str String) URI {
v := &fromStringTo[fyne.URI]{from: str, parser: uriToString, formatter: uriFromString}
str.AddListener(v)
return v
}
func toString[T any](v Item[T], formatter func(T) (string, error), comparator func(T, T) bool, parser func(string) (T, error)) *toStringFrom[T] {
str := &toStringFrom[T]{from: v, formatter: formatter, comparator: comparator, parser: parser}
v.AddListener(str)
return str
}
func toStringComparable[T bool | float64 | int](v Item[T], formatter func(T) (string, error), parser func(string) (T, error)) *toStringFrom[T] {
return toString(v, formatter, func(t1, t2 T) bool { return t1 == t2 }, parser)
}
func toStringWithFormat[T any](v Item[T], format, defaultFormat string, formatter func(T) (string, error), comparator func(T, T) bool, parser func(string) (T, error)) String {
str := toString(v, formatter, comparator, parser)
if format != defaultFormat { // Same as not using custom formatting.
str.format = format
}
return str
}
func toStringWithFormatComparable[T bool | float64 | int](v Item[T], format, defaultFormat string, formatter func(T) (string, error), parser func(string) (T, error)) String {
return toStringWithFormat(v, format, defaultFormat, formatter, func(t1, t2 T) bool { return t1 == t2 }, parser)
}
type convertBaseItem struct {
base
}
func (s *convertBaseItem) DataChanged() {
s.triggerFromMain()
}
type toStringFrom[T any] struct {
convertBaseItem
format string
formatter func(T) (string, error)
comparator func(T, T) bool
parser func(string) (T, error)
from Item[T]
}
func (s *toStringFrom[T]) Get() (string, error) {
val, err := s.from.Get()
if err != nil {
return "", err
}
if s.format != "" {
return fmt.Sprintf(s.format, val), nil
}
return s.formatter(val)
}
func (s *toStringFrom[T]) Set(str string) error {
var val T
if s.format != "" {
safe := stripFormatPrecision(s.format)
n, err := fmt.Sscanf(str, safe+" ", &val) // " " denotes match to end of string
if err != nil {
return err
}
if n != 1 {
return errParseFailed
}
} else {
new, err := s.parser(str)
if err != nil {
return err
}
val = new
}
old, err := s.from.Get()
if err != nil {
return err
}
if s.comparator(val, old) {
return nil
}
if err = s.from.Set(val); err != nil {
return err
}
s.trigger()
return nil
}
type fromStringTo[T any] struct {
convertBaseItem
format string
formatter func(string) (T, error)
parser func(T) (string, error)
from String
}
func (s *fromStringTo[T]) Get() (T, error) {
str, err := s.from.Get()
if str == "" || err != nil {
return *new(T), err
}
var val T
if s.format != "" {
n, err := fmt.Sscanf(str, s.format+" ", &val) // " " denotes match to end of string
if err != nil {
return *new(T), err
}
if n != 1 {
return *new(T), errParseFailed
}
} else {
formatted, err := s.formatter(str)
if err != nil {
return *new(T), err
}
val = formatted
}
return val, nil
}
func (s *fromStringTo[T]) Set(val T) error {
var str string
if s.format != "" {
str = fmt.Sprintf(s.format, val)
} else {
parsed, err := s.parser(val)
if err != nil {
return err
}
str = parsed
}
old, err := s.from.Get()
if str == old {
return err
}
err = s.from.Set(str)
if err != nil {
return err
}
s.trigger()
return nil
}
type toInt[T float64] struct {
convertBaseItem
formatter func(int) (T, error)
parser func(T) (int, error)
from Item[T]
}
func (s *toInt[T]) Get() (int, error) {
val, err := s.from.Get()
if err != nil {
return 0, err
}
return s.parser(val)
}
func (s *toInt[T]) Set(v int) error {
val, err := s.formatter(v)
if err != nil {
return err
}
old, err := s.from.Get()
if err != nil {
return err
}
if val == old {
return nil
}
err = s.from.Set(val)
if err != nil {
return err
}
s.trigger()
return nil
}
type fromIntTo[T float64] struct {
convertBaseItem
formatter func(int) (T, error)
parser func(T) (int, error)
from Item[int]
}
func (s *fromIntTo[T]) Get() (T, error) {
val, err := s.from.Get()
if err != nil {
return *new(T), err
}
return s.formatter(val)
}
func (s *fromIntTo[T]) Set(val T) error {
i, err := s.parser(val)
if err != nil {
return err
}
old, err := s.from.Get()
if i == old {
return nil
}
if err != nil {
return err
}
err = s.from.Set(i)
if err != nil {
return err
}
s.trigger()
return nil
}

98
vendor/fyne.io/fyne/v2/data/binding/convert_helper.go generated vendored Normal file
View File

@ -0,0 +1,98 @@
package binding
import (
"strconv"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/storage"
)
func stripFormatPrecision(in string) string {
// quick exit if certainly not float
if !strings.ContainsRune(in, 'f') {
return in
}
start := -1
end := -1
runes := []rune(in)
for i, r := range runes {
switch r {
case '%':
if i > 0 && start == i-1 { // ignore %%
start = -1
} else {
start = i
}
case 'f':
if start == -1 { // not part of format
continue
}
end = i
}
if end > -1 {
break
}
}
if end == start+1 { // no width/precision
return in
}
sizeRunes := runes[start+1 : end]
width, err := parseFloat(string(sizeRunes))
if err != nil {
return string(runes[:start+1]) + string(runes[:end])
}
if sizeRunes[0] == '.' { // formats like %.2f
return string(runes[:start+1]) + string(runes[end:])
}
return string(runes[:start+1]) + strconv.Itoa(int(width)) + string(runes[end:])
}
func uriFromString(in string) (fyne.URI, error) {
return storage.ParseURI(in)
}
func uriToString(in fyne.URI) (string, error) {
if in == nil {
return "", nil
}
return in.String(), nil
}
func parseBool(in string) (bool, error) {
return strconv.ParseBool(in)
}
func parseFloat(in string) (float64, error) {
return strconv.ParseFloat(in, 64)
}
func parseInt(in string) (int, error) {
out, err := strconv.ParseInt(in, 0, 64)
return int(out), err
}
func formatBool(in bool) (string, error) {
return strconv.FormatBool(in), nil
}
func formatFloat(in float64) (string, error) {
return strconv.FormatFloat(in, 'f', 6, 64), nil
}
func formatInt(in int) (string, error) {
return strconv.FormatInt(int64(in), 10), nil
}
func internalFloatToInt(val float64) (int, error) {
return int(val), nil
}
func internalIntToFloat(val int) (float64, error) {
return float64(val), nil
}

284
vendor/fyne.io/fyne/v2/data/binding/items.go generated vendored Normal file
View File

@ -0,0 +1,284 @@
package binding
import (
"bytes"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/storage"
)
// Item supports binding any type T generically.
//
// Since: 2.6
type Item[T any] interface {
DataItem
Get() (T, error)
Set(T) error
}
// ExternalItem supports binding any external value of type T.
//
// Since: 2.6
type ExternalItem[T any] interface {
Item[T]
Reload() error
}
// NewItem returns a bindable value of type T that is managed internally.
//
// Since: 2.6
func NewItem[T any](comparator func(T, T) bool) Item[T] {
return &item[T]{val: new(T), comparator: comparator}
}
// BindItem returns a new bindable value that controls the contents of the provided variable of type T.
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
//
// Since: 2.6
func BindItem[T any](val *T, comparator func(T, T) bool) ExternalItem[T] {
if val == nil {
val = new(T) // never allow a nil value pointer
}
b := &externalItem[T]{}
b.comparator = comparator
b.val = val
b.old = *val
return b
}
// Bool supports binding a bool value.
//
// Since: 2.0
type Bool = Item[bool]
// ExternalBool supports binding a bool value to an external value.
//
// Since: 2.0
type ExternalBool = ExternalItem[bool]
// NewBool returns a bindable bool value that is managed internally.
//
// Since: 2.0
func NewBool() Bool {
return newItemComparable[bool]()
}
// BindBool returns a new bindable value that controls the contents of the provided bool variable.
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindBool(v *bool) ExternalBool {
return bindExternalComparable(v)
}
// Bytes supports binding a []byte value.
//
// Since: 2.2
type Bytes = Item[[]byte]
// ExternalBytes supports binding a []byte value to an external value.
//
// Since: 2.2
type ExternalBytes = ExternalItem[[]byte]
// NewBytes returns a bindable []byte value that is managed internally.
//
// Since: 2.2
func NewBytes() Bytes {
return NewItem(bytes.Equal)
}
// BindBytes returns a new bindable value that controls the contents of the provided []byte variable.
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
//
// Since: 2.2
func BindBytes(v *[]byte) ExternalBytes {
return BindItem(v, bytes.Equal)
}
// Float supports binding a float64 value.
//
// Since: 2.0
type Float = Item[float64]
// ExternalFloat supports binding a float64 value to an external value.
//
// Since: 2.0
type ExternalFloat = ExternalItem[float64]
// NewFloat returns a bindable float64 value that is managed internally.
//
// Since: 2.0
func NewFloat() Float {
return newItemComparable[float64]()
}
// BindFloat returns a new bindable value that controls the contents of the provided float64 variable.
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindFloat(v *float64) ExternalFloat {
return bindExternalComparable(v)
}
// Int supports binding a int value.
//
// Since: 2.0
type Int = Item[int]
// ExternalInt supports binding a int value to an external value.
//
// Since: 2.0
type ExternalInt = ExternalItem[int]
// NewInt returns a bindable int value that is managed internally.
//
// Since: 2.0
func NewInt() Int {
return newItemComparable[int]()
}
// BindInt returns a new bindable value that controls the contents of the provided int variable.
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindInt(v *int) ExternalInt {
return bindExternalComparable(v)
}
// Rune supports binding a rune value.
//
// Since: 2.0
type Rune = Item[rune]
// ExternalRune supports binding a rune value to an external value.
//
// Since: 2.0
type ExternalRune = ExternalItem[rune]
// NewRune returns a bindable rune value that is managed internally.
//
// Since: 2.0
func NewRune() Rune {
return newItemComparable[rune]()
}
// BindRune returns a new bindable value that controls the contents of the provided rune variable.
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindRune(v *rune) ExternalRune {
return bindExternalComparable(v)
}
// String supports binding a string value.
//
// Since: 2.0
type String = Item[string]
// ExternalString supports binding a string value to an external value.
//
// Since: 2.0
type ExternalString = ExternalItem[string]
// NewString returns a bindable string value that is managed internally.
//
// Since: 2.0
func NewString() String {
return newItemComparable[string]()
}
// BindString returns a new bindable value that controls the contents of the provided string variable.
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindString(v *string) ExternalString {
return bindExternalComparable(v)
}
// URI supports binding a fyne.URI value.
//
// Since: 2.1
type URI = Item[fyne.URI]
// ExternalURI supports binding a fyne.URI value to an external value.
//
// Since: 2.1
type ExternalURI = ExternalItem[fyne.URI]
// NewURI returns a bindable fyne.URI value that is managed internally.
//
// Since: 2.1
func NewURI() URI {
return NewItem(storage.EqualURI)
}
// BindURI returns a new bindable value that controls the contents of the provided fyne.URI variable.
// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings.
//
// Since: 2.1
func BindURI(v *fyne.URI) ExternalURI {
return BindItem(v, storage.EqualURI)
}
func newItemComparable[T bool | float64 | int | rune | string]() Item[T] {
return NewItem[T](func(a, b T) bool { return a == b })
}
type item[T any] struct {
base
comparator func(T, T) bool
val *T
}
func (b *item[T]) Get() (T, error) {
b.lock.RLock()
defer b.lock.RUnlock()
if b.val == nil {
return *new(T), nil
}
return *b.val, nil
}
func (b *item[T]) Set(val T) error {
b.lock.Lock()
equal := b.comparator(*b.val, val)
*b.val = val
b.lock.Unlock()
if !equal {
b.trigger()
}
return nil
}
func bindExternalComparable[T bool | float64 | int | rune | string](val *T) ExternalItem[T] {
return BindItem(val, func(t1, t2 T) bool { return t1 == t2 })
}
type externalItem[T any] struct {
item[T]
old T
}
func (b *externalItem[T]) Set(val T) error {
b.lock.Lock()
if b.comparator(b.old, val) {
b.lock.Unlock()
return nil
}
*b.val = val
b.old = val
b.lock.Unlock()
b.trigger()
return nil
}
func (b *externalItem[T]) Reload() error {
return b.Set(*b.val)
}

662
vendor/fyne.io/fyne/v2/data/binding/lists.go generated vendored Normal file
View File

@ -0,0 +1,662 @@
package binding
import (
"bytes"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/storage"
)
// DataList is the base interface for all bindable data lists.
//
// Since: 2.0
type DataList interface {
DataItem
GetItem(index int) (DataItem, error)
Length() int
}
// BoolList supports binding a list of bool values.
//
// Since: 2.0
type BoolList interface {
DataList
Append(value bool) error
Get() ([]bool, error)
GetValue(index int) (bool, error)
Prepend(value bool) error
Remove(value bool) error
Set(list []bool) error
SetValue(index int, value bool) error
}
// ExternalBoolList supports binding a list of bool values from an external variable.
//
// Since: 2.0
type ExternalBoolList interface {
BoolList
Reload() error
}
// NewBoolList returns a bindable list of bool values.
//
// Since: 2.0
func NewBoolList() BoolList {
return newListComparable[bool]()
}
// BindBoolList returns a bound list of bool values, based on the contents of the passed slice.
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindBoolList(v *[]bool) ExternalBoolList {
return bindListComparable(v)
}
// BytesList supports binding a list of []byte values.
//
// Since: 2.2
type BytesList interface {
DataList
Append(value []byte) error
Get() ([][]byte, error)
GetValue(index int) ([]byte, error)
Prepend(value []byte) error
Remove(value []byte) error
Set(list [][]byte) error
SetValue(index int, value []byte) error
}
// ExternalBytesList supports binding a list of []byte values from an external variable.
//
// Since: 2.2
type ExternalBytesList interface {
BytesList
Reload() error
}
// NewBytesList returns a bindable list of []byte values.
//
// Since: 2.2
func NewBytesList() BytesList {
return newList(bytes.Equal)
}
// BindBytesList returns a bound list of []byte values, based on the contents of the passed slice.
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
//
// Since: 2.2
func BindBytesList(v *[][]byte) ExternalBytesList {
return bindList(v, bytes.Equal)
}
// FloatList supports binding a list of float64 values.
//
// Since: 2.0
type FloatList interface {
DataList
Append(value float64) error
Get() ([]float64, error)
GetValue(index int) (float64, error)
Prepend(value float64) error
Remove(value float64) error
Set(list []float64) error
SetValue(index int, value float64) error
}
// ExternalFloatList supports binding a list of float64 values from an external variable.
//
// Since: 2.0
type ExternalFloatList interface {
FloatList
Reload() error
}
// NewFloatList returns a bindable list of float64 values.
//
// Since: 2.0
func NewFloatList() FloatList {
return newListComparable[float64]()
}
// BindFloatList returns a bound list of float64 values, based on the contents of the passed slice.
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindFloatList(v *[]float64) ExternalFloatList {
return bindListComparable(v)
}
// IntList supports binding a list of int values.
//
// Since: 2.0
type IntList interface {
DataList
Append(value int) error
Get() ([]int, error)
GetValue(index int) (int, error)
Prepend(value int) error
Remove(value int) error
Set(list []int) error
SetValue(index int, value int) error
}
// ExternalIntList supports binding a list of int values from an external variable.
//
// Since: 2.0
type ExternalIntList interface {
IntList
Reload() error
}
// NewIntList returns a bindable list of int values.
//
// Since: 2.0
func NewIntList() IntList {
return newListComparable[int]()
}
// BindIntList returns a bound list of int values, based on the contents of the passed slice.
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindIntList(v *[]int) ExternalIntList {
return bindListComparable(v)
}
// RuneList supports binding a list of rune values.
//
// Since: 2.0
type RuneList interface {
DataList
Append(value rune) error
Get() ([]rune, error)
GetValue(index int) (rune, error)
Prepend(value rune) error
Remove(value rune) error
Set(list []rune) error
SetValue(index int, value rune) error
}
// ExternalRuneList supports binding a list of rune values from an external variable.
//
// Since: 2.0
type ExternalRuneList interface {
RuneList
Reload() error
}
// NewRuneList returns a bindable list of rune values.
//
// Since: 2.0
func NewRuneList() RuneList {
return newListComparable[rune]()
}
// BindRuneList returns a bound list of rune values, based on the contents of the passed slice.
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindRuneList(v *[]rune) ExternalRuneList {
if v == nil {
return NewRuneList().(ExternalRuneList)
}
b := newListComparable[rune]()
b.val = v
b.updateExternal = true
for i := range *v {
b.appendItem(bindListItemComparable(v, i, b.updateExternal))
}
return b
}
// StringList supports binding a list of string values.
//
// Since: 2.0
type StringList interface {
DataList
Append(value string) error
Get() ([]string, error)
GetValue(index int) (string, error)
Prepend(value string) error
Remove(value string) error
Set(list []string) error
SetValue(index int, value string) error
}
// ExternalStringList supports binding a list of string values from an external variable.
//
// Since: 2.0
type ExternalStringList interface {
StringList
Reload() error
}
// NewStringList returns a bindable list of string values.
//
// Since: 2.0
func NewStringList() StringList {
return newListComparable[string]()
}
// BindStringList returns a bound list of string values, based on the contents of the passed slice.
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindStringList(v *[]string) ExternalStringList {
return bindListComparable(v)
}
// UntypedList supports binding a list of any values.
//
// Since: 2.1
type UntypedList interface {
DataList
Append(value any) error
Get() ([]any, error)
GetValue(index int) (any, error)
Prepend(value any) error
Remove(value any) error
Set(list []any) error
SetValue(index int, value any) error
}
// ExternalUntypedList supports binding a list of any values from an external variable.
//
// Since: 2.1
type ExternalUntypedList interface {
UntypedList
Reload() error
}
// NewUntypedList returns a bindable list of any values.
//
// Since: 2.1
func NewUntypedList() UntypedList {
return newList(func(t1, t2 any) bool { return t1 == t2 })
}
// BindUntypedList returns a bound list of any values, based on the contents of the passed slice.
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
//
// Since: 2.1
func BindUntypedList(v *[]any) ExternalUntypedList {
if v == nil {
return NewUntypedList().(ExternalUntypedList)
}
comparator := func(t1, t2 any) bool { return t1 == t2 }
b := newExternalList(v, comparator)
for i := range *v {
b.appendItem(bindListItem(v, i, b.updateExternal, comparator))
}
return b
}
// URIList supports binding a list of fyne.URI values.
//
// Since: 2.1
type URIList interface {
DataList
Append(value fyne.URI) error
Get() ([]fyne.URI, error)
GetValue(index int) (fyne.URI, error)
Prepend(value fyne.URI) error
Remove(value fyne.URI) error
Set(list []fyne.URI) error
SetValue(index int, value fyne.URI) error
}
// ExternalURIList supports binding a list of fyne.URI values from an external variable.
//
// Since: 2.1
type ExternalURIList interface {
URIList
Reload() error
}
// NewURIList returns a bindable list of fyne.URI values.
//
// Since: 2.1
func NewURIList() URIList {
return newList(storage.EqualURI)
}
// BindURIList returns a bound list of fyne.URI values, based on the contents of the passed slice.
// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings.
//
// Since: 2.1
func BindURIList(v *[]fyne.URI) ExternalURIList {
return bindList(v, storage.EqualURI)
}
type listBase struct {
base
items []DataItem
}
// GetItem returns the DataItem at the specified index.
func (b *listBase) GetItem(i int) (DataItem, error) {
b.lock.RLock()
defer b.lock.RUnlock()
if i < 0 || i >= len(b.items) {
return nil, errOutOfBounds
}
return b.items[i], nil
}
// Length returns the number of items in this data list.
func (b *listBase) Length() int {
b.lock.RLock()
defer b.lock.RUnlock()
return len(b.items)
}
func (b *listBase) appendItem(i DataItem) {
b.items = append(b.items, i)
}
func (b *listBase) deleteItem(i int) {
b.items = append(b.items[:i], b.items[i+1:]...)
}
func newList[T any](comparator func(T, T) bool) *boundList[T] {
return &boundList[T]{val: new([]T), comparator: comparator}
}
func newListComparable[T bool | float64 | int | rune | string]() *boundList[T] {
return newList(func(t1, t2 T) bool { return t1 == t2 })
}
func newExternalList[T any](v *[]T, comparator func(T, T) bool) *boundList[T] {
return &boundList[T]{val: v, comparator: comparator, updateExternal: true}
}
func bindList[T any](v *[]T, comparator func(T, T) bool) *boundList[T] {
if v == nil {
return newList(comparator)
}
l := newExternalList(v, comparator)
for i := range *v {
l.appendItem(bindListItem(v, i, l.updateExternal, comparator))
}
return l
}
func bindListComparable[T bool | float64 | int | rune | string](v *[]T) *boundList[T] {
return bindList(v, func(t1, t2 T) bool { return t1 == t2 })
}
type boundList[T any] struct {
listBase
comparator func(T, T) bool
updateExternal bool
val *[]T
parentListener func(int)
}
func (l *boundList[T]) Append(val T) error {
l.lock.Lock()
*l.val = append(*l.val, val)
trigger, err := l.doReload()
l.lock.Unlock()
if trigger {
l.trigger()
}
return err
}
func (l *boundList[T]) Get() ([]T, error) {
l.lock.RLock()
defer l.lock.RUnlock()
return *l.val, nil
}
func (l *boundList[T]) GetValue(i int) (T, error) {
l.lock.RLock()
defer l.lock.RUnlock()
if i < 0 || i >= l.Length() {
return *new(T), errOutOfBounds
}
return (*l.val)[i], nil
}
func (l *boundList[T]) Prepend(val T) error {
l.lock.Lock()
*l.val = append([]T{val}, *l.val...)
trigger, err := l.doReload()
l.lock.Unlock()
if trigger {
l.trigger()
}
return err
}
func (l *boundList[T]) Reload() error {
l.lock.Lock()
trigger, err := l.doReload()
l.lock.Unlock()
if trigger {
l.trigger()
}
return err
}
func (l *boundList[T]) Remove(val T) error {
l.lock.Lock()
v := *l.val
if len(v) == 0 {
l.lock.Unlock()
return nil
}
if l.comparator(v[0], val) {
*l.val = v[1:]
} else if l.comparator(v[len(v)-1], val) {
*l.val = v[:len(v)-1]
} else {
id := -1
for i, v := range v {
if l.comparator(v, val) {
id = i
break
}
}
if id == -1 {
l.lock.Unlock()
return nil
}
*l.val = append(v[:id], v[id+1:]...)
}
trigger, err := l.doReload()
l.lock.Unlock()
if trigger {
l.trigger()
}
return err
}
func (l *boundList[T]) Set(v []T) error {
l.lock.Lock()
*l.val = v
trigger, err := l.doReload()
l.lock.Unlock()
if trigger {
l.trigger()
}
return err
}
func (l *boundList[T]) doReload() (trigger bool, retErr error) {
oldLen := len(l.items)
newLen := len(*l.val)
if oldLen > newLen {
for i := oldLen - 1; i >= newLen; i-- {
l.deleteItem(i)
}
trigger = true
} else if oldLen < newLen {
for i := oldLen; i < newLen; i++ {
item := bindListItem(l.val, i, l.updateExternal, l.comparator)
if l.parentListener != nil {
index := i
item.AddListener(NewDataListener(func() {
l.parentListener(index)
}))
}
l.appendItem(item)
}
trigger = true
}
for i, item := range l.items {
if i > oldLen || i > newLen {
break
}
var err error
if l.updateExternal {
err = item.(*boundExternalListItem[T]).setIfChanged((*l.val)[i])
} else {
err = item.(*boundListItem[T]).doSet((*l.val)[i])
}
if err != nil {
retErr = err
}
}
return
}
func (l *boundList[T]) SetValue(i int, v T) error {
l.lock.RLock()
len := l.Length()
l.lock.RUnlock()
if i < 0 || i >= len {
return errOutOfBounds
}
l.lock.Lock()
(*l.val)[i] = v
l.lock.Unlock()
item, err := l.GetItem(i)
if err != nil {
return err
}
return item.(Item[T]).Set(v)
}
func bindListItem[T any](v *[]T, i int, external bool, comparator func(T, T) bool) Item[T] {
if external {
ret := &boundExternalListItem[T]{old: (*v)[i]}
ret.val = v
ret.index = i
ret.comparator = comparator
return ret
}
return &boundListItem[T]{val: v, index: i, comparator: comparator}
}
func bindListItemComparable[T bool | float64 | int | rune | string](v *[]T, i int, external bool) Item[T] {
return bindListItem(v, i, external, func(t1, t2 T) bool { return t1 == t2 })
}
type boundListItem[T any] struct {
base
comparator func(T, T) bool
val *[]T
index int
}
func (b *boundListItem[T]) Get() (T, error) {
b.lock.Lock()
defer b.lock.Unlock()
if b.index < 0 || b.index >= len(*b.val) {
return *new(T), errOutOfBounds
}
return (*b.val)[b.index], nil
}
func (b *boundListItem[T]) Set(val T) error {
return b.doSet(val)
}
func (b *boundListItem[T]) doSet(val T) error {
b.lock.Lock()
(*b.val)[b.index] = val
b.lock.Unlock()
b.trigger()
return nil
}
type boundExternalListItem[T any] struct {
boundListItem[T]
old T
}
func (b *boundExternalListItem[T]) setIfChanged(val T) error {
b.lock.Lock()
if b.comparator(val, b.old) {
b.lock.Unlock()
return nil
}
(*b.val)[b.index] = val
b.old = val
b.lock.Unlock()
b.trigger()
return nil
}

522
vendor/fyne.io/fyne/v2/data/binding/maps.go generated vendored Normal file
View File

@ -0,0 +1,522 @@
package binding
import (
"errors"
"reflect"
"fyne.io/fyne/v2"
)
// DataMap is the base interface for all bindable data maps.
//
// Since: 2.0
type DataMap interface {
DataItem
GetItem(string) (DataItem, error)
Keys() []string
}
// ExternalUntypedMap is a map data binding with all values untyped (any), connected to an external data source.
//
// Since: 2.0
type ExternalUntypedMap interface {
UntypedMap
Reload() error
}
// UntypedMap is a map data binding with all values Untyped (any).
//
// Since: 2.0
type UntypedMap interface {
DataMap
Delete(string)
Get() (map[string]any, error)
GetValue(string) (any, error)
Set(map[string]any) error
SetValue(string, any) error
}
// NewUntypedMap creates a new, empty map binding of string to any.
//
// Since: 2.0
func NewUntypedMap() UntypedMap {
return &mapBase{items: make(map[string]reflectUntyped), val: &map[string]any{}}
}
// BindUntypedMap creates a new map binding of string to any based on the data passed.
// If your code changes the content of the map this refers to you should call Reload() to inform the bindings.
//
// Since: 2.0
func BindUntypedMap(d *map[string]any) ExternalUntypedMap {
if d == nil {
return NewUntypedMap().(ExternalUntypedMap)
}
m := &mapBase{items: make(map[string]reflectUntyped), val: d, updateExternal: true}
for k := range *d {
m.setItem(k, bindUntypedMapValue(d, k, m.updateExternal))
}
return m
}
// Struct is the base interface for a bound struct type.
//
// Since: 2.0
type Struct interface {
DataMap
GetValue(string) (any, error)
SetValue(string, any) error
Reload() error
}
// BindStruct creates a new map binding of string to any using the struct passed as data.
// The key for each item is a string representation of each exported field with the value set as an any.
// Only exported fields are included.
//
// Since: 2.0
func BindStruct(i any) Struct {
if i == nil {
return NewUntypedMap().(Struct)
}
t := reflect.TypeOf(i)
if t.Kind() != reflect.Ptr ||
(reflect.TypeOf(reflect.ValueOf(i).Elem()).Kind() != reflect.Struct) {
fyne.LogError("Invalid type passed to BindStruct, must be pointer to struct", nil)
return NewUntypedMap().(Struct)
}
s := &boundStruct{orig: i}
s.items = make(map[string]reflectUntyped)
s.val = &map[string]any{}
s.updateExternal = true
v := reflect.ValueOf(i).Elem()
t = v.Type()
for j := 0; j < v.NumField(); j++ {
f := v.Field(j)
if !f.CanSet() {
continue
}
key := t.Field(j).Name
s.items[key] = bindReflect(f)
(*s.val)[key] = f.Interface()
}
return s
}
type reflectUntyped interface {
DataItem
get() (any, error)
set(any) error
}
type mapBase struct {
base
updateExternal bool
items map[string]reflectUntyped
val *map[string]any
}
func (b *mapBase) GetItem(key string) (DataItem, error) {
b.lock.RLock()
defer b.lock.RUnlock()
if v, ok := b.items[key]; ok {
return v, nil
}
return nil, errKeyNotFound
}
func (b *mapBase) Keys() []string {
b.lock.Lock()
defer b.lock.Unlock()
ret := make([]string, len(b.items))
i := 0
for k := range b.items {
ret[i] = k
i++
}
return ret
}
func (b *mapBase) Delete(key string) {
b.lock.Lock()
defer b.lock.Unlock()
delete(b.items, key)
b.trigger()
}
func (b *mapBase) Get() (map[string]any, error) {
b.lock.RLock()
defer b.lock.RUnlock()
if b.val == nil {
return map[string]any{}, nil
}
return *b.val, nil
}
func (b *mapBase) GetValue(key string) (any, error) {
b.lock.RLock()
defer b.lock.RUnlock()
if i, ok := b.items[key]; ok {
return i.get()
}
return nil, errKeyNotFound
}
func (b *mapBase) Reload() error {
b.lock.Lock()
defer b.lock.Unlock()
return b.doReload()
}
func (b *mapBase) Set(v map[string]any) error {
b.lock.Lock()
defer b.lock.Unlock()
if b.val == nil { // was not initialized with a blank value, recover
b.val = &v
b.trigger()
return nil
}
*b.val = v
return b.doReload()
}
func (b *mapBase) SetValue(key string, d any) error {
b.lock.Lock()
defer b.lock.Unlock()
if i, ok := b.items[key]; ok {
return i.set(d)
}
(*b.val)[key] = d
item := bindUntypedMapValue(b.val, key, b.updateExternal)
b.setItem(key, item)
return nil
}
func (b *mapBase) doReload() (retErr error) {
changed := false
// add new
for key := range *b.val {
_, found := b.items[key]
if !found {
b.setItem(key, bindUntypedMapValue(b.val, key, b.updateExternal))
changed = true
}
}
// remove old
for key := range b.items {
_, found := (*b.val)[key]
if !found {
delete(b.items, key)
changed = true
}
}
if changed {
b.trigger()
}
for k, item := range b.items {
var err error
if b.updateExternal {
err = item.(*boundExternalMapValue).setIfChanged((*b.val)[k])
} else {
err = item.(*boundMapValue).set((*b.val)[k])
}
if err != nil {
retErr = err
}
}
return
}
func (b *mapBase) setItem(key string, d reflectUntyped) {
b.items[key] = d
b.trigger()
}
type boundStruct struct {
mapBase
orig any
}
func (b *boundStruct) Reload() (retErr error) {
b.lock.Lock()
defer b.lock.Unlock()
v := reflect.ValueOf(b.orig).Elem()
t := v.Type()
for j := 0; j < v.NumField(); j++ {
f := v.Field(j)
if !f.CanSet() {
continue
}
kind := f.Kind()
if kind == reflect.Slice || kind == reflect.Struct {
fyne.LogError("Data binding does not yet support slice or struct elements in a struct", nil)
continue
}
key := t.Field(j).Name
old := (*b.val)[key]
if f.Interface() == old {
continue
}
var err error
switch kind {
case reflect.Bool:
err = b.items[key].(*reflectBool).Set(f.Bool())
case reflect.Float32, reflect.Float64:
err = b.items[key].(*reflectFloat).Set(f.Float())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
err = b.items[key].(*reflectInt).Set(int(f.Int()))
case reflect.String:
err = b.items[key].(*reflectString).Set(f.String())
}
if err != nil {
retErr = err
}
(*b.val)[key] = f.Interface()
}
return
}
func bindUntypedMapValue(m *map[string]any, k string, external bool) reflectUntyped {
if external {
ret := &boundExternalMapValue{old: (*m)[k]}
ret.val = m
ret.key = k
return ret
}
return &boundMapValue{val: m, key: k}
}
type boundMapValue struct {
base
val *map[string]any
key string
}
func (b *boundMapValue) get() (any, error) {
if v, ok := (*b.val)[b.key]; ok {
return v, nil
}
return nil, errKeyNotFound
}
func (b *boundMapValue) set(val any) error {
(*b.val)[b.key] = val
b.trigger()
return nil
}
type boundExternalMapValue struct {
boundMapValue
old any
}
func (b *boundExternalMapValue) setIfChanged(val any) error {
if val == b.old {
return nil
}
b.old = val
return b.set(val)
}
type boundReflect struct {
base
val reflect.Value
}
func (b *boundReflect) get() (any, error) {
return b.val.Interface(), nil
}
func (b *boundReflect) set(val any) (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("unable to set bool in data binding")
}
}()
b.val.Set(reflect.ValueOf(val))
b.trigger()
return nil
}
type reflectBool struct {
boundReflect
}
func (r *reflectBool) Get() (val bool, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("invalid bool value in data binding")
}
}()
val = r.val.Bool()
return
}
func (r *reflectBool) Set(b bool) (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("unable to set bool in data binding")
}
}()
r.val.SetBool(b)
r.trigger()
return
}
func bindReflectBool(f reflect.Value) reflectUntyped {
r := &reflectBool{}
r.val = f
return r
}
type reflectFloat struct {
boundReflect
}
func (r *reflectFloat) Get() (val float64, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("invalid float64 value in data binding")
}
}()
val = r.val.Float()
return
}
func (r *reflectFloat) Set(f float64) (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("unable to set float64 in data binding")
}
}()
r.val.SetFloat(f)
r.trigger()
return
}
func bindReflectFloat(f reflect.Value) reflectUntyped {
r := &reflectFloat{}
r.val = f
return r
}
type reflectInt struct {
boundReflect
}
func (r *reflectInt) Get() (val int, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("invalid int value in data binding")
}
}()
val = int(r.val.Int())
return
}
func (r *reflectInt) Set(i int) (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("unable to set int in data binding")
}
}()
r.val.SetInt(int64(i))
r.trigger()
return
}
func bindReflectInt(f reflect.Value) reflectUntyped {
r := &reflectInt{}
r.val = f
return r
}
type reflectString struct {
boundReflect
}
func (r *reflectString) Get() (val string, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("invalid string value in data binding")
}
}()
val = r.val.String()
return
}
func (r *reflectString) Set(s string) (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("unable to set string in data binding")
}
}()
r.val.SetString(s)
r.trigger()
return
}
func bindReflectString(f reflect.Value) reflectUntyped {
r := &reflectString{}
r.val = f
return r
}
func bindReflect(field reflect.Value) reflectUntyped {
switch field.Kind() {
case reflect.Bool:
return bindReflectBool(field)
case reflect.Float32, reflect.Float64:
return bindReflectFloat(field)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return bindReflectInt(field)
case reflect.String:
return bindReflectString(field)
}
return &boundReflect{val: field}
}

97
vendor/fyne.io/fyne/v2/data/binding/pref_helper.go generated vendored Normal file
View File

@ -0,0 +1,97 @@
package binding
import (
"sync"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/async"
)
type preferenceItem interface {
checkForChange()
}
type preferenceBindings struct {
async.Map[string, preferenceItem]
}
func (b *preferenceBindings) list() []preferenceItem {
ret := []preferenceItem{}
b.Range(func(_ string, item preferenceItem) bool {
ret = append(ret, item)
return true
})
return ret
}
type preferencesMap struct {
prefs async.Map[fyne.Preferences, *preferenceBindings]
appPrefs fyne.Preferences // the main application prefs, to check if it changed...
appLock sync.Mutex
}
func newPreferencesMap() *preferencesMap {
return &preferencesMap{}
}
func (m *preferencesMap) ensurePreferencesAttached(p fyne.Preferences) *preferenceBindings {
binds, loaded := m.prefs.LoadOrStore(p, &preferenceBindings{})
if loaded {
return binds
}
p.AddChangeListener(func() { m.preferencesChanged(p) })
return binds
}
func (m *preferencesMap) getBindings(p fyne.Preferences) *preferenceBindings {
if p == fyne.CurrentApp().Preferences() {
m.appLock.Lock()
prefs := m.appPrefs
if m.appPrefs == nil {
m.appPrefs = p
}
m.appLock.Unlock()
if prefs != p {
m.migratePreferences(prefs, p)
}
}
binds, _ := m.prefs.Load(p)
return binds
}
func (m *preferencesMap) preferencesChanged(p fyne.Preferences) {
binds := m.getBindings(p)
if binds == nil {
return
}
for _, item := range binds.list() {
item.checkForChange()
}
}
func (m *preferencesMap) migratePreferences(src, dst fyne.Preferences) {
old, loaded := m.prefs.Load(src)
if !loaded {
return
}
m.prefs.Store(dst, old)
m.prefs.Delete(src)
m.appLock.Lock()
m.appPrefs = dst
m.appLock.Unlock()
binds := m.getBindings(dst)
if binds == nil {
return
}
for _, b := range binds.list() {
if backed, ok := b.(interface{ replaceProvider(fyne.Preferences) }); ok {
backed.replaceProvider(dst)
}
}
m.preferencesChanged(dst)
}

261
vendor/fyne.io/fyne/v2/data/binding/preference.go generated vendored Normal file
View File

@ -0,0 +1,261 @@
package binding
import (
"sync/atomic"
"fyne.io/fyne/v2"
)
// Work around Go not supporting generic methods on non-generic types:
type preferenceLookupSetter[T any] func(fyne.Preferences) (func(string) T, func(string, T))
const keyTypeMismatchError = "A previous preference binding exists with different type for key: "
// BindPreferenceBool returns a bindable bool value that is managed by the application preferences.
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
//
// Since: 2.0
func BindPreferenceBool(key string, p fyne.Preferences) Bool {
return bindPreferenceItem(key, p,
func(p fyne.Preferences) (func(string) bool, func(string, bool)) {
return p.Bool, p.SetBool
})
}
// BindPreferenceBoolList returns a bound list of bool values that is managed by the application preferences.
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
//
// Since: 2.6
func BindPreferenceBoolList(key string, p fyne.Preferences) BoolList {
return bindPreferenceListComparable[bool](key, p,
func(p fyne.Preferences) (func(string) []bool, func(string, []bool)) {
return p.BoolList, p.SetBoolList
},
)
}
// BindPreferenceFloat returns a bindable float64 value that is managed by the application preferences.
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
//
// Since: 2.0
func BindPreferenceFloat(key string, p fyne.Preferences) Float {
return bindPreferenceItem(key, p,
func(p fyne.Preferences) (func(string) float64, func(string, float64)) {
return p.Float, p.SetFloat
})
}
// BindPreferenceFloatList returns a bound list of float64 values that is managed by the application preferences.
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
//
// Since: 2.6
func BindPreferenceFloatList(key string, p fyne.Preferences) FloatList {
return bindPreferenceListComparable[float64](key, p,
func(p fyne.Preferences) (func(string) []float64, func(string, []float64)) {
return p.FloatList, p.SetFloatList
},
)
}
// BindPreferenceInt returns a bindable int value that is managed by the application preferences.
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
//
// Since: 2.0
func BindPreferenceInt(key string, p fyne.Preferences) Int {
return bindPreferenceItem(key, p,
func(p fyne.Preferences) (func(string) int, func(string, int)) {
return p.Int, p.SetInt
})
}
// BindPreferenceIntList returns a bound list of int values that is managed by the application preferences.
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
//
// Since: 2.6
func BindPreferenceIntList(key string, p fyne.Preferences) IntList {
return bindPreferenceListComparable[int](key, p,
func(p fyne.Preferences) (func(string) []int, func(string, []int)) {
return p.IntList, p.SetIntList
},
)
}
// BindPreferenceString returns a bindable string value that is managed by the application preferences.
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
//
// Since: 2.0
func BindPreferenceString(key string, p fyne.Preferences) String {
return bindPreferenceItem(key, p,
func(p fyne.Preferences) (func(string) string, func(string, string)) {
return p.String, p.SetString
})
}
// BindPreferenceStringList returns a bound list of string values that is managed by the application preferences.
// Changes to this value will be saved to application storage and when the app starts the previous values will be read.
//
// Since: 2.6
func BindPreferenceStringList(key string, p fyne.Preferences) StringList {
return bindPreferenceListComparable[string](key, p,
func(p fyne.Preferences) (func(string) []string, func(string, []string)) {
return p.StringList, p.SetStringList
},
)
}
func bindPreferenceItem[T bool | float64 | int | string](key string, p fyne.Preferences, setLookup preferenceLookupSetter[T]) Item[T] {
if found, ok := lookupExistingBinding[T](key, p); ok {
return found
}
listen := &prefBoundBase[T]{key: key, setLookup: setLookup}
listen.replaceProvider(p)
binds := prefBinds.ensurePreferencesAttached(p)
binds.Store(key, listen)
return listen
}
func lookupExistingBinding[T any](key string, p fyne.Preferences) (Item[T], bool) {
binds := prefBinds.getBindings(p)
if binds == nil {
return nil, false
}
if listen, ok := binds.Load(key); listen != nil && ok {
if l, ok := listen.(Item[T]); ok {
return l, ok
}
fyne.LogError(keyTypeMismatchError+key, nil)
}
return nil, false
}
func lookupExistingListBinding[T bool | float64 | int | string](key string, p fyne.Preferences) (*prefBoundList[T], bool) {
binds := prefBinds.getBindings(p)
if binds == nil {
return nil, false
}
if listen, ok := binds.Load(key); listen != nil && ok {
if l, ok := listen.(*prefBoundList[T]); ok {
return l, ok
}
fyne.LogError(keyTypeMismatchError+key, nil)
}
return nil, false
}
type prefBoundBase[T bool | float64 | int | string] struct {
base
key string
get func(string) T
set func(string, T)
setLookup preferenceLookupSetter[T]
cache atomic.Pointer[T]
}
func (b *prefBoundBase[T]) Get() (T, error) {
cache := b.get(b.key)
b.cache.Store(&cache)
return cache, nil
}
func (b *prefBoundBase[T]) Set(v T) error {
b.set(b.key, v)
b.lock.RLock()
defer b.lock.RUnlock()
b.trigger()
return nil
}
func (b *prefBoundBase[T]) checkForChange() {
val := b.cache.Load()
if val != nil && b.get(b.key) == *val {
return
}
b.trigger()
}
func (b *prefBoundBase[T]) replaceProvider(p fyne.Preferences) {
b.get, b.set = b.setLookup(p)
}
type prefBoundList[T bool | float64 | int | string] struct {
boundList[T]
key string
get func(string) []T
set func(string, []T)
setLookup preferenceLookupSetter[[]T]
}
func (b *prefBoundList[T]) checkForChange() {
val := *b.val
updated := b.get(b.key)
if val == nil || len(updated) != len(val) {
b.Set(updated)
return
}
if val == nil {
return
}
// incoming changes to a preference list are not at the child level
for i, v := range val {
if i >= len(updated) {
break
}
if !b.comparator(v, updated[i]) {
_ = b.items[i].(Item[T]).Set(updated[i])
}
}
}
func (b *prefBoundList[T]) replaceProvider(p fyne.Preferences) {
b.get, b.set = b.setLookup(p)
}
type internalPrefs = interface{ WriteValues(func(map[string]any)) }
func bindPreferenceListComparable[T bool | float64 | int | string](key string, p fyne.Preferences,
setLookup preferenceLookupSetter[[]T]) *prefBoundList[T] {
if found, ok := lookupExistingListBinding[T](key, p); ok {
return found
}
listen := &prefBoundList[T]{key: key, setLookup: setLookup}
listen.replaceProvider(p)
items := listen.get(listen.key)
listen.boundList = *bindList(nil, func(t1, t2 T) bool { return t1 == t2 })
listen.boundList.AddListener(NewDataListener(func() {
cached := *listen.val
replaced := listen.get(listen.key)
if len(cached) == len(replaced) {
return
}
listen.set(listen.key, *listen.val)
listen.trigger()
}))
listen.boundList.parentListener = func(index int) {
listen.set(listen.key, *listen.val)
// the child changes are not seen on the write end so force it
if prefs, ok := p.(internalPrefs); ok {
prefs.WriteValues(func(map[string]any) {})
}
}
listen.boundList.Set(items)
binds := prefBinds.ensurePreferencesAttached(p)
binds.Store(key, listen)
return listen
}

Some files were not shown because too many files have changed in this diff Show More