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 \n\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())