feature/ui puvodni web appka ve zvlast funkci + funkcni nastrel ve fyne ui od chatGPT
This commit is contained in:
parent
11561b9d24
commit
79fe00f493
23
fyne_ui.go
Normal file
23
fyne_ui.go
Normal file
@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
)
|
||||
|
||||
func NewUI() (stprageDir string, window fyne.Window) {
|
||||
// App + storage dir
|
||||
a := app.New()
|
||||
w := a.NewWindow("Encryptor (Fyne)")
|
||||
w.Resize(fyne.NewSize(1100, 720))
|
||||
|
||||
base, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
base, _ = os.UserHomeDir()
|
||||
}
|
||||
|
||||
return filepath.Join(base, "fckeuspy-go"), w
|
||||
}
|
||||
37
go.mod
37
go.mod
@ -1,3 +1,40 @@
|
||||
module fckeuspy-go
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require fyne.io/fyne/v2 v2.6.3
|
||||
|
||||
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/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/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.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
80
go.sum
Normal file
80
go.sum
Normal file
@ -0,0 +1,80 @@
|
||||
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/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/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/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
|
||||
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
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/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/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
18
identity.crt
Normal file
18
identity.crt
Normal file
@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC4TCCAcmgAwIBAgIBATANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhFbmNy
|
||||
eXB0b3IgTG9jYWwgSWRlbnRpdHkwHhcNMjUwOTE0MTcwMTMzWhcNMjYwOTE0MTcw
|
||||
MTMzWjAjMSEwHwYDVQQDExhFbmNyeXB0b3IgTG9jYWwgSWRlbnRpdHkwggEiMA0G
|
||||
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3FzA4W5AaH2WVmvaCeVcBAPpN8IjT
|
||||
bOaFkWtWR/p/m1e7o8mSgTYaDlETuR4mO/Cc0GppzQc2coufPconne8KCvPv5wPj
|
||||
tXs5rKKRaxmOwKVfbjqdGXHP7CAtoimJl1U6/rDFto4BLr4CEF2/OxFFA89o3Tiy
|
||||
Z6jaGBAVj5w3N9a5QGk+K8brT/XQAV17rk1fzAShsWIoqW3KR6EN1G3GhwjZTg9y
|
||||
6u9wzWN9aywewtRDWB9LhwXrz/GpjNkF90tn64FxscYEdoeaigsPaCg7SbR2ltJQ
|
||||
xu2LmNYJhgCNHFwYDD8PylxXoh4cjnzvvA2UgkTUBSvK0uJEQuEiX6EnAgMBAAGj
|
||||
IDAeMA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUA
|
||||
A4IBAQAdPX998IM8vE5haCO9B4iD+oDEFaTXPUVCwpEmFDpmko004kDg/iLljZJb
|
||||
S9984MTEiMluzBYTVIbCZ/UJzrxN2N+r3o+osg5AveGSlIaoStjo9px3s/RV2v4d
|
||||
jsc+r1IOJF/35Cial9Ie+AT87uEAMi5UkD2Pk356ICaz+oKTh6kbvr5U2GCXw93g
|
||||
Hg1HeuIyWiDpT16GCi0Qs/JmfePgsgW6SYybUp83ZCsMAdw1QkT6nA4vEaZn8uGs
|
||||
CwyEorw3LW1oK4ZKCdlV9L/K3GdY2WdUGDJ8iI/NVIJGi31g88g1fzx27fU2acpU
|
||||
aXA1WFUYcfCrD8EndrHCEkUfECRb
|
||||
-----END CERTIFICATE-----
|
||||
27
identity_key.pem
Normal file
27
identity_key.pem
Normal file
@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAtxcwOFuQGh9llZr2gnlXAQD6TfCI02zmhZFrVkf6f5tXu6PJ
|
||||
koE2Gg5RE7keJjvwnNBqac0HNnKLnz3KJ53vCgrz7+cD47V7OayikWsZjsClX246
|
||||
nRlxz+wgLaIpiZdVOv6wxbaOAS6+AhBdvzsRRQPPaN04smeo2hgQFY+cNzfWuUBp
|
||||
PivG60/10AFde65NX8wEobFiKKltykehDdRtxocI2U4PcurvcM1jfWssHsLUQ1gf
|
||||
S4cF68/xqYzZBfdLZ+uBcbHGBHaHmooLD2goO0m0dpbSUMbti5jWCYYAjRxcGAw/
|
||||
D8pcV6IeHI5877wNlIJE1AUrytLiRELhIl+hJwIDAQABAoIBABWYoZ0e3RoyYTbK
|
||||
sZBlpFKU/Uaw5vgcB1DypzjlLUdcrnY5SpnXrecC5fiv7ujO9yxiv7qSbusINdnn
|
||||
CEUGBwp7N5eGUK93r6eiP7FsHaFOA0kEuoXfWM0QFXeXLNPISvW2JxRwhRg0nZWm
|
||||
vTgaTTOD0IpBAI1irB3AQDLFiW/7GMB2aZD4YaUk2kbEbvSOF7xNqQtth0caP+/L
|
||||
04pI635/6y+a1S/pQgnHBmltAzReUgQtYnNZMIzNiJUCA92Szb0Q/q4lKcn8sZ3r
|
||||
yIFr6pKPAs7qYX73P8gi4Jbajn1HSoAE2jWokmi2TmRSYp7fiWYsPrAakAoxOv2x
|
||||
Fv83ookCgYEA1tl+g7wOREV/7FtRwheWx3m//bxxiGRuxliNqrAaMXjO6cAdZJN3
|
||||
+TM3kFUYW7DSDhW5O7QDVibMZNbnYhtjaJSjnKZFPv+zqwuR8/hMboJjmqSRb+m8
|
||||
4FG7SxZPPhXQ22BZKjIsMLi8D+QRyXPfaVN4Yd3tGCNawMXxFmESepsCgYEA2ih8
|
||||
hFUXAp0epB8Z2m8KbrcTMMu1dNRIuioajSyOfwqGDIwGmDb7lyEgDpjtSrcfBlz5
|
||||
h5Tgg9MTEaJl8MxD4LXOHIZTkimre8iIt43TyTofEiD5nU4OUiwk+9SQwiPqVfy/
|
||||
ErUOwKVJ2oQv81LQheg13p9gUhvMQE4h8wOq5mUCgYBZ4mAViOWHFnRwU7wesXO5
|
||||
PGxgISh2YV4eyQlrsYUj0WDvVhp162Qz84N5dMBeC9m1Xs1B9wu2TUERpv7igobS
|
||||
R+0zKjSqKJvoIU0MfoKrcQ1usw7NfUIxrr/mqAy68rGQNfzXtGnccEztcQMn/rwm
|
||||
+m7QsuHwSUo8gBNew3nRpQKBgCHcPtdbTaL+OA9JNH4O1hOxHq1oMNXdTRx4BH78
|
||||
93EIdR3lbfCaOBqQ7aTWX55FJe+a5rAAj4hmboNCLYhea/qovUD4KGh7Rz6DNZUn
|
||||
0kNdXg02SQf9YYOnjmX37C/12x1ViWKh75Q/E2NzOO4PYIYoMJRJMG4OGnmwptxN
|
||||
KW1xAoGARjaCGJ+QdDrlybbcftAKgUQxev8TnMZz66iil8/8J2SJc5EOKQRwDBpx
|
||||
HTbAcKQPdJZyBOf6rdoPgpCNws8dLukCTBgDCynuyRXqXQk0fNMZKGN5Lk7a5i4U
|
||||
eH9YPMkxkg7n3Ke7yaBFXkpahdw44sQtVz/jgRRhO7NZTUxOXeo=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
239
lib/crypto.go
Normal file
239
lib/crypto.go
Normal file
@ -0,0 +1,239 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Envelope struct {
|
||||
EK string `json:"ek"`
|
||||
N string `json:"n"`
|
||||
CT string `json:"ct"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
priv *rsa.PrivateKey
|
||||
pubPEM []byte
|
||||
certPEM []byte
|
||||
dir string
|
||||
}
|
||||
|
||||
func NewService(storageDir string) (*Service, error) {
|
||||
s := &Service{dir: storageDir}
|
||||
if err := s.loadOrGenerateKeys(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Service) PublicPEM() string { return string(s.pubPEM) }
|
||||
func (s *Service) PublicCert() string { return string(s.certPEM) }
|
||||
|
||||
func (s *Service) Encrypt(message, peerPEMorCert string) (string, error) {
|
||||
pubKey, err := 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
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nil, nonce, []byte(message), nil)
|
||||
label := []byte{}
|
||||
ek, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, aesKey, label)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
env := Envelope{
|
||||
EK: base64.StdEncoding.EncodeToString(ek),
|
||||
N: base64.StdEncoding.EncodeToString(nonce),
|
||||
CT: base64.StdEncoding.EncodeToString(ciphertext),
|
||||
}
|
||||
out, _ := json.MarshalIndent(env, "", " ")
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func (s *Service) Decrypt(payload string) (string, error) {
|
||||
var env Envelope
|
||||
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)
|
||||
}
|
||||
|
||||
label := []byte{}
|
||||
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, s.priv, ek, label)
|
||||
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
|
||||
}
|
||||
plain, err := gcm.Open(nil, nonce, ct, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("GCM open: %w", err)
|
||||
}
|
||||
return string(plain), nil
|
||||
}
|
||||
|
||||
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 (s *Service) loadOrGenerateKeys() error {
|
||||
privPath := filepath.Join(s.dir, "identity_key.pem")
|
||||
pubPath := filepath.Join(s.dir, "public.pem")
|
||||
certPath := filepath.Join(s.dir, "identity.crt")
|
||||
|
||||
if fileExists(privPath) && fileExists(pubPath) && fileExists(certPath) {
|
||||
pkBytes, err := os.ReadFile(privPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block, _ := pem.Decode(pkBytes)
|
||||
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
|
||||
}
|
||||
s.priv = key
|
||||
s.pubPEM, _ = os.ReadFile(pubPath)
|
||||
s.certPEM, _ = os.ReadFile(certPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.priv = key
|
||||
|
||||
pubASN1, _ := x509.MarshalPKIXPublicKey(&s.priv.PublicKey)
|
||||
s.pubPEM = pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1})
|
||||
|
||||
cert, err := generateSelfSignedCert(s.priv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.certPEM = cert
|
||||
|
||||
if err := os.MkdirAll(s.dir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(privPath, pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(s.priv)}), fs.FileMode(0o600)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(pubPath, s.pubPEM, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(certPath, s.certPEM, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateSelfSignedCert(priv *rsa.PrivateKey) ([]byte, error) {
|
||||
tpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{CommonName: "Encryptor Local Identity"},
|
||||
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
|
||||
}
|
||||
|
||||
func fileExists(p string) bool {
|
||||
info, err := os.Stat(p)
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
159
lib/encrypt.go
159
lib/encrypt.go
@ -1,159 +0,0 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"math/big"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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, priv *rsa.PrivateKey) []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(privPath, pubPath, certPath string) (priv *rsa.PrivateKey, pubPEM, certPEM []byte, err 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 nil, nil, nil, err
|
||||
}
|
||||
block, _ := pem.Decode(pkBytes)
|
||||
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
||||
return nil, nil, nil, fmt.Errorf("invalid private key PEM")
|
||||
}
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 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 priv, pubPEM, certPEM, nil
|
||||
}
|
||||
|
||||
// jinak vygeneruj nové
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
priv = key
|
||||
|
||||
// public
|
||||
pubASN1, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey)
|
||||
pubPEM = pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1})
|
||||
|
||||
// cert
|
||||
certPEM = generateSelfSignedCert(&priv.PublicKey, priv)
|
||||
|
||||
// ulož
|
||||
if err := os.WriteFile(privPath,
|
||||
pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}),
|
||||
fs.FileMode(0600)); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if err := os.WriteFile(pubPath, pubPEM, 0644); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
log.Println("Vygenerována nová identita a uložena na disk.")
|
||||
return priv, pubPEM, certPEM, nil
|
||||
}
|
||||
|
||||
func fileExists(p string) bool {
|
||||
info, err := os.Stat(p)
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
150
main.go
150
main.go
@ -6,6 +6,13 @@ import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -27,9 +34,148 @@ type envelope struct {
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
err error
|
||||
svc *encrypt.Service
|
||||
)
|
||||
|
||||
storageDir, w := NewUI()
|
||||
|
||||
// 2) inicializuj šifrovací službu
|
||||
svc, err = encrypt.NewService(storageDir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Toast (stavový pruh)
|
||||
toast := widget.NewLabel("")
|
||||
toast.Hide()
|
||||
showToast := func(msg string) {
|
||||
toast.SetText(msg)
|
||||
toast.Show()
|
||||
time.AfterFunc(1200*time.Millisecond, func() { toast.SetText(""); toast.Hide() })
|
||||
}
|
||||
|
||||
// Helpers
|
||||
copyToClipboard := func(s string) { w.Clipboard().SetContent(s); showToast("Zkopírováno") }
|
||||
/* setErr := func(e error) {
|
||||
if e != nil {
|
||||
dialog.ShowError(e, w)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
outKey := widget.NewMultiLineEntry()
|
||||
cipherOut := widget.NewMultiLineEntry()
|
||||
// cipherOut.SetReadOnly(true)
|
||||
plainOut := widget.NewMultiLineEntry()
|
||||
// plainOut.SetReadOnly(true)
|
||||
// toastLbl := widget.NewLabelWithData(toastB)
|
||||
|
||||
// Widgets
|
||||
outKey = widget.NewMultiLineEntry()
|
||||
outKey.SetPlaceHolder("Veřejný klíč / certifikát…")
|
||||
msg := widget.NewMultiLineEntry()
|
||||
msg.SetPlaceHolder("Sem napiš zprávu…")
|
||||
peer := widget.NewMultiLineEntry()
|
||||
peer.SetPlaceHolder("-----BEGIN PUBLIC KEY----- … nebo CERTIFICATE …")
|
||||
cipherOut = widget.NewMultiLineEntry()
|
||||
cipherOut.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`)
|
||||
// cipherOut.SetReadOnly(true)
|
||||
payload := widget.NewMultiLineEntry()
|
||||
payload.SetPlaceHolder(`{"ek":"…","n":"…","ct":"…"}`)
|
||||
plainOut = widget.NewMultiLineEntry()
|
||||
plainOut.SetPlaceHolder("Dešifrovaná zpráva…")
|
||||
// plainOut.SetReadOnly(true)
|
||||
|
||||
// Sekce: Můj veřejný klíč
|
||||
btnShowPub := widget.NewButton("Zobrazit public.pem", func() { outKey.SetText(svc.PublicPEM()) })
|
||||
btnShowCrt := widget.NewButton("Zobrazit identity.crt", func() { outKey.SetText(svc.PublicCert()) })
|
||||
btnCopyOut := widget.NewButton("Copy", func() { copyToClipboard(outKey.Text) })
|
||||
btnClearOut := widget.NewButton("Clear", func() { outKey.SetText("") })
|
||||
|
||||
myKeyCard := container.NewVBox(
|
||||
widget.NewLabelWithStyle("Můj veřejný klíč", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
container.New(layout.NewGridLayout(4), btnShowPub, btnShowCrt, btnCopyOut, btnClearOut),
|
||||
outKey,
|
||||
)
|
||||
|
||||
// Sekce: Šifrovat
|
||||
btnEnc := widget.NewButton("Encrypt", func() {
|
||||
m := msg.Text
|
||||
p := peer.Text
|
||||
if m == "" {
|
||||
dialog.ShowInformation("Info", "Zpráva nesmí být prázdná.", w)
|
||||
return
|
||||
}
|
||||
if p == "" {
|
||||
dialog.ShowInformation("Info", "Vlož veřejný klíč (PEM) nebo certifikát (CERT).", w)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
res, err := svc.Encrypt(m, p)
|
||||
if err != nil {
|
||||
cipherOut.SetText("")
|
||||
toast.SetText("Chyba: " + err.Error())
|
||||
toast.Show()
|
||||
return
|
||||
}
|
||||
cipherOut.SetText(res)
|
||||
showToast("Zašifrováno")
|
||||
}()
|
||||
})
|
||||
btnCopyCipher := widget.NewButton("Copy", func() { copyToClipboard(cipherOut.Text) })
|
||||
|
||||
encCard := container.NewVBox(
|
||||
widget.NewLabelWithStyle("Šifrovat pro cizí klíč", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
widget.NewLabel("Zpráva"), msg,
|
||||
widget.NewLabel("Veřejný klíč příjemce (PEM nebo CERT)"), peer,
|
||||
container.New(layout.NewGridLayout(2), btnEnc, btnCopyCipher),
|
||||
cipherOut,
|
||||
)
|
||||
|
||||
// Sekce: Dešifrovat
|
||||
btnDec := widget.NewButton("Decrypt", func() {
|
||||
pl := payload.Text
|
||||
if pl == "" {
|
||||
dialog.ShowInformation("Info", "Vlož JSON payload k dešifrování.", w)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
res, err := svc.Decrypt(pl)
|
||||
if err != nil {
|
||||
plainOut.SetText("")
|
||||
toast.SetText("Chyba: " + err.Error())
|
||||
toast.Show()
|
||||
return
|
||||
}
|
||||
plainOut.SetText(res)
|
||||
showToast("Dešifrováno")
|
||||
}()
|
||||
})
|
||||
btnCopyPlain := widget.NewButton("Copy", func() { copyToClipboard(plainOut.Text) })
|
||||
btnClearPayload := widget.NewButton("Clear payload", func() { payload.SetText("") })
|
||||
|
||||
decCard := container.NewVBox(
|
||||
widget.NewLabelWithStyle("Dešifrovat", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
widget.NewLabel("Payload (JSON envelope)"), payload,
|
||||
container.New(layout.NewGridLayout(3), btnDec, btnCopyPlain, btnClearPayload),
|
||||
plainOut,
|
||||
)
|
||||
|
||||
left := container.NewVBox(myKeyCard, encCard)
|
||||
right := container.NewVBox(decCard)
|
||||
grid := container.New(layout.NewGridLayoutWithColumns(2), left, right)
|
||||
|
||||
content := container.NewBorder(nil, toast, nil, nil, container.NewVScroll(grid))
|
||||
w.SetContent(content)
|
||||
w.ShowAndRun()
|
||||
}
|
||||
|
||||
func RunWebApp() {
|
||||
var err error
|
||||
// 1) načti nebo vygeneruj klíče
|
||||
priv, pubPEM, certPEM, err = encrypt.LoadOrGenerateKeys(privPath, pubPath, certPath)
|
||||
// 2) inicializuj šifrovací službu
|
||||
_, err = encrypt.NewService("")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
9
public.pem
Normal file
9
public.pem
Normal file
@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtxcwOFuQGh9llZr2gnlX
|
||||
AQD6TfCI02zmhZFrVkf6f5tXu6PJkoE2Gg5RE7keJjvwnNBqac0HNnKLnz3KJ53v
|
||||
Cgrz7+cD47V7OayikWsZjsClX246nRlxz+wgLaIpiZdVOv6wxbaOAS6+AhBdvzsR
|
||||
RQPPaN04smeo2hgQFY+cNzfWuUBpPivG60/10AFde65NX8wEobFiKKltykehDdRt
|
||||
xocI2U4PcurvcM1jfWssHsLUQ1gfS4cF68/xqYzZBfdLZ+uBcbHGBHaHmooLD2go
|
||||
O0m0dpbSUMbti5jWCYYAjRxcGAw/D8pcV6IeHI5877wNlIJE1AUrytLiRELhIl+h
|
||||
JwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
49
vendor/fyne.io/fyne/v2/.gitignore
generated
vendored
Normal file
49
vendor/fyne.io/fyne/v2/.gitignore
generated
vendored
Normal 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
1
vendor/fyne.io/fyne/v2/.godocdown.import
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
fyne.io/fyne/v2
|
||||
16
vendor/fyne.io/fyne/v2/AUTHORS
generated
vendored
Normal file
16
vendor/fyne.io/fyne/v2/AUTHORS
generated
vendored
Normal 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
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
76
vendor/fyne.io/fyne/v2/CODE_OF_CONDUCT.md
generated
vendored
Normal 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
63
vendor/fyne.io/fyne/v2/CONTRIBUTING.md
generated
vendored
Normal 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
28
vendor/fyne.io/fyne/v2/LICENSE
generated
vendored
Normal 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
184
vendor/fyne.io/fyne/v2/README.md
generated
vendored
Normal 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 :)
|
||||
|
||||

|
||||
15
vendor/fyne.io/fyne/v2/SECURITY.md
generated
vendored
Normal file
15
vendor/fyne.io/fyne/v2/SECURITY.md
generated
vendored
Normal 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
84
vendor/fyne.io/fyne/v2/animation.go
generated
vendored
Normal 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
145
vendor/fyne.io/fyne/v2/app.go
generated
vendored
Normal 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(¤t)
|
||||
}
|
||||
|
||||
// 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
189
vendor/fyne.io/fyne/v2/app/app.go
generated
vendored
Normal 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
59
vendor/fyne.io/fyne/v2/app/app_darwin.go
generated
vendored
Normal 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
60
vendor/fyne.io/fyne/v2/app/app_darwin.m
generated
vendored
Normal 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
54
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go
generated
vendored
Normal 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
12
vendor/fyne.io/fyne/v2/app/app_desktop_darwin.m
generated
vendored
Normal 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
14
vendor/fyne.io/fyne/v2/app/app_gl.go
generated
vendored
Normal 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
26
vendor/fyne.io/fyne/v2/app/app_mobile.go
generated
vendored
Normal 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
130
vendor/fyne.io/fyne/v2/app/app_mobile_and.c
generated
vendored
Normal 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
43
vendor/fyne.io/fyne/v2/app/app_mobile_and.go
generated
vendored
Normal 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
26
vendor/fyne.io/fyne/v2/app/app_mobile_ios.go
generated
vendored
Normal 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
10
vendor/fyne.io/fyne/v2/app/app_mobile_ios.m
generated
vendored
Normal 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
22
vendor/fyne.io/fyne/v2/app/app_mobile_xdg.go
generated
vendored
Normal 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
8
vendor/fyne.io/fyne/v2/app/app_notlegacy_darwin.go
generated
vendored
Normal 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
18
vendor/fyne.io/fyne/v2/app/app_openurl_wasm.go
generated
vendored
Normal 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
12
vendor/fyne.io/fyne/v2/app/app_openurl_web.go
generated
vendored
Normal 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
26
vendor/fyne.io/fyne/v2/app/app_other.go
generated
vendored
Normal 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
15
vendor/fyne.io/fyne/v2/app/app_software.go
generated
vendored
Normal 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
77
vendor/fyne.io/fyne/v2/app/app_wasm.go
generated
vendored
Normal 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
100
vendor/fyne.io/fyne/v2/app/app_windows.go
generated
vendored
Normal 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
139
vendor/fyne.io/fyne/v2/app/app_xdg.go
generated
vendored
Normal 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
47
vendor/fyne.io/fyne/v2/app/cloud.go
generated
vendored
Normal 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
59
vendor/fyne.io/fyne/v2/app/icon_cache_file.go
generated
vendored
Normal 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
36
vendor/fyne.io/fyne/v2/app/meta.go
generated
vendored
Normal 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
65
vendor/fyne.io/fyne/v2/app/meta_development.go
generated
vendored
Normal 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
191
vendor/fyne.io/fyne/v2/app/preferences.go
generated
vendored
Normal 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
24
vendor/fyne.io/fyne/v2/app/preferences_android.go
generated
vendored
Normal 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
25
vendor/fyne.io/fyne/v2/app/preferences_ios.go
generated
vendored
Normal 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
23
vendor/fyne.io/fyne/v2/app/preferences_mobile.go
generated
vendored
Normal 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
67
vendor/fyne.io/fyne/v2/app/preferences_nonweb.go
generated
vendored
Normal 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
32
vendor/fyne.io/fyne/v2/app/preferences_other.go
generated
vendored
Normal 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
62
vendor/fyne.io/fyne/v2/app/preferences_wasm.go
generated
vendored
Normal 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
176
vendor/fyne.io/fyne/v2/app/settings.go
generated
vendored
Normal 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
80
vendor/fyne.io/fyne/v2/app/settings_desktop.go
generated
vendored
Normal 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
34
vendor/fyne.io/fyne/v2/app/settings_file.go
generated
vendored
Normal 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
11
vendor/fyne.io/fyne/v2/app/settings_mobile.go
generated
vendored
Normal 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
25
vendor/fyne.io/fyne/v2/app/settings_wasm.go
generated
vendored
Normal 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
31
vendor/fyne.io/fyne/v2/app/storage.go
generated
vendored
Normal 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
58
vendor/fyne.io/fyne/v2/canvas.go
generated
vendored
Normal 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
86
vendor/fyne.io/fyne/v2/canvas/animation.go
generated
vendored
Normal 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
69
vendor/fyne.io/fyne/v2/canvas/base.go
generated
vendored
Normal 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
49
vendor/fyne.io/fyne/v2/canvas/canvas.go
generated
vendored
Normal 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
95
vendor/fyne.io/fyne/v2/canvas/circle.go
generated
vendored
Normal 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
238
vendor/fyne.io/fyne/v2/canvas/gradient.go
generated
vendored
Normal 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
388
vendor/fyne.io/fyne/v2/canvas/image.go
generated
vendored
Normal 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
108
vendor/fyne.io/fyne/v2/canvas/line.go
generated
vendored
Normal 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
200
vendor/fyne.io/fyne/v2/canvas/raster.go
generated
vendored
Normal 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
68
vendor/fyne.io/fyne/v2/canvas/rectangle.go
generated
vendored
Normal 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
85
vendor/fyne.io/fyne/v2/canvas/text.go
generated
vendored
Normal 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
107
vendor/fyne.io/fyne/v2/canvasobject.go
generated
vendored
Normal 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
9
vendor/fyne.io/fyne/v2/clipboard.go
generated
vendored
Normal 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
39
vendor/fyne.io/fyne/v2/cloud.go
generated
vendored
Normal 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
202
vendor/fyne.io/fyne/v2/container.go
generated
vendored
Normal 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
473
vendor/fyne.io/fyne/v2/container/apptabs.go
generated
vendored
Normal 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
20
vendor/fyne.io/fyne/v2/container/container.go
generated
vendored
Normal 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
492
vendor/fyne.io/fyne/v2/container/doctabs.go
generated
vendored
Normal 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
442
vendor/fyne.io/fyne/v2/container/innerwindow.go
generated
vendored
Normal 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
124
vendor/fyne.io/fyne/v2/container/layouts.go
generated
vendored
Normal 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
105
vendor/fyne.io/fyne/v2/container/multiplewindows.go
generated
vendored
Normal 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
55
vendor/fyne.io/fyne/v2/container/scroll.go
generated
vendored
Normal 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
404
vendor/fyne.io/fyne/v2/container/split.go
generated
vendored
Normal 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 := ÷r{
|
||||
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 ÷rRenderer{
|
||||
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
876
vendor/fyne.io/fyne/v2/container/tabs.go
generated
vendored
Normal 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
116
vendor/fyne.io/fyne/v2/container/theme.go
generated
vendored
Normal 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
169
vendor/fyne.io/fyne/v2/data/binding/binding.go
generated
vendored
Normal 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
118
vendor/fyne.io/fyne/v2/data/binding/bool.go
generated
vendored
Normal 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 ¬{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
409
vendor/fyne.io/fyne/v2/data/binding/convert.go
generated
vendored
Normal 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
98
vendor/fyne.io/fyne/v2/data/binding/convert_helper.go
generated
vendored
Normal 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
284
vendor/fyne.io/fyne/v2/data/binding/items.go
generated
vendored
Normal 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
662
vendor/fyne.io/fyne/v2/data/binding/lists.go
generated
vendored
Normal 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
522
vendor/fyne.io/fyne/v2/data/binding/maps.go
generated
vendored
Normal 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
97
vendor/fyne.io/fyne/v2/data/binding/pref_helper.go
generated
vendored
Normal 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
261
vendor/fyne.io/fyne/v2/data/binding/preference.go
generated
vendored
Normal 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
|
||||
}
|
||||
218
vendor/fyne.io/fyne/v2/data/binding/sprintf.go
generated
vendored
Normal file
218
vendor/fyne.io/fyne/v2/data/binding/sprintf.go
generated
vendored
Normal file
@ -0,0 +1,218 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"fyne.io/fyne/v2/storage"
|
||||
)
|
||||
|
||||
type sprintfString struct {
|
||||
String
|
||||
|
||||
format string
|
||||
source []DataItem
|
||||
err error
|
||||
}
|
||||
|
||||
// NewSprintf returns a String binding that format its content using the
|
||||
// format string and the provide additional parameter that must be other
|
||||
// data bindings. This data binding use fmt.Sprintf and fmt.Scanf internally
|
||||
// and will have all the same limitation as those function.
|
||||
//
|
||||
// Since: 2.2
|
||||
func NewSprintf(format string, b ...DataItem) String {
|
||||
ret := &sprintfString{
|
||||
String: NewString(),
|
||||
format: format,
|
||||
source: append(make([]DataItem, 0, len(b)), b...),
|
||||
}
|
||||
|
||||
for _, value := range b {
|
||||
value.AddListener(ret)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *sprintfString) DataChanged() {
|
||||
data := make([]any, 0, len(s.source))
|
||||
|
||||
s.err = nil
|
||||
for _, value := range s.source {
|
||||
switch x := value.(type) {
|
||||
case Bool:
|
||||
b, err := x.Get()
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
|
||||
data = append(data, b)
|
||||
case Bytes:
|
||||
b, err := x.Get()
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
|
||||
data = append(data, b)
|
||||
case Float:
|
||||
f, err := x.Get()
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
|
||||
data = append(data, f)
|
||||
case Int:
|
||||
i, err := x.Get()
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
|
||||
data = append(data, i)
|
||||
case Rune:
|
||||
r, err := x.Get()
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
|
||||
data = append(data, r)
|
||||
case String:
|
||||
str, err := x.Get()
|
||||
if err != nil {
|
||||
s.err = err
|
||||
// Set error?
|
||||
return
|
||||
}
|
||||
|
||||
data = append(data, str)
|
||||
case URI:
|
||||
u, err := x.Get()
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
|
||||
data = append(data, u)
|
||||
}
|
||||
}
|
||||
|
||||
r := fmt.Sprintf(s.format, data...)
|
||||
s.String.Set(r)
|
||||
}
|
||||
|
||||
func (s *sprintfString) Get() (string, error) {
|
||||
if s.err != nil {
|
||||
return "", s.err
|
||||
}
|
||||
return s.String.Get()
|
||||
}
|
||||
|
||||
func (s *sprintfString) Set(str string) error {
|
||||
data := make([]any, 0, len(s.source))
|
||||
|
||||
s.err = nil
|
||||
for _, value := range s.source {
|
||||
switch value.(type) {
|
||||
case Bool:
|
||||
data = append(data, new(bool))
|
||||
case Bytes:
|
||||
return fmt.Errorf("impossible to convert '%s' to []bytes type", str)
|
||||
case Float:
|
||||
data = append(data, new(float64))
|
||||
case Int:
|
||||
data = append(data, new(int))
|
||||
case Rune:
|
||||
data = append(data, new(rune))
|
||||
case String:
|
||||
data = append(data, new(string))
|
||||
case URI:
|
||||
data = append(data, new(string))
|
||||
}
|
||||
}
|
||||
|
||||
count, err := fmt.Sscanf(str, s.format, data...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count != len(data) {
|
||||
return fmt.Errorf("impossible to decode more than %v parameters in '%s' with format '%s'", count, str, s.format)
|
||||
}
|
||||
|
||||
for i, value := range s.source {
|
||||
switch x := value.(type) {
|
||||
case Bool:
|
||||
v := data[i].(*bool)
|
||||
|
||||
err := x.Set(*v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case Bytes:
|
||||
return fmt.Errorf("impossible to convert '%s' to []bytes type", str)
|
||||
case Float:
|
||||
v := data[i].(*float64)
|
||||
|
||||
err := x.Set(*v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case Int:
|
||||
v := data[i].(*int)
|
||||
|
||||
err := x.Set(*v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case Rune:
|
||||
v := data[i].(*rune)
|
||||
|
||||
err := x.Set(*v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case String:
|
||||
v := data[i].(*string)
|
||||
|
||||
err := x.Set(*v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case URI:
|
||||
v := data[i].(*string)
|
||||
|
||||
if v == nil {
|
||||
return fmt.Errorf("URI can not be nil in '%s'", str)
|
||||
}
|
||||
|
||||
uri, err := storage.ParseURI(*v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = x.Set(uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StringToStringWithFormat creates a binding that converts a string to another string using the specified format.
|
||||
// Changes to the returned String will be pushed to the passed in String and setting a new string value will parse and
|
||||
// set the underlying String if it matches the format and the parse was successful.
|
||||
//
|
||||
// Since: 2.2
|
||||
func StringToStringWithFormat(str String, format string) String {
|
||||
if format == "%s" { // Same as not using custom formatting.
|
||||
return str
|
||||
}
|
||||
|
||||
return NewSprintf(format, str)
|
||||
}
|
||||
700
vendor/fyne.io/fyne/v2/data/binding/trees.go
generated
vendored
Normal file
700
vendor/fyne.io/fyne/v2/data/binding/trees.go
generated
vendored
Normal file
@ -0,0 +1,700 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/storage"
|
||||
)
|
||||
|
||||
// DataTreeRootID const is the value used as ID for the root of any tree binding.
|
||||
const DataTreeRootID = ""
|
||||
|
||||
// DataTree is the base interface for all bindable data trees.
|
||||
//
|
||||
// Since: 2.4
|
||||
type DataTree interface {
|
||||
DataItem
|
||||
GetItem(id string) (DataItem, error)
|
||||
ChildIDs(string) []string
|
||||
}
|
||||
|
||||
// BoolTree supports binding a tree of bool values.
|
||||
//
|
||||
// Since: 2.4
|
||||
type BoolTree interface {
|
||||
DataTree
|
||||
|
||||
Append(parent, id string, value bool) error
|
||||
Get() (map[string][]string, map[string]bool, error)
|
||||
GetValue(id string) (bool, error)
|
||||
Prepend(parent, id string, value bool) error
|
||||
Remove(id string) error
|
||||
Set(ids map[string][]string, values map[string]bool) error
|
||||
SetValue(id string, value bool) error
|
||||
}
|
||||
|
||||
// ExternalBoolTree supports binding a tree of bool values from an external variable.
|
||||
//
|
||||
// Since: 2.4
|
||||
type ExternalBoolTree interface {
|
||||
BoolTree
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewBoolTree returns a bindable tree of bool values.
|
||||
//
|
||||
// Since: 2.4
|
||||
func NewBoolTree() BoolTree {
|
||||
return newTreeComparable[bool]()
|
||||
}
|
||||
|
||||
// BindBoolTree returns a bound tree of bool values, based on the contents of the passed values.
|
||||
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
|
||||
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.4
|
||||
func BindBoolTree(ids *map[string][]string, v *map[string]bool) ExternalBoolTree {
|
||||
return bindTreeComparable(ids, v)
|
||||
}
|
||||
|
||||
// BytesTree supports binding a tree of []byte values.
|
||||
//
|
||||
// Since: 2.4
|
||||
type BytesTree interface {
|
||||
DataTree
|
||||
|
||||
Append(parent, id string, value []byte) error
|
||||
Get() (map[string][]string, map[string][]byte, error)
|
||||
GetValue(id string) ([]byte, error)
|
||||
Prepend(parent, id string, value []byte) error
|
||||
Remove(id string) error
|
||||
Set(ids map[string][]string, values map[string][]byte) error
|
||||
SetValue(id string, value []byte) error
|
||||
}
|
||||
|
||||
// ExternalBytesTree supports binding a tree of []byte values from an external variable.
|
||||
//
|
||||
// Since: 2.4
|
||||
type ExternalBytesTree interface {
|
||||
BytesTree
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewBytesTree returns a bindable tree of []byte values.
|
||||
//
|
||||
// Since: 2.4
|
||||
func NewBytesTree() BytesTree {
|
||||
return newTree(bytes.Equal)
|
||||
}
|
||||
|
||||
// BindBytesTree returns a bound tree of []byte values, based on the contents of the passed values.
|
||||
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
|
||||
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.4
|
||||
func BindBytesTree(ids *map[string][]string, v *map[string][]byte) ExternalBytesTree {
|
||||
return bindTree(ids, v, bytes.Equal)
|
||||
}
|
||||
|
||||
// FloatTree supports binding a tree of float64 values.
|
||||
//
|
||||
// Since: 2.4
|
||||
type FloatTree interface {
|
||||
DataTree
|
||||
|
||||
Append(parent, id string, value float64) error
|
||||
Get() (map[string][]string, map[string]float64, error)
|
||||
GetValue(id string) (float64, error)
|
||||
Prepend(parent, id string, value float64) error
|
||||
Remove(id string) error
|
||||
Set(ids map[string][]string, values map[string]float64) error
|
||||
SetValue(id string, value float64) error
|
||||
}
|
||||
|
||||
// ExternalFloatTree supports binding a tree of float64 values from an external variable.
|
||||
//
|
||||
// Since: 2.4
|
||||
type ExternalFloatTree interface {
|
||||
FloatTree
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewFloatTree returns a bindable tree of float64 values.
|
||||
//
|
||||
// Since: 2.4
|
||||
func NewFloatTree() FloatTree {
|
||||
return newTreeComparable[float64]()
|
||||
}
|
||||
|
||||
// BindFloatTree returns a bound tree of float64 values, based on the contents of the passed values.
|
||||
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
|
||||
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.4
|
||||
func BindFloatTree(ids *map[string][]string, v *map[string]float64) ExternalFloatTree {
|
||||
return bindTreeComparable(ids, v)
|
||||
}
|
||||
|
||||
// IntTree supports binding a tree of int values.
|
||||
//
|
||||
// Since: 2.4
|
||||
type IntTree interface {
|
||||
DataTree
|
||||
|
||||
Append(parent, id string, value int) error
|
||||
Get() (map[string][]string, map[string]int, error)
|
||||
GetValue(id string) (int, error)
|
||||
Prepend(parent, id string, value int) error
|
||||
Remove(id string) error
|
||||
Set(ids map[string][]string, values map[string]int) error
|
||||
SetValue(id string, value int) error
|
||||
}
|
||||
|
||||
// ExternalIntTree supports binding a tree of int values from an external variable.
|
||||
//
|
||||
// Since: 2.4
|
||||
type ExternalIntTree interface {
|
||||
IntTree
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewIntTree returns a bindable tree of int values.
|
||||
//
|
||||
// Since: 2.4
|
||||
func NewIntTree() IntTree {
|
||||
return newTreeComparable[int]()
|
||||
}
|
||||
|
||||
// BindIntTree returns a bound tree of int values, based on the contents of the passed values.
|
||||
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
|
||||
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.4
|
||||
func BindIntTree(ids *map[string][]string, v *map[string]int) ExternalIntTree {
|
||||
return bindTreeComparable(ids, v)
|
||||
}
|
||||
|
||||
// RuneTree supports binding a tree of rune values.
|
||||
//
|
||||
// Since: 2.4
|
||||
type RuneTree interface {
|
||||
DataTree
|
||||
|
||||
Append(parent, id string, value rune) error
|
||||
Get() (map[string][]string, map[string]rune, error)
|
||||
GetValue(id string) (rune, error)
|
||||
Prepend(parent, id string, value rune) error
|
||||
Remove(id string) error
|
||||
Set(ids map[string][]string, values map[string]rune) error
|
||||
SetValue(id string, value rune) error
|
||||
}
|
||||
|
||||
// ExternalRuneTree supports binding a tree of rune values from an external variable.
|
||||
//
|
||||
// Since: 2.4
|
||||
type ExternalRuneTree interface {
|
||||
RuneTree
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewRuneTree returns a bindable tree of rune values.
|
||||
//
|
||||
// Since: 2.4
|
||||
func NewRuneTree() RuneTree {
|
||||
return newTreeComparable[rune]()
|
||||
}
|
||||
|
||||
// BindRuneTree returns a bound tree of rune values, based on the contents of the passed values.
|
||||
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
|
||||
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.4
|
||||
func BindRuneTree(ids *map[string][]string, v *map[string]rune) ExternalRuneTree {
|
||||
return bindTreeComparable(ids, v)
|
||||
}
|
||||
|
||||
// StringTree supports binding a tree of string values.
|
||||
//
|
||||
// Since: 2.4
|
||||
type StringTree interface {
|
||||
DataTree
|
||||
|
||||
Append(parent, id string, value string) error
|
||||
Get() (map[string][]string, map[string]string, error)
|
||||
GetValue(id string) (string, error)
|
||||
Prepend(parent, id string, value string) error
|
||||
Remove(id string) error
|
||||
Set(ids map[string][]string, values map[string]string) error
|
||||
SetValue(id string, value string) error
|
||||
}
|
||||
|
||||
// ExternalStringTree supports binding a tree of string values from an external variable.
|
||||
//
|
||||
// Since: 2.4
|
||||
type ExternalStringTree interface {
|
||||
StringTree
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewStringTree returns a bindable tree of string values.
|
||||
//
|
||||
// Since: 2.4
|
||||
func NewStringTree() StringTree {
|
||||
return newTreeComparable[string]()
|
||||
}
|
||||
|
||||
// BindStringTree returns a bound tree of string values, based on the contents of the passed values.
|
||||
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
|
||||
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.4
|
||||
func BindStringTree(ids *map[string][]string, v *map[string]string) ExternalStringTree {
|
||||
return bindTreeComparable(ids, v)
|
||||
}
|
||||
|
||||
// UntypedTree supports binding a tree of any values.
|
||||
//
|
||||
// Since: 2.5
|
||||
type UntypedTree interface {
|
||||
DataTree
|
||||
|
||||
Append(parent, id string, value any) error
|
||||
Get() (map[string][]string, map[string]any, error)
|
||||
GetValue(id string) (any, error)
|
||||
Prepend(parent, id string, value any) error
|
||||
Remove(id string) error
|
||||
Set(ids map[string][]string, values map[string]any) error
|
||||
SetValue(id string, value any) error
|
||||
}
|
||||
|
||||
// ExternalUntypedTree supports binding a tree of any values from an external variable.
|
||||
//
|
||||
// Since: 2.5
|
||||
type ExternalUntypedTree interface {
|
||||
UntypedTree
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewUntypedTree returns a bindable tree of any values.
|
||||
//
|
||||
// Since: 2.5
|
||||
func NewUntypedTree() UntypedTree {
|
||||
return newTree(func(a1, a2 any) bool { return a1 == a2 })
|
||||
}
|
||||
|
||||
// BindUntypedTree returns a bound tree of any values, based on the contents of the passed values.
|
||||
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
|
||||
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.4
|
||||
func BindUntypedTree(ids *map[string][]string, v *map[string]any) ExternalUntypedTree {
|
||||
return bindTree(ids, v, func(a1, a2 any) bool { return a1 == a2 })
|
||||
}
|
||||
|
||||
// URITree supports binding a tree of fyne.URI values.
|
||||
//
|
||||
// Since: 2.4
|
||||
type URITree interface {
|
||||
DataTree
|
||||
|
||||
Append(parent, id string, value fyne.URI) error
|
||||
Get() (map[string][]string, map[string]fyne.URI, error)
|
||||
GetValue(id string) (fyne.URI, error)
|
||||
Prepend(parent, id string, value fyne.URI) error
|
||||
Remove(id string) error
|
||||
Set(ids map[string][]string, values map[string]fyne.URI) error
|
||||
SetValue(id string, value fyne.URI) error
|
||||
}
|
||||
|
||||
// ExternalURITree supports binding a tree of fyne.URI values from an external variable.
|
||||
//
|
||||
// Since: 2.4
|
||||
type ExternalURITree interface {
|
||||
URITree
|
||||
|
||||
Reload() error
|
||||
}
|
||||
|
||||
// NewURITree returns a bindable tree of fyne.URI values.
|
||||
//
|
||||
// Since: 2.4
|
||||
func NewURITree() URITree {
|
||||
return newTree(storage.EqualURI)
|
||||
}
|
||||
|
||||
// BindURITree returns a bound tree of fyne.URI values, based on the contents of the passed values.
|
||||
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
|
||||
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
|
||||
//
|
||||
// Since: 2.4
|
||||
func BindURITree(ids *map[string][]string, v *map[string]fyne.URI) ExternalURITree {
|
||||
return bindTree(ids, v, storage.EqualURI)
|
||||
}
|
||||
|
||||
type treeBase struct {
|
||||
base
|
||||
|
||||
ids map[string][]string
|
||||
items map[string]DataItem
|
||||
}
|
||||
|
||||
// GetItem returns the DataItem at the specified id.
|
||||
func (t *treeBase) GetItem(id string) (DataItem, error) {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
if item, ok := t.items[id]; ok {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
return nil, errOutOfBounds
|
||||
}
|
||||
|
||||
// ChildIDs returns the ordered IDs of items in this data tree that are children of the specified ID.
|
||||
func (t *treeBase) ChildIDs(id string) []string {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
if ids, ok := t.ids[id]; ok {
|
||||
return ids
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (t *treeBase) appendItem(i DataItem, id, parent string) {
|
||||
t.items[id] = i
|
||||
ids, ok := t.ids[parent]
|
||||
if !ok {
|
||||
ids = make([]string, 0)
|
||||
}
|
||||
|
||||
for _, in := range ids {
|
||||
if in == id {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.ids[parent] = append(ids, id)
|
||||
}
|
||||
|
||||
func (t *treeBase) deleteItem(id, parent string) {
|
||||
delete(t.items, id)
|
||||
|
||||
ids, ok := t.ids[parent]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
off := -1
|
||||
for i, id2 := range ids {
|
||||
if id2 == id {
|
||||
off = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if off == -1 {
|
||||
return
|
||||
}
|
||||
t.ids[parent] = append(ids[:off], ids[off+1:]...)
|
||||
}
|
||||
|
||||
func parentIDFor(id string, ids map[string][]string) string {
|
||||
for parent, list := range ids {
|
||||
for _, child := range list {
|
||||
if child == id {
|
||||
return parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func newTree[T any](comparator func(T, T) bool) *boundTree[T] {
|
||||
t := &boundTree[T]{val: &map[string]T{}, comparator: comparator}
|
||||
t.ids = make(map[string][]string)
|
||||
t.items = make(map[string]DataItem)
|
||||
return t
|
||||
}
|
||||
|
||||
func newTreeComparable[T bool | float64 | int | rune | string]() *boundTree[T] {
|
||||
return newTree(func(t1, t2 T) bool { return t1 == t2 })
|
||||
}
|
||||
|
||||
func bindTree[T any](ids *map[string][]string, v *map[string]T, comparator func(T, T) bool) *boundTree[T] {
|
||||
if v == nil {
|
||||
return newTree[T](comparator)
|
||||
}
|
||||
|
||||
t := &boundTree[T]{val: v, updateExternal: true, comparator: comparator}
|
||||
t.ids = make(map[string][]string)
|
||||
t.items = make(map[string]DataItem)
|
||||
|
||||
for parent, children := range *ids {
|
||||
for _, leaf := range children {
|
||||
t.appendItem(bindTreeItem(v, leaf, t.updateExternal, t.comparator), leaf, parent)
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func bindTreeComparable[T bool | float64 | int | rune | string](ids *map[string][]string, v *map[string]T) *boundTree[T] {
|
||||
return bindTree(ids, v, func(t1, t2 T) bool { return t1 == t2 })
|
||||
}
|
||||
|
||||
type boundTree[T any] struct {
|
||||
treeBase
|
||||
|
||||
comparator func(T, T) bool
|
||||
val *map[string]T
|
||||
updateExternal bool
|
||||
}
|
||||
|
||||
func (t *boundTree[T]) Append(parent, id string, val T) error {
|
||||
t.lock.Lock()
|
||||
ids, ok := t.ids[parent]
|
||||
if !ok {
|
||||
ids = make([]string, 0)
|
||||
}
|
||||
|
||||
t.ids[parent] = append(ids, id)
|
||||
v := *t.val
|
||||
v[id] = val
|
||||
|
||||
trigger, err := t.doReload()
|
||||
t.lock.Unlock()
|
||||
|
||||
if trigger {
|
||||
t.trigger()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *boundTree[T]) Get() (map[string][]string, map[string]T, error) {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
return t.ids, *t.val, nil
|
||||
}
|
||||
|
||||
func (t *boundTree[T]) GetValue(id string) (T, error) {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
if item, ok := (*t.val)[id]; ok {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
return *new(T), errOutOfBounds
|
||||
}
|
||||
|
||||
func (t *boundTree[T]) Prepend(parent, id string, val T) error {
|
||||
t.lock.Lock()
|
||||
ids, ok := t.ids[parent]
|
||||
if !ok {
|
||||
ids = make([]string, 0)
|
||||
}
|
||||
|
||||
t.ids[parent] = append([]string{id}, ids...)
|
||||
v := *t.val
|
||||
v[id] = val
|
||||
|
||||
trigger, err := t.doReload()
|
||||
t.lock.Unlock()
|
||||
|
||||
if trigger {
|
||||
t.trigger()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *boundTree[T]) Remove(id string) error {
|
||||
t.lock.Lock()
|
||||
t.removeChildren(id)
|
||||
delete(t.ids, id)
|
||||
v := *t.val
|
||||
delete(v, id)
|
||||
|
||||
trigger, err := t.doReload()
|
||||
t.lock.Unlock()
|
||||
|
||||
if trigger {
|
||||
t.trigger()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *boundTree[T]) removeChildren(id string) {
|
||||
for _, cid := range t.ids[id] {
|
||||
t.removeChildren(cid)
|
||||
|
||||
delete(t.ids, cid)
|
||||
v := *t.val
|
||||
delete(v, cid)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *boundTree[T]) Reload() error {
|
||||
t.lock.Lock()
|
||||
trigger, err := t.doReload()
|
||||
t.lock.Unlock()
|
||||
|
||||
if trigger {
|
||||
t.trigger()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *boundTree[T]) Set(ids map[string][]string, v map[string]T) error {
|
||||
t.lock.Lock()
|
||||
t.ids = ids
|
||||
*t.val = v
|
||||
|
||||
trigger, err := t.doReload()
|
||||
t.lock.Unlock()
|
||||
|
||||
if trigger {
|
||||
t.trigger()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *boundTree[T]) doReload() (fire bool, retErr error) {
|
||||
updated := []string{}
|
||||
for id := range *t.val {
|
||||
found := false
|
||||
for child := range t.items {
|
||||
if child == id { // update existing
|
||||
updated = append(updated, id)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
|
||||
// append new
|
||||
t.appendItem(bindTreeItem(t.val, id, t.updateExternal, t.comparator), id, parentIDFor(id, t.ids))
|
||||
updated = append(updated, id)
|
||||
fire = true
|
||||
}
|
||||
|
||||
for id := range t.items {
|
||||
remove := true
|
||||
for _, done := range updated {
|
||||
if done == id {
|
||||
remove = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if remove { // remove item no longer present
|
||||
fire = true
|
||||
t.deleteItem(id, parentIDFor(id, t.ids))
|
||||
}
|
||||
}
|
||||
|
||||
for id, item := range t.items {
|
||||
var err error
|
||||
if t.updateExternal {
|
||||
err = item.(*boundExternalTreeItem[T]).setIfChanged((*t.val)[id])
|
||||
} else {
|
||||
err = item.(*boundTreeItem[T]).doSet((*t.val)[id])
|
||||
}
|
||||
if err != nil {
|
||||
retErr = err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *boundTree[T]) SetValue(id string, v T) error {
|
||||
t.lock.Lock()
|
||||
(*t.val)[id] = v
|
||||
t.lock.Unlock()
|
||||
|
||||
item, err := t.GetItem(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return item.(Item[T]).Set(v)
|
||||
}
|
||||
|
||||
func bindTreeItem[T any](v *map[string]T, id string, external bool, comparator func(T, T) bool) Item[T] {
|
||||
if external {
|
||||
ret := &boundExternalTreeItem[T]{old: (*v)[id], comparator: comparator}
|
||||
ret.val = v
|
||||
ret.id = id
|
||||
return ret
|
||||
}
|
||||
|
||||
return &boundTreeItem[T]{id: id, val: v}
|
||||
}
|
||||
|
||||
type boundTreeItem[T any] struct {
|
||||
base
|
||||
|
||||
val *map[string]T
|
||||
id string
|
||||
}
|
||||
|
||||
func (t *boundTreeItem[T]) Get() (T, error) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
v := *t.val
|
||||
if item, ok := v[t.id]; ok {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
return *new(T), errOutOfBounds
|
||||
}
|
||||
|
||||
func (t *boundTreeItem[T]) Set(val T) error {
|
||||
return t.doSet(val)
|
||||
}
|
||||
|
||||
func (t *boundTreeItem[T]) doSet(val T) error {
|
||||
t.lock.Lock()
|
||||
(*t.val)[t.id] = val
|
||||
t.lock.Unlock()
|
||||
|
||||
t.trigger()
|
||||
return nil
|
||||
}
|
||||
|
||||
type boundExternalTreeItem[T any] struct {
|
||||
boundTreeItem[T]
|
||||
|
||||
comparator func(T, T) bool
|
||||
old T
|
||||
}
|
||||
|
||||
func (t *boundExternalTreeItem[T]) setIfChanged(val T) error {
|
||||
t.lock.Lock()
|
||||
if t.comparator(val, t.old) {
|
||||
t.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
(*t.val)[t.id] = val
|
||||
t.old = val
|
||||
t.lock.Unlock()
|
||||
|
||||
t.trigger()
|
||||
return nil
|
||||
}
|
||||
44
vendor/fyne.io/fyne/v2/device.go
generated
vendored
Normal file
44
vendor/fyne.io/fyne/v2/device.go
generated
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
package fyne
|
||||
|
||||
// DeviceOrientation represents the different ways that a mobile device can be held
|
||||
type DeviceOrientation int
|
||||
|
||||
const (
|
||||
// OrientationVertical is the default vertical orientation
|
||||
OrientationVertical DeviceOrientation = iota
|
||||
// OrientationVerticalUpsideDown is the portrait orientation held upside down
|
||||
OrientationVerticalUpsideDown
|
||||
// OrientationHorizontalLeft is used to indicate a landscape orientation with the top to the left
|
||||
OrientationHorizontalLeft
|
||||
// OrientationHorizontalRight is used to indicate a landscape orientation with the top to the right
|
||||
OrientationHorizontalRight
|
||||
)
|
||||
|
||||
// IsVertical is a helper utility that determines if a passed orientation is vertical
|
||||
func IsVertical(orient DeviceOrientation) bool {
|
||||
return orient == OrientationVertical || orient == OrientationVerticalUpsideDown
|
||||
}
|
||||
|
||||
// IsHorizontal is a helper utility that determines if a passed orientation is horizontal
|
||||
func IsHorizontal(orient DeviceOrientation) bool {
|
||||
return !IsVertical(orient)
|
||||
}
|
||||
|
||||
// Device provides information about the devices the code is running on
|
||||
type Device interface {
|
||||
Orientation() DeviceOrientation
|
||||
IsMobile() bool
|
||||
IsBrowser() bool
|
||||
HasKeyboard() bool
|
||||
SystemScaleForWindow(Window) float32
|
||||
|
||||
// Locale returns the information about this device's language and region.
|
||||
//
|
||||
// Since: 2.5
|
||||
Locale() Locale
|
||||
}
|
||||
|
||||
// CurrentDevice returns the device information for the current hardware (via the driver)
|
||||
func CurrentDevice() Device {
|
||||
return CurrentApp().Driver().Device()
|
||||
}
|
||||
259
vendor/fyne.io/fyne/v2/dialog/base.go
generated
vendored
Normal file
259
vendor/fyne.io/fyne/v2/dialog/base.go
generated
vendored
Normal file
@ -0,0 +1,259 @@
|
||||
// Package dialog defines standard dialog windows for application GUIs.
|
||||
package dialog // import "fyne.io/fyne/v2/dialog"
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
col "fyne.io/fyne/v2/internal/color"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
const (
|
||||
padWidth = 32
|
||||
padHeight = 16
|
||||
)
|
||||
|
||||
// Dialog is the common API for any dialog window with a single dismiss button
|
||||
type Dialog interface {
|
||||
Show()
|
||||
Hide()
|
||||
SetDismissText(label string)
|
||||
SetOnClosed(closed func())
|
||||
Refresh()
|
||||
Resize(size fyne.Size)
|
||||
|
||||
// MinSize returns the size that this dialog should not shrink below.
|
||||
//
|
||||
// Since: 2.1
|
||||
MinSize() fyne.Size
|
||||
|
||||
// Dismiss instructs the dialog to close without any affirmative action.
|
||||
//
|
||||
// Since: 2.6
|
||||
Dismiss()
|
||||
}
|
||||
|
||||
// Declare conformity to Dialog interface
|
||||
var _ Dialog = (*dialog)(nil)
|
||||
|
||||
type dialog struct {
|
||||
callback func(bool)
|
||||
title string
|
||||
icon fyne.Resource
|
||||
desiredSize fyne.Size
|
||||
|
||||
win *widget.PopUp
|
||||
content fyne.CanvasObject
|
||||
dismiss *widget.Button
|
||||
parent fyne.Window
|
||||
|
||||
// allows derived dialogs to inject logic that runs before Show()
|
||||
beforeShowHook func()
|
||||
}
|
||||
|
||||
func (d *dialog) Dismiss() {
|
||||
d.Hide()
|
||||
}
|
||||
|
||||
func (d *dialog) Hide() {
|
||||
d.hideWithResponse(false)
|
||||
}
|
||||
|
||||
// MinSize returns the size that this dialog should not shrink below.
|
||||
//
|
||||
// Since: 2.1
|
||||
func (d *dialog) MinSize() fyne.Size {
|
||||
return d.win.MinSize()
|
||||
}
|
||||
|
||||
func (d *dialog) Show() {
|
||||
if d.beforeShowHook != nil {
|
||||
d.beforeShowHook()
|
||||
}
|
||||
if !d.desiredSize.IsZero() {
|
||||
d.win.Resize(d.desiredSize)
|
||||
}
|
||||
d.win.Show()
|
||||
}
|
||||
|
||||
func (d *dialog) Refresh() {
|
||||
d.win.Refresh()
|
||||
}
|
||||
|
||||
// Resize dialog, call this function after dialog show
|
||||
func (d *dialog) Resize(size fyne.Size) {
|
||||
d.desiredSize = size
|
||||
if d.win != nil { // could be called before popup is created!
|
||||
d.win.Resize(size)
|
||||
}
|
||||
}
|
||||
|
||||
// SetDismissText allows custom text to be set in the dismiss button
|
||||
// This is a no-op for dialogs without dismiss buttons.
|
||||
func (d *dialog) SetDismissText(label string) {
|
||||
if d.dismiss == nil {
|
||||
return
|
||||
}
|
||||
|
||||
d.dismiss.SetText(label)
|
||||
d.win.Refresh()
|
||||
}
|
||||
|
||||
// SetOnClosed allows to set a callback function that is called when
|
||||
// the dialog is closed
|
||||
func (d *dialog) SetOnClosed(closed func()) {
|
||||
// if there is already a callback set, remember it and call both
|
||||
originalCallback := d.callback
|
||||
|
||||
d.callback = func(response bool) {
|
||||
if originalCallback != nil {
|
||||
originalCallback(response)
|
||||
}
|
||||
closed()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dialog) hideWithResponse(resp bool) {
|
||||
d.win.Hide()
|
||||
if d.callback != nil {
|
||||
d.callback(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dialog) create(buttons fyne.CanvasObject) {
|
||||
label := widget.NewLabelWithStyle(d.title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||
|
||||
var image fyne.CanvasObject
|
||||
if d.icon != nil {
|
||||
image = &canvas.Image{Resource: d.icon}
|
||||
} else {
|
||||
image = &layout.Spacer{}
|
||||
}
|
||||
|
||||
content := container.New(&dialogLayout{d: d},
|
||||
image,
|
||||
newThemedBackground(),
|
||||
d.content,
|
||||
buttons,
|
||||
label,
|
||||
)
|
||||
|
||||
d.win = widget.NewModalPopUp(content, d.parent.Canvas())
|
||||
}
|
||||
|
||||
func (d *dialog) setButtons(buttons fyne.CanvasObject) {
|
||||
d.win.Content.(*fyne.Container).Objects[3] = buttons
|
||||
d.win.Refresh()
|
||||
}
|
||||
|
||||
func (d *dialog) setIcon(icon fyne.Resource) {
|
||||
if icon == nil {
|
||||
d.win.Content.(*fyne.Container).Objects[0] = &layout.Spacer{}
|
||||
d.win.Refresh()
|
||||
return
|
||||
}
|
||||
d.win.Content.(*fyne.Container).Objects[0] = &canvas.Image{Resource: icon}
|
||||
d.win.Refresh()
|
||||
}
|
||||
|
||||
// The method .create() needs to be called before the dialog can be shown.
|
||||
func newDialog(title, message string, icon fyne.Resource, callback func(bool), parent fyne.Window) *dialog {
|
||||
d := &dialog{content: newCenterWrappedLabel(message), title: title, icon: icon, parent: parent}
|
||||
d.callback = callback
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// ThemedBackground
|
||||
// ===============================================================
|
||||
|
||||
type themedBackground struct {
|
||||
widget.BaseWidget
|
||||
}
|
||||
|
||||
func newThemedBackground() *themedBackground {
|
||||
t := &themedBackground{}
|
||||
t.ExtendBaseWidget(t)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *themedBackground) CreateRenderer() fyne.WidgetRenderer {
|
||||
t.ExtendBaseWidget(t)
|
||||
rect := canvas.NewRectangle(theme.Color(theme.ColorNameOverlayBackground))
|
||||
return &themedBackgroundRenderer{rect, []fyne.CanvasObject{rect}}
|
||||
}
|
||||
|
||||
type themedBackgroundRenderer struct {
|
||||
rect *canvas.Rectangle
|
||||
objects []fyne.CanvasObject
|
||||
}
|
||||
|
||||
func (renderer *themedBackgroundRenderer) Destroy() {
|
||||
}
|
||||
|
||||
func (renderer *themedBackgroundRenderer) Layout(size fyne.Size) {
|
||||
renderer.rect.Resize(size)
|
||||
}
|
||||
|
||||
func (renderer *themedBackgroundRenderer) MinSize() fyne.Size {
|
||||
return renderer.rect.MinSize()
|
||||
}
|
||||
|
||||
func (renderer *themedBackgroundRenderer) Objects() []fyne.CanvasObject {
|
||||
return renderer.objects
|
||||
}
|
||||
|
||||
func (renderer *themedBackgroundRenderer) Refresh() {
|
||||
r, g, b, _ := col.ToNRGBA(theme.Color(theme.ColorNameOverlayBackground))
|
||||
bg := &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 230}
|
||||
renderer.rect.FillColor = bg
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// DialogLayout
|
||||
// ===============================================================
|
||||
|
||||
type dialogLayout struct {
|
||||
d *dialog
|
||||
}
|
||||
|
||||
func (l *dialogLayout) Layout(obj []fyne.CanvasObject, size fyne.Size) {
|
||||
btnMin := obj[3].MinSize()
|
||||
labelMin := obj[4].MinSize()
|
||||
|
||||
// icon
|
||||
iconHeight := padHeight*2 + labelMin.Height*2 - theme.Padding()
|
||||
obj[0].Resize(fyne.NewSize(iconHeight, iconHeight))
|
||||
obj[0].Move(fyne.NewPos(size.Width-iconHeight+theme.Padding(), -theme.Padding()))
|
||||
|
||||
// background
|
||||
obj[1].Move(fyne.NewPos(0, 0))
|
||||
obj[1].Resize(size)
|
||||
|
||||
// content
|
||||
contentStart := obj[4].Position().Y + labelMin.Height + padHeight
|
||||
contentEnd := obj[3].Position().Y - theme.Padding()
|
||||
obj[2].Move(fyne.NewPos(padWidth/2, labelMin.Height+padHeight))
|
||||
obj[2].Resize(fyne.NewSize(size.Width-padWidth, contentEnd-contentStart))
|
||||
|
||||
// buttons
|
||||
obj[3].Resize(btnMin)
|
||||
obj[3].Move(fyne.NewPos(size.Width/2-(btnMin.Width/2), size.Height-padHeight-btnMin.Height))
|
||||
}
|
||||
|
||||
func (l *dialogLayout) MinSize(obj []fyne.CanvasObject) fyne.Size {
|
||||
contentMin := obj[2].MinSize()
|
||||
btnMin := obj[3].MinSize()
|
||||
labelMin := obj[4].MinSize()
|
||||
|
||||
width := fyne.Max(fyne.Max(contentMin.Width, btnMin.Width), labelMin.Width) + padWidth
|
||||
height := contentMin.Height + btnMin.Height + labelMin.Height + theme.Padding() + padHeight*2
|
||||
|
||||
return fyne.NewSize(width, height)
|
||||
}
|
||||
361
vendor/fyne.io/fyne/v2/dialog/color.go
generated
vendored
Normal file
361
vendor/fyne.io/fyne/v2/dialog/color.go
generated
vendored
Normal file
@ -0,0 +1,361 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
"math/cmplx"
|
||||
"strings"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
col "fyne.io/fyne/v2/internal/color"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
const (
|
||||
checkeredBoxSize = 8
|
||||
checkeredNumberOfRings = 12
|
||||
|
||||
preferenceRecents = "color_recents"
|
||||
preferenceMaxRecents = 7
|
||||
)
|
||||
|
||||
// ColorPickerDialog is a simple dialog window that displays a color picker.
|
||||
//
|
||||
// Since: 1.4
|
||||
type ColorPickerDialog struct {
|
||||
*dialog
|
||||
Advanced bool
|
||||
color color.Color
|
||||
callback func(c color.Color)
|
||||
advanced *widget.Accordion
|
||||
picker *colorAdvancedPicker
|
||||
}
|
||||
|
||||
// NewColorPicker creates a color dialog and returns the handle.
|
||||
// Using the returned type you should call Show() and then set its color through SetColor().
|
||||
// The callback is triggered when the user selects a color.
|
||||
//
|
||||
// Since: 1.4
|
||||
func NewColorPicker(title, message string, callback func(c color.Color), parent fyne.Window) *ColorPickerDialog {
|
||||
return &ColorPickerDialog{
|
||||
dialog: newDialog(title, message, theme.ColorPaletteIcon(), nil /*cancel?*/, parent),
|
||||
color: theme.Color(theme.ColorNamePrimary),
|
||||
callback: callback,
|
||||
}
|
||||
}
|
||||
|
||||
// ShowColorPicker creates and shows a color dialog.
|
||||
// The callback is triggered when the user selects a color.
|
||||
//
|
||||
// Since: 1.4
|
||||
func ShowColorPicker(title, message string, callback func(c color.Color), parent fyne.Window) {
|
||||
NewColorPicker(title, message, callback, parent).Show()
|
||||
}
|
||||
|
||||
// Refresh causes this dialog to be updated
|
||||
func (p *ColorPickerDialog) Refresh() {
|
||||
p.updateUI()
|
||||
}
|
||||
|
||||
// SetColor updates the color of the color picker.
|
||||
func (p *ColorPickerDialog) SetColor(c color.Color) {
|
||||
if p.picker == nil && p.Advanced {
|
||||
p.updateUI()
|
||||
} else if !p.Advanced {
|
||||
fyne.LogError("Advanced mode needs to be enabled to use SetColor", nil)
|
||||
return
|
||||
}
|
||||
p.picker.SetColor(c)
|
||||
}
|
||||
|
||||
// Show causes this dialog to be displayed
|
||||
func (p *ColorPickerDialog) Show() {
|
||||
if p.win == nil || p.Advanced != (p.advanced != nil) {
|
||||
p.updateUI()
|
||||
}
|
||||
p.dialog.Show()
|
||||
}
|
||||
|
||||
func (p *ColorPickerDialog) createSimplePickers() (contents []fyne.CanvasObject) {
|
||||
contents = append(contents, newColorBasicPicker(p.selectColor), newColorGreyscalePicker(p.selectColor))
|
||||
if recent := newColorRecentPicker(p.selectColor); len(recent.(*fyne.Container).Objects) > 0 {
|
||||
// Add divider and recents if there are any,
|
||||
contents = append(contents, canvas.NewLine(theme.Color(theme.ColorNameShadow)), recent)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ColorPickerDialog) selectColor(c color.Color) {
|
||||
writeRecentColor(colorToString(c))
|
||||
if p.picker != nil {
|
||||
p.picker.SetColor(c)
|
||||
}
|
||||
if f := p.callback; f != nil {
|
||||
f(c)
|
||||
}
|
||||
p.dialog.Hide()
|
||||
p.updateUI()
|
||||
}
|
||||
|
||||
func (p *ColorPickerDialog) updateUI() {
|
||||
if w := p.win; w != nil {
|
||||
w.Hide()
|
||||
}
|
||||
p.dialog.dismiss = &widget.Button{Text: lang.L("Cancel"), Icon: theme.CancelIcon(),
|
||||
OnTapped: p.dialog.Hide,
|
||||
}
|
||||
if p.Advanced {
|
||||
p.picker = newColorAdvancedPicker(p.color, func(c color.Color) {
|
||||
p.color = c
|
||||
})
|
||||
|
||||
advancedItem := widget.NewAccordionItem(lang.L("Advanced"), p.picker)
|
||||
if p.advanced != nil {
|
||||
advancedItem.Open = p.advanced.Items[0].Open
|
||||
}
|
||||
p.advanced = widget.NewAccordion(advancedItem)
|
||||
|
||||
p.dialog.content = container.NewVBox(
|
||||
container.NewCenter(
|
||||
container.NewVBox(
|
||||
p.createSimplePickers()...,
|
||||
),
|
||||
),
|
||||
widget.NewSeparator(),
|
||||
p.advanced,
|
||||
)
|
||||
|
||||
confirm := &widget.Button{Text: lang.L("Confirm"), Icon: theme.ConfirmIcon(), Importance: widget.HighImportance,
|
||||
OnTapped: func() {
|
||||
p.selectColor(p.color)
|
||||
},
|
||||
}
|
||||
p.dialog.create(container.NewGridWithColumns(2, p.dialog.dismiss, confirm))
|
||||
} else {
|
||||
p.dialog.content = container.NewVBox(p.createSimplePickers()...)
|
||||
p.dialog.create(container.NewGridWithColumns(1, p.dialog.dismiss))
|
||||
}
|
||||
}
|
||||
|
||||
func clamp(value, min, max int) int {
|
||||
if value < min {
|
||||
return min
|
||||
}
|
||||
if value > max {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func wrapHue(hue int) int {
|
||||
for hue < 0 {
|
||||
hue += 360
|
||||
}
|
||||
for hue > 360 {
|
||||
hue -= 360
|
||||
}
|
||||
return hue
|
||||
}
|
||||
|
||||
func newColorButtonBox(colors []color.Color, icon fyne.Resource, callback func(color.Color)) fyne.CanvasObject {
|
||||
var objects []fyne.CanvasObject
|
||||
if icon != nil && len(colors) > 0 {
|
||||
objects = append(objects, widget.NewIcon(icon))
|
||||
}
|
||||
for _, c := range colors {
|
||||
objects = append(objects, newColorButton(c, callback))
|
||||
}
|
||||
return container.NewGridWithColumns(8, objects...)
|
||||
}
|
||||
|
||||
func newCheckeredBackground(radial bool) *canvas.Raster {
|
||||
f := func(x, y, _, _ int) color.Color {
|
||||
if (x/checkeredBoxSize)%2 == (y/checkeredBoxSize)%2 {
|
||||
return color.Gray{Y: 58}
|
||||
}
|
||||
|
||||
return color.Gray{Y: 84}
|
||||
}
|
||||
|
||||
if radial {
|
||||
rect := f
|
||||
f = func(x, y, w, h int) color.Color {
|
||||
r, t := cmplx.Polar(complex(float64(x)-float64(w)/2, float64(y)-float64(h)/2))
|
||||
limit := math.Min(float64(w), float64(h)) / 2.0
|
||||
if r > limit {
|
||||
// Out of bounds
|
||||
return &color.NRGBA{A: 0}
|
||||
}
|
||||
|
||||
x = int((t + math.Pi) / (2 * math.Pi) * checkeredNumberOfRings * checkeredBoxSize)
|
||||
y = int(r)
|
||||
return rect(x, y, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return canvas.NewRasterWithPixels(f)
|
||||
}
|
||||
|
||||
func readRecentColors() (recents []string) {
|
||||
for _, r := range strings.Split(fyne.CurrentApp().Preferences().String(preferenceRecents), ",") {
|
||||
if r != "" {
|
||||
recents = append(recents, r)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeRecentColor(color string) {
|
||||
recents := []string{color}
|
||||
for _, r := range readRecentColors() {
|
||||
if r == color {
|
||||
continue // Color already in recents
|
||||
}
|
||||
recents = append(recents, r)
|
||||
}
|
||||
if len(recents) > preferenceMaxRecents {
|
||||
recents = recents[:preferenceMaxRecents]
|
||||
}
|
||||
fyne.CurrentApp().Preferences().SetString(preferenceRecents, strings.Join(recents, ","))
|
||||
}
|
||||
|
||||
func colorToString(c color.Color) string {
|
||||
red, green, blue, alpha := col.ToNRGBA(c)
|
||||
if alpha == 0xff {
|
||||
return fmt.Sprintf("#%02x%02x%02x", red, green, blue)
|
||||
}
|
||||
return fmt.Sprintf("#%02x%02x%02x%02x", red, green, blue, alpha)
|
||||
}
|
||||
|
||||
func stringToColor(s string) (color.Color, error) {
|
||||
var c color.NRGBA
|
||||
var err error
|
||||
if len(s) == 7 {
|
||||
c.A = 0xFF
|
||||
_, err = fmt.Sscanf(s, "#%02x%02x%02x", &c.R, &c.G, &c.B)
|
||||
} else {
|
||||
_, err = fmt.Sscanf(s, "#%02x%02x%02x%02x", &c.R, &c.G, &c.B, &c.A)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
func stringsToColors(ss ...string) (colors []color.Color) {
|
||||
for _, s := range ss {
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
c, err := stringToColor(s)
|
||||
if err != nil {
|
||||
fyne.LogError("Couldn't parse color:", err)
|
||||
} else {
|
||||
colors = append(colors, c)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func colorToHSLA(c color.Color) (int, int, int, int) {
|
||||
r, g, b, a := col.ToNRGBA(c)
|
||||
h, s, l := rgbToHsl(r, g, b)
|
||||
return h, s, l, a
|
||||
}
|
||||
|
||||
// https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
|
||||
|
||||
func rgbToHsl(r, g, b int) (int, int, int) {
|
||||
red := float64(r) / 255.0
|
||||
green := float64(g) / 255.0
|
||||
blue := float64(b) / 255.0
|
||||
|
||||
min := math.Min(red, math.Min(green, blue))
|
||||
max := math.Max(red, math.Max(green, blue))
|
||||
|
||||
lightness := (max + min) / 2.0
|
||||
|
||||
delta := max - min
|
||||
|
||||
if delta == 0.0 {
|
||||
// Achromatic
|
||||
return 0, 0, int(lightness * 100.0)
|
||||
}
|
||||
|
||||
// Chromatic
|
||||
|
||||
var saturation float64
|
||||
|
||||
if lightness < 0.5 {
|
||||
saturation = (max - min) / (max + min)
|
||||
} else {
|
||||
saturation = (max - min) / (2.0 - max - min)
|
||||
}
|
||||
|
||||
var hue float64
|
||||
|
||||
if red == max {
|
||||
hue = (green - blue) / delta
|
||||
} else if green == max {
|
||||
hue = 2.0 + (blue-red)/delta
|
||||
} else if blue == max {
|
||||
hue = 4.0 + (red-green)/delta
|
||||
}
|
||||
|
||||
h := wrapHue(int(hue * 60.0))
|
||||
s := int(saturation * 100.0)
|
||||
l := int(lightness * 100.0)
|
||||
return h, s, l
|
||||
}
|
||||
|
||||
func hslToRgb(h, s, l int) (int, int, int) {
|
||||
hue := float64(h) / 360.0
|
||||
saturation := float64(s) / 100.0
|
||||
lightness := float64(l) / 100.0
|
||||
|
||||
if saturation == 0.0 {
|
||||
// Greyscale
|
||||
g := int(lightness * 255.0)
|
||||
return g, g, g
|
||||
}
|
||||
|
||||
var v1 float64
|
||||
if lightness < 0.5 {
|
||||
v1 = lightness * (1.0 + saturation)
|
||||
} else {
|
||||
v1 = (lightness + saturation) - (lightness * saturation)
|
||||
}
|
||||
|
||||
v2 := 2.0*lightness - v1
|
||||
|
||||
red := hueToChannel(hue+(1.0/3.0), v1, v2)
|
||||
green := hueToChannel(hue, v1, v2)
|
||||
blue := hueToChannel(hue-(1.0/3.0), v1, v2)
|
||||
|
||||
r := int(math.Round(255.0 * red))
|
||||
g := int(math.Round(255.0 * green))
|
||||
b := int(math.Round(255.0 * blue))
|
||||
|
||||
return r, g, b
|
||||
}
|
||||
|
||||
func hueToChannel(h, v1, v2 float64) float64 {
|
||||
for h < 0.0 {
|
||||
h += 1.0
|
||||
}
|
||||
for h > 1.0 {
|
||||
h -= 1.0
|
||||
}
|
||||
if 6.0*h < 1.0 {
|
||||
return v2 + (v1-v2)*6*h
|
||||
}
|
||||
if 2.0*h < 1.0 {
|
||||
return v1
|
||||
}
|
||||
if 3.0*h < 2.0 {
|
||||
return v2 + (v1-v2)*6*((2.0/3.0)-h)
|
||||
}
|
||||
return v2
|
||||
}
|
||||
114
vendor/fyne.io/fyne/v2/dialog/color_button.go
generated
vendored
Normal file
114
vendor/fyne.io/fyne/v2/dialog/color_button.go
generated
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
internalwidget "fyne.io/fyne/v2/internal/widget"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
var _ fyne.Widget = (*colorButton)(nil)
|
||||
var _ desktop.Hoverable = (*colorButton)(nil)
|
||||
|
||||
// colorButton displays a color and triggers the callback when tapped.
|
||||
type colorButton struct {
|
||||
widget.BaseWidget
|
||||
color color.Color
|
||||
onTap func(color.Color)
|
||||
hovered bool
|
||||
}
|
||||
|
||||
// newColorButton creates a colorButton with the given color and callback.
|
||||
func newColorButton(color color.Color, onTap func(color.Color)) *colorButton {
|
||||
b := &colorButton{
|
||||
color: color,
|
||||
onTap: onTap,
|
||||
}
|
||||
b.ExtendBaseWidget(b)
|
||||
return b
|
||||
}
|
||||
|
||||
// CreateRenderer is a private method to Fyne which links this widget to its renderer
|
||||
func (b *colorButton) CreateRenderer() fyne.WidgetRenderer {
|
||||
b.ExtendBaseWidget(b)
|
||||
background := newCheckeredBackground(false)
|
||||
rectangle := &canvas.Rectangle{
|
||||
FillColor: b.color,
|
||||
}
|
||||
return &colorButtonRenderer{
|
||||
BaseRenderer: internalwidget.NewBaseRenderer([]fyne.CanvasObject{background, rectangle}),
|
||||
button: b,
|
||||
background: background,
|
||||
rectangle: rectangle,
|
||||
}
|
||||
}
|
||||
|
||||
// MouseIn is called when a desktop pointer enters the widget
|
||||
func (b *colorButton) MouseIn(*desktop.MouseEvent) {
|
||||
b.hovered = true
|
||||
b.Refresh()
|
||||
}
|
||||
|
||||
// MouseOut is called when a desktop pointer exits the widget
|
||||
func (b *colorButton) MouseOut() {
|
||||
b.hovered = false
|
||||
b.Refresh()
|
||||
}
|
||||
|
||||
// MouseMoved is called when a desktop pointer hovers over the widget
|
||||
func (b *colorButton) MouseMoved(*desktop.MouseEvent) {
|
||||
}
|
||||
|
||||
// MinSize returns the size that this widget should not shrink below
|
||||
func (b *colorButton) MinSize() fyne.Size {
|
||||
return b.BaseWidget.MinSize()
|
||||
}
|
||||
|
||||
// SetColor updates the color selected in this color widget
|
||||
func (b *colorButton) SetColor(color color.Color) {
|
||||
if b.color == color {
|
||||
return
|
||||
}
|
||||
b.color = color
|
||||
b.Refresh()
|
||||
}
|
||||
|
||||
// Tapped is called when a pointer tapped event is captured and triggers any change handler
|
||||
func (b *colorButton) Tapped(*fyne.PointEvent) {
|
||||
if f := b.onTap; f != nil {
|
||||
f(b.color)
|
||||
}
|
||||
}
|
||||
|
||||
type colorButtonRenderer struct {
|
||||
internalwidget.BaseRenderer
|
||||
button *colorButton
|
||||
background *canvas.Raster
|
||||
rectangle *canvas.Rectangle
|
||||
}
|
||||
|
||||
func (r *colorButtonRenderer) Layout(size fyne.Size) {
|
||||
r.rectangle.Move(fyne.NewPos(0, 0))
|
||||
r.rectangle.Resize(size)
|
||||
r.background.Resize(size)
|
||||
}
|
||||
|
||||
func (r *colorButtonRenderer) MinSize() fyne.Size {
|
||||
return r.rectangle.MinSize().Max(fyne.NewSize(32, 32))
|
||||
}
|
||||
|
||||
func (r *colorButtonRenderer) Refresh() {
|
||||
if r.button.hovered {
|
||||
r.rectangle.StrokeColor = theme.Color(theme.ColorNameHover)
|
||||
r.rectangle.StrokeWidth = theme.Padding()
|
||||
} else {
|
||||
r.rectangle.StrokeWidth = 0
|
||||
}
|
||||
r.rectangle.FillColor = r.button.color
|
||||
r.background.Refresh()
|
||||
canvas.Refresh(r.button)
|
||||
}
|
||||
187
vendor/fyne.io/fyne/v2/dialog/color_channel.go
generated
vendored
Normal file
187
vendor/fyne.io/fyne/v2/dialog/color_channel.go
generated
vendored
Normal file
@ -0,0 +1,187 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
internalwidget "fyne.io/fyne/v2/internal/widget"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
var _ fyne.Widget = (*colorChannel)(nil)
|
||||
|
||||
// colorChannel controls a channel of a color and triggers the callback when changed.
|
||||
type colorChannel struct {
|
||||
widget.BaseWidget
|
||||
name string
|
||||
min, max int
|
||||
value int
|
||||
onChanged func(int)
|
||||
}
|
||||
|
||||
// newColorChannel returns a new color channel control for the channel with the given name.
|
||||
func newColorChannel(name string, min, max, value int, onChanged func(int)) *colorChannel {
|
||||
c := &colorChannel{
|
||||
name: name,
|
||||
min: min,
|
||||
max: max,
|
||||
value: clamp(value, min, max),
|
||||
onChanged: onChanged,
|
||||
}
|
||||
c.ExtendBaseWidget(c)
|
||||
return c
|
||||
}
|
||||
|
||||
// CreateRenderer is a private method to Fyne which links this widget to its renderer
|
||||
func (c *colorChannel) CreateRenderer() fyne.WidgetRenderer {
|
||||
label := widget.NewLabelWithStyle(c.name, fyne.TextAlignTrailing, fyne.TextStyle{Bold: true})
|
||||
entry := newColorChannelEntry(c)
|
||||
slider := &widget.Slider{
|
||||
Value: 0.0,
|
||||
Min: float64(c.min),
|
||||
Max: float64(c.max),
|
||||
Step: 1.0,
|
||||
Orientation: widget.Horizontal,
|
||||
OnChanged: func(value float64) {
|
||||
c.SetValue(int(value))
|
||||
},
|
||||
}
|
||||
r := &colorChannelRenderer{
|
||||
BaseRenderer: internalwidget.NewBaseRenderer([]fyne.CanvasObject{
|
||||
label,
|
||||
slider,
|
||||
entry,
|
||||
}),
|
||||
control: c,
|
||||
label: label,
|
||||
entry: entry,
|
||||
slider: slider,
|
||||
}
|
||||
r.updateObjects()
|
||||
return r
|
||||
}
|
||||
|
||||
// MinSize returns the size that this widget should not shrink below
|
||||
func (c *colorChannel) MinSize() fyne.Size {
|
||||
c.ExtendBaseWidget(c)
|
||||
return c.BaseWidget.MinSize()
|
||||
}
|
||||
|
||||
// SetValue updates the value in this color widget
|
||||
func (c *colorChannel) SetValue(value int) {
|
||||
value = clamp(value, c.min, c.max)
|
||||
if c.value == value {
|
||||
return
|
||||
}
|
||||
c.value = value
|
||||
c.Refresh()
|
||||
if f := c.onChanged; f != nil {
|
||||
f(value)
|
||||
}
|
||||
}
|
||||
|
||||
type colorChannelRenderer struct {
|
||||
internalwidget.BaseRenderer
|
||||
control *colorChannel
|
||||
label *widget.Label
|
||||
entry *colorChannelEntry
|
||||
slider *widget.Slider
|
||||
}
|
||||
|
||||
func (r *colorChannelRenderer) Layout(size fyne.Size) {
|
||||
lMin := r.label.MinSize()
|
||||
eMin := r.entry.MinSize()
|
||||
r.label.Move(fyne.NewPos(0, (size.Height-lMin.Height)/2))
|
||||
r.label.Resize(fyne.NewSize(lMin.Width, lMin.Height))
|
||||
r.slider.Move(fyne.NewPos(lMin.Width, 0))
|
||||
r.slider.Resize(fyne.NewSize(size.Width-lMin.Width-eMin.Width, size.Height))
|
||||
r.entry.Move(fyne.NewPos(size.Width-eMin.Width, 0))
|
||||
r.entry.Resize(fyne.NewSize(eMin.Width, size.Height))
|
||||
}
|
||||
|
||||
func (r *colorChannelRenderer) MinSize() fyne.Size {
|
||||
lMin := r.label.MinSize()
|
||||
sMin := r.slider.MinSize()
|
||||
eMin := r.entry.MinSize()
|
||||
return fyne.NewSize(
|
||||
lMin.Width+sMin.Width+eMin.Width,
|
||||
fyne.Max(lMin.Height, fyne.Max(sMin.Height, eMin.Height)),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *colorChannelRenderer) Refresh() {
|
||||
r.updateObjects()
|
||||
r.Layout(r.control.Size())
|
||||
canvas.Refresh(r.control)
|
||||
}
|
||||
|
||||
func (r *colorChannelRenderer) updateObjects() {
|
||||
r.entry.SetText(strconv.Itoa(r.control.value))
|
||||
r.slider.Value = float64(r.control.value)
|
||||
r.slider.Refresh()
|
||||
}
|
||||
|
||||
type colorChannelEntry struct {
|
||||
userChangeEntry
|
||||
}
|
||||
|
||||
func newColorChannelEntry(c *colorChannel) *colorChannelEntry {
|
||||
e := &colorChannelEntry{}
|
||||
e.Text = "0"
|
||||
e.ExtendBaseWidget(e)
|
||||
e.setOnChanged(func(text string) {
|
||||
value, err := strconv.Atoi(text)
|
||||
if err != nil {
|
||||
fyne.LogError("Couldn't parse: "+text, err)
|
||||
return
|
||||
}
|
||||
c.SetValue(value)
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *colorChannelEntry) MinSize() fyne.Size {
|
||||
// Ensure space for 3 digits
|
||||
min := fyne.MeasureText("000", theme.TextSize(), fyne.TextStyle{})
|
||||
min = min.Add(fyne.NewSize(theme.Padding()*6, theme.Padding()*4))
|
||||
return min.Max(e.Entry.MinSize())
|
||||
}
|
||||
|
||||
type userChangeEntry struct {
|
||||
widget.Entry
|
||||
userTyped bool
|
||||
}
|
||||
|
||||
func newUserChangeEntry(text string) *userChangeEntry {
|
||||
e := &userChangeEntry{}
|
||||
e.Entry.Text = text
|
||||
e.ExtendBaseWidget(e)
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *userChangeEntry) setOnChanged(onChanged func(s string)) {
|
||||
e.Entry.OnChanged = func(text string) {
|
||||
if !e.userTyped {
|
||||
return
|
||||
}
|
||||
|
||||
e.userTyped = false
|
||||
|
||||
if onChanged != nil {
|
||||
onChanged(text)
|
||||
}
|
||||
}
|
||||
e.ExtendBaseWidget(e)
|
||||
}
|
||||
|
||||
func (e *userChangeEntry) TypedRune(r rune) {
|
||||
e.userTyped = true
|
||||
e.Entry.TypedRune(r)
|
||||
}
|
||||
|
||||
func (e *userChangeEntry) TypedKey(ev *fyne.KeyEvent) {
|
||||
e.userTyped = true
|
||||
e.Entry.TypedKey(ev)
|
||||
}
|
||||
304
vendor/fyne.io/fyne/v2/dialog/color_picker.go
generated
vendored
Normal file
304
vendor/fyne.io/fyne/v2/dialog/color_picker.go
generated
vendored
Normal file
@ -0,0 +1,304 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
col "fyne.io/fyne/v2/internal/color"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// newColorBasicPicker returns a component for selecting basic colors.
|
||||
func newColorBasicPicker(callback func(color.Color)) fyne.CanvasObject {
|
||||
return newColorButtonBox(
|
||||
stringsToColors(
|
||||
"#f44336", // red
|
||||
"#ff9800", // orange
|
||||
"#ffeb3b", // yellow
|
||||
"#8bc34a", // green
|
||||
"#296ff6", // blue
|
||||
"#9c27b0", // purple
|
||||
"#795548", // brown
|
||||
),
|
||||
theme.ColorChromaticIcon(),
|
||||
callback,
|
||||
)
|
||||
}
|
||||
|
||||
// newColorGreyscalePicker returns a component for selecting greyscale colors.
|
||||
func newColorGreyscalePicker(callback func(color.Color)) fyne.CanvasObject {
|
||||
return newColorButtonBox(
|
||||
stringsToColors(
|
||||
"#ffffff",
|
||||
"#cccccc",
|
||||
"#aaaaaa",
|
||||
"#808080",
|
||||
"#555555",
|
||||
"#333333",
|
||||
"#000000",
|
||||
),
|
||||
theme.ColorAchromaticIcon(),
|
||||
callback,
|
||||
)
|
||||
}
|
||||
|
||||
// newColorRecentPicker returns a component for selecting recent colors.
|
||||
func newColorRecentPicker(callback func(color.Color)) fyne.CanvasObject {
|
||||
return newColorButtonBox(stringsToColors(readRecentColors()...), theme.HistoryIcon(), callback)
|
||||
}
|
||||
|
||||
var _ fyne.Widget = (*colorAdvancedPicker)(nil)
|
||||
|
||||
// colorAdvancedPicker widget is a component for selecting a color.
|
||||
type colorAdvancedPicker struct {
|
||||
widget.BaseWidget
|
||||
Red, Green, Blue, Alpha int // Range 0-255
|
||||
Hue int // Range 0-360 (degrees)
|
||||
Saturation, Lightness int // Range 0-100 (percent)
|
||||
ColorModel string
|
||||
previousColor color.Color
|
||||
|
||||
onChange func(color.Color)
|
||||
}
|
||||
|
||||
// newColorAdvancedPicker returns a new color widget set to the given color.
|
||||
func newColorAdvancedPicker(color color.Color, onChange func(color.Color)) *colorAdvancedPicker {
|
||||
c := &colorAdvancedPicker{
|
||||
onChange: onChange,
|
||||
}
|
||||
c.ExtendBaseWidget(c)
|
||||
c.previousColor = color
|
||||
c.updateColor(color)
|
||||
return c
|
||||
}
|
||||
|
||||
// Color returns the currently selected color.
|
||||
func (p *colorAdvancedPicker) Color() color.Color {
|
||||
return &color.NRGBA{
|
||||
uint8(p.Red),
|
||||
uint8(p.Green),
|
||||
uint8(p.Blue),
|
||||
uint8(p.Alpha),
|
||||
}
|
||||
}
|
||||
|
||||
// SetColor updates the color selected in this color widget.
|
||||
func (p *colorAdvancedPicker) SetColor(color color.Color) {
|
||||
p.previousColor = color
|
||||
if p.updateColor(color) {
|
||||
p.Refresh()
|
||||
if f := p.onChange; f != nil {
|
||||
f(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetHSLA updated the Hue, Saturation, Lightness, and Alpha components of the currently selected color.
|
||||
func (p *colorAdvancedPicker) SetHSLA(h, s, l, a int) {
|
||||
if p.updateHSLA(h, s, l, a) {
|
||||
p.Refresh()
|
||||
if f := p.onChange; f != nil {
|
||||
f(p.Color())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetRGBA updated the Red, Green, Blue, and Alpha components of the currently selected color.
|
||||
func (p *colorAdvancedPicker) SetRGBA(r, g, b, a int) {
|
||||
if p.updateRGBA(r, g, b, a) {
|
||||
p.Refresh()
|
||||
if f := p.onChange; f != nil {
|
||||
f(p.Color())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MinSize returns the size that this widget should not shrink below.
|
||||
func (p *colorAdvancedPicker) MinSize() fyne.Size {
|
||||
p.ExtendBaseWidget(p)
|
||||
return p.BaseWidget.MinSize()
|
||||
}
|
||||
|
||||
// CreateRenderer is a private method to Fyne which links this widget to its renderer.
|
||||
func (p *colorAdvancedPicker) CreateRenderer() fyne.WidgetRenderer {
|
||||
p.ExtendBaseWidget(p)
|
||||
|
||||
// Preview
|
||||
preview := newColorPreview(p.previousColor)
|
||||
|
||||
// HSL
|
||||
hueChannel := newColorChannel("H", 0, 360, p.Hue, func(h int) {
|
||||
p.SetHSLA(h, p.Saturation, p.Lightness, p.Alpha)
|
||||
})
|
||||
saturationChannel := newColorChannel("S", 0, 100, p.Saturation, func(s int) {
|
||||
p.SetHSLA(p.Hue, s, p.Lightness, p.Alpha)
|
||||
})
|
||||
lightnessChannel := newColorChannel("L", 0, 100, p.Lightness, func(l int) {
|
||||
p.SetHSLA(p.Hue, p.Saturation, l, p.Alpha)
|
||||
})
|
||||
hslBox := container.NewVBox(
|
||||
hueChannel,
|
||||
saturationChannel,
|
||||
lightnessChannel,
|
||||
)
|
||||
|
||||
// RGB
|
||||
redChannel := newColorChannel("R", 0, 255, p.Red, func(r int) {
|
||||
p.SetRGBA(r, p.Green, p.Blue, p.Alpha)
|
||||
})
|
||||
greenChannel := newColorChannel("G", 0, 255, p.Green, func(g int) {
|
||||
p.SetRGBA(p.Red, g, p.Blue, p.Alpha)
|
||||
})
|
||||
blueChannel := newColorChannel("B", 0, 255, p.Blue, func(b int) {
|
||||
p.SetRGBA(p.Red, p.Green, b, p.Alpha)
|
||||
})
|
||||
rgbBox := container.NewVBox(
|
||||
redChannel,
|
||||
greenChannel,
|
||||
blueChannel,
|
||||
)
|
||||
|
||||
// Wheel
|
||||
wheel := newColorWheel(func(hue, saturation, lightness, alpha int) {
|
||||
p.SetHSLA(hue, saturation, lightness, alpha)
|
||||
})
|
||||
|
||||
// Alpha
|
||||
alphaChannel := newColorChannel("A", 0, 255, p.Alpha, func(a int) {
|
||||
p.SetRGBA(p.Red, p.Green, p.Blue, a)
|
||||
})
|
||||
|
||||
// Hex
|
||||
hex := newUserChangeEntry("")
|
||||
hex.setOnChanged(func(text string) {
|
||||
c, err := stringToColor(text)
|
||||
if err != nil {
|
||||
fyne.LogError("Error parsing color: "+text, err)
|
||||
// TODO trigger entry invalid state
|
||||
} else {
|
||||
p.SetColor(c)
|
||||
}
|
||||
})
|
||||
|
||||
contents := container.NewPadded(container.NewVBox(
|
||||
container.NewGridWithColumns(3,
|
||||
container.NewPadded(wheel),
|
||||
hslBox,
|
||||
rgbBox),
|
||||
container.NewGridWithColumns(3,
|
||||
container.NewPadded(preview),
|
||||
|
||||
hex,
|
||||
alphaChannel,
|
||||
),
|
||||
))
|
||||
|
||||
r := &colorPickerRenderer{
|
||||
WidgetRenderer: widget.NewSimpleRenderer(contents),
|
||||
picker: p,
|
||||
redChannel: redChannel,
|
||||
greenChannel: greenChannel,
|
||||
blueChannel: blueChannel,
|
||||
hueChannel: hueChannel,
|
||||
saturationChannel: saturationChannel,
|
||||
lightnessChannel: lightnessChannel,
|
||||
wheel: wheel,
|
||||
preview: preview,
|
||||
alphaChannel: alphaChannel,
|
||||
hex: hex,
|
||||
contents: contents,
|
||||
}
|
||||
r.updateObjects()
|
||||
return r
|
||||
}
|
||||
|
||||
func (p *colorAdvancedPicker) updateColor(color color.Color) bool {
|
||||
r, g, b, a := col.ToNRGBA(color)
|
||||
if p.Red == r && p.Green == g && p.Blue == b && p.Alpha == a {
|
||||
return false
|
||||
}
|
||||
return p.updateRGBA(r, g, b, a)
|
||||
}
|
||||
|
||||
func (p *colorAdvancedPicker) updateHSLA(h, s, l, a int) bool {
|
||||
h = wrapHue(h)
|
||||
s = clamp(s, 0, 100)
|
||||
l = clamp(l, 0, 100)
|
||||
a = clamp(a, 0, 255)
|
||||
if p.Hue == h && p.Saturation == s && p.Lightness == l && p.Alpha == a {
|
||||
return false
|
||||
}
|
||||
p.Hue = h
|
||||
p.Saturation = s
|
||||
p.Lightness = l
|
||||
p.Alpha = a
|
||||
p.Red, p.Green, p.Blue = hslToRgb(p.Hue, p.Saturation, p.Lightness)
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *colorAdvancedPicker) updateRGBA(r, g, b, a int) bool {
|
||||
r = clamp(r, 0, 255)
|
||||
g = clamp(g, 0, 255)
|
||||
b = clamp(b, 0, 255)
|
||||
a = clamp(a, 0, 255)
|
||||
if p.Red == r && p.Green == g && p.Blue == b && p.Alpha == a {
|
||||
return false
|
||||
}
|
||||
p.Red = r
|
||||
p.Green = g
|
||||
p.Blue = b
|
||||
p.Alpha = a
|
||||
p.Hue, p.Saturation, p.Lightness = rgbToHsl(p.Red, p.Green, p.Blue)
|
||||
return true
|
||||
}
|
||||
|
||||
var _ fyne.WidgetRenderer = (*colorPickerRenderer)(nil)
|
||||
|
||||
type colorPickerRenderer struct {
|
||||
fyne.WidgetRenderer
|
||||
picker *colorAdvancedPicker
|
||||
redChannel *colorChannel
|
||||
greenChannel *colorChannel
|
||||
blueChannel *colorChannel
|
||||
hueChannel *colorChannel
|
||||
saturationChannel *colorChannel
|
||||
lightnessChannel *colorChannel
|
||||
wheel *colorWheel
|
||||
preview *colorPreview
|
||||
alphaChannel *colorChannel
|
||||
hex *userChangeEntry
|
||||
contents fyne.CanvasObject
|
||||
}
|
||||
|
||||
func (r *colorPickerRenderer) Refresh() {
|
||||
r.updateObjects()
|
||||
r.WidgetRenderer.Refresh()
|
||||
}
|
||||
|
||||
func (r *colorPickerRenderer) updateObjects() {
|
||||
// HSL
|
||||
r.hueChannel.SetValue(r.picker.Hue)
|
||||
r.saturationChannel.SetValue(r.picker.Saturation)
|
||||
r.lightnessChannel.SetValue(r.picker.Lightness)
|
||||
|
||||
// RGB
|
||||
r.redChannel.SetValue(r.picker.Red)
|
||||
r.greenChannel.SetValue(r.picker.Green)
|
||||
r.blueChannel.SetValue(r.picker.Blue)
|
||||
|
||||
// Wheel
|
||||
r.wheel.SetHSLA(r.picker.Hue, r.picker.Saturation, r.picker.Lightness, r.picker.Alpha)
|
||||
|
||||
color := r.picker.Color()
|
||||
|
||||
// Preview
|
||||
r.preview.SetColor(color)
|
||||
|
||||
// Alpha
|
||||
r.alphaChannel.SetValue(r.picker.Alpha)
|
||||
|
||||
// Hex
|
||||
r.hex.SetText(colorToString(color))
|
||||
}
|
||||
78
vendor/fyne.io/fyne/v2/dialog/color_preview.go
generated
vendored
Normal file
78
vendor/fyne.io/fyne/v2/dialog/color_preview.go
generated
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
internalwidget "fyne.io/fyne/v2/internal/widget"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// colorPreview displays a 2 part rectangle showing the current and previous selected colours
|
||||
type colorPreview struct {
|
||||
widget.BaseWidget
|
||||
|
||||
previous, current color.Color
|
||||
}
|
||||
|
||||
func newColorPreview(previousColor color.Color) *colorPreview {
|
||||
p := &colorPreview{previous: previousColor}
|
||||
|
||||
p.ExtendBaseWidget(p)
|
||||
return p
|
||||
}
|
||||
|
||||
// CreateRenderer is a private method to Fyne which links this widget to its renderer.
|
||||
func (p *colorPreview) CreateRenderer() fyne.WidgetRenderer {
|
||||
oldC := canvas.NewRectangle(p.previous)
|
||||
newC := canvas.NewRectangle(p.current)
|
||||
background := newCheckeredBackground(false)
|
||||
return &colorPreviewRenderer{
|
||||
BaseRenderer: internalwidget.NewBaseRenderer([]fyne.CanvasObject{background, oldC, newC}),
|
||||
preview: p,
|
||||
background: background,
|
||||
old: oldC,
|
||||
new: newC,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *colorPreview) SetColor(c color.Color) {
|
||||
p.current = c
|
||||
p.Refresh()
|
||||
}
|
||||
|
||||
func (p *colorPreview) MinSize() fyne.Size {
|
||||
p.ExtendBaseWidget(p)
|
||||
return p.BaseWidget.MinSize()
|
||||
}
|
||||
|
||||
type colorPreviewRenderer struct {
|
||||
internalwidget.BaseRenderer
|
||||
preview *colorPreview
|
||||
background *canvas.Raster
|
||||
old, new *canvas.Rectangle
|
||||
}
|
||||
|
||||
func (r *colorPreviewRenderer) Layout(size fyne.Size) {
|
||||
s := fyne.NewSize(size.Width/2, size.Height)
|
||||
r.background.Resize(size)
|
||||
r.old.Resize(s)
|
||||
r.new.Resize(s)
|
||||
r.new.Move(fyne.NewPos(s.Width, 0))
|
||||
}
|
||||
|
||||
func (r *colorPreviewRenderer) MinSize() fyne.Size {
|
||||
s := r.old.MinSize()
|
||||
s.Width *= 2
|
||||
return s.Max(fyne.NewSize(16, 8))
|
||||
}
|
||||
|
||||
func (r *colorPreviewRenderer) Refresh() {
|
||||
r.background.Refresh()
|
||||
|
||||
r.old.FillColor = r.preview.previous
|
||||
r.old.Refresh()
|
||||
r.new.FillColor = r.preview.current
|
||||
r.new.Refresh()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user