diff --git a/CHANGELOG.md b/CHANGELOG.md
index c02792b85c..19d973da40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,19 @@
This file lists the main changes with each version of the Fyne toolkit.
More detailed release notes can be found on the [releases page](https://github.com/fyne-io/fyne/releases).
+## 2.6.0 - Ongoing
+
+### Added
+
+### Changed
+
+ * ActionItems in an Entry should now match the standard button size
+
+### Fixed
+
+ * Odd looking SelectEntry with long PlaceHolder (#4430)
+
+
## 2.5.1 - 24 August 2024
### Fixed
@@ -1352,7 +1365,7 @@ The import path is now `fyne.io/fyne/v2` when you are ready to make the update.
* Don't add a button bar to a form if it has no buttons
* Moved driver/gl package to internal/driver/gl
* Clicking/Tapping in an entry will position the cursor
-* A container with no layout will not change the position or size of it's content
+* A container with no layout will not change the position or size of its content
* Update the fyne_demo app to reflect the expanding feature set
### Fixed
diff --git a/animation.go b/animation.go
index a8aeba12fb..2883774390 100644
--- a/animation.go
+++ b/animation.go
@@ -45,7 +45,7 @@ type Animation struct {
}
// 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
+// 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
diff --git a/app.go b/app.go
index 74ac0bf89a..e2f6c28e46 100644
--- a/app.go
+++ b/app.go
@@ -7,9 +7,9 @@ import (
// 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 window.ShowAndRun() function for your main window.
+// 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
@@ -27,7 +27,7 @@ type App interface {
// SetIcon sets the icon resource used for this application instance.
SetIcon(Resource)
- // Run the application - this starts the event loop and waits until Quit()
+ // 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()
@@ -43,7 +43,7 @@ type App interface {
Driver() Driver
// UniqueID returns the application unique identifier, if set.
- // This must be set for use of the Preferences() functions... see NewWithId(string)
+ // 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.
@@ -75,7 +75,7 @@ type App interface {
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.
+ // See [fyne.io/cloud] package for implementation details.
//
// Since: 2.3
SetCloudProvider(CloudProvider) // configure cloud for this app
diff --git a/app/app_desktop_darwin.go b/app/app_desktop_darwin.go
index 273b057f25..026e823d2b 100644
--- a/app/app_desktop_darwin.go
+++ b/app/app_desktop_darwin.go
@@ -53,6 +53,6 @@ func themeChanged() {
fyne.CurrentApp().Settings().(*settings).setupTheme()
}
-func watchTheme() {
+func watchTheme(_ *settings) {
C.watchTheme()
}
diff --git a/app/app_goxjs.go b/app/app_goxjs.go
index e256204810..08c2df7611 100644
--- a/app/app_goxjs.go
+++ b/app/app_goxjs.go
@@ -61,7 +61,7 @@ var themeChanged = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return nil
})
-func watchTheme() {
+func watchTheme(_ *settings) {
js.Global().Call("matchMedia", "(prefers-color-scheme: dark)").Call("addEventListener", "change", themeChanged)
}
func stopWatchingTheme() {
diff --git a/app/app_other.go b/app/app_other.go
index b14d4f7040..83c336defd 100644
--- a/app/app_other.go
+++ b/app/app_other.go
@@ -23,6 +23,6 @@ func (a *fyneApp) SendNotification(_ *fyne.Notification) {
fyne.LogError("Refusing to show notification for unknown operating system", nil)
}
-func watchTheme() {
+func watchTheme(_ *settings) {
// no-op
}
diff --git a/app/app_windows.go b/app/app_windows.go
index 9a01873dd6..e6eb30966d 100644
--- a/app/app_windows.go
+++ b/app/app_windows.go
@@ -95,6 +95,7 @@ func runScript(name, script string) {
fyne.LogError("Failed to launch windows notify script", err)
}
}
-func watchTheme() {
- go internalapp.WatchTheme(fyne.CurrentApp().Settings().(*settings).setupTheme)
+
+func watchTheme(s *settings) {
+ go internalapp.WatchTheme(s.setupTheme)
}
diff --git a/app/app_xdg.go b/app/app_xdg.go
index 28a4593798..e1835181fe 100644
--- a/app/app_xdg.go
+++ b/app/app_xdg.go
@@ -119,17 +119,17 @@ func rootConfigDir() string {
return filepath.Join(desktopConfig, "fyne")
}
-func watchTheme() {
+func watchTheme(s *settings) {
go func() {
// Theme lookup hangs on some desktops. Update theme variant cache from within goroutine.
internalapp.CurrentVariant.Store(uint64(findFreedesktopColorScheme()))
- fyne.CurrentApp().Settings().(*settings).setupTheme()
+ s.setupTheme()
portalSettings.OnSignalSettingChanged(func(changed portalSettings.Changed) {
if changed.Namespace == "org.freedesktop.appearance" && changed.Key == "color-scheme" {
themeVariant := colorSchemeToThemeVariant(appearance.ColorScheme(changed.Value.(uint32)))
internalapp.CurrentVariant.Store(uint64(themeVariant))
- fyne.CurrentApp().Settings().(*settings).setupTheme()
+ s.setupTheme()
}
})
}()
diff --git a/app/settings_desktop.go b/app/settings_desktop.go
index c3de2120f1..bff819b1b2 100644
--- a/app/settings_desktop.go
+++ b/app/settings_desktop.go
@@ -62,7 +62,10 @@ func watchFile(path string, callback func()) *fsnotify.Watcher {
func (s *settings) watchSettings() {
s.watcher = watchFile(s.schema.StoragePath(), s.fileChanged)
- watchTheme()
+ a := fyne.CurrentApp()
+ if a != nil && s != nil && a.Settings() == s { // ignore if testing
+ watchTheme(s)
+ }
}
func (s *settings) stopWatching() {
diff --git a/app/settings_goxjs.go b/app/settings_goxjs.go
index 0e56aa43b4..858257557b 100644
--- a/app/settings_goxjs.go
+++ b/app/settings_goxjs.go
@@ -17,7 +17,7 @@ func watchFile(path string, callback func()) {
}
func (s *settings) watchSettings() {
- watchTheme()
+ watchTheme(s)
}
func (s *settings) stopWatching() {
diff --git a/canvas.go b/canvas.go
index 8869af25a0..8fd97d74cb 100644
--- a/canvas.go
+++ b/canvas.go
@@ -2,7 +2,7 @@ package fyne
import "image"
-// Canvas defines a graphical canvas to which a CanvasObject or Container can be added.
+// 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
@@ -31,7 +31,7 @@ type Canvas interface {
// 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.
+ // The pixel size of a [CanvasObject] can be found by multiplying by this value.
Scale() float32
// Overlays returns the overlay stack.
diff --git a/canvasobject.go b/canvasobject.go
index 05ab7716e8..0566627285 100644
--- a/canvasobject.go
+++ b/canvasobject.go
@@ -36,7 +36,7 @@ type CanvasObject interface {
Refresh()
}
-// Disableable describes any CanvasObject that can be disabled.
+// Disableable describes any [CanvasObject] that can be disabled.
// This is primarily used with objects that also implement the Tappable interface.
type Disableable interface {
Enable()
@@ -44,19 +44,19 @@ type Disableable interface {
Disabled() bool
}
-// DoubleTappable describes any CanvasObject that can also be double tapped.
+// DoubleTappable describes any [CanvasObject] that can also be double tapped.
type DoubleTappable interface {
DoubleTapped(*PointEvent)
}
-// Draggable indicates that a CanvasObject can be dragged.
+// 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.
+// 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.
@@ -75,18 +75,18 @@ type Focusable interface {
TypedKey(*KeyEvent)
}
-// Scrollable describes any CanvasObject that can also be scrolled.
+// 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.
+// 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).
+// Shortcutable describes any [CanvasObject] that can respond to shortcut commands (quit, cut, copy, and paste).
type Shortcutable interface {
TypedShortcut(Shortcut)
}
@@ -95,12 +95,12 @@ type Shortcutable interface {
//
// Since: 2.1
type Tabbable interface {
- // AcceptsTab() is a hook called by the key press handling logic.
+ // 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.
+// 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)
diff --git a/cloud.go b/cloud.go
index c44e53c514..2e815bb4b9 100644
--- a/cloud.go
+++ b/cloud.go
@@ -1,7 +1,7 @@
package fyne
// CloudProvider specifies the identifying information of a cloud provider.
-// This information is mostly used by the `fyne.io/cloud ShowSettings' user flow.
+// This information is mostly used by the [fyne.io/cloud.ShowSettings] user flow.
//
// Since: 2.3
type CloudProvider interface {
diff --git a/cmd/fyne/internal/commands/get.go b/cmd/fyne/internal/commands/get.go
index f6af676c0d..d50b4cd3ed 100644
--- a/cmd/fyne/internal/commands/get.go
+++ b/cmd/fyne/internal/commands/get.go
@@ -35,6 +35,12 @@ func Get() *cli.Command {
Usage: "For darwin and Windows targets an appID in the form of a reversed domain name is required, for ios this must match a valid provisioning profile",
Destination: &g.AppID,
},
+ &cli.StringFlag{
+ Name: "installDir",
+ Aliases: []string{"o"},
+ Usage: "A specific location to install to, rather than the OS default.",
+ Destination: &g.installDir,
+ },
},
Action: func(ctx *cli.Context) error {
if ctx.Args().Len() != 1 {
@@ -50,6 +56,7 @@ func Get() *cli.Command {
// Getter is the command that can handle downloading and installing Fyne apps to the current platform.
type Getter struct {
*appData
+ installDir string
}
// NewGetter returns a command that can handle the download and install of GUI apps built using Fyne.
@@ -98,7 +105,7 @@ func (g *Getter) Get(pkg string) error {
path = filepath.Join(path, dir)
}
- install := &Installer{appData: g.appData, srcDir: path, release: true}
+ install := &Installer{appData: g.appData, installDir: g.installDir, srcDir: path, release: true}
if err := install.validate(); err != nil {
return fmt.Errorf("failed to set up installer: %w", err)
}
diff --git a/cmd/fyne/internal/commands/translate.go b/cmd/fyne/internal/commands/translate.go
new file mode 100644
index 0000000000..3a8383075f
--- /dev/null
+++ b/cmd/fyne/internal/commands/translate.go
@@ -0,0 +1,293 @@
+package commands
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strconv"
+
+ "github.com/urfave/cli/v2"
+)
+
+// Translate returns the cli command to scan for new translation strings.
+func Translate() *cli.Command {
+ return &cli.Command{
+ Name: "translate",
+ Usage: "Scans for new translation strings.",
+ Flags: []cli.Flag{
+ &cli.BoolFlag{
+ Name: "verbose",
+ Aliases: []string{"v"},
+ Usage: "Shows files that are being scanned etc.",
+ },
+ &cli.StringFlag{
+ Name: "sourceDir",
+ Aliases: []string{"src"},
+ Usage: "Directory to scan recursively for go files.",
+ Value: ".",
+ },
+ &cli.StringFlag{
+ Name: "translationsFile",
+ Aliases: []string{"file"},
+ Usage: "File to read from and write translations to.",
+ Value: "translations/en.json",
+ },
+ },
+ Action: func(ctx *cli.Context) error {
+ sourceDir := ctx.String("sourceDir")
+ translationsFile := ctx.String("translationsFile")
+ files := ctx.Args().Slice()
+
+ if len(files) == 0 {
+ err := filepath.Walk(sourceDir, func(path string, fi fs.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if fi.IsDir() {
+ return nil
+ }
+
+ if !fi.Mode().IsRegular() {
+ return nil
+ }
+
+ if filepath.Ext(path) != ".go" {
+ return nil
+ }
+
+ files = append(files, path)
+
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ }
+
+ if ctx.Bool("verbose") {
+ fmt.Printf("files: %v\n", files)
+ }
+
+ translations := make(map[string]interface{})
+
+ // get current translations
+ f, err := os.Open(translationsFile)
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ defer f.Close()
+
+ if f != nil {
+ dec := json.NewDecoder(f)
+ if err := dec.Decode(&translations); err != nil {
+ return err
+ }
+ }
+
+ // update translations hash
+ if err := updateTranslations(translations, files); err != nil {
+ return err
+ }
+
+ // serialize in readable format for humas to change
+ b, err := json.MarshalIndent(translations, "", "\t")
+ if err != nil {
+ return err
+ }
+
+ if ctx.Bool("verbose") {
+ fmt.Printf("%s\n", string(b))
+ }
+
+ if len(translations) == 0 {
+ fmt.Println("No translations found")
+ return nil
+ }
+
+ // use temporary file to do atomic change
+ nf, err := os.CreateTemp(filepath.Dir(translationsFile), filepath.Base(translationsFile)+"-*")
+ if err != nil {
+ return err
+ }
+
+ n, err := nf.Write(b)
+ if err != nil {
+ return err
+ }
+ if n != len(b) {
+ return err
+ }
+
+ if err := nf.Chmod(0644); err != nil {
+ return err
+ }
+ nf.Close()
+ if err := os.Rename(nf.Name(), translationsFile); err != nil {
+ return err
+ }
+
+ return nil
+ },
+ }
+}
+
+// -- 1: key (default)
+// L: Localize(in string, data ...any) string
+// N: LocalizePlural(in string, count int, data ...any) string
+// -- 2: key, default
+// X: LocalizeKey(key, fallback string, data ...any) string
+// XN: LocalizePluralKey(key, fallback string, count int, data ...any) string
+
+func updateTranslations(m map[string]interface{}, srcs []string) error {
+ for _, src := range srcs {
+ fset := token.NewFileSet()
+ af, err := parser.ParseFile(fset, src, nil, parser.AllErrors)
+ if err != nil {
+ return err
+ }
+
+ ast.Walk(&visitor{m: m}, af)
+ }
+
+ return nil
+}
+
+type visitor struct {
+ state stateFn
+ name string
+ key string
+ fallback string
+ m map[string]interface{}
+}
+
+// Visitor pattern using a state machine while walking the tree
+func (v *visitor) Visit(node ast.Node) ast.Visitor {
+ if node == nil {
+ return nil
+ }
+
+ if v.state == nil {
+ v.state = translateNew
+ v.name = ""
+ v.key = ""
+ v.fallback = ""
+ }
+
+ v.state = v.state(v, node)
+
+ return v
+}
+
+// State machine to pick out translation key and fallback from AST
+type stateFn func(*visitor, ast.Node) stateFn
+
+func translateNew(v *visitor, node ast.Node) stateFn {
+ ident, ok := node.(*ast.Ident)
+ if !ok {
+ return nil
+ }
+
+ if ident.Name != "lang" {
+ return nil
+ }
+
+ return translateCall
+}
+
+func translateCall(v *visitor, node ast.Node) stateFn {
+ ident, ok := node.(*ast.Ident)
+ if !ok {
+ return nil
+ }
+
+ v.name = ident.Name
+
+ switch ident.Name {
+ case "L", "Localise":
+ return translateLocalize
+ case "N", "LocalizePlural":
+ return translateLocalize
+ case "X", "LocalizeKey":
+ return translateKey
+ case "XN", "LocalizePluralKey":
+ return translateKey
+ }
+
+ return nil
+}
+
+func translateLocalize(v *visitor, node ast.Node) stateFn {
+ basiclit, ok := node.(*ast.BasicLit)
+ if !ok {
+ return nil
+ }
+
+ val, err := strconv.Unquote(basiclit.Value)
+ if err != nil {
+ return nil
+ }
+
+ v.key = val
+ v.fallback = val
+
+ return translateFinish(v, node)
+}
+
+func translateKey(v *visitor, node ast.Node) stateFn {
+ basiclit, ok := node.(*ast.BasicLit)
+ if !ok {
+ return nil
+ }
+
+ val, err := strconv.Unquote(basiclit.Value)
+ if err != nil {
+ return nil
+ }
+
+ v.key = val
+
+ return translateKeyFallback
+}
+
+func translateKeyFallback(v *visitor, node ast.Node) stateFn {
+ basiclit, ok := node.(*ast.BasicLit)
+ if !ok {
+ return nil
+ }
+
+ val, err := strconv.Unquote(basiclit.Value)
+ if err != nil {
+ return nil
+ }
+
+ v.fallback = val
+
+ return translateFinish(v, node)
+}
+
+func translateFinish(v *visitor, node ast.Node) stateFn {
+ // only adding new keys, ignoring changed or removed (ha!) ones
+ // removing is dangerous as there could be dynamic keys that get removed
+
+ _, found := v.m[v.key]
+ if found {
+ return nil
+ }
+
+ switch v.name {
+ case "LocalizePlural", "LocalizePluralKey", "N", "XN":
+ m := make(map[string]string)
+ m["other"] = v.fallback
+ v.m[v.key] = m
+ default:
+ v.m[v.key] = v.fallback
+ }
+
+ return nil
+}
diff --git a/cmd/fyne/internal/mobile/README.txt b/cmd/fyne/internal/mobile/README.txt
index 4e47111efc..6a4bd5dabc 100644
--- a/cmd/fyne/internal/mobile/README.txt
+++ b/cmd/fyne/internal/mobile/README.txt
@@ -1,6 +1,6 @@
This directory is a partial clone of the golang.org/x/mobile/cmd/gomobile package.
It also includes the golang.org/x/mobile/internal/bindata as a subpackage.
-The full project, it's license and command line tools can be found at https://github.com/golang/mobile
+The full project, its license and command line tools can be found at https://github.com/golang/mobile
This package is for the purpose of removing a runtime dependency and will be removed in due course.
\ No newline at end of file
diff --git a/cmd/fyne/internal/mobile/binres/binres.go b/cmd/fyne/internal/mobile/binres/binres.go
index 68dfe5a8c7..35f99d36fa 100644
--- a/cmd/fyne/internal/mobile/binres/binres.go
+++ b/cmd/fyne/internal/mobile/binres/binres.go
@@ -686,7 +686,7 @@ func addAttributeNamespace(attr xml.Attr, nattr *Attribute, tbl *Table, pool *Po
return fmt.Errorf("invalid bool value %q", attr.Value)
}
case DataIntDec, DataFloat, DataFraction:
- // TODO DataFraction needs it's own case statement. minSdkVersion identifies as DataFraction
+ // TODO DataFraction needs its own case statement. minSdkVersion identifies as DataFraction
// but has accepted input in the past such as android:minSdkVersion="L"
// Other use-cases for DataFraction are currently unknown as applicable to manifest generation
// but this provides minimum support for writing out minSdkVersion="15" correctly.
@@ -851,7 +851,7 @@ func (bx *XML) kind(t ResType) (unmarshaler, error) {
}
}
-// MarshalBinary formats the XML in memory to it's text appearance
+// MarshalBinary formats the XML in memory to its text appearance
func (bx *XML) MarshalBinary() ([]byte, error) {
bx.typ = ResXML
bx.headerByteSize = 8
diff --git a/cmd/fyne/internal/mobile/doc.go b/cmd/fyne/internal/mobile/doc.go
index fa5618f009..fa15e42a40 100644
--- a/cmd/fyne/internal/mobile/doc.go
+++ b/cmd/fyne/internal/mobile/doc.go
@@ -6,7 +6,7 @@
Package mobile is a partial clone of the golang.org/x/mobile/cmd/gomobile package.
It also includes the golang.org/x/mobile/internal/bindata as a subpackage.
-The full project, it's license and command line tools can be found at https://github.com/golang/mobile
+The full project, its license and command line tools can be found at https://github.com/golang/mobile
This package is for the purpose of removing a runtime dependency and will be removed in due course.
*/
diff --git a/cmd/fyne/main.go b/cmd/fyne/main.go
index 02a2137981..4768c6da07 100644
--- a/cmd/fyne/main.go
+++ b/cmd/fyne/main.go
@@ -24,6 +24,7 @@ func main() {
commands.Release(),
commands.Version(),
commands.Serve(),
+ commands.Translate(),
commands.Build(),
// Deprecated: Use "go mod vendor" instead.
diff --git a/cmd/fyne_demo/tutorials/data.go b/cmd/fyne_demo/tutorials/data.go
index 8e63bfa41a..ab731fb573 100644
--- a/cmd/fyne_demo/tutorials/data.go
+++ b/cmd/fyne_demo/tutorials/data.go
@@ -61,7 +61,7 @@ var (
makeSplitTab,
},
"scroll": {"Scroll",
- "A container that provides scrolling for it's content.",
+ "A container that provides scrolling for its content.",
makeScrollTab,
},
"innerwindow": {"InnerWindow",
diff --git a/cmd/fyne_demo/tutorials/icons.go b/cmd/fyne_demo/tutorials/icons.go
index d136737472..fa5fcb8551 100644
--- a/cmd/fyne_demo/tutorials/icons.go
+++ b/cmd/fyne_demo/tutorials/icons.go
@@ -187,6 +187,7 @@ func loadIcons() []iconInfo {
{"VolumeUp", theme.VolumeUpIcon()},
{"AccountIcon", theme.AccountIcon()},
+ {"CalendarIcon", theme.CalendarIcon()},
{"LoginIcon", theme.LoginIcon()},
{"LogoutIcon", theme.LogoutIcon()},
diff --git a/cmd/fyne_demo/tutorials/widget.go b/cmd/fyne_demo/tutorials/widget.go
index f0c6a005bd..a5bd58ab78 100644
--- a/cmd/fyne_demo/tutorials/widget.go
+++ b/cmd/fyne_demo/tutorials/widget.go
@@ -374,6 +374,8 @@ func makeInputTab(_ fyne.Window) fyne.CanvasObject {
"Option Z",
})
selectEntry.PlaceHolder = "Type or select"
+ dateEntry := widget.NewDateEntry()
+ dateEntry.PlaceHolder = "Choose a date"
disabledCheck := widget.NewCheck("Disabled check", func(bool) {})
disabledCheck.Disable()
checkGroup := widget.NewCheckGroup([]string{"CheckGroup Item 1", "CheckGroup Item 2"}, func(s []string) { fmt.Println("selected", s) })
@@ -388,6 +390,7 @@ func makeInputTab(_ fyne.Window) fyne.CanvasObject {
return container.NewVBox(
widget.NewSelect([]string{"Option 1", "Option 2", "Option 3"}, func(s string) { fmt.Println("selected", s) }),
selectEntry,
+ dateEntry,
widget.NewCheck("Check", func(on bool) { fmt.Println("checked", on) }),
disabledCheck,
checkGroup,
diff --git a/container.go b/container.go
index 726e77a626..a9357591cf 100644
--- a/container.go
+++ b/container.go
@@ -2,32 +2,32 @@ package fyne
import "sync"
-// Declare conformity to CanvasObject
+// Declare conformity to [CanvasObject]
var _ CanvasObject = (*Container)(nil)
-// Container is a CanvasObject that contains a collection of child objects.
+// 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 CanvasObjects
+ Layout Layout // The Layout algorithm for arranging child [CanvasObject]s
lock sync.Mutex
- Objects []CanvasObject // The set of CanvasObjects this container holds
+ Objects []CanvasObject // The set of [CanvasObject]s this container holds
}
-// NewContainer returns a new Container instance holding the specified CanvasObjects.
+// NewContainer returns a new [Container] instance holding the specified [CanvasObject]s.
//
-// Deprecated: Use container.NewWithoutLayout() to create a container that uses manual layout.
+// 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
-// CanvasObjects that are manually arranged.
+// NewContainerWithoutLayout returns a new [Container] instance holding the specified
+// [CanvasObject]s that are manually arranged.
//
-// Deprecated: Use container.NewWithoutLayout() instead
+// Deprecated: Use [fyne.io/fyne/v2/container.NewWithoutLayout] instead.
func NewContainerWithoutLayout(objects ...CanvasObject) *Container {
ret := &Container{
Objects: objects,
@@ -37,10 +37,10 @@ func NewContainerWithoutLayout(objects ...CanvasObject) *Container {
return ret
}
-// NewContainerWithLayout returns a new Container instance holding the specified
-// CanvasObjects which will be laid out according to the specified Layout.
+// NewContainerWithLayout returns a new [Container] instance holding the specified
+// [CanvasObject]s which will be laid out according to the specified Layout.
//
-// Deprecated: Use container.New() instead
+// Deprecated: Use [fyne.io/fyne/v2/container.New] instead.
func NewContainerWithLayout(layout Layout, objects ...CanvasObject) *Container {
ret := &Container{
Objects: objects,
@@ -66,9 +66,9 @@ func (c *Container) Add(add CanvasObject) {
c.layout()
}
-// AddObject adds another CanvasObject to the set this Container holds.
+// AddObject adds another [CanvasObject] to the set this Container holds.
//
-// Deprecated: Use replacement Add() function
+// Deprecated: Use [Container.Add] instead.
func (c *Container) AddObject(o CanvasObject) {
c.Add(o)
}
@@ -83,8 +83,8 @@ func (c *Container) Hide() {
repaint(c)
}
-// MinSize calculates the minimum size of a Container.
-// This is delegated to the Layout, if specified, otherwise it will mimic MaxLayout.
+// 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)
@@ -104,12 +104,12 @@ func (c *Container) Move(pos Position) {
repaint(c)
}
-// Position gets the current position of this Container, relative to its parent.
+// 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 it's current state
+// Refresh causes this object to be redrawn in its current state
func (c *Container) Refresh() {
c.layout()
@@ -127,7 +127,7 @@ func (c *Container) Refresh() {
// 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 RemoveAll() instead.
+// It is much more efficient to call [Container.RemoveAll) instead.
func (c *Container) Remove(rem CanvasObject) {
c.lock.Lock()
defer c.lock.Unlock()
@@ -158,7 +158,7 @@ func (c *Container) RemoveAll() {
c.layout()
}
-// Resize sets a new size for the Container.
+// Resize sets a new size for c.
func (c *Container) Resize(size Size) {
if c.size == size {
return
@@ -177,7 +177,7 @@ func (c *Container) Show() {
c.Hidden = false
}
-// Size returns the current size of this container.
+// Size returns the current size c.
func (c *Container) Size() Size {
return c.size
}
diff --git a/container/layouts.go b/container/layouts.go
index e6c53c69ea..1e8446317a 100644
--- a/container/layouts.go
+++ b/container/layouts.go
@@ -16,8 +16,11 @@ func NewAdaptiveGrid(rowcols int, objects ...fyne.CanvasObject) *fyne.Container
}
// 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,
-// the remaining elements will be in the center. Nil can be used to an edge if it should not be filled.
+// 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 {
diff --git a/dialog/file.go b/dialog/file.go
index 7108066666..738cfb7b78 100644
--- a/dialog/file.go
+++ b/dialog/file.go
@@ -30,7 +30,10 @@ const (
GridView
)
-const viewLayoutKey = "fyne:fileDialogViewLayout"
+const (
+ viewLayoutKey = "fyne:fileDialogViewLayout"
+ lastFolderKey = "fyne:fileDialogLastFolder"
+)
type textWidget interface {
fyne.Widget
@@ -245,7 +248,7 @@ func (f *fileDialog) makeUI() fyne.CanvasObject {
f.breadcrumbScroll, f.filesScroll,
),
)
- body.SetOffset(0) // Set the minimum offset so that the favoritesList takes only it's minimal width
+ body.SetOffset(0) // Set the minimum offset so that the favoritesList takes only its minimal width
return container.NewBorder(header, footer, nil, nil, body)
}
@@ -438,6 +441,7 @@ func (f *fileDialog) setLocation(dir fyne.URI) error {
return err
}
+ fyne.CurrentApp().Preferences().SetString(lastFolderKey, dir.String())
isFav := false
for i, fav := range f.favorites {
if fav.loc == nil {
@@ -601,6 +605,8 @@ func (f *fileDialog) getDataItem(id int) (fyne.URI, bool) {
//
// - file.startingDirectory if non-empty, os.Stat()-able, and uses the file://
// URI scheme
+// - previously used file open/close folder within this app
+// - the current app's document storage, if App.Storage() documents have been saved
// - os.UserHomeDir()
// - os.Getwd()
// - "/" (should be filesystem root on all supported platforms)
@@ -619,6 +625,18 @@ func (f *FileDialog) effectiveStartingDir() fyne.ListableURI {
}
+ // last used
+ lastPath := fyne.CurrentApp().Preferences().String(lastFolderKey)
+ if lastPath != "" {
+ parsed, err := storage.ParseURI(lastPath)
+ if err == nil {
+ dir, err := storage.ListerForURI(parsed)
+ if err == nil {
+ return dir
+ }
+ }
+ }
+
// Try app storage
app := fyne.CurrentApp()
if hasAppFiles(app) {
@@ -895,7 +913,7 @@ func getFavoriteOrder() []string {
}
if runtime.GOOS == "darwin" {
- order[4] = "Movies"
+ order[5] = "Movies"
}
return order
diff --git a/dialog/file_test.go b/dialog/file_test.go
index 3c0ae0e323..4e3373e96e 100644
--- a/dialog/file_test.go
+++ b/dialog/file_test.go
@@ -46,7 +46,6 @@ func comparePaths(t *testing.T, u1, u2 fyne.ListableURI) bool {
}
func TestEffectiveStartingDir(t *testing.T) {
-
homeString, err := os.UserHomeDir()
if err != nil {
t.Skipf("os.Gethome() failed, cannot run this test on this system (error stat()-ing ../) error was '%s'", err)
@@ -105,7 +104,28 @@ func TestEffectiveStartingDir(t *testing.T) {
t.Errorf("Expected effectiveStartingDir() to be '%s', but it was '%s'",
expect, res)
}
+}
+
+func TestFileDialogStartRemember(t *testing.T) {
+ testPath, err := filepath.Abs("./testdata")
+ assert.Nil(t, err)
+ start, err := storage.ListerForURI(storage.NewFileURI(testPath))
+ if err != nil {
+ t.Skipf("could not get lister for working directory: %s", err)
+ }
+
+ w := test.NewTempWindow(t, widget.NewLabel("Content"))
+ d := NewFileOpen(nil, w)
+ d.SetLocation(start)
+ d.Show()
+
+ assert.Equal(t, start.String(), d.dialog.dir.String())
+ d.Hide()
+ d2 := NewFileOpen(nil, w)
+ d2.Show()
+ assert.Equal(t, start.String(), d.dialog.dir.String())
+ d2.Hide()
}
func TestFileDialogResize(t *testing.T) {
diff --git a/driver.go b/driver.go
index 21c4906fb5..cfeb6b123d 100644
--- a/driver.go
+++ b/driver.go
@@ -15,9 +15,9 @@ type Driver interface {
// If the source is specified it will be used, otherwise the current theme will be asked for the font.
RenderedTextSize(text string, fontSize float32, style TextStyle, source Resource) (size Size, baseline float32)
- // CanvasForObject returns the canvas that is associated with a given CanvasObject.
+ // CanvasForObject returns the canvas that is associated with a given [CanvasObject].
CanvasForObject(CanvasObject) Canvas
- // AbsolutePositionForObject returns the position of a given CanvasObject relative to the top/left of a canvas.
+ // AbsolutePositionForObject returns the position of a given [CanvasObject] relative to the top/left of a canvas.
AbsolutePositionForObject(CanvasObject) Position
// Device returns the device that the application is currently running on.
@@ -34,7 +34,7 @@ type Driver interface {
StopAnimation(*Animation)
// DoubleTapDelay returns the maximum duration where a second tap after a first one
- // will be considered a DoubleTap instead of two distinct Tap events.
+ // will be considered a [DoubleTap] instead of two distinct [Tap] events.
//
// Since: 2.5
DoubleTapDelay() time.Duration
diff --git a/event.go b/event.go
index 6646e653ec..0f006056fd 100644
--- a/event.go
+++ b/event.go
@@ -1,7 +1,7 @@
package fyne
// HardwareKey contains information associated with physical key events
-// Most applications should use KeyName for cross-platform compatibility.
+// Most applications should use [KeyName] for cross-platform compatibility.
type HardwareKey struct {
// ScanCode represents a hardware ID for (normally desktop) keyboard events.
ScanCode int
@@ -16,7 +16,7 @@ type KeyEvent struct {
}
// PointEvent describes a pointer input event. The position is relative to the
-// top-left of the CanvasObject this is triggered on.
+// top-left of the [CanvasObject] this is triggered on.
type PointEvent struct {
AbsolutePosition Position // The absolute position of the event
Position Position // The relative position of the event
diff --git a/geometry.go b/geometry.go
index e28fd867b7..e0dec55f75 100644
--- a/geometry.go
+++ b/geometry.go
@@ -1,8 +1,10 @@
package fyne
-var _ Vector2 = (*Delta)(nil)
-var _ Vector2 = (*Position)(nil)
-var _ Vector2 = (*Size)(nil)
+var (
+ _ Vector2 = (*Delta)(nil)
+ _ Vector2 = (*Position)(nil)
+ _ Vector2 = (*Size)(nil)
+)
// Vector2 marks geometry types that can operate as a coordinate vector.
type Vector2 interface {
@@ -15,12 +17,12 @@ type Delta struct {
DX, DY float32
}
-// NewDelta returns a newly allocated Delta representing a movement in the X and Y axis.
+// NewDelta returns a newly allocated [Delta] representing a movement in the X and Y axis.
func NewDelta(dx float32, dy float32) Delta {
return Delta{DX: dx, DY: dy}
}
-// Components returns the X and Y elements of this Delta.
+// Components returns the X and Y elements of v.
func (v Delta) Components() (float32, float32) {
return v.DX, v.DY
}
@@ -30,26 +32,26 @@ func (v Delta) IsZero() bool {
return v.DX == 0.0 && v.DY == 0.0
}
-// Position describes a generic X, Y coordinate relative to a parent Canvas
-// or CanvasObject.
+// Position describes a generic X, Y coordinate relative to a parent [Canvas]
+// or [CanvasObject].
type Position struct {
X float32 // The position from the parent's left edge
Y float32 // The position from the parent's top edge
}
-// NewPos returns a newly allocated Position representing the specified coordinates.
+// NewPos returns a newly allocated [Position] representing the specified coordinates.
func NewPos(x float32, y float32) Position {
return Position{x, y}
}
-// NewSquareOffsetPos returns a newly allocated Position with the same x and y position.
+// NewSquareOffsetPos returns a newly allocated [Position] with the same x and y position.
//
// Since: 2.4
func NewSquareOffsetPos(length float32) Position {
return Position{length, length}
}
-// Add returns a new Position that is the result of offsetting the current
+// Add returns a new [Position] that is the result of offsetting the current
// position by p2 X and Y.
func (p Position) Add(v Vector2) Position {
// NOTE: Do not simplify to `return p.AddXY(v.Components())`, it prevents inlining.
@@ -57,12 +59,12 @@ func (p Position) Add(v Vector2) Position {
return Position{p.X + x, p.Y + y}
}
-// AddXY returns a new Position by adding x and y to the current one.
+// AddXY returns a new [Position] by adding x and y to the current one.
func (p Position) AddXY(x, y float32) Position {
return Position{p.X + x, p.Y + y}
}
-// Components returns the X and Y elements of this Position
+// Components returns the X and Y elements of p.
func (p Position) Components() (float32, float32) {
return p.X, p.Y
}
@@ -72,7 +74,7 @@ func (p Position) IsZero() bool {
return p.X == 0.0 && p.Y == 0.0
}
-// Subtract returns a new Position that is the result of offsetting the current
+// Subtract returns a new [Position] that is the result of offsetting the current
// position by p2 -X and -Y.
func (p Position) Subtract(v Vector2) Position {
// NOTE: Do not simplify to `return p.SubtractXY(v.Components())`, it prevents inlining.
@@ -80,7 +82,7 @@ func (p Position) Subtract(v Vector2) Position {
return Position{p.X - x, p.Y - y}
}
-// SubtractXY returns a new Position by subtracting x and y from the current one.
+// SubtractXY returns a new [Position] by subtracting x and y from the current one.
func (p Position) SubtractXY(x, y float32) Position {
return Position{p.X - x, p.Y - y}
}
@@ -121,7 +123,7 @@ func (s Size) IsZero() bool {
return s.Width == 0.0 && s.Height == 0.0
}
-// Max returns a new Size that is the maximum of the current Size and s2.
+// Max returns a new [Size] that is the maximum of the current Size and s2.
func (s Size) Max(v Vector2) Size {
x, y := v.Components()
@@ -131,7 +133,7 @@ func (s Size) Max(v Vector2) Size {
return NewSize(maxW, maxH)
}
-// Min returns a new Size that is the minimum of the current Size and s2.
+// Min returns a new [Size] that is the minimum of s and v.
func (s Size) Min(v Vector2) Size {
x, y := v.Components()
diff --git a/internal/async/pool.go b/internal/async/pool.go
new file mode 100644
index 0000000000..a65f748975
--- /dev/null
+++ b/internal/async/pool.go
@@ -0,0 +1,30 @@
+package async
+
+import "sync"
+
+// Implementation inspired by https://github.com/tailscale/tailscale/blob/main/syncs/pool.go.
+
+// Pool is the generic version of sync.Pool.
+type Pool[T any] struct {
+ pool sync.Pool
+
+ // New specifies a function to generate
+ // a value when Get would otherwise return the zero value of T.
+ New func() T
+}
+
+// Get selects an arbitrary item from the Pool, removes it from the Pool,
+// and returns it to the caller.
+func (p *Pool[T]) Get() T {
+ x, ok := p.pool.Get().(T)
+ if !ok && p.New != nil {
+ return p.New()
+ }
+
+ return x
+}
+
+// Put adds x to the pool.
+func (p *Pool[T]) Put(x T) {
+ p.pool.Put(x)
+}
diff --git a/internal/async/pool_test.go b/internal/async/pool_test.go
new file mode 100644
index 0000000000..67cf5401a7
--- /dev/null
+++ b/internal/async/pool_test.go
@@ -0,0 +1,24 @@
+package async
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPool(t *testing.T) {
+ pool := Pool[int]{}
+
+ item := pool.Get()
+ assert.Equal(t, 0, item)
+
+ item = 5
+ pool.Put(item)
+ assert.Equal(t, item, pool.Get())
+
+ pool.New = func() int {
+ return -1
+ }
+
+ assert.Equal(t, -1, pool.Get())
+}
diff --git a/internal/async/queue_canvasobject.go b/internal/async/queue_canvasobject.go
index 5f4da2b41d..4f6e53a9f4 100755
--- a/internal/async/queue_canvasobject.go
+++ b/internal/async/queue_canvasobject.go
@@ -1,7 +1,6 @@
package async
import (
- "sync"
"sync/atomic"
"fyne.io/fyne/v2"
@@ -30,13 +29,13 @@ type itemCanvasObject struct {
v fyne.CanvasObject
}
-var itemCanvasObjectPool = sync.Pool{
- New: func() any { return &itemCanvasObject{} },
+var itemCanvasObjectPool = Pool[*itemCanvasObject]{
+ New: func() *itemCanvasObject { return &itemCanvasObject{} },
}
// In puts the given value at the tail of the queue.
func (q *CanvasObjectQueue) In(v fyne.CanvasObject) {
- i := itemCanvasObjectPool.Get().(*itemCanvasObject)
+ i := itemCanvasObjectPool.Get()
i.next.Store(nil)
i.v = v
diff --git a/internal/cache/base.go b/internal/cache/base.go
index c42cb3ce2a..b68d4bab36 100644
--- a/internal/cache/base.go
+++ b/internal/cache/base.go
@@ -12,7 +12,6 @@ var (
cacheDuration = 1 * time.Minute
cleanTaskInterval = cacheDuration / 2
- expiredObjects = make([]fyne.CanvasObject, 0, 50)
lastClean time.Time
skippedCleanWithCanvasRefresh = false
@@ -161,45 +160,27 @@ func ResetThemeCaches() {
// destroyExpiredCanvases deletes objects from the canvases cache.
func destroyExpiredCanvases(now time.Time) {
- expiredObjects = expiredObjects[:0]
- canvasesLock.RLock()
+ canvasesLock.Lock()
for obj, cinfo := range canvases {
if cinfo.isExpired(now) {
- expiredObjects = append(expiredObjects, obj)
- }
- }
- canvasesLock.RUnlock()
- if len(expiredObjects) > 0 {
- canvasesLock.Lock()
- for i, exp := range expiredObjects {
- delete(canvases, exp)
- expiredObjects[i] = nil
+ delete(canvases, obj)
}
- canvasesLock.Unlock()
}
+ canvasesLock.Unlock()
}
// destroyExpiredRenderers deletes the renderer from the cache and calls
// renderer.Destroy()
func destroyExpiredRenderers(now time.Time) {
- expiredObjects = expiredObjects[:0]
- renderersLock.RLock()
+ renderersLock.Lock()
for wid, rinfo := range renderers {
if rinfo.isExpired(now) {
rinfo.renderer.Destroy()
overrides.Delete(wid)
- expiredObjects = append(expiredObjects, wid)
+ delete(renderers, wid)
}
}
- renderersLock.RUnlock()
- if len(expiredObjects) > 0 {
- renderersLock.Lock()
- for i, exp := range expiredObjects {
- delete(renderers, exp.(fyne.Widget))
- expiredObjects[i] = nil
- }
- renderersLock.Unlock()
- }
+ renderersLock.Unlock()
}
// matchesACanvas returns true if the canvas represented by the canvasInfo object matches one of
diff --git a/internal/cache/base_test.go b/internal/cache/base_test.go
index adffe67209..f87ad62767 100644
--- a/internal/cache/base_test.go
+++ b/internal/cache/base_test.go
@@ -264,7 +264,6 @@ func (t *timeMock) setTime(min, sec int) {
}
func testClearAll() {
- expiredObjects = make([]fyne.CanvasObject, 0, 50)
skippedCleanWithCanvasRefresh = false
canvases = make(map[fyne.CanvasObject]*canvasInfo, 1024)
svgs.Range(func(key, _ any) bool {
diff --git a/internal/cache/svg.go b/internal/cache/svg.go
index 94911acfca..b21fb711cb 100644
--- a/internal/cache/svg.go
+++ b/internal/cache/svg.go
@@ -46,18 +46,13 @@ type svgInfo struct {
// destroyExpiredSvgs destroys expired svgs cache data.
func destroyExpiredSvgs(now time.Time) {
- expiredSvgs := make([]string, 0, 20)
svgs.Range(func(key, value any) bool {
- s, sinfo := key.(string), value.(*svgInfo)
+ sinfo := value.(*svgInfo)
if sinfo.isExpired(now) {
- expiredSvgs = append(expiredSvgs, s)
+ svgs.Delete(key)
}
return true
})
-
- for _, exp := range expiredSvgs {
- svgs.Delete(exp)
- }
}
func overriddenName(name string, o fyne.CanvasObject) string {
diff --git a/internal/cache/text.go b/internal/cache/text.go
index f41bb55b67..0cbf754e9f 100644
--- a/internal/cache/text.go
+++ b/internal/cache/text.go
@@ -58,18 +58,11 @@ func SetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source
// destroyExpiredFontMetrics destroys expired fontSizeCache entries
func destroyExpiredFontMetrics(now time.Time) {
- expiredObjs := make([]fontSizeEntry, 0, 50)
- fontSizeLock.RLock()
+ fontSizeLock.Lock()
for k, v := range fontSizeCache {
if v.isExpired(now) {
- expiredObjs = append(expiredObjs, k)
+ delete(fontSizeCache, k)
}
}
- fontSizeLock.RUnlock()
-
- fontSizeLock.Lock()
- for _, k := range expiredObjs {
- delete(fontSizeCache, k)
- }
fontSizeLock.Unlock()
}
diff --git a/internal/clip.go b/internal/clip.go
index af9d162d64..dc1ddbc358 100644
--- a/internal/clip.go
+++ b/internal/clip.go
@@ -28,7 +28,7 @@ func (c *ClipStack) Length() int {
}
// Push a new clip onto this stack at position and size specified.
-// The returned clip item is the result of calculating the intersection of the requested clip and it's parent.
+// The returned clip item is the result of calculating the intersection of the requested clip and its parent.
func (c *ClipStack) Push(p fyne.Position, s fyne.Size) *ClipItem {
outer := c.Top()
inner := outer.Intersect(p, s)
diff --git a/internal/driver/common/window.go b/internal/driver/common/window.go
index 0a3c1f0c83..a01576819a 100644
--- a/internal/driver/common/window.go
+++ b/internal/driver/common/window.go
@@ -1,8 +1,6 @@
package common
import (
- "sync"
-
"fyne.io/fyne/v2/internal/async"
)
@@ -38,15 +36,15 @@ func (w *Window) RunEventQueue() {
// WaitForEvents wait for all the events.
func (w *Window) WaitForEvents() {
- done := DonePool.Get().(chan struct{})
+ done := DonePool.Get()
defer DonePool.Put(done)
w.eventQueue.In() <- func() { done <- struct{}{} }
<-done
}
-var DonePool = sync.Pool{
- New: func() any {
+var DonePool = async.Pool[chan struct{}]{
+ New: func() chan struct{} {
return make(chan struct{})
},
}
diff --git a/internal/driver/glfw/loop.go b/internal/driver/glfw/loop.go
index 0b9488dab9..6c775dae8f 100644
--- a/internal/driver/glfw/loop.go
+++ b/internal/driver/glfw/loop.go
@@ -46,7 +46,7 @@ func runOnMain(f func()) {
return
}
- done := common.DonePool.Get().(chan struct{})
+ done := common.DonePool.Get()
defer common.DonePool.Put(done)
funcQueue <- funcData{f: f, done: done}
@@ -60,7 +60,7 @@ func runOnDraw(w *window, f func()) {
runOnMain(func() { w.RunWithContext(f) })
return
}
- done := common.DonePool.Get().(chan struct{})
+ done := common.DonePool.Get()
defer common.DonePool.Put(done)
drawFuncQueue <- drawData{f: f, win: w, done: done}
diff --git a/internal/driver/glfw/menu_darwin.m b/internal/driver/glfw/menu_darwin.m
index 66157be086..b40be98252 100644
--- a/internal/driver/glfw/menu_darwin.m
+++ b/internal/driver/glfw/menu_darwin.m
@@ -83,11 +83,14 @@ void handleException(const char* m, id e) {
exceptionCallback([[NSString stringWithFormat:@"%s failed: %@", m, e] UTF8String]);
}
+int replacedAbout = 0;
+
const void* insertDarwinMenuItem(const void* m, const char* label, const char* keyEquivalent, unsigned int keyEquivalentModifierMask, int nextId, int index, bool isSeparator, const void *imageData, unsigned int imageDataLength) {
NSMenu* menu = (NSMenu*)m;
NSMenuItem* item;
- if (strcmp(label, "About") == 0) {
+ if (strcmp(label, "About") == 0 && !replacedAbout) {
+ replacedAbout = 1;
item = [menu itemArray][0];
[item setAction:@selector(tapped:)];
[item setTarget:[FyneMenuHandler class]];
diff --git a/internal/driver/glfw/window.go b/internal/driver/glfw/window.go
index 81b50a7b07..52cdfe6c2d 100644
--- a/internal/driver/glfw/window.go
+++ b/internal/driver/glfw/window.go
@@ -53,7 +53,7 @@ func (w *window) screenSize(canvasSize fyne.Size) (int, int) {
}
func (w *window) Resize(size fyne.Size) {
- // we cannot perform this until window is prepared as we don't know it's scale!
+ // we cannot perform this until window is prepared as we don't know its scale!
bigEnough := size.Max(w.canvas.canvasSize(w.canvas.Content().MinSize()))
w.runOnMainWhenCreated(func() {
w.viewLock.Lock()
diff --git a/internal/driver/mobile/file_android.go b/internal/driver/mobile/file_android.go
index b266c92916..1bbf82297e 100644
--- a/internal/driver/mobile/file_android.go
+++ b/internal/driver/mobile/file_android.go
@@ -25,6 +25,7 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/internal/driver/mobile/app"
+ "fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/storage/repository"
)
@@ -90,7 +91,7 @@ func nativeFileOpen(f *fileOpen) (io.ReadCloser, error) {
ret := openStream(f.uri.String())
if ret == nil {
- return nil, errors.New("resource not found at URI")
+ return nil, storage.ErrNotExists
}
stream := &javaStream{}
@@ -122,7 +123,7 @@ func nativeFileSave(f *fileSave) (io.WriteCloser, error) {
ret := saveStream(f.uri.String())
if ret == nil {
- return nil, errors.New("resource not found at URI")
+ return nil, storage.ErrNotExists
}
stream := &javaStream{}
diff --git a/internal/painter/gl/texture.go b/internal/painter/gl/texture.go
index 3db3cecd18..207ee6cd12 100644
--- a/internal/painter/gl/texture.go
+++ b/internal/painter/gl/texture.go
@@ -14,6 +14,8 @@ import (
"fyne.io/fyne/v2/theme"
)
+const floatEqualityThreshold = 1e-9
+
var noTexture = Texture(cache.NoTexture)
// Texture represents an uploaded GL texture
@@ -104,10 +106,10 @@ func (p *painter) newGlLinearGradientTexture(obj fyne.CanvasObject) Texture {
w := gradient.Size().Width
h := gradient.Size().Height
- switch gradient.Angle {
- case 90, 270:
+ switch a := gradient.Angle; {
+ case almostEqual(a, 90), almostEqual(a, 270):
h = 1
- case 0, 180:
+ case almostEqual(a, 0), almostEqual(a, 180):
w = 1
}
width := p.textureScale(w)
@@ -178,3 +180,7 @@ func (p *painter) textureScale(v float32) float32 {
return float32(math.Round(float64(v * p.pixScale)))
}
+
+func almostEqual(a, b float64) bool {
+ return math.Abs(a-b) < floatEqualityThreshold
+}
diff --git a/internal/repository/file.go b/internal/repository/file.go
index 45837cd4dc..77e3c307ca 100644
--- a/internal/repository/file.go
+++ b/internal/repository/file.go
@@ -183,7 +183,7 @@ func (r *FileRepository) Parent(u fyne.URI) (fyne.URI, error) {
parent += "/"
}
- // only root is it's own parent
+ // only root is its own parent
if filepath.Clean(parent) == filepath.Clean(s) {
return nil, repository.ErrURIRoot
}
diff --git a/internal/repository/memory.go b/internal/repository/memory.go
index 3537f58523..b24234cfcd 100644
--- a/internal/repository/memory.go
+++ b/internal/repository/memory.go
@@ -39,7 +39,7 @@ type nodeReaderWriter struct {
// "virtual repository". In future, we may consider moving this into the public
// API.
//
-// Because of it's design, this repository has several quirks:
+// Because of its design, this repository has several quirks:
//
// * The Parent() of a path that exists does not necessarily exist
//
diff --git a/lang/lang.go b/lang/lang.go
index 50f15cdbc1..6a2e58bfc7 100644
--- a/lang/lang.go
+++ b/lang/lang.go
@@ -161,8 +161,12 @@ func AddTranslationsFS(fs embed.FS, dir string) (retErr error) {
func addLanguage(data []byte, name string) error {
f, err := bundle.ParseMessageFileBytes(data, name)
+ if err != nil {
+ return err
+ }
+
translated = append(translated, f.Tag)
- return err
+ return nil
}
func init() {
diff --git a/lang/translations/base.de.json b/lang/translations/base.de.json
index 92205eef21..4d92e19db3 100644
--- a/lang/translations/base.de.json
+++ b/lang/translations/base.de.json
@@ -28,4 +28,4 @@
"Paste": "Einfügen",
"Cut": "Ausschneiden",
"OK": "OK"
-}
\ No newline at end of file
+}
diff --git a/lang/translations/base.en.json b/lang/translations/base.en.json
index 559985864d..58535ebeaf 100644
--- a/lang/translations/base.en.json
+++ b/lang/translations/base.en.json
@@ -28,5 +28,20 @@
},
"file.parent": {
"other": "Parent"
- }
+ },
+
+ "monday": "Monday",
+ "monday.short": "Mon",
+ "tuesday": "Tuesday",
+ "tuesday.short": "Tue",
+ "wednesday": "Wednesday",
+ "wednesday.short": "Wed",
+ "thursday": "Thursday",
+ "thursday.short": "Thu",
+ "friday": "Friday",
+ "friday.short": "Fri",
+ "saturday": "Saturday",
+ "saturday.short": "Sat",
+ "sunday": "Sunday",
+ "sunday.short": "Sun"
}
diff --git a/lang/translations/base.pl.json b/lang/translations/base.pl.json
new file mode 100644
index 0000000000..8d8140eb01
--- /dev/null
+++ b/lang/translations/base.pl.json
@@ -0,0 +1,31 @@
+{
+ "Advanced": "Zaawansowane",
+ "Cancel": "Anuluj",
+ "Confirm": "Potwierdz",
+ "Copy": "Kopiuj",
+ "Cut": "Wytnij",
+ "Error": "Błąd",
+ "Favourites": "Ulubione",
+ "File": "Plik",
+ "New Folder": "Nowy folder",
+ "No": "Nie",
+ "OK": "OK",
+ "Open": "Otwórz",
+ "Paste": "Wklej",
+ "Quit": "Wyjdz",
+ "Redo": "Przerób",
+ "Save": "Zapisz",
+ "Show Hidden Files": "Pokaż Ukryte Pliki",
+ "Enter filename": "Wprowadź Nazwę Pliku",
+ "Select all": "Zaznacz Wszystko",
+ "Yes": "Tak",
+ "file.name": {
+ "other": "Nawza"
+ },
+ "Folder": "Folder",
+ "Create Folder": "Utwórz Folder",
+ "Undo": "Cofnij",
+ "file.parent": {
+ "other": "Rodzic"
+ }
+}
diff --git a/lang/translations/base.sv.json b/lang/translations/base.sv.json
index 0967ef424b..d22bcb8abc 100644
--- a/lang/translations/base.sv.json
+++ b/lang/translations/base.sv.json
@@ -1 +1,45 @@
-{}
+{
+ "OK": "OK",
+ "Save": "Spara",
+ "Advanced": "Avancerad",
+ "Confirm": "Bekräfta",
+ "Error": "Fel",
+ "Cancel": "Avbryt",
+ "Copy": "Kopiera",
+ "Create Folder": "Skapa mapp",
+ "Cut": "Klipp ut",
+ "Enter filename": "Ange filnamn",
+ "Favourites": "Favoriter",
+ "File": "Arkiv",
+ "Folder": "Mapp",
+ "New Folder": "Ny mapp",
+ "Open": "Öppna",
+ "Select all": "Markera allt",
+ "Undo": "Ångra",
+ "Yes": "Ja",
+ "file.name": {
+ "other": "Namn"
+ },
+ "Show Hidden Files": "Visa dolda filer",
+ "thursday": "Torsdag",
+ "thursday.short": "Tor",
+ "friday": "Fredag",
+ "friday.short": "Fre",
+ "saturday": "Lördag",
+ "saturday.short": "Lör",
+ "sunday": "Söndag",
+ "sunday.short": "Sön",
+ "No": "Nej",
+ "Paste": "Klistra in",
+ "Quit": "Avsluta",
+ "Redo": "Gör om",
+ "monday": "Måndag",
+ "monday.short": "Mån",
+ "file.parent": {
+ "other": "Överordnad"
+ },
+ "tuesday": "Tisdag",
+ "tuesday.short": "Tis",
+ "wednesday": "Onsdag",
+ "wednesday.short": "Ons"
+}
diff --git a/lang/translations/base.uk.json b/lang/translations/base.uk.json
new file mode 100644
index 0000000000..dcecab482c
--- /dev/null
+++ b/lang/translations/base.uk.json
@@ -0,0 +1,31 @@
+{
+ "Folder": "Тека",
+ "Cancel": "Скасувати",
+ "Confirm": "Підтвердити",
+ "Copy": "Скопіювати",
+ "Create Folder": "Створити теку",
+ "Cut": "Вирізати",
+ "Enter filename": "Введіть назву теки",
+ "Error": "Помилка",
+ "Favourites": "Обране",
+ "File": "Файл",
+ "New Folder": "Нова тека",
+ "No": "Ні",
+ "OK": "Добре",
+ "Open": "Відкрити",
+ "Paste": "Вставити",
+ "Quit": "Вийти",
+ "Save": "Зберегти",
+ "Select all": "Обрати все",
+ "Show Hidden Files": "Показувати приховані файли",
+ "Yes": "Так",
+ "file.name": {
+ "other": "Назва"
+ },
+ "file.parent": {
+ "other": "Батьківська"
+ },
+ "Advanced": "Розширені",
+ "Undo": "Відмінити",
+ "Redo": "Повторити"
+}
diff --git a/layout.go b/layout.go
index f00b4a0cd8..8a3d8047ef 100644
--- a/layout.go
+++ b/layout.go
@@ -1,11 +1,11 @@
package fyne
-// Layout defines how CanvasObjects may be laid out in a specified Size.
+// Layout defines how [CanvasObject]s may be laid out in a specified Size.
type Layout interface {
- // Layout will manipulate the listed CanvasObjects Size and Position
+ // Layout will manipulate the listed [CanvasObject]s Size and Position
// to fit within the specified size.
Layout([]CanvasObject, Size)
// MinSize calculates the smallest size that will fit the listed
- // CanvasObjects using this Layout algorithm.
+ // [CanvasObject]s using this Layout algorithm.
MinSize(objects []CanvasObject) Size
}
diff --git a/layout/borderlayout.go b/layout/borderlayout.go
index 51c54c1037..2cc420c9dd 100644
--- a/layout/borderlayout.go
+++ b/layout/borderlayout.go
@@ -13,8 +13,8 @@ type borderLayout struct {
}
// NewBorderLayout creates a new BorderLayout instance with top, bottom, left
-// and right objects set. All other items in the container will fill the centre
-// space
+// and right objects set. All other items in the container will fill the remaining space in the middle.
+// Multiple extra items will be stacked in the specified order as a Stack container.
func NewBorderLayout(top, bottom, left, right fyne.CanvasObject) fyne.Layout {
return &borderLayout{top, bottom, left, right}
}
diff --git a/menu.go b/menu.go
index e88215c36b..7a3c54736b 100644
--- a/menu.go
+++ b/menu.go
@@ -7,13 +7,13 @@ type systemTrayDriver interface {
}
// Menu stores the information required for a standard menu.
-// A menu can pop down from a MainMenu or could be a pop out menu.
+// A menu can pop down from a [MainMenu] or could be a pop out menu.
type Menu struct {
Label string
Items []*MenuItem
}
-// NewMenu creates a new menu given the specified label (to show in a MainMenu) and list of items to display.
+// NewMenu creates a new menu given the specified label (to show in a [MainMenu]) and list of items to display.
func NewMenu(label string, items ...*MenuItem) *Menu {
return &Menu{Label: label, Items: items}
}
diff --git a/notification.go b/notification.go
index 340173de4f..a068623d85 100644
--- a/notification.go
+++ b/notification.go
@@ -5,7 +5,7 @@ type Notification struct {
Title, Content string
}
-// NewNotification creates a notification that can be passed to App.SendNotification.
+// NewNotification creates a notification that can be passed to [App.SendNotification].
func NewNotification(title, content string) *Notification {
return &Notification{Title: title, Content: content}
}
diff --git a/overlay_stack.go b/overlay_stack.go
index 69be64e472..eb7c9c7658 100644
--- a/overlay_stack.go
+++ b/overlay_stack.go
@@ -1,6 +1,6 @@
package fyne
-// OverlayStack is a stack of CanvasObjects intended to be used as overlays of a Canvas.
+// OverlayStack is a stack of [CanvasObject]s intended to be used as overlays of a [Canvas].
type OverlayStack interface {
// Add adds an overlay on the top of the overlay stack.
Add(overlay CanvasObject)
diff --git a/resource.go b/resource.go
index d2d3bb67d9..95e32370dd 100644
--- a/resource.go
+++ b/resource.go
@@ -16,8 +16,8 @@ type Resource interface {
Content() []byte
}
-// ThemedResource is a version of a resource that can be updated to match a certain theme colour.
-// The `ThemeColorName` will be used to look up the color for the current theme and colorize the resource.
+// ThemedResource is a version of a resource that can be updated to match a certain theme color.
+// The [ThemeColorName] will be used to look up the color for the current theme and colorize the resource.
//
// Since: 2.5
type ThemedResource interface {
@@ -55,7 +55,7 @@ func NewStaticResource(name string, content []byte) *StaticResource {
}
}
-// LoadResourceFromPath creates a new StaticResource in memory using the contents of the specified file.
+// LoadResourceFromPath creates a new [StaticResource] in memory using the contents of the specified file.
func LoadResourceFromPath(path string) (Resource, error) {
bytes, err := os.ReadFile(filepath.Clean(path))
if err != nil {
@@ -66,7 +66,7 @@ func LoadResourceFromPath(path string) (Resource, error) {
return NewStaticResource(name, bytes), nil
}
-// LoadResourceFromURLString creates a new StaticResource in memory using the body of the specified URL.
+// LoadResourceFromURLString creates a new [StaticResource] in memory using the body of the specified URL.
func LoadResourceFromURLString(urlStr string) (Resource, error) {
res, err := http.Get(urlStr)
if err != nil {
diff --git a/settings.go b/settings.go
index b6ad42c144..9475d0cc3e 100644
--- a/settings.go
+++ b/settings.go
@@ -8,7 +8,7 @@ const (
BuildStandard BuildType = iota
// BuildDebug is used when a developer would like more information and visual output for app debugging.
BuildDebug
- // BuildRelease is a final production build, it is like BuildStandard but will use distribution certificates.
+ // BuildRelease is a final production build, it is like [BuildStandard] but will use distribution certificates.
// A release build is typically going to connect to live services and is not usually used during development.
BuildRelease
)
diff --git a/shortcut.go b/shortcut.go
index ebe33ea0e3..4fcef3cae0 100644
--- a/shortcut.go
+++ b/shortcut.go
@@ -5,7 +5,7 @@ import (
)
// ShortcutHandler is a default implementation of the shortcut handler
-// for the canvasObject
+// for [CanvasObject].
type ShortcutHandler struct {
entry sync.Map // map[string]func(Shortcut)
}
@@ -50,16 +50,16 @@ type ShortcutPaste struct {
var _ KeyboardShortcut = (*ShortcutPaste)(nil)
-// Key returns the KeyName for this shortcut.
+// Key returns the [KeyName] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutPaste) Key() KeyName {
return KeyV
}
-// Mod returns the KeyModifier for this shortcut.
+// Mod returns the [KeyModifier] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutPaste) Mod() KeyModifier {
return KeyModifierShortcutDefault
}
@@ -76,16 +76,16 @@ type ShortcutCopy struct {
var _ KeyboardShortcut = (*ShortcutCopy)(nil)
-// Key returns the KeyName for this shortcut.
+// Key returns the [KeyName] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutCopy) Key() KeyName {
return KeyC
}
-// Mod returns the KeyModifier for this shortcut.
+// Mod returns the [KeyModifier] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutCopy) Mod() KeyModifier {
return KeyModifierShortcutDefault
}
@@ -102,16 +102,16 @@ type ShortcutCut struct {
var _ KeyboardShortcut = (*ShortcutCut)(nil)
-// Key returns the KeyName for this shortcut.
+// Key returns the [KeyName] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutCut) Key() KeyName {
return KeyX
}
-// Mod returns the KeyModifier for this shortcut.
+// Mod returns the [KeyModifier] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutCut) Mod() KeyModifier {
return KeyModifierShortcutDefault
}
@@ -126,16 +126,16 @@ type ShortcutSelectAll struct{}
var _ KeyboardShortcut = (*ShortcutSelectAll)(nil)
-// Key returns the KeyName for this shortcut.
+// Key returns the [KeyName] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutSelectAll) Key() KeyName {
return KeyA
}
-// Mod returns the KeyModifier for this shortcut.
+// Mod returns the [KeyModifier] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutSelectAll) Mod() KeyModifier {
return KeyModifierShortcutDefault
}
@@ -152,16 +152,16 @@ type ShortcutUndo struct{}
var _ KeyboardShortcut = (*ShortcutUndo)(nil)
-// Key returns the KeyName for this shortcut.
+// Key returns the [KeyName] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutUndo) Key() KeyName {
return KeyZ
}
-// Mod returns the KeyModifier for this shortcut.
+// Mod returns the [KeyModifier] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutUndo) Mod() KeyModifier {
return KeyModifierShortcutDefault
}
@@ -178,16 +178,16 @@ type ShortcutRedo struct{}
var _ KeyboardShortcut = (*ShortcutRedo)(nil)
-// Key returns the KeyName for this shortcut.
+// Key returns the [KeyName] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutRedo) Key() KeyName {
return KeyY
}
-// Mod returns the KeyModifier for this shortcut.
+// Mod returns the [KeyModifier] for this shortcut.
//
-// Implements: KeyboardShortcut
+// Implements: [KeyboardShortcut]
func (se *ShortcutRedo) Mod() KeyModifier {
return KeyModifierShortcutDefault
}
diff --git a/storage/repository/generic.go b/storage/repository/generic.go
index ebe314c5d9..46d841d702 100644
--- a/storage/repository/generic.go
+++ b/storage/repository/generic.go
@@ -23,7 +23,7 @@ func splitNonEmpty(str, sep string) []string {
// HierarchicalRepository.Parent(). It will create a parent URI based on
// IETF RFC3986.
//
-// In short, the URI is separated into it's component parts, the path component
+// In short, the URI is separated into its component parts, the path component
// is split along instances of '/', and the trailing element is removed. The
// result is concatenated and parsed as a new URI.
//
@@ -63,13 +63,13 @@ func GenericParent(u fyne.URI) (fyne.URI, error) {
// NOTE: we specifically want to use ParseURI, rather than &uri{},
// since the repository for the URI we just created might be a
- // CustomURIRepository that implements it's own ParseURI.
+ // CustomURIRepository that implements its own ParseURI.
return ParseURI(newURI)
}
// GenericChild can be used as a common-case implementation of
// HierarchicalRepository.Child(). It will create a child URI by separating the
-// URI into it's component parts as described in IETF RFC 3986, then appending
+// URI into its component parts as described in IETF RFC 3986, then appending
// "/" + component to the path, then concatenating the result and parsing it as
// a new URI.
//
@@ -97,7 +97,7 @@ func GenericChild(u fyne.URI, component string) (fyne.URI, error) {
// NOTE: we specifically want to use ParseURI, rather than &uri{},
// since the repository for the URI we just created might be a
- // CustomURIRepository that implements it's own ParseURI.
+ // CustomURIRepository that implements its own ParseURI.
return ParseURI(newURI)
}
diff --git a/storage/uri.go b/storage/uri.go
index 5a8974a34e..bdbe6008b3 100644
--- a/storage/uri.go
+++ b/storage/uri.go
@@ -494,7 +494,7 @@ func List(u fyne.URI) ([]fyne.URI, error) {
// Storage repositories which support listing, but not creation of listable
// objects may return repository.ErrOperationNotSupported.
//
-// CreateListable should generally fail if the parent of it's operand does not
+// CreateListable should generally fail if the parent of its operand does not
// exist, however this can vary by the implementation details of the specific
// storage repository. In filesystem terms, this function is "mkdir" not "mkdir
// -p".
diff --git a/test/driver.go b/test/driver.go
index 8d26384b84..e4a30dc06d 100644
--- a/test/driver.go
+++ b/test/driver.go
@@ -74,7 +74,7 @@ func (d *driver) CanvasForObject(fyne.CanvasObject) fyne.Canvas {
return d.windows[len(d.windows)-1].Canvas()
}
-func (d *driver) CreateWindow(string) fyne.Window {
+func (d *driver) CreateWindow(title string) fyne.Window {
c := NewCanvas().(*canvas)
if d.painter != nil {
c.painter = d.painter
@@ -82,7 +82,7 @@ func (d *driver) CreateWindow(string) fyne.Window {
c.painter = software.NewPainter()
}
- w := &window{canvas: c, driver: d}
+ w := &window{canvas: c, driver: d, title: title}
d.windowsMutex.Lock()
d.windows = append(d.windows, w)
diff --git a/test/driver_test.go b/test/driver_test.go
index 923b35fa27..74db6ccab5 100644
--- a/test/driver_test.go
+++ b/test/driver_test.go
@@ -27,3 +27,10 @@ func Test_driver_AbsolutePositionForObject(t *testing.T) {
assert.Equal(t, fyne.NewPos(-2, -3), d.AbsolutePositionForObject(o), "safe area offset (2,3) is subtracted")
})
}
+
+func TestDriver_CreateWindow(t *testing.T) {
+ d := &driver{}
+ w := d.CreateWindow("Test Window")
+
+ assert.Equal(t, "Test Window", w.Title())
+}
diff --git a/test/test.go b/test/test.go
index 2989d6236b..7cc288dc3f 100644
--- a/test/test.go
+++ b/test/test.go
@@ -47,6 +47,19 @@ func AssertObjectRendersToImage(t *testing.T, masterFilename string, o fyne.Canv
return AssertRendersToImage(t, masterFilename, c, msgAndArgs...)
}
+// RenderObjectToMarkup renders the given [fyne.io/fyne/v2.CanvasObject] to a markup string.
+//
+// Since: 2.6
+func RenderObjectToMarkup(o fyne.CanvasObject) string {
+ c := NewCanvas()
+ c.SetPadded(false)
+ size := o.MinSize().Max(o.Size())
+ c.SetContent(o)
+ c.Resize(size) // ensure we are large enough for current size
+
+ return snapshot(c)
+}
+
// AssertObjectRendersToMarkup asserts that the given `CanvasObject` renders the same markup as the one stored in the master file.
// The master filename is relative to the `testdata` directory which is relative to the test.
// The test `t` fails if the rendered markup is not equal to the loaded master markup.
@@ -89,6 +102,13 @@ func AssertRendersToImage(t *testing.T, masterFilename string, c fyne.Canvas, ms
return test.AssertImageMatches(t, masterFilename, c.Capture(), msgAndArgs...)
}
+// RenderToMarkup renders the given [fyne.io/fyne/v2.Canvas] to a markup string.
+//
+// Since: 2.6
+func RenderToMarkup(c fyne.Canvas) string {
+ return snapshot(c)
+}
+
// AssertRendersToMarkup asserts that the given canvas renders the same markup as the one stored in the master file.
// The master filename is relative to the `testdata` directory which is relative to the test.
// The test `t` fails if the rendered markup is not equal to the loaded master markup.
diff --git a/test/test_test.go b/test/test_test.go
index 71eef68587..c4c515ab13 100644
--- a/test/test_test.go
+++ b/test/test_test.go
@@ -1,8 +1,10 @@
package test_test
import (
+ "bytes"
"image/color"
"os"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -39,6 +41,18 @@ func TestAssertObjectRendersToImage(t *testing.T) {
test.AssertObjectRendersToImage(t, "circle.png", obj)
}
+func TestRenderObjectToMarkup(t *testing.T) {
+ obj := canvas.NewCircle(color.Black)
+ obj.Resize(fyne.NewSize(20, 20))
+
+ want, err := os.ReadFile("testdata/circle.xml")
+ require.NoError(t, err)
+ // Fix Windows newlines
+ want = bytes.ReplaceAll(want, []byte("\r\n"), []byte("\n"))
+ got := strings.ReplaceAll(test.RenderObjectToMarkup(obj), "\r\n", "\n")
+ assert.Equal(t, string(want), got, "existing master is equal to rendered markup")
+}
+
func TestAssertObjectRendersToMarkup(t *testing.T) {
obj := canvas.NewCircle(color.Black)
obj.Resize(fyne.NewSize(20, 20))
@@ -82,6 +96,18 @@ func TestAssertRendersToImage(t *testing.T) {
}
}
+func TestRenderToMarkup(t *testing.T) {
+ c := test.NewCanvas()
+ c.SetContent(canvas.NewCircle(color.Black))
+
+ want, err := os.ReadFile("testdata/markup_master.xml")
+ require.NoError(t, err)
+ // Fix Windows newlines
+ want = bytes.ReplaceAll(want, []byte("\r\n"), []byte("\n"))
+ got := strings.ReplaceAll(test.RenderToMarkup(c), "\r\n", "\n")
+ assert.Equal(t, string(want), got, "existing master is equal to rendered markup")
+}
+
func TestAssertRendersToMarkup(t *testing.T) {
c := test.NewCanvas()
c.SetContent(canvas.NewCircle(color.Black))
diff --git a/text.go b/text.go
index 3c8b6df12f..a300811184 100644
--- a/text.go
+++ b/text.go
@@ -41,7 +41,7 @@ const (
// TextTruncate trims the text to the widget's width, no wrapping is applied.
// If an entry is asked to truncate it will provide scrolling capabilities.
//
- // Deprecated: Use `TextTruncateClip` value of the widget `Truncation` field instead
+ // Deprecated: Use [TextTruncateClip] value of the widget `Truncation` field instead
TextTruncate
// TextWrapBreak trims the line of characters to the widget's width adding the excess as new line.
// An Entry with text wrapping will scroll vertically if there is not enough space for all the text.
@@ -62,7 +62,7 @@ type TextStyle struct {
// Since: 2.1
TabWidth int // Width of tabs in spaces
// Since: 2.5
- // Currently only supported by the TextGrid widget.
+ // Currently only supported by [fyne.io/fyne/v2/widget.TextGrid].
Underline bool // Should text be underlined.
}
diff --git a/theme/bundled-icons.go b/theme/bundled-icons.go
index f16676a82b..85d89ca658 100644
--- a/theme/bundled-icons.go
+++ b/theme/bundled-icons.go
@@ -450,6 +450,11 @@ var accountIconRes = &fyne.StaticResource{
StaticContent: []byte(""),
}
+var calendarIconRes = &fyne.StaticResource{
+ StaticName: "calendar.svg",
+ StaticContent: []byte("\n"),
+}
+
var loginIconRes = &fyne.StaticResource{
StaticName: "login.svg",
StaticContent: []byte(""),
diff --git a/theme/gen.go b/theme/gen.go
index f99906dd83..39c2ad7993 100644
--- a/theme/gen.go
+++ b/theme/gen.go
@@ -192,6 +192,7 @@ func main() {
bundleIcon("upload", f)
bundleIcon("account", f)
+ bundleIcon("calendar", f)
bundleIcon("login", f)
bundleIcon("logout", f)
diff --git a/theme/icons.go b/theme/icons.go
index 2082c60cb9..a0fc817849 100644
--- a/theme/icons.go
+++ b/theme/icons.go
@@ -456,6 +456,11 @@ const (
// Since: 2.1
IconNameAccount fyne.ThemeIconName = "account"
+ // IconNameCalendar is the name of theme lookup for calendar icon.
+ //
+ // Since: 2.6
+ IconNameCalendar fyne.ThemeIconName = "calendar"
+
// IconNameLogin is the name of theme lookup for login icon.
//
// Since: 2.1
@@ -599,9 +604,10 @@ var (
IconNameStorage: NewThemedResource(storageIconRes),
IconNameUpload: NewThemedResource(uploadIconRes),
- IconNameAccount: NewThemedResource(accountIconRes),
- IconNameLogin: NewThemedResource(loginIconRes),
- IconNameLogout: NewThemedResource(logoutIconRes),
+ IconNameAccount: NewThemedResource(accountIconRes),
+ IconNameCalendar: NewThemedResource(calendarIconRes),
+ IconNameLogin: NewThemedResource(loginIconRes),
+ IconNameLogout: NewThemedResource(logoutIconRes),
IconNameList: NewThemedResource(listIconRes),
IconNameGrid: NewThemedResource(gridIconRes),
@@ -1285,6 +1291,13 @@ func AccountIcon() fyne.Resource {
return safeIconLookup(IconNameAccount)
}
+// CalendarIcon returns a resource containing the standard account icon for the current theme
+//
+// Since: 2.6
+func CalendarIcon() fyne.Resource {
+ return safeIconLookup(IconNameCalendar)
+}
+
// LoginIcon returns a resource containing the standard login icon for the current theme
func LoginIcon() fyne.Resource {
return safeIconLookup(IconNameLogin)
diff --git a/theme/icons/calendar.svg b/theme/icons/calendar.svg
new file mode 100644
index 0000000000..6c448c22aa
--- /dev/null
+++ b/theme/icons/calendar.svg
@@ -0,0 +1,5 @@
+
diff --git a/tools/playground/playground.go b/tools/playground/playground.go
index 8dfb852dda..d8dbac3773 100644
--- a/tools/playground/playground.go
+++ b/tools/playground/playground.go
@@ -31,7 +31,7 @@ func RenderCanvas(c fyne.Canvas) {
imageToPlayground(software.RenderCanvas(c, test.DarkTheme(theme.DefaultTheme())))
}
-// RenderWindow takes a window and converts it's canvas into an inline image for showing in the playground
+// RenderWindow takes a window and converts its canvas into an inline image for showing in the playground
func RenderWindow(w fyne.Window) {
RenderCanvas(w.Canvas())
}
diff --git a/uri.go b/uri.go
index 4cde1a1e04..a4228306d2 100644
--- a/uri.go
+++ b/uri.go
@@ -26,9 +26,9 @@ type URIWriteCloser interface {
// system.
//
// In general, it is expected that URI implementations follow IETF RFC3896.
-// Implementations are highly recommended to utilize net/url to implement URI
-// parsing methods, especially Scheme(), AUthority(), Path(), Query(), and
-// Fragment().
+// Implementations are highly recommended to utilize [net/url] to implement URI
+// parsing methods, especially [net/url/url.Scheme], [net/url/url.Authority],
+// [net/url/url.Path], [net/url/url.Query], and [net/url/url.Fragment].
type URI interface {
fmt.Stringer
@@ -39,7 +39,7 @@ type URI interface {
Extension() string
// Name should return the base name of the item referenced by the URI.
- // For example, the Name() of 'file://foo/bar.baz' is 'bar.baz'.
+ // For example, the name of 'file://foo/bar.baz' is 'bar.baz'.
Name() string
// MimeType should return the content type of the resource referenced
@@ -57,8 +57,8 @@ type URI interface {
// Authority should return the URI authority, as defined by IETF
// RFC3986.
//
- // NOTE: the RFC3986 can be obtained by combining the User and Host
- // Fields of net/url's URL structure. Consult IETF RFC3986, section
+ // NOTE: the RFC3986 can be obtained by combining the [net/url.URL.User]
+ // and [net/url.URL.Host]. Consult IETF RFC3986, section
// 3.2, pp. 17.
//
// Since: 2.0
@@ -81,7 +81,7 @@ type URI interface {
Fragment() string
}
-// ListableURI represents a URI that can have child items, most commonly a
+// ListableURI represents a [URI] that can have child items, most commonly a
// directory on disk in the native filesystem.
//
// Since: 1.4
@@ -92,7 +92,7 @@ type ListableURI interface {
List() ([]URI, error)
}
-// URIWithIcon describes a URI that should be rendered with a certain icon in file browsers.
+// URIWithIcon describes a [URI] that should be rendered with a certain icon in file browsers.
//
// Since: 2.5
type URIWithIcon interface {
diff --git a/widget.go b/widget.go
index 07741aa976..644a7a456c 100644
--- a/widget.go
+++ b/widget.go
@@ -1,33 +1,33 @@
package fyne
-// Widget defines the standard behaviours of any widget. This extends the
-// CanvasObject - a widget behaves in the same basic way but will encapsulate
+// Widget defines the standard behaviours of any widget. This extends
+// [CanvasObject]. A widget behaves in the same basic way but will encapsulate
// many child objects to create the rendered widget.
type Widget interface {
CanvasObject
- // CreateRenderer returns a new WidgetRenderer for this widget.
+ // CreateRenderer returns a new [WidgetRenderer] for this widget.
// This should not be called by regular code, it is used internally to render a widget.
CreateRenderer() WidgetRenderer
}
// WidgetRenderer defines the behaviour of a widget's implementation.
-// This is returned from a widget's declarative object through the CreateRenderer()
-// function and should be exactly one instance per widget in memory.
+// This is returned from a widget's declarative object through [Widget.CreateRenderer]
+// and should be exactly one instance per widget in memory.
type WidgetRenderer interface {
// Destroy is a hook that is called when the renderer is being destroyed.
// This happens at some time after the widget is no longer visible, and
- // once destroyed a renderer will not be reused.
+ // once destroyed, a renderer will not be reused.
// Renderers should dispose and clean up any related resources, if necessary.
Destroy()
// Layout is a hook that is called if the widget needs to be laid out.
- // This should never call Refresh.
+ // This should never call [Refresh].
Layout(Size)
// MinSize returns the minimum size of the widget that is rendered by this renderer.
MinSize() Size
// Objects returns all objects that should be drawn.
Objects() []CanvasObject
// Refresh is a hook that is called if the widget has updated and needs to be redrawn.
- // This might trigger a Layout.
+ // This might trigger a [Layout].
Refresh()
}
diff --git a/widget/button.go b/widget/button.go
index 62ccad2a37..c508bc0013 100644
--- a/widget/button.go
+++ b/widget/button.go
@@ -393,7 +393,7 @@ func (r *buttonRenderer) padding(th fyne.Theme) fyne.Size {
// must be called with r.button.propertyLock RLocked
func (r *buttonRenderer) updateIconAndText() {
- if r.button.Icon != nil && r.button.Visible() {
+ if r.button.Icon != nil && !r.button.Hidden {
icon := r.button.Icon
if r.icon == nil {
r.icon = canvas.NewImageFromResource(icon)
diff --git a/widget/calendar.go b/widget/calendar.go
new file mode 100644
index 0000000000..5761ecd697
--- /dev/null
+++ b/widget/calendar.go
@@ -0,0 +1,220 @@
+package widget
+
+import (
+ "math"
+ "strconv"
+ "strings"
+ "time"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/lang"
+ "fyne.io/fyne/v2/layout"
+ "fyne.io/fyne/v2/theme"
+)
+
+// Declare conformity with Layout interface
+var _ fyne.Layout = (*calendarLayout)(nil)
+
+const (
+ daysPerWeek = 7
+ maxWeeksPerMonth = 6
+)
+
+var minCellContent = NewLabel("22")
+
+// Calendar creates a new date time picker which returns a time object
+//
+// Since: 2.6
+type Calendar struct {
+ BaseWidget
+ currentTime time.Time
+
+ monthPrevious *Button
+ monthNext *Button
+ monthLabel *Label
+
+ dates *fyne.Container
+
+ OnChanged func(time.Time) `json:"-"`
+}
+
+// NewCalendar creates a calendar instance
+//
+// Since: 2.6
+func NewCalendar(cT time.Time, changed func(time.Time)) *Calendar {
+ c := &Calendar{
+ currentTime: cT,
+ OnChanged: changed,
+ }
+
+ c.ExtendBaseWidget(c)
+ return c
+}
+
+// CreateRenderer returns a new WidgetRenderer for this widget.
+// This should not be called by regular code, it is used internally to render a widget.
+func (c *Calendar) CreateRenderer() fyne.WidgetRenderer {
+ c.monthPrevious = NewButtonWithIcon("", theme.NavigateBackIcon(), func() {
+ c.currentTime = c.currentTime.AddDate(0, -1, 0)
+ // Dates are 'normalised', forcing date to start from the start of the month ensures move from March to February
+ c.currentTime = time.Date(c.currentTime.Year(), c.currentTime.Month(), 1, 0, 0, 0, 0, c.currentTime.Location())
+ c.monthLabel.SetText(c.monthYear())
+ c.dates.Objects = c.calendarObjects()
+ })
+ c.monthPrevious.Importance = LowImportance
+
+ c.monthNext = NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
+ c.currentTime = c.currentTime.AddDate(0, 1, 0)
+ c.monthLabel.SetText(c.monthYear())
+ c.dates.Objects = c.calendarObjects()
+ })
+ c.monthNext.Importance = LowImportance
+
+ c.monthLabel = NewLabel(c.monthYear())
+
+ nav := &fyne.Container{Layout: layout.NewBorderLayout(nil, nil, c.monthPrevious, c.monthNext),
+ Objects: []fyne.CanvasObject{c.monthPrevious, c.monthNext,
+ &fyne.Container{Layout: layout.NewCenterLayout(), Objects: []fyne.CanvasObject{c.monthLabel}}}}
+
+ c.dates = &fyne.Container{Layout: newCalendarLayout(), Objects: c.calendarObjects()}
+
+ dateContainer := &fyne.Container{Layout: layout.NewBorderLayout(nav, nil, nil, nil),
+ Objects: []fyne.CanvasObject{nav, c.dates}}
+
+ return NewSimpleRenderer(dateContainer)
+}
+
+func (c *Calendar) calendarObjects() []fyne.CanvasObject {
+ var columnHeadings []fyne.CanvasObject
+ for i := 0; i < daysPerWeek; i++ {
+ j := i + 1
+ if j == daysPerWeek {
+ j = 0
+ }
+
+ t := NewLabel(shortDayName(time.Weekday(j).String()))
+ t.Alignment = fyne.TextAlignCenter
+ columnHeadings = append(columnHeadings, t)
+ }
+ columnHeadings = append(columnHeadings, c.daysOfMonth()...)
+
+ return columnHeadings
+}
+
+func (c *Calendar) dateForButton(dayNum int) time.Time {
+ oldName, off := c.currentTime.Zone()
+ return time.Date(c.currentTime.Year(), c.currentTime.Month(), dayNum, c.currentTime.Hour(), c.currentTime.Minute(), 0, 0, time.FixedZone(oldName, off)).In(c.currentTime.Location())
+}
+
+func (c *Calendar) daysOfMonth() []fyne.CanvasObject {
+ start := time.Date(c.currentTime.Year(), c.currentTime.Month(), 1, 0, 0, 0, 0, c.currentTime.Location())
+ var buttons []fyne.CanvasObject
+
+ //account for Go time pkg starting on sunday at index 0
+ dayIndex := int(start.Weekday())
+ if dayIndex == 0 {
+ dayIndex += daysPerWeek
+ }
+
+ //add spacers if week doesn't start on Monday
+ for i := 0; i < dayIndex-1; i++ {
+ buttons = append(buttons, layout.NewSpacer())
+ }
+
+ for d := start; d.Month() == start.Month(); d = d.AddDate(0, 0, 1) {
+ dayNum := d.Day()
+ s := strconv.Itoa(dayNum)
+ b := NewButton(s, func() {
+ selectedDate := c.dateForButton(dayNum)
+
+ c.OnChanged(selectedDate)
+ })
+ b.Importance = LowImportance
+
+ buttons = append(buttons, b)
+ }
+
+ return buttons
+}
+
+func (c *Calendar) monthYear() string {
+ return c.currentTime.Format("January 2006")
+}
+
+type calendarLayout struct {
+ cellSize fyne.Size
+}
+
+func newCalendarLayout() fyne.Layout {
+ return &calendarLayout{}
+}
+
+// Layout is called to pack all child objects into a specified size.
+// For a calendar grid this will pack objects into a table format.
+func (g *calendarLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
+ weeks := 1
+ day := 0
+ for i, child := range objects {
+ if !child.Visible() {
+ continue
+ }
+
+ if day%daysPerWeek == 0 && i >= daysPerWeek {
+ weeks++
+ }
+ day++
+ }
+
+ g.cellSize = fyne.NewSize(size.Width/float32(daysPerWeek),
+ size.Height/float32(weeks))
+ row, col := 0, 0
+ i := 0
+ for _, child := range objects {
+ if !child.Visible() {
+ continue
+ }
+
+ lead := g.getLeading(row, col)
+ trail := g.getTrailing(row, col)
+ child.Move(lead)
+ child.Resize(fyne.NewSize(trail.X, trail.Y).Subtract(lead))
+
+ if (i+1)%daysPerWeek == 0 {
+ row++
+ col = 0
+ } else {
+ col++
+ }
+ i++
+ }
+}
+
+// MinSize sets the minimum size for the calendar
+func (g *calendarLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
+ pad := theme.Padding()
+ largestMin := minCellContent.MinSize()
+ return fyne.NewSize(largestMin.Width*daysPerWeek+pad*(daysPerWeek-1),
+ largestMin.Height*maxWeeksPerMonth+pad*(maxWeeksPerMonth-1))
+}
+
+// Get the leading edge position of a grid cell.
+// The row and col specify where the cell is in the calendar.
+func (g *calendarLayout) getLeading(row, col int) fyne.Position {
+ x := (g.cellSize.Width) * float32(col)
+ y := (g.cellSize.Height) * float32(row)
+
+ return fyne.NewPos(float32(math.Round(float64(x))), float32(math.Round(float64(y))))
+}
+
+// Get the trailing edge position of a grid cell.
+// The row and col specify where the cell is in the calendar.
+func (g *calendarLayout) getTrailing(row, col int) fyne.Position {
+ return g.getLeading(row+1, col+1)
+}
+
+func shortDayName(in string) string {
+ lower := strings.ToLower(in)
+ key := lower + ".short"
+ long := lang.X(lower, in)
+ return strings.ToUpper(lang.X(key, long[:3]))
+}
diff --git a/widget/calendar_test.go b/widget/calendar_test.go
new file mode 100644
index 0000000000..68203aacf8
--- /dev/null
+++ b/widget/calendar_test.go
@@ -0,0 +1,94 @@
+package widget
+
+import (
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/test"
+)
+
+func TestNewCalendar(t *testing.T) {
+ now := time.Now()
+ c := NewCalendar(now, func(time.Time) {})
+ assert.Equal(t, now.Day(), c.currentTime.Day())
+ assert.Equal(t, int(now.Month()), int(c.currentTime.Month()))
+ assert.Equal(t, now.Year(), c.currentTime.Year())
+
+ _ = test.WidgetRenderer(c) // and render
+ assert.Equal(t, now.Format("January 2006"), c.monthLabel.Text)
+}
+
+func TestNewCalendar_ButtonDate(t *testing.T) {
+ date := time.Now()
+ c := NewCalendar(date, func(time.Time) {})
+ _ = test.WidgetRenderer(c) // and render
+
+ endNextMonth := date.AddDate(0, 1, 0).AddDate(0, 0, -(date.Day() - 1))
+ last := endNextMonth.AddDate(0, 0, -1)
+
+ firstDate := firstDateButton(c.dates)
+ assert.Equal(t, "1", firstDate.Text)
+ lastDate := c.dates.Objects[len(c.dates.Objects)-1].(*Button)
+ assert.Equal(t, strconv.Itoa(last.Day()), lastDate.Text)
+}
+
+func TestNewCalendar_Next(t *testing.T) {
+ date := time.Now()
+ c := NewCalendar(date, func(time.Time) {})
+ _ = test.WidgetRenderer(c) // and render
+
+ assert.Equal(t, date.Format("January 2006"), c.monthLabel.Text)
+
+ test.Tap(c.monthNext)
+ date = date.AddDate(0, 1, 0)
+ assert.Equal(t, date.Format("January 2006"), c.monthLabel.Text)
+}
+
+func TestNewCalendar_Previous(t *testing.T) {
+ date := time.Now()
+ c := NewCalendar(date, func(time.Time) {})
+ _ = test.WidgetRenderer(c) // and render
+
+ assert.Equal(t, date.Format("January 2006"), c.monthLabel.Text)
+
+ test.Tap(c.monthPrevious)
+ date = date.AddDate(0, -1, 0)
+ assert.Equal(t, date.Format("January 2006"), c.monthLabel.Text)
+}
+
+func TestNewCalendar_Resize(t *testing.T) {
+ date := time.Now()
+ c := NewCalendar(date, func(time.Time) {})
+ r := test.WidgetRenderer(c) // and render
+ layout := c.dates.Layout.(*calendarLayout)
+
+ baseSize := c.MinSize()
+ r.Layout(baseSize)
+ min := layout.cellSize
+
+ r.Layout(baseSize.AddWidthHeight(100, 0))
+ assert.Greater(t, layout.cellSize.Width, min.Width)
+ assert.Equal(t, layout.cellSize.Height, min.Height)
+
+ r.Layout(baseSize.AddWidthHeight(0, 100))
+ assert.Equal(t, layout.cellSize.Width, min.Width)
+ assert.Greater(t, layout.cellSize.Height, min.Height)
+
+ r.Layout(baseSize.AddWidthHeight(100, 100))
+ assert.Greater(t, layout.cellSize.Width, min.Width)
+ assert.Greater(t, layout.cellSize.Height, min.Height)
+}
+
+func firstDateButton(c *fyne.Container) *Button {
+ for _, b := range c.Objects {
+ if nonBlank, ok := b.(*Button); ok {
+ return nonBlank
+ }
+ }
+
+ return nil
+}
diff --git a/widget/date_entry.go b/widget/date_entry.go
new file mode 100644
index 0000000000..4bb415b235
--- /dev/null
+++ b/widget/date_entry.go
@@ -0,0 +1,166 @@
+package widget
+
+import (
+ "time"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/theme"
+)
+
+const dateFormat = "02 Jan 2006"
+
+// DateEntry is an input field which supports selecting from a fixed set of options.
+//
+// Since: 2.6
+type DateEntry struct {
+ Entry
+ Date *time.Time
+ OnChanged func(*time.Time) `json:"-"`
+
+ dropDown *Calendar
+ popUp *PopUp
+}
+
+// NewDateEntry creates a date input where the date can be selected or typed.
+//
+// Since: 2.6
+func NewDateEntry() *DateEntry {
+ e := &DateEntry{}
+ e.ExtendBaseWidget(e)
+ e.Wrapping = fyne.TextWrap(fyne.TextTruncateClip)
+ return e
+}
+
+// CreateRenderer returns a new renderer for this select entry.
+//
+// Implements: fyne.Widget
+func (e *DateEntry) CreateRenderer() fyne.WidgetRenderer {
+ e.ExtendBaseWidget(e)
+ e.Validator = func(in string) error {
+ _, err := time.Parse(dateFormat, in)
+ return err
+ }
+ e.Entry.OnChanged = func(in string) {
+ if in == "" {
+ e.Date = nil
+
+ if f := e.OnChanged; f != nil {
+ f(nil)
+ }
+ }
+ t, err := time.Parse(dateFormat, in)
+ if err != nil {
+ return
+ }
+
+ e.Date = &t
+
+ if f := e.OnChanged; f != nil {
+ f(&t)
+ }
+ }
+
+ if e.ActionItem == nil {
+ e.ActionItem = e.setupDropDown()
+ if e.Disabled() {
+ e.ActionItem.(fyne.Disableable).Disable()
+ }
+ }
+
+ return e.Entry.CreateRenderer()
+}
+
+// Enable this widget, updating any style or features appropriately.
+//
+// Implements: fyne.DisableableWidget
+func (e *DateEntry) Enable() {
+ if e.ActionItem != nil {
+ if d, ok := e.ActionItem.(fyne.Disableable); ok {
+ d.Enable()
+ }
+ }
+ e.Entry.Enable()
+}
+
+// Disable this widget so that it cannot be interacted with, updating any style appropriately.
+//
+// Implements: fyne.DisableableWidget
+func (e *DateEntry) Disable() {
+ if e.ActionItem != nil {
+ if d, ok := e.ActionItem.(fyne.Disableable); ok {
+ d.Disable()
+ }
+ }
+ e.Entry.Disable()
+}
+
+// MinSize returns the minimal size of the select entry.
+//
+// Implements: fyne.Widget
+func (e *DateEntry) MinSize() fyne.Size {
+ e.ExtendBaseWidget(e)
+ return e.Entry.MinSize()
+}
+
+// Move changes the relative position of the date entry.
+//
+// Implements: fyne.Widget
+func (e *DateEntry) Move(pos fyne.Position) {
+ e.Entry.Move(pos)
+ if e.popUp != nil {
+ e.popUp.Move(e.popUpPos())
+ }
+}
+
+// Resize changes the size of the date entry.
+//
+// Implements: fyne.Widget
+func (e *DateEntry) Resize(size fyne.Size) {
+ e.Entry.Resize(size)
+ if e.popUp != nil {
+ e.popUp.Resize(fyne.NewSize(size.Width, e.popUp.Size().Height))
+ }
+}
+
+// SetDate will update the widget to a specific date.
+// You can pass nil to unselect a date.
+func (e *DateEntry) SetDate(d *time.Time) {
+ if d == nil {
+ e.Date = nil
+ e.Entry.SetText("")
+
+ return
+ }
+
+ e.setDate(*d)
+}
+
+func (e *DateEntry) popUpPos() fyne.Position {
+ entryPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(e.super())
+ return entryPos.Add(fyne.NewPos(0, e.Size().Height-e.Theme().Size(theme.SizeNameInputBorder)))
+}
+
+func (e *DateEntry) setDate(d time.Time) {
+ e.Date = &d
+ if e.popUp != nil {
+ e.popUp.Hide()
+ }
+
+ e.Entry.SetText(d.Format(dateFormat))
+}
+
+func (e *DateEntry) setupDropDown() *Button {
+ if e.dropDown == nil {
+ e.dropDown = NewCalendar(time.Now(), e.setDate)
+ }
+ dropDownButton := NewButton("", func() {
+ c := fyne.CurrentApp().Driver().CanvasForObject(e.super())
+
+ e.popUp = NewPopUp(e.dropDown, c)
+ e.popUp.ShowAtPosition(e.popUpPos())
+ e.popUp.Resize(fyne.NewSize(e.Size().Width, e.popUp.MinSize().Height))
+ })
+ dropDownButton.Importance = LowImportance
+ dropDownButton.SetIcon(e.Theme().Icon(theme.IconNameCalendar))
+ return dropDownButton
+}
diff --git a/widget/entry.go b/widget/entry.go
index 5e4ea55fdc..1d0fd93f3d 100644
--- a/widget/entry.go
+++ b/widget/entry.go
@@ -1122,6 +1122,7 @@ func (e *Entry) pasteFromClipboard(clipboard fyne.Clipboard) {
cb := e.OnChanged
e.propertyLock.Unlock()
+ e.validate()
if cb != nil {
cb(content) // We know that the text has changed.
}
@@ -1665,14 +1666,14 @@ func (r *entryRenderer) trailingInset() float32 {
th := r.entry.Theme()
xInset := float32(0)
- iconSpace := th.Size(theme.SizeNameInlineIcon) + th.Size(theme.SizeNameLineSpacing)
if r.entry.ActionItem != nil {
- xInset = iconSpace
+ xInset = r.entry.ActionItem.MinSize().Width
}
if r.entry.Validator != nil {
+ iconSpace := th.Size(theme.SizeNameInlineIcon) + th.Size(theme.SizeNameLineSpacing)
if r.entry.ActionItem == nil {
- xInset = iconSpace
+ xInset = iconSpace + th.Size(theme.SizeNameInnerPadding)
} else {
xInset += iconSpace
}
@@ -1687,7 +1688,6 @@ func (r *entryRenderer) Layout(size fyne.Size) {
iconSize := th.Size(theme.SizeNameInlineIcon)
innerPad := th.Size(theme.SizeNameInnerPadding)
inputBorder := th.Size(theme.SizeNameInputBorder)
- lineSpace := th.Size(theme.SizeNameLineSpacing)
// 0.5 is removed so on low DPI it rounds down on the trailing edge
r.border.Resize(fyne.NewSize(size.Width-borderSize-.5, size.Height-borderSize-.5))
@@ -1696,12 +1696,12 @@ func (r *entryRenderer) Layout(size fyne.Size) {
r.box.Resize(size.Subtract(fyne.NewSquareSize(borderSize * 2)))
r.box.Move(fyne.NewSquareOffsetPos(borderSize))
- actionIconSize := fyne.NewSize(0, 0)
+ pad := theme.InputBorderSize()
+ actionIconSize := fyne.NewSize(0, size.Height-pad*2)
if r.entry.ActionItem != nil {
- actionIconSize = fyne.NewSquareSize(iconSize)
-
+ actionIconSize.Width = r.entry.ActionItem.MinSize().Width
r.entry.ActionItem.Resize(actionIconSize)
- r.entry.ActionItem.Move(fyne.NewPos(size.Width-actionIconSize.Width-innerPad, innerPad))
+ r.entry.ActionItem.Move(fyne.NewPos(size.Width-actionIconSize.Width-pad, pad))
}
validatorIconSize := fyne.NewSize(0, 0)
@@ -1714,7 +1714,7 @@ func (r *entryRenderer) Layout(size fyne.Size) {
if r.entry.ActionItem == nil {
r.entry.validationStatus.Move(fyne.NewPos(size.Width-validatorIconSize.Width-innerPad, innerPad))
} else {
- r.entry.validationStatus.Move(fyne.NewPos(size.Width-validatorIconSize.Width-actionIconSize.Width-innerPad-lineSpace, innerPad))
+ r.entry.validationStatus.Move(fyne.NewPos(size.Width-validatorIconSize.Width-actionIconSize.Width, innerPad))
}
}
diff --git a/widget/entry_internal_test.go b/widget/entry_internal_test.go
index 8af9f2f617..3982741138 100644
--- a/widget/entry_internal_test.go
+++ b/widget/entry_internal_test.go
@@ -389,6 +389,28 @@ func TestEntry_PasteFromClipboard_MultilineWrapping(t *testing.T) {
assert.Equal(t, 7, entry.CursorColumn)
}
+func TestEntry_PasteFromClipboardValidation(t *testing.T) {
+ entry := NewEntry()
+ var triggered int
+ entry.Validator = func(s string) error {
+ triggered++
+ return nil
+ }
+
+ w := test.NewApp().NewWindow("")
+ defer w.Close()
+ w.SetContent(entry)
+
+ testContent := "test"
+
+ clipboard := fyne.CurrentApp().Driver().AllWindows()[0].Clipboard()
+ clipboard.SetContent(testContent)
+
+ entry.pasteFromClipboard(clipboard)
+
+ assert.Equal(t, 2, triggered)
+}
+
func TestEntry_PlaceholderTextStyle(t *testing.T) {
e := NewEntry()
e.TextStyle = fyne.TextStyle{Bold: true, Italic: true}
diff --git a/widget/entry_password.go b/widget/entry_password.go
index ff8f75d4c3..40573a0eef 100644
--- a/widget/entry_password.go
+++ b/widget/entry_password.go
@@ -67,7 +67,7 @@ func (r *passwordRevealerRenderer) Layout(size fyne.Size) {
func (r *passwordRevealerRenderer) MinSize() fyne.Size {
iconSize := r.entry.Theme().Size(theme.SizeNameInlineIcon)
- return fyne.NewSquareSize(iconSize)
+ return fyne.NewSquareSize(iconSize + r.entry.Theme().Size(theme.SizeNameInnerPadding)*2)
}
func (r *passwordRevealerRenderer) Refresh() {
diff --git a/widget/entry_test.go b/widget/entry_test.go
index 58daa5656b..1b11b34b93 100644
--- a/widget/entry_test.go
+++ b/widget/entry_test.go
@@ -1823,8 +1823,9 @@ func TestPasswordEntry_ActionItemSizeAndPlacement(t *testing.T) {
b.Icon = theme.CancelIcon()
e.ActionItem = b
test.TempWidgetRenderer(t, e).Layout(e.MinSize())
- assert.Equal(t, fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize()), b.Size())
- assert.Equal(t, fyne.NewPos(e.MinSize().Width-2*theme.Padding()-b.Size().Width, 2*theme.Padding()), b.Position())
+ assert.Equal(t, theme.IconInlineSize()+theme.InnerPadding()*2, b.Size().Width)
+ assert.Greater(t, b.Size().Height, theme.IconInlineSize())
+ assert.Equal(t, fyne.NewPos(e.MinSize().Width-theme.InputBorderSize()-b.Size().Width, theme.InputBorderSize()), b.Position())
}
func TestPasswordEntry_Disabled(t *testing.T) {
diff --git a/widget/gridwrap.go b/widget/gridwrap.go
index a164ccf735..e7999040c5 100644
--- a/widget/gridwrap.go
+++ b/widget/gridwrap.go
@@ -10,6 +10,7 @@ import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/driver/desktop"
+ "fyne.io/fyne/v2/internal/async"
"fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/theme"
)
@@ -509,15 +510,15 @@ type gridItemAndID struct {
type gridWrapLayout struct {
list *GridWrap
- itemPool syncPool
- slicePool sync.Pool // *[]itemAndID
+ itemPool async.Pool[fyne.CanvasObject]
+ slicePool async.Pool[*[]gridItemAndID]
visible []gridItemAndID
renderLock sync.Mutex
}
func newGridWrapLayout(list *GridWrap) fyne.Layout {
l := &gridWrapLayout{list: list}
- l.slicePool.New = func() any {
+ l.slicePool.New = func() *[]gridItemAndID {
s := make([]gridItemAndID, 0)
return &s
}
@@ -534,7 +535,7 @@ func (l *gridWrapLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
}
func (l *gridWrapLayout) getItem() *gridWrapItem {
- item := l.itemPool.Obtain()
+ item := l.itemPool.Get()
if item == nil {
if f := l.list.CreateItem; f != nil {
child := createItemAndApplyThemeScope(f, l.list)
@@ -628,7 +629,7 @@ func (l *gridWrapLayout) updateGrid(refresh bool) {
// Keep pointer reference for copying slice header when returning to the pool
// https://blog.mike.norgate.xyz/unlocking-go-slice-performance-navigating-sync-pool-for-enhanced-efficiency-7cb63b0b453e
- wasVisiblePtr := l.slicePool.Get().(*[]gridItemAndID)
+ wasVisiblePtr := l.slicePool.Get()
wasVisible := (*wasVisiblePtr)[:0]
wasVisible = append(wasVisible, l.visible...)
@@ -669,13 +670,13 @@ func (l *gridWrapLayout) updateGrid(refresh bool) {
for _, old := range wasVisible {
if _, ok := l.searchVisible(l.visible, old.id); !ok {
- l.itemPool.Release(old.item)
+ l.itemPool.Put(old.item)
}
}
// make a local deep copy of l.visible since rest of this function is unlocked
// and cannot safely access l.visible
- visiblePtr := l.slicePool.Get().(*[]gridItemAndID)
+ visiblePtr := l.slicePool.Get()
visible := (*visiblePtr)[:0]
visible = append(visible, l.visible...)
l.renderLock.Unlock() // user code should not be locked
diff --git a/widget/hyperlink_test.go b/widget/hyperlink_test.go
index a86a5c56fd..164d51db63 100644
--- a/widget/hyperlink_test.go
+++ b/widget/hyperlink_test.go
@@ -204,7 +204,7 @@ func TestHyperlink_Truncate(t *testing.T) {
hyperlink.Truncation = fyne.TextTruncateEllipsis
hyperlink.Refresh()
texts = richTextRenderTexts(&hyperlink.provider)
- assert.Equal(t, "TestingWi…", texts[0].Text)
+ assert.Equal(t, "TestingWit…", texts[0].Text)
}
func TestHyperlink_CreateRendererDoesNotAffectSize(t *testing.T) {
diff --git a/widget/list.go b/widget/list.go
index f48a45a039..d640e13cb7 100644
--- a/widget/list.go
+++ b/widget/list.go
@@ -10,6 +10,7 @@ import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/driver/desktop"
+ "fyne.io/fyne/v2/internal/async"
"fyne.io/fyne/v2/internal/cache"
"fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/theme"
@@ -611,16 +612,16 @@ type listLayout struct {
separators []fyne.CanvasObject
children []fyne.CanvasObject
- itemPool syncPool
+ itemPool async.Pool[fyne.CanvasObject]
visible []listItemAndID
- slicePool sync.Pool // *[]itemAndID
+ slicePool async.Pool[*[]listItemAndID]
visibleRowHeights []float32
renderLock sync.RWMutex
}
func newListLayout(list *List) fyne.Layout {
l := &listLayout{list: list}
- l.slicePool.New = func() any {
+ l.slicePool.New = func() *[]listItemAndID {
s := make([]listItemAndID, 0)
return &s
}
@@ -637,7 +638,7 @@ func (l *listLayout) MinSize([]fyne.CanvasObject) fyne.Size {
}
func (l *listLayout) getItem() *listItem {
- item := l.itemPool.Obtain()
+ item := l.itemPool.Get()
if item == nil {
if f := l.list.CreateItem; f != nil {
item2 := createItemAndApplyThemeScope(f, l.list)
@@ -704,7 +705,7 @@ func (l *listLayout) updateList(newOnly bool) {
// Keep pointer reference for copying slice header when returning to the pool
// https://blog.mike.norgate.xyz/unlocking-go-slice-performance-navigating-sync-pool-for-enhanced-efficiency-7cb63b0b453e
- wasVisiblePtr := l.slicePool.Get().(*[]listItemAndID)
+ wasVisiblePtr := l.slicePool.Get()
wasVisible := (*wasVisiblePtr)[:0]
wasVisible = append(wasVisible, l.visible...)
@@ -747,7 +748,7 @@ func (l *listLayout) updateList(newOnly bool) {
for _, wasVis := range wasVisible {
if _, ok := l.searchVisible(l.visible, wasVis.id); !ok {
- l.itemPool.Release(wasVis.item)
+ l.itemPool.Put(wasVis.item)
}
}
@@ -762,7 +763,7 @@ func (l *listLayout) updateList(newOnly bool) {
// make a local deep copy of l.visible since rest of this function is unlocked
// and cannot safely access l.visible
- visiblePtr := l.slicePool.Get().(*[]listItemAndID)
+ visiblePtr := l.slicePool.Get()
visible := (*visiblePtr)[:0]
visible = append(visible, l.visible...)
l.renderLock.Unlock() // user code should not be locked
diff --git a/widget/pool.go b/widget/pool.go
deleted file mode 100644
index 19f1cb5db6..0000000000
--- a/widget/pool.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package widget
-
-import (
- "sync"
-
- "fyne.io/fyne/v2"
-)
-
-type pool interface {
- Obtain() fyne.CanvasObject
- Release(fyne.CanvasObject)
-}
-
-var _ pool = (*syncPool)(nil)
-
-type syncPool struct {
- sync.Pool
-}
-
-// Obtain returns an item from the pool for use
-func (p *syncPool) Obtain() (item fyne.CanvasObject) {
- o := p.Get()
- if o != nil {
- item = o.(fyne.CanvasObject)
- }
- return
-}
-
-// Release adds an item into the pool to be used later
-func (p *syncPool) Release(item fyne.CanvasObject) {
- p.Put(item)
-}
diff --git a/widget/pool_test.go b/widget/pool_test.go
deleted file mode 100644
index f6032a6238..0000000000
--- a/widget/pool_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package widget
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- "fyne.io/fyne/v2/canvas"
- "fyne.io/fyne/v2/theme"
-)
-
-func TestSyncPool(t *testing.T) {
- t.Run("Empty", func(t *testing.T) {
- pool := &syncPool{}
- assert.Nil(t, pool.Obtain())
- })
- t.Run("Single", func(t *testing.T) {
- pool := &syncPool{}
- rect := canvas.NewRectangle(theme.Color(theme.ColorNamePrimary))
- pool.Release(rect)
- assert.Equal(t, rect, pool.Obtain())
- assert.Nil(t, pool.Obtain())
- })
- t.Run("Multiple", func(t *testing.T) {
- pool := &syncPool{}
- rect := canvas.NewRectangle(theme.Color(theme.ColorNamePrimary))
- circle := canvas.NewCircle(theme.Color(theme.ColorNamePrimary))
- pool.Release(rect)
- pool.Release(circle)
- a := pool.Obtain()
- b := pool.Obtain()
- assert.NotNil(t, a)
- assert.NotNil(t, b)
- if a == rect && b == circle {
- // Pass
- } else if a == circle && b == rect {
- // Pass
- } else {
- t.Error("Obtained incorrect objects")
- }
- assert.Nil(t, pool.Obtain())
- })
-}
diff --git a/widget/richtext.go b/widget/richtext.go
index 5bf8d9e39a..8faea43846 100644
--- a/widget/richtext.go
+++ b/widget/richtext.go
@@ -1120,10 +1120,11 @@ func truncateLimit(s string, text *canvas.Text, limit int, ellipsis []rune) (int
RunStart: 0,
RunEnd: len(ellipsis),
Direction: di.DirectionLTR,
- Face: face.Fonts.ResolveFace('…'),
+ Face: face.Fonts.ResolveFace(ellipsis[0]),
Size: float32ToFixed266(text.TextSize),
}
shaper := &shaping.HarfbuzzShaper{}
+ segmenter := &shaping.Segmenter{}
conf := shaping.WrapConfig{}
conf = conf.WithTruncator(shaper, in)
@@ -1133,12 +1134,19 @@ func truncateLimit(s string, text *canvas.Text, limit int, ellipsis []rune) (int
in.Text = runes
in.RunEnd = len(runes)
- out := shaper.Shape(in)
+ ins := segmenter.Split(in, face.Fonts)
+ outs := make([]shaping.Output, len(ins))
+ for i, in := range ins {
+ outs[i] = shaper.Shape(in)
+ }
- l.Prepare(conf, runes, shaping.NewSliceIterator([]shaping.Output{out}))
+ l.Prepare(conf, runes, shaping.NewSliceIterator(outs))
wrapped, done := l.WrapNextLine(limit)
- count := wrapped.Line[0].Runes.Count
+ count := 0
+ for _, run := range wrapped.Line {
+ count += run.Runes.Count
+ }
full := done && count == len(runes)
if !full && len(ellipsis) > 0 {
count--
diff --git a/widget/richtext_test.go b/widget/richtext_test.go
index 82229686a8..f207c31dc5 100644
--- a/widget/richtext_test.go
+++ b/widget/richtext_test.go
@@ -575,7 +575,7 @@ func TestText_lineBounds(t *testing.T) {
text: "foobar foobar",
trunc: fyne.TextTruncateEllipsis,
want: [][2]int{
- {0, 8},
+ {0, 9},
},
ellipses: 1,
},
@@ -688,8 +688,8 @@ func TestText_lineBounds(t *testing.T) {
trunc: fyne.TextTruncateEllipsis,
want: [][2]int{
{0, 6},
- {7, 15},
- {28, 36},
+ {7, 16},
+ {28, 37},
},
ellipses: 2,
},
@@ -757,8 +757,8 @@ func TestText_lineBounds(t *testing.T) {
trunc: fyne.TextTruncateEllipsis,
want: [][2]int{
{0, 6},
- {7, 14},
- {26, 33},
+ {7, 15},
+ {26, 34},
{39, 39},
},
ellipses: 2,
@@ -909,8 +909,8 @@ func TestText_lineBounds(t *testing.T) {
trunc: fyne.TextTruncateEllipsis,
want: [][2]int{
{0, 6},
- {7, 15},
- {28, 36},
+ {7, 16},
+ {28, 37},
{42, 42},
},
ellipses: 2,
@@ -1041,7 +1041,7 @@ func TestText_lineBounds_variable_char_width(t *testing.T) {
text: "iiiiiiiiiimmmmmmmmmm",
trunc: fyne.TextTruncateEllipsis,
want: [][2]int{
- {0, 8},
+ {0, 9},
},
},
{
diff --git a/widget/select.go b/widget/select.go
index c1ac6114a4..3e776c05d3 100644
--- a/widget/select.go
+++ b/widget/select.go
@@ -1,10 +1,12 @@
package widget
import (
+ "fmt"
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
)
@@ -24,6 +26,8 @@ type Select struct {
PlaceHolder string
OnChanged func(string) `json:"-"`
+ binder basicBinder
+
focused bool
hovered bool
popUp *PopUpMenu
@@ -47,6 +51,30 @@ func NewSelect(options []string, changed func(string)) *Select {
return s
}
+// NewSelectWithData returns a new select widget connected to the specified data source.
+//
+// Since: 2.6
+func NewSelectWithData(options []string, data binding.String) *Select {
+ sel := NewSelect(options, nil)
+ sel.Bind(data)
+
+ return sel
+}
+
+// Bind connects the specified data source to this select.
+// The current value will be displayed and any changes in the data will cause the widget
+// to update.
+//
+// Since: 2.6
+func (s *Select) Bind(data binding.String) {
+ s.binder.SetCallback(s.updateFromData)
+ s.binder.Bind(data)
+
+ s.OnChanged = func(_ string) {
+ s.binder.CallWithData(s.writeData)
+ }
+}
+
// ClearSelected clears the current option of the select widget. After
// clearing the current option, the Select widget's PlaceHolder will
// be displayed.
@@ -239,6 +267,15 @@ func (s *Select) TypedRune(_ rune) {
// intentionally left blank
}
+// Unbind disconnects any configured data source from this Select.
+// The current value will remain at the last value of the data source.
+//
+// Since: 2.6
+func (s *Select) Unbind() {
+ s.OnChanged = nil
+ s.binder.Unbind()
+}
+
func (s *Select) popUpPos() fyne.Position {
buttonPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(s.super())
return buttonPos.Add(fyne.NewPos(0, s.Size().Height-s.Theme().Size(theme.SizeNameInputBorder)))
@@ -279,6 +316,23 @@ func (s *Select) tapAnimation() {
}
}
+func (s *Select) updateFromData(data binding.DataItem) {
+ if data == nil {
+ return
+ }
+ stringSource, ok := data.(binding.String)
+ if !ok {
+ return
+ }
+
+ val, err := stringSource.Get()
+ if err != nil {
+ return
+ }
+ s.SetSelected(val)
+
+}
+
func (s *Select) updateSelected(text string) {
s.Selected = text
@@ -289,6 +343,26 @@ func (s *Select) updateSelected(text string) {
s.Refresh()
}
+func (s *Select) writeData(data binding.DataItem) {
+ if data == nil {
+ return
+ }
+ stringTarget, ok := data.(binding.String)
+ if !ok {
+ return
+ }
+ currentValue, err := stringTarget.Get()
+ if err != nil {
+ return
+ }
+ if currentValue != s.Selected {
+ err := stringTarget.Set(s.Selected)
+ if err != nil {
+ fyne.LogError(fmt.Sprintf("Failed to set binding value to %s", s.Selected), err)
+ }
+ }
+}
+
type selectRenderer struct {
icon *Icon
label *RichText
diff --git a/widget/select_test.go b/widget/select_test.go
index 9503b36448..aeb525ca37 100644
--- a/widget/select_test.go
+++ b/widget/select_test.go
@@ -8,6 +8,7 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/internal/cache"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/test"
@@ -25,6 +26,19 @@ func TestNewSelect(t *testing.T) {
assert.Equal(t, "", combo.Selected)
}
+func TestNewSelectWithData(t *testing.T) {
+ data := binding.NewString()
+ combo := widget.NewSelectWithData([]string{"1", "2", "3"}, data)
+
+ assert.Equal(t, 3, len(combo.Options))
+ assert.Equal(t, "", combo.Selected)
+
+ err := data.Set("2")
+ assert.Nil(t, err)
+ waitForBinding()
+ assert.Equal(t, "2", combo.Selected)
+}
+
func TestSelect_Align(t *testing.T) {
test.NewTempApp(t)
@@ -43,6 +57,52 @@ func TestSelect_Align(t *testing.T) {
assertRendersToPlatformMarkup(t, "select/%s/trailing.xml", c)
}
+func TestSelect_Options(t *testing.T) {
+ s := widget.NewSelect([]string{"1", "2", "3"}, nil)
+ s.SetSelected("2")
+ assert.Equal(t, "2", s.Selected)
+
+ s.SetOptions([]string{"4", "5"})
+ assert.Equal(t, "2", s.Selected)
+ s.Selected = ""
+ assert.Equal(t, "", s.Selected)
+}
+
+func TestSelect_Binding(t *testing.T) {
+ s := widget.NewSelect([]string{"1", "2", "3"}, nil)
+ s.SetSelected("2")
+ assert.Equal(t, "2", s.Selected)
+ waitForBinding() // this time it is the de-echo before binding
+
+ str := binding.NewString()
+ s.Bind(str)
+ waitForBinding()
+ value, err := str.Get()
+ assert.Nil(t, err)
+ assert.Equal(t, "", value)
+ assert.Equal(t, "2", s.Selected) // no match to options, so keep previous value
+
+ err = str.Set("3")
+ assert.Nil(t, err)
+ waitForBinding()
+ assert.Equal(t, "3", s.Selected)
+
+ s.Unbind()
+ assert.Nil(t, s.OnChanged)
+ err = str.Set("1")
+ assert.Nil(t, err)
+ val1, err := str.Get()
+ assert.Nil(t, err)
+ assert.Equal(t, "1", val1)
+ assert.Equal(t, "3", s.Selected)
+
+ s.SetSelected("2")
+ val1, err = str.Get()
+ assert.Nil(t, err)
+ assert.Equal(t, "1", val1)
+ assert.Equal(t, "2", s.Selected)
+}
+
func TestSelect_ChangeTheme(t *testing.T) {
test.NewTempApp(t)
@@ -100,7 +160,7 @@ func TestSelect_ClipValue(t *testing.T) {
r2 := cache.Renderer(text)
assert.Equal(t, 1, len(r2.Objects()))
- assert.Equal(t, "some…", r2.Objects()[0].(*canvas.Text).Text)
+ assert.Equal(t, "some …", r2.Objects()[0].(*canvas.Text).Text)
}
func TestSelect_Disable(t *testing.T) {
diff --git a/widget/table.go b/widget/table.go
index 5f566f045c..1eba170fcf 100644
--- a/widget/table.go
+++ b/widget/table.go
@@ -8,6 +8,7 @@ import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/driver/mobile"
+ "fyne.io/fyne/v2/internal/async"
"fyne.io/fyne/v2/internal/cache"
"fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/theme"
@@ -1176,7 +1177,7 @@ func (c *tableCells) CreateRenderer() fyne.WidgetRenderer {
hover := canvas.NewRectangle(th.Color(theme.ColorNameHover, v))
hover.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
- r := &tableCellsRenderer{cells: c, pool: &syncPool{}, headerPool: &syncPool{},
+ r := &tableCellsRenderer{cells: c,
visible: make(map[TableCellID]fyne.CanvasObject), headers: make(map[TableCellID]fyne.CanvasObject),
headRowBG: canvas.NewRectangle(th.Color(theme.ColorNameHeaderBackground, v)), headColBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)),
headRowStickyBG: canvas.NewRectangle(th.Color(theme.ColorNameHeaderBackground, v)), headColStickyBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)),
@@ -1198,7 +1199,7 @@ type tableCellsRenderer struct {
widget.BaseRenderer
cells *tableCells
- pool, headerPool pool
+ pool, headerPool async.Pool[fyne.CanvasObject]
visible, headers map[TableCellID]fyne.CanvasObject
hover, marker *canvas.Rectangle
dividers []fyne.CanvasObject
@@ -1315,7 +1316,7 @@ func (r *tableCellsRenderer) refreshForID(toDraw TableCellID) {
colWidth := visibleColWidths[col]
c, ok := wasVisible[id]
if !ok {
- c = r.pool.Obtain()
+ c = r.pool.Get()
if f := r.cells.t.CreateCell; f != nil && c == nil {
c = createItemAndApplyThemeScope(f, r.cells.t)
}
@@ -1385,7 +1386,7 @@ func (r *tableCellsRenderer) refreshForID(toDraw TableCellID) {
for id, old := range wasVisible {
if _, ok := r.visible[id]; !ok {
- r.pool.Release(old)
+ r.pool.Put(old)
}
}
visible := r.visible
@@ -1618,7 +1619,7 @@ func (r *tableCellsRenderer) refreshHeaders(visibleRowHeights, visibleColWidths
colWidth := visibleColWidths[col]
c, ok := wasVisible[id]
if !ok {
- c = r.headerPool.Obtain()
+ c = r.headerPool.Get()
if c == nil {
c = r.cells.t.createHeader()
}
@@ -1660,7 +1661,7 @@ func (r *tableCellsRenderer) refreshHeaders(visibleRowHeights, visibleColWidths
rowHeight := visibleRowHeights[row]
c, ok := wasVisible[id]
if !ok {
- c = r.headerPool.Obtain()
+ c = r.headerPool.Get()
if c == nil {
c = r.cells.t.createHeader()
}
@@ -1712,7 +1713,7 @@ func (r *tableCellsRenderer) refreshHeaders(visibleRowHeights, visibleColWidths
for id, old := range wasVisible {
if _, ok := r.headers[id]; !ok {
- r.headerPool.Release(old)
+ r.headerPool.Put(old)
}
}
return cells
diff --git a/widget/testdata/entry/validate_invalid.xml b/widget/testdata/entry/validate_invalid.xml
index 5ca122275c..d771edad9c 100644
--- a/widget/testdata/entry/validate_invalid.xml
+++ b/widget/testdata/entry/validate_invalid.xml
@@ -3,8 +3,8 @@
-
-
+
+ 2020-02
diff --git a/widget/testdata/entry/validate_valid.xml b/widget/testdata/entry/validate_valid.xml
index ecf591962d..c0057bb51b 100644
--- a/widget/testdata/entry/validate_valid.xml
+++ b/widget/testdata/entry/validate_valid.xml
@@ -3,8 +3,8 @@
-
-
+
+ 2020-02-12
diff --git a/widget/testdata/entry/validator_not_empty_focused.xml b/widget/testdata/entry/validator_not_empty_focused.xml
index 5c13984e7a..26a861ab85 100644
--- a/widget/testdata/entry/validator_not_empty_focused.xml
+++ b/widget/testdata/entry/validator_not_empty_focused.xml
@@ -3,12 +3,12 @@
-
-
-
+
+
+
-
+
diff --git a/widget/testdata/entry/validator_not_empty_initial.xml b/widget/testdata/entry/validator_not_empty_initial.xml
index c6d9c799d6..14021e45c7 100644
--- a/widget/testdata/entry/validator_not_empty_initial.xml
+++ b/widget/testdata/entry/validator_not_empty_initial.xml
@@ -3,12 +3,12 @@
-
-
-
+
+
+
-
+
diff --git a/widget/testdata/entry/validator_not_empty_unfocused.xml b/widget/testdata/entry/validator_not_empty_unfocused.xml
index 0ae0f2e3e4..fee8281b68 100644
--- a/widget/testdata/entry/validator_not_empty_unfocused.xml
+++ b/widget/testdata/entry/validator_not_empty_unfocused.xml
@@ -3,12 +3,12 @@
-
-
-
+
+
+
-
+
diff --git a/widget/testdata/form/extended_entry.xml b/widget/testdata/form/extended_entry.xml
index cd6ebedbad..5372cf8369 100644
--- a/widget/testdata/form/extended_entry.xml
+++ b/widget/testdata/form/extended_entry.xml
@@ -9,20 +9,20 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/password_entry/concealed.xml b/widget/testdata/password_entry/concealed.xml
index d940f0ce1c..3872274852 100644
--- a/widget/testdata/password_entry/concealed.xml
+++ b/widget/testdata/password_entry/concealed.xml
@@ -3,16 +3,16 @@
-
-
-
+
+
+ ••••••
-
-
+
+
diff --git a/widget/testdata/password_entry/initial.xml b/widget/testdata/password_entry/initial.xml
index e90fea5221..23a7ce1316 100644
--- a/widget/testdata/password_entry/initial.xml
+++ b/widget/testdata/password_entry/initial.xml
@@ -3,18 +3,18 @@
-
-
-
+
+
+
-
+
-
-
+
+
diff --git a/widget/testdata/password_entry/obfuscation_typed.xml b/widget/testdata/password_entry/obfuscation_typed.xml
index c50cf399d4..fcb28fdf45 100644
--- a/widget/testdata/password_entry/obfuscation_typed.xml
+++ b/widget/testdata/password_entry/obfuscation_typed.xml
@@ -3,16 +3,16 @@
-
-
-
+
+
+ •••••••
-
-
+
+
diff --git a/widget/testdata/password_entry/placeholder_initial.xml b/widget/testdata/password_entry/placeholder_initial.xml
index 788a91c1bd..15b2eecc45 100644
--- a/widget/testdata/password_entry/placeholder_initial.xml
+++ b/widget/testdata/password_entry/placeholder_initial.xml
@@ -3,18 +3,18 @@
-
-
-
+
+
+ Password
-
+
-
-
+
+
diff --git a/widget/testdata/password_entry/placeholder_typed.xml b/widget/testdata/password_entry/placeholder_typed.xml
index c50cf399d4..fcb28fdf45 100644
--- a/widget/testdata/password_entry/placeholder_typed.xml
+++ b/widget/testdata/password_entry/placeholder_typed.xml
@@ -3,16 +3,16 @@
-
-
-
+
+
+ •••••••
-
-
+
+
diff --git a/widget/testdata/password_entry/revealed.xml b/widget/testdata/password_entry/revealed.xml
index e72e2674f7..8faa2e18c3 100644
--- a/widget/testdata/password_entry/revealed.xml
+++ b/widget/testdata/password_entry/revealed.xml
@@ -3,16 +3,16 @@
-
-
-
+
+
+ Secret
-
-
+
+
diff --git a/widget/testdata/select_entry/disableable_disabled.xml b/widget/testdata/select_entry/disableable_disabled.xml
index fb1040ca2a..d88b786b90 100644
--- a/widget/testdata/select_entry/disableable_disabled.xml
+++ b/widget/testdata/select_entry/disableable_disabled.xml
@@ -3,20 +3,20 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/disableable_enabled.xml b/widget/testdata/select_entry/disableable_enabled.xml
index 8c21e77a02..7f971cfbfb 100644
--- a/widget/testdata/select_entry/disableable_enabled.xml
+++ b/widget/testdata/select_entry/disableable_enabled.xml
@@ -3,20 +3,20 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/disableable_enabled_opened.xml b/widget/testdata/select_entry/disableable_enabled_opened.xml
index a406a38880..41f120fa2b 100644
--- a/widget/testdata/select_entry/disableable_enabled_opened.xml
+++ b/widget/testdata/select_entry/disableable_enabled_opened.xml
@@ -3,20 +3,20 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/disableable_enabled_tapped.xml b/widget/testdata/select_entry/disableable_enabled_tapped.xml
index 8c21e77a02..7f971cfbfb 100644
--- a/widget/testdata/select_entry/disableable_enabled_tapped.xml
+++ b/widget/testdata/select_entry/disableable_enabled_tapped.xml
@@ -3,20 +3,20 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/disableable_enabled_tapped_selected.xml b/widget/testdata/select_entry/disableable_enabled_tapped_selected.xml
index 8c21e77a02..7f971cfbfb 100644
--- a/widget/testdata/select_entry/disableable_enabled_tapped_selected.xml
+++ b/widget/testdata/select_entry/disableable_enabled_tapped_selected.xml
@@ -3,20 +3,20 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/dropdown_B_opened.xml b/widget/testdata/select_entry/dropdown_B_opened.xml
index 598666d4ce..773974921f 100644
--- a/widget/testdata/select_entry/dropdown_B_opened.xml
+++ b/widget/testdata/select_entry/dropdown_B_opened.xml
@@ -3,17 +3,17 @@
-
-
-
+
+
+ B
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/dropdown_empty_opened.xml b/widget/testdata/select_entry/dropdown_empty_opened.xml
index a406a38880..41f120fa2b 100644
--- a/widget/testdata/select_entry/dropdown_empty_opened.xml
+++ b/widget/testdata/select_entry/dropdown_empty_opened.xml
@@ -3,20 +3,20 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/dropdown_empty_opened_shrunk.xml b/widget/testdata/select_entry/dropdown_empty_opened_shrunk.xml
index 3e730ba386..afdab382ba 100644
--- a/widget/testdata/select_entry/dropdown_empty_opened_shrunk.xml
+++ b/widget/testdata/select_entry/dropdown_empty_opened_shrunk.xml
@@ -3,20 +3,20 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/dropdown_empty_setopts.xml b/widget/testdata/select_entry/dropdown_empty_setopts.xml
index 0dde8f3b42..98d9c3a686 100644
--- a/widget/testdata/select_entry/dropdown_empty_setopts.xml
+++ b/widget/testdata/select_entry/dropdown_empty_setopts.xml
@@ -3,20 +3,20 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/dropdown_initial.xml b/widget/testdata/select_entry/dropdown_initial.xml
index 8c21e77a02..7f971cfbfb 100644
--- a/widget/testdata/select_entry/dropdown_initial.xml
+++ b/widget/testdata/select_entry/dropdown_initial.xml
@@ -3,20 +3,20 @@
-
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/dropdown_tapped_B.xml b/widget/testdata/select_entry/dropdown_tapped_B.xml
index bdc0de4c2a..dda437d9e8 100644
--- a/widget/testdata/select_entry/dropdown_tapped_B.xml
+++ b/widget/testdata/select_entry/dropdown_tapped_B.xml
@@ -3,17 +3,17 @@
-
-
-
+
+
+ B
-
-
-
-
+
+
+
+
diff --git a/widget/testdata/select_entry/dropdown_tapped_C.xml b/widget/testdata/select_entry/dropdown_tapped_C.xml
index aa5687879d..5d212e3458 100644
--- a/widget/testdata/select_entry/dropdown_tapped_C.xml
+++ b/widget/testdata/select_entry/dropdown_tapped_C.xml
@@ -3,17 +3,17 @@
-
-
-
+
+
+ C
-
-
-
-
+
+
+
+
diff --git a/widget/textgrid.go b/widget/textgrid.go
index 48d62637fd..def67cd93f 100644
--- a/widget/textgrid.go
+++ b/widget/textgrid.go
@@ -289,7 +289,7 @@ func (t *TextGrid) SetStyleRange(startRow, startCol, endRow, endCol int, style T
}
}
-// CreateRenderer is a private method to Fyne which links this widget to it's renderer
+// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (t *TextGrid) CreateRenderer() fyne.WidgetRenderer {
t.ExtendBaseWidget(t)
render := &textGridRenderer{text: t}
diff --git a/widget/tree.go b/widget/tree.go
index db4ff58d3f..34fa3ebcb6 100644
--- a/widget/tree.go
+++ b/widget/tree.go
@@ -7,6 +7,7 @@ import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/driver/desktop"
+ "fyne.io/fyne/v2/internal/async"
"fyne.io/fyne/v2/internal/cache"
"fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/theme"
@@ -543,6 +544,7 @@ func (r *treeRenderer) MinSize() (min fyne.Size) {
func (r *treeRenderer) Layout(size fyne.Size) {
r.content.viewport = size
r.scroller.Resize(size)
+ r.tree.offsetUpdated(r.scroller.Offset)
}
func (r *treeRenderer) Refresh() {
@@ -590,8 +592,6 @@ func (c *treeContent) CreateRenderer() fyne.WidgetRenderer {
treeContent: c,
branches: make(map[string]*branch),
leaves: make(map[string]*leaf),
- branchPool: &syncPool{},
- leafPool: &syncPool{},
}
}
@@ -614,8 +614,8 @@ type treeContentRenderer struct {
objects []fyne.CanvasObject
branches map[string]*branch
leaves map[string]*leaf
- branchPool pool
- leafPool pool
+ branchPool async.Pool[fyne.CanvasObject]
+ leafPool async.Pool[fyne.CanvasObject]
}
func (r *treeContentRenderer) Layout(size fyne.Size) {
@@ -727,12 +727,12 @@ func (r *treeContentRenderer) Layout(size fyne.Size) {
// Release any nodes that haven't been reused
for uid, b := range r.branches {
if _, ok := branches[uid]; !ok {
- r.branchPool.Release(b)
+ r.branchPool.Put(b)
}
}
for uid, l := range r.leaves {
if _, ok := leaves[uid]; !ok {
- r.leafPool.Release(l)
+ r.leafPool.Put(l)
}
}
@@ -808,7 +808,7 @@ func (r *treeContentRenderer) refreshForID(toDraw TreeNodeID) {
}
func (r *treeContentRenderer) getBranch() (b *branch) {
- o := r.branchPool.Obtain()
+ o := r.branchPool.Get()
if o != nil {
b = o.(*branch)
} else {
@@ -822,7 +822,7 @@ func (r *treeContentRenderer) getBranch() (b *branch) {
}
func (r *treeContentRenderer) getLeaf() (l *leaf) {
- o := r.leafPool.Obtain()
+ o := r.leafPool.Get()
if o != nil {
l = o.(*leaf)
} else {
diff --git a/widget/widget.go b/widget/widget.go
index 28a32cd5dd..63b8555633 100644
--- a/widget/widget.go
+++ b/widget/widget.go
@@ -122,7 +122,7 @@ func (w *BaseWidget) Hide() {
canvas.Refresh(impl)
}
-// Refresh causes this widget to be redrawn in it's current state
+// Refresh causes this widget to be redrawn in its current state
func (w *BaseWidget) Refresh() {
impl := w.super()
if impl == nil {
diff --git a/window.go b/window.go
index 3e366acb96..e47bf778dc 100644
--- a/window.go
+++ b/window.go
@@ -65,7 +65,7 @@ type Window interface {
SetOnClosed(func())
// SetCloseIntercept sets a function that runs instead of closing if defined.
- // Close() should be called explicitly in the interceptor to close the window.
+ // [Window.Close] should be called explicitly in the interceptor to close the window.
//
// Since: 1.4
SetCloseIntercept(func())